//********************* // 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(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(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 { 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); } } }