sendou.ink/app/features/params/core/WeaponParams.test.ts
2026-06-17 14:37:44 +03:00

474 lines
11 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
DAMAGE_MULTIPLIER_PARAM_KEY,
INCOMING_DAMAGE_MULTIPLIER_PARAM_KEY,
SPECIAL_POINTS_PARAM_KEY,
} from "../weapon-params-constants";
import type {
DamageMultiplierWithHistory,
ParsedWeaponParams,
} from "../weapon-params-types";
import { classifyParamChange } from "./param-directions";
import * as WeaponParams from "./WeaponParams";
const VERSIONS = ["1.0.0", "2.0.0", "3.0.0"];
const emptyParsed = (weaponId: number): ParsedWeaponParams => ({
weaponId,
categories: {},
});
const row = (overrides: {
mainWeaponIds?: number[];
subWeaponIds?: number[];
specialWeaponIds?: number[];
targets: DamageMultiplierWithHistory[];
}) => ({
mainWeaponIds: [],
subWeaponIds: [],
specialWeaponIds: [],
...overrides,
});
describe("damageMultipliersForWeapon", () => {
it("collects only rows applying to the weapon for the given kind", () => {
const rows = {
a: row({
specialWeaponIds: [11],
targets: [{ target: "Chariot", current: 2, history: [] }],
}),
b: row({
specialWeaponIds: [12],
targets: [{ target: "ShockSonar", current: 2, history: [] }],
}),
};
const result = WeaponParams.damageMultipliersForWeapon(rows, 11, "special");
expect(result.map((m) => m.target)).toEqual(["Chariot"]);
});
it("de-duplicates identical target histories shared across rows", () => {
const sharedTarget: DamageMultiplierWithHistory = {
target: "GreatBarrier_Barrier",
current: 1.4,
history: [{ version: "1.0.0", value: 2.8 }],
};
const rows = {
bullet: row({ specialWeaponIds: [10], targets: [sharedTarget] }),
bombCore: row({
specialWeaponIds: [10],
targets: [{ ...sharedTarget }],
}),
};
const result = WeaponParams.damageMultipliersForWeapon(rows, 10, "special");
expect(result).toHaveLength(1);
});
it("merges several rows of the same target into the most informative entry", () => {
const rows = {
swing: row({
specialWeaponIds: [11],
targets: [{ target: "Chariot", current: 4.5, history: [] }],
}),
throwBombCore: row({
specialWeaponIds: [11],
targets: [
{
target: "Chariot",
current: 3.273,
history: [
{ version: "2.0.0", value: 2 },
{ version: "3.0.0", value: 6 },
],
},
],
}),
};
const result = WeaponParams.damageMultipliersForWeapon(rows, 11, "special");
expect(result).toHaveLength(1);
expect(result[0].history).toHaveLength(2);
expect(result[0].current).toBe(3.273);
});
it("orders entries like DAMAGE_RECEIVERS", () => {
const rows = {
a: row({
specialWeaponIds: [11],
targets: [
{ target: "Wsb_Shield", current: 2, history: [] },
{ target: "Chariot", current: 3, history: [] },
],
}),
};
const result = WeaponParams.damageMultipliersForWeapon(rows, 11, "special");
// Chariot precedes Wsb_Shield in DAMAGE_RECEIVERS
expect(result.map((m) => m.target)).toEqual(["Chariot", "Wsb_Shield"]);
});
});
describe("patchHistory damage multipliers", () => {
const buildWith = (multiplier: DamageMultiplierWithHistory) =>
WeaponParams.patchHistory(emptyParsed(11), VERSIONS, [], [multiplier]);
it("attributes a change to the version after the recorded one and flags a higher rate as a buff", () => {
const patches = buildWith({
target: "Wsb_Shield",
current: 2.2,
history: [{ version: "1.0.0", value: 2 }],
});
expect(patches).toHaveLength(1);
expect(patches[0].version).toBe("2.0.0");
expect(patches[0].changes).toEqual([
{
category: DAMAGE_MULTIPLIER_PARAM_KEY,
key: "Wsb_Shield",
from: 2,
to: 2.2,
kind: "buff",
},
]);
});
it("flags a lower rate as a nerf", () => {
const patches = buildWith({
target: "NiceBall_Armor",
current: 1.82,
history: [{ version: "2.0.0", value: 2.6 }],
});
expect(patches).toHaveLength(1);
expect(patches[0].version).toBe("3.0.0");
expect(patches[0].changes[0].kind).toBe("nerf");
});
});
describe("incomingDamageMultipliersForWeapon", () => {
it("collects other weapons' rates against the weapon's receiver targets", () => {
const rows = {
fromSpecial: row({
specialWeaponIds: [10],
targets: [
{
target: "GreatBarrier_Barrier",
current: 1.4,
history: [{ version: "1.0.0", value: 2.8 }],
},
],
}),
fromMains: row({
mainWeaponIds: [200, 201],
targets: [
{
target: "GreatBarrier_WeakPoint",
current: 3,
history: [{ version: "2.0.0", value: 2 }],
},
],
}),
unrelated: row({
mainWeaponIds: [400],
targets: [{ target: "Chariot", current: 2, history: [] }],
}),
};
// special id 2 is Big Bubbler (GreatBarrier_Barrier + GreatBarrier_WeakPoint)
const result = WeaponParams.incomingDamageMultipliersForWeapon(
rows,
2,
"special",
);
expect(result.map((m) => m.target)).toEqual([
"GreatBarrier_Barrier",
"GreatBarrier_WeakPoint",
]);
expect(result[1].attackers.mainWeaponIds).toEqual([200, 201]);
});
it("de-duplicates the same attacker group and target across rows", () => {
const target = {
target: "GreatBarrier_Barrier",
current: 1.4,
history: [{ version: "1.0.0", value: 2.8 }],
};
const rows = {
bullet: row({ specialWeaponIds: [10], targets: [target] }),
bombCore: row({ specialWeaponIds: [10], targets: [{ ...target }] }),
};
const result = WeaponParams.incomingDamageMultipliersForWeapon(
rows,
2,
"special",
);
expect(result).toHaveLength(1);
});
it("returns nothing for a weapon that is not a damageable object", () => {
const rows = {
a: row({
specialWeaponIds: [10],
targets: [
{
target: "GreatBarrier_Barrier",
current: 1,
history: [{ version: "1.0.0", value: 2 }],
},
],
}),
};
// special id 1 (Trizooka) is not in INCOMING_DAMAGE_RECEIVERS
expect(
WeaponParams.incomingDamageMultipliersForWeapon(rows, 1, "special"),
).toEqual([]);
});
});
describe("patchHistory incoming damage multipliers", () => {
it("flags a higher incoming rate as a nerf to the defending weapon and carries the attackers", () => {
const patches = WeaponParams.patchHistory(
emptyParsed(2),
VERSIONS,
[],
[],
[
{
target: "GreatBarrier_Barrier",
attackers: {
mainWeaponIds: [200],
subWeaponIds: [],
specialWeaponIds: [],
},
current: 2.8,
history: [{ version: "1.0.0", value: 1.4 }],
},
],
);
expect(patches).toHaveLength(1);
expect(patches[0].version).toBe("2.0.0");
expect(patches[0].changes[0]).toMatchObject({
category: INCOMING_DAMAGE_MULTIPLIER_PARAM_KEY,
key: "GreatBarrier_Barrier",
from: 1.4,
to: 2.8,
kind: "nerf",
attackers: { mainWeaponIds: [200] },
});
});
it("flags a lower incoming rate as a buff to the defending weapon", () => {
const patches = WeaponParams.patchHistory(
emptyParsed(2),
VERSIONS,
[],
[],
[
{
target: "GreatBarrier_Barrier",
attackers: {
mainWeaponIds: [200],
subWeaponIds: [],
specialWeaponIds: [],
},
current: 1.4,
history: [{ version: "1.0.0", value: 2.8 }],
},
],
);
expect(patches[0].changes[0].kind).toBe("buff");
});
});
describe("parse damage falloff curves", () => {
it("serializes a DistanceDamage array into a scaled damage @ distance string", () => {
const parsed = WeaponParams.parse(
0,
{
BlastParam: {
DistanceDamage: [
{ Damage: 1800, Distance: 3.6 },
{ Damage: 300, Distance: 7 },
],
},
},
VERSIONS,
);
expect(parsed.categories.BlastParam.DistanceDamage.current).toBe(
"180 @ 3.6, 30 @ 7",
);
});
it("flattens nested breakpoint arrays", () => {
const parsed = WeaponParams.parse(
0,
{
BlastParam: {
DistanceDamage: [
[{ Damage: 1800, Distance: 3.6 }],
[{ Damage: 300, Distance: 7 }],
],
},
},
VERSIONS,
);
expect(parsed.categories.BlastParam.DistanceDamage.current).toBe(
"180 @ 3.6, 30 @ 7",
);
});
it("tracks per-version history of a damage falloff curve", () => {
const parsed = WeaponParams.parse(
0,
{
BlastParam: {
DistanceDamage: [{ Damage: 600, Distance: 4 }],
"DistanceDamage@2.0.0": [{ Damage: 400, Distance: 4 }],
},
},
VERSIONS,
);
expect(parsed.categories.BlastParam.DistanceDamage.history).toEqual([
{ version: "2.0.0", value: "40 @ 4" },
]);
});
});
describe("classifyParamChange damage falloff curves", () => {
it("flags higher damage as a buff", () => {
expect(
classifyParamChange("BlastParam", "DistanceDamage", "40 @ 4", "60 @ 4"),
).toBe("buff");
});
it("flags lower damage as a nerf", () => {
expect(
classifyParamChange("BlastParam", "DistanceDamage", "60 @ 4", "40 @ 4"),
).toBe("nerf");
});
it("flags longer reach at the same damage as a buff", () => {
expect(
classifyParamChange(
"BlastParam",
"DistanceDamage",
"70 @ 0.94, 50 @ 3.3",
"70 @ 1.01, 50 @ 3.37",
),
).toBe("buff");
});
it("flags shorter reach at the same damage as a nerf", () => {
expect(
classifyParamChange(
"BlastParam",
"DistanceDamage",
"70 @ 1.01, 50 @ 3.37",
"70 @ 0.975, 50 @ 3.37",
),
).toBe("nerf");
});
it("is neutral when damage rises but reach shrinks", () => {
expect(
classifyParamChange("BlastParam", "DistanceDamage", "60 @ 4", "70 @ 3.5"),
).toBe("neutral");
});
it("is neutral when the curve gains or loses a breakpoint", () => {
expect(
classifyParamChange(
"BlastParam",
"DistanceDamage",
"60 @ 4",
"60 @ 4, 30 @ 8",
),
).toBe("neutral");
});
});
describe("kitPatchHistories", () => {
const kitHistory = () =>
WeaponParams.kitPatchHistories({
mainParsed: emptyParsed(11),
versions: VERSIONS,
kits: [{ weaponId: 11, subWeaponId: 1, specialWeaponId: 2 }],
specialPointsByKit: {
"11": {
weaponId: 11,
current: 180,
history: [{ version: "1.0.0", value: 200 }],
},
},
mainDamageMultipliers: [
{
target: "Wsb_Shield",
current: 2.2,
history: [{ version: "1.0.0", value: 2 }],
},
],
subParams: { "1": emptyParsed(1) },
subDamageMultipliers: {
"1": [
{
target: "Chariot",
current: 3,
history: [{ version: "1.0.0", value: 2 }],
},
],
},
subIncomingDamageMultipliers: {},
specialParams: { "2": emptyParsed(2) },
specialDamageMultipliers: {
"2": [
{
target: "NiceBall_Armor",
current: 1.5,
history: [{ version: "2.0.0", value: 2 }],
},
],
},
specialIncomingDamageMultipliers: {},
});
it("folds the kit's main, sub and special weapon changes into one descending history", () => {
const [history] = kitHistory();
expect(history.weaponId).toBe(11);
expect(history.patches.map((patch) => patch.version)).toEqual([
"3.0.0",
"2.0.0",
]);
});
it("tags each change with its source and groups main before sub before special", () => {
const [history] = kitHistory();
const v2 = history.patches.find((patch) => patch.version === "2.0.0")!;
// special points + main damage rate (both main), then the sub weapon's damage rate
expect(v2.changes.map((change) => change.source)).toEqual([
"main",
"main",
"sub",
]);
expect(v2.changes[0].category).toBe(SPECIAL_POINTS_PARAM_KEY);
const v3 = history.patches.find((patch) => patch.version === "3.0.0")!;
expect(v3.changes.map((change) => change.source)).toEqual(["special"]);
});
});