Compare commits

..
10 Commits
59 changed files with 419 additions and 475 deletions

No files matched your search

+6 -3
View File
@@ -1,4 +1,4 @@
name: Assets
name: Build Assets
on:
push:
@@ -21,7 +21,7 @@ on:
description: Root dir of ZIP which houses the executable and Data dir
schedule:
- cron: "0 2 * * *"
- cron: '0 2 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.cdn_branch }}
@@ -67,7 +67,7 @@ jobs:
with:
root-reserve-mb: 512
swap-size-mb: 1024
remove-dotnet: "true"
remove-dotnet: 'true'
- uses: actions/checkout@v4
with:
@@ -131,6 +131,9 @@ jobs:
- name: Install dependencies
run: bun install
- name: Lint
run: bun run lint:website
- name: Download depots
uses: actions/download-artifact@v4
with:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Bot
name: Build Bot
on:
push:
+2 -2
View File
@@ -1,9 +1,9 @@
name: IDs
name: Discover IDs
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 1"
- cron: '0 0 * * 1'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
+2 -2
View File
@@ -1,10 +1,10 @@
name: Host
name: Host Bot
on:
push:
branches: main
schedule:
- cron: "0 */5 * * *"
- cron: '0 */5 * * *'
workflow_dispatch:
release:
types: [created, published, released, edited]
+1 -1
View File
@@ -5,7 +5,7 @@ on:
workflow_dispatch:
schedule:
- cron: "0 4 * * *"
- cron: '0 4 * * *'
permissions:
contents: read
-1
View File
@@ -92,7 +92,6 @@
"GLTF",
"gtag",
"haha",
"Herdbreaker",
"highp",
"iife",
"imgur",
+5 -9
View File
@@ -4,7 +4,6 @@
"": {
"name": "blitzkit",
"dependencies": {
"@prisma/client": "^6.17.1",
"@types/xmpp__client": "^0.13.3",
"@xmpp/client": "^0.13.6",
"astro-favicons": "^3.1.5",
@@ -14,7 +13,6 @@
"protobufjs": "^7.5.4",
"quicklime": "^1.2.0",
"sharp": "^0.34.4",
"yaml": "^2.8.1",
},
"devDependencies": {
"@types/bun": "^1.2.22",
@@ -57,24 +55,22 @@
"packages/core": {
"name": "@blitzkit/core",
"dependencies": {
"@blitzkit/i18n": "workspace:*",
"@bufbuild/protobuf": "^2.9.0",
"@octokit/plugin-throttling": "^11.0.1",
"@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2",
"@types/adm-zip": "^0.5.7",
"@types/lz4js": "^0.2.1",
"@types/three": "^0.180.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^17.2.2",
"lz4js": "^0.2.0",
"pickleparser": "^0.2.1",
"protobufjs": "^7.5.4",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1",
},
"devDependencies": {
"@types/adm-zip": "^0.5.7",
"@types/lz4js": "^0.2.1",
},
},
"packages/i18n": {
"name": "@blitzkit/i18n",
@@ -573,7 +569,7 @@
"@oslojs/encoding": ["@oslojs/[email protected]", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
"@prisma/client": ["@prisma/[email protected]7.1", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw=="],
"@prisma/client": ["@prisma/[email protected]6.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw=="],
"@prisma/config": ["@prisma/[email protected]", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg=="],
-10
View File
@@ -1,10 +0,0 @@
# BlitzKit 1.28.4
## Fixes
- The tank names in the Tankopedia sandbox now have less margin from the navbar
- Controls in the Tankopedia sandbox are now shown in all modes, except for the advanced highlighting button which only appears in the dynamic armor mode
- The settings page now reacts to input again
- The settings page now has a tab title and description for search engines
- Added support for transparent textures, fixing tanks like the Herdbreaker and the WZ-132-1
- The tank filters in the Performance and Tier-List tools work once again
+3 -5
View File
@@ -1,7 +1,7 @@
{
"name": "blitzkit",
"private": true,
"version": "1.28.4",
"version": "1.28.3",
"description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module",
"types": "./types.d.ts",
@@ -12,7 +12,7 @@
"build:assets": "cd packages/scripts && bun run build:assets",
"build:averages": "bun --filter=@blitzkit/scripts run build:averages",
"build:bot": "cd packages/scripts && bun run build:bot",
"build:website": "bun build:prisma && cd packages/website && bun run build",
"build:website": "bun build:prisma --no-hints && cd packages/website && bun run build",
"build:prisma": "prisma generate",
"build:sitemap": "bun scripts/sitemap.ts",
"build:docs": "vitepress build docs",
@@ -44,7 +44,6 @@
},
"license": "MIT",
"dependencies": {
"@prisma/client": "^6.17.1",
"@types/xmpp__client": "^0.13.3",
"@xmpp/client": "^0.13.6",
"astro-favicons": "^3.1.5",
@@ -53,8 +52,7 @@
"lodash-es": "^4.17.21",
"protobufjs": "^7.5.4",
"quicklime": "^1.2.0",
"sharp": "^0.34.4",
"yaml": "^2.8.1"
"sharp": "^0.34.4"
},
"devDependencies": {
"@types/bun": "^1.2.22",
+4 -6
View File
@@ -4,22 +4,20 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
"@blitzkit/i18n": "workspace:*",
"@bufbuild/protobuf": "^2.9.0",
"@octokit/plugin-throttling": "^11.0.1",
"@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2",
"@types/adm-zip": "^0.5.7",
"@types/lz4js": "^0.2.1",
"@types/three": "^0.180.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^17.2.2",
"lz4js": "^0.2.0",
"pickleparser": "^0.2.1",
"protobufjs": "^7.5.4",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1"
},
"devDependencies": {
"@types/adm-zip": "^0.5.7",
"@types/lz4js": "^0.2.1"
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { ShellDefinition, ShellType } from "../protos";
import { type ShellDefinition, ShellType } from '@blitzkit/core';
export function canSplash(type: ShellDefinition["type"]): type is ShellType.HE {
export function canSplash(type: ShellDefinition['type']): type is ShellType.HE {
return type === ShellType.HE;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { BlitzStats, calculateWN8 } from "../statistics";
import { BlitzStats, calculateWN8 } from '@blitzkit/core';
export type Stat = keyof ReturnType<typeof generateStats>;
@@ -1,6 +1,5 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
import { fetchBlitz, Region } from '@blitzkit/core';
import { normalizeIds } from './normalizeIds';
interface IndividualAccountAchievements {
achievements: {
@@ -23,11 +22,11 @@ export async function getAccountAchievements<
>(region: Region, ids: Ids) {
const object = await fetchBlitz<AccountAchievements>(
region,
"account/achievements",
{ account_id: normalizeIds(ids) }
'account/achievements',
{ account_id: normalizeIds(ids) },
);
if (typeof ids === "number") {
if (typeof ids === 'number') {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
+11 -8
View File
@@ -1,7 +1,10 @@
import { BlitzStats } from "../statistics";
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
import {
BlitzStats,
FetchBlitzParams,
Region,
fetchBlitz,
} from '@blitzkit/core';
import { normalizeIds } from './normalizeIds';
export interface SupplementaryStats {
WN8?: number;
@@ -80,15 +83,15 @@ export async function getAccountInfo<
region: Region,
ids: Ids,
extra: string[] = [],
params: FetchBlitzParams = {}
params: FetchBlitzParams = {},
) {
const object = await fetchBlitz<AccountInfo>(region, "account/info", {
const object = await fetchBlitz<AccountInfo>(region, 'account/info', {
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(","),
extra: extra.length === 0 ? undefined : extra.join(','),
...params,
});
if (typeof ids === "number") {
if (typeof ids === 'number') {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
@@ -1,6 +1,5 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
import { fetchBlitz, Region } from '@blitzkit/core';
import { normalizeIds } from './normalizeIds';
type IndividualClanAccountInfo = {
account_id: number;
@@ -31,14 +30,14 @@ export async function getClanAccountInfo<
>(region: Region, ids: Ids, extra: string[] = []) {
const object = await fetchBlitz<ClanAccountInfo>(
region,
"clans/accountinfo",
'clans/accountinfo',
{
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(","),
}
extra: extra.length === 0 ? undefined : extra.join(','),
},
);
if (typeof ids === "number") {
if (typeof ids === 'number') {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
+4 -5
View File
@@ -1,6 +1,5 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
import { fetchBlitz, Region } from '@blitzkit/core';
import { normalizeIds } from './normalizeIds';
interface IndividualClanInfo {
recruiting_options: {
@@ -39,11 +38,11 @@ export async function getClanInfo<
Ids extends number | number[],
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
>(region: Region, ids: Ids) {
const object = await fetchBlitz<ClanInfo>(region, "clans/info", {
const object = await fetchBlitz<ClanInfo>(region, 'clans/info', {
clan_id: normalizeIds(ids),
});
if (typeof ids === "number") {
if (typeof ids === 'number') {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
@@ -1,4 +1,4 @@
import { LEAGUES } from "./rating";
import { LEAGUES } from '@blitzkit/core';
export function getLeagueFromScore(score: number) {
return LEAGUES.find(({ minScore }) => score >= minScore)!;
+7 -8
View File
@@ -1,8 +1,7 @@
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import { RatingLeague, RatingReward } from "./rating";
import { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
import { RatingLeague, RatingReward, Region } from '@blitzkit/core';
import { context } from '../blitzkit/context';
import { patientFetch } from '../blitzkit/patientFetch';
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
export type RatingInfo =
| {
@@ -27,12 +26,12 @@ export async function getRatingInfo(region: Region) {
if (cache[region]) return cache[region]!;
const response = await patientFetch(
context === "website"
context === 'website'
? `/api/${region}/rating/current/info`
: `https://${regionToRegionSubdomain(
region
region,
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
undefined
undefined,
);
const data = (await response.json()) as RatingInfo;
+11 -13
View File
@@ -1,29 +1,27 @@
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import { RatingPlayer } from "../statistics";
import { LeagueTop } from "./rating";
import { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
import { LeagueTop, RatingPlayer, Region } from '@blitzkit/core';
import { context } from '../blitzkit/context';
import { patientFetch } from '../blitzkit/patientFetch';
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
export async function getRatingLeague(
region: Region,
league: number,
usePatientFetcher = false
usePatientFetcher = false,
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: "no-store" })
: fetch(url, { cache: "no-store" });
? patientFetch(url, undefined, { cache: 'no-store' })
: fetch(url, { cache: 'no-store' });
const response = await fetcher(
context === "website"
context === 'website'
? `/api/${region}/rating/current/league/${league}`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`
region,
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`,
);
const json = await response.json();
return context === "website"
return context === 'website'
? (json as RatingPlayer[])
: (json as LeagueTop).result;
}
+10 -11
View File
@@ -1,25 +1,24 @@
import { patientFetch } from "../blitzkit/patientFetch";
import { withCORSProxy } from "../blitzkit/withCORSProxy";
import { RatingNeighbors } from "./rating";
import { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
import { RatingNeighbors, Region } from '@blitzkit/core';
import { patientFetch } from '../blitzkit/patientFetch';
import { withCORSProxy } from '../blitzkit/withCORSProxy';
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
export async function getRatingNeighbors(
region: Region,
id: number,
count: number,
usePatientFetcher = false
usePatientFetcher = false,
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: "no-store" })
: fetch(url, { cache: "no-store" });
? patientFetch(url, undefined, { cache: 'no-store' })
: fetch(url, { cache: 'no-store' });
const response = await fetcher(
withCORSProxy(
`https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`
)
region,
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`,
),
);
const json = (await response.json()) as RatingNeighbors;
+8 -5
View File
@@ -1,13 +1,16 @@
import { TanksStats } from "../types";
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import { Region } from "./regions";
import {
fetchBlitz,
FetchBlitzParams,
Region,
TanksStats,
} from '@blitzkit/core';
export async function getTankStats(
region: Region,
id: number,
params?: FetchBlitzParams
params?: FetchBlitzParams,
) {
const tankStats = await fetchBlitz<TanksStats>(region, "tanks/stats", {
const tankStats = await fetchBlitz<TanksStats>(region, 'tanks/stats', {
account_id: id,
...params,
});
@@ -1,6 +1,6 @@
import { getRatingInfo } from "./getRatingInfo";
import { getRatingInfo } from '@blitzkit/core';
export async function isOnGoingRatingSeason() {
const ratingInfo = await getRatingInfo("com");
const ratingInfo = await getRatingInfo('com');
return ratingInfo.detail === undefined;
}
+8 -10
View File
@@ -1,6 +1,4 @@
import { assertSecret } from "../blitzkit";
import { idToRegion } from "./idToRegion";
import { Region } from "./regions";
import { Region, assertSecret, idToRegion } from '@blitzkit/core';
export async function isValidBlitzId(id: number, token?: string) {
let region: Region;
@@ -13,20 +11,20 @@ export async function isValidBlitzId(id: number, token?: string) {
const json = await fetch(
`https://api.wotblitz.${region}/wotb/account/info/?account_id=${id}&application_id=${assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID,
)}${
token === undefined ? "" : `&access_token=${token}`
}&fields=account_id%2C-account_id`
token === undefined ? '' : `&access_token=${token}`
}&fields=account_id%2C-account_id`,
).then(
(response) =>
response.json() as Promise<
| {
status: "ok";
status: 'ok';
data: Record<number, {} | null>;
}
| { status: "error" }
>
| { status: 'error' }
>,
);
return json.status === "ok" && json.data[id] !== null;
return json.status === 'ok' && json.data[id] !== null;
}
@@ -1,5 +1,5 @@
import { Region, RegionSubdomain } from "./regions";
import { Region, RegionSubdomain } from '@blitzkit/core';
export function regionSubdomainToRegion(regionDomain: RegionSubdomain): Region {
return regionDomain === "na" ? "com" : regionDomain;
return regionDomain === 'na' ? 'com' : regionDomain;
}
@@ -1,5 +1,5 @@
import { Region, RegionSubdomain } from "./regions";
import { Region, RegionSubdomain } from '@blitzkit/core';
export function regionToRegionSubdomain(regionDomain: Region): RegionSubdomain {
return regionDomain === "com" ? "na" : regionDomain;
return regionDomain === 'com' ? 'na' : regionDomain;
}
@@ -1,6 +1,5 @@
import { ClanList } from "./clans";
import { fetchBlitz } from "./fetchBlitz";
import { Region } from "./regions";
import { fetchBlitz, Region } from '@blitzkit/core';
import { ClanList } from './clans';
export async function searchClansAcrossRegions(search: string, limit = 25) {
const trimmed = search.trim();
@@ -11,38 +10,38 @@ export async function searchClansAcrossRegions(search: string, limit = 25) {
return (
await Promise.all([
fetchBlitz<ClanList>("com", "clans/list", {
fetchBlitz<ClanList>('com', 'clans/list', {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "com" as Region,
region: 'com' as Region,
}))
.splice(0, normalizedLimit)
.splice(0, normalizedLimit),
),
fetchBlitz<ClanList>("eu", "clans/list", {
fetchBlitz<ClanList>('eu', 'clans/list', {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "eu" as Region,
region: 'eu' as Region,
}))
.splice(0, normalizedLimit)
.splice(0, normalizedLimit),
),
fetchBlitz<ClanList>("asia", "clans/list", {
fetchBlitz<ClanList>('asia', 'clans/list', {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "asia" as Region,
region: 'asia' as Region,
}))
.splice(0, normalizedLimit)
.splice(0, normalizedLimit),
),
])
)
@@ -1,6 +1,6 @@
import { context } from "../blitzkit/context";
import { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
import { Region } from '@blitzkit/core';
import { context } from '../blitzkit/context';
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
type CurrentRatingPlayerSearch = Record<
number,
@@ -27,12 +27,12 @@ type CurrentRatingPlayerSearch = Record<
export async function searchCurrentRatingPlayers(region: Region, name: string) {
const encodedSearch = encodeURIComponent(name);
const response = await fetch(
context === "website"
context === 'website'
? `/api/${region}/rating/current/search/${encodedSearch}`
: `https://${regionToRegionSubdomain(
region
region,
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
{ cache: "no-store" }
{ cache: 'no-store' },
);
const accountList = (await response.json()) as CurrentRatingPlayerSearch;
@@ -1,5 +1,4 @@
import { fetchBlitz } from '../fetchBlitz';
import { Region } from '../regions';
import { fetchBlitz, Region } from '@blitzkit/core';
import { usernamePattern } from './constants';
export interface AccountListItem {
+1 -1
View File
@@ -1,4 +1,4 @@
import { NATION_IDS } from "./nations";
import { NATION_IDS } from "@blitzkit/core";
export function toUniqueId(nation: string, id: number) {
return (id << 8) + (NATION_IDS[nation] << 4) + 1;
@@ -1,12 +1,16 @@
import { GunDefinition, ProvisionDefinitions, TankDefinition } from "../protos";
import { checkConsumableProvisionInclusivity } from "./checkConsumableProvisionInclusivity";
import {
GunDefinition,
ProvisionDefinitions,
TankDefinition,
} from '@blitzkit/core';
import { checkConsumableProvisionInclusivity } from './checkConsumableProvisionInclusivity';
export function availableProvisions(
tank: TankDefinition,
gun: GunDefinition,
provisionDefinitions: ProvisionDefinitions
provisionDefinitions: ProvisionDefinitions,
) {
return Object.values(provisionDefinitions.provisions).filter((provision) =>
checkConsumableProvisionInclusivity(provision, tank, gun)
checkConsumableProvisionInclusivity(provision, tank, gun),
);
}
@@ -1,5 +1,5 @@
import { SkillDefinitions } from "..";
import { permanentSkills } from "../blitz/permanentSkills";
import { SkillDefinitions } from '@blitzkit/core';
import { permanentSkills } from '../blitz/permanentSkills';
export function createDefaultSkills(skillDefinitions: SkillDefinitions) {
const skills: Record<string, number> = {};
@@ -1,5 +1,4 @@
import { getRatingInfo } from "../blitz/getRatingInfo";
import { assertSecret } from "./assertSecret";
import { assertSecret, getRatingInfo } from '@blitzkit/core';
interface GitHubTrees {
tree: { path: string; url: string }[];
@@ -12,21 +11,21 @@ export async function getArchivedLatestSeasonNumber() {
return cachedLatestArchivedSeasonNumber;
}
const ratingInfo = await getRatingInfo("com");
const ratingInfo = await getRatingInfo('com');
if (ratingInfo.detail) {
const regionsURL = (
(await fetch(
`https://api.github.com/repos/tresabhi/${assertSecret(
import.meta.env.PUBLIC_ASSET_REPO
)}/git/trees/main`
import.meta.env.PUBLIC_ASSET_REPO,
)}/git/trees/main`,
).then((response) => response.json())) as GitHubTrees
).tree.find(({ path }) => path === "regions")!.url;
).tree.find(({ path }) => path === 'regions')!.url;
const comURL = (
(await fetch(regionsURL).then((response) =>
response.json()
response.json(),
)) as GitHubTrees
).tree.find(({ path }) => path === "com")!.url;
).tree.find(({ path }) => path === 'com')!.url;
const ratingURL = (
(await fetch(comURL).then((response) => response.json())) as {
tree: [{ url: string }];
@@ -37,7 +36,7 @@ export async function getArchivedLatestSeasonNumber() {
(await fetch(ratingURL).then((response) => response.json())) as {
tree: { path: string }[];
}
).tree.map(({ path }) => Number(path))
).tree.map(({ path }) => Number(path)),
);
cachedLatestArchivedSeasonNumber = latestSeason;
@@ -1,6 +1,4 @@
import { Region } from "../blitz/regions";
import { RatingLeaderboard } from "../protos";
import { asset } from "./asset";
import { asset, RatingLeaderboard, Region } from '@blitzkit/core';
const cache: Record<Region, Record<number, RatingLeaderboard>> = {
asia: {},
@@ -10,7 +8,7 @@ const cache: Record<Region, Record<number, RatingLeaderboard>> = {
export async function getArchivedRatingLeaderboard(
region: Region,
season: number
season: number,
) {
if (!cache[region][season]) {
const info = await fetch(asset(`regions/${region}/rating/${season}.pb`))
+1 -1
View File
@@ -1,4 +1,4 @@
import { Region } from "../blitz/regions";
import { Region } from '@blitzkit/core';
const TIME_ZONE_MAPPINGS: Record<Region, number> = {
com: -5, // Central North American Time
+2 -2
View File
@@ -1,7 +1,7 @@
import { ShellDefinition, ShellType } from "../protos";
import { ShellDefinition, ShellType } from '@blitzkit/core';
export function isExplosive(
type: ShellDefinition["type"]
type: ShellDefinition['type'],
): type is ShellType.HEAT | ShellType.HE {
return type === ShellType.HEAT || type === ShellType.HE;
}
+6 -6
View File
@@ -1,5 +1,5 @@
import { TANK_CLASSES } from "../blitz/constants";
import { GameDefinitions, TankDefinition, TankType } from "../protos";
import { TANK_CLASSES } from '../blitz/constants';
import { GameDefinitions, TankDefinition, TankType } from '../protos';
const treeTypeOrder = [
TankType.RESEARCHABLE,
@@ -9,20 +9,20 @@ const treeTypeOrder = [
export function metaSortTank(
tanks: TankDefinition[],
gameDefinitions: GameDefinitions
gameDefinitions: GameDefinitions,
) {
return tanks
.sort((a, b) => b.tier - a.tier)
.sort(
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type)
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type),
)
.sort(
(a, b) => TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class)
(a, b) => TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class),
)
.sort(
(a, b) =>
gameDefinitions.nations.indexOf(b.nation) -
gameDefinitions.nations.indexOf(a.nation)
gameDefinitions.nations.indexOf(a.nation),
)
.sort((a, b) => a.tier - b.tier);
}
@@ -1,5 +1,4 @@
import { Vector3 } from "three";
import { BoundingBox } from "../protos";
import { BoundingBox, Vector3 } from '@blitzkit/core';
export function normalizeBoundingBox(boundingBox: BoundingBox) {
return {
+1 -1
View File
@@ -1,4 +1,4 @@
import { BKNI_COLORS, BkniPercentile, getBkniPercentile } from "../statistics";
import { BKNI_COLORS, BkniPercentile, getBkniPercentile } from '@blitzkit/core';
export function parseBkni(bkni: number) {
const bkniFraction = (bkni + 1) / 2;
+1 -1
View File
@@ -1,4 +1,4 @@
import { asset } from "./asset";
import { asset } from "@blitzkit/core";
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
@@ -1,4 +1,4 @@
import { BoundingBox } from "../protos";
import { BoundingBox } from '@blitzkit/core';
export function unionBoundingBox(a: BoundingBox, b: BoundingBox) {
return {
+102 -101
View File
@@ -1,19 +1,20 @@
import { Vector3Tuple, Vector4Tuple } from "three";
import { ScpgReadStream } from "./scpg";
import { Vector3Tuple, Vector4Tuple } from 'three';
import { ScpgReadStream } from './scpg';
export interface SC2 {
"#dataNodes": DataNode[];
"#hierarchy": Hierarchy[];
"#sceneComponents": SceneComponents;
'#dataNodes': DataNode[];
'#hierarchy': Hierarchy[];
'#sceneComponents': SceneComponents;
}
type DataNode = {
"##name": "NMaterial";
"#id": ArrayBuffer;
'##name': 'NMaterial';
'#id': ArrayBuffer;
materialName: string;
parentMaterialKey?: bigint;
qualityGroup?: string;
fxName?: string;
textures?: Textures;
} & (
| ({
@@ -79,8 +80,8 @@ export interface Textures {
}
export interface Hierarchy {
"##name": "Entity";
"#hierarchy"?: Hierarchy[];
'##name': 'Entity';
'#hierarchy'?: Hierarchy[];
components: Components;
flags: number;
id: bigint;
@@ -96,12 +97,12 @@ type Components = {
type RoBatches = Record<
string,
{
"##name": "RenderBatch";
"rb.aabbox": RbAabbox;
"rb.classname": "RenderBatch";
"rb.datasource": bigint;
"rb.nmatname": bigint;
"rb.sortingKey": number;
'##name': 'RenderBatch';
'rb.aabbox': RbAabbox;
'rb.classname': 'RenderBatch';
'rb.datasource': bigint;
'rb.nmatname': bigint;
'rb.sortingKey': number;
}
>;
@@ -112,50 +113,50 @@ interface RbAabbox {
export type Component =
| {
"comp.typename": "TransformComponent";
"tc.localRotation": Vector4Tuple;
"tc.localScale": Vector3Tuple;
"tc.localTranslation": Vector3Tuple;
"tc.worldRotation": Vector4Tuple;
"tc.worldScale": Vector3Tuple;
"tc.worldTranslation": Vector3Tuple;
'comp.typename': 'TransformComponent';
'tc.localRotation': Vector4Tuple;
'tc.localScale': Vector3Tuple;
'tc.localTranslation': Vector3Tuple;
'tc.worldRotation': Vector4Tuple;
'tc.worldScale': Vector3Tuple;
'tc.worldTranslation': Vector3Tuple;
}
| {
"comp.typename": "RenderComponent";
"rc.renderObj": {
"##name": "Mesh";
"ro.batchCount": number;
"ro.batches": RoBatches;
"ro.debugflags": number;
"ro.flags": number;
"ro.notShadowOnly": boolean;
"ro.sOclCull": boolean;
"ro.sOclIndex": number;
} & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number>;
'comp.typename': 'RenderComponent';
'rc.renderObj': {
'##name': 'Mesh';
'ro.batchCount': number;
'ro.batches': RoBatches;
'ro.debugflags': number;
'ro.flags': number;
'ro.notShadowOnly': boolean;
'ro.sOclCull': boolean;
'ro.sOclIndex': number;
} & Record<`rb${number}.${'lodIndex' | 'switchIndex'}`, number>;
}
| {
"comp.typename": "SlotComponent";
"sc.attachmentRotation": number[];
"sc.attachmentScale": number[];
"sc.attachmentTranslation": number[];
"sc.configFilePath": string;
"sc.slotName": string;
"sc.template": string;
"sc.typeFiltersCount": number;
'comp.typename': 'SlotComponent';
'sc.attachmentRotation': number[];
'sc.attachmentScale': number[];
'sc.attachmentTranslation': number[];
'sc.configFilePath': string;
'sc.slotName': string;
'sc.template': string;
'sc.typeFiltersCount': number;
}
| {
"comp.typename": "LodComponent";
"lc.loddist": Record<`distance${number}`, number>;
'comp.typename': 'LodComponent';
'lc.loddist': Record<`distance${number}`, number>;
}
| {
"comp.typename": "DecorItemComponent";
'comp.typename': 'DecorItemComponent';
drawOrder: number;
shouldApplyCamo: boolean;
vanishTargetAlpha: number;
vanishWhenArmorShown: boolean;
}
| {
"comp.typename": "NewSlotComponent";
'comp.typename': 'NewSlotComponent';
slotItemsCount: number;
slots: {
[key: string]: {
@@ -170,109 +171,109 @@ export type Component =
};
}
| {
"comp.typename": "ScenarioComponent";
"scenario.scriptId": string;
'comp.typename': 'ScenarioComponent';
'scenario.scriptId': string;
}
| ({
"comp.typename": "StateSwitcherComponent";
"ssc.activeState": number;
"ssc.statesCount": number;
'comp.typename': 'StateSwitcherComponent';
'ssc.activeState': number;
'ssc.statesCount': number;
} & Record<`ssc.state${number}`, string>)
| {
"comp.typename": "CustomPropertiesComponent";
"cpc.properties.archive": {
"editor.donotremove": number;
'comp.typename': 'CustomPropertiesComponent';
'cpc.properties.archive': {
'editor.donotremove': number;
};
}
| ({
"comp.typename": "ActionComponent";
"ac.actionCount": number;
'comp.typename': 'ActionComponent';
'ac.actionCount': number;
} & Record<
string, // 0000, 0001, ...
{
"act.delay": number;
"act.delayVariation": number;
"act.entityName": string;
"act.event": number;
"act.eventToTrigger": string;
"act.motionFrameTime": number;
"act.motionSpeed": number;
"act.stateActivated": number;
"act.stopAfterNRepeats": number;
"act.stopWhenEmpty": boolean;
"act.switchIndex": number;
"act.type": number;
"act.userEventId": string;
'act.delay': number;
'act.delayVariation': number;
'act.entityName': string;
'act.event': number;
'act.eventToTrigger': string;
'act.motionFrameTime': number;
'act.motionSpeed': number;
'act.stateActivated': number;
'act.stopAfterNRepeats': number;
'act.stopWhenEmpty': boolean;
'act.switchIndex': number;
'act.type': number;
'act.userEventId': string;
}
>)
| {
"comp.typename": "ParticleEffectComponent";
"pe.clearOnRestart": boolean;
"pe.effectDuration": number;
"pe.emitters": {
"##name": string;
"emitter.data": {
"emitter.id": `${number}`;
"emitter.quality": string;
'comp.typename': 'ParticleEffectComponent';
'pe.clearOnRestart': boolean;
'pe.effectDuration': number;
'pe.emitters': {
'##name': string;
'emitter.data': {
'emitter.id': `${number}`;
'emitter.quality': string;
}[];
"emitter.position": Vector3Tuple;
"emitter.rotation": Vector4Tuple;
"emitter.scale": Vector3Tuple;
'emitter.position': Vector3Tuple;
'emitter.rotation': Vector4Tuple;
'emitter.scale': Vector3Tuple;
}[];
"pe.inheritScale": boolean;
"pe.nestedEmitters": boolean;
"pe.repeatsCount": number;
"pe.startFromTime": number;
"pe.stopWhenEmpty": boolean;
"pe.version": number;
"ro.flags": number;
'pe.inheritScale': boolean;
'pe.nestedEmitters': boolean;
'pe.repeatsCount': number;
'pe.startFromTime': number;
'pe.stopWhenEmpty': boolean;
'pe.version': number;
'ro.flags': number;
}
| {
"comp.typename": "TankElementComponent";
'comp.typename': 'TankElementComponent';
isDestructible: boolean;
shouldCastShadow: boolean;
shouldGenerateSilhouette: boolean;
}
| {
"comp.typename": "MotionComponent";
"motion.filepath": string;
"motion.playbackRate": number;
"simpleMotion.repeatsCount": number;
'comp.typename': 'MotionComponent';
'motion.filepath': string;
'motion.playbackRate': number;
'simpleMotion.repeatsCount': number;
}
| {
"comp.typename": "SkeletonComponent";
'comp.typename': 'SkeletonComponent';
joints: Record<
string, // 0000, 0001, ...
{
"joint.bbox.max": Vector3Tuple;
"joint.bbox.min": Vector3Tuple;
"joint.bindPose": [
'joint.bbox.max': Vector3Tuple;
'joint.bbox.min': Vector3Tuple;
'joint.bindPose': [
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
];
"joint.invBindPose": [
'joint.invBindPose': [
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
];
"joint.name": string;
"joint.parentIndex": number;
"joint.uid": string;
'joint.name': string;
'joint.parentIndex': number;
'joint.uid': string;
}
>;
jointsCount: number;
}
| {
"comp.typename": "AnimationComponent";
'comp.typename': 'AnimationComponent';
animation: bigint;
animationTimeScale: number;
repeatsCount: number;
}
| {
"comp.typename": "SkeletonJointBindingComponent";
'comp.typename': 'SkeletonJointBindingComponent';
count: number;
map: Record<
string, // 0000, 0001, ...
+5 -1
View File
@@ -1,3 +1,7 @@
import { assertSecret } from "@blitzkit/core";
const dev = assertSecret(import.meta.env.DEV);
export function literals(
string: string,
literals?: Record<string, string | number | undefined>
@@ -5,7 +9,7 @@ export function literals(
for (const key in literals) {
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`);
if (import.meta.env.DEV && mutated === string) {
if (dev && mutated === string) {
console.warn(`Could not find literal {${key}} in string "${string}"`);
}
-2
View File
@@ -731,8 +731,6 @@
},
"settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Development mode",
"language": "Language"
},
+2 -2
View File
@@ -643,8 +643,8 @@
"body": "Эта страница перемещена. Перенаправление..."
},
"settings": {
"title": "Настройки",
"description": "Изменить настройки BlitzKit",
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Режим разработчика",
"language": "Язык"
},
+1 -1
View File
@@ -45,7 +45,7 @@ const methods = [
camouflageIcons,
boosterIcons,
tankIcons,
// blitzkitTankIcons,
blitzkitTankIcons,
// popularTanks,
glossary,
];
@@ -31,12 +31,12 @@ export async function tankModels() {
let index = 0;
for (const [tankKey, tank] of entries) {
console.log(` ${++index} / ${entries.length} : ${tankKey}`);
if (tankKey.includes("tutorial_bot")) continue;
const id = toUniqueId(nation, tank.id);
console.log(` ${++index} / ${entries.length} : ${tankKey}`);
const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
);
@@ -39,95 +39,82 @@ export async function extractModel(data: string, path: string) {
const buffer = document.createBuffer();
const materials = new Map<bigint, Material | bigint | undefined>();
for (const node of sc2["#dataNodes"]) {
const id = new DataView(node["#id"]).getBigUint64(0, true);
// create materials
await Promise.all(
sc2["#dataNodes"].map(async (node) => {
const id = new DataView(node["#id"]).getBigUint64(0, true);
if (node.parentMaterialKey !== undefined) {
/**
* material depends on a parent and doesn't seem to have any
* properties so we just point it to the parent
*/
materials.set(id, node.parentMaterialKey);
continue;
}
if (node.parentMaterialKey !== undefined) {
/**
* material depends on a parent and doesn't seem to have any
* properties so we just point it to the parent
*/
materials.set(id, node.parentMaterialKey);
return;
}
const material = document.createMaterial(node.materialName);
let textures: Textures | undefined = undefined;
const material = document.createMaterial(node.materialName);
let textures: Textures | undefined = undefined;
if (node.textures) {
textures = node.textures;
} else if (typeof node.configCount === "number") {
textures = node.configArchive_0.textures;
}
if (node.textures) {
textures = node.textures;
} else if (typeof node.configCount === "number") {
textures = node.configArchive_0.textures;
}
if (textures) {
material.setBaseColorTexture(
document
.createTexture(node.materialName)
.setMimeType("image/webp")
.setImage(
await readBaseColor(
`${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
textures.miscMap
? `${data}/3d/${dirname(path)}/${textures.miscMap}`
: undefined
if (textures) {
material.setBaseColorTexture(
document
.createTexture(node.materialName)
.setMimeType("image/jpeg")
.setImage(
await readBaseColor(
`${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
textures.miscMap
? `${data}/3d/${dirname(path)}/${textures.miscMap}`
: undefined
)
)
)
);
);
if (node.configCount) {
for (
let configIndex = 0;
configIndex < node.configCount;
configIndex++
) {
const archive = node[`configArchive_${configIndex}`];
if (
archive.configName !== "Default" ||
!archive.enabledPresets?.AlphaTest
) {
continue;
}
material.setAlphaMode("BLEND").setDoubleSided(true);
if (textures.baseRMMap) {
material.setMetallicRoughnessTexture(
document
.createTexture(node.materialName)
.setMimeType("image/jpeg")
.setImage(
await readRoughnessMetallic(
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`
)
)
);
}
}
if (textures.baseRMMap) {
material.setMetallicRoughnessTexture(
document
.createTexture(node.materialName)
.setMimeType("image/webp")
.setImage(
await readRoughnessMetallic(
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`
if (textures.baseNormalMap ?? textures.normalmap) {
const isBase = textures.baseNormalMap !== undefined;
material.setNormalTexture(
document
.createTexture(node.materialName)
.setMimeType("image/jpeg")
.setImage(
await readNormal(
`${data}/3d/${dirname(path)}/${
textures.baseNormalMap ?? textures.normalmap
}`,
isBase
)
)
)
);
}
materials.set(
new DataView(node["#id"]).getBigUint64(0, true),
material
);
}
if (textures.baseNormalMap ?? textures.normalmap) {
const isBase = textures.baseNormalMap !== undefined;
material.setNormalTexture(
document
.createTexture(node.materialName)
.setMimeType("image/webp")
.setImage(
await readNormal(
`${data}/3d/${dirname(path)}/${
textures.baseNormalMap ?? textures.normalmap
}`,
isBase
)
)
);
}
materials.set(new DataView(node["#id"]).getBigUint64(0, true), material);
}
}
})
);
// replace children materials with parents
materials.forEach((material, id) => {
@@ -6,7 +6,10 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
const occlusionRaw = occlusionPath
? await readTexture(occlusionPath)
: undefined;
const base = await sharp(baseRaw.data, { raw: baseRaw }).raw().toBuffer();
const base = await sharp(baseRaw.data, { raw: baseRaw })
.removeAlpha()
.raw()
.toBuffer();
const occlusion = occlusionRaw
? await sharp(occlusionRaw.data, { raw: occlusionRaw })
.extractChannel(3)
@@ -14,7 +17,7 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
.toBuffer()
: undefined;
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 4);
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 3);
for (let i = 0; i < baseRaw.width * baseRaw.height; i++) {
let c = 1;
@@ -30,16 +33,15 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
c = occlusion![occlusionI] / 255;
}
combined[i * 4 + 0] = Math.round(base[i * 4 + 0] * c);
combined[i * 4 + 1] = Math.round(base[i * 4 + 1] * c);
combined[i * 4 + 2] = Math.round(base[i * 4 + 2] * c);
combined[i * 4 + 3] = base[i * 4 + 3];
combined[i * 3 + 0] = Math.round(base[i * 3 + 0] * c);
combined[i * 3 + 1] = Math.round(base[i * 3 + 1] * c);
combined[i * 3 + 2] = Math.round(base[i * 3 + 2] * c);
}
const image = await sharp(combined, {
raw: { width: baseRaw.width, height: baseRaw.height, channels: 4 },
raw: { width: baseRaw.width, height: baseRaw.height, channels: 3 },
})
.webp()
.jpeg()
.toBuffer();
return image;
@@ -25,5 +25,5 @@ export async function readNormal(path: string, isBase: boolean) {
raw.data[index + 3] = 255;
}
return await sharp(raw.data, { raw }).webp().toBuffer();
return await sharp(raw.data, { raw }).jpeg().toBuffer();
}
@@ -17,7 +17,7 @@ export async function readRoughnessMetallic(path: string) {
const image = await sharp(combined, {
raw: { width: raw.width, height: raw.height, channels: 3 },
})
.webp()
.jpeg()
.toBuffer();
return image;
+1 -4
View File
@@ -109,10 +109,7 @@ export class Varuna<Type, Arguments = void> {
const instance = this;
const [slice, setSlice] = useState(initial);
useEffect(() => {
setSlice(slicer(instance.state));
return instance.on(slicer, setSlice);
}, []);
useEffect(() => instance.on(slicer, setSlice), []);
return slice;
}
+3 -3
View File
@@ -29,7 +29,7 @@ const strings = getStrings(locale);
<style>
.navbar,
.navbar-padder {
.padder {
transition-duration: 1s;
}
@@ -37,7 +37,7 @@ const strings = getStrings(locale);
transform: translateY(-100%);
}
body.no-navbar .navbar-padder {
body.no-navbar .padder {
height: 0;
}
@@ -79,7 +79,7 @@ const strings = getStrings(locale);
});
</script>
<Box className="navbar-padder" height={{ initial: "3.5rem", sm: "3rem" }} />
<Box className="padder" height={{ initial: "3.5rem", sm: "3rem" }} />
<Flex
direction="column"
@@ -1,10 +1,9 @@
import { Table } from "@radix-ui/themes";
import { times } from "lodash-es";
import { Fragment, Suspense, useEffect, useMemo, useState } from "react";
import usePromise from "react-promise-suspense";
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
import { filterTanks } from "../../core/blitzkit/filterTanks";
import { filterTank } from "../../core/blitzkit/filterTank";
import { Performance } from "../../stores/performance";
import { TankFilters } from "../../stores/tankFilters";
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
@@ -21,7 +20,12 @@ const [tankDefinitions, averageDefinitions] = await Promise.all([
awaitableAverageDefinitions,
]);
const tankList = Object.values(tankDefinitions.tanks);
const averageDefinitionsArray = Object.entries(averageDefinitions.averages).map(
([id, average]) => ({
id: Number(id),
...average,
})
);
export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
if (skeleton) {
@@ -35,109 +39,98 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
}
const sort = TankPerformanceSort.use();
const filters = TankFilters.use();
const filteredTanks = usePromise(
() =>
filterTanks(filters, tankList).then((tanks) =>
tanks.filter(({ id }) => id in averageDefinitions.averages)
),
// react-promise-suspense has awful type annotations
[filters] as any
);
const tanksMapped = useMemo(
() =>
filteredTanks.map(({ id }) => ({
id,
...averageDefinitions.averages[id],
})),
[filters]
);
const tanksSorted = useMemo(() => {
const { playerCountPeriod } = Performance.state;
switch (sort.type) {
case "accuracy":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots)
);
case "capture_points":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.capture_points / a.mu.battles -
b.mu.capture_points / b.mu.battles)
);
case "damage":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.damage_dealt / a.mu.battles -
b.mu.damage_dealt / b.mu.battles)
);
case "damage_ratio":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.damage_dealt / a.mu.damage_received -
b.mu.damage_dealt / b.mu.damage_received)
);
case "damage_taken":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.damage_received / a.mu.battles -
b.mu.damage_received / b.mu.battles)
);
case "hits":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles)
);
case "kills":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles)
);
case "shots":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles)
);
case "players":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod])
);
case "spots":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles)
);
case "survival":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.survived_battles / a.mu.battles -
b.mu.survived_battles / b.mu.battles)
);
case "winrate":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction *
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles)
);
case "xp":
return tanksMapped.sort(
return averageDefinitionsArray.sort(
(a, b) =>
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles)
);
}
}, [sort]);
const filters = TankFilters.use();
const tanks = useMemo(() => {
return tanksSorted.filter((tank) =>
filterTank(filters, tankDefinitions.tanks[tank.id])
);
}, [filters, sort]);
const [loadedRows, setLoadedRows] = useState(DEFAULT_LOADED_ROWS);
@@ -147,9 +140,9 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
return (
<Table.Body>
<Total tanks={tanksSorted} />
<Total tanks={tanks} />
{tanksSorted.slice(0, loadedRows).map((averages) => {
{tanks.slice(0, loadedRows).map((averages) => {
const tank = tankDefinitions.tanks[averages.id];
if (tank === undefined) return null;
@@ -163,21 +156,16 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
);
})}
{times(
Math.min(PREVIEW_COUNT, tanksSorted.length - loadedRows),
(index) => {
return (
<RowLoader
key={index}
onIntersection={() => {
setLoadedRows((state) =>
Math.min(state + 2, tanksSorted.length)
);
}}
/>
);
}
)}
{times(Math.min(PREVIEW_COUNT, tanks.length - loadedRows), (index) => {
return (
<RowLoader
key={index}
onIntersection={() => {
setLoadedRows((state) => Math.min(state + 2, tanks.length));
}}
/>
);
})}
</Table.Body>
);
}
@@ -2,16 +2,16 @@ import {
AverageDefinitionsEntry,
formatCompact,
type BlitzkitStats,
} from "@blitzkit/core";
import { Table } from "@radix-ui/themes";
import { memo, useCallback } from "react";
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
import { useLocale } from "../../hooks/useLocale";
import { Performance } from "../../stores/performance";
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
} from '@blitzkit/core';
import { Table } from '@radix-ui/themes';
import { memo, useCallback } from 'react';
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
import { useLocale } from '../../hooks/useLocale';
import { Performance } from '../../stores/performance';
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
export interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
id: number;
}
@@ -24,7 +24,7 @@ const averageDefinitions = await awaitableAverageDefinitions;
export const Total = memo<TotalProps>(
({ tanks }) => {
const playerCountPeriod = Performance.use(
(state) => state.playerCountPeriod
(state) => state.playerCountPeriod,
);
const { strings, locale } = useLocale();
const ratio = useAveragesExclusionRatio();
@@ -32,10 +32,10 @@ export const Total = memo<TotalProps>(
(slice: (tank: BlitzkitStats) => number) => {
return tanks.reduce(
(acc, tank) => acc + tank.samples[playerCountPeriod] * slice(tank.mu),
0
0,
);
},
[tanks]
[tanks],
);
const battles = sum(({ battles }) => battles);
@@ -55,14 +55,14 @@ export const Total = memo<TotalProps>(
return (
<Table.Row>
<StickyRowHeaderCell style={{ overflow: "hidden", display: "flex" }}>
<StickyRowHeaderCell style={{ overflow: 'hidden', display: 'flex' }}>
{strings.website.tools.performance.table.tanks.total}
</StickyRowHeaderCell>
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
<Table.Cell align="center">
{formatCompact(
locale,
ratio * averageDefinitions.samples[playerCountPeriod]
ratio * averageDefinitions.samples[playerCountPeriod],
)}
</Table.Cell>
<Table.Cell align="center">
@@ -87,5 +87,5 @@ export const Total = memo<TotalProps>(
</Table.Row>
);
},
(prev, next) => prev.tanks === next.tanks
(prev, next) => prev.tanks === next.tanks,
);
@@ -293,6 +293,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
align="center"
style={{ transitionDuration: "200ms" }}
position="relative"
bottom={display === TankopediaDisplay.DynamicArmor ? "0" : "-7rem"}
>
{disturbed && (
<>
@@ -8,12 +8,11 @@ export function Title() {
const { unwrap } = useLocale();
const protagonist = Duel.use((state) => state.protagonist.tank);
const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed);
const name = unwrap(protagonist.name);
const fontSize = revealed
? disturbed
? "1.5rem"
: "2rem"
? revealed
? "2rem"
: `min(16vh, ${125 / name.length}vw)`
: `min(12vh, ${75 / name.length}vw)`;
return (
@@ -29,7 +28,7 @@ export function Title() {
position="absolute"
align="center"
justify="center"
top={revealed ? (disturbed ? "3rem" : "6rem") : "50%"}
top={revealed ? "6rem" : "50%"}
left="50%"
width="100%"
height={revealed ? fontSize : "100%"}
@@ -1,10 +1,9 @@
import { metaSortTank } from "@blitzkit/core";
import { times } from "lodash-es";
import { useMemo, useState } from "react";
import usePromise from "react-promise-suspense";
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
import { filterTanks } from "../../core/blitzkit/filterTanks";
import { filterTank } from "../../core/blitzkit/filterTank";
import { TankFilters } from "../../stores/tankFilters";
import { TierList } from "../../stores/tierList";
import { SkeletonTankCard } from "../TankSearch/components/SkeletonTankCard";
@@ -24,16 +23,14 @@ const DEFAULT_LOADED_CARDS = 75;
export function TierListTiles() {
const filters = TankFilters.use();
const placedTanks = TierList.use((state) => state.placedTanks);
const filteredTanks = usePromise(
() =>
filterTanks(filters, tanks).then((tanks) =>
tanks.filter(({ id }) => !placedTanks.has(id))
),
// react-promise-suspense has awful type annotations
[filters, placedTanks] as any
);
const sorted = useMemo(
() => metaSortTank(filteredTanks, gameDefinitions).reverse(),
() =>
metaSortTank(
tanks.filter(
(tank) => filterTank(filters, tank) && !placedTanks.has(tank.id)
),
gameDefinitions
).reverse(),
[filters, placedTanks]
);
const [loadedTiles, setLoadedTiles] = useState(DEFAULT_LOADED_CARDS);
@@ -61,7 +61,7 @@ export const getStaticPaths = (() => {
twitter={{ cardType: "summary_large_image" }}
/>
<BaseLayout integrated>
<BaseLayout>
<HomeHero />
<!-- <Flex justify="center" py="6" px="4" justify="center">
@@ -1,21 +1,12 @@
---
import { AstroSeo } from "@astrolib/seo";
import { getStrings } from "../../../core/i18n/getStrings";
import BaseLayout from "../../../layouts/BaseLayout.astro";
import { Page } from "./_index";
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { Page } from './_index';
export { getStaticPaths } from "../index.astro";
export { getStaticPaths } from '../index.astro';
const { locale } = Astro.params;
const strings = getStrings(locale);
---
<AstroSeo
title={strings.website.settings.title}
description={strings.website.settings.description}
/>
<BaseLayout>
<Page {locale} client:load />
</BaseLayout>