From d28c56fe1c51a2a5faf4aff7635c54c2633eec75 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Sun, 28 Dec 2025 09:58:32 +1300 Subject: [PATCH 01/55] Add Randomise function to module selection --- .../components/Modules.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx index 55703da90..92cbacacd 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx @@ -392,6 +392,42 @@ export function Modules() { > {strings.website.tools.tankopedia.configuration.modules.upgrade} + )} From d1001df3cf0599f2be13952102a71c6f0be01c47 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Sun, 28 Dec 2025 10:06:53 +1300 Subject: [PATCH 02/55] Add randomise function to modules --- .../Tankopedia/CharacteristicsSection/components/Modules.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx index 92cbacacd..6b1da4aae 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx @@ -464,4 +464,4 @@ export function Modules() { ); -} +} \ No newline at end of file From 9e225d2093c532f53945131efd5d478083d1c356 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Sun, 28 Dec 2025 12:12:56 +1300 Subject: [PATCH 03/55] Fixed Provison Button Sizes --- .../components/ModuleButtons/GenericTankComponentButton.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx b/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx index 2ca36cd35..1d9409963 100644 --- a/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx +++ b/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx @@ -34,15 +34,14 @@ export function GenericTankComponentButton({ height: 32, position: 'absolute', opacity: props.disabled ? 0.5 : 1, - transform: 'translate(-50%, -50%) scale(0.75)', + transform: 'translate(-50%, -50%) scale(1)', top: '50%', left: '50%', ...iconStyles, }} /> - {children} ); -} +} \ No newline at end of file From 45c7d741600eb699f5c0139ec90ef98e1f3e1517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 27 Dec 2025 18:45:09 -0600 Subject: [PATCH 04/55] revert: icon scale --- .../GenericTankComponentButton.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx b/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx index 1d9409963..65d32a2ef 100644 --- a/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx +++ b/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx @@ -1,12 +1,12 @@ -import type { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from "react"; import { TankComponentButton, type TankComponentButtonProps, -} from './TankComponentButton'; +} from "./TankComponentButton"; interface GenericTankComponentButtonProps extends TankComponentButtonProps { icon: string; - iconStyles?: HTMLAttributes['style']; + iconStyles?: HTMLAttributes["style"]; } export function GenericTankComponentButton({ @@ -21,7 +21,7 @@ export function GenericTankComponentButton({ style={{ width: 48, height: 40, - position: 'relative', + position: "relative", }} > @@ -44,4 +44,4 @@ export function GenericTankComponentButton({ {children} ); -} \ No newline at end of file +} From 5e4c5fc23043ef1791a7d2d730d7ff5af2b91e43 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Tue, 30 Dec 2025 15:43:16 +1300 Subject: [PATCH 05/55] Fixed Provisions not affecting terrain resistances and added commented-out helper function to log provisions later if needed. --- .../CharacteristicsSection/components/Provisions.tsx | 9 +++++++++ .../website/src/core/blitzkit/tankCharacteristics.ts | 11 +++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Provisions.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Provisions.tsx index 258b23141..4590a2edc 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Provisions.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Provisions.tsx @@ -14,6 +14,15 @@ export function Provisions() { const provisionsList = availableProvisions(tank, gun, provisionDefinitions); const { strings } = useLocale(); + // FOR PROVISION LOGGING PURPOSES + // + // console.log('ALL PROVISIONS:', Object.entries(provisionDefinitions.provisions).map(([id, p]) => ({ + // id: Number(id), + // name: p.name, + // crew: p.crew, + // hasCrewBonus: !!p.crew + // }))); + return ( diff --git a/packages/website/src/core/blitzkit/tankCharacteristics.ts b/packages/website/src/core/blitzkit/tankCharacteristics.ts index 86fb8ca5b..396f0d82e 100644 --- a/packages/website/src/core/blitzkit/tankCharacteristics.ts +++ b/packages/website/src/core/blitzkit/tankCharacteristics.ts @@ -236,12 +236,11 @@ export function tankCharacteristics( [hasImprovedEnginePowerBoost, 0.01], [true, crewSkills.virtuoso / 100], ) * coefficient([true, progressiveStat(driverMastery)]); - const resistanceHardCoefficient = coefficient([hasImprovedSuspension, -0.2]); - const resistanceMediumCoefficient = coefficient([ - hasImprovedSuspension, - -0.15, - ]); - const resistanceSoftCoefficient = coefficient([hasImprovedSuspension, -0.3]); + + const resistanceHardCoefficient = coefficient([hasImprovedSuspension, -0.2]) * coefficient([true, degressiveStat(driverMastery)]); + const resistanceMediumCoefficient = coefficient([hasImprovedSuspension, -0.15]) * coefficient([true, degressiveStat(driverMastery)]); + const resistanceSoftCoefficient = coefficient([hasImprovedSuspension, -0.3]) * coefficient([true, degressiveStat(driverMastery)]); + const viewRangeCoefficient = coefficient([ hasImprovedOptics, From 0bd660f255aabcfa232f420643e1b51a5b174135 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Tue, 30 Dec 2025 15:47:22 +1300 Subject: [PATCH 06/55] Revert changes --- .../GenericTankComponentButton.tsx | 20 +++++++++---------- .../components/Modules.tsx | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx b/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx index 65d32a2ef..1d9409963 100644 --- a/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx +++ b/packages/website/src/components/ModuleButtons/GenericTankComponentButton.tsx @@ -1,12 +1,12 @@ -import type { HTMLAttributes } from "react"; +import type { HTMLAttributes } from 'react'; import { TankComponentButton, type TankComponentButtonProps, -} from "./TankComponentButton"; +} from './TankComponentButton'; interface GenericTankComponentButtonProps extends TankComponentButtonProps { icon: string; - iconStyles?: HTMLAttributes["style"]; + iconStyles?: HTMLAttributes['style']; } export function GenericTankComponentButton({ @@ -21,7 +21,7 @@ export function GenericTankComponentButton({ style={{ width: 48, height: 40, - position: "relative", + position: 'relative', }} > @@ -44,4 +44,4 @@ export function GenericTankComponentButton({ {children} ); -} +} \ No newline at end of file diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx index 6b1da4aae..1170e5e16 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx @@ -394,7 +394,7 @@ export function Modules() { - )} From d2fa93ee714058af094b22184498336fceb04d05 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Tue, 30 Dec 2025 15:51:57 +1300 Subject: [PATCH 08/55] Revert changes --- .../components/Modules.tsx | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx index c7fc9837f..63a4275fb 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx @@ -176,17 +176,11 @@ export function Modules() { (turret) => turret.id === unlock.id )!; - // Check if current gun exists in new turret - const gunInNewTurret = draft.protagonist.turret.guns.find( - (gun) => gun.id === draft.protagonist.gun.id - ); - - if (gunInNewTurret) { - // Gun exists - get it FROM this turret (this fixes the bug!) - draft.protagonist.gun = gunInNewTurret; - draft.protagonist.shell = gunInNewTurret.shells[0]; - } else { - // Gun doesn't exist - select top gun from new turret + if ( + !draft.protagonist.turret.guns.some( + (gun) => gun.id === draft.protagonist.gun.id + ) + ) { draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!; draft.protagonist.shell = draft.protagonist.gun.shells[0]; } @@ -428,4 +422,4 @@ export function Modules() { ); -} \ No newline at end of file +} From 53b0f045124fa19e2f07cb195523e81a503d3b2b Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Tue, 30 Dec 2025 15:53:25 +1300 Subject: [PATCH 09/55] Revert changes --- .../components/Modules.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx index 63a4275fb..219412b84 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx @@ -176,11 +176,17 @@ export function Modules() { (turret) => turret.id === unlock.id )!; - if ( - !draft.protagonist.turret.guns.some( - (gun) => gun.id === draft.protagonist.gun.id - ) - ) { + // Check if current gun exists in new turret + const gunInNewTurret = draft.protagonist.turret.guns.find( + (gun) => gun.id === draft.protagonist.gun.id + ); + + if (gunInNewTurret) { + // Gun exists - get it FROM this turret + draft.protagonist.gun = gunInNewTurret; + draft.protagonist.shell = gunInNewTurret.shells[0]; + } else { + // Gun doesn't exist - select top gun from new turret draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!; draft.protagonist.shell = draft.protagonist.gun.shells[0]; } @@ -422,4 +428,4 @@ export function Modules() { ); -} +} \ No newline at end of file From 39f1389d77e81245a31fcafad946049a54aae717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 29 Dec 2025 21:27:41 -0600 Subject: [PATCH 10/55] Remove unused provision logging comments Removed commented-out logging code for provisions. --- .../CharacteristicsSection/components/Provisions.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Provisions.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Provisions.tsx index 4590a2edc..258b23141 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Provisions.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Provisions.tsx @@ -14,15 +14,6 @@ export function Provisions() { const provisionsList = availableProvisions(tank, gun, provisionDefinitions); const { strings } = useLocale(); - // FOR PROVISION LOGGING PURPOSES - // - // console.log('ALL PROVISIONS:', Object.entries(provisionDefinitions.provisions).map(([id, p]) => ({ - // id: Number(id), - // name: p.name, - // crew: p.crew, - // hasCrewBonus: !!p.crew - // }))); - return ( From b6204b1c2c3c4a53583e3c762c2a32e495f05b7a Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Tue, 30 Dec 2025 20:45:44 +1300 Subject: [PATCH 11/55] Added ability to filter tanks by available provisions --- .../TankSearch/components/FilterControl.tsx | 169 +++++++++++++++++- .../website/src/core/blitzkit/filterTank.ts | 17 +- packages/website/src/stores/tankFilters.ts | 2 + 3 files changed, 186 insertions(+), 2 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 68ae1e60f..678ef60d0 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -18,8 +18,10 @@ import { type ButtonProps, type FlexProps, } from "@radix-ui/themes"; -import { isEqual, times } from "lodash-es"; +import { isEqual, times, chunk } from "lodash-es"; +import React from "react"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; +import { awaitableConsumableDefinitions } from "../../../core/awaitables/consumableDefinitions"; import { Var } from "../../../core/radix/var"; import { useLocale } from "../../../hooks/useLocale"; import { App } from "../../../stores/app"; @@ -35,6 +37,7 @@ import { ScienceIcon } from "../../ScienceIcon"; import { ScienceOffIcon } from "../../ScienceOffIcon"; const gameDefinitions = await awaitableGameDefinitions; +const consumableDefinitions = await awaitableConsumableDefinitions; const shellTypeIcons: Record = { [ShellType.AP]: "ap", @@ -43,6 +46,25 @@ const shellTypeIcons: Record = { [ShellType.HEAT]: "hc", }; +// Get consumables +const regularConsumables = Object.values( + consumableDefinitions.consumables +).filter( + (consumable) => + !consumable.game_mode_exclusive && consumable.name.locales.en !== "" +); + +// Split consumables into chunks of 5 for columns +const consumableColumns = chunk(regularConsumables, 5); + +// Get game mode exclusive abilities +const gameModeAbilities = Object.values( + consumableDefinitions.consumables +).filter((consumable) => consumable.game_mode_exclusive); + +// Split abilities into chunks of 5 for columns +const abilityColumns = chunk(gameModeAbilities, 5); + const size: ButtonProps["size"] = { initial: "1", xs: "2" }; function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { @@ -337,6 +359,151 @@ export function FilterControl() { + {/* Consumables Filter - RIGHT AFTER NATIONS */} + + + {consumableColumns[0]?.map((consumable) => { + const selected = tankFilters.consumables?.includes(consumable.id); + + return ( + { + TankFilters.mutate((draft) => { + if (tankFilters.consumables.includes(consumable.id)) { + draft.consumables = draft.consumables.filter( + (c) => c !== consumable.id + ); + } else { + draft.consumables = [ + ...draft.consumables, + consumable.id, + ]; + } + }); + }} + > + + + ); + })} + + + {consumableColumns[1] && ( + <> + + + + {consumableColumns[1].map((consumable) => { + const selected = tankFilters.consumables?.includes( + consumable.id + ); + + return ( + { + TankFilters.mutate((draft) => { + if (tankFilters.consumables.includes(consumable.id)) { + draft.consumables = draft.consumables.filter( + (c) => c !== consumable.id + ); + } else { + draft.consumables = [ + ...draft.consumables, + consumable.id, + ]; + } + }); + }} + > + + + ); + })} + + + )} + + {consumableColumns[2] && ( + <> + + + + {consumableColumns[2].map((consumable) => { + const selected = tankFilters.consumables?.includes( + consumable.id + ); + + return ( + { + TankFilters.mutate((draft) => { + if (tankFilters.consumables.includes(consumable.id)) { + draft.consumables = draft.consumables.filter( + (c) => c !== consumable.id + ); + } else { + draft.consumables = [ + ...draft.consumables, + consumable.id, + ]; + } + }); + }} + > + + + ); + })} + + + )} + + index); +const consumableDefinitions = await awaitableConsumableDefinitions; export async function filterTank( filters: TankFilters, @@ -40,6 +43,18 @@ export async function filterTank( gun.shells[index].type === filters.shells[index] ) ) - ) + ) && + (filters.consumables.length === 0 || + filters.consumables.every((consumableId) => + tank.turrets.some((turret) => + turret.guns.some((gun) => + checkConsumableProvisionInclusivity( + consumableDefinitions.consumables[consumableId], + tank, + gun + ) + ) + ) + )) ); } diff --git a/packages/website/src/stores/tankFilters.ts b/packages/website/src/stores/tankFilters.ts index 68999b562..245455edf 100644 --- a/packages/website/src/stores/tankFilters.ts +++ b/packages/website/src/stores/tankFilters.ts @@ -21,6 +21,7 @@ export interface TankFilters { searching: boolean; gunType: CaseType[]; shells: [ShellType | null, ShellType | null, ShellType | null]; + consumables: number[]; showTesting: boolean; showNonTesting: boolean; @@ -37,6 +38,7 @@ export const TankFilters = new Varuna({ searching: false, gunType: [], shells: [null, null, null], + consumables: [], // Added this line showTesting: true, showNonTesting: true, From 9eb6acc6244fc92747f88281cd78cead724669d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:49:06 -0600 Subject: [PATCH 12/55] Fix formatting of consumables array in tankFilters --- packages/website/src/stores/tankFilters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/stores/tankFilters.ts b/packages/website/src/stores/tankFilters.ts index 245455edf..d758d29a5 100644 --- a/packages/website/src/stores/tankFilters.ts +++ b/packages/website/src/stores/tankFilters.ts @@ -38,7 +38,7 @@ export const TankFilters = new Varuna({ searching: false, gunType: [], shells: [null, null, null], - consumables: [], // Added this line + consumables: [], showTesting: true, showNonTesting: true, From dfa8be63b41bfc488530d42772150e33694cf84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:51:10 -0600 Subject: [PATCH 13/55] Pet peeve --- packages/website/src/core/blitzkit/filterTank.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/website/src/core/blitzkit/filterTank.ts b/packages/website/src/core/blitzkit/filterTank.ts index 6f2c6df5e..46d744c60 100644 --- a/packages/website/src/core/blitzkit/filterTank.ts +++ b/packages/website/src/core/blitzkit/filterTank.ts @@ -5,6 +5,7 @@ import { awaitableConsumableDefinitions } from "../../core/awaitables/consumable import type { TankFilters } from "../../stores/tankFilters"; const SHELLS = times(3, (index) => index); + const consumableDefinitions = await awaitableConsumableDefinitions; export async function filterTank( From b6d6fa7212c0b647f592e34bf8ce8b4ac7ce9c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:52:11 -0600 Subject: [PATCH 14/55] Remove unused React import from FilterControl.tsx --- .../src/components/TankSearch/components/FilterControl.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 678ef60d0..5739a3ee6 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -19,7 +19,6 @@ import { type FlexProps, } from "@radix-ui/themes"; import { isEqual, times, chunk } from "lodash-es"; -import React from "react"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { awaitableConsumableDefinitions } from "../../../core/awaitables/consumableDefinitions"; import { Var } from "../../../core/radix/var"; From faf802cf3de619b292fd40a97ed6f86cbc487573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:02:05 -0600 Subject: [PATCH 15/55] revert: Modules.tsx --- .../Tankopedia/CharacteristicsSection/components/Modules.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx index 219412b84..55703da90 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx @@ -428,4 +428,4 @@ export function Modules() { ); -} \ No newline at end of file +} From 540e56a12df4622f107555c4de7dd537d5da51b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:03:00 -0600 Subject: [PATCH 16/55] feat: format all --- .../src/core/blitzkit/tankCharacteristics.ts | 190 +++++++++--------- 1 file changed, 98 insertions(+), 92 deletions(-) diff --git a/packages/website/src/core/blitzkit/tankCharacteristics.ts b/packages/website/src/core/blitzkit/tankCharacteristics.ts index 396f0d82e..dcab1976c 100644 --- a/packages/website/src/core/blitzkit/tankCharacteristics.ts +++ b/packages/website/src/core/blitzkit/tankCharacteristics.ts @@ -19,9 +19,9 @@ import { type TankDefinition, type TrackDefinition, type TurretDefinition, -} from '@blitzkit/core'; -import { coefficient } from '@blitzkit/core/src/blitzkit/coefficient'; -import type { EquipmentMatrix } from '../../stores/duel'; +} from "@blitzkit/core"; +import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient"; +import type { EquipmentMatrix } from "../../stores/duel"; export type TankCharacteristics = ReturnType; export type TankCharacteristicsKey = keyof TankCharacteristics; @@ -76,7 +76,7 @@ export function tankCharacteristics( tankModelDefinition: ModelDefinition; equipmentDefinitions: EquipmentDefinitions; provisionDefinitions: ProvisionDefinitions; - }, + } ) { const preset = equipmentDefinitions.presets[tank.equipment_preset]; const turretModelDefinition = tankModelDefinition.turrets[turret.id]; @@ -86,7 +86,7 @@ export function tankCharacteristics( return preset.slots.some((slot, index) => { const choice = equipmentMatrix[Math.floor(index / 3)][index % 3]; if (choice === 0) return false; - const equipped = slot[choice === -1 ? 'left' : 'right']; + const equipped = slot[choice === -1 ? "left" : "right"]; return equipped === id; }); } @@ -138,7 +138,7 @@ export function tankCharacteristics( provisionDefinitions.provisions[provision].crew ? total + provisionDefinitions.provisions[provision].crew! / 100 : total, - 0, + 0 ) + (hasImprovedVentilation ? 0.08 : 0); const commanderMastery = 1 + provisionCrewBonus; const loaderMastery = @@ -158,19 +158,19 @@ export function tankCharacteristics( coefficient( [applyReactiveArmor && shell.type !== ShellType.HE, -0.27], [applyDynamicArmor, -0.1], - [applySpallLiner && shell.type === ShellType.HE, -0.2], + [applySpallLiner && shell.type === ShellType.HE, -0.2] ); const assaultDamageCoefficient = gun.assault_ranges && gun.assault_ranges.types.includes(shell.type) - ? (gun.assault_ranges.ranges.find( - ({ distance }) => distance >= assaultDistance, - )?.factor ?? 0) + ? gun.assault_ranges.ranges.find( + ({ distance }) => distance >= assaultDistance + )?.factor ?? 0 : 1; const moduleDamageCoefficient = coefficient([hasTungsten, 0.15]); const reloadCoefficient = (coefficient([hasGunRammer, -0.05]) * coefficient([true, degressiveStat(loaderMastery)])) / - coefficient([hasAdrenaline && gun.gun_type!.$case === 'regular', 0.17]); + coefficient([hasAdrenaline && gun.gun_type!.$case === "regular", 0.17]); const penetrationCoefficient = coefficient([ hasCalibratedShells, resolvePenetrationCoefficient(true, shell.type) - 1, @@ -178,16 +178,16 @@ export function tankCharacteristics( const healthCoefficient = coefficient( [hasSandbagArmor, 0.03], [hasEnhancedSandbagArmor, 0.06], - [hasImprovedAssembly, 0.04], + [hasImprovedAssembly, 0.04] ); const shellVelocityCoefficient = coefficient( [hasSupercharger, 0.35], - [hasImprovedGunPowder, 0.3], + [hasImprovedGunPowder, 0.3] ); const aimTimeCoefficient = coefficient( [hasEnhancedGunLayingDrive, -0.15], - [hasReticleCalibration, -0.3], + [hasReticleCalibration, -0.3] ) * coefficient([true, degressiveStat(gunnerMastery)]); const dispersionStillCoefficient = coefficient([hasRefinedGun, -0.15], [hasReticleCalibration, -0.3]) * @@ -198,19 +198,19 @@ export function tankCharacteristics( ]); const dispersionHullTraverseCoefficient = coefficient( [hasVerticalStabilizer, -0.15], - [true, -crewSkills.smooth_turn / 100], + [true, -crewSkills.smooth_turn / 100] ); const dispersionAfterShotCoefficient = coefficient( [hasVerticalStabilizer, -0.15], - [true, -crewSkills.soft_recoil / 100], + [true, -crewSkills.soft_recoil / 100] ); const dispersionMovingCoefficient = coefficient( [hasVerticalStabilizer, -0.15], - [true, -crewSkills.smooth_driving / 100], + [true, -crewSkills.smooth_driving / 100] ); const dispersionTurretTraverseCoefficient = coefficient( [hasVerticalStabilizer, -0.15], - [true, -crewSkills.smooth_turret * (2 / 100)], + [true, -crewSkills.smooth_turret * (2 / 100)] ); const enginePowerCoefficient = coefficient( [ @@ -225,7 +225,7 @@ export function tankCharacteristics( [hasStandardFuel, 0.03], [hasImprovedFuel, 0.1], [hasGearOil, 0.03], - [hasImprovedGearOil, 0.06], + [hasImprovedGearOil, 0.06] ); const turretTraverseCoefficient = coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) * @@ -234,35 +234,41 @@ export function tankCharacteristics( coefficient( [hasImprovedControl, 0.11], [hasImprovedEnginePowerBoost, 0.01], - [true, crewSkills.virtuoso / 100], + [true, crewSkills.virtuoso / 100] ) * coefficient([true, progressiveStat(driverMastery)]); - const resistanceHardCoefficient = coefficient([hasImprovedSuspension, -0.2]) * coefficient([true, degressiveStat(driverMastery)]); - const resistanceMediumCoefficient = coefficient([hasImprovedSuspension, -0.15]) * coefficient([true, degressiveStat(driverMastery)]); - const resistanceSoftCoefficient = coefficient([hasImprovedSuspension, -0.3]) * coefficient([true, degressiveStat(driverMastery)]); - + const resistanceHardCoefficient = + coefficient([hasImprovedSuspension, -0.2]) * + coefficient([true, degressiveStat(driverMastery)]); + const resistanceMediumCoefficient = + coefficient([hasImprovedSuspension, -0.15]) * + coefficient([true, degressiveStat(driverMastery)]); + const resistanceSoftCoefficient = + coefficient([hasImprovedSuspension, -0.3]) * + coefficient([true, degressiveStat(driverMastery)]); + const viewRangeCoefficient = coefficient([ hasImprovedOptics, tank.class === TankClass.HEAVY ? 0.06 : tank.class === TankClass.MEDIUM - ? 0.08 - : tank.class === TankClass.LIGHT - ? 0.11 - : 0.04, + ? 0.08 + : tank.class === TankClass.LIGHT + ? 0.11 + : 0.04, ]) * coefficient([true, progressiveStat(commanderMastery)]); const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]); const speedForwardsSum = sum( [hasImprovedEnginePowerBoost, 4], [hasGearOil, 2], - [hasImprovedGearOil, 4], + [hasImprovedGearOil, 4] ); const speedBackwardsSum = sum( [hasImprovedEnginePowerBoost, 8], [hasGearOil, 2], - [hasImprovedGearOil, 4], + [hasImprovedGearOil, 4] ); const camouflageCoefficientMoving = sum( [ @@ -272,8 +278,8 @@ export function tankCharacteristics( tank.class === TankClass.TANK_DESTROYER ? 0.04 : tank.class === TankClass.HEAVY - ? 0.03 - : 0.02, + ? 0.03 + : 0.02, ]), ], [ @@ -281,11 +287,11 @@ export function tankCharacteristics( tank.class === TankClass.HEAVY ? 0.02 : tank.class === TankClass.MEDIUM - ? 0.04 - : tank.class === TankClass.LIGHT - ? 0.07 - : 0.1, - ], + ? 0.04 + : tank.class === TankClass.LIGHT + ? 0.07 + : 0.1, + ] ); const camouflageCoefficientStill = sum( [ @@ -296,10 +302,10 @@ export function tankCharacteristics( tank.class === TankClass.TANK_DESTROYER ? 0.04 : tank.class === TankClass.HEAVY - ? 0.03 - : 0.02, + ? 0.03 + : 0.02, ], - [true, crewSkills.camouflage * (3 / 100)], + [true, crewSkills.camouflage * (3 / 100)] ), ], [ @@ -307,18 +313,18 @@ export function tankCharacteristics( tank.class === TankClass.HEAVY ? 0.04 : tank.class === TankClass.MEDIUM - ? 0.08 - : tank.class === TankClass.LIGHT - ? 0.14 - : 0.2, - ], + ? 0.08 + : tank.class === TankClass.LIGHT + ? 0.14 + : 0.2, + ] ); const size = normalizeBoundingBox( unionBoundingBox( tankModelDefinition.bounding_box, - turretModelDefinition.bounding_box, - ), + turretModelDefinition.bounding_box + ) ); const weightKg = tank.weight + engine.weight + track.weight + turret.weight + gun.weight; @@ -338,14 +344,14 @@ export function tankCharacteristics( shell, damageCoefficient, reloadCoefficient, - intraClipCoefficient, + intraClipCoefficient ); const intraClip = - gun.gun_type?.$case === 'regular' + gun.gun_type?.$case === "regular" ? undefined : gun.gun_type!.value.intra_clip * intraClipCoefficient; const mostOptimalShellIndex = - gun.gun_type!.$case === 'auto_reloader' + gun.gun_type!.$case === "auto_reloader" ? gun.gun_type!.value.shell_reloads.reduce gun.gun_type!.value.shell_reloads[1] + gun.gun_type!.value.intra_clip ? ((armorDamageCoefficient * shell.armor_damage) / @@ -388,23 +394,23 @@ export function tankCharacteristics( gun.gun_type!.value.shell_reloads.slice(1).length : undefined; const shells = - gun.gun_type!.$case === 'regular' ? 1 : gun.gun_type!.value.shell_count; + gun.gun_type!.$case === "regular" ? 1 : gun.gun_type!.value.shell_count; const shellReloads = - gun.gun_type!.$case === 'auto_reloader' + gun.gun_type!.$case === "auto_reloader" ? gun.gun_type!.value.shell_reloads.map( - (reload) => reload * reloadCoefficient, + (reload) => reload * reloadCoefficient ) : undefined; const shellReload = - gun.gun_type!.$case === 'auto_reloader' + gun.gun_type!.$case === "auto_reloader" ? undefined - : (gun.gun_type!.$case === 'regular' + : (gun.gun_type!.$case === "regular" ? gun.gun_type!.value.reload : gun.gun_type!.value.clip_reload) * reloadCoefficient; const caliber = shell.caliber; const penetration = shell.penetration.near * penetrationCoefficient; const clipDamage = - gun.gun_type!.$case === 'regular' + gun.gun_type!.$case === "regular" ? undefined : gun.gun_type!.value.shell_count * damage; const moduleDamage = shell.module_damage * moduleDamageCoefficient; @@ -598,45 +604,45 @@ export const characteristicsOrder: { items: TankCharacteristicsKey[]; }[] = [ { - name: 'Firepower', + name: "Firepower", items: [ - 'gunType', - 'dpm', - 'shells', - 'shellReload', - 'shellReloads', - 'intraClip', - 'penetration', - 'penetrationAt250m', - 'damage', - 'clipDamage', - 'moduleDamage', - 'caliber', - 'shellNormalization', - 'shellRicochet', - 'shellVelocity', - 'shellRange', - 'shellCapacity', - 'aimTime', - 'dispersion', - 'dispersionMoving', - 'dispersionHullTraversing', - 'dispersionTurretTraversing', - 'dispersionShooting', - 'dispersionGunDamaged', - 'gunDepression', - 'gunElevation', - 'gunFrontalDepression', - 'gunFrontalElevation', - 'gunRearDepression', - 'gunRearElevation', - 'azimuthLeft', - 'azimuthRight', + "gunType", + "dpm", + "shells", + "shellReload", + "shellReloads", + "intraClip", + "penetration", + "penetrationAt250m", + "damage", + "clipDamage", + "moduleDamage", + "caliber", + "shellNormalization", + "shellRicochet", + "shellVelocity", + "shellRange", + "shellCapacity", + "aimTime", + "dispersion", + "dispersionMoving", + "dispersionHullTraversing", + "dispersionTurretTraversing", + "dispersionShooting", + "dispersionGunDamaged", + "gunDepression", + "gunElevation", + "gunFrontalDepression", + "gunFrontalElevation", + "gunRearDepression", + "gunRearElevation", + "azimuthLeft", + "azimuthRight", ], }, { - name: 'Crew', - items: ['crewCount'], + name: "Crew", + items: ["crewCount"], }, ]; From 06ce263668babf633a536676840eaf768cbdc5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:53:06 -0600 Subject: [PATCH 17/55] feat: compact nations selector --- packages/i18n/strings/en.json | 2 + .../TankSearch/components/FilterControl.tsx | 719 +++--------------- 2 files changed, 98 insertions(+), 623 deletions(-) diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 8ccbe5487..07204b42a 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -759,11 +759,13 @@ "count_singular": "1 tank", "count_plural": "{count} tanks", "sorting_by": "Sorting by {name}", + "no_tanks_found": { "body": "No tanks found.", "search_again": "Try searching again.", "clear_filters": "Try clearing filters." }, + "sort": { "meta.none": "none", "survivability.health": "health", diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 5739a3ee6..3f972ccd3 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,42 +1,28 @@ +import { asset, ShellType } from "@blitzkit/core"; import { - asset, - ShellType, - TANK_CLASSES, - TankType, - TIER_ROMAN_NUMERALS, -} from "@blitzkit/core"; -import { LockClosedIcon, LockOpen2Icon } from "@radix-ui/react-icons"; + LockClosedIcon, + LockOpen2Icon, + TrashIcon, +} from "@radix-ui/react-icons"; import { Box, + Button, + DropdownMenu, Flex, IconButton, Inset, - Link, Popover, Text, - Tooltip, - type ButtonProps, type FlexProps, } from "@radix-ui/themes"; -import { isEqual, times, chunk } from "lodash-es"; +import { isEqual } from "lodash-es"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; -import { awaitableConsumableDefinitions } from "../../../core/awaitables/consumableDefinitions"; -import { Var } from "../../../core/radix/var"; import { useLocale } from "../../../hooks/useLocale"; import { App } from "../../../stores/app"; import { TankFilters } from "../../../stores/tankFilters"; -import { TankSort } from "../../../stores/tankopediaSort"; -import { classIcons } from "../../ClassIcon"; -import { GunAutoloaderIcon } from "../../GunAutoloaderIcon"; -import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon"; -import { GunRegularIcon } from "../../GunRegularIcon"; import { MissingShellIcon } from "../../MissingShellIcon"; -import { ResearchedIcon } from "../../ResearchedIcon"; -import { ScienceIcon } from "../../ScienceIcon"; -import { ScienceOffIcon } from "../../ScienceOffIcon"; const gameDefinitions = await awaitableGameDefinitions; -const consumableDefinitions = await awaitableConsumableDefinitions; const shellTypeIcons: Record = { [ShellType.AP]: "ap", @@ -45,26 +31,7 @@ const shellTypeIcons: Record = { [ShellType.HEAT]: "hc", }; -// Get consumables -const regularConsumables = Object.values( - consumableDefinitions.consumables -).filter( - (consumable) => - !consumable.game_mode_exclusive && consumable.name.locales.en !== "" -); - -// Split consumables into chunks of 5 for columns -const consumableColumns = chunk(regularConsumables, 5); - -// Get game mode exclusive abilities -const gameModeAbilities = Object.values( - consumableDefinitions.consumables -).filter((consumable) => consumable.game_mode_exclusive); - -// Split abilities into chunks of 5 for columns -const abilityColumns = chunk(gameModeAbilities, 5); - -const size: ButtonProps["size"] = { initial: "1", xs: "2" }; +const MAX_NATIONS = 3; function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { const shells = TankFilters.use((state) => state.shells); @@ -72,13 +39,7 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { return ( - + {shells[index] === null && ( @@ -107,7 +68,6 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { return ( state.logins.wargaming); + const nations = + tankFilters.nations.length === 0 + ? gameDefinitions.nations + : tankFilters.nations; + return ( - - - - - {times(5, (index) => { - const tier = index + 1; - const selected = tankFilters.tiers?.includes(tier); - - return ( - { - if (tankFilters.tiers.includes(tier)) { - TankFilters.mutate((draft) => { - draft.tiers = draft.tiers.filter((t) => t !== tier); - }); - } else { - TankFilters.mutate((draft) => { - draft.tiers = [...draft.tiers, tier]; - }); - } + + + + + - - {times(5, (index) => { - const tier = index + 6; - const selected = tankFilters.tiers?.includes(tier); - - return ( - { - if (tankFilters.tiers.includes(tier)) { - TankFilters.mutate((draft) => { - draft.tiers = draft.tiers.filter((t) => t !== tier); - }); - } else { - TankFilters.mutate((draft) => { - draft.tiers = [...draft.tiers, tier]; - }); - } - }} - > - {TIER_ROMAN_NUMERALS[tier]} - - ); - })} - - - - - - {gameDefinitions.nations - .slice(0, Math.ceil(gameDefinitions.nations.length / 2)) - .map((nation) => { - const selected = tankFilters.nations?.includes(nation); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.nations.includes(nation)) { - draft.nations = draft.nations.filter( - (n) => n !== nation - ); - } else { - draft.nations = [...draft.nations, nation]; - } - }); - }} - > - - - ); - })} - - - - - - {gameDefinitions.nations - .slice(Math.ceil(gameDefinitions.nations.length / 2)) - .map((nation) => { - const selected = tankFilters.nations?.includes(nation); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.nations.includes(nation)) { - draft.nations = draft.nations.filter( - (n) => n !== nation - ); - } else { - draft.nations = [...draft.nations, nation]; - } - }); - }} - > - - - ); - })} - - - - {/* Consumables Filter - RIGHT AFTER NATIONS */} - - - {consumableColumns[0]?.map((consumable) => { - const selected = tankFilters.consumables?.includes(consumable.id); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.consumables.includes(consumable.id)) { - draft.consumables = draft.consumables.filter( - (c) => c !== consumable.id - ); - } else { - draft.consumables = [ - ...draft.consumables, - consumable.id, - ]; - } - }); - }} - > - - - ); - })} - - - {consumableColumns[1] && ( - <> - - - - {consumableColumns[1].map((consumable) => { - const selected = tankFilters.consumables?.includes( - consumable.id - ); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.consumables.includes(consumable.id)) { - draft.consumables = draft.consumables.filter( - (c) => c !== consumable.id - ); - } else { - draft.consumables = [ - ...draft.consumables, - consumable.id, - ]; - } - }); - }} - > - - - ); - })} - - - )} - - {consumableColumns[2] && ( - <> - - - - {consumableColumns[2].map((consumable) => { - const selected = tankFilters.consumables?.includes( - consumable.id - ); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.consumables.includes(consumable.id)) { - draft.consumables = draft.consumables.filter( - (c) => c !== consumable.id - ); - } else { - draft.consumables = [ - ...draft.consumables, - consumable.id, - ]; - } - }); - }} - > - - - ); - })} - - - )} - - - - {TANK_CLASSES.map((tankClass) => { - const Icon = classIcons[tankClass]; - const selected = tankFilters.classes?.includes(tankClass); + + {gameDefinitions.nations.map((nation) => { + const selected = tankFilters.nations.includes(nation); return ( - { + { + event.preventDefault(); + TankFilters.mutate((draft) => { - if (tankFilters.classes.includes(tankClass)) { - draft.classes = draft.classes.filter( - (c) => c !== tankClass - ); + if (selected) { + draft.nations = draft.nations.filter((n) => n !== nation); } else { - draft.classes = [...draft.classes, tankClass]; + draft.nations = [...draft.nations, nation]; } }); }} + checked={selected} + key={nation} + style={{ + position: "relative", + }} > - - + + + + + + { + strings.common.nations[ + nation as keyof typeof strings.common.nations + ] + } + + ); })} - - - { - TankFilters.mutate((draft) => { - if (tankFilters.gunType.includes("regular")) { - draft.gunType = draft.gunType.filter((t) => t !== "regular"); - } else { - draft.gunType = [...draft.gunType, "regular"]; - } - }); - }} - > - - - { - TankFilters.mutate((draft) => { - if (tankFilters.gunType.includes("auto_loader")) { - draft.gunType = draft.gunType.filter( - (t) => t !== "auto_loader" - ); - } else { - draft.gunType = [...draft.gunType, "auto_loader"]; - } - }); - }} - > - - - { - TankFilters.mutate((draft) => { - if (tankFilters.gunType.includes("auto_reloader")) { - draft.gunType = draft.gunType.filter( - (t) => t !== "auto_reloader" - ); - } else { - draft.gunType = [...draft.gunType, "auto_reloader"]; - } - }); - }} - > - - - + - - - - - + { + event.preventDefault(); - - { TankFilters.mutate((draft) => { - if (tankFilters.types.includes(TankType.RESEARCHABLE)) { - draft.types = draft.types.filter( - (t) => t !== TankType.RESEARCHABLE - ); - } else { - draft.types = [...draft.types, TankType.RESEARCHABLE]; - } + draft.nations = []; }); }} > - - - { - TankFilters.mutate((draft) => { - if (tankFilters.types.includes(TankType.PREMIUM)) { - draft.types = draft.types.filter( - (t) => t !== TankType.PREMIUM - ); - } else { - draft.types = [...draft.types, TankType.PREMIUM]; - } - }); - }} - > - - - - - { - TankFilters.mutate((draft) => { - if (tankFilters.types.includes(TankType.COLLECTOR)) { - draft.types = draft.types.filter( - (t) => t !== TankType.COLLECTOR - ); - } else { - draft.types = [...draft.types, TankType.COLLECTOR]; - } - }); - }} - > - - - - - - - - { - TankFilters.mutate((draft) => { - draft.showNonTesting = !draft.showNonTesting; - }); - }} - > - - - - { - TankFilters.mutate((draft) => { - draft.showTesting = !draft.showTesting; - }); - }} - > - - - - - {wargaming ? ( - - ) : ( - - - - )} - - - {clearable && ( - { - event.preventDefault(); - TankFilters.set(TankFilters.initial); - TankSort.set(TankSort.initial); - }} - > - {strings.website.common.tank_search.clear_filters} - - )} + + Clear + + + ); } From 4462ebe82c44b288449228bf65b89badc29e16c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:00:18 -0600 Subject: [PATCH 18/55] feat: tiers filter --- .../TankSearch/components/FilterControl.tsx | 211 +++++++----------- 1 file changed, 79 insertions(+), 132 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 3f972ccd3..883156eeb 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,26 +1,11 @@ -import { asset, ShellType } from "@blitzkit/core"; -import { - LockClosedIcon, - LockOpen2Icon, - TrashIcon, -} from "@radix-ui/react-icons"; -import { - Box, - Button, - DropdownMenu, - Flex, - IconButton, - Inset, - Popover, - Text, - type FlexProps, -} from "@radix-ui/themes"; -import { isEqual } from "lodash-es"; +import { asset, ShellType, TIER_ROMAN_NUMERALS } from "@blitzkit/core"; +import { TrashIcon } from "@radix-ui/react-icons"; +import { Box, Button, DropdownMenu, Flex, Text } from "@radix-ui/themes"; +import { isEqual, times } from "lodash-es"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { useLocale } from "../../../hooks/useLocale"; import { App } from "../../../stores/app"; import { TankFilters } from "../../../stores/tankFilters"; -import { MissingShellIcon } from "../../MissingShellIcon"; const gameDefinitions = await awaitableGameDefinitions; @@ -32,119 +17,8 @@ const shellTypeIcons: Record = { }; const MAX_NATIONS = 3; - -function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { - const shells = TankFilters.use((state) => state.shells); - - return ( - - - - {shells[index] === null && ( - - - - )} - {shells[index] !== null && ( - - )} - - - - - - - {Object.values(ShellType).map((shellType) => { - if (typeof shellType === "string") return null; - - const selected = shells[index] === shellType; - - return ( - { - const mutated = [...shells] as TankFilters["shells"]; - - mutated[index] = selected ? null : shellType; - - TankFilters.mutate((draft) => { - draft.shells = mutated; - }); - }} - highContrast={selected} - color={selected ? undefined : "gray"} - > - - - ); - })} - - - - - ); -} - -function OwnershipFilter(props: FlexProps) { - const tankFilters = TankFilters.use(); - const wargaming = App.use((state) => state.logins.wargaming); - - return ( - - { - TankFilters.mutate((draft) => { - draft.showOwned = !draft.showOwned; - }); - }} - > - - - - { - TankFilters.mutate((draft) => { - draft.showUnowned = !draft.showUnowned; - }); - }} - > - - - - ); -} +const MAX_TIERS = 2; +const TIERS = times(10, (i) => i + 1); export function FilterControl() { const tankFilters = TankFilters.use(); @@ -152,6 +26,7 @@ export function FilterControl() { const clearable = !isEqual(tankFilters, TankFilters.initial); const wargaming = App.use((state) => state.logins.wargaming); + const tiers = tankFilters.tiers.length === 0 ? TIERS : tankFilters.tiers; const nations = tankFilters.nations.length === 0 ? gameDefinitions.nations @@ -159,6 +34,78 @@ export function FilterControl() { return ( + + + + + + + {TIERS.map((tier) => { + const selected = tankFilters.tiers.includes(tier); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.tiers = draft.tiers.filter((t) => t !== tier); + } else { + draft.tiers = [...draft.tiers, tier]; + } + }); + }} + checked={selected} + key={tier} + > + Tier {TIER_ROMAN_NUMERALS[tier]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.tiers = []; + }); + }} + > + + Clear + + + + - - - - {TIERS.map((tier) => { - const selected = tankFilters.tiers.includes(tier); - - return ( - { - event.preventDefault(); - - TankFilters.mutate((draft) => { - if (selected) { - draft.tiers = draft.tiers.filter((t) => t !== tier); - } else { - draft.tiers = [...draft.tiers, tier]; - } - }); - }} - checked={selected} - key={tier} - > - Tier {TIER_ROMAN_NUMERALS[tier]} - - ); - })} - - - - { - event.preventDefault(); - - TankFilters.mutate((draft) => { - draft.tiers = []; - }); - }} - > - - Clear - - - - - - - - - - - {gameDefinitions.nations.map((nation) => { - const selected = tankFilters.nations.includes(nation); - - return ( - { - event.preventDefault(); - - TankFilters.mutate((draft) => { - if (selected) { - draft.nations = draft.nations.filter((n) => n !== nation); - } else { - draft.nations = [...draft.nations, nation]; - } - }); - }} - checked={selected} - key={nation} - style={{ - position: "relative", - }} - > - - - - - - { - strings.common.nations[ - nation as keyof typeof strings.common.nations - ] - } - - - ); - })} - - - - { - event.preventDefault(); - - TankFilters.mutate((draft) => { - draft.nations = []; - }); - }} - > - - Clear - - - + + ); } + +function TiersFilter() { + const tiersRaw = TankFilters.use((state) => state.tiers); + const tiers = tiersRaw.length === 0 ? TIERS : tiersRaw; + + return ( + + + + + + + {TIERS.map((tier) => { + const selected = tiersRaw.includes(tier); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.tiers = draft.tiers.filter((t) => t !== tier); + } else { + draft.tiers = [...draft.tiers, tier].sort((a, b) => b - a); + } + }); + }} + checked={selected} + key={tier} + > + Tier {TIER_ROMAN_NUMERALS[tier]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.tiers = []; + }); + }} + > + + Clear + + + + ); +} + +function NationsFilter() { + const rawNations = TankFilters.use((state) => state.nations); + const nations = + rawNations.length === 0 ? gameDefinitions.nations : rawNations; + const { strings } = useLocale(); + + return ( + + + + + + + {gameDefinitions.nations.map((nation) => { + const selected = rawNations.includes(nation); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.nations = draft.nations.filter((n) => n !== nation); + } else { + draft.nations = [...draft.nations, nation]; + } + }); + }} + checked={selected} + key={nation} + style={{ + position: "relative", + }} + > + + + + + + { + strings.common.nations[ + nation as keyof typeof strings.common.nations + ] + } + + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.nations = []; + }); + }} + > + + Clear + + + + ); +} From 5b4c85138e1414ca7eab0e58453f8ec26c5c23fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:12:25 -0600 Subject: [PATCH 20/55] feat: classes --- .../TankSearch/components/FilterControl.tsx | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index a459d5dba..a05a0ef19 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,10 +1,16 @@ -import { asset, ShellType, TIER_ROMAN_NUMERALS } from "@blitzkit/core"; +import { + asset, + ShellType, + TANK_CLASSES, + TIER_ROMAN_NUMERALS, +} from "@blitzkit/core"; import { TrashIcon } from "@radix-ui/react-icons"; import { Box, Button, DropdownMenu, Flex, Text } from "@radix-ui/themes"; import { times } from "lodash-es"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { useLocale } from "../../../hooks/useLocale"; import { TankFilters } from "../../../stores/tankFilters"; +import { classIcons } from "../../ClassIcon"; const gameDefinitions = await awaitableGameDefinitions; @@ -17,6 +23,7 @@ const shellTypeIcons: Record = { const MAX_NATIONS = 3; const MAX_TIERS = 2; + const TIERS = times(10, (i) => 10 - i); export function FilterControl() { @@ -24,6 +31,7 @@ export function FilterControl() { + ); } @@ -111,6 +119,7 @@ function NationsFilter() { {nations.slice(0, MAX_NATIONS).map((nation, index) => ( 0 ? "-0.5em" : undefined, width: "1.25em", height: "1.25em", @@ -205,3 +214,84 @@ function NationsFilter() { ); } + +function ClassFilter() { + const { strings } = useLocale(); + const rawClasses = TankFilters.use((state) => state.classes); + const classes = rawClasses.length === 0 ? TANK_CLASSES : rawClasses; + + return ( + + + + + + + {TANK_CLASSES.map((tankClass) => { + const selected = rawClasses.includes(tankClass); + const Icon = classIcons[tankClass]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.classes = draft.classes.filter( + (c) => c !== tankClass + ); + } else { + draft.classes = [...draft.classes, tankClass]; + } + }); + }} + checked={selected} + key={tankClass} + > + + + {strings.common.tank_class_medium[tankClass]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.classes = []; + }); + }} + > + + Clear + + + + ); +} From 41228560245262f61b716740e6f2513f22561d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:13:37 -0600 Subject: [PATCH 21/55] fix: reduce gap --- packages/website/src/components/TankSearch/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/components/TankSearch/index.tsx b/packages/website/src/components/TankSearch/index.tsx index 581833c1c..4008e0366 100644 --- a/packages/website/src/components/TankSearch/index.tsx +++ b/packages/website/src/components/TankSearch/index.tsx @@ -429,7 +429,7 @@ export const TankSearch = memo( }, [tankFilters, tankopediaSort]); return ( - + Date: Tue, 30 Dec 2025 20:23:06 -0600 Subject: [PATCH 22/55] feat: gun types --- .../TankSearch/components/FilterControl.tsx | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index a05a0ef19..979cb2621 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,5 +1,6 @@ import { asset, + GunDefinition, ShellType, TANK_CLASSES, TIER_ROMAN_NUMERALS, @@ -7,10 +8,14 @@ import { import { TrashIcon } from "@radix-ui/react-icons"; import { Box, Button, DropdownMenu, Flex, Text } from "@radix-ui/themes"; import { times } from "lodash-es"; +import type { ComponentProps, ReactNode } from "react"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { useLocale } from "../../../hooks/useLocale"; -import { TankFilters } from "../../../stores/tankFilters"; +import { TankFilters, type CaseType } from "../../../stores/tankFilters"; import { classIcons } from "../../ClassIcon"; +import { GunAutoloaderIcon } from "../../GunAutoloaderIcon"; +import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon"; +import { GunRegularIcon } from "../../GunRegularIcon"; const gameDefinitions = await awaitableGameDefinitions; @@ -21,6 +26,19 @@ const shellTypeIcons: Record = { [ShellType.HEAT]: "hc", }; +const GUN_TYPE_ICONS: Record< + CaseType, + (props: ComponentProps<"svg">) => ReactNode +> = { + regular: GunRegularIcon, + auto_loader: GunAutoloaderIcon, + auto_reloader: GunAutoreloaderIcon, +}; + +const GUN_TYPES = Object.keys( + GUN_TYPE_ICONS +) as (keyof typeof GUN_TYPE_ICONS)[]; + const MAX_NATIONS = 3; const MAX_TIERS = 2; @@ -32,6 +50,7 @@ export function FilterControl() { + ); } @@ -295,3 +314,81 @@ function ClassFilter() { ); } + +function GunTypeFilter() { + const rawGunTypes = TankFilters.use((state) => state.gunType); + const gunTypes = rawGunTypes.length === 0 ? GUN_TYPES : rawGunTypes; + const { strings } = useLocale(); + + return ( + + + + + + + {GUN_TYPES.map((gunType) => { + const selected = rawGunTypes.includes(gunType); + const Icon = GUN_TYPE_ICONS[gunType]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.gunType = draft.gunType.filter((c) => c !== gunType); + } else { + draft.gunType = [...draft.gunType, gunType]; + } + }); + }} + checked={selected} + key={gunType} + > + + + {strings.common.gun_types[gunType]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.gunType = []; + }); + }} + > + + Clear + + + + ); +} From 71e40a0113e2b16df8b93dc64612c89e76646691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:30:18 -0600 Subject: [PATCH 23/55] feat: tank types --- .../TankSearch/components/FilterControl.tsx | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 979cb2621..38d64521e 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -3,6 +3,8 @@ import { GunDefinition, ShellType, TANK_CLASSES, + TANK_TYPES, + TankType, TIER_ROMAN_NUMERALS, } from "@blitzkit/core"; import { TrashIcon } from "@radix-ui/react-icons"; @@ -16,6 +18,7 @@ import { classIcons } from "../../ClassIcon"; import { GunAutoloaderIcon } from "../../GunAutoloaderIcon"; import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon"; import { GunRegularIcon } from "../../GunRegularIcon"; +import { ResearchedIcon } from "../../ResearchedIcon"; const gameDefinitions = await awaitableGameDefinitions; @@ -35,6 +38,12 @@ const GUN_TYPE_ICONS: Record< auto_reloader: GunAutoreloaderIcon, }; +const TANK_TYPE_COLORS: Record = { + [TankType.RESEARCHABLE]: "gray", + [TankType.PREMIUM]: "amber", + [TankType.COLLECTOR]: "blue", +}; + const GUN_TYPES = Object.keys( GUN_TYPE_ICONS ) as (keyof typeof GUN_TYPE_ICONS)[]; @@ -49,6 +58,7 @@ export function FilterControl() { + @@ -392,3 +402,91 @@ function GunTypeFilter() { ); } + +function TypeFilter() { + const { strings } = useLocale(); + const rawTypes = TankFilters.use((state) => state.types); + const types = rawTypes.length === 0 ? TANK_TYPES : rawTypes; + + return ( + + + + + + + {TANK_TYPES.map((tankType) => { + const selected = rawTypes.includes(tankType); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.types = draft.types.filter((c) => c !== tankType); + } else { + draft.types = [...draft.types, tankType]; + } + }); + }} + checked={selected} + key={tankType} + > + + + {strings.common.tree_type[tankType]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.types = []; + }); + }} + > + + Clear + + + + ); +} From 6b2958442440fa837ba59de4e7467aeaf2982c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:38:32 -0600 Subject: [PATCH 24/55] feat: minor adjusments --- .../TankSearch/components/FilterControl.tsx | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 38d64521e..bd3c86e5e 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -48,14 +48,13 @@ const GUN_TYPES = Object.keys( GUN_TYPE_ICONS ) as (keyof typeof GUN_TYPE_ICONS)[]; -const MAX_NATIONS = 3; -const MAX_TIERS = 2; +const MAX_TIERS = 4; const TIERS = times(10, (i) => 10 - i); export function FilterControl() { return ( - + @@ -73,7 +72,6 @@ function TiersFilter() { @@ -253,7 +244,6 @@ function ClassFilter() { + + + + {consumablesArray.map((consumable) => { + const selected = rawConsumables.includes(consumable); + const consumableDefinition = + consumableDefinitions.consumables[consumable]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.consumables = draft.consumables.filter( + (n) => n !== consumable + ); + } else { + draft.consumables = [...draft.consumables, consumable]; + } + }); + }} + checked={selected} + key={consumable} + > + + + {unwrap(consumableDefinition.name)} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.consumables = []; + }); + }} + > + + Clear + + + + ); +} From 55ffe22fd6538b61400d59deb1a8bc8ee6dd2134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:56:31 -0600 Subject: [PATCH 31/55] feat: gamemode abilities --- .../TankSearch/components/FilterControl.tsx | 106 +++++++++++++++++- packages/website/src/stores/tankFilters.ts | 2 + 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 7c2bcf2d3..9adc8c40d 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -49,9 +49,18 @@ const consumablesArray = Object.values(consumableDefinitions.consumables) consumable.name.locales[locales.default] ) .map((consumable) => consumable.id); -const abilitiesArray = Object.values(consumableDefinitions.consumables) - .filter((consumable) => consumable.game_mode_exclusive) - .map((consumable) => consumable.id); +const abilitiesArray = Array.from( + Object.values(consumableDefinitions.consumables) + .filter((c) => c.game_mode_exclusive) + .reduce((uniqueMap, consumable) => { + if (!uniqueMap.has(consumable.name.locales[locales.default])) { + uniqueMap.set(consumable.name.locales[locales.default], consumable.id); + } + + return uniqueMap; + }, new Map()) + .values() +); const shellTypeIcons: Record = { [ShellType.AP]: "ap", @@ -81,6 +90,7 @@ const GUN_TYPES = Object.keys( const MAX_TIERS = 4; const MAX_CONSUMABLES = 4; +const MAX_ABILITIES = 4; const TIERS = times(10, (i) => 10 - i); @@ -98,6 +108,7 @@ export function FilterControl() { + ); } @@ -815,3 +826,92 @@ function ConsumablesFilter() { ); } + +function AbilitiesFilter() { + const rawAbilities = TankFilters.use((state) => state.abilities); + const abilities = rawAbilities.length === 0 ? abilitiesArray : rawAbilities; + const { unwrap } = useLocale(); + + return ( + + + + + + + {abilitiesArray.map((ability) => { + const selected = rawAbilities.includes(ability); + const abilityDefinition = consumableDefinitions.consumables[ability]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.abilities = draft.abilities.filter( + (n) => n !== ability + ); + } else { + draft.abilities = [...draft.abilities, ability]; + } + }); + }} + checked={selected} + key={ability} + > + + + {unwrap(abilityDefinition.name)} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.abilities = []; + }); + }} + > + + Clear + + + + ); +} diff --git a/packages/website/src/stores/tankFilters.ts b/packages/website/src/stores/tankFilters.ts index d758d29a5..7b23af850 100644 --- a/packages/website/src/stores/tankFilters.ts +++ b/packages/website/src/stores/tankFilters.ts @@ -22,6 +22,7 @@ export interface TankFilters { gunType: CaseType[]; shells: [ShellType | null, ShellType | null, ShellType | null]; consumables: number[]; + abilities: number[]; showTesting: boolean; showNonTesting: boolean; @@ -39,6 +40,7 @@ export const TankFilters = new Varuna({ gunType: [], shells: [null, null, null], consumables: [], + abilities: [], showTesting: true, showNonTesting: true, From b2856eab582be614d20bcee6c5815c90763893e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:02:28 -0600 Subject: [PATCH 32/55] feat: localize --- packages/i18n/strings/en.json | 5 ++- .../TankSearch/components/FilterControl.tsx | 40 ++++++++++++------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index f0a7b00dc..94def8954 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -15,7 +15,8 @@ "hp": "{value}hp", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}°/s" + "deg_s": "{value}°/s", + "plus": "+{value}" }, "gun_types": { @@ -759,6 +760,8 @@ "count_singular": "1 tank", "count_plural": "{count} tanks", "sorting_by": "Sorting by {name}", + "clear": "Clear", + "tier": "Tier {tier}", "no_tanks_found": { "body": "No tanks found.", diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 9adc8c40d..9ca89f958 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -7,6 +7,7 @@ import { TankType, TIER_ROMAN_NUMERALS, } from "@blitzkit/core"; +import { literals } from "@blitzkit/i18n"; import locales from "@blitzkit/i18n/locales.json"; import { LockClosedIcon, @@ -96,7 +97,7 @@ const TIERS = times(10, (i) => 10 - i); export function FilterControl() { return ( - + @@ -114,6 +115,7 @@ export function FilterControl() { } function TiersFilter() { + const { strings } = useLocale(); const tiersRaw = TankFilters.use((state) => state.tiers); const tiers = tiersRaw.length === 0 ? TIERS : tiersRaw; @@ -129,7 +131,11 @@ function TiersFilter() { ))} {tiers.length > MAX_TIERS && ( - +{tiers.length - MAX_TIERS} + + {literals(strings.common.units.plus, { + value: tiers.length - MAX_TIERS, + })} + )} @@ -155,7 +161,9 @@ function TiersFilter() { checked={selected} key={tier} > - Tier {TIER_ROMAN_NUMERALS[tier]} + {literals(strings.website.common.tank_search.tier, { + tier: TIER_ROMAN_NUMERALS[tier], + })} ); })} @@ -173,7 +181,7 @@ function TiersFilter() { }} > - Clear + {strings.website.common.tank_search.clear} @@ -277,7 +285,7 @@ function NationsFilter() { }} > - Clear + {strings.website.common.tank_search.clear} @@ -357,7 +365,7 @@ function ClassFilter() { }} > - Clear + {strings.website.common.tank_search.clear} @@ -435,7 +443,7 @@ function GunTypeFilter() { }} > - Clear + {strings.website.common.tank_search.clear} @@ -521,7 +529,7 @@ function TypeFilter() { }} > - Clear + {strings.website.common.tank_search.clear} @@ -737,10 +745,10 @@ function TestFilter() { } function ConsumablesFilter() { + const { unwrap, strings } = useLocale(); const rawConsumables = TankFilters.use((state) => state.consumables); const consumables = rawConsumables.length === 0 ? consumablesArray : rawConsumables; - const { unwrap } = useLocale(); return ( @@ -762,7 +770,9 @@ function ConsumablesFilter() { {consumables.length > MAX_CONSUMABLES && ( - +{consumables.length - MAX_CONSUMABLES} + {literals(strings.common.units.plus, { + value: consumables.length - MAX_CONSUMABLES, + })} )} @@ -820,7 +830,7 @@ function ConsumablesFilter() { }} > - Clear + {strings.website.common.tank_search.clear} @@ -828,9 +838,9 @@ function ConsumablesFilter() { } function AbilitiesFilter() { + const { unwrap, strings } = useLocale(); const rawAbilities = TankFilters.use((state) => state.abilities); const abilities = rawAbilities.length === 0 ? abilitiesArray : rawAbilities; - const { unwrap } = useLocale(); return ( @@ -852,7 +862,9 @@ function AbilitiesFilter() { {abilities.length > MAX_ABILITIES && ( - +{abilities.length - MAX_ABILITIES} + {literals(strings.common.units.plus, { + value: abilities.length - MAX_ABILITIES, + })} )} @@ -909,7 +921,7 @@ function AbilitiesFilter() { }} > - Clear + {strings.website.common.tank_search.clear} From e16cc63cfe928f858653dd6d31e2ce5ec1350c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:06:08 -0600 Subject: [PATCH 33/55] feat: bigger icons --- .../TankSearch/components/FilterControl.tsx | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 9ca89f958..d9308e416 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -345,7 +345,7 @@ function ClassFilter() { checked={selected} key={tankClass} > - + {strings.common.tank_class_medium[tankClass]} @@ -423,7 +423,7 @@ function GunTypeFilter() { checked={selected} key={gunType} > - + {strings.common.gun_types[gunType]} @@ -506,8 +506,8 @@ function TypeFilter() { tankType === TankType.RESEARCHABLE ? "12" : "11" })`, opacity: 1, - width: "1em", - height: "1em", + width: "1.25em", + height: "1.25em", }} /> @@ -600,7 +600,9 @@ function IndividualShellFilter({ { + onClick={(event) => { + event.preventDefault(); + const mutated = [...shells] as TankFilters["shells"]; mutated[index] = selected ? null : shellType; @@ -617,7 +619,7 @@ function IndividualShellFilter({ premium ? "_premium" : "" }.webp` )} - style={{ width: "1em", height: "1em" }} + style={{ width: "1.25em", height: "1.25em" }} /> { @@ -630,6 +632,26 @@ function IndividualShellFilter({ ); })} + + + + { + event.preventDefault(); + + const mutated = [...shells] as TankFilters["shells"]; + + mutated[index] = null; + + TankFilters.mutate((draft) => { + draft.shells = mutated; + }); + }} + > + + {strings.website.common.tank_search.clear} + @@ -805,8 +827,8 @@ function ConsumablesFilter() { > Date: Wed, 31 Dec 2025 14:08:37 -0600 Subject: [PATCH 34/55] feat: clear button --- .../TankSearch/components/FilterControl.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index d9308e416..e008496d0 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -110,10 +110,29 @@ export function FilterControl() { + + ); } +function ClearButton() { + const { strings } = useLocale(); + + return ( + + ); +} + function TiersFilter() { const { strings } = useLocale(); const tiersRaw = TankFilters.use((state) => state.tiers); From 9416e25ee9326a8a78e56b34537b613178aff802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:18:58 -0600 Subject: [PATCH 35/55] feat: start of logs --- docs/changelogs/1.29.9.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 docs/changelogs/1.29.9.md diff --git a/docs/changelogs/1.29.9.md b/docs/changelogs/1.29.9.md new file mode 100644 index 000000000..6984cba3d --- /dev/null +++ b/docs/changelogs/1.29.9.md @@ -0,0 +1,12 @@ +# BlitzKit 1.29.9 + +Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update. + +## Features + +- [Mark Greene] Added the ability to filter tanks by Consumables and game-mode Abilities + +## Fixes + +- Consumable and provision icons in Compare are now properly sized +- [Mark Greene] Provisions now properly affect terrain resistances diff --git a/package.json b/package.json index 016893c2c..9f39c56cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkit", - "version": "1.29.8", + "version": "1.29.9", "license": "BlitzKit License", "private": true, "scripts": { From 408a14003cb0461502d168a03907f156a00b2587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:19:10 -0600 Subject: [PATCH 36/55] fix: allow interactbility oustisde of dropdoiwn --- .../TankSearch/components/FilterControl.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index e008496d0..4d28983a6 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -99,9 +99,9 @@ export function FilterControl() { return ( - - + + @@ -139,7 +139,7 @@ function TiersFilter() { const tiers = tiersRaw.length === 0 ? TIERS : tiersRaw; return ( - + + + + + {provisionsArray.map((provision) => { + const selected = rawProvisions.includes(provision); + const provisionDefinition = + provisionDefinitions.provisions[provision]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.provisions = draft.provisions.filter( + (n) => n !== provision + ); + } else { + draft.provisions = [...draft.provisions, provision]; + } + }); + }} + checked={selected} + key={provision} + > + + + {unwrap(provisionDefinition.name)} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.provisions = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + function AbilitiesFilter() { const { unwrap, strings } = useLocale(); const rawAbilities = TankFilters.use((state) => state.abilities); diff --git a/packages/website/src/stores/tankFilters.ts b/packages/website/src/stores/tankFilters.ts index 7b23af850..76475c1d1 100644 --- a/packages/website/src/stores/tankFilters.ts +++ b/packages/website/src/stores/tankFilters.ts @@ -22,6 +22,7 @@ export interface TankFilters { gunType: CaseType[]; shells: [ShellType | null, ShellType | null, ShellType | null]; consumables: number[]; + provisions: number[]; abilities: number[]; showTesting: boolean; @@ -40,6 +41,7 @@ export const TankFilters = new Varuna({ gunType: [], shells: [null, null, null], consumables: [], + provisions: [], abilities: [], showTesting: true, From 610ea60ca2bb58b9fc2a12b4ff25203b20d4f7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:31:48 -0600 Subject: [PATCH 39/55] feat: provisions and powers --- .../TankSearch/components/FilterControl.tsx | 120 +++++++++++++++--- packages/website/src/stores/tankFilters.ts | 2 + 2 files changed, 105 insertions(+), 17 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 0b394c6dd..4cfddb32f 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -122,10 +122,7 @@ const GUN_TYPES = Object.keys( GUN_TYPE_ICONS ) as (keyof typeof GUN_TYPE_ICONS)[]; -const MAX_TIERS = 4; -const MAX_CONSUMABLES = 4; -const MAX_PROVISIONS = 4; -const MAX_ABILITIES = 4; +const MAX_ICONS = 4; const TIERS = times(10, (i) => 10 - i); @@ -179,16 +176,16 @@ function TiersFilter() { + + + + {powersArray.map((power) => { + const selected = rawPowers.includes(power); + const powerDefinition = provisionDefinitions.provisions[power]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.powers = draft.powers.filter((n) => n !== power); + } else { + draft.powers = [...draft.powers, power]; + } + }); + }} + checked={selected} + key={power} + > + + + {unwrap(powerDefinition.name)} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.powers = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + function AbilitiesFilter() { const { unwrap, strings } = useLocale(); const rawAbilities = TankFilters.use((state) => state.abilities); @@ -1021,7 +1107,7 @@ function AbilitiesFilter() { + + ); } diff --git a/packages/website/src/pages/[...locale]/tanks/_index.tsx b/packages/website/src/pages/[...locale]/tanks/_index.tsx index af64cd976..f5b22a310 100644 --- a/packages/website/src/pages/[...locale]/tanks/_index.tsx +++ b/packages/website/src/pages/[...locale]/tanks/_index.tsx @@ -12,7 +12,7 @@ export function Page({ }: LocaleAcceptorProps & MaybeSkeletonComponentProps) { return ( - + From 9df5efa4224c21131cb1fa175a040c4677c78b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 11:32:27 -0600 Subject: [PATCH 44/55] feat: enable gamemode exclusive --- .../checkConsumableProvisionInclusivity.ts | 81 +++--- .../scripts/src/buildAssets/definitions.ts | 272 +++++++++--------- 2 files changed, 170 insertions(+), 183 deletions(-) diff --git a/packages/core/src/blitzkit/checkConsumableProvisionInclusivity.ts b/packages/core/src/blitzkit/checkConsumableProvisionInclusivity.ts index b1640498f..ad1a90ac8 100644 --- a/packages/core/src/blitzkit/checkConsumableProvisionInclusivity.ts +++ b/packages/core/src/blitzkit/checkConsumableProvisionInclusivity.ts @@ -11,56 +11,47 @@ export function checkConsumableProvisionInclusivity( tank: TankDefinition, gun: GunDefinition ) { - const included = - !consumableProvision.game_mode_exclusive && - consumableProvision.include?.every((rule) => { - switch (rule.filter_type!.$case) { - case "tiers": - return ( - rule.filter_type!.value.min <= tank.tier && - tank.tier <= rule.filter_type!.value.max - ); + const included = consumableProvision.include?.every((rule) => { + switch (rule.filter_type!.$case) { + case "tiers": + return ( + rule.filter_type!.value.min <= tank.tier && + tank.tier <= rule.filter_type!.value.max + ); - case "ids": - return rule.filter_type!.value.ids.includes(tank.id); + case "ids": + return rule.filter_type!.value.ids.includes(tank.id); - case "nations": - return rule.filter_type!.value.nations.includes(tank.nation); + case "nations": + return rule.filter_type!.value.nations.includes(tank.nation); - case "categories": - throw new SyntaxError("Category filtering found in include rule"); - } - }); - const excluded = - consumableProvision.game_mode_exclusive || - consumableProvision.exclude?.some((rule) => { - switch (rule.filter_type!.$case) { - case "tiers": - return ( - rule.filter_type!.value.min <= tank.tier && - tank.tier <= rule.filter_type!.value.max - ); + case "categories": + throw new SyntaxError("Category filtering found in include rule"); + } + }); + const excluded = consumableProvision.exclude?.some((rule) => { + switch (rule.filter_type!.$case) { + case "tiers": + return ( + rule.filter_type!.value.min <= tank.tier && + tank.tier <= rule.filter_type!.value.max + ); - case "ids": - return rule.filter_type!.value.ids.includes(tank.id); + case "ids": + return rule.filter_type!.value.ids.includes(tank.id); - case "nations": - return rule.filter_type!.value.nations.includes(tank.nation); + case "nations": + return rule.filter_type!.value.nations.includes(tank.nation); - case "categories": - return rule.filter_type!.value.categories.some((category) => { - switch (category) { - case ConsumableTankCategoryFilterCategory.CLIP: - return gun.gun_type!.$case !== "regular"; - } - }); - } - }); + case "categories": + return rule.filter_type!.value.categories.some((category) => { + switch (category) { + case ConsumableTankCategoryFilterCategory.CLIP: + return gun.gun_type!.$case !== "regular"; + } + }); + } + }); - return ( - !consumableProvision.game_mode_exclusive && - consumableProvision.include?.length > 0 && - included && - !excluded - ); + return consumableProvision.include?.length > 0 && included && !excluded; } diff --git a/packages/scripts/src/buildAssets/definitions.ts b/packages/scripts/src/buildAssets/definitions.ts index 94608ef77..ba83125ae 100644 --- a/packages/scripts/src/buildAssets/definitions.ts +++ b/packages/scripts/src/buildAssets/definitions.ts @@ -1557,87 +1557,85 @@ export async function definitions() { const includeRaw = consumable.vehicleFilter?.include.vehicle; const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle; - if (!entry.game_mode_exclusive) { - if (includeRaw) { - entry.include = []; + if (includeRaw) { + entry.include = []; - if ("minLevel" in includeRaw) { - entry.include.push({ - filter_type: { - $case: "tiers", - value: { - min: includeRaw.minLevel, - max: includeRaw.maxLevel, - }, + if ("minLevel" in includeRaw) { + entry.include.push({ + filter_type: { + $case: "tiers", + value: { + min: includeRaw.minLevel, + max: includeRaw.maxLevel, }, - }); - } else if ("name" in includeRaw) { - entry.include.push({ - filter_type: { - $case: "ids", - value: { - ids: includeRaw.name.split(/ +/).map((key) => { - return tankStringIdMap[key]; - }), - }, + }, + }); + } else if ("name" in includeRaw) { + entry.include.push({ + filter_type: { + $case: "ids", + value: { + ids: includeRaw.name.split(/ +/).map((key) => { + return tankStringIdMap[key]; + }), }, - }); - } else throw new SyntaxError("Unhandled include type"); + }, + }); + } else throw new SyntaxError("Unhandled include type"); - if (consumable.vehicleFilter?.include.nations) { - entry.include.push({ - filter_type: { - $case: "nations", - value: { - nations: consumable.vehicleFilter.include.nations.split(" "), - }, + if (consumable.vehicleFilter?.include.nations) { + entry.include.push({ + filter_type: { + $case: "nations", + value: { + nations: consumable.vehicleFilter.include.nations.split(" "), }, - }); - } + }, + }); } + } - if (excludeRaw) { - entry.exclude = []; + if (excludeRaw) { + entry.exclude = []; - if ("name" in excludeRaw) { - entry.exclude!.push({ - filter_type: { - $case: "ids", - value: { - ids: excludeRaw.name.split(/ +/).map((key) => { - return tankStringIdMap[key]; - }), - }, + if ("name" in excludeRaw) { + entry.exclude!.push({ + filter_type: { + $case: "ids", + value: { + ids: excludeRaw.name.split(/ +/).map((key) => { + return tankStringIdMap[key]; + }), }, - }); - } else if ("extendedTags" in excludeRaw) { - entry.exclude!.push({ - filter_type: { - $case: "categories", - value: { - categories: excludeRaw.extendedTags - .split(" ") - .map( - (item) => - blitzTankFilterDefinitionCategoryToBlitzkit[ - item as BlitzTankFilterDefinitionCategory - ] - ), - }, + }, + }); + } else if ("extendedTags" in excludeRaw) { + entry.exclude!.push({ + filter_type: { + $case: "categories", + value: { + categories: excludeRaw.extendedTags + .split(" ") + .map( + (item) => + blitzTankFilterDefinitionCategoryToBlitzkit[ + item as BlitzTankFilterDefinitionCategory + ] + ), }, - }); - } else throw new SyntaxError("Unhandled exclude type"); + }, + }); + } else throw new SyntaxError("Unhandled exclude type"); - if (consumable.vehicleFilter?.exclude?.nations) { - entry.exclude!.push({ - filter_type: { - $case: "nations", - value: { - nations: consumable.vehicleFilter.exclude.nations.split(" "), - }, + if (consumable.vehicleFilter?.exclude?.nations) { + entry.exclude!.push({ + filter_type: { + $case: "nations", + value: { + nations: consumable.vehicleFilter.exclude.nations.split(" "), }, - }); - } + }, + }); } } }); @@ -1657,87 +1655,85 @@ export async function definitions() { const includeRaw = provision.vehicleFilter?.include.vehicle; const excludeRaw = provision.vehicleFilter?.exclude?.vehicle; - if (!entry.game_mode_exclusive) { - if (includeRaw) { - entry.include = []; + if (includeRaw) { + entry.include = []; - if ("minLevel" in includeRaw) { - entry.include.push({ - filter_type: { - $case: "tiers", - value: { - min: includeRaw.minLevel, - max: includeRaw.maxLevel, - }, + if ("minLevel" in includeRaw) { + entry.include.push({ + filter_type: { + $case: "tiers", + value: { + min: includeRaw.minLevel, + max: includeRaw.maxLevel, }, - }); - } else if ("name" in includeRaw) { - entry.include.push({ - filter_type: { - $case: "ids", - value: { - ids: includeRaw.name.split(/ +/).map((key) => { - return tankStringIdMap[key]; - }), - }, + }, + }); + } else if ("name" in includeRaw) { + entry.include.push({ + filter_type: { + $case: "ids", + value: { + ids: includeRaw.name.split(/ +/).map((key) => { + return tankStringIdMap[key]; + }), }, - }); - } else throw new SyntaxError("Unhandled include type"); + }, + }); + } else throw new SyntaxError("Unhandled include type"); - if (provision.vehicleFilter?.include.nations) { - entry.include.push({ - filter_type: { - $case: "nations", - value: { - nations: provision.vehicleFilter.include.nations.split(" "), - }, + if (provision.vehicleFilter?.include.nations) { + entry.include.push({ + filter_type: { + $case: "nations", + value: { + nations: provision.vehicleFilter.include.nations.split(" "), }, - }); - } + }, + }); } + } - if (excludeRaw) { - entry.exclude = []; + if (excludeRaw) { + entry.exclude = []; - if ("name" in excludeRaw) { - entry.exclude!.push({ - filter_type: { - $case: "ids", - value: { - ids: excludeRaw.name - .split(/ +/) - .map((key) => tankStringIdMap[key]), - }, + if ("name" in excludeRaw) { + entry.exclude!.push({ + filter_type: { + $case: "ids", + value: { + ids: excludeRaw.name + .split(/ +/) + .map((key) => tankStringIdMap[key]), }, - }); - } else if ("extendedTags" in excludeRaw) { - entry.exclude!.push({ - filter_type: { - $case: "categories", - value: { - categories: excludeRaw.extendedTags - .split(" ") - .map( - (item) => - blitzTankFilterDefinitionCategoryToBlitzkit[ - item as BlitzTankFilterDefinitionCategory - ] - ), - }, + }, + }); + } else if ("extendedTags" in excludeRaw) { + entry.exclude!.push({ + filter_type: { + $case: "categories", + value: { + categories: excludeRaw.extendedTags + .split(" ") + .map( + (item) => + blitzTankFilterDefinitionCategoryToBlitzkit[ + item as BlitzTankFilterDefinitionCategory + ] + ), }, - }); - } else throw new SyntaxError("Unhandled exclude type"); + }, + }); + } else throw new SyntaxError("Unhandled exclude type"); - if (provision.vehicleFilter?.exclude?.nations) { - entry.exclude!.push({ - filter_type: { - $case: "nations", - value: { - nations: provision.vehicleFilter.exclude.nations.split(" "), - }, + if (provision.vehicleFilter?.exclude?.nations) { + entry.exclude!.push({ + filter_type: { + $case: "nations", + value: { + nations: provision.vehicleFilter.exclude.nations.split(" "), }, - }); - } + }, + }); } } From 56a09536cd3d2871f7596694ef387ecd27731d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:09:23 -0600 Subject: [PATCH 45/55] fix: use roles --- .../src/components/ConsumablesManager.tsx | 8 ++-- .../src/components/ProvisionsManager.tsx | 24 ++++++---- .../components/Tankopedia/GameModeSection.tsx | 45 ++++++++++--------- .../website/src/core/blitzkit/filterTank.ts | 26 ++++------- 4 files changed, 51 insertions(+), 52 deletions(-) diff --git a/packages/website/src/components/ConsumablesManager.tsx b/packages/website/src/components/ConsumablesManager.tsx index 932f4d5cf..8ddff5c06 100644 --- a/packages/website/src/components/ConsumablesManager.tsx +++ b/packages/website/src/components/ConsumablesManager.tsx @@ -1,6 +1,6 @@ -import type { Consumable } from '@blitzkit/core'; -import { Flex, type FlexProps } from '@radix-ui/themes'; -import { ConsumableButton } from './ModuleButtons/ConsumableButton'; +import type { Consumable } from "@blitzkit/core"; +import { Flex, type FlexProps } from "@radix-ui/themes"; +import { ConsumableButton } from "./ModuleButtons/ConsumableButton"; type ConsumablesManagerProps = FlexProps & { consumables: Consumable[]; @@ -27,6 +27,8 @@ export function ConsumablesManager({ return ( {consumables.map((consumable) => { + if (consumable.game_mode_exclusive) return null; + const isSelected = selected.some((id) => id === consumable.id); return ( diff --git a/packages/website/src/components/ProvisionsManager.tsx b/packages/website/src/components/ProvisionsManager.tsx index 4a8a4b740..7ede38889 100644 --- a/packages/website/src/components/ProvisionsManager.tsx +++ b/packages/website/src/components/ProvisionsManager.tsx @@ -1,5 +1,6 @@ -import { Flex } from '@radix-ui/themes'; -import { ProvisionButton } from './ModuleButtons/ProvisionButton'; +import { Flex } from "@radix-ui/themes"; +import { awaitableProvisionDefinitions } from "../core/awaitables/provisionDefinitions"; +import { ProvisionButton } from "./ModuleButtons/ProvisionButton"; interface ProvisionsManagerProps { provisions: number[]; @@ -8,6 +9,8 @@ interface ProvisionsManagerProps { onChange?: (provisions: number[]) => void; } +const provisionDefinitions = await awaitableProvisionDefinitions; + export function ProvisionsManager({ provisions, selected, @@ -15,15 +18,18 @@ export function ProvisionsManager({ disabled, }: ProvisionsManagerProps) { return ( - - {provisions.map((provision) => { - const isSelected = selected.includes(provision); + + {provisions.map((id) => { + const isSelected = selected.includes(id); + const provision = provisionDefinitions.provisions[id]; + + if (provision.game_mode_exclusive) return null; return ( { if (!onChange) return; @@ -31,9 +37,9 @@ export function ProvisionsManager({ const draft = [...selected]; if (isSelected) { - draft.splice(draft.indexOf(provision), 1); + draft.splice(draft.indexOf(id), 1); } else { - draft.push(provision); + draft.push(id); } onChange(draft); diff --git a/packages/website/src/components/Tankopedia/GameModeSection.tsx b/packages/website/src/components/Tankopedia/GameModeSection.tsx index 0c7e48196..b45b5b2aa 100644 --- a/packages/website/src/components/Tankopedia/GameModeSection.tsx +++ b/packages/website/src/components/Tankopedia/GameModeSection.tsx @@ -1,10 +1,10 @@ -import { asset } from '@blitzkit/core'; -import { Box, Flex, Heading, Text } from '@radix-ui/themes'; -import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions'; -import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions'; -import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions'; -import { useLocale } from '../../hooks/useLocale'; -import { Duel } from '../../stores/duel'; +import { asset } from "@blitzkit/core"; +import { Box, Flex, Heading, Text } from "@radix-ui/themes"; +import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions"; +import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions"; +import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions"; +import { useLocale } from "../../hooks/useLocale"; +import { Duel } from "../../stores/duel"; const [gameDefinitions, consumableDefinitions, provisionDefinitions] = await Promise.all([ @@ -39,22 +39,22 @@ export function GameModeSection() { overflow="hidden" direction="column" style={{ - borderRadius: 'var(--radius-3)', + borderRadius: "var(--radius-3)", background: `url(${asset( - `icons/game_mode_banners/${id}.webp`, + `icons/game_mode_banners/${id}.webp` )})`, - backgroundSize: 'cover', - backgroundPosition: 'center', + backgroundSize: "cover", + backgroundPosition: "center", }} > @@ -64,8 +64,8 @@ export function GameModeSection() { height="98px" flexShrink="0" style={{ - backdropFilter: 'blur(4rem)', - WebkitBackdropFilter: 'blur(4rem)', + backdropFilter: "blur(4rem)", + WebkitBackdropFilter: "blur(4rem)", }} align="center" justify="center" @@ -83,9 +83,9 @@ export function GameModeSection() { {unwrap(provisions.name)} ); })} + {gameDefinitions.roles[value].consumables.map((id) => { const consumable = consumableDefinitions.consumables[id]; @@ -103,9 +104,9 @@ export function GameModeSection() { {unwrap(consumable.name)} index); const consumableDefinitions = await awaitableConsumableDefinitions; const provisionDefinitions = await awaitableProvisionDefinitions; +const gameDefinitions = await awaitableGameDefinitions; export async function filterTank( filters: TankFilters, @@ -72,27 +74,15 @@ export async function filterTank( ) )) && (filters.abilities.length === 0 || - filters.abilities.every((ability) => - tank.turrets.some((turret) => - turret.guns.some((gun) => - checkConsumableProvisionInclusivity( - consumableDefinitions.consumables[ability], - tank, - gun - ) - ) + Object.values(tank.roles).some((id) => + gameDefinitions.roles[id as unknown as number].consumables.some( + (consumable) => filters.abilities.includes(consumable) ) )) && (filters.powers.length === 0 || - filters.powers.every((power) => - tank.turrets.some((turret) => - turret.guns.some((gun) => - checkConsumableProvisionInclusivity( - provisionDefinitions.provisions[power], - tank, - gun - ) - ) + Object.values(tank.roles).some((id) => + gameDefinitions.roles[id as unknown as number].provisions.some( + (consumable) => filters.powers.includes(consumable) ) )) ); From 8f54bd597117ca504b99444e3c7fc0aa144ff054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:39:45 -0600 Subject: [PATCH 46/55] fix: use role filters --- .../TankSearch/components/FilterControl.tsx | 238 +++++++++++------- 1 file changed, 151 insertions(+), 87 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 61fa56580..fc51ff253 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -26,10 +26,11 @@ import { type FlexProps, } from "@radix-ui/themes"; import { times } from "lodash-es"; -import type { ComponentProps, ReactNode } from "react"; +import { Fragment, type ComponentProps, type ReactNode } from "react"; import { awaitableConsumableDefinitions } from "../../../core/awaitables/consumableDefinitions"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { awaitableProvisionDefinitions } from "../../../core/awaitables/provisionDefinitions"; +import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions"; import { useLocale } from "../../../hooks/useLocale"; import { App } from "../../../stores/app"; import { TankFilters, type CaseType } from "../../../stores/tankFilters"; @@ -45,6 +46,59 @@ import { ScienceOffIcon } from "../../ScienceOffIcon"; const gameDefinitions = await awaitableGameDefinitions; const consumableDefinitions = await awaitableConsumableDefinitions; const provisionDefinitions = await awaitableProvisionDefinitions; +const tankDefinitions = await awaitableTankDefinitions; + +const gameModeRoleSets: Record> = {}; + +for (const tankIdString in tankDefinitions.tanks) { + const gameMode = tankDefinitions.tanks[tankIdString]; + + for (const gameModeIdString in gameMode.roles) { + const role = gameMode.roles[gameModeIdString]; + const gameModeId = Number(gameModeIdString); + + if (gameModeId in gameModeRoleSets) { + gameModeRoleSets[gameModeId].add(role); + } else { + gameModeRoleSets[gameModeId] = new Set([role]); + } + } +} + +const gameModeRoles: { + gameModeId: number; + consumables: number[]; + provisions: number[]; +}[] = []; + +const allGameModeConsumables = new Set(); +const allGameModeProvisions = new Set(); + +for (const gameModeIdString in gameModeRoleSets) { + const gameModeId = Number(gameModeIdString); + const roles = gameModeRoleSets[gameModeId]; + const consumables = new Set(); + const provisions = new Set(); + + for (const roleId of roles) { + const role = gameDefinitions.roles[roleId]; + + for (const id of role.consumables) { + consumables.add(id); + allGameModeConsumables.add(id); + } + for (const id of role.provisions) { + provisions.add(id); + allGameModeProvisions.add(id); + } + } + + gameModeRoles.push({ + gameModeId: gameModeId, + consumables: Array.from(consumables.values()), + provisions: Array.from(provisions.values()), + }); +} const consumablesArray = Object.values(consumableDefinitions.consumables) .filter( @@ -53,18 +107,6 @@ const consumablesArray = Object.values(consumableDefinitions.consumables) consumable.name.locales[locales.default] ) .map((consumable) => consumable.id); -const abilitiesArray = Array.from( - Object.values(consumableDefinitions.consumables) - .filter((ability) => ability.game_mode_exclusive) - .reduce((uniqueMap, consumable) => { - if (!uniqueMap.has(consumable.name.locales[locales.default])) { - uniqueMap.set(consumable.name.locales[locales.default], consumable.id); - } - - return uniqueMap; - }, new Map()) - .values() -); const provisionsArray = Array.from( Object.values(provisionDefinitions.provisions) .filter( @@ -81,22 +123,6 @@ const provisionsArray = Array.from( }, new Map()) .values() ); -const powersArray = Array.from( - Object.values(provisionDefinitions.provisions) - .filter( - (provision) => - provision.game_mode_exclusive && provision.name.locales[locales.default] - ) - .reduce((uniqueMap, consumable) => { - if (!uniqueMap.has(consumable.name.locales[locales.default])) { - uniqueMap.set(consumable.name.locales[locales.default], consumable.id); - } - - return uniqueMap; - }, new Map()) - .values() -); - const shellTypeIcons: Record = { [ShellType.AP]: "ap", [ShellType.APCR]: "ap_cr", @@ -1008,7 +1034,10 @@ function ProvisionsFilter() { function PowersFilter() { const { unwrap, strings } = useLocale(); const rawPowers = TankFilters.use((state) => state.powers); - const powers = rawPowers.length === 0 ? powersArray : rawPowers; + const powers = + rawPowers.length === 0 + ? Array.from(allGameModeProvisions.values()) + : rawPowers; return ( @@ -1041,37 +1070,54 @@ function PowersFilter() { - {powersArray.map((power) => { - const selected = rawPowers.includes(power); - const powerDefinition = provisionDefinitions.provisions[power]; + {gameModeRoles.map(({ gameModeId, provisions }) => { + const gameMode = gameDefinitions.gameModes[gameModeId]; + + if (provisions.length === 0) return null; return ( - { - event.preventDefault(); + + {unwrap(gameMode.name)} - TankFilters.mutate((draft) => { - if (selected) { - draft.powers = draft.powers.filter((n) => n !== power); - } else { - draft.powers = [...draft.powers, power]; - } - }); - }} - checked={selected} - key={power} - > - + {provisions.map((provision) => { + { + const selected = rawPowers.includes(provision); + const powerDefinition = + provisionDefinitions.provisions[provision]; - {unwrap(powerDefinition.name)} - + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.powers = draft.powers.filter( + (n) => n !== provision + ); + } else { + draft.powers = [...draft.powers, provision]; + } + }); + }} + checked={selected} + key={provision} + > + + + {unwrap(powerDefinition.name)} + + ); + } + })} + ); })} @@ -1098,7 +1144,10 @@ function PowersFilter() { function AbilitiesFilter() { const { unwrap, strings } = useLocale(); const rawAbilities = TankFilters.use((state) => state.abilities); - const abilities = rawAbilities.length === 0 ? abilitiesArray : rawAbilities; + const abilities = + rawAbilities.length === 0 + ? Array.from(allGameModeConsumables.values()) + : rawAbilities; return ( @@ -1131,39 +1180,54 @@ function AbilitiesFilter() { - {abilitiesArray.map((ability) => { - const selected = rawAbilities.includes(ability); - const abilityDefinition = consumableDefinitions.consumables[ability]; + {gameModeRoles.map(({ gameModeId, consumables }) => { + const gameMode = gameDefinitions.gameModes[gameModeId]; + + if (consumables.length === 0) return null; return ( - { - event.preventDefault(); + + {unwrap(gameMode.name)} - TankFilters.mutate((draft) => { - if (selected) { - draft.abilities = draft.abilities.filter( - (n) => n !== ability - ); - } else { - draft.abilities = [...draft.abilities, ability]; - } - }); - }} - checked={selected} - key={ability} - > - + {consumables.map((consumable) => { + { + const selected = rawAbilities.includes(consumable); + const abilityDefinition = + consumableDefinitions.consumables[consumable]; - {unwrap(abilityDefinition.name)} - + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.abilities = draft.abilities.filter( + (n) => n !== consumable + ); + } else { + draft.abilities = [...draft.abilities, consumable]; + } + }); + }} + checked={selected} + key={consumable} + > + + + {unwrap(abilityDefinition.name)} + + ); + } + })} + ); })} From 8d4d730b118823ac11d644627fe02b61a681955b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:43:07 -0600 Subject: [PATCH 47/55] feat: logs --- docs/changelogs/1.29.9.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelogs/1.29.9.md b/docs/changelogs/1.29.9.md index 26740b924..5f8d4a78e 100644 --- a/docs/changelogs/1.29.9.md +++ b/docs/changelogs/1.29.9.md @@ -4,8 +4,9 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions ## Features +- Reworked tank search filters to be far more compact to make way for new filters - [Mark Greene] Added the ability to filter tanks by consumables -- Also added the ability to filter tanks by game-mode abilities and provisions +- Added the ability to filter tanks by provisions and game-mode abilities and powers ## Fixes From b4f452b0261910d39b031eb22805365069390aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:46:46 +0000 Subject: [PATCH 48/55] New translations en.json (French) --- packages/i18n/strings/fr.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/i18n/strings/fr.json b/packages/i18n/strings/fr.json index 9cceeb33c..1dcc88113 100644 --- a/packages/i18n/strings/fr.json +++ b/packages/i18n/strings/fr.json @@ -13,7 +13,8 @@ "hp": "{value}hp", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}°/s" + "deg_s": "{value}°/s", + "plus": "+{value}" }, "gun_types": { "auto_loader": "Chargement auto", @@ -22,8 +23,8 @@ }, "shells": { "ap": "AP", - "apcr": "APCR", - "heat": "HEAT", + "ap_cr": "APCR", + "hc": "HEAT", "he": "HE" }, "player_count_period": { @@ -668,6 +669,8 @@ "count_singular": "1 tank", "count_plural": "{count} tanks", "sorting_by": "Sorting by {name}", + "clear": "Clear", + "tier": "Tier {tier}", "no_tanks_found": { "body": "No tanks found.", "search_again": "Try searching again.", @@ -814,7 +817,13 @@ "next": "Next", "no_results": "No results" }, - "stats": "{correct} / {total} guessed correctly" + "stats": "{correct} / {total} guessed correctly", + "help": { + "title": "Partially reveal tank?", + "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", + "cancel": "Cancel", + "reveal": "Reveal" + } }, "tankopedia": { "search": "Search a stat...", @@ -1135,10 +1144,12 @@ "upgrade": "Upgrade" }, "provisions": { - "clear": "Clear" + "clear": "Clear", + "clear_all": "Clear all tanks" }, "consumables": { - "clear": "Clear" + "clear": "Clear", + "clear_all": "Clear all tanks" } } }, From 8ed97a3173c4e74aa24777bcc2a2de62598894d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:46:47 +0000 Subject: [PATCH 49/55] New translations en.json (Spanish) --- packages/i18n/strings/es.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/i18n/strings/es.json b/packages/i18n/strings/es.json index ee04808ee..c0f7e18df 100644 --- a/packages/i18n/strings/es.json +++ b/packages/i18n/strings/es.json @@ -13,7 +13,8 @@ "hp": "{value}hp", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}°/s" + "deg_s": "{value}°/s", + "plus": "+{value}" }, "gun_types": { "auto_loader": "Auto cargador", @@ -22,8 +23,8 @@ }, "shells": { "ap": "AP", - "apcr": "APCR", - "heat": "AEAC", + "ap_cr": "APCR", + "hc": "HEAT", "he": "AE" }, "player_count_period": { @@ -668,6 +669,8 @@ "count_singular": "1 tanque", "count_plural": "{count} tanks", "sorting_by": "Sorting by {name}", + "clear": "Limpiar", + "tier": "Tier {tier}", "no_tanks_found": { "body": "No se encontraron tanques.", "search_again": "Intenta buscar de nuevo.", @@ -814,7 +817,13 @@ "next": "Next", "no_results": "Sin resultados" }, - "stats": "{correct} / {total} guessed correctly" + "stats": "{correct} / {total} correct, {streak} streak", + "help": { + "title": "Partially reveal tank?", + "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", + "cancel": "Cancelar", + "reveal": "Reveal" + } }, "tankopedia": { "search": "Search a stat...", @@ -1135,10 +1144,12 @@ "upgrade": "Mejorar" }, "provisions": { - "clear": "Limpiar" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" }, "consumables": { - "clear": "Limpiar" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" } } }, From 66f7a8d369d7e7c9c7ee80a8ed0f25e4dec5da2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:46:49 +0000 Subject: [PATCH 50/55] New translations en.json (Japanese) --- packages/i18n/strings/ja.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/i18n/strings/ja.json b/packages/i18n/strings/ja.json index fa5ee2138..1dca6f499 100644 --- a/packages/i18n/strings/ja.json +++ b/packages/i18n/strings/ja.json @@ -13,7 +13,8 @@ "hp": "{value}hp", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}°/s" + "deg_s": "{value}°/s", + "plus": "+{value}" }, "gun_types": { "auto_loader": "自動装填砲", @@ -22,8 +23,8 @@ }, "shells": { "ap": "AP", - "apcr": "APCR", - "heat": "HEAT", + "ap_cr": "APCR", + "hc": "HEAT", "he": "HE" }, "player_count_period": { @@ -668,6 +669,8 @@ "count_singular": "1 輌", "count_plural": "{count} tanks", "sorting_by": "Sorting by {name}", + "clear": "クリア", + "tier": "Tier {tier}", "no_tanks_found": { "body": "該当する車輌が存在しません。", "search_again": "もう一度検索してください。", @@ -814,7 +817,13 @@ "next": "Next", "no_results": "該当なし" }, - "stats": "{correct} / {total} guessed correctly" + "stats": "{correct} / {total} correct, {streak} streak", + "help": { + "title": "Partially reveal tank?", + "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", + "cancel": "Cancel", + "reveal": "Reveal" + } }, "tankopedia": { "search": "Search a stat...", @@ -1135,10 +1144,12 @@ "upgrade": "最終装備" }, "provisions": { - "clear": "クリア" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" }, "consumables": { - "clear": "クリア" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" } } }, From b55924ce2b692dbc012f4ae4cac3607819a387da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:46:50 +0000 Subject: [PATCH 51/55] New translations en.json (Polish) --- packages/i18n/strings/pl.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/i18n/strings/pl.json b/packages/i18n/strings/pl.json index 1d5b0f9ed..67acd7f24 100644 --- a/packages/i18n/strings/pl.json +++ b/packages/i18n/strings/pl.json @@ -13,7 +13,8 @@ "hp": "{value}hp", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}°/s" + "deg_s": "{value}°/s", + "plus": "+{value}" }, "gun_types": { "auto_loader": "Auto loader", @@ -22,8 +23,8 @@ }, "shells": { "ap": "AP", - "apcr": "APCR", - "heat": "HEAT", + "ap_cr": "APCR", + "hc": "HEAT", "he": "OB" }, "player_count_period": { @@ -668,6 +669,8 @@ "count_singular": "1 tank", "count_plural": "{count} tanks", "sorting_by": "Sorting by {name}", + "clear": "Clear", + "tier": "Tier {tier}", "no_tanks_found": { "body": "No tanks found.", "search_again": "Try searching again.", @@ -814,7 +817,13 @@ "next": "Next", "no_results": "Brak wyników" }, - "stats": "{correct} / {total} guessed correctly" + "stats": "{correct} / {total} correct, {streak} streak", + "help": { + "title": "Partially reveal tank?", + "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", + "cancel": "Cancel", + "reveal": "Reveal" + } }, "tankopedia": { "search": "Search a stat...", @@ -1135,10 +1144,12 @@ "upgrade": "Upgrade" }, "provisions": { - "clear": "Clear" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" }, "consumables": { - "clear": "Clear" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" } } }, From d8748c028ead0701088a200f07c03a974e11f1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:46:51 +0000 Subject: [PATCH 52/55] New translations en.json (Portuguese) --- packages/i18n/strings/pt.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/i18n/strings/pt.json b/packages/i18n/strings/pt.json index 5fa7ec4d1..29c85cc3c 100644 --- a/packages/i18n/strings/pt.json +++ b/packages/i18n/strings/pt.json @@ -13,7 +13,8 @@ "hp": "{value}hp", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}°/s" + "deg_s": "{value}°/s", + "plus": "+{value}" }, "gun_types": { "auto_loader": "Autocarregador", @@ -22,8 +23,8 @@ }, "shells": { "ap": "AP", - "apcr": "APCR", - "heat": "HEAT", + "ap_cr": "APCR", + "hc": "HEAT", "he": "HE" }, "player_count_period": { @@ -668,6 +669,8 @@ "count_singular": "1 tanque", "count_plural": "{count} tanks", "sorting_by": "Sorting by {name}", + "clear": "Limpar", + "tier": "Tier {tier}", "no_tanks_found": { "body": "Nenhum tanque encontrado.", "search_again": "Tente pesquisar novamente.", @@ -814,7 +817,13 @@ "next": "Next", "no_results": "Sem resultados" }, - "stats": "{correct} / {total} guessed correctly" + "stats": "{correct} / {total} correct, {streak} streak", + "help": { + "title": "Partially reveal tank?", + "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", + "cancel": "Cancelar", + "reveal": "Reveal" + } }, "tankopedia": { "search": "Search a stat...", @@ -1135,10 +1144,12 @@ "upgrade": "Melhorar" }, "provisions": { - "clear": "Limpar" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" }, "consumables": { - "clear": "Limpar" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" } } }, From 24ff42a6727ed80527c6bb3d9086f5fc320ea920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:46:52 +0000 Subject: [PATCH 53/55] New translations en.json (Russian) --- packages/i18n/strings/ru.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/i18n/strings/ru.json b/packages/i18n/strings/ru.json index ff72db6df..29283091c 100644 --- a/packages/i18n/strings/ru.json +++ b/packages/i18n/strings/ru.json @@ -13,7 +13,8 @@ "hp": "{value}оп", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}°/с" + "deg_s": "{value}°/с", + "plus": "+{value}" }, "gun_types": { "auto_loader": "Барабан", @@ -22,8 +23,8 @@ }, "shells": { "ap": "ББ", - "apcr": "БП", - "heat": "КС", + "ap_cr": "APCR", + "hc": "HEAT", "he": "ОФ" }, "player_count_period": { @@ -668,6 +669,8 @@ "count_singular": "1 танк", "count_plural": "Танков: {count}", "sorting_by": "Сортировать по {name}", + "clear": "Очистить", + "tier": " {tier} уровня", "no_tanks_found": { "body": "Танков не найдено.", "search_again": "Попробуйте повторить поиск.", @@ -814,7 +817,13 @@ "next": "Следующий", "no_results": "Ничего не найденo" }, - "stats": "{correct} / {total} угадано верно" + "stats": "{correct} / {total} правильно, серия: {streak}", + "help": { + "title": "Частично раскрыть танк?", + "description": "Показывает танк не показывая название. Этот раунд будет считаться проигранным и прекратит серию!", + "cancel": "Отмена", + "reveal": "Раскрыть" + } }, "tankopedia": { "search": "Поиск характеристик...", @@ -1135,10 +1144,12 @@ "upgrade": "Топ" }, "provisions": { - "clear": "Очистить" + "clear": "Убрать", + "clear_all": "Убрать со всех" }, "consumables": { - "clear": "Очистить" + "clear": "Убрать", + "clear_all": "Убрать со всех" } } }, From ee4141838c16b79426ee001b9eb840023ada5c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:46:53 +0000 Subject: [PATCH 54/55] New translations en.json (Ukrainian) --- packages/i18n/strings/uk.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/i18n/strings/uk.json b/packages/i18n/strings/uk.json index 5607cc26e..8e43fdf26 100644 --- a/packages/i18n/strings/uk.json +++ b/packages/i18n/strings/uk.json @@ -13,7 +13,8 @@ "hp": "{value}од", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}град/c" + "deg_s": "{value}град/c", + "plus": "+{value}" }, "gun_types": { "auto_loader": "Магазинна система заряджання", @@ -22,8 +23,8 @@ }, "shells": { "ap": "ББ", - "apcr": "БП", - "heat": "КС", + "ap_cr": "APCR", + "hc": "HEAT", "he": "ОФ" }, "player_count_period": { @@ -668,6 +669,8 @@ "count_singular": "1 танк", "count_plural": "Танків: {count} ", "sorting_by": "Відсортувати за {name}", + "clear": "Clear", + "tier": "Рівень {tier}", "no_tanks_found": { "body": "Нічого не знайдено.", "search_again": "Спробуйте шукати заново.", @@ -814,7 +817,13 @@ "next": "Next", "no_results": "Немає результатів" }, - "stats": "{correct} / {total} guessed correctly" + "stats": "{correct} / {total} correct, {streak} streak", + "help": { + "title": "Partially reveal tank?", + "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", + "cancel": "Cancel", + "reveal": "Reveal" + } }, "tankopedia": { "search": "Search a stat...", @@ -1135,10 +1144,12 @@ "upgrade": "Upgrade" }, "provisions": { - "clear": "Clear" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" }, "consumables": { - "clear": "Clear" + "clear": "Clear this tank", + "clear_all": "Clear all tanks" } } }, From 4c8e5cc8ed36699e20b214d96df0bc823aa09fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:46:54 +0000 Subject: [PATCH 55/55] New translations en.json (Chinese Simplified) --- packages/i18n/strings/zh.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/i18n/strings/zh.json b/packages/i18n/strings/zh.json index f56585bf1..42a479522 100644 --- a/packages/i18n/strings/zh.json +++ b/packages/i18n/strings/zh.json @@ -13,7 +13,8 @@ "hp": "{value} 生命值", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}度°/秒" + "deg_s": "{value}度°/秒", + "plus": "+{value}" }, "gun_types": { "auto_loader": "弹夹型坦克", @@ -22,8 +23,8 @@ }, "shells": { "ap": "穿甲弹", - "apcr": "硬式复合穿甲弹", - "heat": "反战车高爆弹", + "ap_cr": "APCR", + "hc": "HEAT", "he": "高爆弹" }, "player_count_period": { @@ -668,6 +669,8 @@ "count_singular": "1辆战车", "count_plural": "{count} 辆坦克", "sorting_by": "按 {name} 排序", + "clear": "清除", + "tier": "{tier} 阶", "no_tanks_found": { "body": "找不到战车。", "search_again": "请尝试重新搜索。", @@ -814,7 +817,13 @@ "next": "下一个", "no_results": "沒有結果" }, - "stats": "猜对了 {correct} / {total}" + "stats": "猜对了 {correct} / {total}", + "help": { + "title": "Partially reveal tank?", + "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", + "cancel": "取消", + "reveal": "Reveal" + } }, "tankopedia": { "search": "搜索属性……", @@ -1135,10 +1144,12 @@ "upgrade": "顶级配置" }, "provisions": { - "clear": "清除" + "clear": "清除", + "clear_all": "Clear all tanks" }, "consumables": { - "clear": "清除" + "clear": "清除", + "clear_all": "Clear all tanks" } } },