You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/ui/BattleWeaponDesc/UIBattleWeaponDesc.ts

370 lines
13 KiB

//*********************
// create by 流云
// time: Fri Jul 10 2026
// desc: 战斗内点击字块 — 武器详情弹窗
//*********************
import {
_decorator,
BlockInputEvents,
EventTouch,
instantiate,
Label,
Layout,
Node,
RichText,
ScrollView,
Sprite,
Tween,
UITransform,
UIOpacity,
UIRenderer,
v2,
Vec3,
} from 'cc';
import { type IPopAnimConfig, type IPopAnimSession } from '../../../mx/module/ui/popAnim';
import { Auto_BattleWeaponDesc } from './Auto_BattleWeaponDesc';
import type { TileEntitySnapshot, TowerRuntimeSnapshot } from '../../battle/combat/TowerQueryTypes';
import { deriveTowerSetupStats, TowerStatDisplay } from '../../battle/combat/TowerStatResolver';
import { GBundle, GPath } from '../../game/ConfigRes';
import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad';
const { ccclass } = _decorator;
/** 局外 pageWeapon.iconSpr 尺寸 */
const HERO_ICON_W = 200;
const HERO_ICON_H = 107;
const HERO_ICON_SCALE = 0.7;
export interface BattleWeaponDescPanelData {
entity: TileEntitySnapshot;
runtime?: TowerRuntimeSnapshot | null;
weaponWorldPos?: { x: number; y: number; z?: number };
gridStep?: number;
trySwitchAtUiPos?: (uiPos: { x: number; y: number }) => boolean;
}
@ccclass('UIBattleWeaponDesc')
export class UIBattleWeaponDesc extends Auto_BattleWeaponDesc {
private static readonly ITEM_HEIGHT_COLLAPSED = 82;
private static readonly ITEM_HEIGHT_EXPANDED = 152;
private static readonly PANEL_GAP_GRIDS = 1.7;
private _popupBody: Node | null = null;
private _heroIconSpr: SpriteLoad | null = null;
/** 递增时取消尚未完成的关窗销毁(关窗动效中复开) */
private _panelLifeGen = 0;
onLoad(): void {
super.onLoad();
this._popupBody = this.node.getChildByName('弹窗主体');
if (this.sv_list) {
this.sv_list.cancelInnerEvents = false;
}
this._setupHeroIconSlot();
}
onEnable(): void {
super.onEnable();
this.ui_bgMask?.on(Node.EventType.TOUCH_END, this.click_ui_bgMask, this);
}
onDisable(): void {
if (this.ui_bgMask?.isValid) {
this.ui_bgMask.off(Node.EventType.TOUCH_END, this.click_ui_bgMask, this);
}
super.onDisable();
}
onInit(): void {
this._panelLifeGen++;
this._ensurePanelVisible();
const data = this.prema as BattleWeaponDescPanelData | null;
if (!data?.entity) {
this._clearList();
this._clearHeroIcon();
return;
}
const { entity, runtime } = data;
const tower = entity.tower;
const w1 = tower?.config;
const stats = runtime?.stats ?? tower?.stats;
const level = runtime?.level ?? entity.level;
const levelLimit = tower?.levelLimit ?? w1?.level_limit ?? level;
this._refreshHeroIcon(w1);
this._refreshStatLabels(w1, level, levelLimit, stats, entity.weapon1Id);
this._refreshSkillList(entity.selectedEntries ?? []);
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();
});
}
/** 仅英雄头像(无品质框),与局外 pageWeapon.iconSpr 同源 */
private _setupHeroIconSlot(): void {
if (!this.ui_node?.isValid) return;
this.ui_node.removeAllChildren();
this.ui_node.setScale(1, 1, 1);
const rootUt = this.ui_node.getComponent(UITransform) ?? this.ui_node.addComponent(UITransform);
rootUt.setContentSize(100, 100);
const iconNode = new Node('iconSpr');
iconNode.parent = this.ui_node;
iconNode.setScale(HERO_ICON_SCALE, HERO_ICON_SCALE, 1);
iconNode.addComponent(UITransform).setContentSize(HERO_ICON_W, HERO_ICON_H);
iconNode.addComponent(Sprite);
this._heroIconSpr = iconNode.addComponent(SpriteLoad);
}
private _refreshHeroIcon(w1: ITableHero | null | undefined): void {
if (!this.ui_node?.isValid || !this._heroIconSpr) return;
if (!w1) {
this._clearHeroIcon();
return;
}
const itemData = gg.data.table.getItemData(gg.data.project.getWeaponScrapId(w1.id));
if (!itemData) {
this._clearHeroIcon();
return;
}
this.ui_node.active = true;
this._heroIconSpr.setSprite(GPath.Item(itemData.icon), GBundle.Items);
}
private _clearHeroIcon(): void {
if (this.ui_node?.isValid) {
this.ui_node.active = false;
}
}
/** 关窗动效未完成时复开:仅恢复弹窗主体渲染,遮罩保持透明触摸层 */
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;
}
for (const layout of body.getComponentsInChildren(Layout)) {
if (layout?.isValid) layout.enabled = true;
}
}
this._keepMaskTouchOnly();
}
/** ui_bgMask 预制体:Sprite 关闭 = 透明,仅 BlockInputEvents 接收点击做字块切换 */
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',
};
}
private _refreshStatLabels(
w1: ITableHero | null | undefined,
level: number,
levelLimit: number,
stats: TowerRuntimeSnapshot['stats'] | undefined,
weapon1Id: number,
): void {
if (!w1) {
if (this.lb_level) this.lb_level.string = '';
if (this.lb_attck) this.lb_attck.string = '';
if (this.lb_attck2?.node) this.lb_attck2.node.active = false;
if (this.lb_attckMethod) this.lb_attckMethod.string = '';
if (this.lb_attckSpeed) this.lb_attckSpeed.string = '';
if (this.lb_attckSpeed2?.node) this.lb_attckSpeed2.node.active = false;
return;
}
const setup = deriveTowerSetupStats(w1, level);
const attack = stats?.attack ?? setup.attack;
const attackSpeed = stats?.attackSpeed ?? setup.attackSpeed;
if (this.lb_level) {
this.lb_level.string = `Lv.${level}(满级${levelLimit}级)`;
}
if (this.lb_attck) {
this.lb_attck.string = `攻击力:${TowerStatDisplay.oneDecimal(attack)}`;
}
if (this.lb_attckMethod) {
this.lb_attckMethod.string = `攻击方式:${w1.type ?? ''}`;
}
if (this.lb_attckSpeed) {
this.lb_attckSpeed.string = `攻速:${TowerStatDisplay.attackSpeed(attackSpeed)}`;
}
const nextLevel = level + 1;
const canLevelUp = nextLevel <= levelLimit;
if (this.lb_attck2) {
this.lb_attck2.node.active = canLevelUp;
if (canLevelUp) {
const nextSetup = deriveTowerSetupStats(w1, nextLevel);
const attackDelta = (nextSetup.attack) - setup.attack;
this.lb_attck2.string = `→Lv.${nextLevel}+${TowerStatDisplay.oneDecimal(attackDelta)}`;
}
}
if (this.lb_attckSpeed2) {
this.lb_attckSpeed2.node.active = canLevelUp;
if (canLevelUp) {
const nextSetup = deriveTowerSetupStats(w1, nextLevel);
const speedDelta = nextSetup.attackSpeed - setup.attackSpeed;
this.lb_attckSpeed2.string = `→Lv.${nextLevel}+${TowerStatDisplay.attackSpeed(speedDelta)}`;
}
}
this.layAttack?.getComponent(Layout)?.updateLayout();
this.laySpeed?.getComponent(Layout)?.updateLayout();
}
private _applyPanelPosition(data: BattleWeaponDescPanelData | 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 = UIBattleWeaponDesc.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);
}
click_ui_bgMask(event?: EventTouch): void {
const uiPos = event?.getUILocation();
const trySwitch = (this.prema as BattleWeaponDescPanelData | null)?.trySwitchAtUiPos;
if (uiPos && trySwitch?.(uiPos)) {
return;
}
this.close(2);
}
private _refreshSkillList(entries: readonly ITableSkill[]): void {
if (!this.ui_content?.isValid || !this.ui_itemTemplate?.isValid) return;
this._clearList();
for (const entry of entries) {
const item = instantiate(this.ui_itemTemplate);
item.parent = this.ui_content;
item.active = true;
this._setupSkillItem(item, entry);
}
this._refreshContentLayout();
this.sv_list?.scrollToOffset(v2(0, 0), 0);
}
private _setupSkillItem(item: Node, entry: ITableSkill): void {
const bg = item.getChildByName('bg');
const bg2 = item.getChildByName('bg2');
if (!bg?.isValid || !bg2?.isValid) return;
bg.getOrAddComponent(SpriteLoad).setSprite(GPath.popRes('battleGameResult', `texture/q${entry.quality}`), GBundle.PopBattle);
bg2.getOrAddComponent(SpriteLoad).setSprite(GPath.popRes('battleGameResult', `texture/q${entry.quality}开`), GBundle.PopBattle);
const name = gg.lang.get(entry.name ?? '');
const desc = gg.lang.get(entry.desc ?? '');
const nameLb = bg2.getChildByName('name')?.getComponent(Label);
const descNode = bg2.getChildByName('desc');
const descLb = descNode?.getComponent(RichText);
const name2Lb = bg.getChildByName('name2')?.getComponent(Label);
if (nameLb) nameLb.string = name;
if (descLb) descLb.string = desc;
if (descNode?.isValid) descNode.active = true;
if (name2Lb) name2Lb.string = name;
this._setSkillItemExpanded(item, false);
item.getOrAddComponent(BlockInputEvents);
item.on(Node.EventType.TOUCH_END, (event: EventTouch) => {
event.propagationStopped = true;
const expanded = bg2.active;
this._setSkillItemExpanded(item, !expanded);
this._refreshContentLayout();
}, this);
}
private _setSkillItemExpanded(item: Node, expanded: boolean): void {
const bg = item.getChildByName('bg');
const bg2 = item.getChildByName('bg2');
if (!bg?.isValid || !bg2?.isValid) return;
bg.active = !expanded;
bg2.active = expanded;
const ut = item.getComponent(UITransform);
if (!ut) return;
const height = expanded
? UIBattleWeaponDesc.ITEM_HEIGHT_EXPANDED
: UIBattleWeaponDesc.ITEM_HEIGHT_COLLAPSED;
ut.setContentSize(ut.contentSize.width, height);
}
private _refreshContentLayout(): void {
this.ui_content?.getComponent(Layout)?.updateLayout();
}
private _clearList(): void {
if (!this.ui_content?.isValid) return;
for (const child of [...this.ui_content.children]) {
child.destroy();
}
}
}