//********************* // create by 流云 // time: Wed Oct 07 2026 // desc: 宝石镶嵌 / 页签仓库 / 一键合成 //********************* import { _decorator, EventMouse, EventTouch, Input, input, instantiate, Label, Node, find, 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 ELEMENT_SKINS = ['', '金', '木', '水', '火', '土']; /** 仓库每帧实例化数量 */ const BAG_PER_FRAME = 6; /** 默认页签:通用 Weapon.type=1 */ const DEFAULT_FILTER = 1; /** 默认展示五行:金 */ const DEFAULT_ELEMENT = 1; /** * 宝石页:左右切五行镶嵌 + 页签仓库 + 拖拽镶嵌/卸下 + 一键合成。 * 页签 btn0~btn5 → type 1~6(通用/金/木/水/火/土),默认通用。 */ @ccclass('UIPanelGemstone') export class UIPanelGemstone extends Auto_PanelGemstone { /** 镶嵌格模板 */ private _itemTpl: Node | null = null; /** 仓库格子模板 */ private _bagTpl: Node | null = null; /** 当前五行的 4 个镶嵌格 */ private _slotItems: Node[] = []; /** 仓库动态格 */ private _bagItems: Node[] = []; /** 当前展示五行 1~5 */ private _currentElement = DEFAULT_ELEMENT; /** 顶部五行数字 Spine */ private _skSkin: sp.Skeleton | null = null; /** 当前筛选: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._skSkin = find('ui_center/top/sk_skin', this.node)?.getComponent(sp.Skeleton) ?? null; 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._currentElement = DEFAULT_ELEMENT; this._selectTag(this.btn0); void this._refresh(); } click_btnClose(): void { void this.close(2); } click_btnLeft(): void { const next = this._currentElement <= 1 ? ELEMENT_COUNT : this._currentElement - 1; this._setElement(next); } click_btnRight(): void { const next = this._currentElement >= ELEMENT_COUNT ? 1 : this._currentElement + 1; this._setElement(next); } 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._refreshSlots(); 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._ensureSlotItems(); this._applyElementVisual(); this._refreshSlots(); this._rebuildBag(); } /** * 切换当前五行。 * @param element 1~5 */ private _setElement(element: number): void { const e = Math.floor(Number(element) || 0); if (e < 1 || e > ELEMENT_COUNT) { console.warn(LOG, '五行非法', element); return; } if (this._currentElement === e) return; this._currentElement = e; this._applyElementVisual(); this._refreshSlots(); } /** 标题图 + 数字皮肤 */ private _applyElementVisual(): void { this._applyTitle(); this._applySkin(); } /** sp_title 加载 title_1~5 */ private _applyTitle(): void { const spNode = this.sp_title?.node; if (!spNode?.isValid) { console.warn(LOG, 'sp_title 缺失'); return; } const path = `texture/title_${this._currentElement}`; if (!spNode.getComponent(Sprite)) spNode.addComponent(Sprite); spNode.getOrAddComponent(SpriteLoad).setSprite(path, GBundle.Gemstone); } /** sk_skin 切五行皮肤 */ private _applySkin(): void { const sk = this._skSkin; if (!sk?.isValid) { console.warn(LOG, 'sk_skin Skeleton 缺失'); return; } const skin = ELEMENT_SKINS[this._currentElement] || ''; if (!skin) { console.warn(LOG, '五行皮肤名无效', this._currentElement); 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 _ensureSlotItems(): void { if (!this.ui_list?.isValid || !this._itemTpl?.isValid) { console.warn(LOG, 'ui_list/itemTemplate 缺失'); return; } if (this._slotItems.length === GEM_SLOT_COUNT) return; for (let i = 0; i < this._slotItems.length; i++) { const n = this._slotItems[i]; if (n?.isValid) n.destroy(); } this._slotItems.length = 0; for (let s = 0; s < GEM_SLOT_COUNT; s++) { const node = instantiate(this._itemTpl); node.parent = this.ui_list; node.active = true; node.name = `slot_${s}`; (node as any)._slot = s; this._slotItems.push(node); } } /** 刷新当前五行 4 槽 */ private _refreshSlots(): void { this._ensureSlotItems(); const slots = gg.gem.getElementSlots(this._currentElement); for (let s = 0; s < this._slotItems.length; s++) { const node = this._slotItems[s]; if (!node?.isValid) continue; (node as any)._element = this._currentElement; (node as any)._slot = s; this._paintSlot(node, slots[s] || 0); } } /** * 画单个镶嵌格:空位 qbg=宝石框小0、icon 空;有宝石写 qbg/icon/lbName。 * @param slotNode itemTemplate 实例 * @param gemId 宝石 id;0=空 */ private _paintSlot(slotNode: Node, gemId: number): void { const qbg = slotNode.getChildByName('qbg'); const icon = slotNode.getChildByName('icon'); const lbName = slotNode.getChildByName('lbName')?.getComponent(Label); const conf = gemId > 0 ? gg.gem.getGem(gemId) : null; if (!conf) { this._setWeaponSprite(qbg, 'weapon/宝石框小0'); this._setWeaponSprite(icon, ''); if (icon?.isValid) icon.active = false; if (lbName?.isValid) lbName.string = ''; return; } const quality = Math.floor(Number(conf.quality) || 0); this._setWeaponSprite(qbg, `weapon/宝石框小${Math.max(0, Math.min(5, quality))}`); const iconName = String(conf.icon || '').trim(); if (!iconName) console.warn(LOG, 'icon 缺失', gemId); this._setWeaponSprite(icon, iconName ? `weapon/${iconName}` : ''); if (icon?.isValid) icon.active = !!iconName; if (lbName?.isValid) lbName.string = String(conf.name ?? '').trim(); } /** * 重建当前页签仓库(打开 / 切页签 / 合成)。 * 镶嵌成败勿调用。 */ 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; } 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._refreshSlots(); return; } const before = gg.gem.getSlotGem(te, ts); if (!gg.gem.wearFromBag(te, ts, gemId)) { gg.ui.showToast('无法镶嵌到该位置'); return; } this._refreshSlots(); 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._refreshSlots(); 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._slotItems.length; i++) { const item = this._slotItems[i]; if (!item?.activeInHierarchy) continue; const ut = item.getComponent(UITransform); if (ut?.getBoundingBoxToWorld().contains(this._hitPos)) return item; } 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); } }