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/PopDailyShopBuy/UIPopDailyShopBuy.ts

584 lines
22 KiB

3 weeks ago
//*********************
// 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<number, string> = {
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<void> {
await this.close(2);
DailyShopDataManager.tryPopLotteryAfterBuyClose();
gg.ui.flushPopQueue();
}
private async _resetAndShow(): Promise<void> {
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<void> {
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 '购买失败';
}
}
}