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.
372 lines
13 KiB
372 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;
|
|
|
|
// Skill 表无 quality(旧词条品级已删);遗留列表用默认框
|
|
const q = 1;
|
|
bg.getOrAddComponent(SpriteLoad).setSprite(GPath.popRes('battleGameResult', `texture/q${q}`), GBundle.PopBattle);
|
|
bg2.getOrAddComponent(SpriteLoad).setSprite(GPath.popRes('battleGameResult', `texture/q${q}开`), 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();
|
|
}
|
|
}
|
|
}
|
|
|