import { Weapon2TileType } from 'db://assets/mx/module/merge'; import { formatWeapon2Brief, logBagRefreshDebug, } from '../debug/BagRefreshDebugLog'; import { StatisticsType } from '../../game/GameDataEnums'; import { SkillCostType, getSkillWeightRow } from '../../game/SkillWeightDefaults'; import type { BagRollEntry } from './BagWeaponPool'; import { WeaponComposeConfigProvider } from './WeaponComposeConfigProvider'; /** Const_weight:格子脸黑 / 脸白 / 组合字脸黑 / 脸白 */ const GRID_PITY_CONFIG_ID = SkillCostType.skillCostType_1400; const GRID_INTERVAL_CONFIG_ID = SkillCostType.skillCostType_1401; const COMBO_PITY_CONFIG_ID = SkillCostType.skillCostType_1402; const COMBO_INTERVAL_CONFIG_ID = SkillCostType.skillCostType_1403; /** 前 X 章:格子脸黑 / 脸白 / 权重(1407 首参为章节上限) */ const EARLY_GRID_PITY_CONFIG_ID = SkillCostType.skillCostType_1405; const EARLY_GRID_INTERVAL_CONFIG_ID = SkillCostType.skillCostType_1406; const EARLY_GRID_WEIGHT_CONFIG_ID = SkillCostType.skillCostType_1407; /** 地图已摆放未激活字:本次刷新权重降低比例(万分比,5000=降低50%) */ const MAP_PLACED_PENALTY_CONFIG_ID = 1404; export type BagPityTripleConfig = { startAddWeightAt: number; weightAddStep: number; guaranteedAt: number; }; export type BagPityPersistedState = { notGridCount: number; notComboCount: number; }; /** 单次刷新内的脸白冷却(按槽位顺序,不持久化) */ export type BagPityTransientState = { gridCooldownRemaining: number; comboCooldownRemaining: number; }; export type BagPitySlotContext = { chapterId: number; /** 玉帝等副本:不刷格子,跳过 Const_weight 1400/1401 格子脸黑/脸白/必出 */ disableGridPity: boolean; persisted: BagPityPersistedState; transient: BagPityTransientState; gridPity: BagPityTripleConfig; comboPity: BagPityTripleConfig; gridInterval: number; comboInterval: number; }; type WeightedEntry = { weight: number; entry: BagRollEntry }; export function loadBagPityPersistedState(): BagPityPersistedState { return { notGridCount: Math.max(0, gg.data.getStatistics(StatisticsType.NotBagGridCount)), notComboCount: Math.max(0, gg.data.getStatistics(StatisticsType.NotBagComboCount)), }; } export function saveBagPityPersistedState(state: BagPityPersistedState): void { gg.data.setStatistics(StatisticsType.NotBagGridCount, Math.max(0, state.notGridCount)); gg.data.setStatistics(StatisticsType.NotBagComboCount, Math.max(0, state.notComboCount)); } export function createBagPityTransientState(): BagPityTransientState { return { gridCooldownRemaining: 0, comboCooldownRemaining: 0 }; } /** Const_weight 1407:前 X 章格子权重(章节号,权重) */ export type EarlyChapterGridConfig = { chapterLimit: number; gridWeight: number; }; export function parseEarlyChapterGridConfig(): EarlyChapterGridConfig | null { const row = getSkillWeightRow(EARLY_GRID_WEIGHT_CONFIG_ID); const parts = (row?.constString ?? '').split(',').map((s) => Number(s.trim())); const chapterLimit = parts[0]; const gridWeight = parts[1]; if (!Number.isFinite(chapterLimit) || chapterLimit <= 0) return null; if (!Number.isFinite(gridWeight) || gridWeight < 0) return null; return { chapterLimit: Math.floor(chapterLimit), gridWeight }; } /** 当前战斗章节 ≤ 1407 章节上限时,启用前 X 章格子专用配置 */ export function isEarlyChapterGridScope(chapterId: number): boolean { const cfg = parseEarlyChapterGridConfig(); return cfg != null && chapterId > 0 && chapterId <= cfg.chapterLimit; } /** 前 X 章内格子基础权重用 1407 第二参,否则用 Weapon2.weight */ export function resolveGridBaseWeightForChapter(chapterId: number, weapon2Weight: number): number { if (!isEarlyChapterGridScope(chapterId)) return weapon2Weight; return parseEarlyChapterGridConfig()!.gridWeight; } export function loadBagPitySlotContext(chapterId: number): BagPitySlotContext { const earlyGrid = isEarlyChapterGridScope(chapterId); return { chapterId, disableGridPity: false, persisted: loadBagPityPersistedState(), transient: createBagPityTransientState(), gridPity: parseBagPityTripleConfig(earlyGrid ? EARLY_GRID_PITY_CONFIG_ID : GRID_PITY_CONFIG_ID), comboPity: parseBagPityTripleConfig(COMBO_PITY_CONFIG_ID), gridInterval: parseBagIntervalConfig(earlyGrid ? EARLY_GRID_INTERVAL_CONFIG_ID : GRID_INTERVAL_CONFIG_ID), comboInterval: parseBagIntervalConfig(COMBO_INTERVAL_CONFIG_ID), }; } export function parseBagPityTripleConfig(configId: number): BagPityTripleConfig { const row = getSkillWeightRow(configId); const parts = (row?.constString ?? '18,2,36').split(',').map((s) => Number(s.trim())); return { startAddWeightAt: finiteOr(parts[0], 18), weightAddStep: finiteOr(parts[1], 2), guaranteedAt: finiteOr(parts[2], 36), }; } export function parseBagIntervalConfig(configId: number): number { const row = getSkillWeightRow(configId); const n = Number((row?.constString ?? '1').split(',')[0]?.trim()); return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 1; } /** Const_weight 1404:地图已摆放未激活字的权重降低比例(万分比) */ export function parseMapPlacedWeightPenaltyWan(): number { const row = getSkillWeightRow(MAP_PLACED_PENALTY_CONFIG_ID); const n = Number((row?.constString ?? '5000').split(',')[0]?.trim()); if (!Number.isFinite(n) || n <= 0) return 0; return Math.min(10000, Math.floor(n)); } /** 单槽结算:更新脸黑持久计数与脸白槽位冷却 */ export function applyBagPitySlotResult( ctx: BagPitySlotContext, weapon2Id: number, provider: WeaponComposeConfigProvider, slotIndex?: number, ): void { const beforeGrid = ctx.persisted.notGridCount; const beforeCombo = ctx.persisted.notComboCount; const tileType = provider.getWeapon2Type(weapon2Id) as Weapon2TileType; if (!ctx.disableGridPity) { if (tileType === Weapon2TileType.Grid) { ctx.persisted.notGridCount = 0; ctx.transient.gridCooldownRemaining = ctx.gridInterval; } else { ctx.persisted.notGridCount++; if (ctx.transient.gridCooldownRemaining > 0) { ctx.transient.gridCooldownRemaining--; } } } if (tileType === Weapon2TileType.Combo) { ctx.persisted.notComboCount = 0; ctx.transient.comboCooldownRemaining = ctx.comboInterval; } else { ctx.persisted.notComboCount++; if (ctx.transient.comboCooldownRemaining > 0) { ctx.transient.comboCooldownRemaining--; } } if (slotIndex == null) return; logBagRefreshDebug( `P4槽${slotIndex} 计数`, `${formatWeapon2Brief(weapon2Id, provider)}`, `| 格子 ${beforeGrid}→${ctx.persisted.notGridCount}`, `| 组合 ${beforeCombo}→${ctx.persisted.notComboCount}`, `| 脸白冷却 格子=${ctx.transient.gridCooldownRemaining} 组合=${ctx.transient.comboCooldownRemaining}`, ); } /** * 按当前脸黑/脸白状态 roll 一个槽位。 * @param basePool 本章基础权重池(Weapon2.weight) */ export function rollBagPitySlot( ctx: BagPitySlotContext, basePool: WeightedEntry[], provider: WeaponComposeConfigProvider, pickWeighted: (pool: WeightedEntry[]) => BagRollEntry | null, slotIndex?: number, ): BagRollEntry | null { if (basePool.length === 0) return null; const blockGrid = !ctx.disableGridPity && ctx.transient.gridCooldownRemaining > 0; const blockCombo = ctx.transient.comboCooldownRemaining > 0; const gridGuaranteed = !ctx.disableGridPity && ctx.persisted.notGridCount >= ctx.gridPity.guaranteedAt; const comboGuaranteed = ctx.persisted.notComboCount >= ctx.comboPity.guaranteedAt; const gridBonus = ctx.disableGridPity ? 0 : calcPityBonus(ctx.persisted.notGridCount, ctx.gridPity); const comboBonus = calcPityBonus(ctx.persisted.notComboCount, ctx.comboPity); if (slotIndex != null) { logBagRefreshDebug( `P4槽${slotIndex} roll前`, `格子计数=${ctx.persisted.notGridCount}`, `组合计数=${ctx.persisted.notComboCount}`, `| 格子加权+${gridBonus}`, `组合加权+${comboBonus}`, blockGrid ? '| 脸白禁格子' : '', blockCombo ? '| 脸白禁组合' : '', gridGuaranteed ? '| 格子必出' : '', comboGuaranteed ? '| 组合必出' : '', ); } if (gridGuaranteed) { const gridPool = filterPoolByType(basePool, provider, Weapon2TileType.Grid); const pick = pickWeighted(gridPool.length > 0 ? gridPool : basePool); if (pick) { if (slotIndex != null) { logBagRefreshDebug(`P4槽${slotIndex} 触发格子必出 → ${formatWeapon2Brief(pick.weapon2Id, provider)}`); } return pick; } } if (comboGuaranteed) { const comboPool = filterPoolByType(basePool, provider, Weapon2TileType.Combo); const pick = pickWeighted(comboPool.length > 0 ? comboPool : basePool); if (pick) { if (slotIndex != null) { logBagRefreshDebug(`P4槽${slotIndex} 触发组合必出 → ${formatWeapon2Brief(pick.weapon2Id, provider)}`); } return pick; } } let pool = ctx.disableGridPity ? basePool : applyTypePityBonus(basePool, provider, Weapon2TileType.Grid, ctx.persisted.notGridCount, ctx.gridPity); pool = applyTypePityBonus(pool, provider, Weapon2TileType.Combo, ctx.persisted.notComboCount, ctx.comboPity); const filteredPool = filterBlockedTypes(pool, provider, blockGrid, blockCombo); const usedFaceWhiteFilter = filteredPool.length > 0 && filteredPool.length < pool.length; pool = filteredPool; if (pool.length === 0) { pool = ctx.disableGridPity ? basePool : applyTypePityBonus(basePool, provider, Weapon2TileType.Grid, ctx.persisted.notGridCount, ctx.gridPity); pool = applyTypePityBonus(pool, provider, Weapon2TileType.Combo, ctx.persisted.notComboCount, ctx.comboPity); if (slotIndex != null) { logBagRefreshDebug(`P4槽${slotIndex} 脸白过滤后池为空,回退全池加权`); } } else if (usedFaceWhiteFilter && slotIndex != null) { logBagRefreshDebug(`P4槽${slotIndex} 脸白过滤生效,候选 ${pool.length}/${basePool.length}`); } const pick = pickWeighted(pool); if (pick && slotIndex != null) { logBagRefreshDebug(`P4槽${slotIndex} 加权随机 → ${formatWeapon2Brief(pick.weapon2Id, provider)}`); } return pick; } function finiteOr(value: number, fallback: number): number { return Number.isFinite(value) ? value : fallback; } function calcPityBonus(count: number, cfg: BagPityTripleConfig): number { if (count < cfg.startAddWeightAt) return 0; return (count - cfg.startAddWeightAt) * cfg.weightAddStep; } function applyTypePityBonus( pool: WeightedEntry[], provider: WeaponComposeConfigProvider, tileType: Weapon2TileType, notCount: number, cfg: BagPityTripleConfig, ): WeightedEntry[] { const bonus = calcPityBonus(notCount, cfg); if (bonus <= 0) return pool; return pool.map((p) => { if (provider.getWeapon2Type(p.entry.weapon2Id) !== tileType) return p; return { ...p, weight: p.weight + bonus }; }); } function filterPoolByType( pool: WeightedEntry[], provider: WeaponComposeConfigProvider, tileType: Weapon2TileType, ): WeightedEntry[] { return pool.filter((p) => provider.getWeapon2Type(p.entry.weapon2Id) === tileType); } function filterBlockedTypes( pool: WeightedEntry[], provider: WeaponComposeConfigProvider, blockGrid: boolean, blockCombo: boolean, ): WeightedEntry[] { if (!blockGrid && !blockCombo) return pool; return pool.filter((p) => { const t = provider.getWeapon2Type(p.entry.weapon2Id) as Weapon2TileType; if (blockGrid && t === Weapon2TileType.Grid) return false; if (blockCombo && t === Weapon2TileType.Combo) return false; return true; }); }