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.
1000 lines
37 KiB
1000 lines
37 KiB
//*********************
|
|
// 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 '购买失败';
|
|
}
|
|
}
|
|
}
|
|
|