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_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 * 空槽用 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(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; /** 已扣费但槽位满、待弹窗替换的商品 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 { await gg.data.table.ensureTableLoaded(TableNames.Battlebuff); } /** 携带槽位数:读 Const 表 id=19(主动) / id=20(被动) 的 para,读取失败用默认 */ static getSlotCaps(): { active: number; passive: number } { const active = gg.data.table.getConst(19) || DAILY_SHOP_ACTIVE_SLOTS; const passive = gg.data.table.getConst(20) || DAILY_SHOP_PASSIVE_SLOTS; return { active, passive }; } /** 读档;同日仅修补缺失货架/抽奖格,不触发跨天重置 */ 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._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._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; } /** 本周期抽奖是否已使用 */ 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._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._settleBuyPop = false; this._settleChainLotteryOnBuyClose = false; this._settleCrossDayTip = false; this._clearPending(); } /** 胜利/失败结算领取后标记:回大厅再弹 PopDailyShopBuy */ 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; } /** * 胜利/失败回大厅:先弹 PopDailyShopBuy;关闭时按 Const 7 判定是否链式弹抽奖窗。 * @returns 是否已触发弹窗 */ static popSettleShopPanelIfNeeded(): boolean { if (!this.consumeSettleBuyPop()) return false; if (this.consumeSettleCrossDayTip()) { gg.ui.showToast('新的一天开始啦,已清空所有道具'); } this._settleChainLotteryOnBuyClose = true; void gg.ui.popPanel(UIID.PopDailyShopBuy); 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); } /** 扣费成功后:有空槽直接放入,满槽则挂起待替换 */ private static _deliverToSlots(parsed: DailyShopParsed, payByAd: boolean): 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; 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; 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 _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(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(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 7 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 21 para1:按权重 roll 本周期广告购道具个数(0~2) */ private static _rollPropAdBuyCount(): number { const weights = gg.data.table.getConstArray(ConstNumType.PropAdBuyCountWeight); if (weights.length < 3) { console.log('[DailyShop] Const 21 广告数量权重非法'); 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 21 广告数量权重总和为 0'); 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._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; 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(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(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', ].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); } }