|
|
|
|
|
|
|
|
|
//*********************
|
|
|
|
|
// create by 流云
|
|
|
|
|
// time: Sat Sep 05 2026
|
|
|
|
|
// desc: 战斗内点击 type=1 单字 — 详情弹窗
|
|
|
|
|
//*********************
|
|
|
|
|
import {
|
|
|
|
|
_decorator,
|
|
|
|
|
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 { GEvent } from '../../../mx/module/event/GEvent';
|
|
|
|
|
import { Auto_BattleWeaponDesc1 } from './Auto_BattleWeaponDesc1';
|
|
|
|
|
import type { TileEntitySnapshot, TowerRuntimeSnapshot } from '../../battle/combat/TowerQueryTypes';
|
|
|
|
|
import { TowerQueryService } from '../../battle/combat/TowerQueryService';
|
|
|
|
|
import { deriveTowerSetupStats, TowerStatDisplay } from '../../battle/combat/TowerStatResolver';
|
|
|
|
|
import { heroNormalSkillId, textIconName } from '../../battle/config/BattleHeroTable';
|
|
|
|
|
import { parseMonsterUnitDefId } from '../../battle/config/MonsterHpCalculator';
|
|
|
|
|
import { applyGlyphSpineSkin } from '../../battle/view/GlyphSpineSkin';
|
|
|
|
|
import { TextGrid } from '../../battle/view/TextGrid';
|
|
|
|
|
import { YING_WEAPON1_ID } from '../../battle/heros/HeroActorRes';
|
|
|
|
|
import { PEACH_TREE_WEAPON2_ID } from '../../battle/bag/BagPrepConfig';
|
|
|
|
|
import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager';
|
|
|
|
|
import { GBundle, GPath } from '../../game/ConfigRes';
|
|
|
|
|
import { TableNames } from '../../game/ConfigTableData';
|
|
|
|
|
import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
|
|
|
|
|
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
|
|
|
|
|
const { ccclass } = _decorator;
|
|
|
|
|
|
|
|
|
|
/** 单字详情弹窗入参 */
|
|
|
|
|
export interface BattleWeaponDesc1PanelData {
|
|
|
|
|
entity: TileEntitySnapshot;
|
|
|
|
|
runtime?: TowerRuntimeSnapshot | null;
|
|
|
|
|
/** 蟠桃树(weapon2Id=102) */
|
|
|
|
|
isPeachTree?: boolean;
|
|
|
|
|
/** 敌方阵营(预留;单字详情暂与己方同形) */
|
|
|
|
|
enemyCamp?: boolean;
|
|
|
|
|
weaponWorldPos?: { x: number; y: number; z?: number };
|
|
|
|
|
gridStep?: number;
|
|
|
|
|
trySwitchAtUiPos?: (uiPos: { x: number; y: number }) => boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const LOG = '[UIBattleWeaponDesc1]';
|
|
|
|
|
/** 图鉴内隐藏 textGrid 子节点 */
|
|
|
|
|
const GLYPH_HIDE_CHILDREN = ['lv', 'select', 'redSelect', 'canUpEff', 'trail1', 'trail2', 'trail3', 'spine'];
|
|
|
|
|
/** ui_node 内字形缩放 */
|
|
|
|
|
const GLYPH_SCALE = 0.85;
|
|
|
|
|
/** textGrid 蟠桃树表现节点 */
|
|
|
|
|
const PEACH_GLYPH_NODE = '桃';
|
|
|
|
|
/** textGrid 蟠桃树 Spine 节点 */
|
|
|
|
|
const PEACH_TREE_SPINE_NODE = '桃树';
|
|
|
|
|
/** 详情图鉴内蟠桃树待机 */
|
|
|
|
|
const PEACH_TREE_IDLE_ANIM = '桃字1';
|
|
|
|
|
|
|
|
|
|
@ccclass('UIBattleWeaponDesc1')
|
|
|
|
|
export class UIBattleWeaponDesc1 extends Auto_BattleWeaponDesc1 {
|
|
|
|
|
private static readonly PANEL_GAP_GRIDS = 1.7;
|
|
|
|
|
|
|
|
|
|
private _popupBody: Node | null = null;
|
|
|
|
|
/** 递增时取消尚未完成的关窗销毁 */
|
|
|
|
|
private _panelLifeGen = 0;
|
|
|
|
|
/** ui_node 下挂的字形节点 */
|
|
|
|
|
private _glyphNode: Node | null = null;
|
|
|
|
|
/** 普通字攻击数值区 */
|
|
|
|
|
private _uiAtkNode: Node | null = null;
|
|
|
|
|
/** 营字召唤信息区 */
|
|
|
|
|
private _uiCampNode: Node | null = null;
|
|
|
|
|
/** 蟠桃树信息区 */
|
|
|
|
|
private _uiPeachNode: Node | null = null;
|
|
|
|
|
private _lbMonsterName: Label | null = null;
|
|
|
|
|
private _lbSoldierSpeed: Label | null = null;
|
|
|
|
|
private _lbSkillDesc: Label | null = null;
|
|
|
|
|
/** 蟠桃树技能描述(ui_peachNode/lb_skillDesc) */
|
|
|
|
|
private _lbPeachSkillDesc: Label | null = null;
|
|
|
|
|
/** GEvent 监听是否已绑定 */
|
|
|
|
|
private _geventBound = false;
|
|
|
|
|
|
|
|
|
|
onLoad(): void {
|
|
|
|
|
super.onLoad();
|
|
|
|
|
this._popupBody = this.node.getChildByName('弹窗主体');
|
|
|
|
|
this._bindStatNodes();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定 ui_atkNode / ui_campNode 及子 Label(Auto 路径可能未随 prefab 更新) */
|
|
|
|
|
private _bindStatNodes(): void {
|
|
|
|
|
const body = this._popupBody ?? this.node.getChildByName('弹窗主体');
|
|
|
|
|
if (!body?.isValid) return;
|
|
|
|
|
|
|
|
|
|
this._uiAtkNode = body.getChildByName('ui_atkNode');
|
|
|
|
|
this._uiCampNode = body.getChildByName('ui_campNode');
|
|
|
|
|
|
|
|
|
|
const atk = this._uiAtkNode;
|
|
|
|
|
if (atk?.isValid) {
|
|
|
|
|
this.lb_attck = atk.getChildByName('lb_attck')?.getComponent(Label) ?? this.lb_attck;
|
|
|
|
|
this.lb_attckSpeed = atk.getChildByName('lb_attckSpeed')?.getComponent(Label) ?? this.lb_attckSpeed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const camp = this._uiCampNode;
|
|
|
|
|
if (camp?.isValid) {
|
|
|
|
|
this._lbMonsterName = camp.getChildByName('lb_monsterName')?.getComponent(Label) ?? null;
|
|
|
|
|
this._lbSoldierSpeed = camp.getChildByName('lb_soldierSpeed')?.getComponent(Label) ?? null;
|
|
|
|
|
this._lbSkillDesc = camp.getChildByName('lb_skillDesc')?.getComponent(Label) ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._uiPeachNode = body.getChildByName('ui_peachNode');
|
|
|
|
|
const peach = this._uiPeachNode ?? this.ui_peachNode;
|
|
|
|
|
if (peach?.isValid) {
|
|
|
|
|
this.ui_peachNode = peach;
|
|
|
|
|
this.lb_time = peach.getChildByName('lb_time')?.getComponent(Label) ?? this.lb_time;
|
|
|
|
|
this._lbPeachSkillDesc = peach.getChildByName('lb_peachSkillDesc')?.getComponent(Label)
|
|
|
|
|
?? this._lbPeachSkillDesc
|
|
|
|
|
?? null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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._clearGlyphNode();
|
|
|
|
|
super.onDisable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 订阅棋盘变更,实时刷新攻击力/攻速 */
|
|
|
|
|
private _bindLiveRefresh(): void {
|
|
|
|
|
if (this._geventBound) return;
|
|
|
|
|
GEvent.Ins.on(GEvent.UpdateBestPlace, this._onLiveDataDirty, this);
|
|
|
|
|
this._geventBound = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 取消实时刷新订阅 */
|
|
|
|
|
private _unbindLiveRefresh(): void {
|
|
|
|
|
if (!this._geventBound) return;
|
|
|
|
|
GEvent.Ins.off(GEvent.UpdateBestPlace, this._onLiveDataDirty, this);
|
|
|
|
|
this._geventBound = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 棋盘/攻速符变化后刷新攻击力/攻速 */
|
|
|
|
|
private _onLiveDataDirty = (): void => {
|
|
|
|
|
this.unschedule(this._deferredRefreshDynamic);
|
|
|
|
|
this.scheduleOnce(this._deferredRefreshDynamic, 0);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 帧末刷新,确保攻速符 modifier 写入后再读 */
|
|
|
|
|
private _deferredRefreshDynamic = (): void => {
|
|
|
|
|
this._refreshDynamicFromLive();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 仅刷新攻击力/攻速 */
|
|
|
|
|
private _refreshDynamicFromLive(): void {
|
|
|
|
|
if (!this.node?.isValid || !this.node.active) return;
|
|
|
|
|
const data = this.prema as BattleWeaponDesc1PanelData | null;
|
|
|
|
|
if (!data?.entity?.tile) return;
|
|
|
|
|
const tile = data.entity.tile;
|
|
|
|
|
if (this._isCampText(tile) || data.isPeachTree || this._isPeachText(tile)) return;
|
|
|
|
|
const w1 = this._resolveHero(data.entity);
|
|
|
|
|
const runtime = this._resolveLiveRuntime(data.runtime ?? null, data.entity);
|
|
|
|
|
data.runtime = runtime;
|
|
|
|
|
const stats = this._resolveLiveStats(runtime, data.entity);
|
|
|
|
|
const level = runtime?.level ?? data.entity.level;
|
|
|
|
|
this._refreshAtkLabels(w1, level, stats);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onInit(): void {
|
|
|
|
|
this._panelLifeGen++;
|
|
|
|
|
this._ensurePanelVisible();
|
|
|
|
|
this._bindLiveRefresh();
|
|
|
|
|
|
|
|
|
|
const data = this.prema as BattleWeaponDesc1PanelData | null;
|
|
|
|
|
if (!data?.entity?.tile) {
|
|
|
|
|
this._clearLabels();
|
|
|
|
|
this._clearGlyphNode();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._bindStatNodes();
|
|
|
|
|
|
|
|
|
|
const { entity } = data;
|
|
|
|
|
const tile = entity.tile;
|
|
|
|
|
const isCampText = this._isCampText(tile);
|
|
|
|
|
const isPeachText = !!data.isPeachTree || this._isPeachText(tile);
|
|
|
|
|
const w1 = this._resolveHero(entity);
|
|
|
|
|
const runtime = this._resolveLiveRuntime(data.runtime ?? null, entity);
|
|
|
|
|
data.runtime = runtime;
|
|
|
|
|
const stats = this._resolveLiveStats(runtime, entity);
|
|
|
|
|
const level = runtime?.level ?? entity.level;
|
|
|
|
|
const levelLimit = entity.tower?.levelLimit ?? w1?.level_limit ?? level;
|
|
|
|
|
|
|
|
|
|
this._refreshGlyph(tile, isPeachText);
|
|
|
|
|
this._refreshCommonLabels(w1, level, levelLimit);
|
|
|
|
|
this._refreshStatPanel(isCampText, isPeachText);
|
|
|
|
|
if (isCampText) {
|
|
|
|
|
this._refreshCampLabels(w1, level, !!data.enemyCamp);
|
|
|
|
|
} else if (isPeachText) {
|
|
|
|
|
this._refreshPeachLabels(w1, level);
|
|
|
|
|
} else {
|
|
|
|
|
this._refreshAtkLabels(w1, level, stats);
|
|
|
|
|
}
|
|
|
|
|
this._applyPanelPosition(data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 BattleWeaponDesc1PanelData | null)?.trySwitchAtUiPos;
|
|
|
|
|
if (uiPos && trySwitch?.(uiPos)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.close(2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** ui_node:textGrid 预制体播待机;蟠桃树隐藏 skin,播桃树 Spine */
|
|
|
|
|
private _refreshGlyph(tile: ITableText, isPeachText: boolean): void {
|
|
|
|
|
if (!this.ui_node?.isValid) return;
|
|
|
|
|
this._clearGlyphNode();
|
|
|
|
|
|
|
|
|
|
const prefab = gg.res.getRes<Prefab>('textGrid', GPath.Prefab, GBundle.BattleGame, Prefab);
|
|
|
|
|
if (!prefab) {
|
|
|
|
|
console.log('[UIBattleWeaponDesc1] textGrid 预制体缺失', tile.id);
|
|
|
|
|
this.ui_node.active = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const gridNode = instantiate(prefab);
|
|
|
|
|
gridNode.parent = this.ui_node;
|
|
|
|
|
gridNode.setPosition(0, 0, 0);
|
|
|
|
|
gridNode.setScale(GLYPH_SCALE, GLYPH_SCALE, 1);
|
|
|
|
|
this._glyphNode = gridNode;
|
|
|
|
|
this.ui_node.active = true;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < GLYPH_HIDE_CHILDREN.length; i++) {
|
|
|
|
|
const child = gridNode.getChildByName(GLYPH_HIDE_CHILDREN[i]);
|
|
|
|
|
if (child) child.active = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const textGrid = gridNode.getComponent(TextGrid);
|
|
|
|
|
if (textGrid) textGrid.enabled = false;
|
|
|
|
|
|
|
|
|
|
if (isPeachText) {
|
|
|
|
|
this._applyPeachGlyphPresentation(gridNode);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const iconName = textIconName(tile);
|
|
|
|
|
if (!iconName) {
|
|
|
|
|
console.log('[UIBattleWeaponDesc1] 字形名为空', tile.id);
|
|
|
|
|
this.ui_node.active = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const skinNode = gridNode.getChildByName('skin');
|
|
|
|
|
if (skinNode) skinNode.active = true;
|
|
|
|
|
const shovelNode = gridNode.getChildByName('铲');
|
|
|
|
|
if (shovelNode) shovelNode.active = false;
|
|
|
|
|
const peachNode = gridNode.getChildByName(PEACH_GLYPH_NODE);
|
|
|
|
|
if (peachNode) peachNode.active = false;
|
|
|
|
|
|
|
|
|
|
const sk = skinNode?.getComponent(sp.Skeleton) ?? skinNode?.getComponentInChildren(sp.Skeleton);
|
|
|
|
|
if (!sk?.isValid) {
|
|
|
|
|
console.log('[UIBattleWeaponDesc1] 字形 Spine 缺失', iconName);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
sk.enabled = true;
|
|
|
|
|
if (!applyGlyphSpineSkin(sk, iconName)) {
|
|
|
|
|
console.log('[UIBattleWeaponDesc1] 字形皮肤未能应用', iconName);
|
|
|
|
|
}
|
|
|
|
|
if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData);
|
|
|
|
|
if (sk.skeletonData?.getRuntimeData?.(true)?.findAnimation?.('待机')) {
|
|
|
|
|
sk.setAnimation(0, '待机', true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 蟠桃树图鉴:隐藏 skin/铲,显示「桃/桃树」Spine */
|
|
|
|
|
private _applyPeachGlyphPresentation(gridNode: Node): void {
|
|
|
|
|
const skinNode = gridNode.getChildByName('skin');
|
|
|
|
|
if (skinNode) skinNode.active = false;
|
|
|
|
|
const shovelNode = gridNode.getChildByName('铲');
|
|
|
|
|
if (shovelNode) shovelNode.active = false;
|
|
|
|
|
|
|
|
|
|
const peachNode = gridNode.getChildByName(PEACH_GLYPH_NODE);
|
|
|
|
|
if (!peachNode?.isValid) {
|
|
|
|
|
console.log(LOG, 'textGrid 桃节点缺失');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
peachNode.active = true;
|
|
|
|
|
|
|
|
|
|
const animNode = peachNode.getChildByName(PEACH_TREE_SPINE_NODE);
|
|
|
|
|
if (animNode?.isValid) animNode.active = true;
|
|
|
|
|
|
|
|
|
|
const sk = animNode?.getComponent(sp.Skeleton) ?? animNode?.getComponentInChildren(sp.Skeleton);
|
|
|
|
|
if (!sk?.isValid) {
|
|
|
|
|
console.log(LOG, '蟠桃树 Spine 缺失');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
sk.enabled = true;
|
|
|
|
|
if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData);
|
|
|
|
|
if (sk.skeletonData?.getRuntimeData?.(true)?.findAnimation?.(PEACH_TREE_IDLE_ANIM)) {
|
|
|
|
|
sk.setAnimation(0, PEACH_TREE_IDLE_ANIM, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _clearGlyphNode(): void {
|
|
|
|
|
if (this._glyphNode?.isValid) {
|
|
|
|
|
this._glyphNode.destroy();
|
|
|
|
|
}
|
|
|
|
|
this._glyphNode = null;
|
|
|
|
|
if (this.ui_node?.isValid) {
|
|
|
|
|
this.ui_node.removeAllChildren();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** type=1 营字(Text.id=5) */
|
|
|
|
|
private _isCampText(tile: ITableText): boolean {
|
|
|
|
|
return Number(tile.type) === 1 && Math.floor(Number(tile.id) || 0) === YING_WEAPON1_ID;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 蟠桃树字(Text.id=102) */
|
|
|
|
|
private _isPeachText(tile: ITableText): boolean {
|
|
|
|
|
return Math.floor(Number(tile.id) || 0) === PEACH_TREE_WEAPON2_ID;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 解析 Hero 行(部署快照缺失时按 weapon1Id / 营字 id 兜底) */
|
|
|
|
|
private _resolveHero(entity: TileEntitySnapshot): ITableHero | null {
|
|
|
|
|
const fromTower = entity.tower?.config ?? null;
|
|
|
|
|
if (fromTower) return fromTower;
|
|
|
|
|
const heroId = entity.weapon1Id > 0 ? entity.weapon1Id : 0;
|
|
|
|
|
if (heroId > 0) {
|
|
|
|
|
return gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId) ?? null;
|
|
|
|
|
}
|
|
|
|
|
if (this._isCampText(entity.tile)) {
|
|
|
|
|
return gg.data.table.getTableData<ITableHero>(TableNames.Hero, YING_WEAPON1_ID) ?? null;
|
|
|
|
|
}
|
|
|
|
|
if (this._isPeachText(entity.tile)) {
|
|
|
|
|
return gg.data.table.getTableData<ITableHero>(TableNames.Hero, PEACH_TREE_WEAPON2_ID) ?? null;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 切换普通攻击区 / 营召唤信息区 / 蟠桃树信息区 */
|
|
|
|
|
private _refreshStatPanel(isCampText: boolean, isPeachText: boolean): void {
|
|
|
|
|
if (this._uiAtkNode?.isValid) this._uiAtkNode.active = !isCampText && !isPeachText;
|
|
|
|
|
if (this._uiCampNode?.isValid) this._uiCampNode.active = isCampText;
|
|
|
|
|
const peachPanel = this._uiPeachNode ?? this.ui_peachNode;
|
|
|
|
|
if (peachPanel?.isValid) peachPanel.active = isPeachText;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _refreshCommonLabels(w1: ITableHero | null, level: number, levelLimit: number): void {
|
|
|
|
|
if (!w1) {
|
|
|
|
|
if (this.lb_name) this.lb_name.string = '';
|
|
|
|
|
if (this.lb_level) this.lb_level.string = '';
|
|
|
|
|
if (this.lb_lvMax) this.lb_lvMax.string = '';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.lb_name) {
|
|
|
|
|
this.lb_name.string = gg.lang.get(w1.name);
|
|
|
|
|
}
|
|
|
|
|
if (this.lb_level) {
|
|
|
|
|
this.lb_level.string = `Lv.${level}`;
|
|
|
|
|
}
|
|
|
|
|
if (this.lb_lvMax) {
|
|
|
|
|
this.lb_lvMax.string = `满级${levelLimit}级`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 普通字:攻击力 / 攻速(ui_atkNode) */
|
|
|
|
|
private _refreshAtkLabels(
|
|
|
|
|
w1: ITableHero | null,
|
|
|
|
|
level: number,
|
|
|
|
|
stats: TowerRuntimeSnapshot['stats'] | undefined,
|
|
|
|
|
): void {
|
|
|
|
|
if (!w1) {
|
|
|
|
|
if (this.lb_attck) this.lb_attck.string = '';
|
|
|
|
|
if (this.lb_attckSpeed) this.lb_attckSpeed.string = '';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const setup = deriveTowerSetupStats(w1, level);
|
|
|
|
|
const attack = stats?.attack ?? setup.attack;
|
|
|
|
|
const attackSpeed = stats?.attackSpeed ?? setup.attackSpeed;
|
|
|
|
|
|
|
|
|
|
if (this.lb_attck) {
|
|
|
|
|
this.lb_attck.string = TowerStatDisplay.oneDecimal(attack);
|
|
|
|
|
}
|
|
|
|
|
if (this.lb_attckSpeed) {
|
|
|
|
|
this.lb_attckSpeed.string = TowerStatDisplay.attackSpeed(attackSpeed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 营字:召唤怪昵称 / 出怪间隔 / 技能描述(ui_campNode) */
|
|
|
|
|
private _refreshCampLabels(w1: ITableHero | null, level: number, enemyCamp: boolean): void {
|
|
|
|
|
if (!w1) {
|
|
|
|
|
this._clearCampLabels();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const skillId = heroNormalSkillId(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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ownerPlayerId = this._resolveOwnerPlayerId(enemyCamp);
|
|
|
|
|
const monsterName = skill ? this._resolveSkillMonsterName(skill, ownerPlayerId, level) : '';
|
|
|
|
|
// 与描述「每 N 秒」及 YingCampSummonHost 一致:Skill.cd 为出兵间隔(秒)
|
|
|
|
|
const soldierIntervalSec = skill && Number(skill.cd) > 0 ? Number(skill.cd) : 0;
|
|
|
|
|
if (skill && !(soldierIntervalSec > 0)) {
|
|
|
|
|
console.log(LOG, '营出兵间隔无效', skill.id, skill.cd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._lbMonsterName) {
|
|
|
|
|
this._lbMonsterName.string = monsterName;
|
|
|
|
|
}
|
|
|
|
|
if (this._lbSoldierSpeed) {
|
|
|
|
|
this._lbSoldierSpeed.string = soldierIntervalSec > 0
|
|
|
|
|
? `${TowerStatDisplay.attackSpeed(soldierIntervalSec)}秒`
|
|
|
|
|
: '';
|
|
|
|
|
}
|
|
|
|
|
if (this._lbSkillDesc) {
|
|
|
|
|
this._lbSkillDesc.string = skill ? gg.lang.get(skill.desc) : '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 蟠桃树:产桃 CD / 技能描述(ui_peachNode) */
|
|
|
|
|
private _refreshPeachLabels(w1: ITableHero | null, level: number): void {
|
|
|
|
|
if (!w1) {
|
|
|
|
|
this._clearPeachLabels();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const skillId = heroNormalSkillId(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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cdSec = skill && Number(skill.cd) > 0 ? Number(skill.cd) : 0;
|
|
|
|
|
if (skill && !(cdSec > 0)) {
|
|
|
|
|
console.log(LOG, '蟠桃产桃 CD 无效', skill.id, skill.cd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.lb_time) {
|
|
|
|
|
this.lb_time.string = cdSec > 0 ? `${TowerStatDisplay.attackSpeed(cdSec)}秒` : '';
|
|
|
|
|
}
|
|
|
|
|
if (this._lbPeachSkillDesc) {
|
|
|
|
|
this._lbPeachSkillDesc.string = skill ? gg.lang.get(skill.desc) : '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 技能 atk_para 首段 → 怪物昵称(Monster.res 按营等级);-1 为本波小怪 */
|
|
|
|
|
private _resolveSkillMonsterName(skill: ITableSkill, ownerPlayerId: number, yingLevel: number): string {
|
|
|
|
|
const parts = String(skill.atk_para ?? '').split(',').map((s) => Number(String(s).trim()));
|
|
|
|
|
let monsterId = Math.floor(parts[0] || 0);
|
|
|
|
|
if (monsterId === -1) {
|
|
|
|
|
monsterId = this._resolveWaveSmallMonsterId(ownerPlayerId);
|
|
|
|
|
}
|
|
|
|
|
if (!(monsterId > 0)) {
|
|
|
|
|
console.log(LOG, '营召唤怪物 id 无效', skill.id, skill.atk_para);
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
const row = MonsterDataManager.getMonsterDataById(monsterId);
|
|
|
|
|
if (!row) {
|
|
|
|
|
console.log(LOG, 'Monster 缺失', monsterId, skill.id);
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
return MonsterDataManager.resolveYingSummonDisplayName(row, yingLevel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 当前波次小怪 Monster.id(与 YingCampSummonHost 同源) */
|
|
|
|
|
private _resolveWaveSmallMonsterId(ownerPlayerId: number): number {
|
|
|
|
|
const battle = gg.game.CurentBattle as ProjectBattleHandle | null;
|
|
|
|
|
const waves = battle?.session?.levelDef?.waves;
|
|
|
|
|
if (!waves?.length) {
|
|
|
|
|
console.log(LOG, '无波次配置,无法解析营召唤怪');
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let wi = battle!.session.spawner.waveIndex;
|
|
|
|
|
if (wi < 0) wi = 0;
|
|
|
|
|
if (wi >= waves.length) wi = waves.length - 1;
|
|
|
|
|
const wave = waves[wi];
|
|
|
|
|
const spawnPort = ownerPlayerId === 0 ? 2 : 1;
|
|
|
|
|
|
|
|
|
|
let portPlan = null as (typeof wave.ports)[number] | null;
|
|
|
|
|
for (let i = 0; i < (wave.ports?.length ?? 0); i++) {
|
|
|
|
|
if (wave.ports[i].port === spawnPort) {
|
|
|
|
|
portPlan = wave.ports[i];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!portPlan) portPlan = wave.ports?.[0] ?? null;
|
|
|
|
|
if (!portPlan?.segments?.length) {
|
|
|
|
|
console.log(LOG, '波次无出怪段', wi, spawnPort);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let s = 0; s < portPlan.segments.length; s++) {
|
|
|
|
|
const seg = portPlan.segments[s];
|
|
|
|
|
const parsed = parseMonsterUnitDefId(seg.unitDefId);
|
|
|
|
|
if (!parsed) continue;
|
|
|
|
|
const row = MonsterDataManager.getMonsterDataById(parsed.monsterId);
|
|
|
|
|
if (row?.type === MonsterType.SmallMonster) {
|
|
|
|
|
return parsed.monsterId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log(LOG, '本波无小怪段', wi);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 营所属玩家 id(敌方格取对手) */
|
|
|
|
|
private _resolveOwnerPlayerId(enemyCamp: boolean): number {
|
|
|
|
|
const battle = gg.game.CurentBattle;
|
|
|
|
|
const localPid = battle?.player?.playerId ?? 0;
|
|
|
|
|
if (!enemyCamp) return localPid;
|
|
|
|
|
return localPid === 0 ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _clearCampLabels(): void {
|
|
|
|
|
if (this._lbMonsterName) this._lbMonsterName.string = '';
|
|
|
|
|
if (this._lbSoldierSpeed) this._lbSoldierSpeed.string = '';
|
|
|
|
|
if (this._lbSkillDesc) this._lbSkillDesc.string = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _clearPeachLabels(): void {
|
|
|
|
|
if (this.lb_time) this.lb_time.string = '';
|
|
|
|
|
if (this._lbPeachSkillDesc) this._lbPeachSkillDesc.string = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 = '';
|
|
|
|
|
this._clearCampLabels();
|
|
|
|
|
this._clearPeachLabels();
|
|
|
|
|
if (this._uiAtkNode?.isValid) this._uiAtkNode.active = false;
|
|
|
|
|
if (this._uiCampNode?.isValid) this._uiCampNode.active = false;
|
|
|
|
|
const peachPanel = this._uiPeachNode ?? this.ui_peachNode;
|
|
|
|
|
if (peachPanel?.isValid) peachPanel.active = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 已部署则重读 ECS 最终数值(含攻速符 aspd_add) */
|
|
|
|
|
private _resolveLiveRuntime(runtime: TowerRuntimeSnapshot | null, entity: TileEntitySnapshot): TowerRuntimeSnapshot | null {
|
|
|
|
|
let live = runtime;
|
|
|
|
|
const battle = gg.game.CurentBattle;
|
|
|
|
|
const session = battle?.session ?? null;
|
|
|
|
|
const entityId = live?.entityId ?? 0;
|
|
|
|
|
if (entityId > 0 && session) {
|
|
|
|
|
live = TowerQueryService.instance.getRuntimeSnapshot(session, entityId) ?? live;
|
|
|
|
|
return live;
|
|
|
|
|
}
|
|
|
|
|
if (entity.location?.kind === 'map' && session) {
|
|
|
|
|
live = TowerQueryService.instance.getRuntimeByGridIndex(entity.location.gridIndex, session, null) ?? live;
|
|
|
|
|
}
|
|
|
|
|
return live;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 优先 ECS 最终攻速/攻击;未部署才回落配表快照 */
|
|
|
|
|
private _resolveLiveStats(runtime: TowerRuntimeSnapshot | null, entity: TileEntitySnapshot): TowerRuntimeSnapshot['stats'] | undefined {
|
|
|
|
|
const session = gg.game.CurentBattle?.session ?? null;
|
|
|
|
|
const entityId = runtime?.entityId ?? 0;
|
|
|
|
|
if (entityId > 0 && session) {
|
|
|
|
|
const live = TowerQueryService.instance.readLiveStats(session, entityId);
|
|
|
|
|
if (live) return live;
|
|
|
|
|
}
|
|
|
|
|
return runtime?.stats ?? entity.tower?.stats;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _applyPanelPosition(data: BattleWeaponDesc1PanelData | 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 = UIBattleWeaponDesc1.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) {
|
|
|
|
|
for (const r of body.getComponentsInChildren(UIRenderer)) {
|
|
|
|
|
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',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|