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)}
); }