Filter out excessive combos in comp analyzer

This commit is contained in:
Kalle 2026-01-26 20:28:43 +02:00
parent 1edbda787f
commit 3ec5af1128
4 changed files with 79 additions and 5 deletions

View File

@ -15,6 +15,8 @@ export const MAX_COMBOS_DISPLAYED = 50;
export const COMBO_DAMAGE_THRESHOLD = 80;
export const LETHAL_DAMAGE = 100;
export const SUB_WEAPON_CATEGORIES: Record<SubWeaponId, SubWeaponCategory> = {
0: "LETHAL", // Splat Bomb
1: "LETHAL", // Suction Bomb

View File

@ -13,6 +13,7 @@ import {
specialWeaponImageUrl,
subWeaponImageUrl,
} from "~/utils/urls";
import { LETHAL_DAMAGE } from "../comp-analyzer-constants";
import { useTargetResAp, useTargetSubDefenseAp } from "../comp-analyzer-hooks";
import type { DamageCombo, DamageSegment } from "../comp-analyzer-types";
import {
@ -36,7 +37,6 @@ function weaponTypeFromSegment(
}
const SLOT_COLORS = ["yellow", "pink", "green", "blue"] as const;
const LETHAL_DAMAGE = 100;
interface DamageComboBarProps {
combo: DamageCombo;

View File

@ -322,6 +322,51 @@ describe("calculateInkTimeToKill", () => {
});
});
describe("calculateDamageCombos - excessive combo filtering", () => {
test("filters out excessive combos (200+ damage where removing any hit still kills)", () => {
const combos = calculateDamageCombos(
[SPLATTERSHOT_ID, SPLAT_ROLLER_ID, SPLAT_CHARGER_ID, AEROSPRAY_MG_ID],
[],
0,
1000,
);
const hasExcessiveCombo = combos.some((combo) => {
const flatDamages = combo.segments.flatMap((s) =>
Array(s.count).fill(s.damageValue),
);
for (const damage of flatDamages) {
const reducedDamage = combo.totalDamage - damage;
if (reducedDamage >= 100) {
return true;
}
}
return false;
});
expect(hasExcessiveCombo).toBe(false);
});
test("allows combos where removing any hit drops below lethal threshold", () => {
const combos = calculateDamageCombos(
[SPLATTERSHOT_ID, SPLAT_ROLLER_ID],
[],
0,
1000,
);
for (const combo of combos) {
const flatDamages = combo.segments.flatMap((s) =>
Array(s.count).fill(s.damageValue),
);
const allHitsNecessary = flatDamages.every(
(damage) => combo.totalDamage - damage < 100,
);
expect(allHitsNecessary).toBe(true);
}
});
});
describe("virtual damage combos", () => {
test("Explosher has COMBO damage type combining DIRECT and DISTANCE", () => {
const sources = extractDamageSources([EXPLOSHER_ID]);

View File

@ -11,6 +11,7 @@ import { weaponParams } from "~/features/build-analyzer/core/utils";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import {
COMBO_DAMAGE_THRESHOLD,
LETHAL_DAMAGE,
MAX_COMBOS_DISPLAYED,
MAX_DAMAGE_TYPES_PER_COMBO,
MAX_REPEATS_PER_DAMAGE_TYPE,
@ -145,6 +146,7 @@ export function calculateDamageCombos(
weaponIds: MainWeaponId[],
excludedKeys: ExcludedDamageKey[] = [],
targetSubDefenseAp = 0,
maxCombosDisplayed = MAX_COMBOS_DISPLAYED,
): DamageCombo[] {
if (weaponIds.length < 2) {
return [];
@ -157,7 +159,7 @@ export function calculateDamageCombos(
const sources = extractDamageSources(weaponIds, targetSubDefenseAp);
const damageOptions = flattenToOptions(sources, excludedSet);
const combos = generateCombinations(damageOptions);
const filtered = filterAndSortCombos(combos);
const filtered = filterAndSortCombos(combos, maxCombosDisplayed);
return filtered;
}
@ -301,7 +303,10 @@ function backtrack(
}
}
function filterAndSortCombos(combos: DamageCombo[]): DamageCombo[] {
function filterAndSortCombos(
combos: DamageCombo[],
maxCombosDisplayed: number,
): DamageCombo[] {
const filtered = combos.filter((combo) => {
if (combo.totalDamage < COMBO_DAMAGE_THRESHOLD) {
return false;
@ -311,6 +316,10 @@ function filterAndSortCombos(combos: DamageCombo[]): DamageCombo[] {
return false;
}
if (isExcessiveCombo(combo)) {
return false;
}
return true;
});
@ -323,11 +332,29 @@ function filterAndSortCombos(combos: DamageCombo[]): DamageCombo[] {
return a.hitCount - b.hitCount;
});
return filtered.slice(0, MAX_COMBOS_DISPLAYED);
return filtered.slice(0, maxCombosDisplayed);
}
function hasOneShot(combo: DamageCombo): boolean {
return combo.segments.some((s) => s.damageValue >= 100);
return combo.segments.some((s) => s.damageValue >= LETHAL_DAMAGE);
}
function isExcessiveCombo(combo: DamageCombo): boolean {
const flatDamages = combo.segments.flatMap((s) =>
Array(s.count).fill(s.damageValue),
);
const totalDamage = combo.totalDamage;
for (let i = 0; i < flatDamages.length; i++) {
const damage = flatDamages[i];
const reducedDamage = totalDamage - damage;
if (reducedDamage >= LETHAL_DAMAGE) {
return true;
}
}
return false;
}
const SPLASH_O_MATIC_ID = 20;