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.
640 lines
24 KiB
640 lines
24 KiB
|
3 weeks ago
|
|
||
|
|
//*********************
|
||
|
|
// create by 流云
|
||
|
|
// time: Wed Aug 26 2026
|
||
|
|
// desc: 每日商店抽奖弹窗(Battlebuff 8 格,与 PopDailyShopBuy 分离)
|
||
|
|
//*********************
|
||
|
|
import { _decorator, Color, EventTouch, instantiate, Label, Layout, Node, Sprite, tween, v3 } from 'cc';
|
||
|
|
import { Auto_PopDailyShopLottery } from './Auto_PopDailyShopLottery';
|
||
|
|
import { DailyShopLotteryIcon, DailyShopOwnedDesc } from './DailyShopLotteryIcon';
|
||
|
|
import { DailyShopBuySkillFlyPresenter } from '../PopDailyShopBuy/DailyShopBuySkillFlyPresenter';
|
||
|
|
import { GEvent } from '../../../mx/module/event/GEvent';
|
||
|
|
import { GAudioPath, UIID } from '../../game/ConfigRes';
|
||
|
|
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 BUY_USED_TIP = '商品已售罄,欢迎下次光临!';
|
||
|
|
const LOTTERY_SLOT_COUNT = 8;
|
||
|
|
/** 顺时针:lottery2 → … → lottery8 → lottery1 */
|
||
|
|
const LOTTERY_CW_INDEX = [1, 2, 3, 4, 5, 6, 7, 0];
|
||
|
|
const LOTTERY_SPIN_LAPS = 2;
|
||
|
|
const LOTTERY_SPIN_STEP_MS = 0.08;
|
||
|
|
|
||
|
|
/** 抽奖格名称描边色(按品质) */
|
||
|
|
const LOTTERY_NAME_OUTLINE_HEX: Record<number, string> = {
|
||
|
|
1: '#386c42',
|
||
|
|
2: '#285c6d',
|
||
|
|
3: '#623073',
|
||
|
|
4: '#734930',
|
||
|
|
};
|
||
|
|
|
||
|
|
@ccclass('UIPopDailyShopLottery')
|
||
|
|
export class UIPopDailyShopLottery extends Auto_PopDailyShopLottery {
|
||
|
|
|
||
|
|
private _busy = false;
|
||
|
|
private _btnLotteryDraw: Node | null = null;
|
||
|
|
private _lotteryItems: Node[] = [];
|
||
|
|
private _lotteryPoolIds: number[] = [];
|
||
|
|
private _lotterySpinToken = 0;
|
||
|
|
private _lotteryWinSlotIndex = -1;
|
||
|
|
private _lotteryPendingWinId = 0;
|
||
|
|
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_lotteryItem?.isValid) this.ui_lotteryItem.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;
|
||
|
|
this._btnLotteryDraw = this.ui_lottery?.getChildByName('btnLottery') ?? null;
|
||
|
|
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);
|
||
|
|
if (this._btnLotteryDraw?.isValid) {
|
||
|
|
this._btnLotteryDraw.on(Node.EventType.TOUCH_END, this._onClickLotteryDraw, this);
|
||
|
|
}
|
||
|
|
void this._refreshView();
|
||
|
|
}
|
||
|
|
|
||
|
|
onDisable(): void {
|
||
|
|
GEvent.Ins.off(GEvent.DailyShopOffersRefresh, this._onOffersRefresh, this);
|
||
|
|
if (this._btnLotteryDraw?.isValid) {
|
||
|
|
this._btnLotteryDraw.off(Node.EventType.TOUCH_END, this._onClickLotteryDraw, this);
|
||
|
|
}
|
||
|
|
this._ownedDesc?.unbindDismiss();
|
||
|
|
this._skillFly?.unbind();
|
||
|
|
this._interruptLotteryDraw();
|
||
|
|
this._lotterySpinToken++;
|
||
|
|
this._lotteryWinSlotIndex = -1;
|
||
|
|
this._lotteryPendingWinId = 0;
|
||
|
|
super.onDisable();
|
||
|
|
}
|
||
|
|
|
||
|
|
onInit(): void {
|
||
|
|
this._busy = false;
|
||
|
|
this.showOpenCenterEff();
|
||
|
|
this.showOpenMaskEff(2);
|
||
|
|
void this._resetAndShow();
|
||
|
|
}
|
||
|
|
|
||
|
|
click_btnClose(): void {
|
||
|
|
this._interruptLotteryDraw();
|
||
|
|
this.close(2);
|
||
|
|
}
|
||
|
|
|
||
|
|
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(): void {
|
||
|
|
if (!this.isValid) return;
|
||
|
|
if (this._busy) return;
|
||
|
|
this._renderOwnedSkills();
|
||
|
|
if (!DailyShopDataManager.hasUsedLotteryPurchase()) {
|
||
|
|
this._renderLottery();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async _refreshView(): Promise<void> {
|
||
|
|
await DailyShopDataManager.ensureTable();
|
||
|
|
if (!this.isValid) return;
|
||
|
|
this._renderOwnedSkills();
|
||
|
|
this._renderLottery();
|
||
|
|
this._refreshLotteryDrawBtn();
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
// 有技能则叠在空模板上面(hideSlotIndex 指定的槽位不显示技能)
|
||
|
|
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 _renderLottery(): void {
|
||
|
|
if (!this.ui_lottery?.isValid || !this.ui_lotteryItem?.isValid) return;
|
||
|
|
this._lotteryItems = [];
|
||
|
|
this._lotteryPoolIds = [];
|
||
|
|
const pool = DailyShopDataManager.getLotteryPoolSlots();
|
||
|
|
for (let i = 0; i < LOTTERY_SLOT_COUNT; i++) {
|
||
|
|
const slot = this.ui_lottery.getChildByName(`lottery${i + 1}`);
|
||
|
|
if (!slot?.isValid) continue;
|
||
|
|
slot.removeAllChildren();
|
||
|
|
const parsed = pool[i];
|
||
|
|
if (!parsed) continue;
|
||
|
|
const item = instantiate(this.ui_lotteryItem);
|
||
|
|
item.active = true;
|
||
|
|
item.setPosition(v3(0, 0, 0));
|
||
|
|
item.parent = slot;
|
||
|
|
this._bindLotteryItem(item, parsed);
|
||
|
|
this._lotteryItems[i] = item;
|
||
|
|
this._lotteryPoolIds[i] = parsed.id;
|
||
|
|
}
|
||
|
|
this._lotteryWinSlotIndex = -1;
|
||
|
|
this._clearLotterySelect();
|
||
|
|
this._selectDefaultLotteryItem();
|
||
|
|
this._refreshLotteryDrawBtn();
|
||
|
|
}
|
||
|
|
|
||
|
|
private _selectDefaultLotteryItem(): void {
|
||
|
|
if (this._lotteryItems.length <= 0 || !this._lotteryItems[0]?.isValid) return;
|
||
|
|
this._selectLotterySlot(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _selectLotterySlot(slotIndex: number): void {
|
||
|
|
this._clearLotterySelect();
|
||
|
|
const item = this._lotteryItems[slotIndex];
|
||
|
|
if (!item?.isValid) return;
|
||
|
|
const parsed = this._getLotteryItemParsed(item, slotIndex);
|
||
|
|
this._applyLotteryItemSelect(item, parsed, true);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _getLotteryItemParsed(item: Node, slotIndex: number): DailyShopParsed | null {
|
||
|
|
const cached = item['dailyShopLotteryParsed'] as DailyShopParsed | undefined;
|
||
|
|
if (cached) return cached;
|
||
|
|
const rawId = Number(item['dailyShopLotteryId']) || Number(this._lotteryPoolIds[slotIndex]) || 0;
|
||
|
|
if (rawId <= 0) return null;
|
||
|
|
return DailyShopDataManager.getParsed(rawId);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _applyLotteryItemSelect(item: Node, parsed: DailyShopParsed | null, selected: boolean): void {
|
||
|
|
const select = item.getChildByName('select');
|
||
|
|
if (select?.isValid) select.active = selected;
|
||
|
|
const main = item.getChildByName('main');
|
||
|
|
if (main?.isValid) {
|
||
|
|
main.active = parsed?.kind === DailyShopKind.Active;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _bindLotteryItem(item: Node, parsed: DailyShopParsed): void {
|
||
|
|
DailyShopLotteryIcon.bindLotteryItem(item, parsed);
|
||
|
|
// 设置名称与品质描边
|
||
|
|
const nameNode = item.getChildByName('name');
|
||
|
|
if (nameNode?.isValid) {
|
||
|
|
const lb = nameNode.getComponent(Label);
|
||
|
|
if (lb) {
|
||
|
|
lb.string = parsed.name;
|
||
|
|
const hex = LOTTERY_NAME_OUTLINE_HEX[Math.max(1, Math.floor(parsed.quality) || 1)];
|
||
|
|
if (hex) {
|
||
|
|
lb.enableOutline = true;
|
||
|
|
lb.outlineColor = Color.fromHEX(new Color(), hex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
item['dailyShopLotteryId'] = parsed.id;
|
||
|
|
item['dailyShopLotteryParsed'] = parsed;
|
||
|
|
this._ownedDesc?.bindLotteryItem(item, parsed);
|
||
|
|
this._applyLotteryItemSelect(item, parsed, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _clearLotterySelect(): void {
|
||
|
|
for (let i = 0; i < this._lotteryItems.length; i++) {
|
||
|
|
const item = this._lotteryItems[i];
|
||
|
|
if (!item?.isValid) continue;
|
||
|
|
const parsed = this._getLotteryItemParsed(item, i);
|
||
|
|
this._applyLotteryItemSelect(item, parsed, false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _showLotterySelect(slotIndex: number): void {
|
||
|
|
this._selectLotterySlot(slotIndex);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _refreshLotteryDrawBtn(): void {
|
||
|
|
if (!this._btnLotteryDraw?.isValid) return;
|
||
|
|
this._setBuyBtnGray(this._btnLotteryDraw, this._isBuyLocked());
|
||
|
|
}
|
||
|
|
|
||
|
|
private _isBuyLocked(): boolean {
|
||
|
|
return DailyShopDataManager.hasUsedLotteryPurchase();
|
||
|
|
}
|
||
|
|
|
||
|
|
private _onClickLotteryDraw(): void {
|
||
|
|
if (this._busy) return;
|
||
|
|
if (this._isBuyLocked()) {
|
||
|
|
gg.ui.showToast(BUY_USED_TIP, false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this._busy = true;
|
||
|
|
gg.sdk.showVideoAd((res: boolean) => {
|
||
|
|
if (!res) {
|
||
|
|
this._busy = false;
|
||
|
|
gg.ui.showToast('观看视频失败', false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
RankReport.adLottery();
|
||
|
|
void this._runLotteryDraw();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private async _runLotteryDraw(): Promise<void> {
|
||
|
|
const winId = DailyShopDataManager.rollLotteryWinnerId();
|
||
|
|
const winSlot = this._lotteryPoolIds.indexOf(winId);
|
||
|
|
if (winId <= 0 || winSlot < 0) {
|
||
|
|
this._busy = false;
|
||
|
|
gg.ui.showToast('抽奖失败', false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this._lotteryPendingWinId = winId;
|
||
|
|
const spinToken = ++this._lotterySpinToken;
|
||
|
|
|
||
|
|
// 1. 转盘(含缩放跳动)
|
||
|
|
await this._playLotterySpin(winSlot, spinToken);
|
||
|
|
if (this._lotteryPendingWinId !== winId) return;
|
||
|
|
|
||
|
|
if (spinToken !== this._lotterySpinToken || !this.isValid) {
|
||
|
|
this._settlePendingLottery(true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. 呼吸 2 次
|
||
|
|
const winItem = this._lotteryItems[winSlot];
|
||
|
|
if (winItem?.isValid) {
|
||
|
|
await this._playWinBreathing(winItem, spinToken);
|
||
|
|
if (this._lotteryPendingWinId !== winId) return;
|
||
|
|
|
||
|
|
if (spinToken !== this._lotterySpinToken || !this.isValid) {
|
||
|
|
this._settlePendingLottery(true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 购买(数据入槽)
|
||
|
|
this._lotteryPendingWinId = 0;
|
||
|
|
const result = DailyShopDataManager.purchaseLottery(winId);
|
||
|
|
|
||
|
|
if (result.ok || isDailyShopNeedReplace(result)) {
|
||
|
|
this._lotteryWinSlotIndex = winSlot;
|
||
|
|
this._showLotterySelect(winSlot);
|
||
|
|
} else {
|
||
|
|
this._clearLotterySelect();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. 飞行到道具栏(成功且非即时,与购买逻辑一致)
|
||
|
|
if (result.ok && !result.instant
|
||
|
|
&& result.slotIndex != null && result.slotIndex >= 0
|
||
|
|
&& result.parsed && result.slotKind != null) {
|
||
|
|
const slotKind = result.slotKind;
|
||
|
|
const slotIndex = result.slotIndex;
|
||
|
|
const sourceIcon = winItem?.isValid
|
||
|
|
? (DailyShopLotteryIcon.resolveShopIcon(winItem) ?? winItem)
|
||
|
|
: null;
|
||
|
|
const slotBase = this._getSlotBase(slotKind, slotIndex);
|
||
|
|
const fly = this._skillFly;
|
||
|
|
|
||
|
|
// 音效 + 提示
|
||
|
|
gg.ui.showToast('抽奖成功', false);
|
||
|
|
gg.dot?.updateRedDot();
|
||
|
|
|
||
|
|
if (sourceIcon?.isValid && slotBase?.isValid && fly) {
|
||
|
|
// 先起飞(终点取当前槽位世界坐标),再隐藏目标槽技能
|
||
|
|
fly.playIconFly(sourceIcon, slotBase, () => {
|
||
|
|
if (!this.isValid) return;
|
||
|
|
this._busy = false;
|
||
|
|
this._refreshAfterLotteryUse();
|
||
|
|
// 落点脉冲(与购买一致)
|
||
|
|
const base = this._getSlotBase(slotKind, slotIndex);
|
||
|
|
const skillItem = base?.isValid ? this._findSlotSkillItem(base) : null;
|
||
|
|
if (skillItem?.isValid) fly.playSlotPulse(skillItem);
|
||
|
|
});
|
||
|
|
this._renderOwnedSkills({ kind: slotKind, slotIndex });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this._busy = false;
|
||
|
|
this._refreshAfterLotteryUse();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 其它情况(即时/替换/失败)
|
||
|
|
this._busy = false;
|
||
|
|
this._handleLotteryResult(result);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _interruptLotteryDraw(): void {
|
||
|
|
if (this._lotteryPendingWinId <= 0) return;
|
||
|
|
this._lotterySpinToken++;
|
||
|
|
this._settlePendingLottery(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _settlePendingLottery(interrupted: boolean): void {
|
||
|
|
const winId = this._lotteryPendingWinId;
|
||
|
|
if (winId <= 0) return;
|
||
|
|
this._lotteryPendingWinId = 0;
|
||
|
|
this._busy = false;
|
||
|
|
const result = DailyShopDataManager.purchaseLottery(winId);
|
||
|
|
if (!interrupted) return;
|
||
|
|
this._clearLotterySelect();
|
||
|
|
if (result.ok || isDailyShopNeedReplace(result)) {
|
||
|
|
gg.dot?.updateRedDot();
|
||
|
|
if (this.isValid) {
|
||
|
|
this._refreshAfterLotteryUse();
|
||
|
|
if (isDailyShopNeedReplace(result)) {
|
||
|
|
this._askReplace(DailyShopDataManager.getPendingReplaceGoodsId());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 顺时针转盘高亮,每进一格播一次音效 */
|
||
|
|
private async _playLotterySpin(winSlotIndex: number, token: number): Promise<void> {
|
||
|
|
this._clearLotterySelect();
|
||
|
|
const cw = LOTTERY_CW_INDEX;
|
||
|
|
let winCwIdx = cw.indexOf(winSlotIndex);
|
||
|
|
if (winCwIdx < 0) winCwIdx = 0;
|
||
|
|
const totalSteps = LOTTERY_SPIN_LAPS * cw.length + winCwIdx + 1;
|
||
|
|
let prevItem: Node | null = null;
|
||
|
|
for (let step = 0; step < totalSteps; step++) {
|
||
|
|
if (token !== this._lotterySpinToken || !this.isValid) return;
|
||
|
|
this._clearLotterySelect();
|
||
|
|
const slotIdx = cw[step % cw.length];
|
||
|
|
const item = this._lotteryItems[slotIdx];
|
||
|
|
// 前一格回到原始缩放
|
||
|
|
if (prevItem?.isValid) {
|
||
|
|
prevItem.setScale(1, 1, 1);
|
||
|
|
}
|
||
|
|
// 当前进格放大 1.1
|
||
|
|
if (item?.isValid) {
|
||
|
|
item.setScale(1.1, 1.1, 1);
|
||
|
|
const select = item.getChildByName('select');
|
||
|
|
if (select?.isValid) select.active = true;
|
||
|
|
prevItem = item;
|
||
|
|
}
|
||
|
|
this._playLotterySpinTick();
|
||
|
|
const tail = totalSteps - step;
|
||
|
|
const delay = tail <= 6
|
||
|
|
? LOTTERY_SPIN_STEP_MS + (6 - tail) * 0.04
|
||
|
|
: LOTTERY_SPIN_STEP_MS;
|
||
|
|
await this._waitSec(delay);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 转盘进一格时播一次音效 */
|
||
|
|
private _playLotterySpinTick(): void {
|
||
|
|
gg.audio.playEffect({ name: '抽奖转盘', path: GAudioPath.SfxUi });
|
||
|
|
}
|
||
|
|
|
||
|
|
private _waitSec(sec: number): Promise<void> {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
this.scheduleOnce(resolve, sec);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 呼吸 2 次(1→1.1→1 循环 2 次) */
|
||
|
|
private async _playWinBreathing(item: Node, token: number): Promise<void> {
|
||
|
|
// 先回到原始缩放(spinning 可能留下 1.1)
|
||
|
|
item.setScale(1, 1, 1);
|
||
|
|
for (let i = 0; i < 2; i++) {
|
||
|
|
if (token !== this._lotterySpinToken || !this.isValid) return;
|
||
|
|
await this._tweenScale(item, 1.1, 0.15);
|
||
|
|
if (token !== this._lotterySpinToken || !this.isValid) return;
|
||
|
|
await this._tweenScale(item, 1, 0.15);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 缩放 tween → Promise */
|
||
|
|
private _tweenScale(node: Node, scale: number, dur: number): Promise<void> {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
if (!node?.isValid) { resolve(); return; }
|
||
|
|
tween(node)
|
||
|
|
.to(dur, { scale: v3(scale, scale, 1) })
|
||
|
|
.call(resolve)
|
||
|
|
.start();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 槽位空模板节点 */
|
||
|
|
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 _handleLotteryResult(result: DailyShopPurchaseResult): void {
|
||
|
|
if (result.ok) {
|
||
|
|
if (result.instant) {
|
||
|
|
gg.ui.showToast('抽奖成功', false);
|
||
|
|
} else if (result.replacedId) {
|
||
|
|
const oldName = DailyShopDataManager.getParsed(result.replacedId)?.name ?? '';
|
||
|
|
gg.ui.showToast(oldName ? `已替换「${oldName}」` : '抽奖成功', false);
|
||
|
|
} else {
|
||
|
|
gg.ui.showToast('抽奖成功', false);
|
||
|
|
}
|
||
|
|
this._refreshAfterLotteryUse();
|
||
|
|
gg.dot?.updateRedDot();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (isDailyShopNeedReplace(result)) {
|
||
|
|
this._refreshAfterLotteryUse();
|
||
|
|
this._askReplace(DailyShopDataManager.getPendingReplaceGoodsId());
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
gg.ui.showToast(this._failTip(dailyShopFailReason(result)), false);
|
||
|
|
this._clearLotterySelect();
|
||
|
|
}
|
||
|
|
|
||
|
|
private _refreshAfterLotteryUse(): void {
|
||
|
|
this._renderOwnedSkills();
|
||
|
|
this._refreshLotteryDrawBtn();
|
||
|
|
}
|
||
|
|
|
||
|
|
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 _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: () => {
|
||
|
|
this._refreshAfterLotteryUse();
|
||
|
|
},
|
||
|
|
} satisfies PopReplaceSkillParam,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private _completeReplace(replaceOwnedId: number): void {
|
||
|
|
if (this._busy) return;
|
||
|
|
this._busy = true;
|
||
|
|
const result = DailyShopDataManager.completeReplace(replaceOwnedId);
|
||
|
|
this._busy = false;
|
||
|
|
if (result.ok) {
|
||
|
|
RankReport.shopBuy(result.parsed?.name ?? '');
|
||
|
|
const oldName = result.replacedId
|
||
|
|
? DailyShopDataManager.getParsed(result.replacedId)?.name ?? ''
|
||
|
|
: '';
|
||
|
|
gg.ui.showToast(oldName ? `已替换「${oldName}」` : '抽奖成功', false);
|
||
|
|
this._refreshAfterLotteryUse();
|
||
|
|
gg.dot?.updateRedDot();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
gg.ui.showToast(this._failTip(dailyShopFailReason(result)), false);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _failTip(reason: DailyShopPurchaseFail): string {
|
||
|
|
switch (reason) {
|
||
|
|
case DailyShopPurchaseFail.AlreadySold:
|
||
|
|
return '已售罄';
|
||
|
|
case DailyShopPurchaseFail.NotInOffer:
|
||
|
|
return '不在今日奖池';
|
||
|
|
case DailyShopPurchaseFail.InvalidReplace:
|
||
|
|
return '替换目标无效';
|
||
|
|
case DailyShopPurchaseFail.PendingReplace:
|
||
|
|
return '请先完成替换';
|
||
|
|
default:
|
||
|
|
return '抽奖失败';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|