feat(loot): 参考 Maxroll 重构怪物选择器与高质感掉落卡片列表 (Issue #110)

This commit is contained in:
troytt 2026-09-19 02:38:53 +00:00
parent 2742172b9f
commit 97787ff99c
3 changed files with 2409 additions and 598 deletions

1485
loot.html

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,140 @@
import {
MONSTER_PRESETS,
FAMOUS_ITEMS_META,
classifyItemCategory,
loadCanonicalTreasureClasses,
calculateAnalyticProbabilities,
runMonteCarloSimulation,
buildReverseLookupIndex,
reverseLookup,
} from '../src/loot';
async function main() {
console.log('=== [1] 验证 MONSTER_PRESETS 与 TC 完整性 ===');
const { tcTable, autoTcs } = loadCanonicalTreasureClasses();
console.log(`载入系统 TC 节点数: ${tcTable.all.length}, 自动生成 TC 节点数: ${autoTcs.size}`);
let missingTcCount = 0;
for (const mon of MONSTER_PRESETS) {
for (const diff of ['normal', 'nightmare', 'hell'] as const) {
const tcName = mon.tc[diff];
if (!tcTable.get(tcName) && !autoTcs.has(tcName)) {
console.error(`❌ [缺失] 怪物 ${mon.nameCn} 在 ${diff} 难度的 TC 不存在: ${tcName}`);
missingTcCount++;
}
if (mon.questTc) {
const questTcName = mon.questTc[diff];
if (!tcTable.get(questTcName) && !autoTcs.has(questTcName)) {
console.error(`❌ [缺失] 怪物 ${mon.nameCn} 在 ${diff} 难度的 首杀TC 不存在: ${questTcName}`);
missingTcCount++;
}
}
}
}
if (missingTcCount === 0) {
console.log(`✅ 所有 ${MONSTER_PRESETS.length} 个怪物预设在全难度的基础 TC 与首杀 TC 校验 100% 存在!`);
} else {
throw new Error(`存在 ${missingTcCount} 个无效的 TC 引用`);
}
console.log('\n=== [2] 验证 FAMOUS_ITEMS_META 与物品分类器 ===');
const famousCount = Object.keys(FAMOUS_ITEMS_META).length;
console.log(`已收录核心暗金与极品底模元数据条目: ${famousCount} 条`);
// Test classifyItemCategory
const r30Cat = classifyItemCategory('r30');
const shakoCat = classifyItemCategory('uap', 'unique');
const phaseBladeCat = classifyItemCategory('7cr');
const sojCat = classifyItemCategory('rin', 'unique');
const tokenCat = classifyItemCategory('tes');
console.log(`- 30# 贝 (r30) 分类: ${r30Cat} (期望: rune)`);
console.log(`- 军帽 (uap, unique) 分类: ${shakoCat} (期望: unique)`);
console.log(`- 幻化之刃 (7cr) 分类: ${phaseBladeCat} (期望: base)`);
console.log(`- 戒指/护身符 (rin) 分类: ${sojCat} (期望: charm_gem)`);
console.log(`- 洗点徽章 (tes) 分类: ${tokenCat} (期望: misc)`);
if (r30Cat !== 'rune' || shakoCat !== 'unique' || phaseBladeCat !== 'base' || sojCat !== 'charm_gem' || tokenCat !== 'misc') {
throw new Error('分类器逻辑测试失败');
}
console.log('✅ 物品多维分类器运行正确!');
console.log('\n=== [3] 验证精确概率表解析 (Baal H 与 Andariel H) ===');
const baalOdds = calculateAnalyticProbabilities('Baal (H)', { tcTable, autoTcs, gamePlayers: 1, partyPlayers: 1 });
console.log(`Baal (H) - 叶子节点数: ${baalOdds.leaves.length}, Picks: ${baalOdds.picks}, NoDrop: ${(baalOdds.noDropCompoundProb * 100).toFixed(2)}%`);
// Verify Shako / uap odds for Baal
const uapLeaf = baalOdds.leaves.find(l => l.item === 'uap');
if (uapLeaf) {
console.log(`- 军帽底模 (uap) 单次概率: ${(uapLeaf.singleProbability * 100).toFixed(4)}%, 综合概率: ${(uapLeaf.compoundProbability * 100).toFixed(4)}%, 几率比: 1 : ${uapLeaf.oneInOdds.toFixed(1)}`);
}
// Verify Quest Drop for Andariel
const andyNormalOdds = calculateAnalyticProbabilities('Andariel (H)', { tcTable, autoTcs, gamePlayers: 1, partyPlayers: 1 });
const andyQuestOdds = calculateAnalyticProbabilities('Andarielq (H)', { tcTable, autoTcs, gamePlayers: 1, partyPlayers: 1 });
console.log(`Andariel (H) 普通杀 NoDrop: ${(andyNormalOdds.noDropCompoundProb * 100).toFixed(2)}%`);
console.log(`Andarielq (H) Bug首杀 NoDrop: ${(andyQuestOdds.noDropCompoundProb * 100).toFixed(2)}%`);
const andyNormalUap = andyNormalOdds.leaves.find(l => l.item === 'uap')?.compoundProbability || 0;
const andyQuestUap = andyQuestOdds.leaves.find(l => l.item === 'uap')?.compoundProbability || 0;
console.log(`- 安姐普通杀 军帽(uap) 几率: ${(andyNormalUap * 100).toFixed(4)}%`);
console.log(`- 安姐Bug首杀 军帽(uap) 几率: ${(andyQuestUap * 100).toFixed(4)}% (提升 +${(((andyQuestUap - andyNormalUap) / andyNormalUap) * 100).toFixed(1)}%)`);
const andyQuestHasJunk = andyQuestOdds.leaves.some(l => l.item === 'tsc' || l.item === 'isc' || l.item === 'gld');
if (andyQuestHasJunk) {
throw new Error('安姐首杀 TC 不应包含卷轴或金币等垃圾杂项!');
}
if (andyQuestUap <= andyNormalUap) {
throw new Error('安姐首杀装备出货率应当显著高于普通击杀!');
}
console.log('✅ 安姐首杀剔除杂质与装备出货率暴增验证通过!');
console.log('\n=== [4] 验证 1,000 次蒙特卡洛掉落模拟 (实测出货与品质分布) ===');
const simRes = await runMonteCarloSimulation({
tcName: 'Baal (H)',
runs: 1000,
gamePlayers: 3,
partyPlayers: 1,
magicFind: 250,
difficulty: 'hell',
tcTable,
autoTcs,
});
console.log(`模拟总轮数: ${simRes.totalRuns}`);
console.log(`掉落总件数: ${simRes.itemsDropped} 件 (平均每轮 ${(simRes.itemsDropped / simRes.totalRuns).toFixed(2)} 件)`);
console.log(`品质分布:`);
console.log(` - Unique (暗金): ${simRes.qualityCounts.unique} 件`);
console.log(` - Set (套装): ${simRes.qualityCounts.set} 件`);
console.log(` - Rare (稀有): ${simRes.qualityCounts.rare} 件`);
console.log(` - Magic (魔法): ${simRes.qualityCounts.magic} 件`);
console.log(` - Rune (符文): ${simRes.qualityCounts.rune} 件`);
console.log(` - Normal/Sup (底模): ${simRes.qualityCounts.normal + simRes.qualityCounts.superior} 件`);
console.log(` - Gold (金币): ${simRes.qualityCounts.gold} 堆`);
if (simRes.itemsDropped === 0) {
throw new Error('1000次模拟依然为0件掉落!');
}
console.log(`✅ 1,000 次模拟正常出货,共 ${simRes.itemSummaries.length} 种不同物品!`);
console.log('\n=== [5] 验证反查索引 (Reverse Lookup) ===');
const revIndex = buildReverseLookupIndex(tcTable, autoTcs);
const shakoRev = reverseLookup(revIndex, 'shako');
console.log(`搜索 "shako" 匹配到的掉落源数量: ${shakoRev[0]?.entries.length || 0} 个 TC`);
if (shakoRev.length > 0 && shakoRev[0]!.entries.length > 0) {
console.log(`- 最佳掉落源 TOP 1: ${shakoRev[0]!.entries[0]!.tcName}, 综合几率: ${(shakoRev[0]!.entries[0]!.compoundProbability * 100).toFixed(4)}%`);
} else {
throw new Error('反查 shako 失败');
}
console.log('✅ 反查索引测试通过!');
console.log('\n========================================');
console.log('🎉 全部 Maxroll 怪物选择器与掉落列表核心功能实测 100% 成功!');
console.log('========================================');
}
main().catch(err => {
console.error(err);
process.exit(1);
});

File diff suppressed because it is too large Load Diff