import { ConstNumType } from '../../game/GameDataEnums'; import { TableNames } from '../../game/ConfigTableData'; import { WeightedPick } from '../../tools/WeightedPick'; import type { DailyShopParsed } from '../DailyShopTypes'; import { BattleBuffCarryStore } from './BattleBuffCarryStore'; /** 品质:1绿 2蓝 3紫 4橙 */ const QUALITY_COUNT = 4; /** 每次三选一选项数 */ const OPTION_COUNT = 3; /** 单次抽取结果的选项 */ export interface BattleBuffSelectOption { /** Battlebuff.id */ id: number; /** 品质 */ quality: number; /** 表行 */ row: ITableBattlebuff; } /** 玩家本局三选一会话状态 */ interface BattleBuffSelectSession { /** playerId */ playerId: number; /** 本局已弹出次数(含当前进行中) */ popupOrdinal: number; /** 免费刷新剩余 */ freeLeft: number; /** 广告刷新剩余 */ adLeft: number; /** 当前选项 */ options: BattleBuffSelectOption[]; } /** * 局内 Battlebuff 三选一:品质权重 → 子池权重 → 选项。 * 每玩家独立会话;开战需 reset。 */ export class BattleBuffSelectService { private static readonly _sessions: BattleBuffSelectSession[] = []; /** 开战清空全部会话 */ static resetAll(): void { this._sessions.length = 0; } /** * 开战初始化某玩家刷新次数。 * @param playerId 槽位 */ static ensurePlayer(playerId: number): void { const pid = Math.max(0, Math.floor(Number(playerId) || 0)); if (this._find(pid)) return; this._sessions.push({ playerId: pid, popupOrdinal: 0, freeLeft: Math.max(0, Math.floor(gg.data.table.getConst(ConstNumType.BattleBuffFreeRefreshCount) || 0)), adLeft: Math.max(0, Math.floor(gg.data.table.getConst(ConstNumType.BattleBuffAdRefreshCount) || 0)), options: [], }); } /** * 开始一次弹窗(自动刷新权重),生成 3 选项。 * @param playerId 槽位 */ static beginPopup(playerId: number): BattleBuffSelectOption[] { this.ensurePlayer(playerId); const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0))); if (!session) { console.warn('[BattleBuffSelect] beginPopup 无会话', playerId); return []; } session.popupOrdinal += 1; session.options = this._rollOptions(session.playerId, session.popupOrdinal, false); return session.options.slice(); } /** * 主动刷新:消耗免费或广告次数后重抽(广告权重)。 * @param playerId 槽位 * @param useAd 是否按广告次数扣减(false=免费) */ static refreshOptions(playerId: number, useAd: boolean): BattleBuffSelectOption[] | null { const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0))); if (!session) { console.warn('[BattleBuffSelect] refresh 无会话', playerId); return null; } if (useAd) { if (session.adLeft <= 0) { console.warn('[BattleBuffSelect] 广告刷新次数已尽', playerId); return null; } session.adLeft -= 1; } else { if (session.freeLeft <= 0) { console.warn('[BattleBuffSelect] 免费刷新次数已尽', playerId); return null; } session.freeLeft -= 1; } session.options = this._rollOptions(session.playerId, session.popupOrdinal, true); return session.options.slice(); } /** * 当前是否应优先免费刷新(免费剩余>0)。 * @param playerId 槽位 */ static shouldUseFreeRefresh(playerId: number): boolean { const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0))); return !!session && session.freeLeft > 0; } /** * 是否还能主动刷新。 * @param playerId 槽位 */ static canRefresh(playerId: number): boolean { const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0))); if (!session) return false; return session.freeLeft > 0 || session.adLeft > 0; } /** * 剩余刷新次数合计(展示用)。 * @param playerId 槽位 */ static remainRefreshTotal(playerId: number): number { const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0))); if (!session) return 0; return session.freeLeft + session.adLeft; } /** 当前弹窗选项 */ static currentOptions(playerId: number): BattleBuffSelectOption[] { const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0))); return session ? session.options.slice() : []; } /** * 确认选择某一选项 id。 * @param playerId 槽位 * @param buffId 选项 id */ static confirmPick(playerId: number, buffId: number): DailyShopParsed | null { const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0))); if (!session) { console.warn('[BattleBuffSelect] confirm 无会话', playerId); return null; } let hit = false; for (let i = 0; i < session.options.length; i++) { if (session.options[i].id === buffId) { hit = true; break; } } if (!hit) { console.warn('[BattleBuffSelect] confirm 非当前选项', playerId, buffId); return null; } const parsed = BattleBuffCarryStore.appendBuffId(playerId, buffId); session.options = []; return parsed; } /** * AI:开始弹窗并自动选中第一项(若有)。 * @param playerId AI playerId */ static autoPickOne(playerId: number): DailyShopParsed | null { const options = this.beginPopup(playerId); if (!options.length) { console.warn('[BattleBuffSelect] AI 无可选项', playerId); return null; } return this.confirmPick(playerId, options[0].id); } /** * Const27 是否包含该解锁次数。 * @param unlockCount 本阵营已完成解锁次数(含本次) */ static isTriggerUnlockCount(unlockCount: number): boolean { const n = Math.floor(Number(unlockCount) || 0); if (n <= 0) return false; const list = gg.data.table.getConstArray(ConstNumType.BattleSelectTriggerWaves); for (let i = 0; i < list.length; i++) { if (Math.floor(list[i]) === n) return true; } return false; } private static _find(playerId: number): BattleBuffSelectSession | null { for (let i = 0; i < this._sessions.length; i++) { if (this._sessions[i].playerId === playerId) return this._sessions[i]; } return null; } /** * @param playerId 槽位 * @param popupOrdinal 弹窗序号 * @param activeRefresh true=主动刷新权重 */ private static _rollOptions(playerId: number, popupOrdinal: number, activeRefresh: boolean): BattleBuffSelectOption[] { const qualities = this._rollQualities(popupOrdinal, activeRefresh); const pickedIds: number[] = []; const out: BattleBuffSelectOption[] = []; for (let i = 0; i < qualities.length; i++) { const q = qualities[i]; const row = this._pickRowByQuality(playerId, q, pickedIds); if (!row) { console.warn('[BattleBuffSelect] 品质池为空', q, 'player', playerId); continue; } pickedIds.push(row.id); out.push({ id: row.id, quality: q, row }); } return out; } private static _rollQualities(popupOrdinal: number, activeRefresh: boolean): number[] { const weights = this._qualityWeights(popupOrdinal, activeRefresh); const out: number[] = []; for (let i = 0; i < OPTION_COUNT; i++) { const entries: { quality: number; weight: number }[] = []; for (let q = 1; q <= QUALITY_COUNT; q++) { entries.push({ quality: q, weight: weights[q - 1] }); } const hit = WeightedPick.pick(entries); if (!hit) { console.warn('[BattleBuffSelect] 品质权重全 0'); out.push(1); continue; } out.push(hit.quality); } return out; } private static _qualityWeights(popupOrdinal: number, activeRefresh: boolean): number[] { const baseId = activeRefresh ? ConstNumType.BattleBuffAdWeight : ConstNumType.BattleBuffFreeWeight; const purpleId = activeRefresh ? ConstNumType.BattleBuffAdPurpleBonus : ConstNumType.BattleBuffFreePurpleBonus; const orangeId = activeRefresh ? ConstNumType.BattleBuffAdOrangeBonus : ConstNumType.BattleBuffFreeOrangeBonus; const base = gg.data.table.getConstArray(baseId); const weights = [0, 0, 0, 0]; for (let i = 0; i < QUALITY_COUNT; i++) { weights[i] = Math.max(0, Math.floor(Number(base[i]) || 0)); } if (base.length < QUALITY_COUNT) { console.warn('[BattleBuffSelect] 品质基础权重不足 4 项', baseId, base); } this._applyQualityBonus(weights, 2, purpleId, popupOrdinal); this._applyQualityBonus(weights, 3, orangeId, popupOrdinal); return weights; } /** * @param weights 绿蓝紫橙 * @param index 0~3 * @param bonusConstId 次数,加值 * @param popupOrdinal 弹窗序号 */ private static _applyQualityBonus(weights: number[], index: number, bonusConstId: number, popupOrdinal: number): void { const arr = gg.data.table.getConstArray(bonusConstId); if (arr.length < 2) { console.warn('[BattleBuffSelect] 品质加值配置非法', bonusConstId, arr); return; } const need = Math.floor(arr[0]); const add = Math.floor(arr[1]); if (need <= 0) return; if (popupOrdinal >= need) { weights[index] = Math.max(0, weights[index] + add); } } private static _pickRowByQuality(playerId: number, quality: number, excludeIds: readonly number[]): ITableBattlebuff | null { const list = gg.data.table.getTableList(TableNames.Battlebuff) ?? []; const pool: ITableBattlebuff[] = []; for (let i = 0; i < list.length; i++) { const row = list[i]; if (Math.floor(Number(row.quality) || 0) !== quality) continue; const id = Math.floor(Number(row.id) || 0); if (id <= 0) continue; let excluded = false; for (let e = 0; e < excludeIds.length; e++) { if (excludeIds[e] === id) { excluded = true; break; } } if (excluded) continue; const limit = Math.floor(Number(row.limit) || 0); if (limit > 0 && BattleBuffCarryStore.getObtainCount(playerId, id) >= limit) continue; const w = Number(row.weight) || 0; if (w <= 0) continue; pool.push(row); } if (!pool.length) return null; return WeightedPick.pick(pool); } }