import { TableNames } from '../../game/ConfigTableData'; import type { ComposedUnit, BagSlotRecord } from 'db://assets/mx/module/merge'; import { membershipAdd, membershipFromArray } from '../../../mx/tools/MembershipSet'; import { OtherDataType } from '../../game/GameDataEnums'; import type { BattleBagSession } from './BattleBagSession'; import { WeaponComposeConfigProvider } from './WeaponComposeConfigProvider'; import { isComboHero } from '../config/BattleHeroTable'; /** 通关时场上一个完整英雄单位(同 weapon1 不同局内等级视为不同单位) */ export interface KeepHeroClearUnit { weapon1Id: number; level: number; } /** 抽奖阶段:0 未抽 / 1 已抽出待开局生效(多次抽覆盖) */ export enum KeepHeroLotteryPhase { None = 0, Pending = 1, } export interface KeepHeroLotteryState { drawCount: number; drawn: KeepHeroClearUnit | null; phase: KeepHeroLotteryPhase; } /** 测试:弹窗/抽奖池用假数据;正式环境改为 false */ export const KEEP_HERO_USE_FAKE_UNITS = false; const FAKE_UNITS: KeepHeroClearUnit[] = [ { weapon1Id: 1, level: 1 }, { weapon1Id: 1, level: 2 }, { weapon1Id: 2, level: 3 }, { weapon1Id: 3, level: 1 }, { weapon1Id: 5, level: 1 }, { weapon1Id: 8, level: 2 }, ]; const DEFAULT_KEEP_HERO_LEVEL = 1; const KEEP_HERO_LEVEL_SLOTS = [1, 2, 3, 4]; /** * 从 Merge 合成态收集通关完整英雄。 * 每个 compositions 条目算 1 个单位(刀/斧/悟空等;Lv 不同分开记)。 */ export function collectCompleteHeroUnits( compositions: readonly ComposedUnit[], ): KeepHeroClearUnit[] { const units: KeepHeroClearUnit[] = []; for (const c of compositions) { if (c.weapon1Id <= 0 || c.level <= 0) continue; units.push({ weapon1Id: c.weapon1Id, level: c.level }); } units.sort((a, b) => a.weapon1Id - b.weapon1Id || a.level - b.level); return units; } /** 序列化:`weapon1Id,level;weapon1Id,level;...`(避免与 keyKeyValue 的 | # 冲突) */ export function serializeKeepHeroClearUnits(units: readonly KeepHeroClearUnit[]): string { if (units.length === 0) return ''; return units.map((u) => `${u.weapon1Id},${u.level}`).join(';'); } export function parseKeepHeroClearUnits(raw: string): KeepHeroClearUnit[] { if (!raw) return []; const out: KeepHeroClearUnit[] = []; const parts = raw.includes(';') ? raw.split(';') : raw.split('|'); for (const part of parts) { if (!part) continue; const [idStr, lvStr] = part.split(','); const weapon1Id = Number(idStr); const level = Number(lvStr); if (!(weapon1Id > 0) || !(level > 0)) continue; out.push({ weapon1Id, level }); } return out; } function resolveLevelId(levelId?: number): number { return levelId ?? gg.game.CurentBattle?.LevelConfigId ?? gg.game.CurentSelectLevelId ?? DEFAULT_KEEP_HERO_LEVEL; } function writeClearUnits(levelId: number, units: readonly KeepHeroClearUnit[]): void { gg.data.setkeyKeyValue( OtherDataType.KeepHeroClearUnits, levelId, serializeKeepHeroClearUnits(units), ); } /** 通关结算:按战场覆盖写入完整英雄列表,并重置该战场抽奖态 */ export function saveKeepHeroClearUnits( units: readonly KeepHeroClearUnit[], levelId?: number, ): void { const lv = resolveLevelId(levelId); writeClearUnits(lv, units); saveLotteryState(lv, { drawCount: 0, drawn: null, phase: KeepHeroLotteryPhase.None }); } export function loadKeepHeroClearUnits(levelId?: number): KeepHeroClearUnit[] { const lv = resolveLevelId(levelId); const raw = gg.data.getkeyKeyValue(OtherDataType.KeepHeroClearUnits, lv) || ''; return parseKeepHeroClearUnits(raw); } /** 弹窗展示 / 抽奖池(可走假数据) */ export function getKeepHeroDisplayUnits(levelId?: number): KeepHeroClearUnit[] { if (KEEP_HERO_USE_FAKE_UNITS) return FAKE_UNITS.slice(); return loadKeepHeroClearUnits(levelId); } /** 清空该战场结算池(不改抽奖态) */ export function clearKeepHeroClearUnits(levelId?: number): void { writeClearUnits(resolveLevelId(levelId), []); } /** 本局发放的单位是否仍在背包(已部署到地图则 false) */ export function isKeepHeroGrantInBag( bag: BattleBagSession, unit: KeepHeroClearUnit, ): boolean { WeaponComposeConfigProvider.instance.load(); const group = WeaponComposeConfigProvider.instance.getWeapon1Group(unit.weapon1Id); if (group.length === 0) return false; const level = Math.max(1, unit.level); const slots = bag.merge.bag; for (let i = 0; i < slots.slotCount; i++) { const slot = slots.getSlot(i); if (!slot || slot.level !== level) continue; if (group.includes(slot.weapon2Id)) return true; } return false; } /** 生效后清空该战场全部保留文字数据(结算池 + 抽奖态) */ export function clearKeepHeroLevel(levelId: number): void { writeClearUnits(levelId, []); saveLotteryState(levelId, { drawCount: 0, drawn: null, phase: KeepHeroLotteryPhase.None }); } /** 保留文字抽奖池最少条数(不足时用局外已上阵基础单字 1 级补足) */ const MIN_KEEP_HERO_POOL_SIZE = 3; function keepHeroUnitKey(u: KeepHeroClearUnit): string { return `${u.weapon1Id}_${u.level}`; } function shuffleInPlace(arr: T[]): void { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } } /** 局外已上阵且 herotype === 1 的基础武器 id */ function collectEquippedBasicWeapon1Ids(): number[] { return gg.data.project.getEquippedBasicWeaponDataList().map((w) => w.id); } /** 场上完整英雄不足 {@link MIN_KEEP_HERO_POOL_SIZE} 时,随机补局外上阵基础单字 1 级 */ function padKeepHeroUnitsWithEquippedBasics( units: readonly KeepHeroClearUnit[], ): KeepHeroClearUnit[] { if (units.length >= MIN_KEEP_HERO_POOL_SIZE) return units.slice(); const existingKeys = membershipFromArray(units.map(keepHeroUnitKey)); const candidates = collectEquippedBasicWeapon1Ids() .filter((weapon1Id) => !existingKeys.has(`${weapon1Id}_1`)); shuffleInPlace(candidates); const out = units.slice(); let need = MIN_KEEP_HERO_POOL_SIZE - out.length; for (const weapon1Id of candidates) { if (need <= 0) break; const filler: KeepHeroClearUnit = { weapon1Id, level: 1 }; out.push(filler); membershipAdd(existingKeys, keepHeroUnitKey(filler)); need--; } out.sort((a, b) => a.weapon1Id - b.weapon1Id || a.level - b.level); return out; } /** 从当前对局合成态记录通关英雄(仅常规关;按本局战场分存) */ export function recordKeepHeroClearFromBattle(): KeepHeroClearUnit[] { const battle = gg.game.CurentBattle; if (!battle) return []; const compositions = battle.bagSession?.compositions ?? gg.game.localPlayer?.compositions ?? []; const units = padKeepHeroUnitsWithEquippedBasics( collectCompleteHeroUnits(compositions), ); saveKeepHeroClearUnits(units, battle.LevelConfigId); return units; } /** 是否有可抽奖的结算池(用于弹窗) */ export function hasPendingKeepHeroLottery(levelId?: number): boolean { if (levelId != null) { return getKeepHeroDisplayUnits(levelId).length > 0; } return findPendingKeepHeroLevel() != null; } /** 各战场中第一个「有结算池且尚未抽出」的 levelId */ export function findPendingKeepHeroLevel(): number | null { if (KEEP_HERO_USE_FAKE_UNITS) { return resolveLevelId(); } for (const lv of KEEP_HERO_LEVEL_SLOTS) { if (loadKeepHeroClearUnits(lv).length === 0) continue; const state = loadLotteryState(lv); if (state.phase === KeepHeroLotteryPhase.Pending && state.drawn) continue; return lv; } return null; } /** @deprecated 使用 findPendingKeepHeroLevel */ export function findPendingKeepHeroDifficulty(): number | null { return findPendingKeepHeroLevel(); } function serializeLotteryState(state: KeepHeroLotteryState): string { const unit = state.drawn ? `${state.drawn.weapon1Id},${state.drawn.level}` : '0,0'; return `${state.drawCount};${unit};${state.phase}`; } function parseLotteryState(raw: string): KeepHeroLotteryState { if (!raw) { return { drawCount: 0, drawn: null, phase: KeepHeroLotteryPhase.None }; } const [countStr, unitStr, phaseStr] = raw.split(';'); const drawCount = Math.max(0, Number(countStr) || 0); const rawPhase = Number(phaseStr) || KeepHeroLotteryPhase.None; const phase = rawPhase > 0 ? KeepHeroLotteryPhase.Pending : KeepHeroLotteryPhase.None; let drawn: KeepHeroClearUnit | null = null; if (unitStr) { const [idStr, lvStr] = unitStr.split(','); const weapon1Id = Number(idStr); const level = Number(lvStr); if (weapon1Id > 0 && level > 0) drawn = { weapon1Id, level }; } return { drawCount, drawn, phase: drawn ? phase : KeepHeroLotteryPhase.None }; } export function loadLotteryState(levelId?: number): KeepHeroLotteryState { const lv = resolveLevelId(levelId); const raw = gg.data.getkeyKeyValue(OtherDataType.KeepHeroLotteryState, lv) || ''; return parseLotteryState(raw); } export function saveLotteryState( levelId: number, state: KeepHeroLotteryState, ): void { gg.data.setkeyKeyValue( OtherDataType.KeepHeroLotteryState, levelId, serializeLotteryState(state), ); } /** keep_hero:按 herotype 与等级取 type,再按抽奖次数取 weight */ export function resolveKeepHeroWeight( _weapon1Id: number, _level: number, _drawIndex: number, ): number { return 0; } /** 按当前抽奖次数做加权随机;drawIndex = 已抽次数(第 1 次用 weight[0]) */ export function rollKeepHeroUnit( units: readonly KeepHeroClearUnit[], drawIndex: number, ): KeepHeroClearUnit | null { if (units.length === 0) return null; let total = 0; const weights: number[] = []; for (const u of units) { const w = Math.max(0, resolveKeepHeroWeight(u.weapon1Id, u.level, drawIndex)); weights.push(w); total += w; } if (total <= 0) { return units[Math.floor(Math.random() * units.length)] ?? null; } let r = Math.random() * total; for (let i = 0; i < units.length; i++) { r -= weights[i]; if (r <= 0) return units[i]; } return units[units.length - 1] ?? null; } /** * 免费/广告抽一次:抽出即存为待开局单位(覆盖上次)。 * 结算池仍保留,供本面板内广告再抽;点「领取」后再清池结束本次。 */ export function performKeepHeroDraw( levelId?: number, pool?: readonly KeepHeroClearUnit[], ): KeepHeroClearUnit | null { const lv = resolveLevelId(levelId); const units = pool?.length ? pool : getKeepHeroDisplayUnits(lv); const state = loadLotteryState(lv); const drawn = rollKeepHeroUnit(units, state.drawCount); if (!drawn) return null; saveLotteryState(lv, { drawCount: state.drawCount + 1, drawn, phase: KeepHeroLotteryPhase.Pending, }); return drawn; } /** 点领取关闭:本次保留文字弹窗流程结束(清结算池,不再回主页弹) */ export function finishKeepHeroLotteryUi(levelId?: number): void { clearKeepHeroClearUnits(resolveLevelId(levelId)); } export interface KeepHeroBagGrant { records: BagSlotRecord[]; unit: KeepHeroClearUnit; } export function tryGrantPendingKeepHeroToBag( bag: BattleBagSession, levelId: number, ): KeepHeroBagGrant | null { const state = loadLotteryState(levelId); if (state.phase !== KeepHeroLotteryPhase.Pending || !state.drawn) return null; const provider = WeaponComposeConfigProvider.instance; provider.load(); const group = provider.getWeapon1Group(state.drawn.weapon1Id); if (group.length === 0) { console.error('[KeepHero] 无法解析 Weapon1.characters', state.drawn.weapon1Id); return null; } const drawn = state.drawn; const isSingle = group.length === 1; const level = Math.max(1, drawn.level); const records: BagSlotRecord[] = group.map((weapon2Id) => ( isSingle ? { weapon2Id, weapon1Id: drawn.weapon1Id, level } : { weapon2Id, level } )); const slots = bag.merge.bag; const emptyIndices: number[] = []; for (let i = 0; i < slots.slotCount; i++) { if (!slots.getSlot(i)) emptyIndices.push(i); } if (emptyIndices.length < records.length) { console.log( '[KeepHero] 背包空位不足,保留待赠送', `需要${records.length}`, `空位${emptyIndices.length}`, ); return null; } for (let i = 0; i < records.length; i++) { slots.setSlot(emptyIndices[i], records[i]); } clearKeepHeroLevel(levelId); return { records, unit: drawn }; }