//********************* // create by 流云 // time: Tue Oct 13 2026 // desc: 局内 Battlebuff 三选一 //********************* import { _decorator, EventTouch, find, instantiate, Label, Layout, Node, RichText, sp, Tween, tween, UISkew, v3, Vec3 } from 'cc'; import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad'; import { Auto_PopSelect } from './Auto_PopSelect'; import { GAudioPath, GBundle, GPath, UIID } from '../../game/ConfigRes'; import { BattleBuffSelectService, type BattleBuffSelectOption, } from '../../manager/battleBuff/BattleBuffSelectService'; import { BattleBuffInGameRefresh } from '../../battle/combat/BattleBuffInGameRefresh'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; const { ccclass } = _decorator; /** 五行 target → 图标名 */ const TARGET_ICON: string[] = ['', '金', '木', '水', '火', '土']; /** Spine 皮肤 */ const SKIN_PURPLE = '紫色'; const SKIN_ORANGE = '橙色'; /** 伪 3D 翻牌半程(秒) */ const FLIP_HALF_SEC = 0.28; /** 翻牌透视斜切峰值(度) */ const FLIP_SKEW_X = 16; /** 翻牌厚度感:中间 scaleY 倍率 */ const FLIP_MID_SCALE_Y = 1.12; /** 三张牌依次翻开间隔(秒) */ const FLIP_STAGGER_SEC = 0.08; /** 翻开后回弹峰值倍率 */ const REVEAL_POP_SCALE = 1.08; /** 回弹半程(秒) */ const REVEAL_POP_HALF_SEC = 0.1; @ccclass('UIPopSelect') export class UIPopSelect extends Auto_PopSelect { /** 本机 playerId */ private _playerId = 0; /** itemTemplate 原型 */ private _itemTemplate: Node | null = null; /** 已实例化选项节点 */ private readonly _itemNodes: Node[] = []; /** 与 _itemNodes 对齐的 buffId */ private readonly _itemBuffIds: number[] = []; /** 选项模板基准缩放 */ private readonly _homeScale = new Vec3(1, 1, 1); /** 翻牌进行中,禁止点选/刷新 */ private _busy = false; /** 翻牌代数(取消过期回调) */ private _flipGen = 0; /** 当前阶段待完成张数 */ private _flipWait = 0; /** 刷新翻牌待写入的选项 */ private _pendingOptions: BattleBuffSelectOption[] | null = null; onInit(): void { const battle = gg.game.CurentBattle as ProjectBattleHandle | null; this._playerId = battle?.localPlayerId ?? 0; if (!this.lb_haveCount) { this.lb_haveCount = find('ui_center/btnRefresh/lb_haveCount', this.node)?.getComponent(Label) ?? null; } this._cacheTemplate(); BattleBuffSelectService.ensurePlayer(this._playerId); const options = BattleBuffSelectService.beginPopup(this._playerId); this._applyOptions(options, true); this._refreshCountUi(); } click_btnRefresh(): void { if (this._busy) return; if (!BattleBuffSelectService.canRefresh(this._playerId)) { gg.ui.showToast('刷新次数已用完'); return; } if (BattleBuffSelectService.shouldUseFreeRefresh(this._playerId)) { const next = BattleBuffSelectService.refreshOptions(this._playerId, false); if (next) { this._applyOptions(next, true); this._refreshCountUi(); } return; } gg.sdk.showVideoAd((res) => { if (!res) { gg.ui.showToast('观看视频失败'); return; } if (!this.node?.isValid) return; const next = BattleBuffSelectService.refreshOptions(this._playerId, true); if (next) { this._applyOptions(next, true); this._refreshCountUi(); } }); } private _cacheTemplate(): void { if (this._itemTemplate) return; const list = this.ui_itemList; if (!list) { console.warn('[UIPopSelect] 缺少 ui_itemList'); return; } const tpl = list.getChildByName('itemTemplate') ?? list.getChildByName('ui_itemTemplate'); if (!tpl) { console.warn('[UIPopSelect] 缺少 itemTemplate/ui_itemTemplate'); return; } this._itemTemplate = tpl; tpl.getScale(this._homeScale); tpl.active = false; } private _refreshCountUi(): void { const remain = BattleBuffSelectService.remainRefreshTotal(this._playerId); if (this.lb_haveCount) { this.lb_haveCount.string = `${remain}`; } if (this.btnRefresh) { this.btnRefresh.active = BattleBuffSelectService.canRefresh(this._playerId); this._syncRefreshAdIcon(); } } /** 有免费次数时隐藏广告视频标 */ private _syncRefreshAdIcon(): void { const adIcon = this.btnRefresh?.getChildByName('ad'); if (!adIcon?.isValid) return; adIcon.active = !BattleBuffSelectService.shouldUseFreeRefresh(this._playerId); } /** * 写入选项;withFlip 时走翻牌(首开仅展开,刷新先合再展)。 * @param options 选项 * @param withFlip 是否翻牌 */ private _applyOptions(options: BattleBuffSelectOption[], withFlip: boolean): void { if (!withFlip) { this._spawnOptions(options, false); return; } if (this._itemNodes.length > 0) { this._startRefreshFlip(options); return; } this._spawnOptions(options, true); this._startOpenFlip(); } /** * 实例化选项:先用满缩放让 Layout 排好位,再冻结;合拢态在排版后再设。 * @param options 选项 * @param startClosed 是否以合拢 scaleX=0 起步(翻牌用) */ private _spawnOptions(options: BattleBuffSelectOption[], startClosed: boolean): void { this._stopAllItemTweens(); this._clearItems(); const list = this.ui_itemList; const tpl = this._itemTemplate; if (!list || !tpl) return; const layout = list.getComponent(Layout); if (layout) layout.enabled = true; const hx = this._homeScale.x; const hy = this._homeScale.y; const hz = this._homeScale.z; for (let i = 0; i < options.length; i++) { const node = instantiate(tpl); node.active = true; node.name = `item_${i}`; list.addChild(node); this._itemNodes.push(node); this._bindItem(node, options[i]); node.setScale(hx, hy, hz); } // 必须在满缩放下排版;若先 scaleX=0 再冻 Layout,三张会叠在中心 this._freezeListLayout(); if (!startClosed) return; for (let i = 0; i < this._itemNodes.length; i++) { const node = this._itemNodes[i]; if (node?.isValid) node.setScale(0, hy * FLIP_MID_SCALE_Y, hz); } } /** 满尺寸排版一次后关闭 Layout,翻牌改 scale 不再重排 */ private _freezeListLayout(): void { const layout = this.ui_itemList?.getComponent(Layout); if (!layout) return; layout.updateLayout(); layout.enabled = false; } private _clearItems(): void { for (let i = 0; i < this._itemNodes.length; i++) { const n = this._itemNodes[i]; if (n?.isValid) { n.off(Node.EventType.TOUCH_END, this._onItemTouch, this, true); n.destroy(); } } this._itemNodes.length = 0; this._itemBuffIds.length = 0; } private _bindItem(node: Node, opt: BattleBuffSelectOption): void { const row = opt.row; const q = Math.floor(Number(opt.quality) || 0); this._setSprite(node.getChildByName('bg'), GPath.PingjiKuang(`b${q}`)); this._setSprite(node.getChildByName('quality'), GPath.PingjiKuang(`q${q}`)); const iconName = String(row.icon || row.name || '').trim(); if (iconName) { this._setSprite(node.getChildByName('icon'), GPath.ShopSkill(iconName)); } else { console.warn('[UIPopSelect] buff icon 为空', row.id); } const five = node.getChildByName('fiveIcon'); const target = Math.floor(Number(row.target) || 0); if (five) { if (target >= 1 && target <= 5) { five.active = true; this._setSprite(five, GPath.ElementAttr(TARGET_ICON[target])); } else { five.active = false; } } const lbName = node.getChildByName('lbName')?.getComponent(Label); if (lbName) lbName.string = String(row.name ?? ''); const rtDesc = node.getChildByName('rtDesc')?.getComponent(RichText); if (rtDesc) rtDesc.string = String(row.desc ?? ''); this._syncLight(node.getChildByName('light'), q); let idx = -1; for (let i = 0; i < this._itemNodes.length; i++) { if (this._itemNodes[i] === node) { idx = i; break; } } if (idx < 0) { console.warn('[UIPopSelect] bind 未找到节点'); return; } this._itemBuffIds[idx] = opt.id; node.off(Node.EventType.TOUCH_END, this._onItemTouch, this, true); node.on(Node.EventType.TOUCH_END, this._onItemTouch, this, true); } /** 选项点击(capture,子节点命中也能收到) */ private _onItemTouch(event: EventTouch): void { if (this._busy) return; const cur = event.currentTarget as Node; if (!cur?.isValid) return; for (let i = 0; i < this._itemNodes.length; i++) { if (this._itemNodes[i] === cur) { this._onPick(this._itemBuffIds[i]); return; } } } private _syncLight(light: Node | null, quality: number): void { if (!light) return; if (quality !== 3 && quality !== 4) { light.active = false; return; } light.active = true; const skel = light.getComponent(sp.Skeleton); if (!skel) { console.warn('[UIPopSelect] light 缺少 Skeleton'); return; } const skin = quality === 4 ? SKIN_ORANGE : SKIN_PURPLE; try { skel.setSkin(skin); skel.setAnimation(0, skel.animation || 'animation', true); } catch (e) { console.warn('[UIPopSelect] setSkin 失败', skin, e); } } private _setSprite(node: Node | null, path: string): void { if (!node) return; let sl = node.getComponent(SpriteLoad); if (!sl) { sl = node.addComponent(SpriteLoad); } sl.setSprite(path, GBundle.Items); } private _onPick(buffId: number): void { if (this._busy) return; const parsed = BattleBuffSelectService.confirmPick(this._playerId, buffId); if (!parsed) return; const battle = gg.game.CurentBattle as ProjectBattleHandle | null; if (battle) { BattleBuffInGameRefresh.afterPick(battle, this._playerId); } const battleUi = gg.ui.getPanelBase(UIID.BattleGame) as { refreshBattleBuffSkills?: () => void } | null; battleUi?.refreshBattleBuffSkills?.(); this.close(2); } /** 首开:三张依次翻开 */ private _startOpenFlip(): void { this._busy = true; this._flipGen += 1; const gen = this._flipGen; this._flipWait = this._itemNodes.length; if (this._flipWait <= 0) { this._busy = false; return; } for (let i = 0; i < this._itemNodes.length; i++) { this.scheduleOnce(this._onOpenDue.bind(this, i, gen), FLIP_STAGGER_SEC * i); } } /** * 刷新:先依次合拢,换内容后再依次翻开。 * @param options 新选项 */ private _startRefreshFlip(options: BattleBuffSelectOption[]): void { this._busy = true; this._pendingOptions = options; this._flipGen += 1; const gen = this._flipGen; this._flipWait = this._itemNodes.length; if (this._flipWait <= 0) { this._pendingOptions = null; this._spawnOptions(options, true); this._startOpenFlip(); return; } for (let i = 0; i < this._itemNodes.length; i++) { this.scheduleOnce(this._onRefreshCloseDue.bind(this, i, gen), FLIP_STAGGER_SEC * i); } } /** * @param index 下标 * @param gen 代数 */ private _onOpenDue(index: number, gen: number): void { if (gen !== this._flipGen || !this.isValid) return; this._playOpenHalf(this._itemNodes[index], gen); } /** * @param index 下标 * @param gen 代数 */ private _onRefreshCloseDue(index: number, gen: number): void { if (gen !== this._flipGen || !this.isValid) return; this._playCloseHalf(this._itemNodes[index], gen); } /** * 合拢半程:scaleX → 0。 * @param node 牌 * @param gen 代数 */ private _playCloseHalf(node: Node | null, gen: number): void { if (!node?.isValid) { this._onCloseHalfDone(gen); return; } const hy = this._homeScale.y; const hz = this._homeScale.z; const skew = this._ensureSkew(node); Tween.stopAllByTarget(node); Tween.stopAllByTarget(skew); skew.setSkew(0, 0); node.setScale(this._homeScale.x, hy, hz); tween(skew) .to(FLIP_HALF_SEC, { x: FLIP_SKEW_X }, { easing: 'sineIn' }) .start(); tween(node) .to(FLIP_HALF_SEC, { scale: v3(0, hy * FLIP_MID_SCALE_Y, hz) }, { easing: 'sineIn' }) .call(this._onCloseHalfDone.bind(this, gen)) .start(); } /** * @param gen 代数 */ private _onCloseHalfDone(gen: number): void { if (gen !== this._flipGen || !this.isValid) return; this._flipWait -= 1; if (this._flipWait > 0) return; const opts = this._pendingOptions; this._pendingOptions = null; if (!opts) { this._busy = false; return; } const count = Math.min(opts.length, this._itemNodes.length); for (let i = 0; i < count; i++) { const node = this._itemNodes[i]; if (!node?.isValid) continue; this._bindItem(node, opts[i]); const skew = node.getComponent(UISkew); if (skew) skew.setSkew(-FLIP_SKEW_X, 0); node.setScale(0, this._homeScale.y * FLIP_MID_SCALE_Y, this._homeScale.z); } if (opts.length !== this._itemNodes.length) { console.warn('[UIPopSelect] 刷新选项数与牌面不一致', opts.length, this._itemNodes.length); } this._flipWait = this._itemNodes.length; for (let i = 0; i < this._itemNodes.length; i++) { this.scheduleOnce(this._onOpenDue.bind(this, i, gen), FLIP_STAGGER_SEC * i); } } /** * 展开半程:scaleX 0→home,带回弹。 * @param node 牌 * @param gen 代数 */ private _playOpenHalf(node: Node | null, gen: number): void { if (!node?.isValid) { this._onOpenHalfDone(gen); return; } gg.audio.playEffect({ name: '点击按钮', path: GAudioPath.SfxUi }); const hx = this._homeScale.x; const hy = this._homeScale.y; const hz = this._homeScale.z; const skew = this._ensureSkew(node); Tween.stopAllByTarget(node); Tween.stopAllByTarget(skew); skew.setSkew(-FLIP_SKEW_X, 0); node.setScale(0, hy * FLIP_MID_SCALE_Y, hz); tween(skew) .to(FLIP_HALF_SEC, { x: 0 }, { easing: 'sineOut' }) .start(); tween(node) .to(FLIP_HALF_SEC, { scale: v3(hx, hy, hz) }, { easing: 'sineOut' }) .to(REVEAL_POP_HALF_SEC, { scale: v3(hx * REVEAL_POP_SCALE, hy * REVEAL_POP_SCALE, hz) }, { easing: 'sineOut' }) .to(REVEAL_POP_HALF_SEC, { scale: v3(hx, hy, hz) }, { easing: 'sineIn' }) .call(this._onOpenHalfDone.bind(this, gen)) .start(); } /** * @param gen 代数 */ private _onOpenHalfDone(gen: number): void { if (gen !== this._flipGen || !this.isValid) return; this._flipWait -= 1; if (this._flipWait > 0) return; this._busy = false; } private _ensureSkew(node: Node): UISkew { let skew = node.getComponent(UISkew); if (!skew) skew = node.addComponent(UISkew); return skew; } private _stopAllItemTweens(): void { for (let i = 0; i < this._itemNodes.length; i++) { const n = this._itemNodes[i]; if (!n?.isValid) continue; Tween.stopAllByTarget(n); const skew = n.getComponent(UISkew); if (skew) { Tween.stopAllByTarget(skew); skew.setSkew(0, 0); } } } onDisable(): void { this._flipGen += 1; this._busy = false; this._pendingOptions = null; this._flipWait = 0; this.unscheduleAllCallbacks(); this._stopAllItemTweens(); this._clearItems(); super.onDisable(); } }