You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/battle/bag/BagPityAlgorithm.ts

330 lines
13 KiB

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