This commit is contained in:
tresabhi committed 2026-08-06 20:01:50 -05:00
1 parent 13cda7aec0
commit b26c5137f3
53 files changed
+236 -932

No files matched your search

@@ -1,5 +0,0 @@
import { LEAGUES } from "./rating";
export function getLeagueFromScore(score: number) {
return LEAGUES.find(({ minScore }) => score >= minScore)!;
}
-42
View File
@@ -1,42 +0,0 @@
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import type { RatingLeague, RatingReward } from "./rating";
import type { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export type RatingInfo =
| {
detail: undefined;
title: string;
icon: string | null;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingReward[];
leagues: RatingLeague[];
}
| { detail: { error: string } };
const cache: Partial<Record<Region, RatingInfo>> = {};
export async function getRatingInfo(region: Region) {
if (cache[region]) return cache[region]!;
const response = await patientFetch(
context === "website"
? `/api/${region}/rating/current/info`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
undefined
);
const data = (await response.json()) as RatingInfo;
cache[region] = data;
return data;
}
@@ -1,29 +0,0 @@
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import type { RatingPlayer } from "../statistics";
import type { LeagueTop } from "./rating";
import type { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export async function getRatingLeague(
region: Region,
league: number,
usePatientFetcher = false
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: "no-store" })
: fetch(url, { cache: "no-store" });
const response = await fetcher(
context === "website"
? `/api/${region}/rating/current/league/${league}`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`
);
const json = await response.json();
return context === "website"
? (json as RatingPlayer[])
: (json as LeagueTop).result;
}
@@ -1,27 +0,0 @@
import { patientFetch } from "../blitzkit/patientFetch";
import { withCORSProxy } from "../blitzkit/withCORSProxy";
import type { RatingNeighbors } from "./rating";
import type { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export async function getRatingNeighbors(
region: Region,
id: number,
count: number,
usePatientFetcher = false
) {
const fetcher = (url: string) =>
usePatientFetcher
? 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}`
)
);
const json = (await response.json()) as RatingNeighbors;
return json;
}
-6
View File
@@ -10,13 +10,8 @@ export * from "./getAccountAchievements";
export * from "./getAccountInfo";
export * from "./getClanAccountInfo";
export * from "./getClanInfo";
export * from "./getLeagueFromScore";
export * from "./getRatingInfo";
export * from "./getRatingLeague";
export * from "./getRatingNeighbors";
export * from "./getTankStats";
export * from "./idToRegion";
export * from "./isOnGoingRatingSeason";
export * from "./isValidBlitzId";
export * from "./nations";
export * from "./normalizeExtra";
@@ -28,6 +23,5 @@ export * from "./regions";
export * from "./regionSubdomainToRegion";
export * from "./regionToRegionSubdomain";
export * from "./searchClansAcrossRegions";
export * from "./searchCurrentRatingPlayers";
export * from "./searchPlayersAcrossRegions";
export * from "./toUniqueId";
@@ -1,6 +0,0 @@
import { getRatingInfo } from "./getRatingInfo";
export async function isOnGoingRatingSeason() {
const ratingInfo = await getRatingInfo("com");
return ratingInfo.detail === undefined;
}
@@ -1,40 +0,0 @@
import { context } from "../blitzkit/context";
import { type Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
type CurrentRatingPlayerSearch = Record<
number,
{
spa_id: number;
nickname: string;
clan_tag: string;
} & (
| {
skip: true;
}
| {
skip: false;
mmr: number;
season_number: number;
calibrationBattlesLeft: number;
number: number;
updated_at: string;
neighbors: [];
}
)
>;
export async function searchCurrentRatingPlayers(region: Region, name: string) {
const encodedSearch = encodeURIComponent(name);
const response = await fetch(
context === "website"
? `/api/${region}/rating/current/search/${encodedSearch}`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
{ cache: "no-store" }
);
const accountList = (await response.json()) as CurrentRatingPlayerSearch;
return accountList;
}
+13
View File
@@ -0,0 +1,13 @@
import { environmentConfig } from "@blitzkit/core";
export function alias(alias: string, path: string) {
const environment = environmentConfig();
if (!(alias in environment.aliases)) {
throw new Error(`Invalid alias "${alias}"`);
}
const base = environment.aliases[alias];
return `${base}${path}`;
}
+22 -9
View File
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { fetchPB } from "../../protobuf";
import {
CamouflageDefinitions,
@@ -14,38 +15,50 @@ import { BlitzKitAPI } from "./base";
export class ClientBlitzKitAPI extends BlitzKitAPI {
gameDefinitions() {
return fetchPB("/api/definitions/game.pb", GameDefinitions);
return fetchPB(alias("api", "/definitions/game.pb"), GameDefinitions);
}
consumableDefinitions() {
return fetchPB("/api/definitions/consumables.pb", ConsumableDefinitions);
return fetchPB(
alias("api", "/definitions/consumables.pb"),
ConsumableDefinitions,
);
}
tankDefinitions() {
return fetchPB("/api/definitions/tanks.pb", TankDefinitions);
return fetchPB(alias("api", "/definitions/tanks.pb"), TankDefinitions);
}
camouflageDefinitions() {
return fetchPB("/api/definitions/camouflage.pb", CamouflageDefinitions);
return fetchPB(
alias("api", "/definitions/camouflage.pb"),
CamouflageDefinitions,
);
}
provisionDefinitions() {
return fetchPB("/api/definitions/provisions.pb", ProvisionDefinitions);
return fetchPB(
alias("api", "/definitions/provisions.pb"),
ProvisionDefinitions,
);
}
modelDefinitions() {
return fetchPB("/api/definitions/models.pb", ModelDefinitions);
return fetchPB(alias("api", "/definitions/models.pb"), ModelDefinitions);
}
skillDefinitions() {
return fetchPB("/api/definitions/skills.pb", SkillDefinitions);
return fetchPB(alias("api", "/definitions/skills.pb"), SkillDefinitions);
}
equipmentDefinitions() {
return fetchPB("/api/definitions/equipment.pb", EquipmentDefinitions);
return fetchPB(
alias("api", "/definitions/equipment.pb"),
EquipmentDefinitions,
);
}
galleryDefinitions() {
return fetchPB("/api/definitions/gallery.pb", Gallery);
return fetchPB(alias("api", "/definitions/gallery.pb"), Gallery);
}
}
@@ -1,8 +1,8 @@
import { Avatar, fetchGlossary, Gallery } from "@blitzkit/core";
import { ServerBlitzKitAPI9 } from "@blitzkit/core/src/blitzkit/api/server/9_gameDefinitions";
import locales from "@blitzkit/i18n/locales.json";
import { extname } from "path";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI9 } from "./9_gameDefinitions";
export abstract class ServerBlitzKitAPI10 extends ServerBlitzKitAPI9 {
@Cache()
+33
View File
@@ -0,0 +1,33 @@
import { assertSecret } from "@blitzkit/core";
interface EnvironmentConfig {
paths: Record<string, string | null>;
nodes: Record<string, string>;
aliases: Record<string, string>;
}
export const environments = import.meta.glob(
"../../../../environments/*.json",
{
eager: true,
import: "default",
},
) as Record<string, EnvironmentConfig>;
const root = "../../../../environments";
for (const key in environments) {
const trimmed = key.slice(root.length + 1, -5);
environments[trimmed] = environments[key];
delete environments[key];
}
export function environmentConfig() {
const environment = assertSecret(import.meta.env.ENVIRONMENT);
if (!(environment in environments)) {
throw new Error(`Invalid environment "${environment}"`);
}
return environments[environment];
}
@@ -1,49 +0,0 @@
import { getRatingInfo } from "../blitz/getRatingInfo";
import { assertSecret } from "./assertSecret";
interface GitHubTrees {
tree: { path: string; url: string }[];
}
let cachedLatestArchivedSeasonNumber: number | null = null;
export async function getArchivedLatestSeasonNumber() {
if (cachedLatestArchivedSeasonNumber !== null) {
return cachedLatestArchivedSeasonNumber;
}
const ratingInfo = await getRatingInfo("com");
if (ratingInfo.detail) {
const regionsURL = (
(await fetch(
`https://api.github.com/repos/${assertSecret(
import.meta.env.PUBLIC_ASSET_REPO
)}/git/trees/main`
).then((response) => response.json())) as GitHubTrees
).tree.find(({ path }) => path === "regions")!.url;
const comURL = (
(await fetch(regionsURL).then((response) =>
response.json()
)) as GitHubTrees
).tree.find(({ path }) => path === "com")!.url;
const ratingURL = (
(await fetch(comURL).then((response) => response.json())) as {
tree: [{ url: string }];
}
).tree[0].url;
const latestSeason = Math.max(
...(
(await fetch(ratingURL).then((response) => response.json())) as {
tree: { path: string }[];
}
).tree.map(({ path }) => Number(path))
);
cachedLatestArchivedSeasonNumber = latestSeason;
} else {
cachedLatestArchivedSeasonNumber = ratingInfo.current_season - 1;
}
return cachedLatestArchivedSeasonNumber;
}
@@ -1,30 +0,0 @@
// import { RatingInfo, Region, asset } from '@blitzkit/core';
// const cache: Record<
// Region,
// Record<
// number,
// RatingInfo & {
// detail: undefined;
// }
// >
// > = {
// com: {},
// eu: {},
// asia: {},
// };
// export async function getArchivedRatingInfo(region: Region, season: number) {
// if (!cache[region][season]) {
// const info = await fetch(
// asset(`regions/${region}/rating/${season}/info.cdon.lz4`),
// )
// .then((response) => response.arrayBuffer())
// .then((buffer) =>
// superDecompress<RatingInfo & { detail: undefined }>(buffer),
// );
// cache[region][season] = info;
// }
// return cache[region][season];
// }
+2
View File
@@ -1,3 +1,4 @@
export * from "./alias";
export * from "./assertSecret";
export * from "./availableProvisions";
export * from "./averageDefinitions";
@@ -7,6 +8,7 @@ export * from "./createDefaultSkills";
export * from "./degressiveStat";
export * from "./discoveredIdDefinitions";
export * from "./emblemURL";
export * from "./environment";
export * from "./eventManager";
export * from "./getTimeDaysAgo";
export * from "./isExplosive";
+3 -1
View File
@@ -1,5 +1,7 @@
import { alias } from "@blitzkit/core";
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
export function tankIcon(id: number, size: "big" | "small" = "big") {
return `/api/tanks/${id}/icons/${size}.webp`;
return alias("api", `/tanks/${id}/icons/${size}.webp`);
}
@@ -1,169 +0,0 @@
import {
getAccountInfo,
patientFetchJSON,
RatingInfo,
RatingLeaderboard,
RatingLeaderboardEntryV1,
RatingLeaderboardEntryV2,
RatingNeighbors,
RatingPlayer,
Region,
REGIONS,
regionToRegionSubdomain,
} from "@blitzkit/core";
import { chunk } from "lodash-es";
import { argv } from "process";
import { AssetUploader } from "./core/github/assetUploader";
const region = argv
.find((arg) => arg.startsWith("--region="))
?.split("=")[1] as Region | undefined;
if (region === undefined || !REGIONS.includes(region)) {
throw new Error("No valid region specified");
}
console.log(`Building rating leaderboard for ${region}`);
const regionSubdomain = regionToRegionSubdomain(region);
const NEIGHBORS = 2 ** 8;
const PLAYERS = await patientFetchJSON<RatingInfo>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`
).then((data) => (data.detail === undefined ? data.count : undefined));
if (PLAYERS === undefined) {
throw new Error("No current rating season running");
}
const players: Record<number, RatingPlayer> = {};
const entriesV1: RatingLeaderboardEntryV1[] = [];
let registered = 0;
async function branchFromPlayer(id: number) {
const { neighbors } = await patientFetchJSON<RatingNeighbors>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`
);
const firstPlayer = neighbors[0];
const lastPlayer = neighbors[neighbors.length - 1];
const branches: Promise<void>[] = [];
if (!players[firstPlayer.number])
branches.push(branchFromPlayer(firstPlayer.spa_id));
if (!players[lastPlayer.number])
branches.push(branchFromPlayer(lastPlayer.spa_id));
neighbors.forEach((neighbor) => {
if (players[neighbor.number]) return;
players[neighbor.number] = neighbor;
registered++;
});
console.log(
`Registered ${registered} / ${PLAYERS} (${(
(registered / PLAYERS!) *
100
).toFixed(2)}%)`
);
await Promise.all(branches);
}
async function branchFromLeague(id: number) {
const { result } = await patientFetchJSON<{ result: RatingPlayer[] }>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`
);
const firstPlayer = result[0];
const lastPlayer = result.at(-1)!;
await Promise.all([
branchFromPlayer(firstPlayer.spa_id),
branchFromPlayer(lastPlayer.spa_id),
]);
}
console.log("Branching from leagues...");
await Promise.all([
branchFromLeague(0),
branchFromLeague(1),
branchFromLeague(2),
branchFromLeague(3),
branchFromLeague(4),
]);
console.log("Converting to an array...");
for (let index = 0; index < PLAYERS; index++) {
const listing = players[index + 1];
if (listing) {
entriesV1[index] = { id: listing.spa_id, score: listing.score };
} else {
console.warn(`Position ${index + 1} had no player`);
entriesV1[index] = { id: 0, score: 0 };
}
}
console.log("Requesting further stats...");
const entries: RatingLeaderboardEntryV2[] = [];
let chunkIndex = 0;
for (const leaderboardChunk of chunk(entriesV1, 100)) {
console.log(
`Chunk ${chunkIndex + 1} of ${Math.ceil(entriesV1.length / 100)}`
);
const stats = await getAccountInfo(
region,
leaderboardChunk.map(({ id }) => id),
["statistics.rating"]
);
stats.forEach((stat, index) => {
const trueIndex = chunkIndex * 100 + index;
if (
leaderboardChunk[index].id === 0 ||
stat?.statistics.rating === undefined
) {
entries[trueIndex] = {
battles: 0,
damage: 0,
id: 0,
kills: 0,
score: 0,
survived: 0,
wins: 0,
};
} else {
entries[trueIndex] = {
id: leaderboardChunk[index].id,
score: leaderboardChunk[index].score,
battles: stat.statistics.rating.battles,
damage: stat.statistics.rating.damage_dealt,
kills: stat.statistics.rating.frags,
survived: stat.statistics.rating.survived_battles,
wins: stat.statistics.rating.wins,
};
}
});
chunkIndex++;
}
const info = await patientFetchJSON<RatingInfo & { detail: undefined }>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`
);
const normalizedServer = regionSubdomain === "na" ? "com" : regionSubdomain;
const content = RatingLeaderboard.encode({
version: { $case: "v2", value: { entries } },
}).finish();
using uploader = new AssetUploader(`rating leaderboard ${regionSubdomain}`);
await uploader.add({
path: `regions/${normalizedServer}/rating/${info.current_season}.pb`,
content,
});
await uploader.flush();
@@ -1,39 +0,0 @@
import decompress from "decompress";
import { existsSync } from "fs";
import { mkdir, readdir, writeFile } from "fs/promises";
if (!existsSync("../../temp")) {
console.log("Creating temp directory...");
await mkdir("../../temp");
}
console.log("Downloading depot downloader...");
await fetch(
"https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest"
)
.then(
(response) =>
response.json() as Promise<{
assets: { browser_download_url: string; name: string }[];
}>
)
.then(({ assets }) => {
const asset = assets.find(({ name }) => name.includes("linux-x64"));
if (!asset) throw new Error("No asset found");
return fetch(asset.browser_download_url);
})
.then((response) => response.arrayBuffer())
.then((arrayBuffer) =>
writeFile("../../temp/depotDownloader.zip", new Uint8Array(arrayBuffer))
);
console.log("Unzipping...");
await decompress(
"../../temp/depotDownloader.zip",
"../../temp/depotDownloader"
);
console.log(
"Download complete; files:",
await readdir("../../temp/depotDownloader")
);
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { useLocale } from "../../hooks/useLocale";
@@ -57,7 +58,10 @@ export function Body({ stats }: BodyProps) {
alt={unwrap(thisShell.name!)}
width={16}
height={16}
src={`/api/icons/shells/${thisShell.icon}.webp`}
src={alias(
"api",
`/icons/shells/${thisShell.icon}.webp`,
)}
/>
</IconButton>
))}
@@ -1,4 +1,4 @@
import { availableProvisions, permanentSkills } from "@blitzkit/core";
import { alias, availableProvisions, permanentSkills } from "@blitzkit/core";
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
import {
@@ -231,7 +231,10 @@ export function CompareTable({ stats }: CompareTableProps) {
alt={module}
width={24}
height={24}
src={`/api/icons/modules/${module}.webp`}
src={alias(
"api",
`/icons/modules/${module}.webp`,
)}
style={{
marginLeft: index > 0 ? -8 : 0,
objectFit: "contain",
@@ -320,7 +323,10 @@ export function CompareTable({ stats }: CompareTableProps) {
.name!,
)}
key={provision}
src={`/api//icons/provisions/${provision}.webp`}
src={alias(
"api",
`//icons/provisions/${provision}.webp`,
)}
style={{
left: "50%",
/**
@@ -525,7 +531,10 @@ export function CompareTable({ stats }: CompareTableProps) {
].name!,
)}
key={consumable}
src={`/api/icons/consumables/${consumable}.webp`}
src={alias(
"api",
`/icons/consumables/${consumable}.webp`,
)}
style={{
left: "50%",
/**
@@ -617,7 +626,7 @@ export function CompareTable({ stats }: CompareTableProps) {
variant={camouflage ? "solid" : "outline"}
>
<img
src={`/api/icons/camo.webp`}
src={alias("api", `/icons/camo.webp`)}
style={{
width: "1em",
height: "1em",
@@ -1,4 +1,4 @@
import { permanentSkills, romanize } from "@blitzkit/core";
import { alias, permanentSkills, romanize } from "@blitzkit/core";
import { Flex } from "@radix-ui/themes";
import { api } from "../core/blitzkit/api";
import { GenericTankComponentButton } from "./ModuleButtons/GenericTankComponentButton";
@@ -27,7 +27,7 @@ export function CrewSkillManager({
special={!permanentSkills.includes(skill)}
selected={level > 0}
discriminator={level === 0 ? undefined : romanize(level)}
icon={`/api/icons/skills/${skill}.webp`}
icon={alias("api", `/icons/skills/${skill}.webp`)}
onClick={(event) => {
if (!onChange) return;
@@ -1,4 +1,4 @@
import { Avatar } from "@blitzkit/core";
import { alias, Avatar } from "@blitzkit/core";
import { Cross1Icon, DownloadIcon } from "@radix-ui/react-icons";
import {
Dialog,
@@ -18,7 +18,10 @@ type GalleryCardProps = MaybeSkeletonComponentProps<{
}>;
export function GalleryCard(props: GalleryCardProps) {
const src = `/api/gallery/avatars/${!props.skeleton && `${props.avatar.id}${props.avatar.extension}`}`;
const src = alias(
"api",
`/gallery/avatars/${!props.skeleton && `${props.avatar.id}${props.avatar.extension}`}`,
);
const card = useRef<HTMLDivElement>(null!);
const { unwrap } = useLocale();
+5 -1
View File
@@ -1,4 +1,5 @@
import {
alias,
SEARCH_KEYS,
TankDefinition,
TIER_ROMAN_NUMERALS,
@@ -149,7 +150,10 @@ export function Guesser() {
prefix={
<img
style={{ width: "1em", height: "1em" }}
src={`/api/flags/circle/${result.nation}.webp`}
src={alias(
"api",
`/flags/circle/${result.nation}.webp`,
)}
/>
}
discriminator={
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { GenericTankComponentButton } from "./GenericTankComponentButton";
import type { TankComponentButtonProps } from "./TankComponentButton";
@@ -8,7 +9,7 @@ interface ModuleProps extends TankComponentButtonProps {
export function ModuleButton({ module, ...props }: ModuleProps) {
return (
<GenericTankComponentButton
icon={`/api/icons/modules/${module}.webp`}
icon={alias("api", `/icons/modules/${module}.webp`)}
iconStyles={{
top: "50%",
left: "50%",
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { api } from "../../core/blitzkit/api";
import { useLocale } from "../../hooks/useLocale";
import { GenericTankComponentButton } from "./GenericTankComponentButton";
@@ -15,7 +16,7 @@ export function ProvisionButton({ provision, ...props }: ProvisionButtonProps) {
return (
<GenericTankComponentButton
tooltip={unwrap(provisionDefinitions.provisions[provision].name!)}
icon={`/api/icons/provisions/${provision}.webp`}
icon={alias("api", `/icons/provisions/${provision}.webp`)}
{...props}
/>
);
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { GenericTankComponentButton } from "./GenericTankComponentButton";
import type { TankComponentButtonProps } from "./TankComponentButton";
@@ -8,7 +9,7 @@ interface ShellButtonProps extends TankComponentButtonProps {
export function ShellButton({ shell, ...props }: ShellButtonProps) {
return (
<GenericTankComponentButton
icon={`/api/icons/shells/${shell}.webp`}
icon={alias("api", `/icons/shells/${shell}.webp`)}
{...props}
/>
);
@@ -1,3 +1,5 @@
import { alias } from "@blitzkit/core";
interface SmallTankIconProps {
id: number;
size?: number;
@@ -17,7 +19,7 @@ export function SmallTankIcon({ id, size = HEIGHT }: SmallTankIconProps) {
>
<img
alt={`Tank ${id}`}
src={`/api/tanks/${id}/icons/small.webp`}
src={alias("api", `/tanks/${id}/icons/small.webp`)}
style={{
transform: `scale(${(size / HEIGHT) * 100}%)`,
transformOrigin: "top left",
@@ -1,4 +1,4 @@
import { TankType, type TankDefinition } from "@blitzkit/core";
import { alias, TankType, type TankDefinition } from "@blitzkit/core";
import { Flex, Text, type TextProps } from "@radix-ui/themes";
import { uniq } from "lodash-es";
import { forwardRef, type ReactNode } from "react";
@@ -60,7 +60,10 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
className="tank-search-card"
data-provide-link={provideLink}
style={{
backgroundImage: `url(/api/flags/scratched/${tank.nation}.webp)`,
backgroundImage: `url(${alias(
"api",
`/flags/scratched/${tank.nation}.webp`,
)})`,
...style,
}}
{...props}
@@ -76,7 +79,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
>
<img
alt={unwrap(tank.name!)}
src={`/api/tanks/${tank.id}/icons/big.webp`}
src={alias("api", `/tanks/${tank.id}/icons/big.webp`)}
className="image"
draggable={false}
/>
@@ -1,4 +1,5 @@
import {
alias,
GunDefinition,
ShellType,
TANK_CLASSES,
@@ -309,7 +310,7 @@ function NationsFilter() {
width: "1.25em",
height: "1.25em",
}}
src={`/api/flags/circle/${nation}.webp`}
src={alias("api", `/flags/circle/${nation}.webp`)}
/>
))}
</Flex>
@@ -346,7 +347,7 @@ function NationsFilter() {
right="0"
mr="-2"
style={{
backgroundImage: `url(${`/api/flags/scratched/${nation}.webp`})`,
backgroundImage: `url(${alias("api", `/flags/scratched/${nation}.webp`)})`,
backgroundSize: "100%",
backgroundRepeat: "no-repeat",
backgroundPosition: "5rem center",
@@ -679,9 +680,10 @@ function IndividualShellFilter({
{shells[index] !== null && (
<img
style={{ width: "1em", height: "1em" }}
src={`/api/icons/shells/${shellTypeIcons[shells[index]]}${
premium ? "_premium" : ""
}.webp`}
src={alias(
"api",
`/icons/shells/${shellTypeIcons[shells[index]]}${premium ? "_premium" : ""}.webp`,
)}
/>
)}
</IconButton>
@@ -712,9 +714,12 @@ function IndividualShellFilter({
color={selected ? undefined : "gray"}
>
<img
src={`/api/icons/shells/${shellTypeIcons[shellType]}${
premium ? "_premium" : ""
}.webp`}
src={alias(
"api",
`/icons/shells/${shellTypeIcons[shellType]}${
premium ? "_premium" : ""
}.webp`,
)}
style={{ width: "1.25em", height: "1.25em" }}
/>
@@ -883,7 +888,7 @@ function ConsumablesFilter() {
height: "1.25em",
objectFit: "contain",
}}
src={`/api/icons/consumables/${consumable}.webp`}
src={alias("api", `/icons/consumables/${consumable}.webp`)}
/>
))}
@@ -928,7 +933,7 @@ function ConsumablesFilter() {
height: "1.25em",
objectFit: "contain",
}}
src={`/api/icons/consumables/${consumable}.webp`}
src={alias("api", `/icons/consumables/${consumable}.webp`)}
/>
{unwrap(consumableDefinition.name!)}
@@ -977,7 +982,7 @@ function ProvisionsFilter() {
height: "1.25em",
objectFit: "contain",
}}
src={`/api/icons/provisions/${provision}.webp`}
src={alias("api", `/icons/provisions/${provision}.webp`)}
/>
))}
@@ -1022,7 +1027,7 @@ function ProvisionsFilter() {
height: "1.25em",
objectFit: "contain",
}}
src={`/api/icons/provisions/${provision}.webp`}
src={alias("api", `/icons/provisions/${provision}.webp`)}
/>
{unwrap(provisionDefinition.name!)}
@@ -1151,7 +1156,10 @@ function GameModeAbilitiesFilter() {
height: "1.25em",
objectFit: "contain",
}}
src={`/api/icons/consumables/${consumable}.webp`}
src={alias(
"api",
`/icons/consumables/${consumable}.webp`,
)}
/>
{unwrap(abilityDefinition.name!)}
@@ -1200,7 +1208,10 @@ function GameModeAbilitiesFilter() {
height: "1.25em",
objectFit: "contain",
}}
src={`/api/icons/provisions/${provision}.webp`}
src={alias(
"api",
`/icons/provisions/${provision}.webp`,
)}
/>
{unwrap(powerDefinition.name!)}
@@ -1,4 +1,4 @@
import { isExplosive } from "@blitzkit/core";
import { alias, isExplosive } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Flex, Heading, IconButton, Tooltip } from "@radix-ui/themes";
import { useLocale } from "../../../../../../hooks/useLocale";
@@ -62,7 +62,7 @@ export function Firepower({
alt={unwrap(thisShell.name!)}
width={16}
height={16}
src={`/api/icons/shells/${thisShell.icon}.webp`}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
/>
</IconButton>
</Tooltip>
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Flex, IconButton, Tooltip } from "@radix-ui/themes";
import { useLocale } from "../../../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../../../stores/duel";
@@ -37,7 +38,7 @@ export function ShellSwitcher() {
<img
alt={unwrap(thisShell.name!)}
style={{ width: "1.125rem", height: "1.125rem" }}
src={`/api/icons/shells/${thisShell.icon}.webp`}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
/>
</IconButton>
</Tooltip>
@@ -1,4 +1,4 @@
import { isExplosive } from "@blitzkit/core";
import { alias, isExplosive } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Box, Flex, Text } from "@radix-ui/themes";
import { useRef, useState } from "react";
@@ -124,7 +124,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
/>
<img
src={`/api/icons/shells/${shell.icon}.webp`}
src={alias("api", `/icons/shells/${shell.icon}.webp`)}
style={{
objectFit: "contain",
transform: "translate(-50%, -50%) rotate(-45deg)",
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Badge, Box, Flex, Heading, Switch, Text } from "@radix-ui/themes";
import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from "../../../../stores/duel";
@@ -33,7 +34,7 @@ export function Equalizer() {
style={{
cursor: "pointer",
borderRadius: "var(--radius-3)",
backgroundImage: "url(/api/gamemodes/45/banner.webp)",
backgroundImage: `url(${alias("api", `/gamemodes/45/banner.webp`)})`,
backgroundSize: "cover",
backgroundPosition: "center",
overflow: "hidden",
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Flex, Heading } from "@radix-ui/themes";
import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from "../../../../stores/duel";
@@ -21,7 +22,7 @@ export function Miscellaneous() {
<Flex gap="2" align="center">
{!tank.fixed_camouflage && (
<GenericTankComponentButton
icon={"/api/icons/camo.webp"}
icon={alias("api", "/icons/camo.webp")}
selected={camouflage}
iconStyles={{
top: "50%",
@@ -36,7 +37,7 @@ export function Miscellaneous() {
/>
)}
<GenericTankComponentButton
icon={"/api/icons/boosters/equipment.webp"}
icon={alias("api", "/icons/boosters/equipment.webp")}
selected={cooldownBooster > 0}
banner={
cooldownBooster === 0
@@ -1,4 +1,5 @@
import {
alias,
EngineDefinition,
formatCompact,
GunDefinition,
@@ -110,7 +111,7 @@ function ModuleButton({
<img
alt={unlock.cost!.type}
src={`/api/icons/currencies/${unlock.cost!.type}.webp`}
src={alias("api", `/icons/currencies/${unlock.cost!.type}.webp`)}
style={{
width: 12,
height: 12,
@@ -125,7 +126,10 @@ function ModuleButton({
src={
isTank
? tankIcon(unlock.id)
: `/api/icons/modules/${moduleTypeTypeIconName[unlock.type]}.webp`
: alias(
"api",
`/icons/modules/${moduleTypeTypeIconName[unlock.type]}.webp`,
)
}
style={{
width: isTank ? 64 : 32,
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Box, Flex, Heading, Text } from "@radix-ui/themes";
import { api } from "../../core/blitzkit/api";
import { useLocale } from "../../hooks/useLocale";
@@ -37,7 +38,10 @@ export function GameModeSection() {
direction="column"
style={{
borderRadius: "var(--radius-3)",
background: `url(/api/gamemodes/${id}/banner.webp)`,
background: `url(${alias(
"api",
`/gamemodes/${id}/banner.webp`,
)})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
@@ -45,7 +49,10 @@ export function GameModeSection() {
<Box
flexGrow="1"
style={{
background: `url(/api/gamemodes/${id}/banner.webp)`,
background: `url(${alias(
"api",
`/gamemodes/${id}/banner.webp`,
)})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
@@ -80,7 +87,7 @@ export function GameModeSection() {
height: "1.5em",
objectFit: "contain",
}}
src={`/api/icons/provisions/${id}.webp`}
src={alias("api", `/icons/provisions/${id}.webp`)}
alt={unwrap(provisions.name!)}
/>
{unwrap(provisions.name!)}
@@ -101,7 +108,7 @@ export function GameModeSection() {
height: "1.5em",
objectFit: "contain",
}}
src={`/api/icons/consumables/${id}.webp`}
src={alias("api", `/icons/consumables/${id}.webp`)}
alt={unwrap(consumable.name!)}
/>
{unwrap(consumable.name!)}
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { IconButton } from "@radix-ui/themes";
import { useArmor } from "../../../../../../hooks/useArmor";
import { Duel } from "../../../../../../stores/duel";
@@ -37,7 +38,7 @@ export function DynamicArmorSwitcher() {
>
<img
alt="Dynamic Armor"
src={"/api/icons/consumables/73.webp"}
src={alias("api", "/icons/consumables/73.webp")}
style={{
width: "50%",
height: "50%",
@@ -1,4 +1,5 @@
import {
alias,
resolvePenetrationCoefficient,
TIER_ROMAN_NUMERALS,
uniqueGuns,
@@ -139,7 +140,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<img
alt="Antagonist Gun"
src={"/api/icons/modules/gun.webp"}
src={alias("api", "/icons/modules/gun.webp")}
style={{
width: "65%",
height: "65%",
@@ -208,7 +209,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
>
<img
alt={unwrap(thisShell.name!)}
src={`/api/icons/shells/${thisShell.icon}.webp`}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
style={{
width: "50%",
height: "50%",
@@ -245,7 +246,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
>
<img
alt="Calibrated Shells"
src={"/api/icons/equipment/103.webp"}
src={alias("api", "/icons/equipment/103.webp")}
style={{
width: "50%",
height: "50%",
@@ -270,7 +271,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
>
<img
alt="Enhanced Armor"
src={"/api/icons/equipment/110.webp"}
src={alias("api", "/icons/equipment/110.webp")}
style={{
width: "50%",
height: "50%",
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Flex, type FlexProps, Progress } from "@radix-ui/themes";
import { useProgress } from "@react-three/drei";
@@ -11,7 +12,7 @@ export function TankSandboxLoader({ id, ...props }: TankSandboxLoaderProps) {
return (
<Flex width="100%" height="100%" align="center" justify="center" {...props}>
<img
src={`/api/tanks/${id}/icons/big.webp`}
src={alias("api", `/tanks/${id}/icons/big.webp`)}
style={{ height: "75%", filter: "blur(1rem)" }}
/>
<Progress
@@ -1,5 +1,5 @@
import { Flex, Text, type TextProps } from '@radix-ui/themes';
import { Var } from '../../../../core/radix/var';
import { Flex, Text, type TextProps } from "@radix-ui/themes";
import { Var } from "../../../../core/radix/var";
type ListingProps = TextProps & {
label: string;
@@ -11,7 +11,7 @@ export function Listing({ label, ...props }: ListingProps) {
minWidth="17rem"
justify="between"
gap="4"
style={{ borderBottom: `1px solid ${Var('gray-a9')}` }}
style={{ borderBottom: `1px solid ${Var("gray-a9")}` }}
>
<Text size="4">{label}</Text>
<Text align="right" size="4" weight="medium" {...props} />
@@ -1,25 +0,0 @@
import { StarFilledIcon, StarIcon } from '@radix-ui/react-icons';
import { Box, type BoxProps } from '@radix-ui/themes';
type PartialStarProps = BoxProps & {
fill: number;
};
export function PartialStar({ fill, ...props }: PartialStarProps) {
return (
<Box height="1em" width="1em" position="relative" {...props}>
<StarIcon width="1em" height="1em" />
{fill > 0 && (
<Box
top="0"
left="0"
width={`${fill * 100}%`}
position="absolute"
overflow="hidden"
>
<StarFilledIcon width="1em" height="1em" />
</Box>
)}
</Box>
);
}
@@ -1,44 +0,0 @@
import type { StarsInt } from '@blitzkit/core';
import { type TextProps, Flex, Text } from '@radix-ui/themes';
import { times } from 'lodash-es';
import { PartialStar } from './components/PartialStar';
type StarsProps = TextProps & {
stars: number | null;
onCast?: (value: StarsInt) => void;
};
export function Stars({ stars, onCast, ...props }: StarsProps) {
let color: TextProps['color'] = 'green';
if (stars === null) color = 'gray';
else if (stars < 1.5) color = 'tomato';
else if (stars < 2.5) color = 'orange';
else if (stars < 3.5) color = 'amber';
else if (stars < 4.5) color = 'blue';
return (
<Text color={color} {...props}>
<Flex style={{ cursor: onCast ? 'pointer' : 'unset' }}>
{stars !== null &&
times(Math.floor(stars), (index) => (
<PartialStar
fill={1}
key={index}
onClick={() => onCast?.((index + 1) as StarsInt)}
/>
))}
{stars !== null && stars % 1 !== 0 && <PartialStar fill={stars % 1} />}
{times(5 - Math.ceil(stars ?? 0), (index) => (
<PartialStar
fill={0}
key={index}
onClick={() =>
onCast?.((Math.floor(stars ?? 0) + index + 1) as StarsInt)
}
/>
))}
</Flex>
</Text>
);
}
@@ -1,18 +0,0 @@
import { Flex, Text } from '@radix-ui/themes';
import { Stars } from '../../Stars';
interface StarRowProps {
stars: number | null;
children: string;
}
export function StarRow({ stars, children }: StarRowProps) {
return (
<Text color={stars === null ? 'gray' : undefined}>
<Flex align="center" justify="between" width="15rem">
{children}
<Stars stars={stars} />
</Flex>
</Text>
);
}
@@ -1,179 +0,0 @@
import type { BlitzkitResponse, StarsInt } from "@blitzkit/core";
import { PaperPlaneIcon } from "@radix-ui/react-icons";
import {
AlertDialog,
Button,
Dialog,
Flex,
Spinner,
Text,
} from "@radix-ui/themes";
import { useState } from "react";
import { useLocale } from "../../../../../../hooks/useLocale";
import { App } from "../../../../../../stores/app";
import { Duel } from "../../../../../../stores/duel";
import { Stars } from "../../Stars";
export function VoteCaster() {
const tank = Duel.use((state) => state.protagonist.tank);
const [easiness, setEasiness] = useState<StarsInt | null>(null);
const [firepower, setFirepower] = useState<StarsInt | null>(null);
const [maneuverability, setManeuverability] = useState<StarsInt | null>(null);
const [survivability, setSurvivability] = useState<StarsInt | null>(null);
// true = vote successful, false = vote failed, null = hide alert
const [postVote, setPostVote] = useState<boolean | null>(null);
// true = open, false = closed, null = casting
const [showDialogue, setShowDialogue] = useState<boolean | null>(false);
const { unwrap } = useLocale();
return (
<>
<Dialog.Root open={showDialogue !== false} onOpenChange={setShowDialogue}>
<Dialog.Trigger>
<Button variant="surface">
<PaperPlaneIcon /> Vote
</Button>
</Dialog.Trigger>
<Dialog.Content maxWidth="23rem">
<Dialog.Title>Voting for {unwrap(tank.name!)}</Dialog.Title>
<Flex direction="column" mt="4">
<Flex align="center" justify="between">
<Text>
Easiness{" "}
<Text color="gray" size="2">
{easiness === null ? "(empty)" : `(${easiness})`}
</Text>
</Text>
<Stars stars={easiness} size="5" onCast={setEasiness} />
</Flex>
<Flex align="center" justify="between">
<Text>
Firepower{" "}
<Text color="gray" size="2">
{firepower === null ? "(empty)" : `(${firepower})`}
</Text>
</Text>
<Stars stars={firepower} size="5" onCast={setFirepower} />
</Flex>
<Flex align="center" justify="between">
<Text>
Maneuverability{" "}
<Text color="gray" size="2">
{maneuverability === null
? "(empty)"
: `(${maneuverability})`}
</Text>
</Text>
<Stars
stars={maneuverability}
size="5"
onCast={setManeuverability}
/>
</Flex>
<Flex align="center" justify="between">
<Text>
Survivability{" "}
<Text color="gray" size="2">
{survivability === null ? "(empty)" : `(${survivability})`}
</Text>
</Text>
<Stars stars={survivability} size="5" onCast={setSurvivability} />
</Flex>
</Flex>
<Flex justify="end" mt="4" gap="2">
<Dialog.Close>
<Button variant="outline" color="red">
Cancel
</Button>
</Dialog.Close>
<Button
disabled={
showDialogue === null ||
(easiness === null &&
firepower === null &&
maneuverability === null &&
survivability === null)
}
onClick={() => {
const { wargaming } = App.state.logins;
if (!wargaming) {
return setShowDialogue(false);
}
setShowDialogue(null);
fetch(
`/api/tank-voting/${tank.id}/cast?${
easiness === null ? "" : `easiness=${easiness}&`
}${firepower === null ? "" : `firepower=${firepower}&`}${
maneuverability === null
? ""
: `maneuverability=${maneuverability}&`
}${
survivability === null
? ""
: `survivability=${survivability}&`
}player=${wargaming.id}&token=${wargaming.token}`,
)
.then(
(response) => response.json() as Promise<BlitzkitResponse>,
)
.then((json) => {
setShowDialogue(false);
setPostVote(json.status === "ok");
if (json.status === "error") {
console.error("vote failed", json);
}
});
}}
>
{showDialogue === null ? <Spinner /> : <PaperPlaneIcon />} Vote
</Button>
</Flex>
</Dialog.Content>
</Dialog.Root>
<AlertDialog.Root
open={postVote !== null}
onOpenChange={() => setPostVote(null)}
>
{postVote === true && (
<AlertDialog.Content maxWidth="20rem">
<AlertDialog.Title>Vote casted</AlertDialog.Title>
<AlertDialog.Description>
It may take a few minutes for it to show up.
</AlertDialog.Description>
<Flex justify="end" mt="4">
<AlertDialog.Cancel>
<Button>Okay</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
)}
{postVote === false && (
<AlertDialog.Content maxWidth="20rem">
<AlertDialog.Title>Voting failed</AlertDialog.Title>
<AlertDialog.Description>
So sorry about this! If you're a nerd, feel free to check out the
console.
</AlertDialog.Description>
<Flex justify="end" mt="4">
<AlertDialog.Cancel>
<Button>Gotcha</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
)}
</AlertDialog.Root>
</>
);
}
@@ -1,91 +0,0 @@
import { formatCompact } from '@blitzkit/core';
import { Flex, Heading, Skeleton, Text } from '@radix-ui/themes';
import { memo } from 'react';
import { useLocale } from '../../../../../hooks/useLocale';
import { useTankVotes } from '../../../../../hooks/useTankVotes';
import { App } from '../../../../../stores/app';
import { Duel } from '../../../../../stores/duel';
import { WargamingLoginButton } from '../../../../WargamingLoginButton';
import { StarRow } from './components/StarRow';
import { VoteCaster } from './components/VoteCaster';
export const Votes = memo(
() => {
const id = Duel.use((state) => state.protagonist.tank.id);
const votes = useTankVotes(id);
const wargaming = App.use((state) => state.logins.wargaming);
const { locale } = useLocale();
return (
<Flex direction="column" gap="3">
<Flex justify="between" align="center">
<Heading size="4">
{!votes && <Skeleton height="1em" width="4rem" />}
{votes && formatCompact(locale, votes.votes)}{' '}
{votes && (votes.votes > 1 ? 'votes' : 'vote')}
</Heading>
{!wargaming && (
<WargamingLoginButton variant="surface">
Login to vote
</WargamingLoginButton>
)}
{wargaming && <VoteCaster />}
</Flex>
<Flex gap="0" direction="column">
<Flex
gap={{ initial: '0', sm: '6' }}
direction={{ initial: 'column', sm: 'row' }}
>
<StarRow
stars={votes?.categories.easiness ?? null}
children="Easiness"
/>
<StarRow
stars={votes?.categories.firepower ?? null}
children="Firepower"
/>
</Flex>
<Flex
gap={{ initial: '0', sm: '6' }}
direction={{ initial: 'column', sm: 'row' }}
>
<StarRow
stars={votes?.categories.maneuverability ?? null}
children="Maneuverability"
/>
<StarRow
stars={votes?.categories.survivability ?? null}
children="Survivability"
/>
</Flex>
<Flex justify="center">
<Text size="2" color="gray" mt="2">
<>
{votes ? (
wargaming ? (
votes.last_updated ? (
`You voted on ${new Date(
votes.last_updated,
).toLocaleDateString()}`
) : (
`You haven't voted yet`
)
) : (
'Login to vote'
)
) : (
<Skeleton height="1em" width="5rem" />
)}
</>
</Text>
</Flex>
</Flex>
</Flex>
);
},
() => true,
);
@@ -1,4 +1,9 @@
import { TankPriceType, TankType, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import {
alias,
TankPriceType,
TankType,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import { Box, Flex } from "@radix-ui/themes";
import { Var } from "../../../core/radix/var";
import { useLocale } from "../../../hooks/useLocale";
@@ -35,7 +40,7 @@ export function MetaSection() {
>
<Box
style={{
background: `url(${`/api/flags/scratched/${protagonist.nation}.webp`})`,
background: `url(${alias("api", `/flags/scratched/${protagonist.nation}.webp`)})`,
backgroundRepeat: "no-repeat",
backgroundSize: "min(53rem, 80vw)",
backgroundPosition: "-8rem 50%",
@@ -60,7 +65,10 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt={protagonist.nation}
src={`/api/flags/circle/${protagonist.nation}.webp`}
src={alias(
"api",
`/flags/circle/${protagonist.nation}.webp`,
)}
/>
{
strings.common.nations[
@@ -130,7 +138,7 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt="gold"
src={"/api/icons/currencies/gold.webp"}
src={alias("api", "/icons/currencies/gold.webp")}
/>
</Flex>
</Listing>
@@ -147,12 +155,15 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt={TankPriceType[protagonist.price!.type]}
src={`/api/icons/currencies/${
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`}
src={alias(
"api",
`/icons/currencies/${
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`,
)}
/>
</Flex>
</Listing>
@@ -162,12 +173,15 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt={TankPriceType[protagonist.price!.type]}
src={`/api/icons/currencies/${
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`}
src={alias(
"api",
`/icons/currencies/${
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`,
)}
/>
</Flex>
</Listing>
@@ -183,7 +197,7 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt="xp"
src={"/api/icons/currencies/xp.webp"}
src={alias("api", "/icons/currencies/xp.webp")}
/>
</Flex>
</Listing>
@@ -1,4 +1,4 @@
import { formatCompact, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { alias, formatCompact, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import { Box, Flex, Skeleton, Text } from "@radix-ui/themes";
import { awaitableAverageDefinitions } from "../../../../core/awaitables/averageDefinitions";
@@ -58,7 +58,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
mx={highlight ? "2" : "0"}
style={{
background: highlight
? `url(${`/api/flags/fade/${tank.nation}.webp`})`
? `url(${alias("api", `/flags/fade/${tank.nation}.webp`)})`
: undefined,
backgroundPosition: "center",
backgroundSize: "cover",
@@ -79,7 +79,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
>
<img
alt={unwrap(tank.name!)}
src={`/api/tanks/${id}/icons/big.webp`}
src={alias("api", `/tanks/${id}/icons/big.webp`)}
width={64}
height={64}
style={{
@@ -99,7 +99,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
<Flex gap="1" align="center">
<img
alt="XP"
src={"/api/icons/currencies/xp.webp"}
src={alias("api", "/icons/currencies/xp.webp")}
style={{
width: "1em",
height: "1em",
@@ -114,7 +114,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
<Flex gap="1" align="center">
<img
alt="Silver"
src={"/api/icons/currencies/silver.webp"}
src={alias("api", "/icons/currencies/silver.webp")}
style={{
width: "1em",
height: "1em",
@@ -1,4 +1,4 @@
import { TankType } from "@blitzkit/core";
import { alias, TankType } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from "@radix-ui/react-icons";
import { Flex, Heading, IconButton, ScrollArea, Text } from "@radix-ui/themes";
@@ -105,7 +105,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
<Flex gap="1" align="center">
<img
alt="XP"
src={"/api/icons/currencies/xp.webp"}
src={alias("api", "/icons/currencies/xp.webp")}
style={{
width: "1em",
height: "1em",
@@ -121,7 +121,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
<Flex gap="1" align="center">
<img
alt="Silver"
src={"/api/icons/currencies/silver.webp"}
src={alias("api", "/icons/currencies/silver.webp")}
style={{
width: "1em",
height: "1em",
@@ -1,8 +1,9 @@
import { assertSecret } from "@blitzkit/core";
import { assertSecret, environmentConfig } from "@blitzkit/core";
import type { GetStaticPaths } from "astro";
export function chunkedStaticPaths(chunk: string) {
return function () {
const environment = environmentConfig();
const chunksEnv = assertSecret(import.meta.env.CHUNK);
const chunks = chunksEnv.split(",");
@@ -10,6 +11,8 @@ export function chunkedStaticPaths(chunk: string) {
return [];
}
return [{ params: { [`chunk_${chunk}`]: undefined } }];
return [
{ params: { [`chunk_${chunk}`]: environment.paths[chunk] ?? undefined } },
];
} satisfies GetStaticPaths;
}
+2 -1
View File
@@ -1,10 +1,11 @@
import { alias } from "@blitzkit/core";
import { useLoader } from "@react-three/fiber";
import { Mesh, MeshStandardMaterial } from "three";
import { GLTFLoader } from "three-stdlib";
import { useDispose } from "./useDispose";
export function useModel(id: number) {
const path = `/api/tanks/${id}/model.glb`;
const path = alias("api", `/tanks/${id}/model.glb`);
const gltf = useLoader(GLTFLoader, path);
useDispose(gltf, path);
@@ -1,36 +0,0 @@
import {
unwrapBlitzkitResponse,
type BlitzkitResponse,
type TankVotes,
} from '@blitzkit/core';
import { useEffect, useState } from 'react';
import { App } from '../stores/app';
const cache: Record<string, Promise<TankVotes> | TankVotes> = {};
export function useTankVotes(id: number) {
const [votes, setVotes] = useState<TankVotes | null>(null);
const wargaming = App.use((state) => state.logins.wargaming);
useEffect(() => {
if (!wargaming) return;
if (id in cache) {
Promise.all([cache[id]]).then(([votes]) => setVotes(votes));
} else {
const cacheable = fetch(
`/api/tank-voting/${id}/average${
wargaming ? `?player=${wargaming.id}&token=${wargaming.token}` : ''
}`,
)
.then(
(response) => response.json() as Promise<BlitzkitResponse<TankVotes>>,
)
.then(unwrapBlitzkitResponse);
cacheable.then(setVotes);
cache[id] = cacheable;
}
}, [id, wargaming]);
return votes;
}
@@ -1,4 +1,5 @@
import {
alias,
GunDefinition,
TankDefinition,
TIER_ROMAN_NUMERALS,
@@ -110,7 +111,7 @@ function ModuleChooser({
height: "4em",
objectFit: "contain",
}}
src={`/api/tanks/${tank}/icons/big.webp`}
src={alias("api", `/tanks/${tank}/icons/big.webp`)}
/>
)}
</Button>
@@ -1,6 +1,7 @@
---
import { AstroSeo } from "@astrolib/seo";
import {
alias,
assertSecret,
createDefaultProvisions,
createDefaultSkills,
@@ -51,7 +52,7 @@ const title = literals(strings.website.tools.tankopedia.tank_seo_title, {
});
const url = import.meta.env.SITE + Astro.url.pathname;
const icon = `/api/tanks/${id}/icons/big.webp`;
const icon = alias("api", `/tanks/${id}/icons/big.webp`);
const turret = tank.turrets.at(-1)!;
const gun = turret.guns.at(-1)!;
const shell = gun.shells[0];