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/PanelGemstone/UIPanelGemstone.ts

604 lines
22 KiB

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