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/PanelShenjiang/UIPanelShenjiang.ts

1487 lines
56 KiB

//*********************
// create by 流云
// time: Tue Aug 25 2026
// desc:
//*********************
import { _decorator, assetManager, AssetManager, Component, EventMouse, EventTouch, Input, input, instantiate, Label, Layout, Node, find, Prefab, Rect, sp, Sprite, Tween, tween, UIOpacity, UITransform, Vec2, Vec3 } from 'cc';
import { Auto_PanelShenjiang } from './Auto_PanelShenjiang';
import { GBundle, GPath, UIID } from '../../game/ConfigRes';
import { TableNames } from '../../game/ConfigTableData';
import { RankReport } from '../../manager/RankReport';
import { shenjiangItem } from './shenjiangItem';
import type { IMingjiangkuBagEntry, MingjiangkuBagKind } from '../../manager/EquipDataManager';
import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad';
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
import { RedDotView } from 'db://assets/mx/module/redDot/RedDotView';
import { UIMainUI } from '../MainUI/UIMainUI';
import { HERO_DEFAULT_EQUIP_ID } from '../../game/GameDataEnums';
import { FrameFlowHost } from '../../game/FrameFlowHost';
const { ccclass } = _decorator;
/** ui_atk 相对神将格在 center 本地 Y 偏移 */
const ATK_HINT_OFFSET_Y = 90;
const DRAG_THRESHOLD = 18;
const GHOST_OPACITY = 180;
const LOG = '[PanelShenjiang]';
/** 未穿戴成品武器时 name 节点图 */
const WEAPON_NAME_EMPTY_SPRITE = 'weapon/无';
/** 武器库每页格子数 */
const BAG_PAGE_SIZE = 16;
/** nameBg.name:不少于 5 字缩放 0.8,否则 1 */
const WEAPON_NAME_LONG_LEN = 5;
const HERO_NAMEBG_NAME_SCALE_LONG = 0.8;
const HERO_NAMEBG_NAME_SCALE_NORMAL = 1;
/** 武器冒出起始缩放 */
const WEAPON_POP_SCALE_FROM = 0.18;
/** 武器渐显秒 */
const WEAPON_POP_FADE_SEC = 0.2;
/** 武器回弹冒出秒 */
const WEAPON_POP_SCALE_SEC = 0.42;
/**
* 神将武器:合成神将网格 + 武器库拖拽穿戴
* 神将字:shenjiang/prefab/{Hero.res},Spine 为 res/spine/likeRole
*/
@ccclass('UIPanelShenjiang')
export class UIPanelShenjiang extends Auto_PanelShenjiang {
/** 武器库格子模板 */
private _bagItemTpl: Node | null = null;
/** 武器库拖拽幽灵模板(itemNode1) */
private _dragItemTpl: Node | null = null;
/** 当前页武器库格子节点 */
private _bagItems: Node[] = [];
/** 是否已用模板作为第一格 */
private _bagItemBuilt = false;
/** 武器库当前页(0 起) */
private _bagPageIndex = 0;
/** 武器库总页数 */
private _bagPageCount = 1;
/** 神将格子模板 */
private _heroTpl: Node | null = null;
/** 神将网格根(center/shenjiang) */
private _heroRoot: Node | null = null;
/** 已创建的神将格子 */
private _heroItems: Node[] = [];
/** 本轮打开页填充是否仍有效 */
private _fillAlive = false;
/** 填充代数,过期加载丢弃 */
private _fillToken = 0;
/** 武器库展示列表 */
private _bagList: IMingjiangkuBagEntry[] = [];
/** 正在拖的成品武器 id */
private _dragEquipId = 0;
/** 是否已进入拖拽 */
private _dragging = false;
/** 按下起点 */
private _dragStart = new Vec2();
/** 拖拽幽灵节点 */
private _ghost: Node | null = null;
/** 幽灵世界坐标缓存 */
private _ghostPos = new Vec3();
/** 命中检测坐标缓存 */
private _hitPos = new Vec2();
/** 拖拽监听是否已绑定 */
private _dragListening = false;
/** 武器库按下的格子 */
private _dragItem: shenjiangItem | null = null;
/** >0:从神将身上拖成品;0:从武器库拖 */
private _dragFromHeroId = 0;
/** 按下命中的成品/碎片,松手未拖则打开介绍 */
private _tapEquipId = 0;
/** 按下命中的武器库格子类型 */
private _tapBagKind: MingjiangkuBagKind = 'equip';
/** 本次按下是否来自神将格 */
private _pressFromHero = false;
/** 手指/鼠标已按下 */
private _pressing = false;
/** 回收弹窗打开中 */
private _openingHuishou = false;
/** center 根节点(ui_atk 定位参照) */
private _centerNode: Node | null = null;
/** ui_atk/atkUp */
private _atkUpNode: Node | null = null;
/** ui_atk/atkReduce */
private _atkReduceNode: Node | null = null;
/** 打开页分帧填充 */
private _flow: FrameFlowHost | null = null;
/** 本轮待填充神将 */
private _fillHeroes: ITableHero[] = [];
/** 已克隆神将格数 */
private _heroCloneIndex = 0;
/** 已绑名字条的神将下标 */
private _heroMetaIndex = 0;
/** 已填充的武器库格下标 */
private _bagFillIndex = 0;
/** 尚未回调的神将预制体预取 */
private _heroPrefabPending = 0;
/** 分帧泵已预约 */
private _pumpScheduled = false;
onLoad(): void {
super.onLoad();
this._centerNode = find('center', this.node);
if (this.ui_atk?.isValid) {
this._atkUpNode = this.ui_atk.getChildByName('atkUp');
this._atkReduceNode = this.ui_atk.getChildByName('atkReduce');
this.ui_atk.active = false;
}
if (this.ui_content?.isValid) {
this.ui_content.active = true;
this._dragItemTpl = find('center/ui_content/itemNode1', this.node)
?? this.ui_content.getChildByName('itemNode1')
?? this.ui_content.children[0]
?? null;
this._bagItemTpl = this._dragItemTpl;
if (this._bagItemTpl?.isValid) {
RedDotView.setLocal(this._bagItemTpl, false);
}
}
this._heroTpl = find('center/shenjiang/shenjiangNode', this.node);
this._heroRoot = this._heroTpl?.parent ?? null;
if (this._heroTpl) {
this._heroTpl.removeFromParent();
this._heroTpl.parent = this.node;
this._heroTpl.active = false;
this._setHeroWearMask(this._heroTpl, false);
this._hideHeroWeapon(this._heroTpl);
}
this._setupHeroGridLayout();
this._warmHeroPrefabs();
this._bindWeaponDrag(true);
}
onEnable(): void {
super.onEnable();
this._bindWeaponDrag(true);
}
onDisable(): void {
this._bindWeaponDrag(false);
this._endEquipDrag();
this._openingHuishou = false;
this._fillAlive = false;
this._fillToken++;
this._stopFillFlow();
this._stopAllWeaponPops();
gg.equip.markNewWeaponBadgeDismissed(gg.equip.collectVisibleNewWeaponBadgeIds());
this._hideAllBagNewWeaponTags();
super.onDisable();
}
onInit(): void {
this._bindWeaponDrag(true);
this._bagPageIndex = 0;
gg.equip.markHeroSeen();
void this._refresh().then(() => {
if (!this.isValid) return;
this._tryOpenPopNewWeapon();
});
}
/** 有待展示的新成品武器时弹出 PopNewWeapon */
private _tryOpenPopNewWeapon(): void {
if (gg.ui.hasPopPanelActiveOrQueued(UIID.PopNewWeapon)) return;
const ids = gg.equip.collectPendingPopNewWeaponIds();
if (!ids.length) return;
void gg.ui.openPanel(UIID.PopNewWeapon, { data: ids });
}
/** 主界面 Tab,遮罩不关闭 */
click_bgMask(): void {
}
/** 关闭神将页,回到首页 Tab */
click_btnClose(): void {
const main = gg.ui.getPanelBase(UIID.MainUI) as UIMainUI;
if (main) {
void main.showPage(UIID.Home);
return;
}
void this.close(2);
}
/** 关闭按钮图标 */
click_icon(): void {
this.click_btnClose();
}
/** 打开回收弹窗 */
click_btn_huishou(): void {
if (this._openingHuishou || gg.ui.hasPopPanelActiveOrQueued(UIID.popHuishou)) return;
this._openingHuishou = true;
void gg.ui.openPanel(UIID.popHuishou, {
onClose: this._onHuishouClosed,
}).then(this._onHuishouOpenDone).catch(this._onHuishouOpenFail);
}
/** 武器库下一页 */
click_btnNext(): void {
if (this._bagPageIndex >= this._bagPageCount - 1) return;
this._bagPageIndex++;
this._renderBagPage();
}
/** 武器库上一页 */
click_btnPre(): void {
if (this._bagPageIndex <= 0) return;
this._bagPageIndex--;
this._renderBagPage();
}
private _onHuishouClosed = (): void => {
this._openingHuishou = false;
this._refreshBagList();
};
private _onHuishouOpenDone = (ok: boolean): void => {
if (!ok) this._openingHuishou = false;
};
private _onHuishouOpenFail = (): void => {
this._openingHuishou = false;
};
private _bindWeaponDrag(on: boolean): void {
if (this._dragListening === on) return;
this._dragListening = on;
const bagRoot = this.ui_content;
const heroRoot = this._heroRoot;
if (on) {
bagRoot?.on(Node.EventType.TOUCH_START, this._onBagAreaTouchStart, this, true);
bagRoot?.on(Node.EventType.TOUCH_MOVE, this._onBagAreaTouchMove, this, true);
bagRoot?.on(Node.EventType.TOUCH_END, this._onBagAreaTouchEnd, this, true);
bagRoot?.on(Node.EventType.TOUCH_CANCEL, this._onBagAreaTouchEnd, this, true);
heroRoot?.on(Node.EventType.TOUCH_START, this._onHeroTouchStart, this, true);
heroRoot?.on(Node.EventType.TOUCH_MOVE, this._onBagAreaTouchMove, this, true);
heroRoot?.on(Node.EventType.TOUCH_END, this._onBagAreaTouchEnd, this, true);
heroRoot?.on(Node.EventType.TOUCH_CANCEL, this._onBagAreaTouchEnd, this, true);
this.node.on(Node.EventType.TOUCH_MOVE, this._onBagAreaTouchMove, this, true);
this.node.on(Node.EventType.TOUCH_END, this._onBagAreaTouchEnd, this, true);
this.node.on(Node.EventType.TOUCH_CANCEL, this._onBagAreaTouchEnd, this, true);
input.on(Input.EventType.TOUCH_START, this._onBagTouchStart, this);
input.on(Input.EventType.TOUCH_MOVE, this._onBagTouchMove, this);
input.on(Input.EventType.TOUCH_END, this._onBagTouchEnd, this);
input.on(Input.EventType.TOUCH_CANCEL, this._onBagTouchEnd, this);
input.on(Input.EventType.MOUSE_DOWN, this._onBagMouseDown, this);
input.on(Input.EventType.MOUSE_MOVE, this._onBagMouseMove, this);
input.on(Input.EventType.MOUSE_UP, this._onBagMouseUp, this);
} else {
bagRoot?.off(Node.EventType.TOUCH_START, this._onBagAreaTouchStart, this, true);
bagRoot?.off(Node.EventType.TOUCH_MOVE, this._onBagAreaTouchMove, this, true);
bagRoot?.off(Node.EventType.TOUCH_END, this._onBagAreaTouchEnd, this, true);
bagRoot?.off(Node.EventType.TOUCH_CANCEL, this._onBagAreaTouchEnd, this, true);
heroRoot?.off(Node.EventType.TOUCH_START, this._onHeroTouchStart, this, true);
heroRoot?.off(Node.EventType.TOUCH_MOVE, this._onBagAreaTouchMove, this, true);
heroRoot?.off(Node.EventType.TOUCH_END, this._onBagAreaTouchEnd, this, true);
heroRoot?.off(Node.EventType.TOUCH_CANCEL, this._onBagAreaTouchEnd, this, true);
this.node.off(Node.EventType.TOUCH_MOVE, this._onBagAreaTouchMove, this, true);
this.node.off(Node.EventType.TOUCH_END, this._onBagAreaTouchEnd, this, true);
this.node.off(Node.EventType.TOUCH_CANCEL, this._onBagAreaTouchEnd, this, true);
input.off(Input.EventType.TOUCH_START, this._onBagTouchStart, this);
input.off(Input.EventType.TOUCH_MOVE, this._onBagTouchMove, this);
input.off(Input.EventType.TOUCH_END, this._onBagTouchEnd, this);
input.off(Input.EventType.TOUCH_CANCEL, this._onBagTouchEnd, this);
input.off(Input.EventType.MOUSE_DOWN, this._onBagMouseDown, this);
input.off(Input.EventType.MOUSE_MOVE, this._onBagMouseMove, this);
input.off(Input.EventType.MOUSE_UP, this._onBagMouseUp, this);
}
}
private _onBagAreaTouchStart(e: EventTouch): void {
this._pointerStart(e.getUILocation());
}
private _onBagAreaTouchMove(e: EventTouch): void {
this._pointerMove(e.getUILocation());
}
private _onBagAreaTouchEnd(e: EventTouch): void {
this._pointerEnd(e.getUILocation());
}
private _onHeroTouchStart(e: EventTouch): void {
this._pointerStart(e.getUILocation());
}
private _onBagTouchStart(e: EventTouch): void {
this._pointerStart(e.getUILocation());
}
private _onBagTouchMove(e: EventTouch): void {
this._pointerMove(e.getUILocation());
}
private _onBagTouchEnd(e: EventTouch): void {
this._pointerEnd(e.getUILocation());
}
private _onBagMouseDown(e: EventMouse): void {
if (e.getButton() !== EventMouse.BUTTON_LEFT) return;
this._pointerStart(e.getUILocation());
}
private _onBagMouseMove(e: EventMouse): void {
this._pointerMove(e.getUILocation());
}
private _onBagMouseUp(e: EventMouse): void {
this._pointerEnd(e.getUILocation());
}
private _pointerStart(loc: Vec2): void {
if (this._pressing || this._dragging) return;
const bag = this._hitBagItem(loc);
if (bag) {
this._resetDragFields();
this._pressing = true;
this._pressFromHero = false;
this._dragStart.set(loc);
this._tapEquipId = bag.data?.equipId || 0;
this._tapBagKind = bag.data?.kind === 'fragment' ? 'fragment' : 'equip';
this._dragFromHeroId = 0;
this._dragging = false;
if (bag.data?.kind === 'equip' && this._tapEquipId > 0) {
this._dragEquipId = this._tapEquipId;
this._dragItem = bag;
}
return;
}
const heroNode = this._hitHero(loc);
const heroId = Number(heroNode?.['heroId']) || 0;
if (!heroNode || heroId <= 0) return;
const isDefault = gg.equip.isHeroDefaultWeapon(heroId);
const equipId = gg.equip.getHeroEquipId(heroId);
if (isDefault || equipId <= 0) return;
this._resetDragFields();
this._pressing = true;
this._pressFromHero = true;
this._dragStart.set(loc);
this._tapEquipId = equipId;
this._dragEquipId = equipId;
this._dragItem = null;
this._dragFromHeroId = heroId;
this._dragging = false;
}
private _pointerMove(loc: Vec2): void {
if (this._dragging) {
this._moveGhost(loc);
return;
}
if (!this._pressing) return;
if (this._dragEquipId <= 0 && this._tapEquipId <= 0) return;
const dx = loc.x - this._dragStart.x;
const dy = loc.y - this._dragStart.y;
if (dx * dx + dy * dy < DRAG_THRESHOLD * DRAG_THRESHOLD) return;
if (this._dragEquipId > 0) {
this._beginEquipDrag(loc);
return;
}
this._tapEquipId = 0;
this._dragFromHeroId = 0;
this._dragItem = null;
this._pressFromHero = false;
}
private _pointerEnd(loc: Vec2): void {
if (!this._pressing && !this._dragging) return;
if (this._dragging) {
this._finishEquipDrag(loc);
return;
}
const tapEquipId = this._tapEquipId;
const tapKind = this._tapBagKind;
this._endEquipDrag();
if (tapEquipId > 0) {
if (tapKind === 'equip') {
gg.equip.markNewWeaponBadgeDismissed([tapEquipId]);
this._hideBagNewWeaponTag(tapEquipId);
}
gg.equip.markFragSeen(tapEquipId);
this._refreshBagList();
gg.ui.openPanel(UIID.mingjiangIntroduction, { data: { equipId: tapEquipId, kind: tapKind } });
}
}
private _hitBagItem(loc: Vec2): shenjiangItem | null {
const content = this.ui_content;
if (!content?.isValid) return null;
this._hitPos.set(loc.x, loc.y);
const kids = content.children;
for (let i = kids.length - 1; i >= 0; i--) {
const child = kids[i];
if (!child?.activeInHierarchy) continue;
const ut = child.getComponent(UITransform);
if (!ut?.getBoundingBoxToWorld().contains(this._hitPos)) continue;
const comp = child.getComponent(shenjiangItem);
if ((comp?.data?.equipId | 0) > 0) return comp;
return null;
}
return null;
}
private _finishEquipDrag(loc: Vec2): void {
const heroNode = this._hitHero(loc);
const toHeroId = Number(heroNode?.['heroId']) || 0;
const equipId = this._dragEquipId;
const fromHeroId = this._dragFromHeroId;
if (heroNode && toHeroId > 0) {
if (fromHeroId > 0) {
if (fromHeroId !== toHeroId && gg.equip.moveOrSwapHeroEquip(fromHeroId, toHeroId)) {
this._refreshHeroWeaponSkin(this._heroItemById(fromHeroId));
this._refreshHeroWeaponSkin(heroNode);
this._reportWear(toHeroId, gg.equip.getHeroEquipId(toHeroId));
}
} else if (gg.equip.wear(toHeroId, equipId)) {
this._refreshHeroWeaponSkin(heroNode);
this._reportWear(toHeroId, equipId);
}
} else if (fromHeroId > 0 && this._hitBagArea(loc)) {
if (gg.equip.unequip(fromHeroId)) {
this._refreshHeroWeaponSkin(this._heroItemById(fromHeroId));
}
}
this._endEquipDrag();
}
private _reportWear(heroId: number, equipId: number): void {
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
const equip = gg.equip.getEquip(equipId);
RankReport.mingjiangWear(
String(hero?.name ?? '').trim(),
String(equip?.name ?? '').trim(),
);
}
private _beginEquipDrag(loc: Vec2): void {
this._dragging = true;
this._applyWearHint(this._dragEquipId);
this._spawnBagDragGhost(loc);
if (this._dragFromHeroId <= 0) {
const left = Math.max(0, gg.equip.getFreeEquipCount(this._dragEquipId) - 1);
this._dragItem?.setCount(left);
}
this._moveGhost(loc);
}
/** 武器库拖拽:复制 itemNode1,仅展示 quality + name */
private _spawnBagDragGhost(loc: Vec2): void {
this._clearGhost();
const tpl = this._dragItemTpl;
if (!tpl?.isValid) return;
const data = this._dragItem?.data ?? this._ghostEquipData(this._dragEquipId);
if (!data || data.kind !== 'equip') return;
const ghost = instantiate(tpl);
ghost.parent = this.node;
ghost.layer = this.node.layer;
this._setLayerRecursive(ghost, this.node.layer);
const siblings = this.node.children;
if (siblings) ghost.setSiblingIndex(siblings.length - 1);
ghost.active = true;
this._disableLogicScripts(ghost);
const comp = ghost.getComponent(shenjiangItem) || ghost.addComponent(shenjiangItem);
comp.setData(data);
this._applyBagDragGhostView(ghost);
RedDotView.setLocal(ghost, false);
const ut = ghost.getComponent(UITransform);
if (ut) ut.hitTest = this._ghostNoHit;
const uo = ghost.getComponent(UIOpacity) || ghost.addComponent(UIOpacity);
uo.opacity = GHOST_OPACITY;
this._ghost = ghost;
this._moveGhost(loc);
}
/** 拖拽幽灵仅保留品质底与武器名 */
private _applyBagDragGhostView(ghost: Node): void {
const suipian = ghost.getChildByName('suipianNode');
if (suipian) suipian.active = false;
const openNode = ghost.getChildByName('openNode');
if (openNode) {
openNode.active = true;
const numBg = openNode.getChildByName('numBg');
if (numBg) numBg.active = false;
for (let t = 1; t <= 4; t++) {
const typeNode = openNode.getChildByName(`type${t}`);
if (typeNode) typeNode.active = false;
}
const nameNode = openNode.getChildByName('name');
if (nameNode) nameNode.active = true;
const newWeapon = openNode.getChildByName('newWeapon');
if (newWeapon) newWeapon.active = false;
}
const quality = ghost.getChildByName('quality');
if (quality) quality.active = true;
}
/** 神将格拖拽:用武器表拼幽灵数据 */
private _ghostEquipData(equipId: number): IMingjiangkuBagEntry | null {
const id = Number(equipId) | 0;
if (id <= 0) return null;
if (!gg.equip.getEquip(id)) {
console.log(LOG, '拖拽幽灵武器缺失', id);
return null;
}
return { kind: 'equip', equipId: id, itemId: 0, count: 1, need: 0 };
}
private _moveGhost(loc: Vec2): void {
if (this._ghost?.isValid) {
this._ghost.setWorldPosition(this._ghostPos.set(loc.x, loc.y, 0));
}
if (this._dragging) this._updateAtkHint(loc);
}
private _hitHero(loc: Vec2): Node | null {
this._hitPos.set(loc.x, loc.y);
let best: Node | null = null;
let bestD = Number.POSITIVE_INFINITY;
for (let i = 0; i < this._heroItems.length; i++) {
const item = this._heroItems[i];
if (!item?.isValid || !item.activeInHierarchy) continue;
const box = this._heroHitBox(item);
if (!box.contains(this._hitPos)) continue;
const dx = this._hitPos.x - (box.x + box.width * 0.5);
const dy = this._hitPos.y - (box.y + box.height * 0.5);
const d = dx * dx + dy * dy;
if (d < bestD) {
bestD = d;
best = item;
}
}
return best;
}
/** 按格子 UITransform 切块,避免 Spine 包围盒叠到上一行 */
private _heroHitBox(item: Node): Rect {
const ut = item.getComponent(UITransform);
const p = item.worldPosition;
if (!ut) {
console.log(LOG, '神将格缺少 UITransform', item.name);
return new Rect(p.x, p.y, 0, 0);
}
return new Rect(p.x - ut.width * 0.5, p.y - ut.height * 0.5, ut.width, ut.height);
}
private _applyWearHint(equipId: number): void {
const fromHero = this._dragFromHeroId;
for (let i = 0; i < this._heroItems.length; i++) {
const item = this._heroItems[i];
if (!item?.isValid || !item.activeInHierarchy) continue;
const heroId = Number(item['heroId']) || 0;
if (fromHero > 0 && heroId === fromHero) {
this._setHeroWearMask(item, false);
continue;
}
const can = this._canPreviewEquipOnHero(heroId, equipId, fromHero);
this._setHeroWearMask(item, heroId <= 0 || !can);
}
}
private _clearWearHint(): void {
for (let i = 0; i < this._heroItems.length; i++) {
const item = this._heroItems[i];
if (!item?.isValid) continue;
this._setHeroWearMask(item, false);
}
this._hideAtkHint();
}
/** 拖拽悬停神将或武器库卸装区时刷新 ui_atk */
private _updateAtkHint(loc: Vec2): void {
if (!this._dragging || this._dragEquipId <= 0) {
this._hideAtkHint();
return;
}
const fromHero = this._dragFromHeroId;
if (fromHero > 0 && this._hitBagArea(loc)) {
this._showUnequipAtkHint(fromHero);
return;
}
const heroNode = this._hitHero(loc);
const heroId = Number(heroNode?.['heroId']) || 0;
if (!heroNode || heroId <= 0) {
this._hideAtkHint();
return;
}
if (fromHero > 0 && heroId === fromHero) {
this._hideAtkHint();
return;
}
if (!this._canPreviewEquipOnHero(heroId, this._dragEquipId, fromHero)) {
this._hideAtkHint();
return;
}
const curWeaponId = gg.equip.getHeroDisplayWeaponId(heroId);
const nextWeaponId = this._dragEquipId;
if (curWeaponId === nextWeaponId) {
this._hideAtkHint();
return;
}
const curAtk = this._resolveHeroPanelAttack(heroId, curWeaponId);
const nextAtk = this._resolveHeroPanelAttack(heroId, nextWeaponId);
this._showAtkHint(heroNode, curAtk, nextAtk);
}
/**
* 拖拽中是否可在目标神将上预览/放置武器(含已穿戴替换、神将互换)。
* 已穿戴成品时替换仅校验类型,不要求背包空闲数(与原武器回库等价)。
*/
private _canPreviewEquipOnHero(heroId: number, equipId: number, fromHeroId: number): boolean {
const id = Number(heroId) | 0;
const eq = Number(equipId) | 0;
const from = Number(fromHeroId) | 0;
if (id <= 0 || eq <= 0) return false;
if (from > 0) {
if (from === id) return false;
if (!gg.equip.canWearType(id, eq)) return false;
const targetWorn = gg.equip.getHeroEquipId(id);
if (targetWorn > 0 && !gg.equip.canWearType(from, targetWorn)) return false;
return true;
}
if (!gg.equip.canWearType(id, eq)) return false;
if (!gg.equip.isHeroDefaultWeapon(id)) return true;
return gg.equip.canWear(id, eq);
}
/** 神将武器拖入武器库:在源神将格展示卸装后攻击力变化 */
private _showUnequipAtkHint(fromHeroId: number): void {
const heroItem = this._heroItemById(fromHeroId);
if (!heroItem?.isValid) {
this._hideAtkHint();
return;
}
const curWeaponId = gg.equip.getHeroDisplayWeaponId(fromHeroId);
if (curWeaponId <= 0) {
this._hideAtkHint();
return;
}
const curAtk = this._resolveHeroPanelAttack(fromHeroId, curWeaponId);
const nextAtk = this._resolveHeroPanelAttack(fromHeroId, HERO_DEFAULT_EQUIP_ID);
this._showAtkHint(heroItem, curAtk, nextAtk);
}
/** 在神将格上方展示攻击力变化(center 本地 y 偏移) */
private _showAtkHint(heroItem: Node, curAtk: number, nextAtk: number): void {
const atkRoot = this.ui_atk;
const center = this._centerNode;
if (!atkRoot?.isValid || !center?.isValid || !heroItem?.isValid) return;
const up = this._atkUpNode?.isValid ? this._atkUpNode : atkRoot.getChildByName('atkUp');
const down = this._atkReduceNode?.isValid ? this._atkReduceNode : atkRoot.getChildByName('atkReduce');
if (up?.isValid) this._atkUpNode = up;
if (down?.isValid) this._atkReduceNode = down;
const centerUt = center.getComponent(UITransform);
if (!centerUt) return;
const local = centerUt.convertToNodeSpaceAR(heroItem.worldPosition);
atkRoot.setPosition(local.x, local.y + ATK_HINT_OFFSET_Y, 0);
atkRoot.active = true;
atkRoot.setSiblingIndex(center.children.length - 1);
const increased = nextAtk > curAtk;
const decreased = nextAtk < curAtk;
if (up?.isValid) {
up.active = increased || (!increased && !decreased);
if (up.active) this._fillAtkHintPanel(up, curAtk, nextAtk);
}
if (down?.isValid) {
down.active = decreased;
if (down.active) this._fillAtkHintPanel(down, curAtk, nextAtk);
}
}
/** 写入 atkUp/atkReduce 内 cur、next 数值 */
private _fillAtkHintPanel(panel: Node, curAtk: number, nextAtk: number): void {
const curLb = panel.getChildByName('cur')?.getComponent(Label);
const nextLb = panel.getChildByName('next')?.getComponent(Label);
if (curLb) curLb.string = `${curAtk}`;
if (nextLb) nextLb.string = `${nextAtk}`;
}
/** 隐藏攻击力变化提示 */
private _hideAtkHint(): void {
if (this.ui_atk?.isValid) this.ui_atk.active = false;
if (this._atkUpNode?.isValid) this._atkUpNode.active = false;
if (this._atkReduceNode?.isValid) this._atkReduceNode.active = false;
}
/**
* 神将面板攻击力:Hero.atk + 武器/宝石 atk(无等级系数、无专属倍率)。
* @param weaponEquipId 预览武器;缺省为当前展示武器
*/
private _resolveHeroPanelAttack(heroId: number, weaponEquipId?: number): number {
const id = Number(heroId) | 0;
if (id <= 0) return 0;
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, id);
if (!hero) return 0;
let weaponId = weaponEquipId;
if (weaponId === undefined) {
weaponId = gg.equip.getHeroDisplayWeaponId(id);
} else if (!(weaponId > 0)) {
weaponId = 0;
}
const weaponRow = weaponId > 0 ? gg.equip.getEquip(weaponId) : null;
const weaponAtk = Math.floor(Number(weaponRow?.atk) || 0);
const heroBase = Number(hero.atk) || 0;
return Math.floor(heroBase + weaponAtk);
}
/** 拖拽穿戴提示:mask 显隐表示是否可穿戴 */
private _setHeroWearMask(item: Node, show: boolean): void {
const mask = item.getChildByName('mask');
if (mask) mask.active = show;
}
/** 穿戴变化后刷新武器 Spine、名字条并冒出 */
private _refreshHeroWeaponSkin(heroItem: Node | null | undefined): void {
if (!heroItem?.isValid) return;
const heroId = Number(heroItem['heroId']) || 0;
this._applyHeroWeaponNameBar(heroItem, heroId);
this._applyHeroWeaponNode(heroItem, heroId);
this._playHeroWeaponPop(heroItem);
}
private _heroItemById(heroId: number): Node | null {
const id = Number(heroId) | 0;
if (id <= 0) return null;
for (let i = 0; i < this._heroItems.length; i++) {
if ((Number(this._heroItems[i]?.['heroId']) || 0) === id) return this._heroItems[i];
}
return null;
}
/** 武器库卸装落点:ui_wuqi 固定区域(ui_content 空库时 Layout 高度为 0) */
private _hitBagArea(loc: Vec2): boolean {
const n = this.ui_wuqi;
if (!n?.isValid) {
console.log(LOG, 'ui_wuqi 缺失');
return false;
}
const ut = n.getComponent(UITransform);
return !!ut?.getBoundingBoxToWorld().contains(this._hitPos.set(loc.x, loc.y));
}
private _clearGhost(): void {
if (this._ghost?.isValid) {
this._ghost.removeFromParent();
this._ghost.destroy();
}
this._ghost = null;
}
private _resetDragFields(): void {
this._pressing = false;
this._pressFromHero = false;
this._dragging = false;
this._dragEquipId = 0;
this._dragItem = null;
this._dragFromHeroId = 0;
this._tapEquipId = 0;
this._tapBagKind = 'equip';
this._clearGhost();
this._clearWearHint();
}
private _endEquipDrag(): void {
const needRefresh = this._dragging;
this._resetDragFields();
if (needRefresh) this._refreshBagList();
}
private async _refresh(): Promise<void> {
await gg.equip.ensureTable();
if (!this.isValid) return;
gg.equip.composeReadyFragments();
this._beginFill();
}
private _refreshBagList(): void {
this._bagList = gg.equip.getBagDisplayList();
const pageCount = this._bagList.length > 0
? Math.ceil(this._bagList.length / BAG_PAGE_SIZE)
: 1;
this._bagPageCount = pageCount;
if (this._bagPageIndex >= pageCount) {
this._bagPageIndex = Math.max(0, pageCount - 1);
}
this._renderBagPage();
}
/** 渲染当前页武器库格子 */
private _renderBagPage(): void {
this._ensureBagItemNodes();
const start = this._bagPageIndex * BAG_PAGE_SIZE;
for (let i = 0; i < this._bagItems.length; i++) {
const item = this._bagItems[i];
if (!item?.isValid) continue;
const data = this._bagList[start + i];
if (!data) {
item.active = false;
continue;
}
item.active = true;
const comp = item.getComponent(shenjiangItem) || item.addComponent(shenjiangItem);
comp.setData(data);
}
this._updateBagPageNav();
}
/** 隐藏指定武器格子的 newWeapon 角标 */
private _hideBagNewWeaponTag(equipId: number): void {
const id = Math.floor(Number(equipId) || 0);
if (id <= 0) return;
for (let i = 0; i < this._bagItems.length; i++) {
const comp = this._bagItems[i]?.getComponent(shenjiangItem);
if ((comp?.data?.equipId | 0) !== id) continue;
comp.hideNewWeaponTag();
}
}
/** 隐藏全部武器库格子 newWeapon 角标 */
private _hideAllBagNewWeaponTags(): void {
for (let i = 0; i < this._bagItems.length; i++) {
this._bagItems[i]?.getComponent(shenjiangItem)?.hideNewWeaponTag();
}
}
/** 保证 ui_content 内有足够格子节点 */
private _ensureBagItemNodes(): void {
const root = this.ui_content;
const tpl = this._bagItemTpl;
if (!root?.isValid || !tpl?.isValid) {
console.log(LOG, 'ui_content 或武器格子模板缺失');
return;
}
if (!this._bagItemBuilt) {
this._bagItems = [tpl];
this._bagItemBuilt = true;
}
const layout = root.getComponent(Layout);
if (layout) layout.enabled = false;
while (this._bagItems.length < BAG_PAGE_SIZE) {
const n = instantiate(tpl);
if (!n?.isValid || !root.isValid) break;
n.parent = root;
this._bagItems.push(n);
}
if (layout?.isValid) {
layout.enabled = true;
layout.updateLayout();
}
}
/** 刷新页码(翻页按钮始终显示,边界由点击逻辑拦截) */
private _updateBagPageNav(): void {
if (this.lb_page) {
this.lb_page.string = `${this._bagPageIndex + 1}/${this._bagPageCount}`;
}
}
/** 打开页:空格一次铺齐,预制体并行预取,分帧 instantiate 挂体 */
private _beginFill(): void {
this._stopFillFlow();
this._fillToken++;
this._fillAlive = true;
this._heroPrefabPending = 0;
this._fillHeroes = gg.equip.getNormalHeroList().slice().sort(this._sortHeroById);
this._heroCloneIndex = 0;
this._heroMetaIndex = 0;
this._bagFillIndex = 0;
for (let i = 0; i < this._heroItems.length; i++) {
const item = this._heroItems[i];
if (!item?.isValid) continue;
item['weaponPopped'] = false;
}
this._bagList = gg.equip.getBagDisplayList();
const pageCount = this._bagList.length > 0
? Math.ceil(this._bagList.length / BAG_PAGE_SIZE)
: 1;
this._bagPageCount = pageCount;
if (this._bagPageIndex >= pageCount) {
this._bagPageIndex = Math.max(0, pageCount - 1);
}
this._updateBagPageNav();
this._prefetchAllHeroPrefabs();
this._onFillPump();
}
/** 分帧泵:格子一次铺齐,缓存命中的神将体立刻挂上 */
private readonly _onFillPump = (): void => {
this._pumpScheduled = false;
if (!this._canFill()) return;
if (this._heroCloneIndex < this._fillHeroes.length) {
this._cloneHeroStep();
}
if (this._heroMetaIndex < this._heroCloneIndex) {
this._bindHeroMetaStep();
}
this._applyHeroGridLayout();
this._kickHeroMounts();
if (this._bagItems.length < BAG_PAGE_SIZE && this._bagFillIndex < BAG_PAGE_SIZE) {
this._cloneBagStep();
}
if (this._bagFillIndex < BAG_PAGE_SIZE) {
this._fillBagStep();
}
if (this._needFillPump()) this._schedulePump();
};
/** 是否还有打开页填充任务 */
private _needFillPump(): boolean {
if (this._heroCloneIndex < this._fillHeroes.length) return true;
if (this._heroMetaIndex < this._fillHeroes.length) return true;
if (this._bagFillIndex < BAG_PAGE_SIZE) return true;
if (this._heroPrefabPending > 0) return true;
const bundle = assetManager.getBundle(GBundle.Shenjiang);
for (let i = 0; i < this._fillHeroes.length; i++) {
const hero = this._fillHeroes[i];
const item = this._heroItems[i];
if (!hero || !item?.isValid) continue;
if ((Number(item['heroFilledId']) || 0) === hero.id) continue;
const full = this._heroPrefabPath(hero);
if (full && bundle?.get(full, Prefab)?.isValid) return true;
}
return false;
}
/** 打开页填充是否仍可写节点 */
private _canFill(): boolean {
return this._fillAlive && this.isValid && !!this.node?.isValid && this.node.activeInHierarchy;
}
/** 预约下一帧继续填充 */
private _schedulePump(): void {
if (!this._canFill() || this._pumpScheduled) return;
if (!this._flow) this._flow = new FrameFlowHost();
this._pumpScheduled = true;
this._flow.waitFrames(1, this._onFillPump);
}
/** 停掉打开页分帧 */
private _stopFillFlow(): void {
this._pumpScheduled = false;
this._heroPrefabPending = 0;
this._flow?.dispose();
this._flow = null;
}
/** 克隆空神将格(模板只作拷贝源,不挂体) */
private _cloneHeroStep(): void {
const target = this._fillHeroes.length;
const tpl = this._heroTpl;
const parent = this._heroRoot;
if (!tpl?.isValid || !parent?.isValid) {
this._heroCloneIndex = target;
return;
}
const layout = parent.getComponent(Layout);
if (layout) layout.enabled = false;
while (this._heroItems.length < target) {
const n = instantiate(tpl);
if (!n?.isValid || !parent.isValid) break;
n.parent = parent;
n.active = false;
this._hideHeroWeapon(n);
this._heroItems.push(n);
}
this._heroCloneIndex = Math.min(this._heroItems.length, target);
for (let i = target; i < this._heroItems.length; i++) {
const extra = this._heroItems[i];
if (extra?.isValid) extra.active = false;
}
}
/** 按预制体 Grid:4 列、横向贴齐、纵向 spacingY */
private _setupHeroGridLayout(): void {
const root = this._heroRoot;
if (!root?.isValid) return;
const layout = root.getComponent(Layout);
if (!layout) {
console.log(LOG, 'shenjiang 缺少 Layout');
return;
}
layout.type = Layout.Type.GRID;
layout.constraint = Layout.Constraint.FIXED_COL;
layout.constraintNum = 4;
layout.enabled = true;
}
/** 格子显隐变化后刷新神将网格 */
private _applyHeroGridLayout(): void {
const layout = this._heroRoot?.getComponent(Layout);
if (!layout?.isValid) return;
layout.enabled = true;
layout.updateLayout();
}
/** 先写 Hero.id 和武器名条,武器 Spine 延后 */
private _bindHeroMetaStep(): void {
const end = this._heroCloneIndex;
for (let i = this._heroMetaIndex; i < end; i++) {
const item = this._heroItems[i];
const hero = this._fillHeroes[i];
if (!item?.isValid || !hero) continue;
item.active = true;
this._setHeroWearMask(item, false);
item['heroId'] = hero.id;
this._applyHeroWeaponNameBar(item, hero.id);
this._hideHeroWeapon(item);
}
this._heroMetaIndex = end;
}
/** 克隆一批武器库格 */
private _cloneBagStep(): void {
const root = this.ui_content;
const tpl = this._bagItemTpl;
if (!root?.isValid || !tpl?.isValid) {
console.log(LOG, 'ui_content 或武器格子模板缺失');
this._bagFillIndex = BAG_PAGE_SIZE;
return;
}
if (!this._bagItemBuilt) {
this._bagItems = [tpl];
this._bagItemBuilt = true;
}
const want = BAG_PAGE_SIZE;
const layout = root.getComponent(Layout);
if (layout) layout.enabled = false;
while (this._bagItems.length < want) {
const n = instantiate(tpl);
if (!n?.isValid || !root.isValid) break;
n.parent = root;
this._bagItems.push(n);
}
if (layout?.isValid) {
layout.enabled = true;
layout.updateLayout();
}
}
/** 填充一批当前页武器库格 */
private _fillBagStep(): void {
const start = this._bagPageIndex * BAG_PAGE_SIZE;
const end = BAG_PAGE_SIZE;
for (let i = this._bagFillIndex; i < end; i++) {
const item = this._bagItems[i];
if (!item?.isValid) continue;
const data = this._bagList[start + i];
if (!data) {
item.active = false;
continue;
}
item.active = true;
const comp = item.getComponent(shenjiangItem) || item.addComponent(shenjiangItem);
comp.setData(data);
}
this._bagFillIndex = end;
this._updateBagPageNav();
}
/** 缓存里有预制体则立刻挂上 */
private _kickHeroMounts(): void {
if (!this._canFill()) return;
for (let i = 0; i < this._heroMetaIndex; i++) {
const item = this._heroItems[i];
const hero = this._fillHeroes[i];
if (!item?.isValid || !hero) continue;
this._tryMountHeroFromCache(item, hero);
}
}
/**
* 从已加载预制体挂神将体;未就绪返回 false。
* @param item 神将格
* @param hero 英雄表
*/
private _tryMountHeroFromCache(item: Node, hero: ITableHero): boolean {
const slot = item.getChildByName('hero');
if (!slot?.isValid || !slot.children) return false;
const kids = slot.children;
if ((Number(item['heroFilledId']) || 0) === hero.id && kids && kids.length > 0) {
if (!item['weaponPopped']) {
this._applyHeroWeaponNode(item, hero.id);
this._playHeroWeaponPop(item);
item['weaponPopped'] = true;
}
return false;
}
const full = this._heroPrefabPath(hero);
if (!full) return false;
const bundle = assetManager.getBundle(GBundle.Shenjiang);
const prefab = bundle?.get(full, Prefab);
if (!prefab?.isValid) return false;
const body = instantiate(prefab);
if (!body?.isValid || !this._canFill() || !slot.isValid) {
this._freeDisplay(body);
return false;
}
this._disableLogicScripts(body);
this._mountDisplay(slot, body);
if (!body.isValid || !slot.isValid) return false;
this._applySpine(body, '', '待机');
item['heroFilledId'] = hero.id;
this._applyHeroWeaponNode(item, hero.id);
this._playHeroWeaponPop(item);
item['weaponPopped'] = true;
return true;
}
/** shenjiang/prefab/{Hero.res} */
private _heroPrefabPath(hero: ITableHero): string {
const resName = String(hero.res || hero.name || '').trim();
if (!resName) {
console.log(LOG, 'Hero.res 为空', hero.id);
return '';
}
return `${GPath.UIPrefab}${resName}`;
}
/** 打开页前把 shenjiang/prefab 打进缓存,避免打开后再排队 */
private _warmHeroPrefabs(): void {
const existed = assetManager.getBundle(GBundle.Shenjiang);
if (existed) {
existed.loadDir('prefab', Prefab, this._onWarmPrefabDir);
return;
}
assetManager.loadBundle(GBundle.Shenjiang, this._onWarmBundle);
}
/** 预热时分包就绪 */
private readonly _onWarmBundle = (err: Error | null, bundle: AssetManager.Bundle | null): void => {
if (err || !bundle) {
if (err) console.error(LOG, 'bundle 加载失败', GBundle.Shenjiang, String(err));
return;
}
if (!this.isValid) return;
bundle.loadDir('prefab', Prefab, this._onWarmPrefabDir);
};
/** 预热目录结束,若页面已在填则立刻挂体 */
private readonly _onWarmPrefabDir = (err: Error | null): void => {
if (err) console.error(LOG, '神将预制体预热失败', String(err));
if (!this._canFill()) return;
this._kickHeroMounts();
if (this._needFillPump()) this._schedulePump();
};
/** 并行预取全部神将展示预制体,不 instantiate */
private _prefetchAllHeroPrefabs(): void {
const existed = assetManager.getBundle(GBundle.Shenjiang);
if (existed) {
this._prefetchPrefabsInBundle(existed);
return;
}
this._heroPrefabPending++;
assetManager.loadBundle(GBundle.Shenjiang, this._onShenjiangBundle);
}
/** shenjiang 分包就绪后继续预取预制体 */
private readonly _onShenjiangBundle = (err: Error | null, bundle: AssetManager.Bundle | null): void => {
if (this._heroPrefabPending > 0) this._heroPrefabPending--;
if (err) console.error(LOG, 'bundle 加载失败', GBundle.Shenjiang, String(err));
if (err || !bundle || !this._canFill()) return;
this._prefetchPrefabsInBundle(bundle);
this._kickHeroMounts();
if (this._needFillPump()) this._schedulePump();
};
/**
* 在已加载分包内预取神将预制体。
* @param bundle shenjiang
*/
private _prefetchPrefabsInBundle(bundle: AssetManager.Bundle): void {
for (let i = 0; i < this._fillHeroes.length; i++) {
const full = this._heroPrefabPath(this._fillHeroes[i]);
if (!full) continue;
if (bundle.get(full, Prefab)?.isValid) continue;
this._heroPrefabPending++;
bundle.load(full, Prefab, this._onPrefetchAsset);
}
}
/** 单个预制体预取结束,立刻尝试挂体 */
private readonly _onPrefetchAsset = (err: Error | null): void => {
if (this._heroPrefabPending > 0) this._heroPrefabPending--;
if (err) console.error(LOG, '预制体预取失败', String(err));
if (!this._canFill()) return;
this._kickHeroMounts();
if (this._needFillPump()) this._schedulePump();
};
/** 隐藏神将武器,待冒出 */
private _hideHeroWeapon(item: Node): void {
const weaponNode = item.getChildByName('weaponNode');
if (!weaponNode?.isValid) return;
Tween.stopAllByTarget(weaponNode);
const uo = weaponNode.getComponent(UIOpacity);
if (uo) Tween.stopAllByTarget(uo);
weaponNode.active = false;
}
/** 已展示武器:渐显 + backOut 回弹冒出 */
private _playHeroWeaponPop(item: Node): void {
const weaponNode = item.getChildByName('weaponNode');
if (!weaponNode?.isValid || !weaponNode.active) return;
Tween.stopAllByTarget(weaponNode);
const uo = weaponNode.getComponent(UIOpacity) || weaponNode.addComponent(UIOpacity);
Tween.stopAllByTarget(uo);
uo.opacity = 0;
weaponNode.angle = -14;
weaponNode.setScale(WEAPON_POP_SCALE_FROM, WEAPON_POP_SCALE_FROM, 1);
tween(uo).to(WEAPON_POP_FADE_SEC, { opacity: 255 }).start();
tween(weaponNode).to(WEAPON_POP_SCALE_SEC, { scale: new Vec3(1, 1, 1), angle: 0 }, { easing: 'backOut' }).start();
}
/** 关闭页时停掉全部武器冒出 */
private _stopAllWeaponPops(): void {
for (let i = 0; i < this._heroItems.length; i++) {
const item = this._heroItems[i];
if (item?.isValid) this._hideHeroWeapon(item);
}
}
/** 神将网格按 Hero.id 升序 */
private _sortHeroById(a: ITableHero, b: ITableHero): number {
return a.id - b.id;
}
/**
* 穿戴武器名条:nameBg=icons/weapon/武器名字底0~4(未穿成品为 0)
* name=icons/weapon/{武器名};未穿成品显示 icons/weapon/无
*/
private _applyHeroWeaponNameBar(item: Node, heroId: number): void {
const nameBg = item.getChildByName('nameBg');
if (!nameBg?.isValid) {
console.log(LOG, 'nameBg 缺失', item.name);
return;
}
nameBg.active = true;
const worn = gg.equip.getHeroWearEquip(heroId);
const bgIndex = worn ? this._weaponNameBgIndex(Number(worn.quality), heroId, worn.id) : 0;
this._setWeaponIconSprite(nameBg, `weapon/武器名字底${bgIndex}`);
const nameNode = nameBg.getChildByName('name');
if (!worn) {
this._setWeaponIconSprite(nameNode, WEAPON_NAME_EMPTY_SPRITE);
this._applyHeroWeaponNameScale(nameNode, '');
return;
}
const weaponName = String(worn.name ?? '').trim();
if (!weaponName) {
console.log(LOG, '武器名缺失', heroId, worn.id);
this._clearWeaponIconSprite(nameNode);
this._applyHeroWeaponNameScale(nameNode, '');
return;
}
if (nameNode?.isValid) {
this._setWeaponIconSprite(nameNode, `weapon/${weaponName}`);
this._applyHeroWeaponNameScale(nameNode, weaponName);
}
}
/** nameBg 内 name 节点:不少于 5 字缩放 0.8,否则 1 */
private _applyHeroWeaponNameScale(nameNode: Node | null, weaponName: string): void {
if (!nameNode?.isValid) return;
const scale = weaponName.length >= WEAPON_NAME_LONG_LEN
? HERO_NAMEBG_NAME_SCALE_LONG
: HERO_NAMEBG_NAME_SCALE_NORMAL;
nameNode.setScale(scale, scale, 1);
}
/**
* weaponNode:按 Weapon.type 显隐 type1~4,Spine 皮肤为当前展示武器 skin
* 未穿成品时武器 id 为 0(同 getHeroDisplayWeaponId)
*/
private _applyHeroWeaponNode(item: Node, heroId: number): void {
const weaponNode = item.getChildByName('weaponNode');
if (!weaponNode?.isValid) {
console.log(LOG, 'weaponNode 缺失', item.name);
return;
}
const equipId = gg.equip.getHeroDisplayWeaponId(heroId);
const weapon = equipId > 0 ? gg.equip.getEquip(equipId) : null;
if (!weapon) {
weaponNode.active = false;
item['weaponDisplay'] = null;
return;
}
const weaponType = Number(weapon.type) | 0;
const skin = String(weapon.skin ?? '').trim();
if (weaponType < 1 || weaponType > 4) {
console.log(LOG, '展示武器 type 无效', heroId, equipId, weaponType);
weaponNode.active = false;
item['weaponDisplay'] = null;
return;
}
if (!skin) console.log(LOG, '展示武器 skin 缺失', heroId, equipId);
weaponNode.active = true;
const uo = weaponNode.getComponent(UIOpacity) || weaponNode.addComponent(UIOpacity);
uo.opacity = 0;
weaponNode.setScale(WEAPON_POP_SCALE_FROM, WEAPON_POP_SCALE_FROM, 1);
const shown = this._applyWeaponSpineGroup(weaponNode, weaponType, skin);
item['weaponDisplay'] = shown;
}
/** 按武器类型显隐 type1~4,并设置 Spine 皮肤;返回当前展示的 type 节点 */
private _applyWeaponSpineGroup(parent: Node, weaponType: number, skin: string): Node | null {
let shown: Node | null = null;
for (let t = 1; t <= 4; t++) {
const typeNode = parent.getChildByName(`type${t}`);
if (!typeNode) continue;
const active = t === weaponType;
typeNode.active = active;
if (!active) continue;
shown = typeNode;
this._applyWeaponSpine(typeNode, skin);
}
if (!shown) console.log(LOG, 'type 节点缺失', parent.name, weaponType);
return shown;
}
/** 设置单个武器 Spine 皮肤与待机 */
private _applyWeaponSpine(typeNode: Node, skin: string): void {
const skel = this._findSkeleton(typeNode);
if (!skel) {
console.log(LOG, '武器 Spine 缺失', typeNode.name);
return;
}
this._applySpine(typeNode, skin, '待机');
}
/** 已穿戴成品品质 → 武器名字底 0~4(白/绿/蓝/紫/橙) */
private _weaponNameBgIndex(quality: number, heroId: number, equipId: number): number {
const q = Number(quality) | 0;
if (q < 0 || q > 4) {
console.log(LOG, '已穿戴武器品质无效', heroId, equipId, q);
}
return q;
}
/** icons/weapon 下图 */
private _setWeaponIconSprite(node: Node | null, path: string, onLoaded?: () => void): void {
if (!node?.isValid) return;
if (!path) {
this._clearWeaponIconSprite(node);
return;
}
node.active = true;
if (!node.getComponent(Sprite)) node.addComponent(Sprite);
node.getOrAddComponent(SpriteLoad).setSprite(path, GBundle.Items, onLoaded ?? null);
}
/** 清除武器名图(未穿戴) */
private _clearWeaponIconSprite(node: Node | null): void {
if (!node?.isValid) return;
node.active = false;
node.getComponent(SpriteLoad)?.clear();
const sp = node.getComponent(Sprite);
if (sp) sp.spriteFrame = null;
}
private _mountDisplay(slot: Node, child: Node, clear = true): void {
if (!this._canFill() || !slot?.isValid || !child?.isValid || !slot.children) {
this._freeDisplay(child);
return;
}
if (clear) this._clearHeroSlot(slot);
if (!this._canFill() || !slot.isValid || !child.isValid || !slot.children) {
this._freeDisplay(child);
return;
}
child.parent = slot;
this._setLayerRecursive(child, slot.layer);
child.setPosition(0, 0, 0);
child.setScale(1, 1, 1);
child.angle = 0;
child.active = true;
}
private _clearHeroSlot(slot: Node): void {
if (!slot?.isValid) return;
const kids = slot.children;
if (!kids || kids.length <= 0) return;
const copy: Node[] = [];
for (let i = 0; i < kids.length; i++) copy.push(kids[i]);
for (let i = 0; i < copy.length; i++) this._freeDisplay(copy[i]);
}
private _freeDisplay(node: Node | null | undefined): void {
if (!node?.isValid) return;
if (node.parent) node.removeFromParent();
if (node.isValid) node.destroy();
}
private _disableLogicScripts(node: Node): void {
if (!node?.isValid) return;
const comps = node.getComponents(Component);
for (let i = 0; i < comps.length; i++) {
const c = comps[i];
if (c instanceof UITransform || c instanceof Sprite || c instanceof sp.Skeleton) continue;
if (c instanceof SpriteLoad || c instanceof UIOpacity) continue;
c.enabled = false;
}
const kids = node.children;
if (!kids) return;
for (let i = 0; i < kids.length; i++) {
this._disableLogicScripts(kids[i]);
}
}
/** 拖拽幽灵不参与点击 */
private _ghostNoHit(): boolean {
return false;
}
private _findSkeleton(node: Node | null | undefined): sp.Skeleton | null {
if (!node?.isValid) return null;
return node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton) ?? null;
}
private _applySpine(node: Node, skin: string, anim: string): void {
const skel = this._findSkeleton(node);
if (!skel) return;
if (skel.skeletonData) ensureSpineSkeletonDataRuntime(skel.skeletonData);
if (skin) {
try {
skel.setSkin(skin);
skel.setSlotsToSetupPose();
} catch (e) {
console.log(LOG, `setSkin failed "${skin}"`, String(e));
}
}
const clip = anim || '待机';
if (!clip) return;
try {
skel.setAnimation(0, clip, true);
} catch (e) {
console.log(LOG, `setAnimation failed "${clip}"`, String(e));
}
}
private _setLayerRecursive(node: Node, layer: number): void {
if (!node?.isValid) return;
node.layer = layer;
const kids = node.children;
if (!kids) return;
for (let i = 0; i < kids.length; i++) {
const child = kids[i];
if (child?.isValid) this._setLayerRecursive(child, layer);
}
}
}