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.
736 lines
27 KiB
736 lines
27 KiB
|
3 weeks ago
|
|
||
|
|
//*********************
|
||
|
|
// create by 流云
|
||
|
|
// time: Sat Sep 05 2026
|
||
|
|
// desc: 战斗内点击组合字武将 — 详情弹窗
|
||
|
|
//*********************
|
||
|
|
import {
|
||
|
|
_decorator,
|
||
|
|
Color,
|
||
|
|
EventTouch,
|
||
|
|
instantiate,
|
||
|
|
Label,
|
||
|
|
Node,
|
||
|
|
Prefab,
|
||
|
|
sp,
|
||
|
|
Sprite,
|
||
|
|
Tween,
|
||
|
|
UITransform,
|
||
|
|
UIOpacity,
|
||
|
|
UIRenderer,
|
||
|
|
Vec3,
|
||
|
|
} from 'cc';
|
||
|
|
import { type IPopAnimConfig, type IPopAnimSession } from '../../../mx/module/ui/popAnim';
|
||
|
|
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
|
||
|
|
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 { getTowerExpHost, needTowerExpForLevel } from '../../battle/combat/TowerExpHost';
|
||
|
|
import { resolveHeroBattleWeaponId } from '../../battle/combat/TowerLoadoutService';
|
||
|
|
import { HeroSkin } from '../../battle/view/HeroSkin';
|
||
|
|
import { Skill } from '../../battle/skill/Skill';
|
||
|
|
import { heroUltSkillId } from '../../battle/config/BattleHeroTable';
|
||
|
|
import { applyWeaponSpineSkin } from '../../battle/view/GlyphSpineSkin';
|
||
|
|
import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback';
|
||
|
|
import { GBundle, GPath } from '../../game/ConfigRes';
|
||
|
|
import { TableNames } from '../../game/ConfigTableData';
|
||
|
|
import type { Player } from '../../game/Player';
|
||
|
|
const { ccclass } = _decorator;
|
||
|
|
|
||
|
|
const LOG = '[UIBattleWeaponDescHero]';
|
||
|
|
/** 品质 → 武器名字体色 */
|
||
|
|
const WEAPON_NAME_FONT_HEX: Record<number, string> = {
|
||
|
|
0: '#D8D8D8',
|
||
|
|
1: '#6FA86F',
|
||
|
|
2: '#6FA2A8',
|
||
|
|
3: '#B390D9',
|
||
|
|
4: '#F9C948',
|
||
|
|
};
|
||
|
|
/** 品质 → 武器名描边色 */
|
||
|
|
const WEAPON_NAME_OUTLINE_HEX: Record<number, string> = {
|
||
|
|
0: '#4A4A4A',
|
||
|
|
1: '#386c42',
|
||
|
|
2: '#285c6d',
|
||
|
|
3: '#623073',
|
||
|
|
4: '#734930',
|
||
|
|
};
|
||
|
|
|
||
|
|
/** 组合武将详情弹窗入参 */
|
||
|
|
export interface BattleWeaponDescHeroPanelData {
|
||
|
|
entity: TileEntitySnapshot;
|
||
|
|
runtime?: TowerRuntimeSnapshot | null;
|
||
|
|
/** 敌方阵营皮肤(组合字 皮肤字2_*) */
|
||
|
|
enemyCamp?: boolean;
|
||
|
|
/** 已解析的展示武器 id(敌方优先 AI 档案) */
|
||
|
|
weaponId?: number;
|
||
|
|
weaponWorldPos?: { x: number; y: number; z?: number };
|
||
|
|
gridStep?: number;
|
||
|
|
trySwitchAtUiPos?: (uiPos: { x: number; y: number }) => boolean;
|
||
|
|
/** 监视中的地图/背包格(切换选中格时随 prema 更新) */
|
||
|
|
watchTarget?: { kind: 'map' | 'bag'; index: number } | null;
|
||
|
|
/** 已部署 ECS 实体(地图格实时刷新 fallback) */
|
||
|
|
watchEntityId?: EntityId;
|
||
|
|
/** 从棋盘重新读取 entity/runtime(击杀加经验后刷新数值) */
|
||
|
|
resolveLiveData?: () => Pick<BattleWeaponDescHeroPanelData, 'entity' | 'runtime' | 'weaponId'> | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 经验条满宽(像素) */
|
||
|
|
const PROGRESS_MAX_WIDTH = 225;
|
||
|
|
const WEAPON_SPINE_IDLE = '待机';
|
||
|
|
|
||
|
|
@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 _heroNode: Node | null = null;
|
||
|
|
/** 当前面板已解析的展示武器(defer 复用,避免二次解析失败把 ui_type 隐藏) */
|
||
|
|
private _displayWeaponRow: ITableWeapon | null = null;
|
||
|
|
/** 静态区(武将/技能/武器名等)缓存键;切换仅刷新数值区时不重建 */
|
||
|
|
private _staticViewKey = '';
|
||
|
|
/** 战斗事件监听是否已绑定 */
|
||
|
|
private _battleEventBound = false;
|
||
|
|
/** GEvent 监听是否已绑定 */
|
||
|
|
private _geventBound = false;
|
||
|
|
|
||
|
|
onLoad(): void {
|
||
|
|
super.onLoad();
|
||
|
|
this._popupBody = this.node.getChildByName('弹窗主体');
|
||
|
|
}
|
||
|
|
|
||
|
|
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._deferRefreshWeaponType);
|
||
|
|
this.unschedule(this._deferredRefreshDynamic);
|
||
|
|
this._staticViewKey = '';
|
||
|
|
this._clearHeroNode();
|
||
|
|
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();
|
||
|
|
|
||
|
|
const ctx = this._resolveRefreshContext();
|
||
|
|
if (!ctx) {
|
||
|
|
this._staticViewKey = '';
|
||
|
|
this._clearLabels();
|
||
|
|
this._clearHeroNode();
|
||
|
|
this._hideWeaponTypes();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const { data, w1, level, levelLimit, stats, weaponRow, runtime, enemyCamp } = ctx;
|
||
|
|
const staticViewKey = this._buildStaticViewKey(w1.id, weaponRow?.id ?? 0, enemyCamp);
|
||
|
|
const needStaticRefresh = staticViewKey !== this._staticViewKey || !this._heroNode?.isValid;
|
||
|
|
|
||
|
|
if (needStaticRefresh) {
|
||
|
|
this._staticViewKey = staticViewKey;
|
||
|
|
this._displayWeaponRow = weaponRow;
|
||
|
|
this._refreshHero(w1, level, enemyCamp);
|
||
|
|
this._refreshStaticLabels(w1, level, levelLimit, weaponRow);
|
||
|
|
this._refreshWeaponType(weaponRow);
|
||
|
|
this._scheduleRefreshWeaponType(weaponRow);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._refreshDynamicStats(w1, level, levelLimit, stats, weaponRow, runtime);
|
||
|
|
this._applyPanelPosition(data);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 合并 prema 与 resolveLiveData,得到最新等级/属性 */
|
||
|
|
private _resolveRefreshContext(): {
|
||
|
|
data: BattleWeaponDescHeroPanelData;
|
||
|
|
w1: ITableHero;
|
||
|
|
level: number;
|
||
|
|
levelLimit: number;
|
||
|
|
stats: TowerRuntimeSnapshot['stats'] | undefined;
|
||
|
|
weaponRow: ITableWeapon | null;
|
||
|
|
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;
|
||
|
|
|
||
|
|
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 levelLimit = data.entity.tower?.levelLimit ?? w1.level_limit ?? 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;
|
||
|
|
const weaponRow = this._resolveWeaponRow(w1.id, runtime, enemyCamp, data.weaponId ?? 0);
|
||
|
|
|
||
|
|
return { data, w1, level, levelLimit, stats, weaponRow, runtime, enemyCamp };
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 击杀/升级后仅刷新 lb_level 等数值区 */
|
||
|
|
private _refreshDynamicFromLive(): void {
|
||
|
|
if (!this.node?.isValid || !this.node.active) return;
|
||
|
|
const ctx = this._resolveRefreshContext();
|
||
|
|
if (!ctx) return;
|
||
|
|
const { w1, level, levelLimit, stats, weaponRow, runtime, enemyCamp } = ctx;
|
||
|
|
const staticViewKey = this._buildStaticViewKey(w1.id, weaponRow?.id ?? 0, enemyCamp);
|
||
|
|
if (staticViewKey !== this._staticViewKey) {
|
||
|
|
this.onInit();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this._refreshDynamicStats(w1, level, levelLimit, stats, weaponRow, runtime);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 入场动效结束后再刷一次武器 Spine(避免首帧节点未就绪) */
|
||
|
|
protected onPopOpenRevealEnd(): void {
|
||
|
|
if (this._displayWeaponRow) {
|
||
|
|
this._refreshWeaponType(this._displayWeaponRow);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** ui_node:heroTemplate + HeroSkin 播待机 */
|
||
|
|
private _refreshHero(w1: ITableHero, level: number, enemyCamp = false): void {
|
||
|
|
if (!this.ui_node?.isValid) return;
|
||
|
|
this._clearHeroNode();
|
||
|
|
|
||
|
|
const prefab = gg.res.getRes<Prefab>('heroTemplate', GPath.Prefab, GBundle.Heros, Prefab);
|
||
|
|
if (!prefab) {
|
||
|
|
console.log('[UIBattleWeaponDescHero] heroTemplate 预制体缺失', w1.id);
|
||
|
|
this.ui_node.active = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const heroNode = instantiate(prefab);
|
||
|
|
heroNode.parent = this.ui_node;
|
||
|
|
heroNode.setPosition(0, 0, 0);
|
||
|
|
heroNode.setScale(1, 1, 1);
|
||
|
|
this._heroNode = heroNode;
|
||
|
|
this.ui_node.active = true;
|
||
|
|
|
||
|
|
const skill = heroNode.getComponent(Skill);
|
||
|
|
if (skill) skill.enabled = false;
|
||
|
|
|
||
|
|
const heroSkin = heroNode.getComponent(HeroSkin);
|
||
|
|
if (!heroSkin) {
|
||
|
|
console.log('[UIBattleWeaponDescHero] HeroSkin 缺失', w1.id);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
heroSkin.setQualityFrameVisible(false);
|
||
|
|
heroSkin.setEnemyCamp(enemyCamp);
|
||
|
|
heroSkin.setData(w1);
|
||
|
|
heroSkin.setSkinByLevelAndQuality(level, enemyCamp);
|
||
|
|
heroSkin.playIdleAnim();
|
||
|
|
}
|
||
|
|
|
||
|
|
private _clearHeroNode(): void {
|
||
|
|
if (this._heroNode?.isValid) {
|
||
|
|
const heroSkin = this._heroNode.getComponent(HeroSkin);
|
||
|
|
heroSkin?.releaseDisplay();
|
||
|
|
this._heroNode.destroy();
|
||
|
|
}
|
||
|
|
this._heroNode = null;
|
||
|
|
if (this.ui_node?.isValid) {
|
||
|
|
this.ui_node.removeAllChildren();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 可展示的武器配表行(Weapon 表且 type 1~4) */
|
||
|
|
private _tryDisplayWeaponRow(weaponId: number): ITableWeapon | null {
|
||
|
|
const id = Math.floor(Number(weaponId) || 0);
|
||
|
|
if (id <= 0) return null;
|
||
|
|
const row = gg.equip.getEquip(id)
|
||
|
|
?? gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, id);
|
||
|
|
if (!row) return null;
|
||
|
|
const type = Number(row.type) | 0;
|
||
|
|
if (type < 1 || type > 4) return null;
|
||
|
|
return row;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _resolveWeaponRow(
|
||
|
|
heroId: number,
|
||
|
|
runtime?: TowerRuntimeSnapshot | null,
|
||
|
|
enemyCamp = false,
|
||
|
|
presetWeaponId = 0,
|
||
|
|
): ITableWeapon | null {
|
||
|
|
const fromPreset = this._tryDisplayWeaponRow(presetWeaponId);
|
||
|
|
if (fromPreset) return fromPreset;
|
||
|
|
|
||
|
|
const loadout = runtime?.loadout;
|
||
|
|
if (loadout && loadout.weapon1Id === heroId) {
|
||
|
|
const fromRuntime = this._tryDisplayWeaponRow(loadout.weaponId ?? 0);
|
||
|
|
if (fromRuntime) return fromRuntime;
|
||
|
|
}
|
||
|
|
|
||
|
|
const fallbackId = enemyCamp
|
||
|
|
? resolveHeroBattleWeaponId(heroId, this._resolveEnemyPlayer())
|
||
|
|
: gg.equip.getHeroDisplayWeaponId(heroId);
|
||
|
|
const fromFallback = this._tryDisplayWeaponRow(fallbackId);
|
||
|
|
if (fromFallback) return fromFallback;
|
||
|
|
|
||
|
|
console.log(
|
||
|
|
LOG,
|
||
|
|
enemyCamp ? '敌方武器 id 缺失' : '武器 id 缺失',
|
||
|
|
heroId,
|
||
|
|
'preset=',
|
||
|
|
presetWeaponId,
|
||
|
|
'runtime=',
|
||
|
|
loadout?.weaponId ?? 0,
|
||
|
|
'fallback=',
|
||
|
|
fallbackId,
|
||
|
|
);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 弹窗入场后再刷一次武器 Spine(使用 onInit 缓存,避免二次解析失败清空 ui_type) */
|
||
|
|
private _deferRefreshWeaponType = (): void => {
|
||
|
|
if (!this.node?.isValid) return;
|
||
|
|
const weaponRow = this._displayWeaponRow;
|
||
|
|
if (!weaponRow) return;
|
||
|
|
this._refreshWeaponType(weaponRow);
|
||
|
|
};
|
||
|
|
|
||
|
|
/** 延后刷新武器 Spine(本帧末 + 下一帧各一次) */
|
||
|
|
private _scheduleRefreshWeaponType(weaponRow: ITableWeapon | null): void {
|
||
|
|
this.unschedule(this._deferRefreshWeaponType);
|
||
|
|
if (!weaponRow) return;
|
||
|
|
this.scheduleOnce(this._deferRefreshWeaponType, 0);
|
||
|
|
this.scheduleOnce(this._deferRefreshWeaponType, 0.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 敌方 Player(与 TowerLoadout 解析同源) */
|
||
|
|
private _resolveEnemyPlayer(): Player | null {
|
||
|
|
const battle = gg.game.CurentBattle;
|
||
|
|
if (!battle) return null;
|
||
|
|
const localPid = battle.player?.playerId ?? 0;
|
||
|
|
return battle.getPlayer(localPid === 0 ? 1 : 0) ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 静态区缓存键:同武将+武器+阵营切换时仅刷新数值 */
|
||
|
|
private _buildStaticViewKey(heroId: number, weaponId: number, enemyCamp: boolean): string {
|
||
|
|
return `${Math.floor(Number(heroId) || 0)}|${Math.floor(Number(weaponId) || 0)}|${enemyCamp ? 1 : 0}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 武将名/满级/技能/武器文案与 Spine(打开或切换武将时刷新) */
|
||
|
|
private _refreshStaticLabels(
|
||
|
|
w1: ITableHero,
|
||
|
|
level: number,
|
||
|
|
levelLimit: number,
|
||
|
|
weaponRow: ITableWeapon | null,
|
||
|
|
): 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);
|
||
|
|
|
||
|
|
if (!weaponRow) {
|
||
|
|
if (this.lb_weaponName) {
|
||
|
|
this.lb_weaponName.string = '';
|
||
|
|
this._resetWeaponNameStyle();
|
||
|
|
}
|
||
|
|
if (this.lb_weaponDesc) this.lb_weaponDesc.string = '';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.lb_weaponName) {
|
||
|
|
this.lb_weaponName.string = gg.lang.get(weaponRow.name);
|
||
|
|
this._applyWeaponNameStyle(weaponRow.quality);
|
||
|
|
}
|
||
|
|
if (this.lb_weaponDesc) {
|
||
|
|
this.lb_weaponDesc.string = gg.lang.get(weaponRow.desc);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 实时数值:等级、攻击、攻速、武器攻击、经验条(每次打开/切换格子都读最新 runtime)
|
||
|
|
*/
|
||
|
|
private _refreshDynamicStats(
|
||
|
|
w1: ITableHero,
|
||
|
|
level: number,
|
||
|
|
levelLimit: number,
|
||
|
|
stats: TowerRuntimeSnapshot['stats'] | undefined,
|
||
|
|
weaponRow: ITableWeapon | null,
|
||
|
|
runtime?: TowerRuntimeSnapshot | null,
|
||
|
|
): void {
|
||
|
|
const setup = deriveTowerSetupStats(w1, level, weaponRow?.id);
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
if (this.lb_weaponAtk) {
|
||
|
|
if (!weaponRow) {
|
||
|
|
this.lb_weaponAtk.string = '';
|
||
|
|
} else {
|
||
|
|
const exclusiveMul = gg.equip.getEquipExclusiveMul(w1.id, weaponRow);
|
||
|
|
const weaponAtk = Math.floor((weaponRow.atk ?? 0) * exclusiveMul);
|
||
|
|
this.lb_weaponAtk.string = `${weaponAtk}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this._refreshExpProgress(w1, level, levelLimit, runtime);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 刷新当前等级武将技能名与描述 */
|
||
|
|
private _refreshSkillLabels(w1: ITableHero, level: number): void {
|
||
|
|
const skillId = heroUltSkillId(w1, level);
|
||
|
|
const skill = skillId > 0
|
||
|
|
? gg.data.table.getTableData<ITableSkill>(TableNames.Skill, skillId)
|
||
|
|
: null;
|
||
|
|
if (skillId > 0 && !skill) {
|
||
|
|
console.log(LOG, 'Skill 缺失', skillId, w1.id, level);
|
||
|
|
}
|
||
|
|
if (this.lb_skillName) {
|
||
|
|
this.lb_skillName.string = skill ? gg.lang.get(skill.name) : '';
|
||
|
|
}
|
||
|
|
if (this.lb_skillDesc) {
|
||
|
|
this.lb_skillDesc.string = skill ? gg.lang.get(skill.desc) : '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 按武器品质设置武器名字体色与描边 */
|
||
|
|
private _applyWeaponNameStyle(quality: number): void {
|
||
|
|
const label = this.lb_weaponName;
|
||
|
|
if (!label) return;
|
||
|
|
const q = Math.floor(Number(quality) || 0);
|
||
|
|
const fontHex = WEAPON_NAME_FONT_HEX[q];
|
||
|
|
const outlineHex = WEAPON_NAME_OUTLINE_HEX[q];
|
||
|
|
if (!fontHex || !outlineHex) {
|
||
|
|
console.log(LOG, '未知武器品质色', quality);
|
||
|
|
this._resetWeaponNameStyle();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
label.color = Color.fromHEX(new Color(), fontHex);
|
||
|
|
label.enableOutline = true;
|
||
|
|
label.outlineColor = Color.fromHEX(new Color(), outlineHex);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 重置武器名样式 */
|
||
|
|
private _resetWeaponNameStyle(): void {
|
||
|
|
const label = this.lb_weaponName;
|
||
|
|
if (!label) return;
|
||
|
|
label.color = Color.WHITE;
|
||
|
|
label.enableOutline = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 从弹窗主体取 ui_type 节点(不依赖 Auto 缓存引用) */
|
||
|
|
private _getWeaponTypeNode(weaponType: number): Node | null {
|
||
|
|
const body = this._popupBody ?? this.node.getChildByName('弹窗主体');
|
||
|
|
if (!body?.isValid) return null;
|
||
|
|
const t = Math.floor(Number(weaponType) || 0);
|
||
|
|
if (t < 1 || t > 4) return null;
|
||
|
|
return body.getChildByName(`ui_type${t}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 同步 Auto 节点引用(供外部工具链;展示以 _getWeaponTypeNode 为准) */
|
||
|
|
private _ensureWeaponTypeNodes(): void {
|
||
|
|
if (!this.ui_type1?.isValid) this.ui_type1 = this._getWeaponTypeNode(1);
|
||
|
|
if (!this.ui_type2?.isValid) this.ui_type2 = this._getWeaponTypeNode(2);
|
||
|
|
if (!this.ui_type3?.isValid) this.ui_type3 = this._getWeaponTypeNode(3);
|
||
|
|
if (!this.ui_type4?.isValid) this.ui_type4 = this._getWeaponTypeNode(4);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** ui_type1~4:按武器 type 显隐并切肤 */
|
||
|
|
private _refreshWeaponType(weaponRow: ITableWeapon | null): void {
|
||
|
|
this._ensureWeaponTypeNodes();
|
||
|
|
this._hideWeaponTypes();
|
||
|
|
if (!weaponRow) return;
|
||
|
|
|
||
|
|
const weaponType = Number(weaponRow.type) | 0;
|
||
|
|
const skin = String(weaponRow.skin ?? '').trim();
|
||
|
|
if (weaponType < 1 || weaponType > 4) {
|
||
|
|
console.log(LOG, '武器 type 无效', weaponRow.id, weaponType);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!skin) {
|
||
|
|
console.log(LOG, '武器 skin 缺失', weaponRow.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
const node = this._getWeaponTypeNode(weaponType);
|
||
|
|
if (!node?.isValid) {
|
||
|
|
console.log(LOG, '武器类型节点缺失', weaponType, weaponRow.id);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
node.active = true;
|
||
|
|
node.setScale(1, 1, 1);
|
||
|
|
this._applyWeaponSpine(node, skin, weaponRow.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _hideWeaponTypes(): void {
|
||
|
|
for (let t = 1; t <= 4; t++) {
|
||
|
|
const node = this._getWeaponTypeNode(t);
|
||
|
|
if (node?.isValid) node.active = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _applyWeaponSpine(typeNode: Node, skin: string, weaponId: number): void {
|
||
|
|
typeNode.active = true;
|
||
|
|
const skel = typeNode.getComponent(sp.Skeleton) ?? typeNode.getComponentInChildren(sp.Skeleton);
|
||
|
|
if (!skel?.isValid) {
|
||
|
|
console.log(LOG, '武器 Spine 缺失', typeNode.name, weaponId);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (skel.node?.isValid) skel.node.active = true;
|
||
|
|
skel.enabled = true;
|
||
|
|
if (skel.skeletonData) ensureSpineSkeletonDataRuntime(skel.skeletonData);
|
||
|
|
|
||
|
|
const resolvedSkin = applyWeaponSpineSkin(skel, skin, LOG);
|
||
|
|
if (skin && !resolvedSkin) {
|
||
|
|
console.log(LOG, '武器 skin 不可用', weaponId, skin);
|
||
|
|
}
|
||
|
|
|
||
|
|
BattleSpinePlayback.resetTracks(skel);
|
||
|
|
try {
|
||
|
|
skel.setAnimation(0, WEAPON_SPINE_IDLE, true);
|
||
|
|
} catch (e) {
|
||
|
|
console.log(LOG, 'setAnimation 失败', WEAPON_SPINE_IDLE, typeNode.name, weaponId, String(e));
|
||
|
|
}
|
||
|
|
|
||
|
|
const renderers = typeNode.getComponentsInChildren(UIRenderer);
|
||
|
|
for (let i = 0; i < renderers.length; i++) {
|
||
|
|
const r = renderers[i];
|
||
|
|
if (r?.isValid) r.enabled = true;
|
||
|
|
}
|
||
|
|
const op = typeNode.getComponent(UIOpacity) ?? typeNode.getComponentInChildren(UIOpacity);
|
||
|
|
if (op?.isValid) op.opacity = 255;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _refreshExpProgress(
|
||
|
|
w1: ITableHero,
|
||
|
|
level: number,
|
||
|
|
levelLimit: number,
|
||
|
|
runtime?: TowerRuntimeSnapshot | null,
|
||
|
|
): void {
|
||
|
|
const progressUt = this.ui_progress?.getComponent(UITransform);
|
||
|
|
if (!progressUt?.isValid) return;
|
||
|
|
|
||
|
|
if (level >= levelLimit) {
|
||
|
|
progressUt.setContentSize(PROGRESS_MAX_WIDTH, progressUt.contentSize.height);
|
||
|
|
if (this.lb_progress) this.lb_progress.string = '满级';
|
||
|
|
if (this.ui_progress) this.ui_progress.active = true;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const need = needTowerExpForLevel(w1, level);
|
||
|
|
const curRaw = runtime?.entityId ? getTowerExpHost().getExp(runtime.entityId) : 0;
|
||
|
|
const ratio = need > 0 ? Math.min(1, Math.max(0, curRaw / need)) : 0;
|
||
|
|
progressUt.setContentSize(PROGRESS_MAX_WIDTH * ratio, progressUt.contentSize.height);
|
||
|
|
|
||
|
|
if (this.lb_progress) {
|
||
|
|
const curText = TowerStatDisplay.oneDecimal(curRaw);
|
||
|
|
const needText = TowerStatDisplay.oneDecimal(need);
|
||
|
|
this.lb_progress.string = need > 0 ? `${curText}/${needText}` : `${curText}/0`;
|
||
|
|
}
|
||
|
|
if (this.ui_progress) this.ui_progress.active = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
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_weaponName) {
|
||
|
|
this.lb_weaponName.string = '';
|
||
|
|
this._resetWeaponNameStyle();
|
||
|
|
}
|
||
|
|
if (this.lb_weaponAtk) this.lb_weaponAtk.string = '';
|
||
|
|
if (this.lb_weaponDesc) this.lb_weaponDesc.string = '';
|
||
|
|
if (this.lb_skillName) this.lb_skillName.string = '';
|
||
|
|
if (this.lb_skillDesc) this.lb_skillDesc.string = '';
|
||
|
|
if (this.lb_progress) this.lb_progress.string = '';
|
||
|
|
if (this.ui_progress?.isValid) {
|
||
|
|
const ut = this.ui_progress.getComponent(UITransform);
|
||
|
|
if (ut) ut.setContentSize(0, ut.contentSize.height);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|