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.
308 lines
12 KiB
308 lines
12 KiB
|
3 weeks ago
|
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;
|
||
|
|
});
|
||
|
|
}
|