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/PopDailyShopOpen/UIPopDailyShopOpen.ts

1001 lines
37 KiB

1 week ago
//*********************
// create by 流云
// time: Sun Sep 20 2026
// desc: 结算后每日 buff 翻牌(点哪张先翻哪张,飞行入包后其余两张带间隔依次翻开;铜币领点中那张,广告再领一张)
//*********************
import { _decorator, Button, Color, EventTouch, instantiate, Label, Layout, Node, Tween, tween, UISkew, UITransform, v3, Vec3 } from 'cc';
import { Auto_PopDailyShopOpen } from './Auto_PopDailyShopOpen';
import { DailyShopBuySkillFlyPresenter } from '../PopDailyShopBuy/DailyShopBuySkillFlyPresenter';
import { DailyShopLotteryIcon, DailyShopOwnedDesc } from '../PopDailyShopLottery/DailyShopLotteryIcon';
import { GEvent } from '../../../mx/module/event/GEvent';
import { UIID, GAudioPath } from '../../game/ConfigRes';
import { ItemNumType } from '../../game/GameData';
import { DailyShopDataManager } from '../../manager/DailyShopDataManager';
import { PopReplaceSkillParam } from '../PopReplaceSkill/UIPopReplaceSkill';
import { PopToastType } from '../PopToast/UIPopToast';
import {
DailyShopKind,
DailyShopParsed,
DailyShopPurchaseFail,
DailyShopPurchaseResult,
dailyShopFailReason,
isDailyShopNeedReplace,
} from '../../manager/DailyShopTypes';
import { RankReport } from '../../manager/RankReport';
const { ccclass } = _decorator;
const CARD_COUNT = 3;
/** 伪 3D 翻牌半程(scaleX 收/展) */
const FLIP_HALF_SEC = 0.32;
/** 翻牌透视斜切峰值(度) */
const FLIP_SKEW_X = 16;
/** 翻牌厚度感:中间 scaleY */
const FLIP_MID_SCALE_Y = 1.12;
/** 点中牌飞行入包后,其余两张的翻开间隔(秒) */
const OTHER_FLIP_STAGGER_SEC = 0.25;
/** 翻开后的放大回弹峰值(1→1.1→1) */
const REVEAL_POP_SCALE = 1.1;
/** 放大/回落各占时长(秒) */
const REVEAL_POP_HALF_SEC = 0.12;
/** 商店物品名称描边色(按品质) */
const SHOP_NAME_OUTLINE_HEX: Record<number, string> = {
1: '#386c42',
2: '#285c6d',
3: '#623073',
4: '#734930',
};
@ccclass('UIPopDailyShopOpen')
export class UIPopDailyShopOpen extends Auto_PopDailyShopOpen {
private _busy = false;
private _ownedDesc: DailyShopOwnedDesc | null = null;
private _skillFly: DailyShopBuySkillFlyPresenter | null = null;
private _btnCards: Node[] = [];
private _uiOpens: (Node | null)[] = [];
private _btnVideos: (Node | null)[] = [];
/** 该格是否已领取道具 */
private _claimed: boolean[] = [false, false, false];
/** 三张牌是否已翻开(视觉) */
private _opened = false;
private _goldDone = false;
private _adDone = false;
private _flipItems: (Node | null)[] = [null, null, null];
/** 翻开时三格预览 buff(广告领取用已抽结果,不再重抽) */
private _previewBuffs: (DailyShopParsed | null)[] = [null, null, null];
private _cardBasePos: Vec3[] = [];
private _pendingFlipIndex = -1;
private _pendingFlipParsed: DailyShopParsed | null = null;
private _pendingFlipPayByAd = false;
private _pendingFlipResult: DailyShopPurchaseResult | null = null;
/** 等待三张牌翻开完成 */
private _openWaitLeft = 0;
/** 单张开牌揭示剩余段数 */
private _revealPartLeft: number[] = [0, 0, 0];
/** 翻牌阶段:0 无 / 1 点中那张 / 2 其余两张 */
private _flipPhase: 0 | 1 | 2 = 0;
/** 点中的牌索引 */
private _flippedCardIndex = -1;
/** 飞行落定后待翻开其余两张 */
private _flipOthersPending = false;
/** 广告再领的牌索引 */
private _adClaimIndex = -1;
/** 待删除槽 */
private _deleteKind = DailyShopKind.Active;
private _deleteSlot = 0;
onLoad(): void {
super.onLoad();
this._skillFly = new DailyShopBuySkillFlyPresenter();
this._skillFly.bind(this.ui_center);
this._cacheNodes();
if (this.ui_shopItem?.isValid) this.ui_shopItem.active = false;
if (this.ui_skillMainItem?.isValid) this.ui_skillMainItem.active = false;
if (this.ui_skillPassiveItem?.isValid) this.ui_skillPassiveItem.active = false;
if (this.ui_emptySkillMainItem?.isValid) this.ui_emptySkillMainItem.active = false;
if (this.ui_emptySkillPassiveItem?.isValid) this.ui_emptySkillPassiveItem.active = false;
if (this.ui_descLeft?.isValid || this.ui_descRight?.isValid) {
this._ownedDesc = new DailyShopOwnedDesc(this.ui_descLeft, this.ui_descRight);
this._ownedDesc.init();
}
}
onEnable(): void {
super.onEnable();
this._ownedDesc?.bindDismiss();
GEvent.Ins.on(GEvent.UpdateItemNum, this._onItemNumChange, this);
void this._refreshView();
}
onDisable(): void {
GEvent.Ins.off(GEvent.UpdateItemNum, this._onItemNumChange, this);
this._ownedDesc?.unbindDismiss();
this._skillFly?.unbind();
this._stopFlipTweens();
super.onDisable();
}
onInit(): void {
this._busy = false;
this._goldDone = false;
this._adDone = false;
this._opened = false;
this._claimed = [false, false, false];
this._previewBuffs = [null, null, null];
this._pendingFlipIndex = -1;
this._pendingFlipParsed = null;
this._pendingFlipResult = null;
this._pendingFlipPayByAd = false;
this._flipPhase = 0;
this._flippedCardIndex = -1;
this._flipOthersPending = false;
if (this.ui_guide?.isValid) this.ui_guide.active = !DailyShopDataManager.hasUsedOpenGuide();
this.showOpenCenterEff();
this.showOpenMaskEff(2);
void this._resetAndShow();
}
click_btnClose(): void {
void this.close(2);
}
click_btnCard0(): void {
this._onClickCard(0);
}
click_btnCard1(): void {
this._onClickCard(1);
}
click_btnCard2(): void {
this._onClickCard(2);
}
click_btnVideo0(): void {
this._onClickVideo(0);
}
click_btnVideo1(): void {
this._onClickVideo(1);
}
click_btnVideo2(): void {
this._onClickVideo(2);
}
private _cacheNodes(): void {
this._btnCards = [this.btnCard0, this.btnCard1, this.btnCard2];
this._uiOpens = [this.ui_open0, this.ui_open1, this.ui_open2];
this._btnVideos = [this.btnVideo0, this.btnVideo1, this.btnVideo2];
this._cardBasePos = [];
for (let i = 0; i < CARD_COUNT; i++) {
const card = this._btnCards[i];
this._cardBasePos.push(card?.isValid ? card.position.clone() : v3(0, 0, 0));
}
}
private async _resetAndShow(): Promise<void> {
await DailyShopDataManager.ensureTable();
if (!this.isValid) return;
DailyShopDataManager.ensureToday();
this._goldDone = DailyShopDataManager.hasUsedOpenGold();
this._adDone = DailyShopDataManager.hasUsedOpenAd();
this._resetCardVisual();
await this._refreshView();
const pendingId = DailyShopDataManager.getPendingReplaceGoodsId();
if (pendingId > 0) this._askReplace(pendingId);
}
private _onItemNumChange(itemId?: number): void {
if (itemId != null && itemId !== ItemNumType.money) return;
if (!this.isValid) return;
this._refreshPriceLabel();
}
private async _refreshView(): Promise<void> {
await DailyShopDataManager.ensureTable();
if (!this.isValid) return;
this._refreshPriceLabel();
this._renderOwnedSkills();
}
private _refreshPriceLabel(): void {
if (!this.lb_price?.isValid) return;
const cost = DailyShopDataManager.getOpenCost();
this.lb_price.string = `${cost}`;
const enough = gg.data.getItemNum(ItemNumType.money) >= cost;
this.lb_price.color = enough
? new Color(255, 255, 255, 255)
: new Color(255, 0, 0, 255);
}
private _resetCardVisual(): void {
this._clearFlipItems();
this._opened = false;
this._flipPhase = 0;
this._flippedCardIndex = -1;
this._flipOthersPending = false;
for (let i = 0; i < CARD_COUNT; i++) {
const card = this._btnCards[i];
if (card?.isValid) {
Tween.stopAllByTarget(card);
const skew = card.getComponent(UISkew);
if (skew) {
Tween.stopAllByTarget(skew);
skew.setSkew(0, 0);
}
card.active = true;
card.setScale(1, 1, 1);
card.setPosition(this._cardBasePos[i] ?? card.position);
this._setBtnInteractable(card, !this._goldDone);
}
const openFace = this._uiOpens[i];
if (openFace?.isValid) {
Tween.stopAllByTarget(openFace);
const skew = openFace.getComponent(UISkew);
if (skew) {
Tween.stopAllByTarget(skew);
skew.setSkew(0, 0);
}
openFace.active = false;
openFace.setScale(1, 1, 1);
}
const video = this._btnVideos[i];
if (video?.isValid) video.active = false;
this._claimed[i] = false;
this._previewBuffs[i] = null;
this._revealPartLeft[i] = 0;
}
if (this.ui_need?.isValid) {
this.ui_need.active = !this._goldDone;
}
}
private _clearFlipItems(): void {
for (let i = 0; i < this._flipItems.length; i++) {
const item = this._flipItems[i];
this._flipItems[i] = null;
if (item?.isValid) {
Tween.stopAllByTarget(item);
item.destroy();
}
}
if (this.ui_open?.isValid) {
this.ui_open.removeAllChildren();
}
}
private _stopFlipTweens(): void {
for (let i = 0; i < CARD_COUNT; i++) {
const card = this._btnCards[i];
if (card?.isValid) {
Tween.stopAllByTarget(card);
const skew = card.getComponent(UISkew);
if (skew) Tween.stopAllByTarget(skew);
}
const openFace = this._uiOpens[i];
if (openFace?.isValid) {
Tween.stopAllByTarget(openFace);
const skew = openFace.getComponent(UISkew);
if (skew) Tween.stopAllByTarget(skew);
}
const item = this._flipItems[i];
if (item?.isValid) Tween.stopAllByTarget(item);
}
}
private _onClickCard(index: number): void {
if (this._busy) return;
if (this._goldDone || this._opened) return;
if (index < 0 || index >= CARD_COUNT) return;
const cost = DailyShopDataManager.getOpenCost();
if (gg.data.getItemNum(ItemNumType.money) < cost) {
gg.game.showPopCoinToast('金币不足,是否免费抽取?', this._onCoinToastOk.bind(this, index));
return;
}
this._startGoldFlip(index, false);
}
/** 金币不足弹窗:看完广告后按铜币翻牌入包 */
private _onCoinToastOk(index: number): void {
if (!this.isValid) return;
RankReport.adFlipCard();
this._startGoldFlip(index, true);
}
/**
* 铜币翻三张入点中那张。
* @param freeByAd true=看广告免铜币,仍记为铜币次数
*/
private _startGoldFlip(index: number, freeByAd: boolean): void {
if (this._busy) return;
if (this._goldDone || this._opened) return;
if (index < 0 || index >= CARD_COUNT) return;
this._busy = true;
if (!DailyShopDataManager.hasUsedOpenGuide()) DailyShopDataManager.markOpenGuideUsed();
if (this.ui_guide?.isValid) this.ui_guide.active = false;
this._disableAllCards();
const previews = DailyShopDataManager.rollOpenBuffs(CARD_COUNT);
if (previews.length < CARD_COUNT) {
this._busy = false;
this._enableUnflippedCards();
console.log('[PopDailyShopOpen] 翻牌预览不足', previews.length);
gg.ui.showToast('道具不足', false);
return;
}
this._previewBuffs = [previews[0], previews[1], previews[2]];
const claim = previews[index];
const result = DailyShopDataManager.purchaseOpen({
payByAd: freeByAd,
goodsId: claim.id,
countAsGold: freeByAd,
});
this._beginGoldFlipAll(index, result);
}
private _onClickVideo(index: number): void {
if (this._busy) return;
if (!this._goldDone || this._adDone || !this._opened) return;
if (index < 0 || index >= CARD_COUNT || this._claimed[index]) return;
const preview = this._previewBuffs[index];
if (!preview) {
console.log('[PopDailyShopOpen] 广告格无预览 buff', index);
return;
}
this._busy = true;
this._adClaimIndex = index;
this._hideAllVideos();
gg.sdk.showVideoAd(this._onOpenVideoClosed);
}
/** 广告再领一张的观看结果 */
private readonly _onOpenVideoClosed = (res: boolean): void => {
if (!this.isValid) return;
const index = this._adClaimIndex;
if (!res) {
this._busy = false;
this._showVideosAfterGold();
RankReport.adShopBuyAgain();
gg.ui.showToast('观看视频失败', false);
return;
}
const preview = this._previewBuffs[index];
if (!preview) {
this._busy = false;
console.log('[PopDailyShopOpen] 广告格无预览 buff', index);
return;
}
const result = DailyShopDataManager.purchaseOpen({ payByAd: true, goodsId: preview.id });
this._beginAdClaim(index, result);
};
/** 铜币:三张都展示已抽 buff,只入包点中那张 */
private _beginGoldFlipAll(index: number, result: DailyShopPurchaseResult): void {
if (!result.ok && !isDailyShopNeedReplace(result)) {
this._busy = false;
this._enableUnflippedCards();
this._previewBuffs = [null, null, null];
gg.ui.showToast(this._failTip(dailyShopFailReason(result)), false);
return;
}
const parsed = result.ok
? result.parsed
: DailyShopDataManager.getParsed(DailyShopDataManager.getPendingReplaceGoodsId());
if (!parsed) {
this._busy = false;
console.log('[PopDailyShopOpen] 翻牌结果无道具');
return;
}
this._goldDone = true;
this._pendingFlipIndex = index;
this._pendingFlipParsed = parsed;
this._pendingFlipResult = result;
this._pendingFlipPayByAd = false;
this._claimed[index] = true;
this._flipPhase = 1;
this._flippedCardIndex = index;
this._flipOthersPending = true;
if (this.ui_need?.isValid) this.ui_need.active = false;
const clickedBuff = this._previewBuffs[index];
if (clickedBuff) this._spawnFlipItem(index, clickedBuff);
this._playClickedCardFlip(index);
}
/** 广告:领取已展示的预览 buff(不重抽) */
private _beginAdClaim(index: number, result: DailyShopPurchaseResult): void {
if (!result.ok && !isDailyShopNeedReplace(result)) {
this._busy = false;
this._showVideosAfterGold();
gg.ui.showToast(this._failTip(dailyShopFailReason(result)), false);
return;
}
const parsed = result.ok
? result.parsed
: DailyShopDataManager.getParsed(DailyShopDataManager.getPendingReplaceGoodsId());
if (!parsed) {
this._busy = false;
console.log('[PopDailyShopOpen] 广告翻牌结果无道具');
return;
}
RankReport.adShopBuy();
this._adDone = true;
this._pendingFlipIndex = index;
this._pendingFlipParsed = parsed;
this._pendingFlipResult = result;
this._pendingFlipPayByAd = true;
this._claimed[index] = true;
if (!this._flipItems[index]?.isValid) {
this._spawnFlipItem(index, parsed);
const item = this._flipItems[index];
if (item?.isValid) item.setScale(1, 1, 1);
}
this._onFlipRevealDone();
}
private _spawnFlipItem(index: number, parsed: DailyShopParsed): void {
const openFace = this._uiOpens[index];
if (!this.ui_shopItem?.isValid || !this.ui_open?.isValid || !openFace?.isValid) {
console.log('[PopDailyShopOpen] 翻牌模板无效');
return;
}
const old = this._flipItems[index];
if (old?.isValid) {
Tween.stopAllByTarget(old);
old.destroy();
}
const item = instantiate(this.ui_shopItem);
item.active = true;
item.parent = this.ui_open;
item.setScale(0, FLIP_MID_SCALE_Y, 1);
const openUt = this.ui_open.getComponent(UITransform);
if (openUt) {
item.setPosition(openUt.convertToNodeSpaceAR(openFace.worldPosition.clone()));
} else {
item.setWorldPosition(openFace.worldPosition.clone());
}
this._bindOpenShopItem(item, parsed);
this._flipItems[index] = item;
}
private _bindOpenShopItem(item: Node, parsed: DailyShopParsed): void {
DailyShopLotteryIcon.bindShopItem(item, parsed);
this._ownedDesc?.bindShopItem(item, parsed);
const nameNode = item.getChildByName('name') ?? item.getChildByName('Label');
if (nameNode?.isValid) {
const lb = nameNode.getComponent(Label);
if (lb) {
lb.string = parsed.name;
const hex = SHOP_NAME_OUTLINE_HEX[Math.max(1, Math.floor(parsed.quality) || 1)];
if (hex) {
lb.enableOutline = true;
lb.outlineColor = Color.fromHEX(new Color(), hex);
}
}
}
const descNode = item.getChildByName('desc');
if (descNode?.isValid) {
const lb = descNode.getComponent(Label);
if (lb) lb.string = DailyShopLotteryIcon.battleBuffDesc(parsed);
}
}
/** 阶段1:先翻点中的那张 */
private _playClickedCardFlip(index: number): void {
this._openWaitLeft = 1;
this._playOneCardFlip3d(index);
}
/** 阶段2:飞行落定后,其余两张带间隔依次翻开 */
private _playOtherCardsFlip(clicked: number): void {
const others: number[] = [];
for (let i = 0; i < CARD_COUNT; i++) {
if (i !== clicked) others.push(i);
}
this._openWaitLeft = others.length;
for (let k = 0; k < others.length; k++) {
const idx = others[k];
this.scheduleOnce(this._onOtherCardFlip.bind(this, idx), OTHER_FLIP_STAGGER_SEC * k);
}
}
/** 间隔翻开其余牌 */
private _onOtherCardFlip(index: number): void {
if (!this.isValid || this._flipPhase !== 2) return;
const buff = this._previewBuffs[index];
if (buff && !this._flipItems[index]?.isValid) this._spawnFlipItem(index, buff);
this._playOneCardFlip3d(index);
}
/**
* 单张伪 3D 翻牌:背面 scaleX 1→0,换正面后 0→1。
* @param index 牌下标
*/
private _playOneCardFlip3d(index: number): void {
const card = this._btnCards[index];
const openFace = this._uiOpens[index];
if (!card?.isValid) {
this._onOneCardClosed(index);
return;
}
gg.audio.playEffect({ name: '点击抽卡', path: GAudioPath.SfxUi });
const targetX = openFace?.isValid ? openFace.position.x : card.position.x;
const targetY = openFace?.isValid ? openFace.position.y : card.position.y;
const skew = this._ensureSkew(card);
Tween.stopAllByTarget(card);
Tween.stopAllByTarget(skew);
skew.setSkew(0, 0);
card.setScale(1, 1, 1);
tween(skew)
.to(FLIP_HALF_SEC, { x: FLIP_SKEW_X }, { easing: 'sineIn' })
.start();
tween(card)
.to(FLIP_HALF_SEC, {
position: v3(targetX, targetY, 0),
scale: v3(0, FLIP_MID_SCALE_Y, 1),
}, { easing: 'sineIn' })
.call(this._onOneCardClosed.bind(this, index))
.start();
}
/** 背面收拢后:正面 + 道具从 scaleX=0 展开 */
private _onOneCardClosed(index: number): void {
if (!this.isValid) return;
const card = this._btnCards[index];
if (card?.isValid) {
card.active = false;
card.setScale(1, 1, 1);
const skew = card.getComponent(UISkew);
if (skew) skew.setSkew(0, 0);
}
const openFace = this._uiOpens[index];
const item = this._flipItems[index];
let parts = 0;
if (openFace?.isValid) {
parts++;
openFace.active = true;
openFace.setScale(0, FLIP_MID_SCALE_Y, 1);
const oSkew = this._ensureSkew(openFace);
Tween.stopAllByTarget(openFace);
Tween.stopAllByTarget(oSkew);
oSkew.setSkew(-FLIP_SKEW_X, 0);
tween(oSkew)
.to(FLIP_HALF_SEC, { x: 0 }, { easing: 'sineOut' })
.start();
tween(openFace)
.to(FLIP_HALF_SEC, { scale: v3(1, 1, 1) }, { easing: 'sineOut' })
.to(REVEAL_POP_HALF_SEC, { scale: v3(REVEAL_POP_SCALE, REVEAL_POP_SCALE, 1) }, { easing: 'sineOut' })
.to(REVEAL_POP_HALF_SEC, { scale: v3(1, 1, 1) }, { easing: 'sineIn' })
.call(this._onRevealPartDone.bind(this, index))
.start();
}
if (item?.isValid) {
parts++;
Tween.stopAllByTarget(item);
item.setScale(0, FLIP_MID_SCALE_Y, 1);
tween(item)
.to(FLIP_HALF_SEC, { scale: v3(1, 1, 1) }, { easing: 'sineOut' })
.to(REVEAL_POP_HALF_SEC, { scale: v3(REVEAL_POP_SCALE, REVEAL_POP_SCALE, 1) }, { easing: 'sineOut' })
.to(REVEAL_POP_HALF_SEC, { scale: v3(1, 1, 1) }, { easing: 'sineIn' })
.call(this._onRevealPartDone.bind(this, index))
.start();
}
this._revealPartLeft[index] = parts;
if (parts <= 0) this._onOneCardOpenFullyDone();
}
private _onRevealPartDone(index: number): void {
this._revealPartLeft[index]--;
if (this._revealPartLeft[index] === 0) this._onOneCardOpenFullyDone();
}
private _onOneCardOpenFullyDone(): void {
this._openWaitLeft--;
if (this._openWaitLeft > 0) return;
if (this._flipPhase === 1) {
this._opened = true;
this._onFlipRevealDone();
return;
}
if (this._flipPhase === 2) this._finishOthersReveal();
}
/** 阶段2 收尾:其余两张全部翻开,放开交互 */
private _finishOthersReveal(): void {
this._flipPhase = 0;
this._busy = false;
}
private _ensureSkew(node: Node): UISkew {
let skew = node.getComponent(UISkew);
if (!skew) skew = node.addComponent(UISkew);
return skew;
}
private _onFlipRevealDone(): void {
if (!this.isValid) return;
const result = this._pendingFlipResult;
const parsed = this._pendingFlipParsed;
const payByAd = this._pendingFlipPayByAd;
const index = this._pendingFlipIndex;
this._pendingFlipResult = null;
this._pendingFlipParsed = null;
this._pendingFlipIndex = -1;
if (!result || !parsed) {
this._busy = false;
return;
}
if (result.ok) {
RankReport.shopBuy(parsed.name);
this._showPurchaseSuccessToast(result);
gg.dot?.updateRedDot();
if (result.instant) {
this._afterFlipSettled(payByAd);
return;
}
const slotIndex = result.slotIndex;
const slotKind = result.slotKind ?? parsed.kind;
const sourceIcon = this._resolveFlipIcon(index);
if (slotIndex != null && slotIndex >= 0 && sourceIcon?.isValid) {
this._playBuyToEmptySlotAnim(sourceIcon, parsed, slotKind, slotIndex, payByAd);
return;
}
this._afterFlipSettled(payByAd);
return;
}
if (isDailyShopNeedReplace(result)) {
RankReport.shopBuy(parsed.name);
void this._refreshView();
this._askReplace(DailyShopDataManager.getPendingReplaceGoodsId(), payByAd);
return;
}
this._afterFlipSettled(payByAd);
}
private _resolveFlipIcon(index: number): Node | null {
const item = this._flipItems[index];
if (!item?.isValid) return null;
return DailyShopLotteryIcon.resolveShopIcon(item);
}
private _afterFlipSettled(payByAd: boolean): void {
void this._refreshView();
if (!payByAd && !this._adDone) {
this._showVideosAfterGold();
}
if (this._flipOthersPending) {
this._flipOthersPending = false;
this._flipPhase = 2;
this._playOtherCardsFlip(this._flippedCardIndex);
return;
}
this._busy = false;
}
private _showVideosAfterGold(): void {
for (let i = 0; i < CARD_COUNT; i++) {
const video = this._btnVideos[i];
if (!video?.isValid) continue;
video.active = !this._claimed[i] && !this._adDone;
}
}
private _hideAllVideos(): void {
for (let i = 0; i < this._btnVideos.length; i++) {
const video = this._btnVideos[i];
if (video?.isValid) video.active = false;
}
}
private _disableAllCards(): void {
for (let i = 0; i < this._btnCards.length; i++) {
this._setBtnInteractable(this._btnCards[i], false);
}
}
private _enableUnflippedCards(): void {
for (let i = 0; i < this._btnCards.length; i++) {
this._setBtnInteractable(this._btnCards[i], !this._goldDone);
}
}
private _setBtnInteractable(node: Node | null | undefined, on: boolean): void {
if (!node?.isValid) return;
const btn = node.getComponent(Button);
if (btn) btn.interactable = on;
}
private _renderOwnedSkills(hideSlot?: { kind: DailyShopKind; slotIndex: number }): void {
this._ownedDesc?.hide();
const { active, passive } = DailyShopDataManager.getSlotCaps();
this._fillSkillSlots(
this.ui_skillMain,
this.ui_skillMainItem,
this.ui_emptySkillMainItem,
DailyShopDataManager.getActiveSlotIds(),
active,
DailyShopKind.Active,
hideSlot?.kind === DailyShopKind.Active ? hideSlot.slotIndex : -1,
);
this._fillSkillSlots(
this.ui_skillPassive,
this.ui_skillPassiveItem,
this.ui_emptySkillPassiveItem,
DailyShopDataManager.getPassiveSlotIds(),
passive,
DailyShopKind.Passive,
hideSlot?.kind === DailyShopKind.Passive ? hideSlot.slotIndex : -1,
);
}
private _fillSkillSlots(
container: Node | null | undefined,
template: Node | null | undefined,
emptyTemplate: Node | null | undefined,
slotIds: number[],
slotCount: number,
kind: DailyShopKind,
hideSlotIndex = -1,
): void {
if (!container?.isValid || !template?.isValid || slotCount <= 0) {
container?.removeAllChildren();
return;
}
container.removeAllChildren();
for (let i = 0; i < slotCount; i++) {
const base = emptyTemplate?.isValid ? instantiate(emptyTemplate) : null;
if (base) {
base.active = true;
base.setPosition(v3(0, 0, 0));
base.parent = container;
}
const id = Number(slotIds[i]) || 0;
const parsed = id > 0 ? DailyShopDataManager.getParsed(id) : null;
if (parsed && i !== hideSlotIndex) {
const item = instantiate(template);
item.active = true;
item.setPosition(v3(0, 0, 0));
item.parent = base ?? container;
DailyShopLotteryIcon.bindSlotItem(item, parsed, kind);
this._ownedDesc?.bindSlotItem(item, parsed, kind);
this._bindDeleteBtn(item, kind, i);
}
}
container.getComponent(Layout)?.updateLayout();
}
private _bindDeleteBtn(item: Node, kind: DailyShopKind, slotIndex: number): void {
const deleteBtn = item.getChildByName('deleteBtn');
if (!deleteBtn?.isValid) return;
deleteBtn.active = true;
deleteBtn.setPosition(v3(32, 32, 0));
deleteBtn.setSiblingIndex(item.children.length - 1);
const deleteIcon = deleteBtn.getChildByName('icon');
if (deleteIcon?.isValid) deleteIcon.active = true;
deleteBtn['dailyShopSlotKind'] = kind;
deleteBtn['dailyShopSlotIndex'] = slotIndex;
deleteBtn.targetOff(Node.EventType.TOUCH_END);
deleteBtn.on(Node.EventType.TOUCH_END, this._onClickDeleteSlot, this);
}
private _onClickDeleteSlot(event: EventTouch): void {
if (this._busy) return;
const btn = event.currentTarget as Node;
this._deleteKind = Number(btn?.['dailyShopSlotKind']) as DailyShopKind;
this._deleteSlot = Number(btn?.['dailyShopSlotIndex']) || 0;
const name = this._getSlotGoodsName(this._deleteKind, this._deleteSlot);
gg.game.showPopToast(`你确定要删除${name}道具吗?`, PopToastType.DeleteItem, this._onDeleteSlotOk);
}
/** 确认删除携带槽 */
private readonly _onDeleteSlotOk = (): void => {
if (!DailyShopDataManager.removeSlotAt(this._deleteKind, this._deleteSlot)) {
gg.ui.showToast('删除失败', false);
return;
}
void this._refreshView();
};
private _getSlotGoodsName(kind: DailyShopKind, slotIndex: number): string {
const ids = kind === DailyShopKind.Passive
? DailyShopDataManager.getPassiveSlotIds()
: DailyShopDataManager.getActiveSlotIds();
const id = Number(ids[slotIndex]) || 0;
if (id <= 0) return '';
return DailyShopDataManager.getParsed(id)?.name ?? '';
}
private _showPurchaseSuccessToast(result: DailyShopPurchaseResult & { ok: true }): void {
if (result.instant) {
gg.ui.showToast('购买成功', false);
return;
}
if (result.replacedId) {
const oldName = DailyShopDataManager.getParsed(result.replacedId)?.name ?? '';
gg.ui.showToast(oldName ? `已替换「${oldName}」` : '购买成功', false);
return;
}
gg.ui.showToast('购买成功', false);
}
private _playBuyToEmptySlotAnim(
sourceIcon: Node,
parsed: DailyShopParsed,
slotKind: DailyShopKind,
slotIndex: number,
payByAd: boolean,
): void {
const slotBase = this._getSlotBase(slotKind, slotIndex);
const fly = this._skillFly;
if (!sourceIcon?.isValid || !slotBase?.isValid || !fly) {
console.log('[PopDailyShopOpen] 购买飞行动画参数无效');
this._afterFlipSettled(payByAd);
return;
}
this._pendingFlipPayByAd = payByAd;
fly.playIconFly(sourceIcon, slotBase, this._onBuyIconFlyArrived.bind(this, parsed, slotKind, slotIndex));
this._renderOwnedSkills({ kind: slotKind, slotIndex });
}
private _onBuyIconFlyArrived(parsed: DailyShopParsed, slotKind: DailyShopKind, slotIndex: number): void {
if (!this.isValid) return;
const skillItem = this._revealSkillAtSlot(parsed, slotKind, slotIndex);
const fly = this._skillFly;
if (!skillItem?.isValid || !fly) {
this._afterFlipSettled(this._pendingFlipPayByAd);
return;
}
fly.playSlotPulse(skillItem, this._onBuySkillPulseDone.bind(this));
}
private _onBuySkillPulseDone(): void {
this._afterFlipSettled(this._pendingFlipPayByAd);
}
private _revealSkillAtSlot(parsed: DailyShopParsed, kind: DailyShopKind, slotIndex: number): Node | null {
const template = kind === DailyShopKind.Active ? this.ui_skillMainItem : this.ui_skillPassiveItem;
const base = this._getSlotBase(kind, slotIndex);
if (!template?.isValid || !base?.isValid) return null;
const item = instantiate(template);
item.active = true;
item.setPosition(v3(0, 0, 0));
item.parent = base;
DailyShopLotteryIcon.bindSlotItem(item, parsed, kind);
this._ownedDesc?.bindSlotItem(item, parsed, kind);
this._bindDeleteBtn(item, kind, slotIndex);
return item;
}
private _getSlotBase(kind: DailyShopKind, slotIndex: number): Node | null {
const container = kind === DailyShopKind.Active ? this.ui_skillMain : this.ui_skillPassive;
if (!container?.isValid || slotIndex < 0 || slotIndex >= container.children.length) return null;
return container.children[slotIndex];
}
private _findSlotSkillItem(base: Node): Node | null {
const kids = base.children;
for (let i = 0; i < kids.length; i++) {
const child = kids[i];
if (child.name === 'deleteBtn') continue;
return child;
}
return null;
}
private _playReplaceSlotPulse(kind: DailyShopKind, slotIndex: number, payByAd: boolean): void {
const base = this._getSlotBase(kind, slotIndex);
const skillItem = base?.isValid ? this._findSlotSkillItem(base) : null;
const fly = this._skillFly;
this._pendingFlipPayByAd = payByAd;
if (!skillItem?.isValid || !fly) {
this._afterFlipSettled(payByAd);
return;
}
fly.playSlotPulse(skillItem, this._onReplaceSkillPulseDone.bind(this));
}
private _onReplaceSkillPulseDone(): void {
this._afterFlipSettled(this._pendingFlipPayByAd);
}
private _askReplace(goodsId: number, payByAd = false): void {
const parsed = DailyShopDataManager.getParsed(goodsId);
if (!parsed) {
this._afterFlipSettled(payByAd);
return;
}
const entries = DailyShopDataManager.getOccupiedSlotEntries(parsed.kind);
if (entries.length <= 0) {
gg.ui.showToast('没有可替换的道具', false);
this._afterFlipSettled(payByAd);
return;
}
this._pendingFlipPayByAd = payByAd;
gg.ui.openPanel(UIID.PopReplaceSkill, {
data: {
goodsId,
onConfirm: this._onReplaceConfirm,
onCancel: this._onReplaceCancel,
} satisfies PopReplaceSkillParam,
});
}
/** 替换窗确认 */
private readonly _onReplaceConfirm = (replaceOwnedId: number): void => {
this._completeReplace(replaceOwnedId);
};
/** 替换窗取消 */
private readonly _onReplaceCancel = (): void => {
void this._refreshView();
this._afterFlipSettled(this._pendingFlipPayByAd);
};
private _completeReplace(replaceOwnedId: number): void {
const result = DailyShopDataManager.completeReplace(replaceOwnedId);
this._handleReplaceResult(result);
}
private _handleReplaceResult(result: DailyShopPurchaseResult): void {
if (result.ok) {
const oldName = result.replacedId
? DailyShopDataManager.getParsed(result.replacedId)?.name ?? ''
: '';
gg.ui.showToast(oldName ? `已替换「${oldName}」` : '购买成功', false);
gg.dot?.updateRedDot();
void this._refreshView().then(this._onReplaceViewRefreshed.bind(this, result));
return;
}
this._afterFlipSettled(this._pendingFlipPayByAd);
gg.ui.showToast(this._failTip(dailyShopFailReason(result)), false);
}
private _onReplaceViewRefreshed(result: DailyShopPurchaseResult & { ok: true }): void {
if (!this.isValid) return;
const slotIndex = result.slotIndex;
const slotKind = result.slotKind ?? result.parsed.kind;
if (slotIndex != null && slotIndex >= 0) {
this._playReplaceSlotPulse(slotKind, slotIndex, this._pendingFlipPayByAd);
return;
}
this._afterFlipSettled(this._pendingFlipPayByAd);
}
private _failTip(reason: DailyShopPurchaseFail): string {
switch (reason) {
case DailyShopPurchaseFail.NotEnoughMoney:
return '铜币不足';
case DailyShopPurchaseFail.AlreadySold:
return '今日翻牌次数已用完';
case DailyShopPurchaseFail.InvalidReplace:
return '替换目标无效';
case DailyShopPurchaseFail.PendingReplace:
return '请先完成替换';
default:
return '购买失败';
}
}
}