feat(loot): 参考 Maxroll 重构怪物选择器与高质感掉落卡片列表 (Issue #110)
This commit is contained in:
parent
2742172b9f
commit
97787ff99c
|
|
@ -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);
|
||||||
|
});
|
||||||
1382
src/loot.ts
1382
src/loot.ts
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue