//********************* // create by 流云 // time: Wed Oct 07 2026 // desc: 宝石镶嵌 / 页签仓库 / 一键合成 //********************* import { _decorator, EventMouse, EventTouch, Input, input, instantiate, Node, sp, Sprite, UIOpacity, UITransform, Vec2, Vec3 } from 'cc'; import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; import { GemGrid } from '../../com/GemGrid'; import { FrameFlowHost } from '../../game/FrameFlowHost'; import { GBundle, UIID } from '../../game/ConfigRes'; import { GEM_SLOT_COUNT, IGemBagEntry } from '../../game/GameDataEnums'; import { Auto_PanelGemstone } from './Auto_PanelGemstone'; const { ccclass } = _decorator; const LOG = '[PanelGemstone]'; /** 按下后移动超过此值开始拖 */ const DRAG_THRESHOLD = 18; const GHOST_OPACITY = 180; const ELEMENT_COUNT = 5; const SLOT_NAMES = ['g1', 'g2', 'g3', 'g4']; const ELEMENT_SKINS = ['', '金', '木', '水', '火', '土']; /** 仓库每帧实例化数量 */ const BAG_PER_FRAME = 6; /** 默认页签:通用 Weapon.type=1 */ const DEFAULT_FILTER = 1; /** * 宝石页:五行镶嵌 + 页签仓库(无滚动)+ 拖拽镶嵌/卸下 + 一键合成。 * 页签 btn0~btn5 → type 1~6(通用/金/木/水/火/土),默认通用。 */ @ccclass('UIPanelGemstone') export class UIPanelGemstone extends Auto_PanelGemstone { /** 五行格子模板 */ private _itemTpl: Node | null = null; /** 仓库格子模板 */ private _bagTpl: Node | null = null; /** 已创建的五行格 */ private _elementItems: Node[] = []; /** 仓库动态格 */ private _bagItems: Node[] = []; /** 当前筛选:1 通用;2~6 金木水火土 */ private _filterType = DEFAULT_FILTER; /** 仓库数据 */ private _bagList: IGemBagEntry[] = []; /** 仓库分帧 */ private _flow: FrameFlowHost | null = null; /** 仓库填充代数 */ private _bagToken = 0; /** 已分帧填充下标 */ private _bagFillIndex = 0; private _pressing = false; private _dragging = false; private _dragStart = new Vec2(); private _hitPos = new Vec2(); private _ghostPos = new Vec3(); private _ghost: Node | null = null; private _dragListening = false; /** 拖中的宝石 id */ private _dragGemId = 0; /** 从槽拖出:五行 1~5;0=仓库 */ private _dragFromElement = 0; /** 从槽拖出:槽下标 0~3;-1=仓库 */ private _dragFromSlot = -1; onLoad(): void { super.onLoad(); this._itemTpl = this.ui_list?.getChildByName('itemTemplate') ?? null; if (this._itemTpl) this._itemTpl.active = false; this._bagTpl = this.ui_content?.getChildByName('bagItemTemplate') ?? null; if (this._bagTpl) this._bagTpl.active = false; this._bindDrag(true); } onEnable(): void { super.onEnable(); this._bindDrag(true); } onDisable(): void { this._bindDrag(false); this._endDrag(); this._stopBagFill(); super.onDisable(); } onInit(): void { this._filterType = DEFAULT_FILTER; this._selectTag(this.btn0); void this._refresh(); } click_btnClose(): void { void this.close(2); } click_btn0(): void { this._onTag(1, this.btn0); } click_btn1(): void { this._onTag(2, this.btn1); } click_btn2(): void { this._onTag(3, this.btn2); } click_btn3(): void { this._onTag(4, this.btn3); } click_btn4(): void { this._onTag(5, this.btn4); } click_btn5(): void { this._onTag(6, this.btn5); } click_btnFuse(): void { const gains = gg.gem.fuseAll(); this._rebuildBag(); this._refreshAllSlots(); if (!gains.length) { gg.ui.showToast('没有可合成的宝石'); return; } void gg.ui.openPanel(UIID.PopGetGem, { data: gains }); } /** 页签切换 */ private _onTag(filterType: number, btn: Node): void { if (this._filterType === filterType) return; this._filterType = filterType; this._selectTag(btn); this._rebuildBag(); } /** 标签底移到当前按钮 */ private _selectTag(btn: Node | null): void { if (!this.ui_tagBtnBg?.isValid || !btn?.isValid) return; this.ui_tagBtnBg.setWorldPosition(btn.worldPosition); } /** 全量刷新 */ private async _refresh(): Promise { await gg.gem.ensureTable(); if (!this.isValid) return; this._buildElements(); this._rebuildBag(); } /** 实例化五行 */ private _buildElements(): void { if (!this.ui_list?.isValid || !this._itemTpl?.isValid) { console.warn(LOG, 'ui_list/itemTemplate 缺失'); return; } for (let i = 0; i < this._elementItems.length; i++) { const n = this._elementItems[i]; if (n?.isValid) n.destroy(); } this._elementItems.length = 0; for (let e = 1; e <= ELEMENT_COUNT; e++) { const node = instantiate(this._itemTpl); node.parent = this.ui_list; node.active = true; node.name = `element_${e}`; (node as any)._element = e; this._applyNumSkin(node, e); this._refreshElementSlots(node, e); this._elementItems.push(node); } } /** numSkin 切五行皮肤 */ private _applyNumSkin(item: Node, element: number): void { const skinNode = item.getChildByName('numSkin'); const sk = skinNode?.getComponent(sp.Skeleton); if (!sk) { console.warn(LOG, 'numSkin Skeleton 缺失', element); return; } const skin = ELEMENT_SKINS[element] || ''; if (!skin) { console.warn(LOG, '五行皮肤名无效', element); return; } if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData); try { sk.setSkin(skin); sk.setSlotsToSetupPose(); if (sk.findAnimation('待机')) sk.setAnimation(0, '待机', true); } catch (err) { console.warn(LOG, 'setSkin 失败', skin, err); } } /** 刷新某一行五行的 4 槽 */ private _refreshElementSlots(item: Node, element: number): void { const slots = gg.gem.getElementSlots(element); for (let s = 0; s < GEM_SLOT_COUNT; s++) { const slotNode = item.getChildByName(SLOT_NAMES[s]); if (!slotNode?.isValid) { console.warn(LOG, '镶嵌位缺失', SLOT_NAMES[s], element); continue; } (slotNode as any)._element = element; (slotNode as any)._slot = s; this._paintSlot(slotNode, slots[s] || 0); } } /** 刷新全部镶嵌 */ private _refreshAllSlots(): void { for (let i = 0; i < this._elementItems.length; i++) { const item = this._elementItems[i]; const e = Number((item as any)?._element) || (i + 1); this._refreshElementSlots(item, e); } } /** 画单个镶嵌位 */ private _paintSlot(slotNode: Node, gemId: number): void { const bg = slotNode.getChildByName('bg'); const icon = slotNode.getChildByName('icon'); const conf = gemId > 0 ? gg.gem.getGem(gemId) : null; const quality = conf ? Math.floor(Number(conf.quality) || 0) : 0; this._setWeaponSprite(bg, `weapon/宝石框小${Math.max(0, Math.min(5, quality))}`); const iconName = conf ? String(conf.icon || '').trim() : ''; if (conf && !iconName) console.warn(LOG, 'icon 缺失', gemId); this._setWeaponSprite(icon, iconName ? `weapon/${iconName}` : ''); if (icon?.isValid) icon.active = !!iconName; } /** * 重建当前页签仓库(打开 / 切页签 / 合成)。 * 镶嵌成败勿调用。 */ private _rebuildBag(): void { if (!this.ui_content?.isValid || !this._bagTpl?.isValid) { console.warn(LOG, 'ui_content/bagItemTemplate 缺失'); return; } this._stopBagFill(); for (let i = 0; i < this._bagItems.length; i++) { const n = this._bagItems[i]; if (n?.isValid) n.destroy(); } this._bagItems.length = 0; this._bagList = gg.gem.getBagList(this._filterType); this._bagFillIndex = 0; this._bagToken++; this._pumpBagFill(this._bagToken); } /** 停掉仓库分帧 */ private _stopBagFill(): void { this._bagToken++; this._flow?.dispose(); this._flow = null; } /** 分帧填充仓库格 */ private _pumpBagFill(token: number): void { if (!this.isValid || token !== this._bagToken) return; if (!this.ui_content?.isValid || !this._bagTpl?.isValid) return; const end = Math.min(this._bagFillIndex + BAG_PER_FRAME, this._bagList.length); for (let i = this._bagFillIndex; i < end; i++) { this._bagItems.push(this._spawnBagItem(this._bagList[i])); } this._bagFillIndex = end; if (this._bagFillIndex >= this._bagList.length) return; if (!this._flow) this._flow = new FrameFlowHost(); this._flow.waitFrames(1, () => this._pumpBagFill(token)); } /** 创建一格仓库项 */ private _spawnBagItem(entry: IGemBagEntry): Node { const node = instantiate(this._bagTpl!); node.parent = this.ui_content; node.active = true; const grid = node.getComponent(GemGrid) || node.addComponent(GemGrid); grid.setData(gg.gem.getGem(entry.gemId), entry.count); (node as any)._gemId = entry.gemId; return node; } /** * 镶嵌/卸下后局部同步仓库数量。 * @param gemIds 涉及的宝石 id */ private _patchBagGems(gemIds: readonly number[]): void { for (let i = 0; i < gemIds.length; i++) { const gemId = Math.floor(Number(gemIds[i]) || 0); if (gemId <= 0) continue; const free = gg.gem.getFreeCount(gemId); const conf = gg.gem.getGem(gemId); const matchFilter = this._matchFilter(conf); const idx = this._findBagItemIndex(gemId); if (free <= 0 || !matchFilter) { if (idx >= 0) this._removeBagItemAt(idx); continue; } if (idx >= 0) { const node = this._bagItems[idx]; node.getComponent(GemGrid)?.setData(conf, free); this._bagList[idx] = { gemId, count: free }; continue; } const entry: IGemBagEntry = { gemId, count: free }; this._bagList.push(entry); this._bagItems.push(this._spawnBagItem(entry)); } } /** 当前页签是否应展示该宝石 */ private _matchFilter(conf: ITableWeapon | null): boolean { if (!conf) return false; return Math.floor(Number(conf.type) || 0) === this._filterType; } /** 查找仓库格下标 */ private _findBagItemIndex(gemId: number): number { for (let i = 0; i < this._bagItems.length; i++) { if (Number((this._bagItems[i] as any)._gemId) === gemId) return i; } return -1; } /** 移除一格仓库 */ private _removeBagItemAt(idx: number): void { const node = this._bagItems[idx]; if (node?.isValid) node.destroy(); this._bagItems.splice(idx, 1); this._bagList.splice(idx, 1); } /** icons/weapon */ private _setWeaponSprite(node: Node | null, path: string): void { if (!node?.isValid) return; if (!path) { node.getComponent(SpriteLoad)?.clear(); const spComp = node.getComponent(Sprite); if (spComp) spComp.spriteFrame = null; return; } if (!node.getComponent(Sprite)) node.addComponent(Sprite); node.getOrAddComponent(SpriteLoad).setSprite(path, GBundle.Items); } // ---------- 拖拽镶嵌 ---------- private _bindDrag(on: boolean): void { if (this._dragListening === on) return; this._dragListening = on; if (on) { this.node.on(Node.EventType.TOUCH_START, this._onTouchStart, this, true); this.node.on(Node.EventType.TOUCH_MOVE, this._onTouchMove, this, true); this.node.on(Node.EventType.TOUCH_END, this._onTouchEnd, this, true); this.node.on(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this, true); input.on(Input.EventType.MOUSE_DOWN, this._onMouseDown, this); input.on(Input.EventType.MOUSE_MOVE, this._onMouseMove, this); input.on(Input.EventType.MOUSE_UP, this._onMouseUp, this); } else { this.node.off(Node.EventType.TOUCH_START, this._onTouchStart, this, true); this.node.off(Node.EventType.TOUCH_MOVE, this._onTouchMove, this, true); this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this, true); this.node.off(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this, true); input.off(Input.EventType.MOUSE_DOWN, this._onMouseDown, this); input.off(Input.EventType.MOUSE_MOVE, this._onMouseMove, this); input.off(Input.EventType.MOUSE_UP, this._onMouseUp, this); } } private _onTouchStart(e: EventTouch): void { this._pointerStart(e.getUILocation()); } private _onTouchMove(e: EventTouch): void { this._pointerMove(e.getUILocation()); } private _onTouchEnd(e: EventTouch): void { this._pointerEnd(e.getUILocation()); } private _onMouseDown(e: EventMouse): void { if (e.getButton() !== EventMouse.BUTTON_LEFT) return; this._pointerStart(e.getUILocation()); } private _onMouseMove(e: EventMouse): void { this._pointerMove(e.getUILocation()); } private _onMouseUp(e: EventMouse): void { this._pointerEnd(e.getUILocation()); } private _pointerStart(loc: Vec2): void { if (this._pressing || this._dragging) return; const bag = this._hitBag(loc); if (bag) { const gemId = Number((bag as any)._gemId) || 0; if (gemId <= 0) return; this._resetDrag(); this._pressing = true; this._dragStart.set(loc); this._dragGemId = gemId; this._dragFromElement = 0; this._dragFromSlot = -1; return; } const slot = this._hitSlot(loc); if (!slot) return; const element = Number((slot as any)._element) || 0; const si = Number((slot as any)._slot); if (element <= 0 || si < 0) return; const gemId = gg.gem.getSlotGem(element, si); if (gemId <= 0) return; this._resetDrag(); this._pressing = true; this._dragStart.set(loc); this._dragGemId = gemId; this._dragFromElement = element; this._dragFromSlot = si; } private _pointerMove(loc: Vec2): void { if (this._dragging) { this._moveGhost(loc); return; } if (!this._pressing || this._dragGemId <= 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; this._beginDrag(loc); } private _pointerEnd(loc: Vec2): void { if (!this._pressing && !this._dragging) return; if (this._dragging) { this._finishDrag(loc); return; } // 镶嵌槽轻点看信息;仓库格由 GemGrid 自行处理 const gemId = this._dragGemId; const fromSlot = this._dragFromElement > 0 && this._dragFromSlot >= 0; this._endDrag(); if (fromSlot && gemId > 0) GemGrid.openInfo(gemId); } private _beginDrag(loc: Vec2): void { this._dragging = true; this._spawnGhost(loc); this._moveGhost(loc); } private _finishDrag(loc: Vec2): void { const gemId = this._dragGemId; const fromE = this._dragFromElement; const fromS = this._dragFromSlot; const toSlot = this._hitSlot(loc); const toBag = this._hitBagArea(loc); this._endDrag(); if (gemId <= 0) return; if (toSlot) { const te = Number((toSlot as any)._element) || 0; const ts = Number((toSlot as any)._slot); if (te <= 0 || ts < 0) return; if (fromE > 0 && fromS >= 0) { if (fromE === te && fromS === ts) return; if (!gg.gem.swapSlots(fromE, fromS, te, ts)) { gg.ui.showToast('无法镶嵌到该位置'); return; } this._refreshAllSlots(); return; } const before = gg.gem.getSlotGem(te, ts); if (!gg.gem.wearFromBag(te, ts, gemId)) { gg.ui.showToast('无法镶嵌到该位置'); return; } this._refreshAllSlots(); const patch = [gemId]; if (before > 0 && before !== gemId) patch.push(before); this._patchBagGems(patch); return; } if (fromE > 0 && fromS >= 0 && toBag) { const worn = gg.gem.getSlotGem(fromE, fromS); if (worn <= 0) return; if (!gg.gem.unequip(fromE, fromS)) return; this._refreshAllSlots(); this._patchBagGems([worn]); } } private _spawnGhost(loc: Vec2): void { this._clearGhost(); const tpl = this._bagTpl; if (!tpl?.isValid) return; const conf = gg.gem.getGem(this._dragGemId); if (!conf) return; const ghost = instantiate(tpl); ghost.parent = this.node; ghost.active = true; const grid = ghost.getComponent(GemGrid) || ghost.addComponent(GemGrid); grid.canClick = false; grid.setData(conf, -1); const op = ghost.getComponent(UIOpacity) || ghost.addComponent(UIOpacity); op.opacity = GHOST_OPACITY; const ut = ghost.getComponent(UITransform); if (ut) ut.hitTest = () => false; this._ghost = ghost; this._moveGhost(loc); } private _moveGhost(loc: Vec2): void { if (this._ghost?.isValid) { this._ghost.setWorldPosition(this._ghostPos.set(loc.x, loc.y, 0)); } } private _clearGhost(): void { if (this._ghost?.isValid) this._ghost.destroy(); this._ghost = null; } private _endDrag(): void { this._clearGhost(); this._resetDrag(); } private _resetDrag(): void { this._pressing = false; this._dragging = false; this._dragGemId = 0; this._dragFromElement = 0; this._dragFromSlot = -1; } private _hitBag(loc: Vec2): Node | null { if (!this.ui_content?.isValid) return null; this._hitPos.set(loc.x, loc.y); for (let i = this._bagItems.length - 1; i >= 0; i--) { const child = this._bagItems[i]; if (!child?.activeInHierarchy) continue; const ut = child.getComponent(UITransform); if (ut?.getBoundingBoxToWorld().contains(this._hitPos)) return child; } return null; } private _hitSlot(loc: Vec2): Node | null { this._hitPos.set(loc.x, loc.y); for (let i = 0; i < this._elementItems.length; i++) { const item = this._elementItems[i]; if (!item?.activeInHierarchy) continue; for (let s = 0; s < SLOT_NAMES.length; s++) { const slot = item.getChildByName(SLOT_NAMES[s]); if (!slot?.activeInHierarchy) continue; const ut = slot.getComponent(UITransform); if (ut?.getBoundingBoxToWorld().contains(this._hitPos)) return slot; } } return null; } /** 是否落在仓库区(卸下) */ private _hitBagArea(loc: Vec2): boolean { const root = this.ui_content; if (!root?.isValid) return false; this._hitPos.set(loc.x, loc.y); const ut = root.getComponent(UITransform); return !!ut?.getBoundingBoxToWorld().contains(this._hitPos); } }