import { isElementGlyphType, 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 ELEMENT_PITY_CONFIG_ID = SkillCostType.skillCostType_1402; const ELEMENT_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; /** 连续未出五行字的次数(持久化键仍为 NotBagComboCount) */ notElementGlyphCount: number; }; /** 单次刷新内的脸白冷却(按槽位顺序,不持久化) */ export type BagPityTransientState = { gridCooldownRemaining: number; elementGlyphCooldownRemaining: number; }; export type BagPitySlotContext = { chapterId: number; /** 玉帝等副本:不刷格子,跳过 Const_weight 1400/1401 格子脸黑/脸白/必出 */ disableGridPity: boolean; persisted: BagPityPersistedState; transient: BagPityTransientState; gridPity: BagPityTripleConfig; elementPity: BagPityTripleConfig; gridInterval: number; elementInterval: number; }; type WeightedEntry = { weight: number; entry: BagRollEntry }; export function loadBagPityPersistedState(): BagPityPersistedState { return { notGridCount: Math.max(0, gg.data.getStatistics(StatisticsType.NotBagGridCount)), notElementGlyphCount: 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.notElementGlyphCount)); } export function createBagPityTransientState(): BagPityTransientState { return { gridCooldownRemaining: 0, elementGlyphCooldownRemaining: 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), elementPity: parseBagPityTripleConfig(ELEMENT_PITY_CONFIG_ID), gridInterval: parseBagIntervalConfig(earlyGrid ? EARLY_GRID_INTERVAL_CONFIG_ID : GRID_INTERVAL_CONFIG_ID), elementInterval: parseBagIntervalConfig(ELEMENT_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 beforeElement = ctx.persisted.notElementGlyphCount; const tileType = provider.getWeapon2Type(weapon2Id); 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 (isElementGlyphType(tileType)) { ctx.persisted.notElementGlyphCount = 0; ctx.transient.elementGlyphCooldownRemaining = ctx.elementInterval; } else { ctx.persisted.notElementGlyphCount++; if (ctx.transient.elementGlyphCooldownRemaining > 0) { ctx.transient.elementGlyphCooldownRemaining--; } } if (slotIndex == null) return; logBagRefreshDebug( `P4槽${slotIndex} 计数`, `${formatWeapon2Brief(weapon2Id, provider)}`, `| 格子 ${beforeGrid}→${ctx.persisted.notGridCount}`, `| 五行 ${beforeElement}→${ctx.persisted.notElementGlyphCount}`, `| 脸白冷却 格子=${ctx.transient.gridCooldownRemaining} 五行=${ctx.transient.elementGlyphCooldownRemaining}`, ); } /** * 按当前脸黑/脸白状态 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 blockElement = ctx.transient.elementGlyphCooldownRemaining > 0; const gridGuaranteed = !ctx.disableGridPity && ctx.persisted.notGridCount >= ctx.gridPity.guaranteedAt; const elementGuaranteed = ctx.persisted.notElementGlyphCount >= ctx.elementPity.guaranteedAt; const gridBonus = ctx.disableGridPity ? 0 : calcPityBonus(ctx.persisted.notGridCount, ctx.gridPity); const elementBonus = calcPityBonus(ctx.persisted.notElementGlyphCount, ctx.elementPity); if (slotIndex != null) { logBagRefreshDebug( `P4槽${slotIndex} roll前`, `格子计数=${ctx.persisted.notGridCount}`, `五行计数=${ctx.persisted.notElementGlyphCount}`, `| 格子加权+${gridBonus}`, `五行加权+${elementBonus}`, blockGrid ? '| 脸白禁格子' : '', blockElement ? '| 脸白禁五行' : '', gridGuaranteed ? '| 格子必出' : '', elementGuaranteed ? '| 五行必出' : '', ); } 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 (elementGuaranteed) { const elementPool = filterPoolByElementGlyph(basePool, provider); const pick = pickWeighted(elementPool.length > 0 ? elementPool : 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 = applyElementGlyphPityBonus(pool, provider, ctx.persisted.notElementGlyphCount, ctx.elementPity); const filteredPool = filterBlockedTypes(pool, provider, blockGrid, blockElement); 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 = applyElementGlyphPityBonus(pool, provider, ctx.persisted.notElementGlyphCount, ctx.elementPity); 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 applyElementGlyphPityBonus( pool: WeightedEntry[], provider: WeaponComposeConfigProvider, notCount: number, cfg: BagPityTripleConfig, ): WeightedEntry[] { const bonus = calcPityBonus(notCount, cfg); if (bonus <= 0) return pool; return pool.map((p) => { if (!isElementGlyphType(provider.getWeapon2Type(p.entry.weapon2Id))) 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 filterPoolByElementGlyph( pool: WeightedEntry[], provider: WeaponComposeConfigProvider, ): WeightedEntry[] { return pool.filter((p) => isElementGlyphType(provider.getWeapon2Type(p.entry.weapon2Id))); } function filterBlockedTypes( pool: WeightedEntry[], provider: WeaponComposeConfigProvider, blockGrid: boolean, blockElement: boolean, ): WeightedEntry[] { if (!blockGrid && !blockElement) return pool; return pool.filter((p) => { const t = provider.getWeapon2Type(p.entry.weapon2Id); if (blockGrid && t === Weapon2TileType.Grid) return false; if (blockElement && isElementGlyphType(t)) return false; return true; }); }