From d93b202ee8e4f832ba18f7b2f5a6cd69e1deb807 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Thu, 8 Oct 2026 18:36:17 +1300 Subject: [PATCH 01/17] Feature: Switch all shells in Compare --- packages/i18n/strings/en.json | 3 +- .../website/src/components/Compare/Body.tsx | 8 +++- .../src/components/Compare/Controls.tsx | 37 ++++++++++++++++++- .../website/src/stores/compareEphemeral.ts | 10 +++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 04125f138..0377c9e62 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -742,7 +742,8 @@ "none": "No deltas", "percentage": "Percentage", "absolute": "Absolute" - } + }, + "sync_shells": "Sync shells" }, "no_tanks": { diff --git a/packages/website/src/components/Compare/Body.tsx b/packages/website/src/components/Compare/Body.tsx index ee88cd4e4..e9648106e 100644 --- a/packages/website/src/components/Compare/Body.tsx +++ b/packages/website/src/components/Compare/Body.tsx @@ -2,7 +2,7 @@ import { alias } from "@blitzkit/core"; import { Flex, Heading, IconButton, Table } from "@radix-ui/themes"; import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics"; import { useLocale } from "../../hooks/useLocale"; -import { CompareEphemeral } from "../../stores/compareEphemeral"; +import { CompareEphemeral, syncShellSlot } from "../../stores/compareEphemeral"; import { CompareCellDirection } from "../CompareCell"; import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell"; import { CompareRow } from "./CompareRow"; @@ -50,7 +50,11 @@ export function Body({ stats }: BodyProps) { }} onClick={() => { CompareEphemeral.mutate((draft) => { - draft.members[index].shell = thisShell; + if (draft.syncShells) { + syncShellSlot(draft.members, shellIndex); + } else { + draft.members[index].shell = thisShell; + } }); }} > diff --git a/packages/website/src/components/Compare/Controls.tsx b/packages/website/src/components/Compare/Controls.tsx index 1bb45c6cb..25d71d7f0 100644 --- a/packages/website/src/components/Compare/Controls.tsx +++ b/packages/website/src/components/Compare/Controls.tsx @@ -1,9 +1,16 @@ import { PlusIcon, TrashIcon } from "@radix-ui/react-icons"; -import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes"; +import { + Button, + Checkbox, + Dialog, + Flex, + SegmentedControl, + Text, +} from "@radix-ui/themes"; import { api } from "../../core/blitzkit/api"; import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember"; import { useLocale } from "../../hooks/useLocale"; -import { CompareEphemeral } from "../../stores/compareEphemeral"; +import { CompareEphemeral, syncShellSlot } from "../../stores/compareEphemeral"; import { ComparePersistent, type DeltaMode, @@ -22,6 +29,7 @@ export function Controls({ onAddTankDialogOpenChange, }: ControlsProps) { const deltaMode = ComparePersistent.use((state) => state.deltaMode); + const syncShells = CompareEphemeral.use((state) => state.syncShells); const { strings } = useLocale(); return ( @@ -111,6 +119,31 @@ export function Controls({ {strings.website.tools.compare.actions.deltas.absolute} + + { + CompareEphemeral.mutate((draft) => { + draft.syncShells = !draft.syncShells; + + if (!draft.syncShells || draft.members.length === 0) return; + + const [first] = draft.members; + + syncShellSlot( + draft.members, + first.gun.shells.findIndex(({ id }) => id === first.shell.id), + ); + }); + }} + > + + + {strings.website.tools.compare.actions.sync_shells} + + ); } diff --git a/packages/website/src/stores/compareEphemeral.ts b/packages/website/src/stores/compareEphemeral.ts index 2c5a2a6e3..80fcdb6e0 100644 --- a/packages/website/src/stores/compareEphemeral.ts +++ b/packages/website/src/stores/compareEphemeral.ts @@ -8,6 +8,7 @@ export interface CompareMember extends DuelMember { export interface CompareEphemeral { crewSkills: Record; equalize: boolean; + syncShells: boolean; members: CompareMember[]; sorting?: { direction: "ascending" | "descending"; @@ -21,5 +22,14 @@ export const CompareEphemeral = new Varuna< >((crewSkills) => ({ crewSkills, equalize: false, + syncShells: false, members: [], })); + +export function syncShellSlot(members: CompareMember[], slot: number) { + for (const member of members) { + const shell = member.gun.shells[slot]; + + if (shell) member.shell = shell; + } +} From 2288c089e83e92b32be8a4b349012131c46af350 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Sat, 10 Oct 2026 11:16:36 +1300 Subject: [PATCH 02/17] Fix: dialogs clipping under the navbar and uneven Compare spacing --- packages/website/src/components/Compare/Controls.tsx | 5 +++-- packages/website/src/components/Compare/TankControl.tsx | 3 ++- .../Tankopedia/HeroSection/components/Options/index.tsx | 3 ++- packages/website/src/constants/dialog.ts | 3 +++ .../pages/[...chunk_default]/[...locale]/mixer/_index.tsx | 3 ++- 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 packages/website/src/constants/dialog.ts diff --git a/packages/website/src/components/Compare/Controls.tsx b/packages/website/src/components/Compare/Controls.tsx index 1bb45c6cb..571674518 100644 --- a/packages/website/src/components/Compare/Controls.tsx +++ b/packages/website/src/components/Compare/Controls.tsx @@ -1,5 +1,6 @@ import { PlusIcon, TrashIcon } from "@radix-ui/react-icons"; import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes"; +import { DIALOG_MAX_HEIGHT } from "../../constants/dialog"; import { api } from "../../core/blitzkit/api"; import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember"; import { useLocale } from "../../hooks/useLocale"; @@ -36,11 +37,11 @@ export function Controls({ - + {strings.website.tools.compare.actions.add.title} - + {strings.website.tools.compare.actions.add.description} diff --git a/packages/website/src/components/Compare/TankControl.tsx b/packages/website/src/components/Compare/TankControl.tsx index 28ab0e129..dcb6cccc3 100644 --- a/packages/website/src/components/Compare/TankControl.tsx +++ b/packages/website/src/components/Compare/TankControl.tsx @@ -1,6 +1,7 @@ import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons"; import { Dialog, Flex, IconButton } from "@radix-ui/themes"; import { useState } from "react"; +import { DIALOG_MAX_HEIGHT } from "../../constants/dialog"; import { api } from "../../core/blitzkit/api"; import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember"; import { useLocale } from "../../hooks/useLocale"; @@ -43,7 +44,7 @@ export function TankControl({ index, slug }: TankControlProps) { - + {strings.website.common.tank_search.swap_dialog_title} diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx index 410a7b8c5..16d115a86 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx @@ -27,6 +27,7 @@ import { Tooltip, } from "@radix-ui/themes"; import { Suspense, useState, type RefObject } from "react"; +import { DIALOG_MAX_HEIGHT } from "../../../../../constants/dialog"; import { Pose, poseEvent } from "../../../../../core/blitzkit/pose"; import { useEquipment } from "../../../../../hooks/useEquipment"; import { useFullScreen } from "../../../../../hooks/useFullScreen"; @@ -485,7 +486,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) { - + {strings.website.common.tank_search.select_dialog_title} diff --git a/packages/website/src/constants/dialog.ts b/packages/website/src/constants/dialog.ts new file mode 100644 index 000000000..922f0df2f --- /dev/null +++ b/packages/website/src/constants/dialog.ts @@ -0,0 +1,3 @@ +import { NAVBAR_HEIGHT } from "./navbar"; + +export const DIALOG_MAX_HEIGHT = `calc(100svh - 2 * (${NAVBAR_HEIGHT}px + var(--space-4)))`; diff --git a/packages/website/src/pages/[...chunk_default]/[...locale]/mixer/_index.tsx b/packages/website/src/pages/[...chunk_default]/[...locale]/mixer/_index.tsx index 7cf3539c4..9d6a52414 100644 --- a/packages/website/src/pages/[...chunk_default]/[...locale]/mixer/_index.tsx +++ b/packages/website/src/pages/[...chunk_default]/[...locale]/mixer/_index.tsx @@ -24,6 +24,7 @@ import { ModuleButton } from "../../../../components/ModuleButtons/ModuleButton" import { PageWrapper } from "../../../../components/PageWrapper"; import { ScreenshotButton } from "../../../../components/ScreenshotButton"; import { TankSearch } from "../../../../components/TankSearch"; +import { DIALOG_MAX_HEIGHT } from "../../../../constants/dialog"; import { api } from "../../../../core/blitzkit/api"; import { curateMixer } from "../../../../core/blitzkit/curateMixer"; import { @@ -117,7 +118,7 @@ function ModuleChooser({ - + {strings.website.common.tank_search.select_dialog_title} From 6e30da497814e490a101fb62e61e0415d27be6eb Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:56:42 -0500 Subject: [PATCH 03/17] add some padding to distinguish navbar end with the start of the dialog --- packages/website/src/constants/dialog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/constants/dialog.ts b/packages/website/src/constants/dialog.ts index 922f0df2f..2c6b9b86e 100644 --- a/packages/website/src/constants/dialog.ts +++ b/packages/website/src/constants/dialog.ts @@ -1,3 +1,3 @@ import { NAVBAR_HEIGHT } from "./navbar"; -export const DIALOG_MAX_HEIGHT = `calc(100svh - 2 * (${NAVBAR_HEIGHT}px + var(--space-4)))`; +export const DIALOG_MAX_HEIGHT = `calc(100svh - 2 * (${NAVBAR_HEIGHT}px + var(--space-4) + var(--space-2)))`; From a0c54b50fcac10b954d22ebbbc64429b0f29a3f6 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Sat, 10 Oct 2026 11:45:07 +1300 Subject: [PATCH 04/17] Feature: tank distribution scale on Tankopedia stats --- .../components/InfoWithDelta.tsx | 34 ++++++--- .../Characteristics/components/StatScale.tsx | 71 +++++++++++++++++++ 2 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx index 7d910738d..352317ddf 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx @@ -1,5 +1,5 @@ import { createDefaultSkills } from "@blitzkit/core"; -import { Flex, Progress, Text } from "@radix-ui/themes"; +import { Box, Flex, Progress, Text } from "@radix-ui/themes"; import { clamp } from "lodash-es"; import { memo, useMemo, type ComponentProps, type ReactNode } from "react"; import { api } from "../../../../../../core/blitzkit/api"; @@ -16,6 +16,7 @@ import { TankopediaRelativeAgainst, } from "../../../../../../stores/tankopedia"; import { Info, type InfoProps } from "./Info"; +import { StatScale } from "./StatScale"; type InfoWithDeltaProps = Omit & { stats: TankCharacteristics; @@ -122,6 +123,15 @@ export const InfoWithDelta = memo( return othersValue !== undefined; }) as TankCharacteristics[]; }, [relativeAgainst, shellIndex, equipmentMatrix, equalize]); + const values = useMemo( + () => + others.map((tank) => + typeof props.value === "function" + ? props.value(tank)! + : (tank[props.value] as number), + ), + [others], + ); const betterTanks = others.filter((tank) => { const othersValue = typeof props.value === "function" @@ -161,13 +171,21 @@ export const InfoWithDelta = memo( {!noRanking && ( - + + + + + {clamp(betterTanks.length + 1, 1, others.length)} /{" "} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx new file mode 100644 index 000000000..32f867b01 --- /dev/null +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx @@ -0,0 +1,71 @@ +import { memo, useMemo } from "react"; + +const RESOLUTION = 1000; +const HEIGHT = 9; +const DENSE_TICKS = 100; + +interface StatScaleProps { + values: number[]; + value: number; + lowerIsBetter?: boolean; +} + +export const StatScale = memo( + ({ values, value, lowerIsBetter }) => { + const ticks = useMemo(() => { + const min = Math.min(value, ...values); + const max = Math.max(value, ...values); + const range = max - min; + const counts = new Map(); + + for (const other of values) { + const raw = range === 0 ? 0.5 : (other - min) / range; + const x = Math.round((lowerIsBetter ? 1 - raw : raw) * RESOLUTION); + + if (x === 0 || x === RESOLUTION) continue; + + counts.set(x, (counts.get(x) ?? 0) + 1); + } + + return Array.from(counts, ([x, count]) => ({ + x, + color: + count === 1 + ? "var(--gray-a5)" + : count <= 3 + ? "var(--gray-a6)" + : "var(--gray-a7)", + })); + }, [values, value, lowerIsBetter]); + + return ( +
+ {ticks.map(({ x, color }) => ( +
+ ))} +
+ ); + }, +); From b65229dc80f1928faa7050c1f218f61dd43ddd59 Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:03:37 -0600 Subject: [PATCH 05/17] remove granuality --- .../components/Characteristics/components/StatScale.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx index 32f867b01..d124a2c81 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx @@ -1,6 +1,5 @@ import { memo, useMemo } from "react"; -const RESOLUTION = 1000; const HEIGHT = 9; const DENSE_TICKS = 100; @@ -20,9 +19,9 @@ export const StatScale = memo( for (const other of values) { const raw = range === 0 ? 0.5 : (other - min) / range; - const x = Math.round((lowerIsBetter ? 1 - raw : raw) * RESOLUTION); + const x = lowerIsBetter ? 1 - raw : raw; - if (x === 0 || x === RESOLUTION) continue; + if (x === 0 || x === 1) continue; counts.set(x, (counts.get(x) ?? 0) + 1); } @@ -58,7 +57,7 @@ export const StatScale = memo( style={{ position: "absolute", top: 0, - left: `${(x / RESOLUTION) * 100}%`, + left: `${x * 100}%`, width: 1, height: "100%", backgroundColor: color, From ff507ed4e61ada022fcbe1e576823568a609cb09 Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:05:23 -0600 Subject: [PATCH 06/17] use canvas --- .../Characteristics/components/StatScale.tsx | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx index d124a2c81..0be1f2904 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo } from "react"; +import { memo, useEffect, useMemo, useRef } from "react"; const HEIGHT = 9; const DENSE_TICKS = 100; @@ -11,6 +11,7 @@ interface StatScaleProps { export const StatScale = memo( ({ values, value, lowerIsBetter }) => { + const canvas = useRef(null); const ticks = useMemo(() => { const min = Math.min(value, ...values); const max = Math.max(value, ...values); @@ -29,16 +30,42 @@ export const StatScale = memo( return Array.from(counts, ([x, count]) => ({ x, color: - count === 1 - ? "var(--gray-a5)" - : count <= 3 - ? "var(--gray-a6)" - : "var(--gray-a7)", + count === 1 ? "--gray-a5" : count <= 3 ? "--gray-a6" : "--gray-a7", })); }, [values, value, lowerIsBetter]); + useEffect(() => { + const element = canvas.current; + if (!element) return; + + const draw = () => { + const { width } = element.getBoundingClientRect(); + const pixelRatio = window.devicePixelRatio || 1; + const context = element.getContext("2d"); + if (!context) return; + + element.width = Math.round(width * pixelRatio); + element.height = Math.round(HEIGHT * pixelRatio); + context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + + const styles = getComputedStyle(element); + for (const { x, color } of ticks) { + context.fillStyle = styles.getPropertyValue(color); + context.fillRect(x * width, 0, 1, HEIGHT); + } + }; + + const observer = new ResizeObserver(draw); + observer.observe(element); + draw(); + + return () => observer.disconnect(); + }, [ticks]); + return ( -