import clsx from "clsx"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { Ability } from "~/components/Ability"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { SendouButton } from "../../../components/elements/Button"; import { SendouPopover } from "../../../components/elements/Popover"; import { MAX_AP } from "../analyzer-constants"; import type { FullInkTankOption } from "../analyzer-types"; import { fullInkTankOptions } from "../core/stats"; import { weaponParams } from "../core/utils"; interface PerInkTankGridProps { weaponSplId: MainWeaponId; } export function PerInkTankGrid(props: PerInkTankGridProps) { const { t } = useTranslation(["analyzer"]); return ( {t("analyzer:button.showConsumptionGrid")} } > ); } // if adding to this, also update analyzer.css const AP_VALUES_TO_SHOW = [ 0, 3, 6, 9, 10, 12, 13, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 41, 42, 44, 45, 47, 48, 51, 54, 57, ]; function Grid({ weaponSplId }: PerInkTankGridProps) { const { t } = useTranslation(["weapons", "analyzer"]); const [subsUsed, setSubsUsed] = React.useState(1); const [ismHovered, setIsmHovered] = React.useState(); const [issHovered, setIssHovered] = React.useState(); const values = React.useMemo( () => calculateGrid({ weaponSplId, subsUsed }), [weaponSplId, subsUsed], ); const handleHover = ({ ismAP, issAP }: { ismAP: number; issAP: number }) => { setIsmHovered(ismAP); setIssHovered(issAP); }; const handleMouseLeaveGrid = () => { setIsmHovered(undefined); setIssHovered(undefined); }; return (

{t("analyzer:perInkTankGrid.header", { weapon: t(`weapons:MAIN_${weaponSplId}`), count: subsUsed, })}

{[0, 1, 2].map((subsUsedOption) => { const id = String(subsUsedOption); return (
setSubsUsed(Number(e.target.value))} />
); })}
{/** biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration */}
{AP_VALUES_TO_SHOW.map((ap) => (
{ap}
))} {values.map((row, i) => [
{AP_VALUES_TO_SHOW[i]}
, ].concat( row.map((cell, j) => { const key = `${i}-${j}`; if (cell === "N/A") { return
; } const title = `${cell.shots ?? "-"} (ISM: ${cell.ismAP}, ISS: ${ cell.issAP })`; if (typeof cell.shots !== "number") { return ( // biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
handleHover({ ismAP: cell.ismAP, issAP: cell.issAP }) } > -
); } return ( // biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
handleHover({ ismAP: cell.ismAP, issAP: cell.issAP }) } > {cell.shots}
); }), ), )}
); } type ShotCellData = { shots: number | null; ismAP: number; issAP: number }; // LDE boosts both ISM and ISS by max 18 AP each, but you need 10 AP to wear it. const MAX_LDE_AP = 18 * 2; const AP_NEEDED_TO_WEAR_LDE = 10; const apsArePossible = (issAP: number, ismAP: number) => issAP + ismAP - MAX_LDE_AP + AP_NEEDED_TO_WEAR_LDE <= MAX_AP; function calculateGrid({ weaponSplId, subsUsed, }: { weaponSplId: MainWeaponId; subsUsed: number; }) { const result: ("N/A" | ShotCellData)[][] = []; for ( let issAPIndex = 0; issAPIndex < AP_VALUES_TO_SHOW.length; issAPIndex++ ) { const issAP = AP_VALUES_TO_SHOW[issAPIndex]; const row: ("N/A" | ShotCellData)[] = []; for ( let ismAPIndex = 0; ismAPIndex < AP_VALUES_TO_SHOW.length; ismAPIndex++ ) { const ismAP = AP_VALUES_TO_SHOW[ismAPIndex]; if (!apsArePossible(issAP, ismAP)) { row.push("N/A" as const); continue; } const option = inkTankOptionsWhenNSubsUsed({ ismAP, issAP, subsUsed, weaponSplId, }); row.push({ shots: option?.value ? Math.floor(option.value) : null, issAP, ismAP, }); } result.push(row); } const withColors = addGridColors(result); return withColors; } // this is a performance optimization over simply calling "buildStats" // as it would be doing a lot of unnecessary work function inkTankOptionsWhenNSubsUsed({ issAP, ismAP, subsUsed, weaponSplId, }: { issAP: number; ismAP: number; subsUsed: number; weaponSplId: MainWeaponId; }) { const mainWeaponParams = weaponParams().mainWeapons[weaponSplId]; const subWeaponParams = weaponParams().subWeapons[mainWeaponParams.subWeaponId]; const specialWeaponParams = weaponParams().specialWeapons[mainWeaponParams.specialWeaponId]; const options = fullInkTankOptions({ abilityPoints: new Map([ ["ISS", issAP], ["ISM", ismAP], ]), weaponSplId, hasTacticooler: false, mainOnlyAbilities: [], mainWeaponParams, specialWeaponParams, subWeaponParams, }); return optionsToPreferred(options).find((o) => o.subsUsed === subsUsed); } const PREFERRED_KEYS: FullInkTankOption["type"][] = ["FULL_CHARGE"]; function optionsToPreferred(options: ReturnType) { if (!options.some((option) => PREFERRED_KEYS.includes(option.type))) { return options; } return options.filter((option) => PREFERRED_KEYS.includes(option.type)); } function addGridColors(grid: ("N/A" | ShotCellData)[][]) { const maxValue = grid .flat() .filter( (v): v is { shots: number; ismAP: number; issAP: number } => v !== "N/A" && v.shots !== null, ) .reduce((max, v) => Math.max(max, v.shots), 0); const minValue = grid .flat() .filter( (v): v is { shots: number; ismAP: number; issAP: number } => v !== "N/A" && v.shots !== null, ) .reduce((min, v) => Math.min(min, v.shots), Number.POSITIVE_INFINITY); const result = grid.map((row) => row.map((cell) => { if (cell === "N/A") return cell; if (cell.shots === null) return { ...cell, hex: undefined }; const { shots } = cell; const hex = generateHexCode({ minValue, maxValue, value: shots, }); return { ...cell, hex, }; }), ); return result; } function generateHexCode({ minValue, maxValue, value, }: { minValue: number; maxValue: number; value: number; }) { const clampedValue = Math.max(minValue, Math.min(value, maxValue)); const normalizedPosition = (clampedValue - minValue) / (maxValue - minValue); // Adjust saturation and brightness to lighten the colors const saturationFactor = 0.5; // Adjust this to control the saturation const brightnessFactor = 0.8; // Adjust this to control the brightness const red = Math.floor(255 * (1 - normalizedPosition) * saturationFactor) + Math.floor(255 * (1 - brightnessFactor)); const green = Math.floor(255 * normalizedPosition * saturationFactor) + Math.floor(255 * (1 - brightnessFactor)); const blue = Math.floor(255 * (1 - brightnessFactor)); const hex = ((1 << 24) | (red << 16) | (green << 8) | blue) .toString(16) .slice(1) .toUpperCase(); return `#${hex}`; }