diff --git a/app/features/build-analyzer/analyzer-constants.ts b/app/features/build-analyzer/analyzer-constants.ts index 190be4a7b..52131e323 100644 --- a/app/features/build-analyzer/analyzer-constants.ts +++ b/app/features/build-analyzer/analyzer-constants.ts @@ -45,6 +45,7 @@ export const DAMAGE_TYPE = [ "SPECIAL_TICK", "SECONDARY_MODE_MAX", "SECONDARY_MODE_MIN", + "COMBO", ] as const; export const damageTypeToWeaponType: Record< @@ -93,6 +94,7 @@ export const damageTypeToWeaponType: Record< SPECIAL_BUMP: "SPECIAL", SPECIAL_JUMP: "SPECIAL", SPECIAL_TICK: "SPECIAL", + COMBO: "MAIN", }; export const RAINMAKER_SPEED_PENALTY_MODIFIER = 0.8; diff --git a/app/features/build-analyzer/core/stats.ts b/app/features/build-analyzer/core/stats.ts index 9e31953d9..cb3f6d820 100644 --- a/app/features/build-analyzer/core/stats.ts +++ b/app/features/build-analyzer/core/stats.ts @@ -446,6 +446,8 @@ const damageTypeToParamsKey: Record< SPECIAL_BUMP: "BumpDamage", SPECIAL_JUMP: "JumpDamage", SPECIAL_TICK: "TickDamage", + // Virtual damage type for comp-analyzer, calculated from other damages + COMBO: [], }; function damages(args: StatFunctionInput): AnalyzedBuild["stats"]["damages"] { diff --git a/app/features/comp-analyzer/comp-analyzer-constants.ts b/app/features/comp-analyzer/comp-analyzer-constants.ts index 1f85a86d7..c68c8edc1 100644 --- a/app/features/comp-analyzer/comp-analyzer-constants.ts +++ b/app/features/comp-analyzer/comp-analyzer-constants.ts @@ -1,4 +1,6 @@ +import type { DamageType } from "~/features/build-analyzer/analyzer-types"; import type { + MainWeaponId, SpecialWeaponId, SubWeaponId, } from "~/modules/in-game-lists/types"; @@ -76,3 +78,21 @@ export const SPECIAL_CATEGORY_ORDER = [ export type SubWeaponCategory = (typeof SUB_CATEGORY_ORDER)[number]; export type SpecialWeaponCategory = (typeof SPECIAL_CATEGORY_ORDER)[number]; + +interface VirtualDamageCombo { + damageTypes: DamageType[]; + virtualType: DamageType; +} + +const EXPLOSHER_ID = 3040 as MainWeaponId; + +export const VIRTUAL_DAMAGE_COMBOS: Partial< + Record +> = { + [EXPLOSHER_ID]: [ + { + damageTypes: ["DIRECT", "DISTANCE"], + virtualType: "COMBO", + }, + ], +}; diff --git a/app/features/comp-analyzer/core/damage-combinations.test.ts b/app/features/comp-analyzer/core/damage-combinations.test.ts index 229e5af70..c114aad59 100644 --- a/app/features/comp-analyzer/core/damage-combinations.test.ts +++ b/app/features/comp-analyzer/core/damage-combinations.test.ts @@ -16,6 +16,7 @@ const SPLAT_ROLLER_ID = 1010; const SPLAT_CHARGER_ID = 2010; const AEROSPRAY_MG_ID = 30; const SPLATTERSHOT_JR_ID = 10; +const EXPLOSHER_ID = 3040; describe("extractDamageSources", () => { test("extracts main weapon damages", () => { @@ -309,3 +310,33 @@ describe("calculateInkTimeToKill", () => { expect(result).toBeGreaterThan(0); }); }); + +describe("virtual damage combos", () => { + test("Explosher has COMBO damage type combining DIRECT and DISTANCE", () => { + const sources = extractDamageSources([EXPLOSHER_ID]); + const damages = sources[0].damages; + + const comboDamage = damages.find((d) => d.type === "COMBO"); + expect(comboDamage).toBeDefined(); + expect(comboDamage?.weaponType).toBe("MAIN"); + + const directDamage = damages.find((d) => d.type === "DIRECT"); + const distanceDamage = damages.find((d) => d.type === "DISTANCE"); + + expect(directDamage).toBeDefined(); + expect(distanceDamage).toBeDefined(); + + const expectedComboValue = + (directDamage?.value ?? 0) + (distanceDamage?.value ?? 0); + expect(comboDamage?.value).toBeCloseTo(expectedComboValue, 1); + }); + + test("COMBO damage appears in damage combos", () => { + const combos = calculateDamageCombos([EXPLOSHER_ID, SPLATTERSHOT_ID]); + + const comboWithComboType = combos.find((c) => + c.segments.some((s) => s.damageType === "COMBO"), + ); + expect(comboWithComboType).toBeDefined(); + }); +}); diff --git a/app/features/comp-analyzer/core/damage-combinations.ts b/app/features/comp-analyzer/core/damage-combinations.ts index 5eb32cbd1..212000427 100644 --- a/app/features/comp-analyzer/core/damage-combinations.ts +++ b/app/features/comp-analyzer/core/damage-combinations.ts @@ -14,6 +14,7 @@ import { MAX_COMBOS_DISPLAYED, MAX_DAMAGE_TYPES_PER_COMBO, MAX_REPEATS_PER_DAMAGE_TYPE, + VIRTUAL_DAMAGE_COMBOS, } from "../comp-analyzer-constants"; import type { DamageCombo, @@ -76,6 +77,24 @@ export function extractDamageSources( }); } + const virtualCombos = VIRTUAL_DAMAGE_COMBOS[weaponId]; + if (virtualCombos) { + for (const combo of virtualCombos) { + const combinedValue = combo.damageTypes.reduce((sum, type) => { + const damage = damages.find((d) => d.type === type); + return sum + (damage?.value ?? 0); + }, 0); + + if (combinedValue > 0) { + damages.push({ + type: combo.virtualType, + value: combinedValue, + weaponType: "MAIN", + }); + } + } + } + return { weaponSlot: slot, weaponId, diff --git a/app/features/object-damage-calculator/calculator-hooks.ts b/app/features/object-damage-calculator/calculator-hooks.ts index 2b89d28c0..1322bff09 100644 --- a/app/features/object-damage-calculator/calculator-hooks.ts +++ b/app/features/object-damage-calculator/calculator-hooks.ts @@ -144,6 +144,7 @@ const damageTypePriorityList = [ "SPECIAL_TICK", "SECONDARY_MODE_MAX", "SECONDARY_MODE_MIN", + "COMBO", ] as const; assertType< (typeof damageTypePriorityList)[number], diff --git a/locales/da/analyzer.json b/locales/da/analyzer.json index 658c56c1e..b3f326c64 100644 --- a/locales/da/analyzer.json +++ b/locales/da/analyzer.json @@ -136,6 +136,7 @@ "damage.SPECIAL_TICK": "Løbende", "damage.TURRET_MIN": "Stationær skydning (Minimum)", "damage.TURRET_MAX": "Stationær skydning (Maximum)", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "HP", "suffix.specialPointsShort": "p", diff --git a/locales/de/analyzer.json b/locales/de/analyzer.json index 1d0c790a5..451ef68c2 100644 --- a/locales/de/analyzer.json +++ b/locales/de/analyzer.json @@ -136,6 +136,7 @@ "damage.SPECIAL_TICK": "", "damage.TURRET_MIN": "", "damage.TURRET_MAX": "", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "hp", "suffix.specialPointsShort": "p", diff --git a/locales/en/analyzer.json b/locales/en/analyzer.json index 3a7b73739..eaa171b44 100644 --- a/locales/en/analyzer.json +++ b/locales/en/analyzer.json @@ -136,6 +136,7 @@ "damage.SPECIAL_TICK": "Tick", "damage.TURRET_MIN": "Turret (Minimum)", "damage.TURRET_MAX": "Turret (Maximum)", + "damage.COMBO": "Combo", "suffix.seconds": "s", "suffix.hp": "hp", "suffix.specialPointsShort": "p", diff --git a/locales/es-ES/analyzer.json b/locales/es-ES/analyzer.json index e59e384e2..ceddf9b64 100644 --- a/locales/es-ES/analyzer.json +++ b/locales/es-ES/analyzer.json @@ -137,6 +137,7 @@ "damage.SPECIAL_TICK": "Por tiempo", "damage.TURRET_MIN": "Tras rodar (mínimo)", "damage.TURRET_MAX": "Tras rodar (máximo)", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "hp", "suffix.specialPointsShort": "p", diff --git a/locales/es-US/analyzer.json b/locales/es-US/analyzer.json index e59e384e2..ceddf9b64 100644 --- a/locales/es-US/analyzer.json +++ b/locales/es-US/analyzer.json @@ -137,6 +137,7 @@ "damage.SPECIAL_TICK": "Por tiempo", "damage.TURRET_MIN": "Tras rodar (mínimo)", "damage.TURRET_MAX": "Tras rodar (máximo)", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "hp", "suffix.specialPointsShort": "p", diff --git a/locales/fr-CA/analyzer.json b/locales/fr-CA/analyzer.json index 832c82461..333ec5d21 100644 --- a/locales/fr-CA/analyzer.json +++ b/locales/fr-CA/analyzer.json @@ -137,6 +137,7 @@ "damage.SPECIAL_TICK": "Dégâts sur la durée", "damage.TURRET_MIN": "Combiné (Minimum)", "damage.TURRET_MAX": "Combiné (Maximum)", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "pv", "suffix.specialPointsShort": "p", diff --git a/locales/fr-EU/analyzer.json b/locales/fr-EU/analyzer.json index fb78aba58..56d2548f9 100644 --- a/locales/fr-EU/analyzer.json +++ b/locales/fr-EU/analyzer.json @@ -137,6 +137,7 @@ "damage.SPECIAL_TICK": "Dégâts sur la durée", "damage.TURRET_MIN": "Combiné (Minimum)", "damage.TURRET_MAX": "Combiné (Maximum)", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "pv", "suffix.specialPointsShort": "p", diff --git a/locales/he/analyzer.json b/locales/he/analyzer.json index 8ed7404d5..6a8ed1ab8 100644 --- a/locales/he/analyzer.json +++ b/locales/he/analyzer.json @@ -137,6 +137,7 @@ "damage.SPECIAL_TICK": "Tick", "damage.TURRET_MIN": "נייח (מינימום)", "damage.TURRET_MAX": "נייח (מקסימום)", + "damage.COMBO": "", "suffix.seconds": "ש", "suffix.hp": "hp", "suffix.specialPointsShort": "p", diff --git a/locales/it/analyzer.json b/locales/it/analyzer.json index 8d9eef055..eb3f64233 100644 --- a/locales/it/analyzer.json +++ b/locales/it/analyzer.json @@ -137,6 +137,7 @@ "damage.SPECIAL_TICK": "Tick", "damage.TURRET_MIN": "Torretta (Minimo)", "damage.TURRET_MAX": "Torretta (Massimo)", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "ps", "suffix.specialPointsShort": "p", diff --git a/locales/ja/analyzer.json b/locales/ja/analyzer.json index fea95db6f..25f7e6167 100644 --- a/locales/ja/analyzer.json +++ b/locales/ja/analyzer.json @@ -134,6 +134,7 @@ "damage.SPECIAL_TICK": "ティックあたり", "damage.TURRET_MIN": "スライド (最小)", "damage.TURRET_MAX": "スライド (最大)", + "damage.COMBO": "", "suffix.seconds": "秒", "suffix.hp": "ダメージ", "suffix.specialPointsShort": "ポイント", diff --git a/locales/ko/analyzer.json b/locales/ko/analyzer.json index f7149eaaa..b8090e9d5 100644 --- a/locales/ko/analyzer.json +++ b/locales/ko/analyzer.json @@ -134,6 +134,7 @@ "damage.SPECIAL_TICK": "틱", "damage.TURRET_MIN": "구르기 (최소)", "damage.TURRET_MAX": "구르기 (최대)", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "hp", "suffix.specialPointsShort": "p", diff --git a/locales/nl/analyzer.json b/locales/nl/analyzer.json index 245055ae8..d24168b3a 100644 --- a/locales/nl/analyzer.json +++ b/locales/nl/analyzer.json @@ -136,6 +136,7 @@ "damage.SPECIAL_TICK": "", "damage.TURRET_MIN": "", "damage.TURRET_MAX": "", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "hp", "suffix.specialPointsShort": "p", diff --git a/locales/pl/analyzer.json b/locales/pl/analyzer.json index 48856503f..93c6cf62c 100644 --- a/locales/pl/analyzer.json +++ b/locales/pl/analyzer.json @@ -138,6 +138,7 @@ "damage.SPECIAL_TICK": "", "damage.TURRET_MIN": "", "damage.TURRET_MAX": "", + "damage.COMBO": "", "suffix.seconds": "s", "suffix.hp": "hp", "suffix.specialPointsShort": "pkt", diff --git a/locales/pt-BR/analyzer.json b/locales/pt-BR/analyzer.json index 5e39aad02..3bd753258 100644 --- a/locales/pt-BR/analyzer.json +++ b/locales/pt-BR/analyzer.json @@ -137,6 +137,7 @@ "damage.SPECIAL_TICK": "Tick", "damage.TURRET_MIN": "Tiros contínuos (Mínimo)", "damage.TURRET_MAX": "Tiros contínuos (Máximo)", + "damage.COMBO": "", "suffix.seconds": " seg", "suffix.hp": "HP", "suffix.specialPointsShort": "p", diff --git a/locales/ru/analyzer.json b/locales/ru/analyzer.json index ab5240d0a..24ab506dd 100644 --- a/locales/ru/analyzer.json +++ b/locales/ru/analyzer.json @@ -138,6 +138,7 @@ "damage.SPECIAL_TICK": "Тик", "damage.TURRET_MIN": "Турель (Минимум)", "damage.TURRET_MAX": "Турель (Максимум)", + "damage.COMBO": "", "suffix.seconds": "сек", "suffix.hp": "hp", "suffix.specialPointsShort": "очк.", diff --git a/locales/zh/analyzer.json b/locales/zh/analyzer.json index 0b7a48abc..aca82c5c5 100644 --- a/locales/zh/analyzer.json +++ b/locales/zh/analyzer.json @@ -134,6 +134,7 @@ "damage.SPECIAL_TICK": "雨滴", "damage.TURRET_MIN": "双枪合一 (最小值)", "damage.TURRET_MAX": "双枪合一 (最大值)", + "damage.COMBO": "", "suffix.seconds": "秒", "suffix.hp": "hp", "suffix.specialPointsShort": "p",