mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-10 11:16:10 +01:00
Compare commits
17
Commits
c6b006c2ad
...
03a51c47bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03a51c47bb | ||
|
|
1790443698 | ||
|
|
a60f95340c | ||
|
|
e407463ca7 | ||
|
|
61cc15aafc | ||
|
|
1a947a6300 | ||
|
|
75262647f7 | ||
|
|
9914371dd8 | ||
|
|
a4e48a2feb | ||
|
|
4ab03e56a2 | ||
|
|
06a3791ddd | ||
|
|
36cce4b0c1 | ||
|
|
158141fdc7 | ||
|
|
1d319551e7 | ||
|
|
0552ac790e | ||
|
|
5ad7566874 | ||
|
|
638f178fbf |
No files matched your search
@@ -1,4 +1,4 @@
|
||||
name: Build Assets
|
||||
name: 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,9 +131,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint:website
|
||||
|
||||
- name: Download depots
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build Bot
|
||||
name: Bot
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name: Discover IDs
|
||||
name: IDs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
- cron: "0 0 * * 1"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
name: Host Bot
|
||||
name: Host
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
schedule:
|
||||
- cron: '0 */5 * * *'
|
||||
- cron: "0 */5 * * *"
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created, published, released, edited]
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
- cron: "0 4 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
Vendored
+1
@@ -92,6 +92,7 @@
|
||||
"GLTF",
|
||||
"gtag",
|
||||
"haha",
|
||||
"Herdbreaker",
|
||||
"highp",
|
||||
"iife",
|
||||
"imgur",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"": {
|
||||
"name": "blitzkit",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.17.1",
|
||||
"@types/xmpp__client": "^0.13.3",
|
||||
"@xmpp/client": "^0.13.6",
|
||||
"astro-favicons": "^3.1.5",
|
||||
@@ -13,6 +14,7 @@
|
||||
"protobufjs": "^7.5.4",
|
||||
"quicklime": "^1.2.0",
|
||||
"sharp": "^0.34.4",
|
||||
"yaml": "^2.8.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.22",
|
||||
@@ -55,22 +57,24 @@
|
||||
"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",
|
||||
@@ -569,7 +573,7 @@
|
||||
|
||||
"@oslojs/encoding": ["@oslojs/[email protected]", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||
|
||||
"@prisma/client": ["@prisma/[email protected]6.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw=="],
|
||||
"@prisma/client": ["@prisma/[email protected]7.1", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
+5
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "blitzkit",
|
||||
"private": true,
|
||||
"version": "1.28.3",
|
||||
"version": "1.28.4",
|
||||
"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 --no-hints && cd packages/website && bun run build",
|
||||
"build:website": "bun build:prisma && cd packages/website && bun run build",
|
||||
"build:prisma": "prisma generate",
|
||||
"build:sitemap": "bun scripts/sitemap.ts",
|
||||
"build:docs": "vitepress build docs",
|
||||
@@ -44,6 +44,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.17.1",
|
||||
"@types/xmpp__client": "^0.13.3",
|
||||
"@xmpp/client": "^0.13.6",
|
||||
"astro-favicons": "^3.1.5",
|
||||
@@ -52,7 +53,8 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"protobufjs": "^7.5.4",
|
||||
"quicklime": "^1.2.0",
|
||||
"sharp": "^0.34.4"
|
||||
"sharp": "^0.34.4",
|
||||
"yaml": "^2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.22",
|
||||
|
||||
@@ -4,20 +4,22 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type ShellDefinition, ShellType } from '@blitzkit/core';
|
||||
import { ShellDefinition, ShellType } from "../protos";
|
||||
|
||||
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,4 +1,4 @@
|
||||
import { BlitzStats, calculateWN8 } from '@blitzkit/core';
|
||||
import { BlitzStats, calculateWN8 } from "../statistics";
|
||||
|
||||
export type Stat = keyof ReturnType<typeof generateStats>;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fetchBlitz, Region } from '@blitzkit/core';
|
||||
import { normalizeIds } from './normalizeIds';
|
||||
import { fetchBlitz } from "./fetchBlitz";
|
||||
import { normalizeIds } from "./normalizeIds";
|
||||
import { Region } from "./regions";
|
||||
|
||||
interface IndividualAccountAchievements {
|
||||
achievements: {
|
||||
@@ -22,11 +23,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;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
BlitzStats,
|
||||
FetchBlitzParams,
|
||||
Region,
|
||||
fetchBlitz,
|
||||
} from '@blitzkit/core';
|
||||
import { normalizeIds } from './normalizeIds';
|
||||
import { BlitzStats } from "../statistics";
|
||||
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
|
||||
import { normalizeIds } from "./normalizeIds";
|
||||
import { Region } from "./regions";
|
||||
|
||||
export interface SupplementaryStats {
|
||||
WN8?: number;
|
||||
@@ -83,15 +80,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,5 +1,6 @@
|
||||
import { fetchBlitz, Region } from '@blitzkit/core';
|
||||
import { normalizeIds } from './normalizeIds';
|
||||
import { fetchBlitz } from "./fetchBlitz";
|
||||
import { normalizeIds } from "./normalizeIds";
|
||||
import { Region } from "./regions";
|
||||
|
||||
type IndividualClanAccountInfo = {
|
||||
account_id: number;
|
||||
@@ -30,14 +31,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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fetchBlitz, Region } from '@blitzkit/core';
|
||||
import { normalizeIds } from './normalizeIds';
|
||||
import { fetchBlitz } from "./fetchBlitz";
|
||||
import { normalizeIds } from "./normalizeIds";
|
||||
import { Region } from "./regions";
|
||||
|
||||
interface IndividualClanInfo {
|
||||
recruiting_options: {
|
||||
@@ -38,11 +39,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 '@blitzkit/core';
|
||||
import { LEAGUES } from "./rating";
|
||||
|
||||
export function getLeagueFromScore(score: number) {
|
||||
return LEAGUES.find(({ minScore }) => score >= minScore)!;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { RatingLeague, RatingReward, Region } from '@blitzkit/core';
|
||||
import { context } from '../blitzkit/context';
|
||||
import { patientFetch } from '../blitzkit/patientFetch';
|
||||
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
|
||||
import { context } from "../blitzkit/context";
|
||||
import { patientFetch } from "../blitzkit/patientFetch";
|
||||
import { RatingLeague, RatingReward } from "./rating";
|
||||
import { Region } from "./regions";
|
||||
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
|
||||
|
||||
export type RatingInfo =
|
||||
| {
|
||||
@@ -26,12 +27,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;
|
||||
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { LeagueTop, RatingPlayer, Region } from '@blitzkit/core';
|
||||
import { context } from '../blitzkit/context';
|
||||
import { patientFetch } from '../blitzkit/patientFetch';
|
||||
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
|
||||
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";
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
import { RatingNeighbors, Region } from '@blitzkit/core';
|
||||
import { patientFetch } from '../blitzkit/patientFetch';
|
||||
import { withCORSProxy } from '../blitzkit/withCORSProxy';
|
||||
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
|
||||
import { patientFetch } from "../blitzkit/patientFetch";
|
||||
import { withCORSProxy } from "../blitzkit/withCORSProxy";
|
||||
import { RatingNeighbors } from "./rating";
|
||||
import { Region } from "./regions";
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import {
|
||||
fetchBlitz,
|
||||
FetchBlitzParams,
|
||||
Region,
|
||||
TanksStats,
|
||||
} from '@blitzkit/core';
|
||||
import { TanksStats } from "../types";
|
||||
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
|
||||
import { Region } from "./regions";
|
||||
|
||||
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 '@blitzkit/core';
|
||||
import { getRatingInfo } from "./getRatingInfo";
|
||||
|
||||
export async function isOnGoingRatingSeason() {
|
||||
const ratingInfo = await getRatingInfo('com');
|
||||
const ratingInfo = await getRatingInfo("com");
|
||||
return ratingInfo.detail === undefined;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Region, assertSecret, idToRegion } from '@blitzkit/core';
|
||||
import { assertSecret } from "../blitzkit";
|
||||
import { idToRegion } from "./idToRegion";
|
||||
import { Region } from "./regions";
|
||||
|
||||
export async function isValidBlitzId(id: number, token?: string) {
|
||||
let region: Region;
|
||||
@@ -11,20 +13,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 '@blitzkit/core';
|
||||
import { Region, RegionSubdomain } from "./regions";
|
||||
|
||||
export function regionSubdomainToRegion(regionDomain: RegionSubdomain): Region {
|
||||
return regionDomain === 'na' ? 'com' : regionDomain;
|
||||
return regionDomain === "na" ? "com" : regionDomain;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Region, RegionSubdomain } from '@blitzkit/core';
|
||||
import { Region, RegionSubdomain } from "./regions";
|
||||
|
||||
export function regionToRegionSubdomain(regionDomain: Region): RegionSubdomain {
|
||||
return regionDomain === 'com' ? 'na' : regionDomain;
|
||||
return regionDomain === "com" ? "na" : regionDomain;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fetchBlitz, Region } from '@blitzkit/core';
|
||||
import { ClanList } from './clans';
|
||||
import { ClanList } from "./clans";
|
||||
import { fetchBlitz } from "./fetchBlitz";
|
||||
import { Region } from "./regions";
|
||||
|
||||
export async function searchClansAcrossRegions(search: string, limit = 25) {
|
||||
const trimmed = search.trim();
|
||||
@@ -10,38 +11,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 { Region } from '@blitzkit/core';
|
||||
import { context } from '../blitzkit/context';
|
||||
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
|
||||
import { context } from "../blitzkit/context";
|
||||
import { Region } from "./regions";
|
||||
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,4 +1,5 @@
|
||||
import { fetchBlitz, Region } from '@blitzkit/core';
|
||||
import { fetchBlitz } from '../fetchBlitz';
|
||||
import { Region } from '../regions';
|
||||
import { usernamePattern } from './constants';
|
||||
|
||||
export interface AccountListItem {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NATION_IDS } from "@blitzkit/core";
|
||||
import { NATION_IDS } from "./nations";
|
||||
|
||||
export function toUniqueId(nation: string, id: number) {
|
||||
return (id << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import {
|
||||
GunDefinition,
|
||||
ProvisionDefinitions,
|
||||
TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import { checkConsumableProvisionInclusivity } from './checkConsumableProvisionInclusivity';
|
||||
import { GunDefinition, ProvisionDefinitions, TankDefinition } from "../protos";
|
||||
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 '@blitzkit/core';
|
||||
import { permanentSkills } from '../blitz/permanentSkills';
|
||||
import { SkillDefinitions } from "..";
|
||||
import { permanentSkills } from "../blitz/permanentSkills";
|
||||
|
||||
export function createDefaultSkills(skillDefinitions: SkillDefinitions) {
|
||||
const skills: Record<string, number> = {};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { assertSecret, getRatingInfo } from '@blitzkit/core';
|
||||
import { getRatingInfo } from "../blitz/getRatingInfo";
|
||||
import { assertSecret } from "./assertSecret";
|
||||
|
||||
interface GitHubTrees {
|
||||
tree: { path: string; url: string }[];
|
||||
@@ -11,21 +12,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 }];
|
||||
@@ -36,7 +37,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,4 +1,6 @@
|
||||
import { asset, RatingLeaderboard, Region } from '@blitzkit/core';
|
||||
import { Region } from "../blitz/regions";
|
||||
import { RatingLeaderboard } from "../protos";
|
||||
import { asset } from "./asset";
|
||||
|
||||
const cache: Record<Region, Record<number, RatingLeaderboard>> = {
|
||||
asia: {},
|
||||
@@ -8,7 +10,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,4 +1,4 @@
|
||||
import { Region } from '@blitzkit/core';
|
||||
import { Region } from "../blitz/regions";
|
||||
|
||||
const TIME_ZONE_MAPPINGS: Record<Region, number> = {
|
||||
com: -5, // Central North American Time
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ShellDefinition, ShellType } from '@blitzkit/core';
|
||||
import { ShellDefinition, ShellType } from "../protos";
|
||||
|
||||
export function isExplosive(
|
||||
type: ShellDefinition['type'],
|
||||
type: ShellDefinition["type"]
|
||||
): type is ShellType.HEAT | ShellType.HE {
|
||||
return type === ShellType.HEAT || type === ShellType.HE;
|
||||
}
|
||||
@@ -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,4 +1,5 @@
|
||||
import { BoundingBox, Vector3 } from '@blitzkit/core';
|
||||
import { Vector3 } from "three";
|
||||
import { BoundingBox } from "../protos";
|
||||
|
||||
export function normalizeBoundingBox(boundingBox: BoundingBox) {
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BKNI_COLORS, BkniPercentile, getBkniPercentile } from '@blitzkit/core';
|
||||
import { BKNI_COLORS, BkniPercentile, getBkniPercentile } from "../statistics";
|
||||
|
||||
export function parseBkni(bkni: number) {
|
||||
const bkniFraction = (bkni + 1) / 2;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { asset } from "@blitzkit/core";
|
||||
import { asset } from "./asset";
|
||||
|
||||
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BoundingBox } from '@blitzkit/core';
|
||||
import { BoundingBox } from "../protos";
|
||||
|
||||
export function unionBoundingBox(a: BoundingBox, b: BoundingBox) {
|
||||
return {
|
||||
|
||||
+101
-102
@@ -1,20 +1,19 @@
|
||||
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;
|
||||
} & (
|
||||
| ({
|
||||
@@ -80,8 +79,8 @@ export interface Textures {
|
||||
}
|
||||
|
||||
export interface Hierarchy {
|
||||
'##name': 'Entity';
|
||||
'#hierarchy'?: Hierarchy[];
|
||||
"##name": "Entity";
|
||||
"#hierarchy"?: Hierarchy[];
|
||||
components: Components;
|
||||
flags: number;
|
||||
id: bigint;
|
||||
@@ -97,12 +96,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;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -113,50 +112,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]: {
|
||||
@@ -171,109 +170,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, ...
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { assertSecret } from "@blitzkit/core";
|
||||
|
||||
const dev = assertSecret(import.meta.env.DEV);
|
||||
|
||||
export function literals(
|
||||
string: string,
|
||||
literals?: Record<string, string | number | undefined>
|
||||
@@ -9,7 +5,7 @@ export function literals(
|
||||
for (const key in literals) {
|
||||
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`);
|
||||
|
||||
if (dev && mutated === string) {
|
||||
if (import.meta.env.DEV && mutated === string) {
|
||||
console.warn(`Could not find literal {${key}} in string "${string}"`);
|
||||
}
|
||||
|
||||
|
||||
@@ -731,6 +731,8 @@
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "Development mode",
|
||||
"language": "Language"
|
||||
},
|
||||
|
||||
@@ -643,6 +643,8 @@
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "Modo de desarrollador",
|
||||
"language": "Idioma"
|
||||
},
|
||||
|
||||
@@ -643,6 +643,8 @@
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "Development mode",
|
||||
"language": "Language"
|
||||
},
|
||||
|
||||
@@ -643,6 +643,8 @@
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "開発者モード",
|
||||
"language": "言語 / Language"
|
||||
},
|
||||
|
||||
@@ -643,6 +643,8 @@
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "Development mode",
|
||||
"language": "Language"
|
||||
},
|
||||
|
||||
@@ -643,6 +643,8 @@
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "Modo de desenvolvimento",
|
||||
"language": "Idioma"
|
||||
},
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"test": {
|
||||
"default": "тест",
|
||||
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
|
||||
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт — {fruit}, потому что это цвет {color}. Он мне так нравится, так что я скажу его 3 раза {fruit}! {fruit}! {fruit}! И ещё раз, это {color}, а не {not_color}.",
|
||||
"fox": "Съешь же ещё этих мягких французских булок, да выпей чаю."
|
||||
},
|
||||
"common": {
|
||||
"name": "BlitzKit",
|
||||
"units": {
|
||||
"m": "{value}m",
|
||||
"mm": "{value}mm",
|
||||
"m": "{value}м",
|
||||
"mm": "{value}мм",
|
||||
"deg": "{value}°",
|
||||
"hp": "{value}hp",
|
||||
"hp": "{value}оп",
|
||||
"x": "x{value}",
|
||||
"percentage": "{value}%",
|
||||
"deg_s": "{value}°/s"
|
||||
"deg_s": "{value}°/с"
|
||||
},
|
||||
"gun_types": {
|
||||
"auto_loader": "Барабан",
|
||||
@@ -186,11 +186,11 @@
|
||||
"bot": {
|
||||
"common": {
|
||||
"errors": {
|
||||
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
|
||||
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
|
||||
"clan_not_found": "# Не удалось найти клан\nНе найден «{search}». Попробуйте выбрать клан из результатов поиска.",
|
||||
"player_not_found": "# Не удалось найти пользователя\nНе удалось найти пользователя «{search}». Попробуйте выбрать пользователя из результатов поиска, введя правильное имя пользователя или используя команду «/link».",
|
||||
"player_not_linked": "# Используйте «/link», чтобы начать\nЯ не знаю, кто ты, но давай познакомимся сегодня!",
|
||||
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
|
||||
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
|
||||
"tank_name_not_found": "# Танк не найден\nНе удалось найти танк с названием «{search}».",
|
||||
"tank_id_not_found": "# Танк не найден\nНе удалось найти танк с ID «{search}». Попробуйте ввести название танка вместо числа.",
|
||||
"no_tank_stats": "# Статистика танка недоступна\nУ этого игрока нет доступной статистики. Возможно, это не тот игрок, которого вы ищете.",
|
||||
"get_help": "Получить помощь",
|
||||
"uncaught_error": "# BlitzKit столкнулся с ошибкой!\nНам очень жаль. Не стесняйтесь и [присоединяйтесь к официальному серверу Discord](https://discord.gg/nDt7AjGJQH), чтобы получить помощь.",
|
||||
@@ -284,19 +284,19 @@
|
||||
"name": "уровень",
|
||||
"description": "Уровень",
|
||||
"choices": {
|
||||
"tier": "Tier {tier_1} - {tier_2}"
|
||||
"tier": "Уровень {tier_1} - {tier_2}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"periods": {
|
||||
"career": "Карьера",
|
||||
"today": "Cегодня",
|
||||
"days": "{days} days",
|
||||
"custom": "{day_1} - {day_2} days"
|
||||
"days": "{days} дней",
|
||||
"custom": "{day_1} - {day_2} дней"
|
||||
},
|
||||
"filters": {
|
||||
"none": "Без фильтров",
|
||||
"tier": "Tier {tier}"
|
||||
"tier": " {tier} уровня"
|
||||
},
|
||||
"no_data": {
|
||||
"battles_in_period": "За этот период боёв не было",
|
||||
@@ -339,19 +339,19 @@
|
||||
"body": {
|
||||
"subtitle": "Исследование",
|
||||
"total": "Всего",
|
||||
"research": "Research: {cost}",
|
||||
"upgrades": "Upgrades: {cost}",
|
||||
"purchase": "Purchase: {cost}",
|
||||
"equipment": "Equipment: {cost}",
|
||||
"research": "Исследование: {cost}",
|
||||
"upgrades": "Улучшения: {cost}",
|
||||
"purchase": "Покупка: {cost}",
|
||||
"equipment": "Снаряжение: {cost}",
|
||||
"estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете."
|
||||
},
|
||||
"errors": {
|
||||
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
|
||||
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
|
||||
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
|
||||
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
|
||||
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
|
||||
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank."
|
||||
"non_tech_tree": "# {tank} отсутствует в дереве танков\n\nВы не можете исследовать премиумный / коллекционный танк.",
|
||||
"tier_1": "# {tank} — танк I уровня\n\nУровень I всегда открыт бесплатно для всех игроков.",
|
||||
"already_researched": "# {tank} уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.",
|
||||
"start_end_equal": "# {tank} был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.",
|
||||
"tanks_not_on_line": "# {tank_1} и {tank_2} находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.",
|
||||
"unordered": "# У {tank_1} более высокий уровень чем у {tank_2}\n\nОбязательно укажите {tank_2} в качестве «starting-tank», так как это танк более низкого уровня."
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
@@ -394,7 +394,7 @@
|
||||
"wn8": "WN8",
|
||||
"winrate": "Коэффициент побед",
|
||||
"damage": "Урон",
|
||||
"unknown_tank": "Unknown tank {id}"
|
||||
"unknown_tank": "Неизвестный танк {id}"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
@@ -430,12 +430,12 @@
|
||||
"options": {
|
||||
"threshold": {
|
||||
"name": "порог",
|
||||
"description": "The number of days inactive (default: {days} days)"
|
||||
"description": "Количество дней не активности (по умолчанию: {days} дней)"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"subtitle": "Inactive for {days}+ days",
|
||||
"listing": "{days} days"
|
||||
"subtitle": "Неактивен {days}+ дней",
|
||||
"listing": "{days} дней"
|
||||
}
|
||||
},
|
||||
"full_stats": {
|
||||
@@ -478,7 +478,7 @@
|
||||
"body": {
|
||||
"accounts_linked": "Аккаунты привязаны",
|
||||
"no_role_permissions": {
|
||||
"title": "{name} failed to verify",
|
||||
"title": "{name} не удалось проверить",
|
||||
"description": "У меня нет разрешения на управление ролями."
|
||||
}
|
||||
},
|
||||
@@ -493,11 +493,11 @@
|
||||
"options": {
|
||||
"limit": {
|
||||
"name": "лимит",
|
||||
"description": "The max number of clan results (default: {limit})"
|
||||
"description": "Максимальное количество результатов клана (по умолчанию: {limit})"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"title": "Clan search for {search} in region {region}",
|
||||
"title": "Поиск клана {search} в регионе {region}",
|
||||
"no_results": "Кланов не найдено"
|
||||
}
|
||||
},
|
||||
@@ -507,11 +507,11 @@
|
||||
"options": {
|
||||
"limit": {
|
||||
"name": "лимит",
|
||||
"description": "The max number of player results (default: {limit})"
|
||||
"description": "Максимальное число результатов игрока (по умолчанию: {limit})"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"title": "Player search for {search} in region {region}",
|
||||
"title": "Поиск игрока {search} в регионе {region}",
|
||||
"no_results": "Игроков не найдено"
|
||||
}
|
||||
},
|
||||
@@ -531,13 +531,13 @@
|
||||
"options": {
|
||||
"limit": {
|
||||
"name": "лимит",
|
||||
"description": "How many neighbors to display (default: {count})"
|
||||
"description": "Количество отображаемых ближайших игроков (по умолчанию: {count})"
|
||||
},
|
||||
"season": {
|
||||
"name": "сезон",
|
||||
"description": "Номер сезона (по умолчанию: текущий)",
|
||||
"choices": {
|
||||
"current": "{season} (current)"
|
||||
"current": "{season} (текущий)"
|
||||
}
|
||||
},
|
||||
"league": {
|
||||
@@ -546,12 +546,12 @@
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"top_players": "Top {count} players",
|
||||
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
|
||||
"top_players": "Топ {count} игроков",
|
||||
"no_participation": "# {player} не играл в рейтинг в сезоне {season}\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.",
|
||||
"no_ongoing_season": "# Нет текущего сезона\n\nВ данный момент нет рейтингового сезона.",
|
||||
"deleted_player": "Deleted {id}",
|
||||
"deleted_player": "Удаленный {id}",
|
||||
"neighbors": "Рейтинг ближайших игроков",
|
||||
"more": "+ {count} more"
|
||||
"more": "+ ещё {count}"
|
||||
}
|
||||
},
|
||||
"today": {
|
||||
@@ -566,14 +566,14 @@
|
||||
"name": "cобственные-танки",
|
||||
"description": "Показывает все принадлежащие танки на уровне",
|
||||
"body": {
|
||||
"title": "# {name}'s owned tier {tier} tanks\n\n"
|
||||
"title": "# {name} принадлежат танки {tier} уровня\n\n"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"name": "разрешения",
|
||||
"description": "Проверяет, есть ли у бота необходимые разрешения для работы",
|
||||
"body": {
|
||||
"title": "# Permissions\n\n{body}",
|
||||
"title": "# Разрешения\n\n{body}",
|
||||
"view_channels": "Просмотр каналов: необходимо найти сообщение, на котором была использована кнопка обновления",
|
||||
"read_message_history": "Просмотр истории сообщений: необходимо найти сообщение для обновления",
|
||||
"attach_files": "Прикрепить файлы: необходимо для добавления изображений в команды на основе изображения",
|
||||
@@ -585,7 +585,7 @@
|
||||
"description": "Проверьте, что бот жив.",
|
||||
"body": {
|
||||
"pong": "Понг 🏓",
|
||||
"pong_with_time": "Pong 🏓 - {time}ms"
|
||||
"pong_with_time": "Понг 🏓 — {time}мс"
|
||||
},
|
||||
"subcommands": {
|
||||
"blitzkit": {
|
||||
@@ -602,7 +602,7 @@
|
||||
"name": "информация-об-игроке",
|
||||
"description": "Основная информация о игроке",
|
||||
"body": {
|
||||
"title": "{name}'s information",
|
||||
"title": "Информация о {name}",
|
||||
"nickname": "Никнейм",
|
||||
"battles": "Боёв",
|
||||
"winrate": "Коэффициент побед",
|
||||
@@ -631,7 +631,7 @@
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
|
||||
"no_participation": "# {name} не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.",
|
||||
"unrecorded_stats": "# Статистика недоступна в настоящее время\n\nВ настоящее время Wargaming, возможно, сталкивается с простоями сервера. Если сезон продолжается, статистика должна быть доступна в течение 24 часов."
|
||||
}
|
||||
}
|
||||
@@ -643,6 +643,8 @@
|
||||
"body": "Эта страница перемещена. Перенаправление..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
"description": "Изменить настройки BlitzKit",
|
||||
"dev_mode": "Режим разработчика",
|
||||
"language": "Язык"
|
||||
},
|
||||
@@ -659,13 +661,13 @@
|
||||
"select_dialog_title": "Поиск танка",
|
||||
"swap_dialog_title": "Заменить танк",
|
||||
"clear_filters": "Сбросить фильтры",
|
||||
"login": "Login to use this",
|
||||
"login": "Войти, чтобы воспользоваться",
|
||||
"search_bar_hint": "Поиск танков...",
|
||||
"recent": "Недавние",
|
||||
"select_all": "Выбрать всё",
|
||||
"count_singular": "1 танк",
|
||||
"count_plural": "{count} tanks",
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"count_plural": "Танков: {count}",
|
||||
"sorting_by": "Сортировать по {name}",
|
||||
"no_tanks_found": {
|
||||
"body": "Танков не найдено.",
|
||||
"search_again": "Попробуйте повторить поиск.",
|
||||
@@ -741,16 +743,16 @@
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "By {name} ❤️"
|
||||
"credit": "От {name} ❤️"
|
||||
},
|
||||
"hot_tanks": {
|
||||
"title": "Популярные танки",
|
||||
"subtitle": "Last {days} days"
|
||||
"subtitle": "За последнее {days} дней"
|
||||
},
|
||||
"other_projects": {
|
||||
"title": "Другие классные проекты"
|
||||
},
|
||||
"help_translate": "Please help translate {name} to {language}!",
|
||||
"help_translate": "Пожалуйста, помогите перевести {name} на {language}!",
|
||||
"seo_description": "Инструменты для World of Tanks Blitz"
|
||||
},
|
||||
"docs": {
|
||||
@@ -761,7 +763,7 @@
|
||||
},
|
||||
"tools": {
|
||||
"mixer": {
|
||||
"name": "Смешиватель",
|
||||
"name": "Смешать",
|
||||
"button": "Смешать и собрать",
|
||||
"description": "Модули смешанного танка",
|
||||
"seo_description": "Смешивание и сбор корпусов, башен и пушек танков из World of Tanks Blitz",
|
||||
@@ -769,7 +771,7 @@
|
||||
"randomize": "Случайно"
|
||||
},
|
||||
"guess": {
|
||||
"name": "Угадайте танк",
|
||||
"name": "Угадай танк",
|
||||
"button": "Угадать",
|
||||
"description": "Мини-игра «Силуэт»",
|
||||
"seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz",
|
||||
@@ -780,7 +782,7 @@
|
||||
"next": "Следующий",
|
||||
"no_results": "Ничего не найденo"
|
||||
},
|
||||
"stats": "{correct} / {total} guessed correctly"
|
||||
"stats": "{correct} / {total} угадано верно"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Поиск характеристик...",
|
||||
@@ -788,8 +790,8 @@
|
||||
"button": "Поиск танков",
|
||||
"description": "Танковая энциклопедия Blitz",
|
||||
"seo_description": "Статистика, броня и многое другое по всем танкам World of Tanks Blitz",
|
||||
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
|
||||
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
|
||||
"tank_seo_title": "{name} — {class} {nationality} {tier} уровня | Танкопедия {website} | World of Tanks Blitz (WoTB)",
|
||||
"tank_seo_description": "Броня, статистика, оборудование, снабжение {name} и многое другое. DPM {dpm}, альфа {alpha}оп, пробитие{penetration}мм, лучшая скорость{speed}км/ч, здоровье {health}оп.",
|
||||
"visualizers": {
|
||||
"reload": {
|
||||
"skip": "Пропуск"
|
||||
@@ -867,15 +869,15 @@
|
||||
"track": "Гусеницы"
|
||||
},
|
||||
"status": {
|
||||
"penetration": "{damage}hp penetration",
|
||||
"penetration": "Пробитие на {damage}оп",
|
||||
"blocked": "Не пробил",
|
||||
"ricochet": "Рикошет",
|
||||
"splash": "{damage}hp splash"
|
||||
"splash": "Урон осколками на {damage}оп"
|
||||
},
|
||||
"stats": {
|
||||
"thickness_and_angle": "{thickness}mm {angle}°",
|
||||
"nominal": "{thickness}mm nominal",
|
||||
"ricochet_loss": "{loss}% penetration"
|
||||
"thickness_and_angle": "{thickness}мм {angle}°",
|
||||
"nominal": "{thickness}1мм номинала",
|
||||
"ricochet_loss": "{loss}% пробития"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -913,15 +915,15 @@
|
||||
"tech_tree": {
|
||||
"title": "Дерево танков",
|
||||
"battle": "1 бой",
|
||||
"battles": "{count} battles",
|
||||
"route": "Route {index} of {count}"
|
||||
"battles": "{count} боёв",
|
||||
"route": "Ветка {index} из {count}"
|
||||
},
|
||||
"game_modes": {
|
||||
"title": "Способности в игровых режимах"
|
||||
},
|
||||
"review": {
|
||||
"title": "Видеообзоры",
|
||||
"join": "Apply to {name} YouTuber program"
|
||||
"join": "Предложить ютубера для программы {name}"
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Конфигурация",
|
||||
@@ -957,15 +959,15 @@
|
||||
"title": "Характеристики",
|
||||
"compare": {
|
||||
"label": "Сравнивать с",
|
||||
"class": "Tier {tier} {class}",
|
||||
"tier": "All of tier {tier}",
|
||||
"class": "{class} {tier} уровня",
|
||||
"tier": "Все {tier} уровня",
|
||||
"all": "Все танки"
|
||||
},
|
||||
"values": {
|
||||
"shell_index": "Shell {index}",
|
||||
"at_distance": "At {distance}m",
|
||||
"shell_index": "Снаряд {index}",
|
||||
"at_distance": "На {distance}м",
|
||||
"at_distance_slider_label": "Дистанция",
|
||||
"dispersion_at_distance": "Dispersion at {distance}m",
|
||||
"dispersion_at_distance": "Разброс на {distance}м",
|
||||
"gun_flexibility": "Углы наводки",
|
||||
"speed": "Скорость",
|
||||
"terrain_coefficients": "Сопротивление грунтам",
|
||||
@@ -1114,9 +1116,9 @@
|
||||
"description": "Статистика эффективности танков",
|
||||
"seo_description": "Лучшие танки и их эффективность в World of Tanks Blitz",
|
||||
"estimation": {
|
||||
"body": "Estimated stats based on {count} players. Updated {age} ago.",
|
||||
"hours": "{count} hours",
|
||||
"minutes": "{count} minutes"
|
||||
"body": "Примерная статистика на основе {count} игроков. Обновлено {age} назад.",
|
||||
"hours": "{count} часов",
|
||||
"minutes": "{count} минут"
|
||||
},
|
||||
"player_count": {
|
||||
"label": "Количество игроков"
|
||||
@@ -1148,10 +1150,10 @@
|
||||
"button": "Просмотреть",
|
||||
"description": "Аватары игрока Blitz",
|
||||
"seo_description": "Все аватары игрока World of Tanks Blitz",
|
||||
"last_updated": "Last updated on {time}",
|
||||
"last_updated": "Последнее обновление {time}",
|
||||
"search": {
|
||||
"hint": "Поиск аватаров...",
|
||||
"results": "{count} avatars"
|
||||
"results": "Аватаров: {count}"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
@@ -1166,8 +1168,8 @@
|
||||
"no_results": "Игроков не найдено"
|
||||
},
|
||||
"controls": {
|
||||
"title": "Tracking {name}",
|
||||
"subtitle": "Since {time}",
|
||||
"title": "Отслеживание {name}",
|
||||
"subtitle": "Начиная с {time}",
|
||||
"reset": "Сброс",
|
||||
"change": "Изменить"
|
||||
},
|
||||
@@ -1227,7 +1229,7 @@
|
||||
"cancel": "Отмена",
|
||||
"success": {
|
||||
"title": "Скопировать URL",
|
||||
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
|
||||
"info": "Сгенерирован URL для {name}. Встройка начнет отслеживание, как только URL будет вставлен. Можно сбросить, щелкнув правой кнопкой мыши на ПК или долгим нажатием на мобильном телефоне и выбором «Сброс».",
|
||||
"back": "Назад",
|
||||
"copy": "Скопировать URL"
|
||||
}
|
||||
|
||||
@@ -643,6 +643,8 @@
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "Development mode",
|
||||
"language": "Мова"
|
||||
},
|
||||
|
||||
@@ -643,6 +643,8 @@
|
||||
"body": "此页面已被移动。正在重定向……"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "开发者模式",
|
||||
"language": "语言"
|
||||
},
|
||||
|
||||
@@ -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,82 +39,95 @@ export async function extractModel(data: string, path: string) {
|
||||
const buffer = document.createBuffer();
|
||||
const materials = new Map<bigint, Material | bigint | undefined>();
|
||||
|
||||
// create materials
|
||||
await Promise.all(
|
||||
sc2["#dataNodes"].map(async (node) => {
|
||||
const id = new DataView(node["#id"]).getBigUint64(0, true);
|
||||
for (const node of sc2["#dataNodes"]) {
|
||||
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);
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
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 (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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
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 (textures) {
|
||||
material.setBaseColorTexture(
|
||||
if (textures.baseRMMap) {
|
||||
material.setMetallicRoughnessTexture(
|
||||
document
|
||||
.createTexture(node.materialName)
|
||||
.setMimeType("image/jpeg")
|
||||
.setMimeType("image/webp")
|
||||
.setImage(
|
||||
await readBaseColor(
|
||||
`${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
|
||||
textures.miscMap
|
||||
? `${data}/3d/${dirname(path)}/${textures.miscMap}`
|
||||
: undefined
|
||||
await readRoughnessMetallic(
|
||||
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (textures.baseRMMap) {
|
||||
material.setMetallicRoughnessTexture(
|
||||
document
|
||||
.createTexture(node.materialName)
|
||||
.setMimeType("image/jpeg")
|
||||
.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/webp")
|
||||
.setImage(
|
||||
await readNormal(
|
||||
`${data}/3d/${dirname(path)}/${
|
||||
textures.baseNormalMap ?? textures.normalmap
|
||||
}`,
|
||||
isBase
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
materials.set(new DataView(node["#id"]).getBigUint64(0, true), material);
|
||||
}
|
||||
}
|
||||
|
||||
// replace children materials with parents
|
||||
materials.forEach((material, id) => {
|
||||
|
||||
@@ -6,10 +6,7 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
|
||||
const occlusionRaw = occlusionPath
|
||||
? await readTexture(occlusionPath)
|
||||
: undefined;
|
||||
const base = await sharp(baseRaw.data, { raw: baseRaw })
|
||||
.removeAlpha()
|
||||
.raw()
|
||||
.toBuffer();
|
||||
const base = await sharp(baseRaw.data, { raw: baseRaw }).raw().toBuffer();
|
||||
const occlusion = occlusionRaw
|
||||
? await sharp(occlusionRaw.data, { raw: occlusionRaw })
|
||||
.extractChannel(3)
|
||||
@@ -17,7 +14,7 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
|
||||
.toBuffer()
|
||||
: undefined;
|
||||
|
||||
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 3);
|
||||
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 4);
|
||||
|
||||
for (let i = 0; i < baseRaw.width * baseRaw.height; i++) {
|
||||
let c = 1;
|
||||
@@ -33,15 +30,16 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
|
||||
c = occlusion![occlusionI] / 255;
|
||||
}
|
||||
|
||||
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);
|
||||
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];
|
||||
}
|
||||
|
||||
const image = await sharp(combined, {
|
||||
raw: { width: baseRaw.width, height: baseRaw.height, channels: 3 },
|
||||
raw: { width: baseRaw.width, height: baseRaw.height, channels: 4 },
|
||||
})
|
||||
.jpeg()
|
||||
.webp()
|
||||
.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 }).jpeg().toBuffer();
|
||||
return await sharp(raw.data, { raw }).webp().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 },
|
||||
})
|
||||
.jpeg()
|
||||
.webp()
|
||||
.toBuffer();
|
||||
|
||||
return image;
|
||||
|
||||
@@ -109,7 +109,10 @@ export class Varuna<Type, Arguments = void> {
|
||||
const instance = this;
|
||||
const [slice, setSlice] = useState(initial);
|
||||
|
||||
useEffect(() => instance.on(slicer, setSlice), []);
|
||||
useEffect(() => {
|
||||
setSlice(slicer(instance.state));
|
||||
return instance.on(slicer, setSlice);
|
||||
}, []);
|
||||
|
||||
return slice;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const strings = getStrings(locale);
|
||||
|
||||
<style>
|
||||
.navbar,
|
||||
.padder {
|
||||
.navbar-padder {
|
||||
transition-duration: 1s;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const strings = getStrings(locale);
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
body.no-navbar .padder {
|
||||
body.no-navbar .navbar-padder {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ const strings = getStrings(locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Box className="padder" height={{ initial: "3.5rem", sm: "3rem" }} />
|
||||
<Box className="navbar-padder" height={{ initial: "3.5rem", sm: "3rem" }} />
|
||||
|
||||
<Flex
|
||||
direction="column"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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 { filterTank } from "../../core/blitzkit/filterTank";
|
||||
import { filterTanks } from "../../core/blitzkit/filterTanks";
|
||||
import { Performance } from "../../stores/performance";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
|
||||
@@ -20,12 +21,7 @@ const [tankDefinitions, averageDefinitions] = await Promise.all([
|
||||
awaitableAverageDefinitions,
|
||||
]);
|
||||
|
||||
const averageDefinitionsArray = Object.entries(averageDefinitions.averages).map(
|
||||
([id, average]) => ({
|
||||
id: Number(id),
|
||||
...average,
|
||||
})
|
||||
);
|
||||
const tankList = Object.values(tankDefinitions.tanks);
|
||||
|
||||
export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
if (skeleton) {
|
||||
@@ -39,98 +35,109 @@ 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 averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots)
|
||||
);
|
||||
case "capture_points":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.capture_points / a.mu.battles -
|
||||
b.mu.capture_points / b.mu.battles)
|
||||
);
|
||||
case "damage":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.damage_dealt / a.mu.battles -
|
||||
b.mu.damage_dealt / b.mu.battles)
|
||||
);
|
||||
case "damage_ratio":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.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 averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.damage_received / a.mu.battles -
|
||||
b.mu.damage_received / b.mu.battles)
|
||||
);
|
||||
case "hits":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles)
|
||||
);
|
||||
case "kills":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles)
|
||||
);
|
||||
case "shots":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles)
|
||||
);
|
||||
case "players":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod])
|
||||
);
|
||||
case "spots":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles)
|
||||
);
|
||||
case "survival":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.survived_battles / a.mu.battles -
|
||||
b.mu.survived_battles / b.mu.battles)
|
||||
);
|
||||
case "winrate":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles)
|
||||
);
|
||||
case "xp":
|
||||
return averageDefinitionsArray.sort(
|
||||
return tanksMapped.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);
|
||||
|
||||
@@ -140,9 +147,9 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
|
||||
return (
|
||||
<Table.Body>
|
||||
<Total tanks={tanks} />
|
||||
<Total tanks={tanksSorted} />
|
||||
|
||||
{tanks.slice(0, loadedRows).map((averages) => {
|
||||
{tanksSorted.slice(0, loadedRows).map((averages) => {
|
||||
const tank = tankDefinitions.tanks[averages.id];
|
||||
|
||||
if (tank === undefined) return null;
|
||||
@@ -156,16 +163,21 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
);
|
||||
})}
|
||||
|
||||
{times(Math.min(PREVIEW_COUNT, tanks.length - loadedRows), (index) => {
|
||||
return (
|
||||
<RowLoader
|
||||
key={index}
|
||||
onIntersection={() => {
|
||||
setLoadedRows((state) => Math.min(state + 2, tanks.length));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{times(
|
||||
Math.min(PREVIEW_COUNT, tanksSorted.length - loadedRows),
|
||||
(index) => {
|
||||
return (
|
||||
<RowLoader
|
||||
key={index}
|
||||
onIntersection={() => {
|
||||
setLoadedRows((state) =>
|
||||
Math.min(state + 2, tanksSorted.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";
|
||||
|
||||
interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
|
||||
export 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,7 +293,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
align="center"
|
||||
style={{ transitionDuration: "200ms" }}
|
||||
position="relative"
|
||||
bottom={display === TankopediaDisplay.DynamicArmor ? "0" : "-7rem"}
|
||||
>
|
||||
{disturbed && (
|
||||
<>
|
||||
|
||||
+5
-4
@@ -8,11 +8,12 @@ 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
|
||||
? revealed
|
||||
? "2rem"
|
||||
: `min(16vh, ${125 / name.length}vw)`
|
||||
? disturbed
|
||||
? "1.5rem"
|
||||
: "2rem"
|
||||
: `min(12vh, ${75 / name.length}vw)`;
|
||||
|
||||
return (
|
||||
@@ -28,7 +29,7 @@ export function Title() {
|
||||
position="absolute"
|
||||
align="center"
|
||||
justify="center"
|
||||
top={revealed ? "6rem" : "50%"}
|
||||
top={revealed ? (disturbed ? "3rem" : "6rem") : "50%"}
|
||||
left="50%"
|
||||
width="100%"
|
||||
height={revealed ? fontSize : "100%"}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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 { filterTank } from "../../core/blitzkit/filterTank";
|
||||
import { filterTanks } from "../../core/blitzkit/filterTanks";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TierList } from "../../stores/tierList";
|
||||
import { SkeletonTankCard } from "../TankSearch/components/SkeletonTankCard";
|
||||
@@ -23,14 +24,16 @@ const DEFAULT_LOADED_CARDS = 75;
|
||||
export function TierListTiles() {
|
||||
const filters = TankFilters.use();
|
||||
const placedTanks = TierList.use((state) => state.placedTanks);
|
||||
const sorted = useMemo(
|
||||
const filteredTanks = usePromise(
|
||||
() =>
|
||||
metaSortTank(
|
||||
tanks.filter(
|
||||
(tank) => filterTank(filters, tank) && !placedTanks.has(tank.id)
|
||||
),
|
||||
gameDefinitions
|
||||
).reverse(),
|
||||
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(),
|
||||
[filters, placedTanks]
|
||||
);
|
||||
const [loadedTiles, setLoadedTiles] = useState(DEFAULT_LOADED_CARDS);
|
||||
|
||||
@@ -61,7 +61,7 @@ export const getStaticPaths = (() => {
|
||||
twitter={{ cardType: "summary_large_image" }}
|
||||
/>
|
||||
|
||||
<BaseLayout>
|
||||
<BaseLayout integrated>
|
||||
<HomeHero />
|
||||
|
||||
<!-- <Flex justify="center" py="6" px="4" justify="center">
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import { Page } from './_index';
|
||||
import { AstroSeo } from "@astrolib/seo";
|
||||
import { getStrings } from "../../../core/i18n/getStrings";
|
||||
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>
|
||||
Reference in new issue
Block a user