import { describe, expect, test } from "vitest"; import type { AbilityPoints, AnalyzedBuild, DamageType, } from "~/features/build-analyzer/analyzer-types"; import { buildStats } from "~/features/build-analyzer/core/stats"; import type { MainWeaponId, SpecialWeaponId, SubWeaponId, } from "~/modules/in-game-lists/types"; import { exampleMainWeaponIdWithSpecialWeaponId, mainWeaponIds, specialWeaponIds, } from "~/modules/in-game-lists/weapon-ids"; import { calculateDamage } from "./objectDamage"; function calculate({ mainWeaponId = 10, subWeaponId, specialWeaponId, abilityPoints = new Map(), damageType = "NORMAL_MAX", preAnalyzed, }: { mainWeaponId?: MainWeaponId; subWeaponId?: SubWeaponId; specialWeaponId?: SpecialWeaponId; abilityPoints?: AbilityPoints; damageType?: DamageType; preAnalyzed?: AnalyzedBuild; }) { const analyzed = preAnalyzed ?? buildStats({ weaponSplId: mainWeaponId, hasTacticooler: false, }); return calculateDamage({ abilityPoints, analyzed, anyWeapon: typeof subWeaponId === "number" ? { type: "SUB", id: subWeaponId } : typeof specialWeaponId === "number" ? { type: "SPECIAL", id: specialWeaponId } : { type: "MAIN", id: mainWeaponId }, damageType, isMultiShot: true, }); } describe("calculateDamage()", () => { // the function throws if weapon resolves to more than one set of damage rates // so this test goes through all of them to make sure they all work test("Every weapon can calculate damage", () => { for (const mainWeaponId of mainWeaponIds) { const analyzed = buildStats({ weaponSplId: mainWeaponId, hasTacticooler: false, }); for (const damage of analyzed.stats.damages) { if (damage.type === "COMBO") continue; calculate({ mainWeaponId, damageType: damage.type }); } } const analyzed = buildStats({ weaponSplId: 0, hasTacticooler: false, }); for (const damage of analyzed.stats.subWeaponDefenseDamages) { calculate({ subWeaponId: damage.subWeaponId, damageType: damage.type, preAnalyzed: analyzed, }); } for (const specialWeaponId of specialWeaponIds) { const analyzedWithSpecialWeapon = buildStats({ weaponSplId: exampleMainWeaponIdWithSpecialWeaponId(specialWeaponId), hasTacticooler: false, }); for (const damage of analyzedWithSpecialWeapon.stats .specialWeaponDamages) { if (damage.type === "COMBO") continue; calculate({ specialWeaponId, damageType: damage.type, preAnalyzed: analyzedWithSpecialWeapon, }); } } }); test("BRU increases Splash Wall hitpoints", () => { const withoutBRU = calculate({}); const withBRU = calculate({ abilityPoints: new Map([["BRU", 10]]), }); const hpWithoutBRU = withoutBRU.find( (d) => d.receiver === "Wsb_Shield", )?.hitPoints; // biome-ignore lint/suspicious/noNonNullAssertedOptionalChain: Biome 2.3.1 upgrade const hpWithBRU = withBRU.find((d) => d.receiver === "Wsb_Shield") ?.hitPoints!; expect(typeof hpWithoutBRU).toBe("number"); expect(typeof hpWithBRU).toBe("number"); expect(hpWithoutBRU).toBeLessThan(hpWithBRU); }); test("SPU increases Big Bubbler hitpoints", () => { const withoutSPU = calculate({}); const withSPU = calculate({ abilityPoints: new Map([["SPU", 10]]), }); const hpWithoutSPU = withoutSPU.find( (d) => d.receiver === "GreatBarrier_Barrier", )?.hitPoints; // biome-ignore lint/suspicious/noNonNullAssertedOptionalChain: Biome 2.3.1 upgrade const hpWithSPU = withSPU.find((d) => d.receiver === "GreatBarrier_Barrier") ?.hitPoints!; expect(typeof hpWithoutSPU).toBe("number"); expect(typeof hpWithSPU).toBe("number"); expect(hpWithoutSPU).toBeLessThan(hpWithSPU); }); const shotsToPopRM: Array< [ weaponId: MainWeaponId, damageType: DamageType, shotsToPop: number, shotsToPopOS: number, ] > = [ // Splattershot [40, "NORMAL_MAX", 28, 26], // Range Blaster [220, "DIRECT", 5, 4], // .96 Gal [80, "NORMAL_MAX", 17, 15], // Luna Blaster [200, "DIRECT", 4, 4], // Splat Charger [2010, "FULL_CHARGE", 4, 3], // E-liter 4K [2030, "TAP_SHOT", 13, 12], // Hydra Splatling [4020, "NORMAL_MAX", 32, 29], // Sloshing Machine [3020, "DIRECT_MAX", 6, 5], // Splat Dualies [5010, "NORMAL_MAX", 34, 31], // Tenta Brella [6010, "NORMAL_MAX", 4, 4], // // Tri-Stringer [7010, "NORMAL_MAX", 3, 3], // REEF-LUX [7020, "NORMAL_MIN", 8, 7], // Splatana Wiper [8010, "SPLATANA_HORIZONTAL", 11, 10], // Splatana Wiper [8010, "SPLATANA_HORIZONTAL_DIRECT", 9, 8], // Splatana Stamper [8000, "SPLATANA_VERTICAL_DIRECT", 3, 3], ]; test("Calculates matching HTD Rainmaker shield to in-game tests", () => { for (const [ mainWeaponId, damageType, shotsToPop, shotsToPopOS, ] of shotsToPopRM) { const damages = calculate({ mainWeaponId, damageType }); const damageVsRM = damages.find( (d) => d.receiver === "Gachihoko_Barrier", )!; expect( damageVsRM.damages.find((d) => !d.objectShredder)!.hitsToDestroy, `Shots to pop wrong for weapon id: ${mainWeaponId}`, ).toBe(shotsToPop); expect( damageVsRM.damages.find((d) => d.objectShredder)!.hitsToDestroy, `Shots to pop wrong with OS for weapon id: ${mainWeaponId}`, ).toBe(shotsToPopOS); } }); const HYDRA_SPLATLING_ID = 4020; test("Hits to destroy Minimum < Maximum < Maximum (Fully charged)", () => { const min = calculate({ mainWeaponId: HYDRA_SPLATLING_ID, damageType: "NORMAL_MIN", })[0]?.damages[0]?.hitsToDestroy; const max = calculate({ mainWeaponId: HYDRA_SPLATLING_ID, damageType: "NORMAL_MAX", })[0]?.damages[0]?.hitsToDestroy; const maxFullyCharged = calculate({ mainWeaponId: HYDRA_SPLATLING_ID, damageType: "NORMAL_MAX_FULL_CHARGE", })[0]?.damages[0]?.hitsToDestroy; expect(typeof min).toBe("number"); expect(typeof max).toBe("number"); expect(typeof maxFullyCharged).toBe("number"); expect(min).toBeGreaterThan(max); expect(max).toBeGreaterThan(maxFullyCharged); }); });