diff --git a/bun.lock b/bun.lock
index 79e088ab6..8c5daddbe 100644
--- a/bun.lock
+++ b/bun.lock
@@ -118,8 +118,9 @@
"packages/varuna": {
"name": "varuna",
"dependencies": {
+ "@types/react": "^19.2.2",
"immer": "^10.1.3",
- "react": "^19.1.1",
+ "react": "^19.2.0",
},
},
"packages/website": {
@@ -2880,6 +2881,10 @@
"unbzip2-stream/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
+ "varuna/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
+
+ "varuna/react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
+
"vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json
index 0af384bb6..9a7815e7c 100644
--- a/packages/i18n/strings/en.json
+++ b/packages/i18n/strings/en.json
@@ -860,6 +860,11 @@
"tools": {
"playlist": {
+ "table": {
+ "tank": "Tank"
+ },
+ "no_login_warning": "You are not logged in. BlitzKit will not keep track of your played tanks.",
+ "generate": "Generate playlist",
"name": "Playlist",
"description": "Tank playlist generator",
"seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!",
diff --git a/packages/varuna/package.json b/packages/varuna/package.json
index ef1e420e5..afc1716cb 100644
--- a/packages/varuna/package.json
+++ b/packages/varuna/package.json
@@ -3,7 +3,8 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
+ "@types/react": "^19.2.2",
"immer": "^10.1.3",
- "react": "^19.1.1"
+ "react": "^19.2.0"
}
}
diff --git a/packages/website/src/components/PlaylistFilters.tsx b/packages/website/src/components/PlaylistFilters.tsx
index 8df4cc594..5f8e83fb6 100644
--- a/packages/website/src/components/PlaylistFilters.tsx
+++ b/packages/website/src/components/PlaylistFilters.tsx
@@ -7,12 +7,13 @@ import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
import { filterTanks } from "../core/blitzkit/filterTanks";
import { useLocale } from "../hooks/useLocale";
import { TankFilters } from "../stores/tankFilters";
+import { LinkI18n } from "./LinkI18n";
import { FilterControl } from "./TankSearch/components/FilterControl";
const tankDefinitions = await awaitableTankDefinitions;
export function PlaylistPool() {
- const { strings } = useLocale();
+ const { strings, locale } = useLocale();
const filters = TankFilters.use();
const randomTankList = useMemo(() => {
const list = Object.values(tankDefinitions.tanks);
@@ -52,7 +53,10 @@ export function PlaylistPool() {
width="100%"
>
{tanksTrimmed.map((tank) => (
-
diff --git a/packages/website/src/components/PlaylistGenerator.tsx b/packages/website/src/components/PlaylistGenerator.tsx
new file mode 100644
index 000000000..e8a968c1d
--- /dev/null
+++ b/packages/website/src/components/PlaylistGenerator.tsx
@@ -0,0 +1,34 @@
+import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
+import { Button, Flex, Text } from "@radix-ui/themes";
+import { generatePlaylist } from "../core/blitzkit/generatePlaylist";
+import { useLocale } from "../hooks/useLocale";
+import { App } from "../stores/app";
+
+export function PlaylistGenerator() {
+ const { strings } = useLocale();
+ const wargaming = App.use((state) => state.logins.wargaming);
+
+ return (
+
+
+
+ {!wargaming && (
+
+
+
+ {strings.website.tools.playlist.no_login_warning}
+
+
+ )}
+
+ );
+}
diff --git a/packages/website/src/components/PlaylistOrder.tsx b/packages/website/src/components/PlaylistOrder.tsx
new file mode 100644
index 000000000..e1439384f
--- /dev/null
+++ b/packages/website/src/components/PlaylistOrder.tsx
@@ -0,0 +1,38 @@
+import { Checkbox, Flex, Table } from "@radix-ui/themes";
+import { useLocale } from "../hooks/useLocale";
+import { Playlist } from "../stores/playlist";
+import { TankRowHeaderCell } from "./TankRowHeaderCell";
+
+export function PlaylistOrder() {
+ const { unwrap, strings } = useLocale();
+ const list = Playlist.use((state) => state.list);
+
+ if (!list) return null;
+
+ return (
+
+
+
+
+
+
+ {strings.website.tools.playlist.table.tank}
+
+
+
+
+
+ {list.map(({ tank }) => (
+
+
+ 0.5} />
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/packages/website/src/core/blitzkit/generatePlaylist.ts b/packages/website/src/core/blitzkit/generatePlaylist.ts
new file mode 100644
index 000000000..b47235906
--- /dev/null
+++ b/packages/website/src/core/blitzkit/generatePlaylist.ts
@@ -0,0 +1,40 @@
+import {
+ getTankStats,
+ idToRegion,
+ type IndividualTankStats,
+} from "@blitzkit/core";
+import { App } from "../../stores/app";
+import { Playlist } from "../../stores/playlist";
+import { TankFilters } from "../../stores/tankFilters";
+import { awaitableTankDefinitions } from "../awaitables/tankDefinitions";
+import { filterTanks } from "./filterTanks";
+
+const tankDefinitions = await awaitableTankDefinitions;
+const tanks = Object.values(tankDefinitions.tanks);
+
+export async function generatePlaylist() {
+ const id = App.state.logins.wargaming?.id;
+ const filtered = await filterTanks(TankFilters.state, tanks);
+ const statsList = id ? await getTankStats(idToRegion(id), id) : undefined;
+ const stats: Record = {};
+
+ if (statsList) {
+ for (const tankStat of statsList) {
+ stats[tankStat.tank_id] = tankStat;
+ }
+ }
+
+ for (let i = filtered.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [filtered[i], filtered[j]] = [filtered[j], filtered[i]];
+ }
+
+ Playlist.mutate((draft) => {
+ draft.list = filtered.map((tank, index) => ({
+ index,
+ tank,
+ battles: stats[tank.id]?.all.battles,
+ wins: stats[tank.id]?.all.wins,
+ }));
+ });
+}
diff --git a/packages/website/src/core/blitzkit/pwa.ts b/packages/website/src/core/blitzkit/pwa.ts
deleted file mode 100644
index e69de29bb..000000000
diff --git a/packages/website/src/pages/[...locale]/playlist/_index.tsx b/packages/website/src/pages/[...locale]/playlist/_index.tsx
index 7aa84a8aa..ce45efe53 100644
--- a/packages/website/src/pages/[...locale]/playlist/_index.tsx
+++ b/packages/website/src/pages/[...locale]/playlist/_index.tsx
@@ -1,6 +1,12 @@
+import { useEffect } from "react";
import { PageWrapper } from "../../../components/PageWrapper";
import { PlaylistPool } from "../../../components/PlaylistFilters";
+import { PlaylistGenerator } from "../../../components/PlaylistGenerator";
+import { PlaylistOrder } from "../../../components/PLaylistOrder";
import { LocaleProvider } from "../../../hooks/useLocale";
+import { App } from "../../../stores/app";
+import { Playlist } from "../../../stores/playlist";
+import { TankFilters } from "../../../stores/tankFilters";
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
interface Props {
@@ -11,10 +17,23 @@ export function Page({
locale,
skeleton,
}: Props & MaybeSkeletonComponentProps) {
+ const list = Playlist.use((state) => state.list);
+ const wargaming = App.use((state) => state.logins.wargaming);
+
+ useEffect(() => {
+ if (wargaming) {
+ TankFilters.mutate((draft) => {
+ draft.ownership = "owned";
+ });
+ }
+ }, []);
+
return (
-
+
+ {!list && }
+ {list && }
);
diff --git a/packages/website/src/stores/playlist.ts b/packages/website/src/stores/playlist.ts
new file mode 100644
index 000000000..2084e15e7
--- /dev/null
+++ b/packages/website/src/stores/playlist.ts
@@ -0,0 +1,13 @@
+import type { TankDefinition } from "@blitzkit/core";
+import { Varuna } from "varuna";
+
+export interface Playlist {
+ list?: {
+ index: number;
+ tank: TankDefinition;
+ battles?: number;
+ wins?: number;
+ }[];
+}
+
+export const Playlist = new Varuna({}, "playlist");