Fix duplicate damage combos in comp analyzer

Closes #2788
This commit is contained in:
Kalle
2026-02-20 18:10:00 +02:00
parent ddd9b184aa
commit 713ddc1678
2 changed files with 96 additions and 2 deletions

View File

@@ -367,6 +367,42 @@ describe("calculateDamageCombos - excessive combo filtering", () => {
});
});
const TRI_SLOSHER_ID = 3010;
const INKBRUSH_ID = 1100;
const GOLD_DYNAMO_ROLLER_ID = 1021;
const RAPID_BLASTER_PRO_WNT_R_ID = 252;
describe("calculateDamageCombos - deduplication", () => {
test("no duplicate combos with bug report weapons", () => {
const combos = calculateDamageCombos(
[
TRI_SLOSHER_ID,
INKBRUSH_ID,
GOLD_DYNAMO_ROLLER_ID,
RAPID_BLASTER_PRO_WNT_R_ID,
],
[],
0,
1000,
);
const canonicalKeys = combos.map((combo) => {
const grouped = new Map<string, number>();
for (const segment of combo.segments) {
const key = `${segment.damageType}:${segment.damageValue}`;
grouped.set(key, (grouped.get(key) ?? 0) + segment.count);
}
return [...grouped.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, count]) => `${key}:${count}`)
.join("|");
});
const uniqueKeys = new Set(canonicalKeys);
expect(uniqueKeys.size).toBe(canonicalKeys.length);
});
});
describe("virtual damage combos", () => {
test("Explosher has COMBO damage type combining DIRECT and DISTANCE", () => {
const sources = extractDamageSources([EXPLOSHER_ID]);

View File

@@ -303,6 +303,62 @@ function backtrack(
}
}
function normalizeCombo(combo: DamageCombo): DamageCombo {
const grouped = new Map<
string,
{ segment: DamageSegment; totalCount: number }
>();
for (const segment of combo.segments) {
const key = `${segment.damageType}:${segment.damageValue}`;
const existing = grouped.get(key);
if (existing) {
existing.totalCount += segment.count;
} else {
grouped.set(key, { segment, totalCount: segment.count });
}
}
const segments: DamageSegment[] = [];
for (const { segment, totalCount } of grouped.values()) {
segments.push({ ...segment, count: totalCount });
}
segments.sort((a, b) => {
const typeCompare = a.damageType.localeCompare(b.damageType);
if (typeCompare !== 0) return typeCompare;
return a.damageValue - b.damageValue;
});
return {
segments,
totalDamage: combo.totalDamage,
hitCount: combo.hitCount,
};
}
function comboKey(normalized: DamageCombo): string {
return normalized.segments
.map((s) => `${s.damageType}:${s.damageValue}:${s.count}`)
.join("|");
}
function deduplicateCombos(combos: DamageCombo[]): DamageCombo[] {
const seen = new Map<string, DamageCombo>();
for (const combo of combos) {
const normalized = normalizeCombo(combo);
const key = comboKey(normalized);
const existing = seen.get(key);
if (!existing || combo.segments.length < existing.segments.length) {
seen.set(key, combo);
}
}
return [...seen.values()];
}
function filterAndSortCombos(
combos: DamageCombo[],
maxCombosDisplayed: number,
@@ -323,7 +379,9 @@ function filterAndSortCombos(
return true;
});
filtered.sort((a, b) => {
const deduplicated = deduplicateCombos(filtered);
deduplicated.sort((a, b) => {
const aDistTo100 = Math.abs(a.totalDamage - 100);
const bDistTo100 = Math.abs(b.totalDamage - 100);
if (aDistTo100 !== bDistTo100) {
@@ -332,7 +390,7 @@ function filterAndSortCombos(
return a.hitCount - b.hitCount;
});
return filtered.slice(0, maxCombosDisplayed);
return deduplicated.slice(0, maxCombosDisplayed);
}
function hasOneShot(combo: DamageCombo): boolean {