diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7c8a532cb..ec47aeb32 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -19,6 +19,7 @@
"betakrieg",
"BINORMAL",
"Binormals",
+ "bkni",
"BKRL",
"BKRTS",
"BKRTSMANIFEST",
@@ -87,6 +88,7 @@
"loddist",
"lods",
"lossrate",
+ "malum",
"mantlets",
"metallicness",
"minitelrose",
@@ -136,6 +138,7 @@
"Specularity",
"SSAO",
"startcase",
+ "statkit",
"statsfull",
"steamapps",
"Stringifying",
diff --git a/docs/technical/bkni.md b/docs/technical/bkni.md
index 2f7b6f507..78383ad9a 100644
--- a/docs/technical/bkni.md
+++ b/docs/technical/bkni.md
@@ -5,9 +5,9 @@
> [!CAUTION]
> This is a working draft and is incomplete. Do not implement.
-## Why BkPM over WN8?
+## Why BkNI over WN8?
-BkPM addresses a lot of issues with WN8:
+BkNI addresses a lot of issues with WN8:
- WN8 incorporates win rate into the metric; a metric like this must be a correlate of win rate, not a dependent
- The weights and biases are not dynamic and their formulation is not transparent
diff --git a/src/app/tools/player-stats/[id]/page.tsx b/src/app/tools/player-stats/[id]/page.tsx
new file mode 100644
index 000000000..6a4478676
--- /dev/null
+++ b/src/app/tools/player-stats/[id]/page.tsx
@@ -0,0 +1,71 @@
+import { Flex, Heading, Text } from '@radix-ui/themes';
+import PageWrapper from '../../../../components/PageWrapper';
+import { getAccountInfo } from '../../../../core/blitz/getAccountInfo';
+import { getClanAccountInfo } from '../../../../core/blitz/getClanAccountInfo';
+import { idToRegion } from '../../../../core/blitz/idToRegion';
+import getBkniPercentile from '../../../../core/statistics/getBkniPercentile';
+import strings from '../../../../lang/en-US.json';
+
+export default async function Page({ params }: { params: { id: string } }) {
+ const id = parseInt(params.id);
+ const region = idToRegion(id);
+ const accountInfo = await getAccountInfo(region, id);
+ const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
+ const bkniRaw = Math.random() * 2 - 1;
+ const bkniFraction = Math.random();
+ const bkniMetric = Math.round(bkniFraction * 200);
+ const bkniPercentile = getBkniPercentile(bkniMetric);
+
+ return (
+
+
+ {accountInfo.nickname}
+ {clanAccountInfo?.clan && (
+ [{clanAccountInfo.clan.tag}]
+ )}
+
+
+
+
+
+
+
+ {bkniMetric}
+
+
+ {strings.common.bkni_percentile[bkniPercentile]}
+
+
+
+
+ );
+}
diff --git a/src/app/tools/player-stats/page.tsx b/src/app/tools/player-stats/page.tsx
new file mode 100644
index 000000000..2ad419791
--- /dev/null
+++ b/src/app/tools/player-stats/page.tsx
@@ -0,0 +1,110 @@
+'use client';
+
+import { ArrowDownIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
+import { Button, Card, Flex, Text, TextField } from '@radix-ui/themes';
+import { useRef, useState } from 'react';
+import { Link } from '../../../components/Link';
+import PageWrapper from '../../../components/PageWrapper';
+import { UNLOCALIZED_REGION_NAMES_SHORT } from '../../../constants/regions';
+import searchPlayersAcrossRegions, {
+ AccountListWithServer,
+} from '../../../core/blitz/searchPlayersAcrossRegions';
+
+export default function Page() {
+ const input = useRef(null);
+ const [showSearch, setShowSearch] = useState(false);
+ const [searching, setSearching] = useState(false);
+ const [searchResults, setSearchResults] = useState([]);
+
+ async function search() {
+ if (!input.current) return;
+
+ setSearchResults(await searchPlayersAcrossRegions(input.current.value, 9));
+ setSearching(false);
+ }
+
+ return (
+
+
+
+ Look up a player to get started
+
+
+
+ {
+ if (!input.current) return;
+
+ const sanitized = input.current.value.trim();
+
+ if (sanitized.length > 0) {
+ setShowSearch(true);
+ setSearching(true);
+ search();
+ } else {
+ setShowSearch(false);
+ }
+ }}
+ >
+
+
+
+
+ {showSearch && (
+
+ {searching && (
+
+ Searching...
+
+ )}
+
+ {!searching && searchResults.length === 0 && (
+
+ No players found
+
+ )}
+
+ {!searching && searchResults.length > 0 && (
+
+ {searchResults.map((player) => (
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/constants/tools.tsx b/src/constants/tools.tsx
index a5f1cff41..1808df49a 100644
--- a/src/constants/tools.tsx
+++ b/src/constants/tools.tsx
@@ -31,9 +31,9 @@ export const TOOLS: Tool[] = [
significant: true,
},
{
- id: 'statistics',
- title: 'Statistics',
- description: 'Player statistics',
+ id: 'player-stats',
+ title: 'Player stats',
+ description: 'Periodical player statistics',
pageDescription:
'Periodical player statistics and tanks played in World of Tanks Blitz',
image: '8gJGkKP',
diff --git a/src/core/statistics/getBkniPercentile.ts b/src/core/statistics/getBkniPercentile.ts
new file mode 100644
index 000000000..1989b2606
--- /dev/null
+++ b/src/core/statistics/getBkniPercentile.ts
@@ -0,0 +1,46 @@
+/**
+ * (10) 0 - 10: super malum
+ * (10) 10 - 20: malum
+ * (20) 20 - 40: very bad
+ * (20) 40 - 60: bad
+ * (20) 60 - 80: below average
+ * (40) 80 - 120: average
+ * (20) 120 - 140: above average
+ * (20) 140 - 160: good
+ * (20) 160 - 180: very good
+ * (10) 180 - 190: unicum
+ * (10) 190 - 200: super unicum
+ */
+
+export enum BkniPercentile {
+ SuperMalum,
+ Malum,
+ VeryBad,
+ Bad,
+ BelowAverage,
+ Average,
+ AboveAverage,
+ Good,
+ VeryGood,
+ Unicum,
+ SuperUnicum,
+}
+
+const BKNI_PERCENTILES = [
+ { min: 0, percentile: BkniPercentile.SuperMalum },
+ { min: 10, percentile: BkniPercentile.Malum },
+ { min: 20, percentile: BkniPercentile.VeryBad },
+ { min: 40, percentile: BkniPercentile.Bad },
+ { min: 60, percentile: BkniPercentile.BelowAverage },
+ { min: 80, percentile: BkniPercentile.Average },
+ { min: 120, percentile: BkniPercentile.AboveAverage },
+ { min: 140, percentile: BkniPercentile.Good },
+ { min: 160, percentile: BkniPercentile.VeryGood },
+ { min: 180, percentile: BkniPercentile.Unicum },
+ { min: 190, percentile: BkniPercentile.SuperUnicum },
+];
+
+export default function getBkniPercentile(wn8: number) {
+ const lastIndex = BKNI_PERCENTILES.findLastIndex(({ min }) => min <= wn8);
+ return BKNI_PERCENTILES[lastIndex === -1 ? 0 : lastIndex].percentile;
+}
diff --git a/src/lang/en-US.json b/src/lang/en-US.json
index bd88ee6da..5730a1157 100644
--- a/src/lang/en-US.json
+++ b/src/lang/en-US.json
@@ -13,6 +13,20 @@
"9": "Super unicum"
},
+ "bkni_percentile": {
+ "0": "Super malum",
+ "1": "Malum",
+ "2": "Very bad",
+ "3": "Bad",
+ "4": "Below average",
+ "5": "Average",
+ "6": "Above average",
+ "7": "Good",
+ "8": "Very good",
+ "9": "Unicum",
+ "10": "Super unicum"
+ },
+
"nations": {
"ussr": "U.S.S.R.",
"germany": "Germany",