import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { Divider } from "~/components/Divider";
import {
SendouChipRadio,
SendouChipRadioGroup,
} from "~/components/elements/ChipRadio";
import { SendouSwitch } from "~/components/elements/Switch";
import {
SpecialWeaponImage,
SubWeaponImage,
WeaponImage,
} from "~/components/Image";
import { LocaleTime } from "~/components/LocaleTime";
import {
damageReceiverSuffix,
translateDamageReceiver,
} from "~/features/object-damage-calculator/calculator-constants";
import type { DamageReceiver } from "~/features/object-damage-calculator/calculator-types";
import {
useSearchParamState,
useSearchParamStateEncoder,
} from "~/hooks/useSearchParamState";
import type {
MainWeaponId,
SpecialWeaponId,
SubWeaponId,
} from "~/modules/in-game-lists/types";
import * as WeaponParams from "../core/WeaponParams";
import {
DAMAGE_MULTIPLIER_PARAM_KEY,
INCOMING_DAMAGE_MULTIPLIER_PARAM_KEY,
SPECIAL_POINTS_PARAM_KEY,
} from "../weapon-params-constants";
import type {
IncomingDamageAttackers,
KitPatchHistory,
PatchChange,
WeaponPatch,
} from "../weapon-params-types";
import styles from "./WeaponPatchHistory.module.css";
const PATCH_DATE_OPTIONS: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "short",
year: "numeric",
};
export function WeaponPatchHistory({ patches }: { patches: WeaponPatch[] }) {
const { t } = useTranslation(["params"]);
if (patches.length === 0) {
return
{t("params:noPatches")}
;
}
return (
{patches.map((patch) => (
{patch.changes.map((change, i) => (
))}
))}
);
}
/**
* Patch history of a main weapon shown one kit at a time: the selected kit's main weapon, sub
* weapon and special weapon changes are grouped under dividers within the same patch column. Sub
* and special weapon changes can be toggled off.
*/
export function WeaponPatchHistoryByKit({
kits,
defaultWeaponId,
}: {
kits: KitPatchHistory[];
defaultWeaponId: MainWeaponId;
}) {
const { t } = useTranslation(["params"]);
const kitIds = kits.map((kit) => kit.weaponId);
const [selectedWeaponId, setSelectedWeaponId] =
useSearchParamStateEncoder({
name: "kit",
defaultValue: defaultWeaponId,
revive: (value) => {
const id = Number(value) as MainWeaponId;
return kitIds.includes(id) ? id : undefined;
},
encode: (value) => String(value),
});
const [showSubSpecial, setShowSubSpecial] = useSearchParamState({
name: "kitExtras",
defaultValue: true,
revive: (value) =>
value === "false" ? false : value === "true" ? true : undefined,
});
const selectedKit =
kits.find((kit) => kit.weaponId === selectedWeaponId) ?? kits[0];
const patches = selectedKit.patches
.map((patch) => ({
...patch,
changes: showSubSpecial
? patch.changes
: patch.changes.filter((change) => change.source === "main"),
}))
.filter((patch) => patch.changes.length > 0);
return (
{kits.length > 1 ? (
) : null}
{t("params:patches.showSubSpecial")}
{patches.length === 0 ? (
{t("params:noPatches")}
) : (
{patches.map((patch) => (
))}
)}
);
}
function KitFilter({
kits,
selectedWeaponId,
onSelect,
}: {
kits: KitPatchHistory[];
selectedWeaponId: MainWeaponId;
onSelect: (weaponId: MainWeaponId) => void;
}) {
const { t } = useTranslation(["weapons"]);
return (
{kits.map((kit) => (
onSelect(Number(value) as MainWeaponId)}
>
{t(`weapons:MAIN_${kit.weaponId}`)}
))}
);
}
function KitPatchColumn({
patch,
kit,
}: {
patch: WeaponPatch;
kit: KitPatchHistory;
}) {
const mainChanges = patch.changes.filter(
(change) => change.source === "main",
);
const subChanges = patch.changes.filter((change) => change.source === "sub");
const specialChanges = patch.changes.filter(
(change) => change.source === "special",
);
return (
{mainChanges.map((change, i) => (
))}
{subChanges.length > 0 ? (
<>
{subChanges.map((change, i) => (
))}
>
) : null}
{specialChanges.length > 0 ? (
<>
{specialChanges.map((change, i) => (
))}
>
) : null}
);
}
function SubWeaponDivider({ subWeaponId }: { subWeaponId: SubWeaponId }) {
const { t } = useTranslation(["weapons"]);
return (
{t(`weapons:SUB_${subWeaponId}`)}
);
}
function SpecialWeaponDivider({
specialWeaponId,
}: {
specialWeaponId: SpecialWeaponId;
}) {
const { t } = useTranslation(["weapons"]);
return (
{t(`weapons:SPECIAL_${specialWeaponId}`)}
);
}
function PatchColumnHeader({
version,
date,
}: {
version: string;
date: string | null;
}) {
return (
{version}
{date ? (
) : null}
);
}
function changeKey(change: PatchChange, index: number) {
return `${change.category}.${change.key}.${change.weaponId ?? ""}.${change.source ?? ""}.${index}`;
}
function ChangeBadge({ change }: { change: PatchChange }) {
const { t } = useTranslation(["analyzer", "weapons", "game-misc"]);
if (
change.category === INCOMING_DAMAGE_MULTIPLIER_PARAM_KEY &&
change.attackers
) {
return ;
}
const isSpecialPoints = change.category === SPECIAL_POINTS_PARAM_KEY;
const isDamageMultiplier = change.category === DAMAGE_MULTIPLIER_PARAM_KEY;
// Damage falloff curves serialize to long "damage @ distance" lists that need their own line.
const isWideValue =
typeof change.from === "string" && change.from.includes("@");
const label = isSpecialPoints
? t("analyzer:stat.specialPoints")
: isDamageMultiplier
? translateDamageReceiver(t, change.key as DamageReceiver)
: change.key;
return (
{isSpecialPoints && change.weaponId ? (
) : null}
{label}
{WeaponParams.formatValue(change.from)}
→
{WeaponParams.formatValue(change.to)}
);
}
/**
* An incoming damage multiplier change: a set of attacking weapons whose shared damage rate against
* the page's sub or special weapon changed. Shows the attacking weapons' icons (with a suffix for
* multi-part objects, e.g. a Big Bubbler's shield vs. weak point) and the from→to rate.
*/
function IncomingChangeBadge({
change,
attackers,
}: {
change: PatchChange;
attackers: IncomingDamageAttackers;
}) {
const { t } = useTranslation(["analyzer", "weapons", "game-misc"]);
const suffix = damageReceiverSuffix(t, change.key as DamageReceiver);
return (
{attackers.mainWeaponIds.map((id) => (
))}
{attackers.subWeaponIds.map((id) => (
))}
{attackers.specialWeaponIds.map((id) => (
))}
{suffix ? (
{suffix}
) : null}
{WeaponParams.formatValue(change.from)}
→
{WeaponParams.formatValue(change.to)}
);
}