You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/manager/DailyShopDataManager.ts

1372 lines
53 KiB

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<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;
/** 已扣费但槽位满、待弹窗替换的商品 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 表 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<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 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<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',
].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);
}
}