//********************* // 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 = { 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 { 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 { 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 '购买失败'; } } }