//********************* // create by 流云 // time: Wed Aug 19 2026 // desc: 结算后每日buff购买(抽奖已拆至 PopDailyShopLottery) //********************* import { _decorator, Color, EventTouch, instantiate, Label, Layout, Node, Sprite, v3 } from 'cc'; import { Auto_PopDailyShopBuy } from './Auto_PopDailyShopBuy'; import { DailyShopBuySkillFlyPresenter } from './DailyShopBuySkillFlyPresenter'; import { DailyShopLotteryIcon, DailyShopOwnedDesc } from '../PopDailyShopLottery/DailyShopLotteryIcon'; import { GEvent } from '../../../mx/module/event/GEvent'; import { UIID } from '../../game/ConfigRes'; import { ItemNumType } from '../../game/GameData'; import { DailyShopDataManager } from '../../manager/DailyShopDataManager'; import { PopReplaceSkillParam } from '../PopReplaceSkill/UIPopReplaceSkill'; import { PopToastType } from '../PopToast/UIPopToast'; import { DailyShopGoods, DailyShopKind, DailyShopParsed, DailyShopPurchaseFail, DailyShopPurchaseResult, dailyShopFailReason, isDailyShopNeedReplace, } from '../../manager/DailyShopTypes'; import { RankReport } from '../../manager/RankReport'; const { ccclass } = _decorator; const BUY_USED_TIP = '商品已售罄,欢迎下次光临!'; /** 商店物品名称描边色(按品质) */ const SHOP_NAME_OUTLINE_HEX: Record = { 1: '#386c42', 2: '#285c6d', 3: '#623073', 4: '#734930', }; @ccclass('UIPopDailyShopBuy') export class UIPopDailyShopBuy extends Auto_PopDailyShopBuy { private _busy = false; private _ownedDesc: DailyShopOwnedDesc | null = null; private _skillFly: DailyShopBuySkillFlyPresenter | null = null; onLoad(): void { super.onLoad(); this._skillFly = new DailyShopBuySkillFlyPresenter(); this._skillFly.bind(this.ui_center); 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.DailyShopOffersRefresh, this._onOffersRefresh, this); GEvent.Ins.on(GEvent.UpdateItemNum, this._onItemNumChange, this); void this._refreshView(); } onDisable(): void { GEvent.Ins.off(GEvent.DailyShopOffersRefresh, this._onOffersRefresh, this); GEvent.Ins.off(GEvent.UpdateItemNum, this._onItemNumChange, this); this._ownedDesc?.unbindDismiss(); this._skillFly?.unbind(); super.onDisable(); } onInit(): void { this._busy = false; this.showOpenCenterEff(); this.showOpenMaskEff(2); void this._resetAndShow(); } click_btnClose(): void { void this._closeThenTryLottery(); } /** 先关购买窗,再按概率链式弹抽奖(避免队列被当前窗阻塞 + 0.5s 轮询延迟) */ private async _closeThenTryLottery(): Promise { await this.close(2); DailyShopDataManager.tryPopLotteryAfterBuyClose(); gg.ui.flushPopQueue(); } private async _resetAndShow(): Promise { await DailyShopDataManager.ensureTable(); if (!this.isValid) return; DailyShopDataManager.ensureToday(); await this._refreshView(); const pendingId = DailyShopDataManager.getPendingReplaceGoodsId(); if (pendingId > 0) this._askReplace(pendingId); } private _onOffersRefresh(offers: DailyShopGoods[]): void { if (!this.isValid) return; if (this._busy) return; this._renderOwnedSkills(); this._renderOffers(offers); } private _onItemNumChange(itemId?: number): void { if (itemId != null && itemId !== ItemNumType.money) return; if (this._busy) return; if (!this.isValid || !this.ui_shopContent?.isValid || !this.ui_shopItem?.isValid) return; this._renderOffers(DailyShopDataManager.getOffers()); } private async _refreshView(): Promise { await DailyShopDataManager.ensureTable(); if (!this.isValid) return; this._renderOwnedSkills(); this._renderOffers(DailyShopDataManager.getOffers()); } private _renderOffers(offers: DailyShopGoods[]): void { if (!this.ui_shopContent?.isValid || !this.ui_shopItem?.isValid) return; this.ui_shopContent.removeAllChildren(); for (const goods of offers) { const item = instantiate(this.ui_shopItem); item.active = true; item.setPosition(v3(0, 0, 0)); item.parent = this.ui_shopContent; this._bindShopItem(item, goods); } this.ui_shopContent.getComponent(Layout)?.updateLayout(); } 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'); // 预制体已内置删除图标 spriteFrame,勿再动态加载不存在的 popCommon 路径 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; const kind = Number(btn?.['dailyShopSlotKind']) as DailyShopKind; const slotIndex = Number(btn?.['dailyShopSlotIndex']) || 0; const name = this._getSlotGoodsName(kind, slotIndex); gg.game.showPopToast( `你确定要删除${name}道具吗?`, PopToastType.DeleteItem, () => { if (!DailyShopDataManager.removeSlotAt(kind, slotIndex)) { 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 _bindShopItem(item: Node, goods: DailyShopGoods): void { DailyShopLotteryIcon.bindShopItem(item, goods); this._ownedDesc?.bindShopItem(item, goods); const row = goods.row as ITableBattlebuff; const nameNode = item.getChildByName('name') ?? item.getChildByName('Label'); if (nameNode?.isValid) { const lb = nameNode.getComponent(Label); if (lb) { lb.string = goods.name; const hex = SHOP_NAME_OUTLINE_HEX[Math.max(1, Math.floor(goods.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 = String(row?.desc ?? ''); } const complete = item.getChildByName('complete'); const buyBtn = item.getChildByName('buyBtn'); const buyBtnAd = item.getChildByName('buyBtnAd'); const lockBuy = this._isBuyLocked(); const priceNode = buyBtn?.getChildByName('lay')?.getChildByName('price'); if (priceNode?.isValid) { const lb = priceNode.getComponent(Label); if (lb) { const cost = Math.max(0, Math.floor(Number(goods.cost) || 0)); lb.string = `${cost}`; if (lockBuy) { lb.color = new Color(255, 255, 255, 255); } else { const enough = gg.data.getItemNum(ItemNumType.money) >= cost; lb.color = enough ? new Color(255, 255, 255, 255) : new Color(255, 0, 0, 255); } } } if (goods.sold) { if (complete?.isValid) complete.active = true; if (buyBtn?.isValid) buyBtn.active = false; if (buyBtnAd?.isValid) buyBtnAd.active = false; return; } if (complete?.isValid) complete.active = false; if (goods.isAd) { if (buyBtnAd?.isValid) { buyBtnAd.active = true; this._setBuyBtnGray(buyBtnAd, lockBuy); this._bindBuyBtn(buyBtnAd, goods.id, true); } if (buyBtn?.isValid) buyBtn.active = false; } else { if (buyBtn?.isValid) { buyBtn.active = true; this._setBuyBtnGray(buyBtn, lockBuy); this._bindBuyBtn(buyBtn, goods.id, false); } if (buyBtnAd?.isValid) buyBtnAd.active = false; } } private _isBuyLocked(): boolean { return DailyShopDataManager.hasUsedShopPurchase(); } private _setBuyBtnGray(btn: Node, gray: boolean): void { const walk = (n: Node) => { const sp = n.getComponent(Sprite); if (sp) sp.grayscale = gray; const kids = n.children; for (let i = 0; i < kids.length; i++) walk(kids[i]); }; walk(btn); } private _bindBuyBtn(btn: Node, goodsId: number, payByAd: boolean): void { btn['dailyShopGoodsId'] = goodsId; btn['dailyShopPayByAd'] = payByAd; btn.targetOff(Node.EventType.TOUCH_END); btn.on(Node.EventType.TOUCH_END, this._onClickBuy, this); } private _onClickBuy(event: EventTouch): void { if (this._busy) return; if (this._isBuyLocked()) { gg.ui.showToast(BUY_USED_TIP, false); return; } const btn = event.currentTarget as Node; const shopItem = btn.parent; const sourceIcon = shopItem?.isValid ? DailyShopLotteryIcon.resolveShopIcon(shopItem) : null; const goodsId = Number(btn?.['dailyShopGoodsId']) || 0; const payByAd = !!btn?.['dailyShopPayByAd']; if (goodsId <= 0) return; if (payByAd) { gg.sdk.showVideoAd((res: boolean) => { if (!res) { gg.ui.showToast('观看视频失败', false); return; } this._tryPurchase(goodsId, true, sourceIcon); }); return; } this._tryPurchase(goodsId, false, sourceIcon); } private _tryPurchase(goodsId: number, payByAd: boolean, sourceIcon?: Node | null): void { if (this._busy) return; this._busy = true; const result = DailyShopDataManager.purchase(goodsId, { payByAd }); this._handlePurchaseResult(goodsId, result, payByAd, sourceIcon ?? null); } private _handlePurchaseResult( goodsId: number, result: DailyShopPurchaseResult, payByAd = false, sourceIcon: Node | null = null, ): void { if (result.ok) { const name = DailyShopDataManager.getParsed(goodsId)?.name ?? ''; if (payByAd) RankReport.adShopBuy(); RankReport.shopBuy(name); this._showPurchaseSuccessToast(result); gg.dot?.updateRedDot(); if (result.instant) { this._busy = false; void this._refreshView(); return; } const slotIndex = result.slotIndex; const slotKind = result.slotKind ?? result.parsed.kind; if (slotIndex != null && slotIndex >= 0 && sourceIcon?.isValid) { this._playBuyToEmptySlotAnim(sourceIcon, result.parsed, slotKind, slotIndex); return; } this._busy = false; void this._refreshView(); return; } this._busy = false; if (isDailyShopNeedReplace(result)) { if (payByAd) RankReport.adShopBuy(); void this._refreshView(); this._askReplace(goodsId); return; } gg.ui.showToast(this._failTip(dailyShopFailReason(result)), false); } /** 购买成功提示文案 */ 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); } /** 购买入空槽:货架 icon 影子飞行 + 技能放大 */ private _playBuyToEmptySlotAnim( sourceIcon: Node, parsed: DailyShopParsed, slotKind: DailyShopKind, slotIndex: number, ): void { const slotBase = this._getSlotBase(slotKind, slotIndex); const fly = this._skillFly; if (!sourceIcon?.isValid || !slotBase?.isValid || !fly) { console.log('[PopDailyShopBuy] 购买飞行动画参数无效'); this._busy = false; void this._refreshView(); return; } fly.playIconFly(sourceIcon, slotBase, this._onBuyIconFlyArrived.bind(this, parsed, slotKind, slotIndex)); this._renderOwnedSkills({ kind: slotKind, slotIndex }); this._renderOffers(DailyShopDataManager.getOffers()); } /** 购买 icon 飞行结束:显示技能并放大缩小 */ 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._busy = false; void this._refreshView(); return; } fly.playSlotPulse(skillItem, this._onBuySkillPulseDone.bind(this)); } /** 购买技能放大动画结束 */ private _onBuySkillPulseDone(): void { this._busy = false; } /** 在槽位空模板上显示新技能 */ 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]; } /** 槽位上的技能节点(排除 deleteBtn) */ 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): void { const base = this._getSlotBase(kind, slotIndex); const skillItem = base?.isValid ? this._findSlotSkillItem(base) : null; const fly = this._skillFly; if (!skillItem?.isValid || !fly) { this._busy = false; return; } fly.playSlotPulse(skillItem, this._onReplaceSkillPulseDone.bind(this)); } /** 替换技能放大动画结束 */ private _onReplaceSkillPulseDone(): void { this._busy = false; } private _askReplace(goodsId: number): void { const parsed = DailyShopDataManager.getParsed(goodsId); if (!parsed) return; const entries = DailyShopDataManager.getOccupiedSlotEntries(parsed.kind); if (entries.length <= 0) { gg.ui.showToast('没有可替换的道具', false); return; } gg.ui.openPanel(UIID.PopReplaceSkill, { data: { goodsId, onConfirm: (replaceOwnedId: number) => { this._completeReplace(replaceOwnedId); }, onCancel: () => { void this._refreshView(); }, } satisfies PopReplaceSkillParam, }); } private _completeReplace(replaceOwnedId: number): void { if (this._busy) return; this._busy = true; const result = DailyShopDataManager.completeReplace(replaceOwnedId); this._handleReplaceResult(result); } private _handleReplaceResult(result: DailyShopPurchaseResult): void { if (result.ok) { RankReport.shopBuy(result.parsed?.name ?? ''); 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._busy = false; 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); return; } this._busy = false; } private _failTip(reason: DailyShopPurchaseFail): string { switch (reason) { case DailyShopPurchaseFail.NotEnoughMoney: return '铜币不足'; case DailyShopPurchaseFail.AlreadySold: return '已售罄'; case DailyShopPurchaseFail.NotInOffer: return '不在今日货架'; case DailyShopPurchaseFail.NotAdOffer: return '该商品不可广告购买'; case DailyShopPurchaseFail.InvalidReplace: return '替换目标无效'; case DailyShopPurchaseFail.PendingReplace: return '请先完成替换'; default: return '购买失败'; } } }