|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
}
|