|
|
|
|
import { Weapon2TileType, isElementGlyphType } from 'db://assets/mx/module/merge';
|
|
|
|
|
import { createMembershipSet, membershipAdd, membershipFromArray, membershipDedupeArray, membershipToArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../mx/tools/MembershipSet';
|
|
|
|
|
import {
|
|
|
|
|
formatBagRollEntries,
|
|
|
|
|
formatWeapon2Brief,
|
|
|
|
|
formatIdList,
|
|
|
|
|
logBagRefreshDebug,
|
|
|
|
|
} from '../debug/BagRefreshDebugLog';
|
|
|
|
|
import {
|
|
|
|
|
applyBagPitySlotResult,
|
|
|
|
|
isEarlyChapterGridScope,
|
|
|
|
|
loadBagPitySlotContext,
|
|
|
|
|
parseEarlyChapterGridConfig,
|
|
|
|
|
resolveGridBaseWeightForChapter,
|
|
|
|
|
rollBagPitySlot,
|
|
|
|
|
saveBagPityPersistedState,
|
|
|
|
|
} from './BagPityAlgorithm';
|
|
|
|
|
import { WeaponComposeConfigProvider } from './WeaponComposeConfigProvider';
|
|
|
|
|
import { textDisplayName, textIconName } from '../config/BattleHeroTable';
|
|
|
|
|
import { applyBagRefreshWeightBonus } from '../skill/GlobalSkillEffects';
|
|
|
|
|
import { isChapter1NormalOnly } from './BagPrepConfig';
|
|
|
|
|
|
|
|
|
|
/** 高阶刷新:1/2/3 级文字权重(常量表无此行,局内固定) */
|
|
|
|
|
const AD_BAG_REFRESH_LEVEL_WEIGHTS = '5,3,1';
|
|
|
|
|
|
|
|
|
|
export type BagRollEntry = { weapon2Id: number; weapon1Id?: number; level?: number };
|
|
|
|
|
|
|
|
|
|
type WeightedEntry = { weight: number; entry: BagRollEntry };
|
|
|
|
|
|
|
|
|
|
/** 地图已摆放未激活字的刷新降权(Const_weight 1404) */
|
|
|
|
|
export type BagRefreshPoolOptions = {
|
|
|
|
|
mapPlacedUncomposedIds?: ReadonlyMembershipSet<number>;
|
|
|
|
|
mapPlacedPenaltyWan?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析 GM 输入为 Weapon2.id 列表。
|
|
|
|
|
* 支持:id 逗号分隔(11,12)、名称(悟,空)、资源名;空串或 0 表示关闭。
|
|
|
|
|
*/
|
|
|
|
|
export function parseGmBagWeapon2Filter(raw: string): number[] | null {
|
|
|
|
|
const trimmed = raw.trim();
|
|
|
|
|
if (!trimmed || trimmed === '0') return null;
|
|
|
|
|
|
|
|
|
|
const config = WeaponComposeConfigProvider.instance;
|
|
|
|
|
config.load();
|
|
|
|
|
const all = config.getAllWeapon2Configs();
|
|
|
|
|
const ids: number[] = [];
|
|
|
|
|
|
|
|
|
|
for (const part of trimmed.split(/[,|,\s]+/)) {
|
|
|
|
|
const token = part.trim();
|
|
|
|
|
if (!token) continue;
|
|
|
|
|
|
|
|
|
|
const num = Number(token);
|
|
|
|
|
if (Number.isFinite(num) && num > 0) {
|
|
|
|
|
if (all.some((w) => Number(w.id) === num)) ids.push(num);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const row = all.find((w) => {
|
|
|
|
|
const icon = textIconName(w);
|
|
|
|
|
const group = w.group?.trim();
|
|
|
|
|
return icon === token || group === token || textDisplayName(w) === token;
|
|
|
|
|
});
|
|
|
|
|
if (row) ids.push(Number(row.id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ids.length === 0) return null;
|
|
|
|
|
return membershipDedupeArray(ids);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 背包刷新:基础权重来自 Weapon2.weight(unlock 过滤);GM 可指定仅刷某些字 */
|
|
|
|
|
export class BagWeaponPool {
|
|
|
|
|
private readonly _config = WeaponComposeConfigProvider.instance;
|
|
|
|
|
|
|
|
|
|
rollSlots(
|
|
|
|
|
count: number,
|
|
|
|
|
chapterId: number,
|
|
|
|
|
gmWeapon2Ids?: number[] | null,
|
|
|
|
|
forcedWeapon2Ids?: number[] | null,
|
|
|
|
|
/** 仅第一章引导首次刷新:保持 ConstConf 配表顺序 */
|
|
|
|
|
preserveForcedOrder = false,
|
|
|
|
|
/** PopGM 预设 Weapon2:独占槽位前缀,顺序固定 */
|
|
|
|
|
gmPresetWeapon2Ids?: number[] | null,
|
|
|
|
|
poolOptions?: BagRefreshPoolOptions,
|
|
|
|
|
): BagRollEntry[] {
|
|
|
|
|
this._config.load();
|
|
|
|
|
|
|
|
|
|
const fixed = gmWeapon2Ids?.length ? gmWeapon2Ids : null;
|
|
|
|
|
if (fixed) {
|
|
|
|
|
const result = this._rollFixedSlots(count, fixed);
|
|
|
|
|
logBagRefreshDebug('基础roll: GM固定池', formatBagRollEntries(result, this._config));
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pool = this._buildBasePool(chapterId, poolOptions);
|
|
|
|
|
const result: BagRollEntry[] = [];
|
|
|
|
|
let presetFilled = 0;
|
|
|
|
|
|
|
|
|
|
if (gmPresetWeapon2Ids?.length) {
|
|
|
|
|
for (const id of gmPresetWeapon2Ids) {
|
|
|
|
|
if (result.length >= count) break;
|
|
|
|
|
const row = this._config.getWeapon2Config(id);
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
result.push(this._entryFromWeapon2Row(row));
|
|
|
|
|
}
|
|
|
|
|
presetFilled = result.length;
|
|
|
|
|
logBagRefreshDebug('基础roll: GM预设前缀', formatBagRollEntries(result, this._config));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pool.length === 0 && result.length === 0) {
|
|
|
|
|
logBagRefreshDebug('基础roll: 候选池为空');
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let forcedFilled = 0;
|
|
|
|
|
if (forcedWeapon2Ids?.length) {
|
|
|
|
|
for (const id of forcedWeapon2Ids) {
|
|
|
|
|
if (result.length >= count) break;
|
|
|
|
|
const row = this._config.getWeapon2Config(id);
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
result.push(this._entryFromWeapon2Row(row));
|
|
|
|
|
}
|
|
|
|
|
forcedFilled = result.length - presetFilled;
|
|
|
|
|
if (forcedFilled > 0) {
|
|
|
|
|
logBagRefreshDebug('基础roll: forced前置', formatBagRollEntries(result.slice(presetFilled), this._config));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let usedRandomFill = false;
|
|
|
|
|
while (result.length < count && pool.length > 0) {
|
|
|
|
|
usedRandomFill = true;
|
|
|
|
|
const pick = this._weightedPick(pool);
|
|
|
|
|
if (!pick) break;
|
|
|
|
|
result.push(pick);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (gmPresetWeapon2Ids?.length) {
|
|
|
|
|
const head = result.slice(0, presetFilled);
|
|
|
|
|
const tail = usedRandomFill ? this._shuffle(result.slice(presetFilled)) : result.slice(presetFilled);
|
|
|
|
|
const out = [...head, ...tail].slice(0, count);
|
|
|
|
|
logBagRefreshDebug('基础roll: 保持GM预设顺序', formatBagRollEntries(out, this._config));
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
if (preserveForcedOrder && forcedWeapon2Ids?.length) {
|
|
|
|
|
const head = result.slice(0, presetFilled + forcedFilled);
|
|
|
|
|
const tail = usedRandomFill ? this._shuffle(result.slice(presetFilled + forcedFilled)) : result.slice(presetFilled + forcedFilled);
|
|
|
|
|
const out = [...head, ...tail].slice(0, count);
|
|
|
|
|
logBagRefreshDebug('基础roll: 保持forced顺序', formatBagRollEntries(out, this._config));
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
const out = forcedWeapon2Ids?.length ? this._shuffle(result) : result;
|
|
|
|
|
logBagRefreshDebug(
|
|
|
|
|
'基础roll: 完成',
|
|
|
|
|
formatBagRollEntries(out, this._config),
|
|
|
|
|
forcedWeapon2Ids?.length ? '(已打乱)' : '',
|
|
|
|
|
);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* P4 最终保底:按槽位顺序逐格 roll,脸黑计数持久化,脸白限制同次刷新内生效。
|
|
|
|
|
* forced 项按顺序优先填入前若干槽,再对剩余槽逐格随机。
|
|
|
|
|
*/
|
|
|
|
|
rollSlotsWithPity(
|
|
|
|
|
count: number,
|
|
|
|
|
chapterId: number,
|
|
|
|
|
forcedWeapon2Ids?: number[] | null,
|
|
|
|
|
preserveForcedOrder = false,
|
|
|
|
|
poolOptions?: BagRefreshPoolOptions,
|
|
|
|
|
): BagRollEntry[] {
|
|
|
|
|
this._config.load();
|
|
|
|
|
|
|
|
|
|
const basePool = this._buildBasePool(chapterId, poolOptions);
|
|
|
|
|
if (basePool.length === 0) {
|
|
|
|
|
logBagRefreshDebug('P4最终保底: 候选池为空');
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ctx = loadBagPitySlotContext(chapterId);
|
|
|
|
|
logBagRefreshDebug(
|
|
|
|
|
'P4最终保底: 入参',
|
|
|
|
|
`格子计数=${ctx.persisted.notGridCount}`,
|
|
|
|
|
`五行计数=${ctx.persisted.notElementGlyphCount}`,
|
|
|
|
|
`| 格子配置 ${ctx.gridPity.startAddWeightAt},${ctx.gridPity.weightAddStep},${ctx.gridPity.guaranteedAt}`,
|
|
|
|
|
`| 五行配置 ${ctx.elementPity.startAddWeightAt},${ctx.elementPity.weightAddStep},${ctx.elementPity.guaranteedAt}`,
|
|
|
|
|
`| 脸白间隔 格子=${ctx.gridInterval} 五行=${ctx.elementInterval}`,
|
|
|
|
|
);
|
|
|
|
|
if (forcedWeapon2Ids?.length) {
|
|
|
|
|
logBagRefreshDebug('P4最终保底: forced队列', formatBagRollEntries(
|
|
|
|
|
forcedWeapon2Ids.map((id) => ({ weapon2Id: id })),
|
|
|
|
|
this._config,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const forcedQueue = forcedWeapon2Ids?.length ? [...forcedWeapon2Ids] : [];
|
|
|
|
|
const result: BagRollEntry[] = [];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
|
|
|
let entry: BagRollEntry | null = null;
|
|
|
|
|
|
|
|
|
|
if (forcedQueue.length > 0) {
|
|
|
|
|
const forcedId = forcedQueue.shift()!;
|
|
|
|
|
const row = this._config.getWeapon2Config(forcedId);
|
|
|
|
|
if (row) {
|
|
|
|
|
entry = this._entryFromWeapon2Row(row);
|
|
|
|
|
logBagRefreshDebug(`P4槽${i} forced → ${formatWeapon2Brief(entry.weapon2Id, this._config)}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!entry) {
|
|
|
|
|
entry = rollBagPitySlot(ctx, basePool, this._config, (pool) => this._weightedPick(pool), i);
|
|
|
|
|
}
|
|
|
|
|
if (!entry) break;
|
|
|
|
|
|
|
|
|
|
result.push(entry);
|
|
|
|
|
applyBagPitySlotResult(ctx, entry.weapon2Id, this._config, i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveBagPityPersistedState(ctx.persisted);
|
|
|
|
|
logBagRefreshDebug(
|
|
|
|
|
'P4最终保底: 持久计数出',
|
|
|
|
|
`格子=${ctx.persisted.notGridCount}`,
|
|
|
|
|
`五行=${ctx.persisted.notElementGlyphCount}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (preserveForcedOrder && forcedWeapon2Ids?.length && result.length === forcedWeapon2Ids.length) {
|
|
|
|
|
const out = result.slice(0, count);
|
|
|
|
|
logBagRefreshDebug('P4最终保底: 保持顺序', formatBagRollEntries(out, this._config));
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
const rollOrder = [...result];
|
|
|
|
|
const out = forcedWeapon2Ids?.length ? this._shuffle(result) : result;
|
|
|
|
|
logBagRefreshDebug('P4最终保底: roll顺序', formatBagRollEntries(rollOrder, this._config));
|
|
|
|
|
if (forcedWeapon2Ids?.length) {
|
|
|
|
|
logBagRefreshDebug('P4最终保底: 打乱后', formatBagRollEntries(out, this._config));
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 高阶刷新:
|
|
|
|
|
* 1)必出 1 个场上没有的五行字(Text.type 1–5)
|
|
|
|
|
* 2)其余位按正常权重随机
|
|
|
|
|
* 3)抽到的常规字(Single)按常量表 45 权重滚等级
|
|
|
|
|
* 4)打乱顺序后返回
|
|
|
|
|
*/
|
|
|
|
|
rollAdSlots(
|
|
|
|
|
count: number,
|
|
|
|
|
chapterId: number,
|
|
|
|
|
mapWeapon2Ids: ReadonlyMembershipSet<number>,
|
|
|
|
|
gmWeapon2Ids?: number[] | null,
|
|
|
|
|
forcedPresetWeapon2Ids?: number[] | null,
|
|
|
|
|
poolOptions?: BagRefreshPoolOptions,
|
|
|
|
|
): BagRollEntry[] {
|
|
|
|
|
this._config.load();
|
|
|
|
|
|
|
|
|
|
if (gmWeapon2Ids?.length) {
|
|
|
|
|
const result = this._shuffle(this._rollFixedSlots(count, gmWeapon2Ids));
|
|
|
|
|
logBagRefreshDebug('高阶刷新: GM固定池', formatBagRollEntries(result, this._config));
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pool = this._buildBasePool(chapterId, poolOptions);
|
|
|
|
|
const result: BagRollEntry[] = [];
|
|
|
|
|
if (forcedPresetWeapon2Ids?.length) {
|
|
|
|
|
for (const id of forcedPresetWeapon2Ids) {
|
|
|
|
|
const row = this._config.getWeapon2Config(id);
|
|
|
|
|
if (row) result.push(this._entryFromWeapon2Row(row));
|
|
|
|
|
}
|
|
|
|
|
logBagRefreshDebug('高阶刷新: GM预设前置', formatBagRollEntries(result, this._config));
|
|
|
|
|
}
|
|
|
|
|
if (result.length >= count) {
|
|
|
|
|
return result.slice(0, count);
|
|
|
|
|
}
|
|
|
|
|
if (pool.length === 0) {
|
|
|
|
|
logBagRefreshDebug('高阶刷新: 候选池为空');
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const missingGlyphs = pool.filter(
|
|
|
|
|
(p) =>
|
|
|
|
|
isElementGlyphType(this._config.getWeapon2Type(p.entry.weapon2Id))
|
|
|
|
|
&& !mapWeapon2Ids.has(p.entry.weapon2Id),
|
|
|
|
|
);
|
|
|
|
|
const allGlyphs = pool.filter(
|
|
|
|
|
(p) => isElementGlyphType(this._config.getWeapon2Type(p.entry.weapon2Id)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let guaranteed = this._weightedPick(missingGlyphs);
|
|
|
|
|
let guaranteedSource = '场上缺失五行字池';
|
|
|
|
|
if (!guaranteed) {
|
|
|
|
|
guaranteed = this._weightedPick(allGlyphs) ?? this._weightedPick(pool);
|
|
|
|
|
guaranteedSource = guaranteed
|
|
|
|
|
? (allGlyphs.length > 0 ? '全五行字池/总池回退' : '总池回退')
|
|
|
|
|
: '无';
|
|
|
|
|
}
|
|
|
|
|
if (!guaranteed) {
|
|
|
|
|
logBagRefreshDebug('高阶刷新: 无法抽出保底五行字');
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
logBagRefreshDebug(
|
|
|
|
|
'高阶刷新: 保底五行字',
|
|
|
|
|
formatWeapon2Brief(guaranteed.weapon2Id, this._config),
|
|
|
|
|
`来源=${guaranteedSource}`,
|
|
|
|
|
`场上已有=${membershipToArray(mapWeapon2Ids).join(',')}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
result.push(this._applyAdRefreshLevel(guaranteed));
|
|
|
|
|
for (let i = result.length; i < count; i++) {
|
|
|
|
|
const pick = this._weightedPick(pool);
|
|
|
|
|
if (!pick) break;
|
|
|
|
|
result.push(this._applyAdRefreshLevel(pick));
|
|
|
|
|
}
|
|
|
|
|
const out = forcedPresetWeapon2Ids?.length ? result : this._shuffle(result);
|
|
|
|
|
logBagRefreshDebug('高阶刷新: 完成', formatBagRollEntries(out, this._config));
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _rollFixedSlots(count: number, weapon2Ids: number[]): BagRollEntry[] {
|
|
|
|
|
const pool: BagRollEntry[] = [];
|
|
|
|
|
for (const id of weapon2Ids) {
|
|
|
|
|
const row = this._config.getAllWeapon2Configs().find((w) => w.id === id);
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
pool.push(this._entryFromWeapon2Row(row));
|
|
|
|
|
}
|
|
|
|
|
if (pool.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const result: BagRollEntry[] = [];
|
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
|
|
|
const pick = pool[Math.floor(Math.random() * pool.length)];
|
|
|
|
|
result.push({ ...pick });
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _entryFromWeapon2Row(row: ITableText): BagRollEntry {
|
|
|
|
|
const weapon2Id = Number(row.id);
|
|
|
|
|
const entry: BagRollEntry = { weapon2Id, level: 1 };
|
|
|
|
|
if (Number(row.type) === Weapon2TileType.Single) {
|
|
|
|
|
const weapon1Id = this._config.getWeapon1ByGroupSequence([weapon2Id]);
|
|
|
|
|
if (weapon1Id != null) {
|
|
|
|
|
entry.weapon1Id = weapon1Id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 五行字按 const 45 权重赋等级;格子保持 1 级 */
|
|
|
|
|
private _applyAdRefreshLevel(entry: BagRollEntry): BagRollEntry {
|
|
|
|
|
const out = { ...entry };
|
|
|
|
|
if (!isElementGlyphType(this._config.getWeapon2Type(out.weapon2Id))) {
|
|
|
|
|
out.level = 1;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
out.level = this._rollAdRefreshLevel();
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _rollAdRefreshLevel(): number {
|
|
|
|
|
const raw = AD_BAG_REFRESH_LEVEL_WEIGHTS;
|
|
|
|
|
const parts = raw.split(',').map((s) => Number(s.trim()));
|
|
|
|
|
const weights = [
|
|
|
|
|
Number.isFinite(parts[0]) && parts[0] >= 0 ? parts[0] : 5,
|
|
|
|
|
Number.isFinite(parts[1]) && parts[1] >= 0 ? parts[1] : 3,
|
|
|
|
|
Number.isFinite(parts[2]) && parts[2] >= 0 ? parts[2] : 1,
|
|
|
|
|
];
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (const w of weights) total += w;
|
|
|
|
|
if (total <= 0) return 1;
|
|
|
|
|
|
|
|
|
|
let r = Math.random() * total;
|
|
|
|
|
for (let i = 0; i < weights.length; i++) {
|
|
|
|
|
r -= weights[i];
|
|
|
|
|
if (r <= 0) return i + 1;
|
|
|
|
|
}
|
|
|
|
|
return 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 基础池:Weapon2 表 weight + unlock;全局词条加成;地图未激活字降权 */
|
|
|
|
|
private _buildBasePool(chapterId: number, poolOptions?: BagRefreshPoolOptions): WeightedEntry[] {
|
|
|
|
|
const pool: WeightedEntry[] = [];
|
|
|
|
|
const skills = gg.game.localPlayer?.selectedSkills ?? [];
|
|
|
|
|
const penaltyWan = poolOptions?.mapPlacedPenaltyWan ?? 0;
|
|
|
|
|
const penalizedIds = poolOptions?.mapPlacedUncomposedIds;
|
|
|
|
|
|
|
|
|
|
let gridBase = 0;
|
|
|
|
|
let gridFinal = 0;
|
|
|
|
|
let totalFinal = 0;
|
|
|
|
|
let mapPenaltyHits = 0;
|
|
|
|
|
const earlyGridCfg = isEarlyChapterGridScope(chapterId) ? parseEarlyChapterGridConfig() : null;
|
|
|
|
|
if (earlyGridCfg) {
|
|
|
|
|
logBagRefreshDebug(
|
|
|
|
|
'前X章格子配置',
|
|
|
|
|
`章节≤${earlyGridCfg.chapterLimit}`,
|
|
|
|
|
`格子权重=${earlyGridCfg.gridWeight}`,
|
|
|
|
|
'保底1405/脸白1406',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
for (const row of this._config.getWeapon2RefreshCandidates(chapterId)) {
|
|
|
|
|
const tileType = Number(row.type);
|
|
|
|
|
const weapon2Id = Number(row.id);
|
|
|
|
|
let base = Number(row.weight);
|
|
|
|
|
if (tileType === Weapon2TileType.Grid) {
|
|
|
|
|
base = resolveGridBaseWeightForChapter(chapterId, base);
|
|
|
|
|
}
|
|
|
|
|
let weight = applyBagRefreshWeightBonus(base, tileType, weapon2Id, skills);
|
|
|
|
|
if (penaltyWan > 0 && penalizedIds?.has(weapon2Id)) {
|
|
|
|
|
weight *= Math.max(0, 1 - penaltyWan / 10000);
|
|
|
|
|
mapPenaltyHits++;
|
|
|
|
|
}
|
|
|
|
|
if (tileType === Weapon2TileType.Grid) {
|
|
|
|
|
gridBase += base;
|
|
|
|
|
gridFinal += weight;
|
|
|
|
|
}
|
|
|
|
|
totalFinal += weight;
|
|
|
|
|
pool.push({ weight, entry: this._entryFromWeapon2Row(row) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mapPenaltyHits > 0) {
|
|
|
|
|
logBagRefreshDebug(
|
|
|
|
|
'地图未激活字降权',
|
|
|
|
|
`命中${mapPenaltyHits}种`,
|
|
|
|
|
`降低${penaltyWan}/10000`,
|
|
|
|
|
formatIdList(membershipToArray(penalizedIds ?? createMembershipSet())),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (skills.length > 0 && totalFinal > 0) {
|
|
|
|
|
const share = ((gridFinal / totalFinal) * 100).toFixed(2);
|
|
|
|
|
logBagRefreshDebug(
|
|
|
|
|
'全局词条权重池',
|
|
|
|
|
`格子 ${gridBase}→${gridFinal}`,
|
|
|
|
|
`池总权重=${totalFinal.toFixed(0)}`,
|
|
|
|
|
`格子占比≈${share}%`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pool;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _weightedPick(pool: WeightedEntry[]): BagRollEntry | null {
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (const p of pool) total += p.weight;
|
|
|
|
|
if (total <= 0) return null;
|
|
|
|
|
|
|
|
|
|
let r = Math.random() * total;
|
|
|
|
|
for (const p of pool) {
|
|
|
|
|
r -= p.weight;
|
|
|
|
|
if (r <= 0) return { ...p.entry };
|
|
|
|
|
}
|
|
|
|
|
return { ...pool[pool.length - 1].entry };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _shuffle<T>(arr: T[]): T[] {
|
|
|
|
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
|
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
|
|
|
const tmp = arr[i];
|
|
|
|
|
arr[i] = arr[j];
|
|
|
|
|
arr[j] = tmp;
|
|
|
|
|
}
|
|
|
|
|
return arr;
|
|
|
|
|
}
|
|
|
|
|
}
|