|
|
|
|
import { ConstNumType, ItemNumType, OtherDataType } from '../game/GameDataEnums';
|
|
|
|
|
import { TableNames } from '../game/ConfigTableData';
|
|
|
|
|
import { UIID } from '../game/ConfigRes';
|
|
|
|
|
import { GEvent } from '../../mx/module/event/GEvent';
|
|
|
|
|
import { WeightedPick } from '../tools/WeightedPick';
|
|
|
|
|
import {
|
|
|
|
|
parseBattleBuffRow,
|
|
|
|
|
isBattleBuffParsed,
|
|
|
|
|
isBattleBuffInstant,
|
|
|
|
|
battleBuffRowKind,
|
|
|
|
|
battleBuffSlotType,
|
|
|
|
|
} from './BattleBuffParse';
|
|
|
|
|
import { BattleBuffSlotType } from './battleBuff/BattleBuffTypes';
|
|
|
|
|
import { BattleBuffEffectRegistry } from './battleBuff/BattleBuffEffectRegistry';
|
|
|
|
|
import { BattleBuffEffectSource } from './battleBuff/BattleBuffTypes';
|
|
|
|
|
import type { IBattleBuffEffectContext } from './battleBuff/BattleBuffEffectTypes';
|
|
|
|
|
import { toDailyShopActiveSkill } from './DailyShopParse';
|
|
|
|
|
import {
|
|
|
|
|
BATTLE_BUFF_STORE_OFFSET,
|
|
|
|
|
DAILY_SHOP_ACTIVE_SLOTS,
|
|
|
|
|
DAILY_SHOP_OFFER_COUNT,
|
|
|
|
|
DAILY_SHOP_LOTTERY_COUNT,
|
|
|
|
|
DAILY_SHOP_OPEN_COST,
|
|
|
|
|
DAILY_SHOP_PASSIVE_SLOTS,
|
|
|
|
|
DailyShopActiveSkill,
|
|
|
|
|
DailyShopGoods,
|
|
|
|
|
DailyShopKind,
|
|
|
|
|
DailyShopParsed,
|
|
|
|
|
DailyShopSlotEntry,
|
|
|
|
|
DailyShopPassiveType,
|
|
|
|
|
DailyShopPurchaseFail,
|
|
|
|
|
DailyShopPurchaseResult,
|
|
|
|
|
} from './DailyShopTypes';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 每日商店数据层(Battlebuff 表):
|
|
|
|
|
* - 按日刷货架 3 格 + 抽奖 8 格(权重、不重复;仅 roll 时排除已携带)
|
|
|
|
|
* - 商品池仅在胜利结算 refreshPoolsOnSettle 或凌晨跨天 applyDayResetIfNeeded 时重抽
|
|
|
|
|
* - 凌晨跨天重置仅在登录 / 胜利结算 / 失败结算时触发,对局中过零点不重置
|
|
|
|
|
* - 购买/抽奖入槽后不剔除池内商品、不补抽;手动 refreshOffers 只重置次数
|
|
|
|
|
* - 携带槽位数:ConstConf 78 constgroup[0] 主动、[1] 被动
|
|
|
|
|
* - 购买成功即写入携带(或即时类直接发奖),供战斗侧读取生效
|
|
|
|
|
*
|
|
|
|
|
* 存档格式(OtherDataType.DailyShopState,禁止下划线):
|
|
|
|
|
* day|offers|passive|active|sold|offerAd|pending|pendingAd|lottery|lotteryUsed|lotteryTabOpen|openGold|openAd|openGuide
|
|
|
|
|
* 空槽用 0;offerAd 为 1 表示该格广告购、0 表示铜币购(Const 21 权重决定 0~2 格广告,随机分配)。
|
|
|
|
|
* 入槽 id 为 Battlebuff.id + BATTLE_BUFF_STORE_OFFSET(10000)。
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/** GM「指定商店被动」:空数组则走存档 */
|
|
|
|
|
export let DEBUG_FORCE_CARRY_PASSIVE_IDS: number[] = [];
|
|
|
|
|
/** GM「指定商店主动」:空数组则走存档 */
|
|
|
|
|
export let DEBUG_FORCE_CARRY_ACTIVE_IDS: number[] = [];
|
|
|
|
|
|
|
|
|
|
function _sanitizeShopIds(ids: number[] | null | undefined): number[] {
|
|
|
|
|
return (ids ?? [])
|
|
|
|
|
.map((x) => Math.floor(Number(x) || 0))
|
|
|
|
|
.filter((x) => x > 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 设置强制携带被动(仅内存);空=关闭并恢复存档槽 */
|
|
|
|
|
export function setDebugForceCarryPassiveIds(ids: number[] | null | undefined): void {
|
|
|
|
|
DEBUG_FORCE_CARRY_PASSIVE_IDS = _sanitizeShopIds(ids);
|
|
|
|
|
DailyShopDataManager.syncGmForceCarry();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 设置强制携带主动(仅内存);空=关闭并恢复存档槽 */
|
|
|
|
|
export function setDebugForceCarryActiveIds(ids: number[] | null | undefined): void {
|
|
|
|
|
DEBUG_FORCE_CARRY_ACTIVE_IDS = _sanitizeShopIds(ids);
|
|
|
|
|
DailyShopDataManager.syncGmForceCarry();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** GM:第 1 携带槽写入结果 */
|
|
|
|
|
export type DailyShopGmFirstSlotResult = {
|
|
|
|
|
/** 是否成功(含清空) */
|
|
|
|
|
ok: boolean;
|
|
|
|
|
/** 写入后的 buff;清空时 null */
|
|
|
|
|
parsed: DailyShopParsed | null;
|
|
|
|
|
/** 解析到的 id 与槽位 type 不符 */
|
|
|
|
|
wrongKind: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析 GM 输入为 Battlebuff.id。
|
|
|
|
|
* 支持 id、名称;空串或 0 → null(关闭)。kind 过滤被动/主动。
|
|
|
|
|
*/
|
|
|
|
|
export function parseGmDailyShopCarryIds(raw: string, kind: DailyShopKind): number[] | null {
|
|
|
|
|
const trimmed = String(raw ?? '').trim();
|
|
|
|
|
if (!trimmed || trimmed === '0') return null;
|
|
|
|
|
if (typeof gg === 'undefined' || !gg?.data?.table) return null;
|
|
|
|
|
|
|
|
|
|
const all = gg.data.table.getTableList<ITableBattlebuff>(TableNames.Battlebuff) ?? [];
|
|
|
|
|
const ids: number[] = [];
|
|
|
|
|
for (const part of trimmed.split(/[,|,\s]+/)) {
|
|
|
|
|
const token = part.trim();
|
|
|
|
|
if (!token) continue;
|
|
|
|
|
const num = Number(token);
|
|
|
|
|
let row: ITableBattlebuff | undefined;
|
|
|
|
|
if (Number.isFinite(num) && num > 0) {
|
|
|
|
|
row = all.find((w) => (w.id | 0) === (num | 0));
|
|
|
|
|
} else {
|
|
|
|
|
row = all.find((w) => String(w.name ?? '').trim() === token);
|
|
|
|
|
}
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
if (battleBuffRowKind(row) !== kind) continue;
|
|
|
|
|
const id = row.id | 0;
|
|
|
|
|
if (id > 0 && ids.indexOf(id) < 0) ids.push(id);
|
|
|
|
|
}
|
|
|
|
|
return ids.length > 0 ? ids : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class DailyShopDataManager {
|
|
|
|
|
/** GM「每日商店/抽奖无限次数」:true 时不限制货架购买和抽奖次数 */
|
|
|
|
|
public static GM_UNLIMITED_DAILY_SHOP = false;
|
|
|
|
|
private static _day = '';
|
|
|
|
|
private static _offers: number[] = [];
|
|
|
|
|
/** 与 _offers 同序:1=广告购 0=铜币购 */
|
|
|
|
|
private static _offerAd: number[] = [];
|
|
|
|
|
private static _passive: number[] = [];
|
|
|
|
|
private static _active: number[] = [];
|
|
|
|
|
private static _sold: number[] = [];
|
|
|
|
|
/** 今日抽奖池 8 格(Battlebuff 权重随机) */
|
|
|
|
|
private static _lotteryPool: number[] = [];
|
|
|
|
|
/** 本周期是否已抽奖(与货架购买独立,各 1 次) */
|
|
|
|
|
private static _lotteryUsed = false;
|
|
|
|
|
/** 本周期是否已铜币翻牌 */
|
|
|
|
|
private static _openGoldUsed = false;
|
|
|
|
|
/** 本周期是否已广告再翻 */
|
|
|
|
|
private static _openAdUsed = false;
|
|
|
|
|
/** 翻牌引导是否已用过(跨天不清) */
|
|
|
|
|
private static _openGuideUsed = false;
|
|
|
|
|
/** 已扣费但槽位满、待弹窗替换的商品 id */
|
|
|
|
|
private static _pendingPaidGoodsId = 0;
|
|
|
|
|
private static _pendingPaidByAd = false;
|
|
|
|
|
private static _loaded = false;
|
|
|
|
|
/** 胜利/失败结算领取后待弹购买窗 */
|
|
|
|
|
private static _settleBuyPop = false;
|
|
|
|
|
/** 结算链:关闭 PopDailyShopBuy 后按 Const 7 判定是否弹抽奖窗 */
|
|
|
|
|
private static _settleChainLotteryOnBuyClose = false;
|
|
|
|
|
/** 胜负结算触发了跨天重置,回大厅弹购买窗前提示 */
|
|
|
|
|
private static _settleCrossDayTip = false;
|
|
|
|
|
|
|
|
|
|
static async ensureTable(): Promise<void> {
|
|
|
|
|
await gg.data.table.ensureTableLoaded(TableNames.Battlebuff);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 携带槽位数(Const 最大主动/被动槽已删,用代码常量) */
|
|
|
|
|
static getSlotCaps(): { active: number; passive: number } {
|
|
|
|
|
return { active: DAILY_SHOP_ACTIVE_SLOTS, passive: DAILY_SHOP_PASSIVE_SLOTS };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 读档;同日仅修补缺失货架/抽奖格,不触发跨天重置 */
|
|
|
|
|
static ensureToday(): void {
|
|
|
|
|
this._loadFromDoc();
|
|
|
|
|
const day = this._todayKey();
|
|
|
|
|
if (this._day === day) {
|
|
|
|
|
if (this._offers.length < DAILY_SHOP_OFFER_COUNT) {
|
|
|
|
|
this._offers = this._rollOffers();
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
}
|
|
|
|
|
if (this._lotteryPool.length < DAILY_SHOP_LOTTERY_COUNT) {
|
|
|
|
|
this._lotteryPool = this._rollLotteryPool();
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._loaded = true;
|
|
|
|
|
this._applyDebugForceCarry();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 登录 / 胜负结算:若存档日已过凌晨 0 点则清空携带并重抽货架/抽奖。
|
|
|
|
|
* @returns 是否执行了跨天重置
|
|
|
|
|
*/
|
|
|
|
|
static applyDayResetIfNeeded(): boolean {
|
|
|
|
|
this._loadFromDoc();
|
|
|
|
|
const day = this._todayKey();
|
|
|
|
|
if (this._day === day) {
|
|
|
|
|
this._loaded = true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
this._applyCrossDayReset(day);
|
|
|
|
|
this._loaded = true;
|
|
|
|
|
this._applyDebugForceCarry();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** GM 改完强制携带后:先还原存档槽,再套内存覆盖(不写档) */
|
|
|
|
|
static syncGmForceCarry(): void {
|
|
|
|
|
if (!this._loaded) return;
|
|
|
|
|
this._reloadCarrySlotsFromDoc();
|
|
|
|
|
this._applyDebugForceCarry();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 仅内存覆盖,不写存档;空数组则保持存档携带 */
|
|
|
|
|
private static _applyDebugForceCarry(): void {
|
|
|
|
|
const forcePassive = DEBUG_FORCE_CARRY_PASSIVE_IDS.length > 0;
|
|
|
|
|
const forceActive = DEBUG_FORCE_CARRY_ACTIVE_IDS.length > 0;
|
|
|
|
|
if (!forcePassive && !forceActive) return;
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
if (forcePassive) {
|
|
|
|
|
this._passive = this._padSlots(DEBUG_FORCE_CARRY_PASSIVE_IDS, caps.passive);
|
|
|
|
|
console.log('[DailyShop] GM 强制携带被动', this._passive.slice());
|
|
|
|
|
}
|
|
|
|
|
if (forceActive) {
|
|
|
|
|
this._active = this._padSlots(DEBUG_FORCE_CARRY_ACTIVE_IDS, caps.active);
|
|
|
|
|
console.log('[DailyShop] GM 强制携带主动', this._active.slice());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 今日货架未售铜币商品的最低消耗;无则 0 */
|
|
|
|
|
static getMinUnsoldCopperCost(): number {
|
|
|
|
|
const offers = this.getOffers();
|
|
|
|
|
let min = 0;
|
|
|
|
|
for (let i = 0; i < offers.length; i++) {
|
|
|
|
|
const g = offers[i];
|
|
|
|
|
if (!g || g.sold || g.isAd) continue;
|
|
|
|
|
const c = Math.max(0, Math.floor(Number(g.cost) || 0));
|
|
|
|
|
if (c <= 0) continue;
|
|
|
|
|
if (min <= 0 || c < min) min = c;
|
|
|
|
|
}
|
|
|
|
|
return min;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 今日货架(含 sold) */
|
|
|
|
|
static getOffers(): DailyShopGoods[] {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
return this._buildGoodsList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 手动刷新:仅重置本周期购买/抽奖次数,不重抽商品池。
|
|
|
|
|
* 商品池只在胜利结算 refreshPoolsOnSettle 或凌晨跨天 applyDayResetIfNeeded 时重抽。
|
|
|
|
|
*/
|
|
|
|
|
static refreshOffers(): DailyShopGoods[] | null {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
if (this._offers.length <= 0) return null;
|
|
|
|
|
this._sold = [];
|
|
|
|
|
this._lotteryUsed = false;
|
|
|
|
|
this._openGoldUsed = false;
|
|
|
|
|
this._openAdUsed = false;
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
const offers = this._buildGoodsList();
|
|
|
|
|
GEvent.Ins.emit(GEvent.DailyShopOffersRefresh, offers);
|
|
|
|
|
console.log('[DailyShop] 手动重置购买次数 offers=', this._offers.slice(), 'lottery=', this._lotteryPool.slice());
|
|
|
|
|
return offers;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 胜利结算:重抽货架 3 格 + 抽奖 12 格,并重置购买次数。
|
|
|
|
|
*/
|
|
|
|
|
static refreshPoolsOnSettle(): DailyShopGoods[] | null {
|
|
|
|
|
const crossDay = this.applyDayResetIfNeeded();
|
|
|
|
|
if (crossDay) this._settleCrossDayTip = true;
|
|
|
|
|
const next = this._rollOffers();
|
|
|
|
|
if (next.length <= 0) return null;
|
|
|
|
|
this._offers = next;
|
|
|
|
|
this._sold = [];
|
|
|
|
|
this._lotteryUsed = false;
|
|
|
|
|
this._openGoldUsed = false;
|
|
|
|
|
this._openAdUsed = false;
|
|
|
|
|
this._lotteryPool = this._rollLotteryPool();
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
const offers = this._buildGoodsList();
|
|
|
|
|
GEvent.Ins.emit(GEvent.DailyShopOffersRefresh, offers);
|
|
|
|
|
console.log('[DailyShop] 胜利结算刷新', this._offers.slice(), 'lottery=', this._lotteryPool.slice());
|
|
|
|
|
return offers;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 本周期货架是否已购买 */
|
|
|
|
|
static hasUsedShopPurchase(): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
if (DailyShopDataManager.GM_UNLIMITED_DAILY_SHOP) return false;
|
|
|
|
|
return this._sold.length > 0 || this._openGoldUsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 翻牌窗铜币花费 */
|
|
|
|
|
static getOpenCost(): number {
|
|
|
|
|
return DAILY_SHOP_OPEN_COST;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 本周期是否已铜币翻牌 */
|
|
|
|
|
static hasUsedOpenGold(): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
if (DailyShopDataManager.GM_UNLIMITED_DAILY_SHOP) return false;
|
|
|
|
|
return this._openGoldUsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 本周期是否已广告再翻 */
|
|
|
|
|
static hasUsedOpenAd(): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
if (DailyShopDataManager.GM_UNLIMITED_DAILY_SHOP) return false;
|
|
|
|
|
return this._openAdUsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 翻牌引导是否已用过(翻过一次后永久不再显示,跨天不清) */
|
|
|
|
|
static hasUsedOpenGuide(): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
return this._openGuideUsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 标记翻牌引导已用过并写档 */
|
|
|
|
|
static markOpenGuideUsed(): void {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
if (this._openGuideUsed) return;
|
|
|
|
|
this._openGuideUsed = true;
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 翻牌预览:按权重抽 count 个不重复 Battlebuff(排除已携带,不入包、不扣费)。
|
|
|
|
|
* @param count 张数
|
|
|
|
|
*/
|
|
|
|
|
static rollOpenBuffs(count: number): DailyShopParsed[] {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const n = Math.max(0, Math.floor(Number(count) || 0));
|
|
|
|
|
if (n <= 0) return [];
|
|
|
|
|
const ids = this._pickBattleBuffBatch(n, this._ownedBattleBuffRawIds());
|
|
|
|
|
const out: DailyShopParsed[] = [];
|
|
|
|
|
for (let i = 0; i < ids.length; i++) {
|
|
|
|
|
const parsed = this._parseBattleBuffById(Number(ids[i]) || 0);
|
|
|
|
|
if (parsed) out.push(parsed);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 翻牌入包:扣费或广告后交付指定 Battlebuff。
|
|
|
|
|
* @param opts.payByAd true=不扣铜币;false=扣铜币
|
|
|
|
|
* @param opts.goodsId 指定 id;省略则现场权重抽 1 个
|
|
|
|
|
* @param opts.countAsGold true=记为铜币翻牌次数(金币不足看广告免费抽)
|
|
|
|
|
*/
|
|
|
|
|
static purchaseOpen(opts: { payByAd: boolean; goodsId?: number; countAsGold?: boolean }): DailyShopPurchaseResult {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const payByAd = !!opts?.payByAd;
|
|
|
|
|
const countAsGold = !!opts?.countAsGold;
|
|
|
|
|
if (!DailyShopDataManager.GM_UNLIMITED_DAILY_SHOP) {
|
|
|
|
|
if (!countAsGold && payByAd && this._openAdUsed) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.AlreadySold };
|
|
|
|
|
}
|
|
|
|
|
if ((countAsGold || !payByAd) && this._openGoldUsed) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.AlreadySold };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (this._pendingPaidGoodsId > 0) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.PendingReplace };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let parsed: DailyShopParsed | null = null;
|
|
|
|
|
const forceId = Math.max(0, Math.floor(Number(opts?.goodsId) || 0));
|
|
|
|
|
if (forceId > 0) {
|
|
|
|
|
parsed = this._parseBattleBuffById(forceId);
|
|
|
|
|
} else {
|
|
|
|
|
const picked = this._pickBattleBuffBatch(1, this._ownedBattleBuffRawIds());
|
|
|
|
|
const rawId = picked.length > 0 ? Number(picked[0]) || 0 : 0;
|
|
|
|
|
parsed = rawId > 0 ? this._parseBattleBuffById(rawId) : null;
|
|
|
|
|
}
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotFound };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const instant = this._isInstant(parsed);
|
|
|
|
|
if (!instant && parsed.kind !== DailyShopKind.Passive && parsed.kind !== DailyShopKind.Active) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.BadKind };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!payByAd) {
|
|
|
|
|
const payFail = this._tryPay(DAILY_SHOP_OPEN_COST);
|
|
|
|
|
if (payFail) return payFail;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (instant) {
|
|
|
|
|
this._applyInstant(parsed, { source: BattleBuffEffectSource.DailyShopBuy });
|
|
|
|
|
this._markOpenUsed(countAsGold ? false : payByAd);
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
return { ok: true, parsed, replacedId: null, instant: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._markOpenUsed(countAsGold ? false : payByAd);
|
|
|
|
|
return this._deliverToSlots(parsed, payByAd, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 本周期抽奖是否已使用 */
|
|
|
|
|
static hasUsedLotteryPurchase(): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
if (DailyShopDataManager.GM_UNLIMITED_DAILY_SHOP) return false;
|
|
|
|
|
return this._lotteryUsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 今日抽奖池(8 格 Battlebuff;不含已携带,仅 roll 预览等用) */
|
|
|
|
|
static getLotteryPool(): DailyShopParsed[] {
|
|
|
|
|
const out: DailyShopParsed[] = [];
|
|
|
|
|
for (const slot of this.getLotteryPoolSlots()) {
|
|
|
|
|
if (!slot) continue;
|
|
|
|
|
if (this._isCarriedRawId(slot.id)) continue;
|
|
|
|
|
out.push(slot);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 今日抽奖池按格位(lottery1~8 固定 8 格,购买/抽奖后不补抽) */
|
|
|
|
|
static getLotteryPoolSlots(): (DailyShopParsed | null)[] {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const out: (DailyShopParsed | null)[] = [];
|
|
|
|
|
for (let i = 0; i < DAILY_SHOP_LOTTERY_COUNT; i++) {
|
|
|
|
|
const rawId = Number(this._lotteryPool[i]) || 0;
|
|
|
|
|
if (rawId <= 0) {
|
|
|
|
|
out.push(null);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
out.push(this._parseBattleBuffById(rawId));
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 从今日 8 格中均匀随机 1 个 Battlebuff id(仅预览,不入背包;排除已携带) */
|
|
|
|
|
static rollLotteryWinnerId(): number {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const pool = this._lotteryPoolEligibleRawIds();
|
|
|
|
|
if (pool.length <= 0) return 0;
|
|
|
|
|
const idx = Math.floor(Math.random() * pool.length);
|
|
|
|
|
return pool[idx];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 看广告抽奖:从今日 8 格中取指定 Battlebuff id 入背包。
|
|
|
|
|
* 与货架购买独立,各本周期 1 次。
|
|
|
|
|
*/
|
|
|
|
|
static purchaseLottery(id: number): DailyShopPurchaseResult {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
if (!DailyShopDataManager.GM_UNLIMITED_DAILY_SHOP && this._lotteryUsed) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.AlreadySold };
|
|
|
|
|
}
|
|
|
|
|
const parsed = this._parseBattleBuffById(id);
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotFound };
|
|
|
|
|
}
|
|
|
|
|
if (!this._lotteryPool.includes(parsed.id)) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotInOffer };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const storedId = this._toStoredId(parsed);
|
|
|
|
|
if (this._pendingPaidGoodsId > 0) {
|
|
|
|
|
if (storedId === this._pendingPaidGoodsId) {
|
|
|
|
|
return this._needReplaceResult(parsed);
|
|
|
|
|
}
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.PendingReplace };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._isInstant(parsed)) {
|
|
|
|
|
this._applyInstant(parsed, { source: BattleBuffEffectSource.DailyShopLottery });
|
|
|
|
|
this._lotteryUsed = true;
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
return { ok: true, parsed, replacedId: null, instant: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parsed.kind !== DailyShopKind.Passive && parsed.kind !== DailyShopKind.Active) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.BadKind };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = this._deliverToSlots(parsed, true);
|
|
|
|
|
this._lotteryUsed = true;
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 当前携带的被动(已解析,空槽略过) */
|
|
|
|
|
static getOwnedPassive(): DailyShopParsed[] {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
return this._passive.map((id) => this._toParsed(id)).filter((p): p is DailyShopParsed => !!p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 当前携带的主动 */
|
|
|
|
|
static getOwnedActive(): DailyShopParsed[] {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
return this._active.map((id) => this._toParsed(id)).filter((p): p is DailyShopParsed => !!p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 当前携带的主动(可施放视图:id/name/cd/effect) */
|
|
|
|
|
static getOwnedActiveSkills(): DailyShopActiveSkill[] {
|
|
|
|
|
const out: DailyShopActiveSkill[] = [];
|
|
|
|
|
for (const p of this.getOwnedActive()) {
|
|
|
|
|
const skill = toDailyShopActiveSkill(p);
|
|
|
|
|
if (skill) out.push(skill);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 全部携带(开战注入用) */
|
|
|
|
|
static getCarried(): DailyShopParsed[] {
|
|
|
|
|
return [...this.getOwnedPassive(), ...this.getOwnedActive()];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 是否已携带该 Battlebuff 原始 id(兼容槽位 storedId) */
|
|
|
|
|
static isCarried(id: number): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const n = Number(id) || 0;
|
|
|
|
|
if (n <= 0) return false;
|
|
|
|
|
if (n >= BATTLE_BUFF_STORE_OFFSET) {
|
|
|
|
|
return this._isCarriedRawId(n - BATTLE_BUFF_STORE_OFFSET);
|
|
|
|
|
}
|
|
|
|
|
return this._isCarriedRawId(n);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 被动槽位 id(含 0 空槽) */
|
|
|
|
|
static getPassiveSlotIds(): number[] {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
return this._passive.slice();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 主动槽位 id(含 0 空槽) */
|
|
|
|
|
static getActiveSlotIds(): number[] {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
return this._active.slice();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 同类型已占用槽位(替换弹窗列表) */
|
|
|
|
|
static getOccupiedSlotEntries(kind: DailyShopKind): DailyShopSlotEntry[] {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const ids = kind === DailyShopKind.Passive
|
|
|
|
|
? this.getPassiveSlotIds()
|
|
|
|
|
: this.getActiveSlotIds();
|
|
|
|
|
const out: DailyShopSlotEntry[] = [];
|
|
|
|
|
for (let i = 0; i < ids.length; i++) {
|
|
|
|
|
const id = Number(ids[i]) || 0;
|
|
|
|
|
if (id <= 0) continue;
|
|
|
|
|
const parsed = this.getParsed(id);
|
|
|
|
|
if (parsed) out.push({ slotIndex: i, id, parsed });
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static getParsed(id: number): DailyShopParsed | null {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
return this._toParsed(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 货架 id 对应格是否为广告购 */
|
|
|
|
|
static isOfferAd(id: number): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const idx = this._offers.indexOf(Number(id) || 0);
|
|
|
|
|
return idx >= 0 && this._offerAd[idx] === 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 卸下指定槽位携带道具(按槽位下标,空槽返回 false) */
|
|
|
|
|
static removeSlotAt(kind: DailyShopKind, slotIndex: number): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
const isPassive = kind === DailyShopKind.Passive;
|
|
|
|
|
const cap = isPassive ? caps.passive : caps.active;
|
|
|
|
|
const idx = Math.floor(Number(slotIndex) || 0);
|
|
|
|
|
if (idx < 0 || idx >= cap) return false;
|
|
|
|
|
|
|
|
|
|
const slots = (isPassive ? this._passive : this._active).slice();
|
|
|
|
|
const removedId = Number(slots[idx]) || 0;
|
|
|
|
|
if (removedId <= 0) return false;
|
|
|
|
|
|
|
|
|
|
slots[idx] = 0;
|
|
|
|
|
const next = this._padSlots(slots, cap);
|
|
|
|
|
if (isPassive) this._passive = next;
|
|
|
|
|
else this._active = next;
|
|
|
|
|
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
console.log(`[DailyShop] 卸下槽位 kind=${kind} idx=${idx} id=${removedId}`);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 购买前校验(不扣费、不写槽) */
|
|
|
|
|
static checkCanBuy(id: number, payByAd = false): DailyShopPurchaseFail | null {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const parsed = this._toParsed(id);
|
|
|
|
|
if (!parsed) return DailyShopPurchaseFail.NotFound;
|
|
|
|
|
if (!this._offers.includes(parsed.id)) return DailyShopPurchaseFail.NotInOffer;
|
|
|
|
|
if (!DailyShopDataManager.GM_UNLIMITED_DAILY_SHOP) {
|
|
|
|
|
if (this._isOfferSold(parsed.id)) return DailyShopPurchaseFail.AlreadySold;
|
|
|
|
|
}
|
|
|
|
|
if (payByAd && !this.isOfferAd(parsed.id)) return DailyShopPurchaseFail.NotAdOffer;
|
|
|
|
|
if (!payByAd) {
|
|
|
|
|
const c = Math.max(0, Math.floor(Number(parsed.cost) || 0));
|
|
|
|
|
if (c > 0 && gg.data.getItemNum(ItemNumType.money) < c) {
|
|
|
|
|
return DailyShopPurchaseFail.NotEnoughMoney;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 非即时道具且同类型槽位已满(仅预览,购买流程以扣费后 NeedReplace 为准) */
|
|
|
|
|
static needsReplaceBeforeBuy(id: number): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const parsed = this._toParsed(id);
|
|
|
|
|
if (!parsed || this._isInstant(parsed)) return false;
|
|
|
|
|
if (parsed.kind !== DailyShopKind.Passive && parsed.kind !== DailyShopKind.Active) return false;
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
const slots = parsed.kind === DailyShopKind.Passive ? this._passive : this._active;
|
|
|
|
|
const cap = parsed.kind === DailyShopKind.Passive ? caps.passive : caps.active;
|
|
|
|
|
const padded = this._padSlots(slots, cap);
|
|
|
|
|
return padded.findIndex((x) => !x) < 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 购买:先扣费(或广告视为已付),再入槽。
|
|
|
|
|
* - 有空槽:直接放入并售罄
|
|
|
|
|
* - 槽位满:记录待替换订单并弹窗(不再扣费,由 completeReplace 入槽)
|
|
|
|
|
*/
|
|
|
|
|
static purchase(
|
|
|
|
|
id: number,
|
|
|
|
|
opts?: { payByAd?: boolean },
|
|
|
|
|
): DailyShopPurchaseResult {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const parsed = this._toParsed(id);
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotFound };
|
|
|
|
|
}
|
|
|
|
|
if (!this._offers.includes(parsed.id)) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotInOffer };
|
|
|
|
|
}
|
|
|
|
|
if (!DailyShopDataManager.GM_UNLIMITED_DAILY_SHOP) {
|
|
|
|
|
if (this._isOfferSold(parsed.id)) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.AlreadySold };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const payByAd = !!opts?.payByAd;
|
|
|
|
|
if (payByAd && !this.isOfferAd(parsed.id)) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotAdOffer };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._pendingPaidGoodsId > 0) {
|
|
|
|
|
if (this._toStoredId(parsed) === this._pendingPaidGoodsId) {
|
|
|
|
|
return this._needReplaceResult(parsed);
|
|
|
|
|
}
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.PendingReplace };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._isInstant(parsed)) {
|
|
|
|
|
const payFail = this._charge(parsed, payByAd);
|
|
|
|
|
if (payFail) return payFail;
|
|
|
|
|
this._applyInstant(parsed, { source: BattleBuffEffectSource.DailyShopBuy });
|
|
|
|
|
this._markSold(parsed.id);
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
return { ok: true, parsed, replacedId: null, instant: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parsed.kind !== DailyShopKind.Passive && parsed.kind !== DailyShopKind.Active) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.BadKind };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payFail = this._charge(parsed, payByAd);
|
|
|
|
|
if (payFail) return payFail;
|
|
|
|
|
|
|
|
|
|
return this._deliverToSlots(parsed, payByAd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 放弃待替换订单:不入槽,不保留待替换状态(不退费、不恢复货架/抽奖次数)。
|
|
|
|
|
* @returns 是否取消了有效待替换
|
|
|
|
|
*/
|
|
|
|
|
static cancelPendingReplace(): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const id = this._pendingPaidGoodsId;
|
|
|
|
|
if (id <= 0) return false;
|
|
|
|
|
|
|
|
|
|
const parsed = this._toParsed(id);
|
|
|
|
|
this._clearPending();
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
console.log(`[DailyShop] 放弃待替换 id=${id}(${parsed?.name ?? ''})`);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 扣费成功后,在弹窗选定要替换的槽位(不再扣费) */
|
|
|
|
|
static completeReplace(replaceOwnedId: number): DailyShopPurchaseResult {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const id = this._pendingPaidGoodsId;
|
|
|
|
|
if (id <= 0) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotFound };
|
|
|
|
|
}
|
|
|
|
|
const parsed = this._toParsed(id);
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
this._clearPending();
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotFound };
|
|
|
|
|
}
|
|
|
|
|
return this._applyReplace(parsed, replaceOwnedId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static hasPendingReplace(): boolean {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
return this._pendingPaidGoodsId > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 已扣费、待弹窗替换的商品 id(0 表示无) */
|
|
|
|
|
static getPendingReplaceGoodsId(): number {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
return this._pendingPaidGoodsId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 调试:强制设置携带(不扣费、不校验货架) */
|
|
|
|
|
static debugSetCarry(passiveIds: number[], activeIds: number[]): void {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
this._passive = this._padSlots(passiveIds, caps.passive);
|
|
|
|
|
this._active = this._padSlots(activeIds, caps.active);
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 登录跨天:清空携带并重抽货架(与 applyDayResetIfNeeded 一致) */
|
|
|
|
|
static onCrossDay(): void {
|
|
|
|
|
this._loaded = false;
|
|
|
|
|
this.applyDayResetIfNeeded();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* GM:模拟凌晨重置(重抽货架/抽奖池、清零购买次数),保留当前携带道具。
|
|
|
|
|
*/
|
|
|
|
|
static gmResetShopAndLotteryKeepCarry(): DailyShopGoods[] | null {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
this._sold = [];
|
|
|
|
|
this._lotteryUsed = false;
|
|
|
|
|
this._openGoldUsed = false;
|
|
|
|
|
this._openAdUsed = false;
|
|
|
|
|
this._clearPending();
|
|
|
|
|
this._offers = this._rollOffers();
|
|
|
|
|
this._lotteryPool = this._rollLotteryPool();
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
const offers = this._buildGoodsList();
|
|
|
|
|
GEvent.Ins.emit(GEvent.DailyShopOffersRefresh, offers);
|
|
|
|
|
console.log(
|
|
|
|
|
'[DailyShop] GM 重置商店/抽奖(保留携带) offers=',
|
|
|
|
|
this._offers.slice(),
|
|
|
|
|
'lottery=',
|
|
|
|
|
this._lotteryPool.slice(),
|
|
|
|
|
'carry passive=',
|
|
|
|
|
this._passive.slice(),
|
|
|
|
|
'active=',
|
|
|
|
|
this._active.slice(),
|
|
|
|
|
);
|
|
|
|
|
return offers;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* GM:写入携带第 1 槽(有则替换);仅接受对应 type 的 Battlebuff。
|
|
|
|
|
* @param kind 主动/被动
|
|
|
|
|
* @param raw 输入框 Battlebuff.id 或名称;0/空=清空第 1 槽
|
|
|
|
|
*/
|
|
|
|
|
static gmSetFirstCarrySlot(kind: DailyShopKind, raw: string): DailyShopGmFirstSlotResult {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const trimmed = String(raw ?? '').trim();
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
const isPassive = kind === DailyShopKind.Passive;
|
|
|
|
|
const cap = isPassive ? caps.passive : caps.active;
|
|
|
|
|
if (cap <= 0) {
|
|
|
|
|
return { ok: false, parsed: null, wrongKind: false };
|
|
|
|
|
}
|
|
|
|
|
const slots = this._padSlots(isPassive ? this._passive.slice() : this._active.slice(), cap);
|
|
|
|
|
|
|
|
|
|
if (!trimmed || trimmed === '0') {
|
|
|
|
|
slots[0] = 0;
|
|
|
|
|
if (isPassive) this._passive = slots;
|
|
|
|
|
else this._active = slots;
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
if (isPassive) setDebugForceCarryPassiveIds(null);
|
|
|
|
|
else setDebugForceCarryActiveIds(null);
|
|
|
|
|
this._emitCarryRefresh();
|
|
|
|
|
return { ok: true, parsed: null, wrongKind: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const wrongKind = this._gmFirstSlotHasWrongKind(trimmed, kind);
|
|
|
|
|
if (wrongKind) {
|
|
|
|
|
return { ok: false, parsed: null, wrongKind: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ids = parseGmDailyShopCarryIds(trimmed, kind);
|
|
|
|
|
if (!ids?.length) {
|
|
|
|
|
return { ok: false, parsed: null, wrongKind: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = this._parseBattleBuffById(ids[0]);
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
return { ok: false, parsed: null, wrongKind: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slots[0] = this._toStoredId(parsed);
|
|
|
|
|
if (isPassive) this._passive = slots;
|
|
|
|
|
else this._active = slots;
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
if (isPassive) setDebugForceCarryPassiveIds(null);
|
|
|
|
|
else setDebugForceCarryActiveIds(null);
|
|
|
|
|
this._emitCarryRefresh();
|
|
|
|
|
console.log(`[DailyShop] GM 设置第1槽 kind=${kind} id=${parsed.id}(${parsed.name})`);
|
|
|
|
|
return { ok: true, parsed, wrongKind: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** GM 改携带后:剔除池内已携带并补抽,通知 UI 全量刷新 */
|
|
|
|
|
private static _emitCarryRefresh(): void {
|
|
|
|
|
if (this._reconcilePoolsExcludeCarried()) {
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
}
|
|
|
|
|
GEvent.Ins.emit(GEvent.DailyShopOffersRefresh, this._buildGoodsList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** GM:清空当前携带的每日商店/BUFF 道具(主动+被动槽)。 */
|
|
|
|
|
static gmClearCarriedBuffs(): void {
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
this._passive = this._emptySlots(caps.passive);
|
|
|
|
|
this._active = this._emptySlots(caps.active);
|
|
|
|
|
this._clearPending();
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
setDebugForceCarryPassiveIds(null);
|
|
|
|
|
setDebugForceCarryActiveIds(null);
|
|
|
|
|
const offers = this._buildGoodsList();
|
|
|
|
|
GEvent.Ins.emit(GEvent.DailyShopOffersRefresh, offers);
|
|
|
|
|
console.log('[DailyShop] GM 清空携带 passive=', this._passive.slice(), 'active=', this._active.slice());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 存档重置:丢掉内存货架/携带,避免写回空档 */
|
|
|
|
|
static clearForReset(): void {
|
|
|
|
|
this._loaded = false;
|
|
|
|
|
this._day = '';
|
|
|
|
|
this._offers = [];
|
|
|
|
|
this._offerAd = [];
|
|
|
|
|
this._sold = [];
|
|
|
|
|
this._passive = [];
|
|
|
|
|
this._active = [];
|
|
|
|
|
this._openGoldUsed = false;
|
|
|
|
|
this._openAdUsed = false;
|
|
|
|
|
this._settleBuyPop = false;
|
|
|
|
|
this._settleChainLotteryOnBuyClose = false;
|
|
|
|
|
this._settleCrossDayTip = false;
|
|
|
|
|
this._clearPending();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 胜利/失败结算领取后标记:回大厅再弹 PopDailyShopOpen */
|
|
|
|
|
static markSettleBuyPop(): void {
|
|
|
|
|
this._settleBuyPop = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 取出并清除结算购买弹窗标记 */
|
|
|
|
|
static consumeSettleBuyPop(): boolean {
|
|
|
|
|
const v = this._settleBuyPop;
|
|
|
|
|
this._settleBuyPop = false;
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 取出并清除「结算跨天重置」提示标记 */
|
|
|
|
|
static consumeSettleCrossDayTip(): boolean {
|
|
|
|
|
const v = this._settleCrossDayTip;
|
|
|
|
|
this._settleCrossDayTip = false;
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 胜利/失败回大厅:弹 PopDailyShopOpen。
|
|
|
|
|
* @returns 是否已触发弹窗
|
|
|
|
|
*/
|
|
|
|
|
static popSettleShopPanelIfNeeded(): boolean {
|
|
|
|
|
if (!this.consumeSettleBuyPop()) return false;
|
|
|
|
|
if (this.consumeSettleCrossDayTip()) {
|
|
|
|
|
gg.ui.showToast('新的一天开始啦,已清空所有道具');
|
|
|
|
|
}
|
|
|
|
|
void gg.ui.popPanel(UIID.PopDailyShopOpen);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* PopDailyShopBuy 关闭时:结算链下按 Const 7 万分比判定是否弹 PopDailyShopLottery。
|
|
|
|
|
* 已购货架用 para1 第 1 档,未购用第 2 档。
|
|
|
|
|
*/
|
|
|
|
|
static tryPopLotteryAfterBuyClose(): void {
|
|
|
|
|
if (!this._settleChainLotteryOnBuyClose) return;
|
|
|
|
|
this._settleChainLotteryOnBuyClose = false;
|
|
|
|
|
this.ensureToday();
|
|
|
|
|
if (this.hasUsedLotteryPurchase()) return;
|
|
|
|
|
if (this._lotteryPool.length <= 0) return;
|
|
|
|
|
const shopPurchased = this.hasUsedShopPurchase();
|
|
|
|
|
const prob = this._resolveWheelRefreshProbWan(shopPurchased);
|
|
|
|
|
if (prob <= 0) return;
|
|
|
|
|
const hit = Math.floor(Math.random() * 10000) < prob;
|
|
|
|
|
console.log(
|
|
|
|
|
`[DailyShop] 关闭购买窗 roll 抽奖 shopPurchased=${shopPurchased} prob=${prob} hit=${hit}`,
|
|
|
|
|
);
|
|
|
|
|
if (!hit) return;
|
|
|
|
|
void gg.ui.popPanel(UIID.PopDailyShopLottery);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- internal ----------
|
|
|
|
|
|
|
|
|
|
/** 扣铜币;广告购视为已付,不扣铜币 */
|
|
|
|
|
private static _charge(parsed: DailyShopParsed, payByAd: boolean): DailyShopPurchaseResult | null {
|
|
|
|
|
if (payByAd) return null;
|
|
|
|
|
return this._tryPay(parsed.cost);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 扣费成功后:有空槽直接放入,满槽则挂起待替换。
|
|
|
|
|
* @param markOfferSold false 时不记货架已购(翻牌入包)
|
|
|
|
|
*/
|
|
|
|
|
private static _deliverToSlots(parsed: DailyShopParsed, payByAd: boolean, markOfferSold = true): DailyShopPurchaseResult {
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
const isPassive = parsed.kind === DailyShopKind.Passive;
|
|
|
|
|
const cap = isPassive ? caps.passive : caps.active;
|
|
|
|
|
const slots = isPassive ? this._passive.slice() : this._active.slice();
|
|
|
|
|
const padded = this._padSlots(slots, cap);
|
|
|
|
|
const emptyIdx = this._findEmptySlotIndex(padded);
|
|
|
|
|
|
|
|
|
|
if (emptyIdx >= 0) {
|
|
|
|
|
const storedId = this._toStoredId(parsed);
|
|
|
|
|
padded[emptyIdx] = storedId;
|
|
|
|
|
if (isPassive) this._passive = padded;
|
|
|
|
|
else this._active = padded;
|
|
|
|
|
if (markOfferSold) this._markSold(parsed.id);
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
console.log(
|
|
|
|
|
`[DailyShop] 购买入槽 id=${storedId}(${parsed.name}) slot=${emptyIdx}`
|
|
|
|
|
+ ` passive=${this._passive} active=${this._active}`,
|
|
|
|
|
);
|
|
|
|
|
return { ok: true, parsed, replacedId: null, slotIndex: emptyIdx, slotKind: parsed.kind };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const candidates = padded.filter((x) => x > 0);
|
|
|
|
|
this._pendingPaidGoodsId = this._toStoredId(parsed);
|
|
|
|
|
this._pendingPaidByAd = payByAd;
|
|
|
|
|
if (markOfferSold) this._markSold(parsed.id);
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
console.log(`[DailyShop] 已扣费待替换 id=${parsed.id}(${parsed.name})`);
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
reason: DailyShopPurchaseFail.NeedReplace,
|
|
|
|
|
replaceCandidates: candidates.slice(),
|
|
|
|
|
paid: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 翻牌窗次数标记 */
|
|
|
|
|
private static _markOpenUsed(payByAd: boolean): void {
|
|
|
|
|
if (payByAd) this._openAdUsed = true;
|
|
|
|
|
else this._openGoldUsed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 待替换订单:仅写入槽位替换,不再扣费 */
|
|
|
|
|
private static _applyReplace(parsed: DailyShopParsed, replaceOwnedId: number): DailyShopPurchaseResult {
|
|
|
|
|
const storedId = this._toStoredId(parsed);
|
|
|
|
|
if (this._pendingPaidGoodsId !== storedId) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotFound };
|
|
|
|
|
}
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
const isPassive = parsed.kind === DailyShopKind.Passive;
|
|
|
|
|
const cap = isPassive ? caps.passive : caps.active;
|
|
|
|
|
const slots = isPassive ? this._passive.slice() : this._active.slice();
|
|
|
|
|
const padded = this._padSlots(slots, cap);
|
|
|
|
|
const candidates = padded.filter((x) => x > 0);
|
|
|
|
|
|
|
|
|
|
const replaceId = Number(replaceOwnedId) || 0;
|
|
|
|
|
if (!replaceId) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
reason: DailyShopPurchaseFail.NeedReplace,
|
|
|
|
|
replaceCandidates: candidates.slice(),
|
|
|
|
|
paid: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const ri = padded.indexOf(replaceId);
|
|
|
|
|
if (ri < 0) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
reason: DailyShopPurchaseFail.InvalidReplace,
|
|
|
|
|
replaceCandidates: candidates,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
padded[ri] = storedId;
|
|
|
|
|
if (isPassive) this._passive = padded;
|
|
|
|
|
else this._active = padded;
|
|
|
|
|
|
|
|
|
|
this._clearPending();
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
console.log(
|
|
|
|
|
`[DailyShop] 替换入槽 id=${parsed.id}(${parsed.name}) replace=${replaceId}`
|
|
|
|
|
+ ` passive=${this._passive} active=${this._active}`,
|
|
|
|
|
);
|
|
|
|
|
return { ok: true, parsed, replacedId: replaceId, slotIndex: ri, slotKind: parsed.kind };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _findEmptySlotIndex(padded: number[]): number {
|
|
|
|
|
for (let i = 0; i < padded.length; i++) {
|
|
|
|
|
if (!padded[i]) return i;
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _needReplaceResult(parsed: DailyShopParsed): DailyShopPurchaseResult {
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
const slots = parsed.kind === DailyShopKind.Passive
|
|
|
|
|
? this._padSlots(this._passive, caps.passive)
|
|
|
|
|
: this._padSlots(this._active, caps.active);
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
reason: DailyShopPurchaseFail.NeedReplace,
|
|
|
|
|
replaceCandidates: slots.filter((x) => x > 0),
|
|
|
|
|
paid: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _clearPending(): void {
|
|
|
|
|
this._pendingPaidGoodsId = 0;
|
|
|
|
|
this._pendingPaidByAd = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _isInstant(parsed: DailyShopParsed): boolean {
|
|
|
|
|
if (isBattleBuffParsed(parsed)) {
|
|
|
|
|
return isBattleBuffInstant(parsed.row as ITableBattlebuff);
|
|
|
|
|
}
|
|
|
|
|
return parsed.kind === DailyShopKind.Passive
|
|
|
|
|
&& parsed.passive?.type === DailyShopPassiveType.MetaVigour;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _applyInstant(parsed: DailyShopParsed, ctx?: IBattleBuffEffectContext): void {
|
|
|
|
|
if (isBattleBuffParsed(parsed)) {
|
|
|
|
|
const row = parsed.row as ITableBattlebuff;
|
|
|
|
|
BattleBuffEffectRegistry.applyInstant(row, ctx ?? { source: BattleBuffEffectSource.DailyShopBuy });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (parsed.passive?.type === DailyShopPassiveType.MetaVigour) {
|
|
|
|
|
const v = Math.max(0, parsed.passive.vigour | 0);
|
|
|
|
|
if (v > 0) gg.data.addVigour(v);
|
|
|
|
|
console.log(`[DailyShop] 即时体力 +${v} id=${parsed.id}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _tryPay(cost: number): DailyShopPurchaseResult | null {
|
|
|
|
|
const c = Math.max(0, Math.floor(Number(cost) || 0));
|
|
|
|
|
if (c <= 0) return null;
|
|
|
|
|
if (gg.data.getItemNum(ItemNumType.money) < c) {
|
|
|
|
|
return { ok: false, reason: DailyShopPurchaseFail.NotEnoughMoney };
|
|
|
|
|
}
|
|
|
|
|
gg.data.subMoney(c);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 货架售罄标记(存 Battlebuff 原始 id,与 _offers 一致) */
|
|
|
|
|
private static _markSold(id: number): void {
|
|
|
|
|
const rawId = Number(id) || 0;
|
|
|
|
|
if (rawId <= 0) return;
|
|
|
|
|
const offerId = rawId >= BATTLE_BUFF_STORE_OFFSET
|
|
|
|
|
? rawId - BATTLE_BUFF_STORE_OFFSET
|
|
|
|
|
: rawId;
|
|
|
|
|
if (!this._sold.includes(offerId)) this._sold.push(offerId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 货架格是否已售(兼容旧存档误写偏移 id) */
|
|
|
|
|
private static _isOfferSold(offerId: number): boolean {
|
|
|
|
|
if (DailyShopDataManager.GM_UNLIMITED_DAILY_SHOP) return false;
|
|
|
|
|
const id = Number(offerId) || 0;
|
|
|
|
|
if (id <= 0) return false;
|
|
|
|
|
return this._sold.includes(id) || this._sold.includes(id + BATTLE_BUFF_STORE_OFFSET);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _rollOffers(): number[] {
|
|
|
|
|
const picked = this._rollWeightedPickBattleBuff(DAILY_SHOP_OFFER_COUNT);
|
|
|
|
|
const adCount = this._rollPropAdBuyCount();
|
|
|
|
|
this._offerAd = this._buildOfferAdFlags(picked.length, adCount);
|
|
|
|
|
return picked;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _rollLotteryPool(): number[] {
|
|
|
|
|
return this._rollWeightedPickBattleBuff(DAILY_SHOP_LOTTERY_COUNT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Battlebuff 权重随机、不重复、排除 excludeRawIds(含已携带) */
|
|
|
|
|
private static _pickBattleBuffBatch(count: number, excludeRawIds: readonly number[]): number[] {
|
|
|
|
|
const exclude = this._dedupePositiveIds(excludeRawIds);
|
|
|
|
|
const all = gg.data.table.getTableList<ITableBattlebuff>(TableNames.Battlebuff) ?? [];
|
|
|
|
|
const remain = all.filter((r) => {
|
|
|
|
|
const id = Number(r?.id) || 0;
|
|
|
|
|
if (id <= 0) return false;
|
|
|
|
|
if ((Number(r.weight) || 0) <= 0) return false;
|
|
|
|
|
if (exclude.indexOf(id) >= 0) return false;
|
|
|
|
|
const type = Number(r.type) || 0;
|
|
|
|
|
return type === 1 || type === 2 || type === 3;
|
|
|
|
|
});
|
|
|
|
|
const picked: number[] = [];
|
|
|
|
|
for (let i = 0; i < count && remain.length > 0; i++) {
|
|
|
|
|
const one = WeightedPick.pick(remain);
|
|
|
|
|
if (!one) break;
|
|
|
|
|
const id = Number(one.id) || 0;
|
|
|
|
|
if (id <= 0) break;
|
|
|
|
|
picked.push(id);
|
|
|
|
|
exclude.push(id);
|
|
|
|
|
const idx = remain.indexOf(one);
|
|
|
|
|
if (idx >= 0) remain.splice(idx, 1);
|
|
|
|
|
}
|
|
|
|
|
return picked;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Battlebuff 权重随机、不重复、排除已携带 */
|
|
|
|
|
private static _rollWeightedPickBattleBuff(count: number): number[] {
|
|
|
|
|
return this._pickBattleBuffBatch(count, this._ownedBattleBuffRawIds());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 货架/抽奖池:移除已携带项并按权重补位 */
|
|
|
|
|
private static _reconcilePoolsExcludeCarried(): boolean {
|
|
|
|
|
const offersChanged = this._sanitizeOffersPool();
|
|
|
|
|
const lotteryChanged = this._sanitizeLotteryPool();
|
|
|
|
|
return offersChanged || lotteryChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 剔除货架内已携带 buff,不足则补抽 */
|
|
|
|
|
private static _sanitizeOffersPool(): boolean {
|
|
|
|
|
const owned = this._ownedBattleBuffRawIds();
|
|
|
|
|
const nextOffers: number[] = [];
|
|
|
|
|
const nextAd: number[] = [];
|
|
|
|
|
let changed = false;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < this._offers.length; i++) {
|
|
|
|
|
const id = Number(this._offers[i]) || 0;
|
|
|
|
|
if (id <= 0) continue;
|
|
|
|
|
if (owned.indexOf(id) >= 0) {
|
|
|
|
|
changed = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (nextOffers.indexOf(id) >= 0) {
|
|
|
|
|
changed = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
nextOffers.push(id);
|
|
|
|
|
nextAd.push(this._offerAd[i] === 1 ? 1 : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const need = DAILY_SHOP_OFFER_COUNT - nextOffers.length;
|
|
|
|
|
if (need > 0) {
|
|
|
|
|
const filled = this._pickBattleBuffBatch(need, [...owned, ...nextOffers]);
|
|
|
|
|
if (filled.length > 0) changed = true;
|
|
|
|
|
for (let i = 0; i < filled.length; i++) {
|
|
|
|
|
nextOffers.push(filled[i]);
|
|
|
|
|
nextAd.push(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < nextOffers.length; i++) {
|
|
|
|
|
if (nextOffers[i] !== this._offers[i]) changed = true;
|
|
|
|
|
}
|
|
|
|
|
if (nextOffers.length !== this._offers.length) changed = true;
|
|
|
|
|
|
|
|
|
|
this._offers = nextOffers.slice(0, DAILY_SHOP_OFFER_COUNT);
|
|
|
|
|
this._offerAd = this._padOfferAdFlags(nextAd, this._offers.length);
|
|
|
|
|
return changed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 剔除抽奖池内已携带 buff,不足则补抽 */
|
|
|
|
|
private static _sanitizeLotteryPool(): boolean {
|
|
|
|
|
const owned = this._ownedBattleBuffRawIds();
|
|
|
|
|
const next: number[] = [];
|
|
|
|
|
let changed = false;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < this._lotteryPool.length; i++) {
|
|
|
|
|
const id = Number(this._lotteryPool[i]) || 0;
|
|
|
|
|
if (id <= 0) continue;
|
|
|
|
|
if (owned.indexOf(id) >= 0) {
|
|
|
|
|
changed = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (next.indexOf(id) >= 0) {
|
|
|
|
|
changed = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
next.push(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const need = DAILY_SHOP_LOTTERY_COUNT - next.length;
|
|
|
|
|
if (need > 0) {
|
|
|
|
|
const filled = this._pickBattleBuffBatch(need, [...owned, ...next]);
|
|
|
|
|
if (filled.length > 0) changed = true;
|
|
|
|
|
for (let i = 0; i < filled.length; i++) next.push(filled[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < next.length; i++) {
|
|
|
|
|
if (next[i] !== this._lotteryPool[i]) changed = true;
|
|
|
|
|
}
|
|
|
|
|
if (next.length !== this._lotteryPool.length) changed = true;
|
|
|
|
|
|
|
|
|
|
this._lotteryPool = next.slice(0, DAILY_SHOP_LOTTERY_COUNT);
|
|
|
|
|
return changed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 抽奖可用原始 id(排除已携带) */
|
|
|
|
|
private static _lotteryPoolEligibleRawIds(): number[] {
|
|
|
|
|
const out: number[] = [];
|
|
|
|
|
for (let i = 0; i < this._lotteryPool.length; i++) {
|
|
|
|
|
const id = Number(this._lotteryPool[i]) || 0;
|
|
|
|
|
if (id <= 0) continue;
|
|
|
|
|
if (this._isCarriedRawId(id)) continue;
|
|
|
|
|
if (out.indexOf(id) >= 0) continue;
|
|
|
|
|
out.push(id);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 是否已携带 Battlebuff 原始 id */
|
|
|
|
|
private static _isCarriedRawId(rawId: number): boolean {
|
|
|
|
|
const id = Number(rawId) || 0;
|
|
|
|
|
if (id <= 0) return false;
|
|
|
|
|
return this._ownedBattleBuffRawIds().indexOf(id) >= 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 去重正整数 id */
|
|
|
|
|
private static _dedupePositiveIds(ids: readonly number[]): number[] {
|
|
|
|
|
const out: number[] = [];
|
|
|
|
|
for (let i = 0; i < ids.length; i++) {
|
|
|
|
|
const id = Number(ids[i]) || 0;
|
|
|
|
|
if (id <= 0) continue;
|
|
|
|
|
if (out.indexOf(id) >= 0) continue;
|
|
|
|
|
out.push(id);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 对齐货架广告标记长度 */
|
|
|
|
|
private static _padOfferAdFlags(flags: number[], offerCount: number): number[] {
|
|
|
|
|
const n = Math.max(0, Math.floor(Number(offerCount) || 0));
|
|
|
|
|
const out: number[] = [];
|
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
|
|
|
out.push(flags[i] === 1 ? 1 : 0);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 已携带 Battlebuff 原始 id */
|
|
|
|
|
private static _ownedBattleBuffRawIds(): number[] {
|
|
|
|
|
const out: number[] = [];
|
|
|
|
|
const all = [...this._passive, ...this._active];
|
|
|
|
|
for (let i = 0; i < all.length; i++) {
|
|
|
|
|
const id = Number(all[i]) || 0;
|
|
|
|
|
if (id <= 0) continue;
|
|
|
|
|
if (id >= BATTLE_BUFF_STORE_OFFSET) {
|
|
|
|
|
out.push(id - BATTLE_BUFF_STORE_OFFSET);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const parsed = this._parseBattleBuffById(id);
|
|
|
|
|
if (parsed) out.push(parsed.id);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _parseBattleBuffById(rawId: number): DailyShopParsed | null {
|
|
|
|
|
const n = Number(rawId) || 0;
|
|
|
|
|
if (n <= 0) return null;
|
|
|
|
|
const row = gg.data.table.getTableData<ITableBattlebuff>(TableNames.Battlebuff, n);
|
|
|
|
|
return row ? parseBattleBuffRow(row) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 槽位存档 id:Battlebuff 加偏移 */
|
|
|
|
|
private static _toStoredId(parsed: DailyShopParsed): number {
|
|
|
|
|
return parsed.id + BATTLE_BUFF_STORE_OFFSET;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _toParsed(id: number): DailyShopParsed | null {
|
|
|
|
|
const n = Number(id) || 0;
|
|
|
|
|
if (n <= 0) return null;
|
|
|
|
|
const rawId = n >= BATTLE_BUFF_STORE_OFFSET ? n - BATTLE_BUFF_STORE_OFFSET : n;
|
|
|
|
|
return this._parseBattleBuffById(rawId);
|
|
|
|
|
}
|
|
|
|
|
/** Const 转盘刷新几率 para1 万分比:已购货架取第 1 档,未购取第 2 档 */
|
|
|
|
|
private static _resolveWheelRefreshProbWan(shopPurchased: boolean): number {
|
|
|
|
|
const arr = gg.data.table.getConstArray(ConstNumType.WheelRefreshProb);
|
|
|
|
|
const idx = shopPurchased ? 0 : 1;
|
|
|
|
|
const raw = Math.floor(Number(arr[idx]) || 0);
|
|
|
|
|
if (raw <= 0) return 0;
|
|
|
|
|
return Math.min(10000, raw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Const 7 para1:按权重 roll 本周期广告购道具个数(0~2) */
|
|
|
|
|
private static _rollPropAdBuyCount(): number {
|
|
|
|
|
const weights = gg.data.table.getConstArray(ConstNumType.PropAdBuyWeight);
|
|
|
|
|
if (weights.length < 3) {
|
|
|
|
|
console.log('[DailyShop] Const 广告数量权重非法', ConstNumType.PropAdBuyWeight);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
const entries = [
|
|
|
|
|
{ count: 0, weight: Math.max(0, Math.floor(Number(weights[0]) || 0)) },
|
|
|
|
|
{ count: 1, weight: Math.max(0, Math.floor(Number(weights[1]) || 0)) },
|
|
|
|
|
{ count: 2, weight: Math.max(0, Math.floor(Number(weights[2]) || 0)) },
|
|
|
|
|
];
|
|
|
|
|
const picked = WeightedPick.pick(entries);
|
|
|
|
|
if (!picked) {
|
|
|
|
|
console.log('[DailyShop] Const 广告数量权重总和为 0', ConstNumType.PropAdBuyWeight);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return picked.count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 在货架格中随机指定 adCount 格为广告购。
|
|
|
|
|
* @param offerCount 货架格数
|
|
|
|
|
* @param adCount 广告购格数
|
|
|
|
|
*/
|
|
|
|
|
private static _buildOfferAdFlags(offerCount: number, adCount: number): number[] {
|
|
|
|
|
const n = Math.max(0, Math.floor(Number(offerCount) || 0));
|
|
|
|
|
let k = Math.min(n, Math.max(0, Math.floor(Number(adCount) || 0)));
|
|
|
|
|
const flags: number[] = [];
|
|
|
|
|
for (let i = 0; i < n; i++) flags.push(0);
|
|
|
|
|
const pool: number[] = [];
|
|
|
|
|
for (let i = 0; i < n; i++) pool.push(i);
|
|
|
|
|
while (k > 0 && pool.length > 0) {
|
|
|
|
|
const pickIdx = Math.floor(Math.random() * pool.length);
|
|
|
|
|
flags[pool[pickIdx]] = 1;
|
|
|
|
|
pool.splice(pickIdx, 1);
|
|
|
|
|
k--;
|
|
|
|
|
}
|
|
|
|
|
return flags;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _toGoods(id: number, sold: boolean, isAd: boolean): DailyShopGoods | null {
|
|
|
|
|
const p = this._toParsed(id);
|
|
|
|
|
return p ? { ...p, sold, isAd } : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _buildGoodsList(): DailyShopGoods[] {
|
|
|
|
|
return this._offers
|
|
|
|
|
.map((id, i) => this._toGoods(id, this._isOfferSold(id), this._offerAd[i] === 1))
|
|
|
|
|
.filter((g): g is DailyShopGoods => !!g);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 商店周期:每天凌晨 0 点换一天 */
|
|
|
|
|
private static _todayKey(): string {
|
|
|
|
|
const d = new Date(gg.data.getCurentTime2());
|
|
|
|
|
const y = d.getFullYear();
|
|
|
|
|
const m = `${d.getMonth() + 1}`.padStart(2, '0');
|
|
|
|
|
const day = `${d.getDate()}`.padStart(2, '0');
|
|
|
|
|
return `${y}${m}${day}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 跨天重置:清空携带并重抽货架/抽奖 */
|
|
|
|
|
private static _applyCrossDayReset(day: string): void {
|
|
|
|
|
this._day = day;
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
this._passive = this._emptySlots(caps.passive);
|
|
|
|
|
this._active = this._emptySlots(caps.active);
|
|
|
|
|
this._sold = [];
|
|
|
|
|
this._lotteryUsed = false;
|
|
|
|
|
this._openGoldUsed = false;
|
|
|
|
|
this._openAdUsed = false;
|
|
|
|
|
this._clearPending();
|
|
|
|
|
this._offers = this._rollOffers();
|
|
|
|
|
this._lotteryPool = this._rollLotteryPool();
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
console.log('[DailyShop] 凌晨重置', day, 'offers=', this._offers.slice(), 'lottery=', this._lotteryPool.slice());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _emptySlots(n: number): number[] {
|
|
|
|
|
return Array.from({ length: n }, () => 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _padSlots(ids: number[], n: number): number[] {
|
|
|
|
|
const out = this._emptySlots(n);
|
|
|
|
|
let j = 0;
|
|
|
|
|
for (const raw of ids) {
|
|
|
|
|
const id = Number(raw) || 0;
|
|
|
|
|
if (id <= 0) continue;
|
|
|
|
|
if (j >= n) break;
|
|
|
|
|
out[j++] = id;
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _loadFromDoc(): void {
|
|
|
|
|
if (this._loaded) return;
|
|
|
|
|
const raw = gg.data.getStringData(OtherDataType.DailyShopState) || '';
|
|
|
|
|
// day|offers|passive|active|sold
|
|
|
|
|
const parts = raw.split('|');
|
|
|
|
|
this._day = parts[0] || '';
|
|
|
|
|
this._offers = this._parseIdList(parts[1]);
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
this._passive = this._padSlots(this._parseIdList(parts[2]), caps.passive);
|
|
|
|
|
this._active = this._padSlots(this._parseIdList(parts[3]), caps.active);
|
|
|
|
|
this._sold = this._parseIdList(parts[4]).filter((x) => x > 0);
|
|
|
|
|
this._offerAd = this._parseIdList(parts[5]);
|
|
|
|
|
this._pendingPaidGoodsId = Math.max(0, Math.floor(Number(parts[6]) || 0));
|
|
|
|
|
this._pendingPaidByAd = Math.floor(Number(parts[7]) || 0) === 1;
|
|
|
|
|
this._lotteryPool = this._parseIdList(parts[8]);
|
|
|
|
|
this._lotteryUsed = Math.floor(Number(parts[9]) || 0) === 1;
|
|
|
|
|
this._openGoldUsed = Math.floor(Number(parts[11]) || 0) === 1;
|
|
|
|
|
this._openAdUsed = Math.floor(Number(parts[12]) || 0) === 1;
|
|
|
|
|
this._openGuideUsed = Math.floor(Number(parts[13]) || 0) === 1;
|
|
|
|
|
if (this._offerAd.length !== this._offers.length) {
|
|
|
|
|
const adCount = this._rollPropAdBuyCount();
|
|
|
|
|
this._offerAd = this._buildOfferAdFlags(this._offers.length, adCount);
|
|
|
|
|
}
|
|
|
|
|
this._reconcileMisplacedCarrySlots();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按配表 type 修正误槽位携带 buff(旧版曾按 functiontype 推断入槽)。
|
|
|
|
|
* type=1→主动槽,type=2→被动槽,type=3→清出槽位。
|
|
|
|
|
*/
|
|
|
|
|
private static _reconcileMisplacedCarrySlots(): void {
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
const passive = this._padSlots(this._passive.slice(), caps.passive);
|
|
|
|
|
const active = this._padSlots(this._active.slice(), caps.active);
|
|
|
|
|
let changed = false;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < passive.length; i++) {
|
|
|
|
|
const storedId = Number(passive[i]) || 0;
|
|
|
|
|
if (storedId <= 0) continue;
|
|
|
|
|
const row = this._resolveCarryBuffRow(storedId);
|
|
|
|
|
const slotType = row ? battleBuffSlotType(row) : null;
|
|
|
|
|
if (slotType === BattleBuffSlotType.Active) {
|
|
|
|
|
const emptyIdx = this._findEmptySlotIndex(active);
|
|
|
|
|
if (emptyIdx < 0) {
|
|
|
|
|
console.log('[DailyShop] type=1 在被动槽但主动槽已满', row?.name, storedId);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
active[emptyIdx] = storedId;
|
|
|
|
|
passive[i] = 0;
|
|
|
|
|
changed = true;
|
|
|
|
|
console.log('[DailyShop] 主动 buff 已从被动槽迁移', row?.name, storedId);
|
|
|
|
|
} else if (slotType === BattleBuffSlotType.Auto) {
|
|
|
|
|
passive[i] = 0;
|
|
|
|
|
changed = true;
|
|
|
|
|
console.log('[DailyShop] type=3 自动 buff 已从被动槽清出', row?.name, storedId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < active.length; i++) {
|
|
|
|
|
const storedId = Number(active[i]) || 0;
|
|
|
|
|
if (storedId <= 0) continue;
|
|
|
|
|
const row = this._resolveCarryBuffRow(storedId);
|
|
|
|
|
const slotType = row ? battleBuffSlotType(row) : null;
|
|
|
|
|
if (slotType === BattleBuffSlotType.Passive) {
|
|
|
|
|
const emptyIdx = this._findEmptySlotIndex(passive);
|
|
|
|
|
if (emptyIdx < 0) {
|
|
|
|
|
console.log('[DailyShop] type=2 在主动槽但被动槽已满', row?.name, storedId);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
passive[emptyIdx] = storedId;
|
|
|
|
|
active[i] = 0;
|
|
|
|
|
changed = true;
|
|
|
|
|
console.log('[DailyShop] 被动 buff 已从主动槽迁移', row?.name, storedId);
|
|
|
|
|
} else if (slotType === BattleBuffSlotType.Auto) {
|
|
|
|
|
active[i] = 0;
|
|
|
|
|
changed = true;
|
|
|
|
|
console.log('[DailyShop] type=3 自动 buff 已从主动槽清出', row?.name, storedId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!changed) return;
|
|
|
|
|
this._passive = passive;
|
|
|
|
|
this._active = active;
|
|
|
|
|
this._saveToDoc();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* GM:输入是否明确为另一 type 的 Battlebuff.id(名称不匹配时不判错)。
|
|
|
|
|
* @param raw 输入框
|
|
|
|
|
* @param kind 目标槽位
|
|
|
|
|
*/
|
|
|
|
|
private static _gmFirstSlotHasWrongKind(raw: string, kind: DailyShopKind): boolean {
|
|
|
|
|
const token = String(raw ?? '').trim().split(/[,|,\s]+/)[0]?.trim() ?? '';
|
|
|
|
|
if (!token) return false;
|
|
|
|
|
const num = Math.floor(Number(token));
|
|
|
|
|
if (!Number.isFinite(num) || num <= 0) return false;
|
|
|
|
|
const row = gg.data.table.getTableData<ITableBattlebuff>(TableNames.Battlebuff, num);
|
|
|
|
|
if (!row) return false;
|
|
|
|
|
const rowKind = battleBuffRowKind(row);
|
|
|
|
|
if (!rowKind) return false;
|
|
|
|
|
return rowKind !== kind;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 携带槽 storedId → Battlebuff 表行 */
|
|
|
|
|
private static _resolveCarryBuffRow(storedId: number): ITableBattlebuff | null {
|
|
|
|
|
const n = Number(storedId) || 0;
|
|
|
|
|
if (n <= 0) return null;
|
|
|
|
|
const rawId = n >= BATTLE_BUFF_STORE_OFFSET ? n - BATTLE_BUFF_STORE_OFFSET : n;
|
|
|
|
|
return gg.data.table.getTableData<ITableBattlebuff>(TableNames.Battlebuff, rawId) ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 只把携带槽从存档读回,供 GM 关闭强制后还原 */
|
|
|
|
|
private static _reloadCarrySlotsFromDoc(): void {
|
|
|
|
|
const raw = gg.data.getStringData(OtherDataType.DailyShopState) || '';
|
|
|
|
|
const parts = raw.split('|');
|
|
|
|
|
const caps = this.getSlotCaps();
|
|
|
|
|
this._passive = this._padSlots(this._parseIdList(parts[2]), caps.passive);
|
|
|
|
|
this._active = this._padSlots(this._parseIdList(parts[3]), caps.active);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _saveToDoc(): void {
|
|
|
|
|
const s = [
|
|
|
|
|
this._day,
|
|
|
|
|
this._offers.join(','),
|
|
|
|
|
this._passive.join(','),
|
|
|
|
|
this._active.join(','),
|
|
|
|
|
this._sold.join(','),
|
|
|
|
|
this._offerAd.join(','),
|
|
|
|
|
String(this._pendingPaidGoodsId || 0),
|
|
|
|
|
this._pendingPaidByAd ? '1' : '0',
|
|
|
|
|
this._lotteryPool.join(','),
|
|
|
|
|
this._lotteryUsed ? '1' : '0',
|
|
|
|
|
'0',
|
|
|
|
|
this._openGoldUsed ? '1' : '0',
|
|
|
|
|
this._openAdUsed ? '1' : '0',
|
|
|
|
|
this._openGuideUsed ? '1' : '0',
|
|
|
|
|
].join('|');
|
|
|
|
|
gg.data.setStringData(OtherDataType.DailyShopState, s);
|
|
|
|
|
gg.data.saveLocal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _parseIdList(s: string | undefined): number[] {
|
|
|
|
|
if (!s) return [];
|
|
|
|
|
return s.split(',').map((x) => Number(x) || 0);
|
|
|
|
|
}
|
|
|
|
|
}
|