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.
484 lines
17 KiB
484 lines
17 KiB
|
|
//*********************
|
|
// 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();
|
|
}
|
|
}
|
|
|