Merge branch 'main' into stable-tank-sort-order

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-10-07 16:30:42 -05:00
commit aa6268a86b
19 files changed
+136 -42

No files matched your search

+3 -2
View File
@@ -4,12 +4,13 @@
"license": "BlitzKit License", "license": "BlitzKit License",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "cd packages/website && bun run dev", "dev": "cd packages/website && bun --bun run dev",
"build": "cd packages/website && bun run build", "build": "cd packages/website && bun run build",
"discover-ids": "cd packages/scripts && bun run discover-ids", "discover-ids": "cd packages/scripts && bun run discover-ids",
"build-protos": "cd packages/scripts && bun run build-protos", "build-protos": "cd packages/scripts && bun run build-protos",
"record-performance": "cd packages/scripts && bun run record-performance", "record-performance": "cd packages/scripts && bun run record-performance",
"download-client": "cd packages/scripts && bun run download-client" "download-client": "cd packages/scripts && bun run download-client",
"clean-astro": "rm -rf packages/website/.astro packages/website/node_modules/.astro packages/website/node_modules/.vite"
}, },
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
+25 -16
View File
@@ -25,8 +25,8 @@ import {
VehicleDefinitionList, VehicleDefinitionList,
} from "@blitzkit/core"; } from "@blitzkit/core";
import { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract"; import { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json"; import locales from "@blitzkit/i18n/locales.json";
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n/src/strings";
import { parse as parseYaml } from "yaml"; import { parse as parseYaml } from "yaml";
import { BlitzKitAPI } from "../base"; import { BlitzKitAPI } from "../base";
@@ -113,6 +113,9 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
} }
async init() { async init() {
const { existsSync } = await import("fs");
const { readFile, writeFile, mkdir } = await import("fs/promises");
console.log("Initializing virtual file system..."); console.log("Initializing virtual file system...");
await this.vfs.init(); await this.vfs.init();
@@ -155,31 +158,37 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
console.log("Fetching game localizations..."); console.log("Fetching game localizations...");
if (import.meta.env.DEV) {
locales.supported = [locales.supported[0]];
}
let fetchedLocalizations = 0;
await Promise.all( await Promise.all(
locales.supported.map(async ({ locale }) => { locales.supported.map(async ({ locale }) => {
const root = "../../temp/game-strings";
const cachePath = `${root}/${locale}.json`;
if (existsSync(cachePath)) {
console.log(` Cache hit for ${locale}`);
const content = await readFile(cachePath, "utf-8");
this.stringsI18n[locale] = JSON.parse(content) as BlitzStrings;
return;
}
console.log(` Cache miss for ${locale} (this will take a while...)`);
const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale]; const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale];
const cache = await fetch( const networkStrings = (await fetch(
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`, `https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`,
) )
.then((response) => response.text()) .then((response) => response.text())
.then((string) => parseYaml(string) as BlitzStrings); .then(parseYaml)) as BlitzStrings;
const preInstalled = await this.vfs.yaml<BlitzStrings>( const localStrings = await this.vfs.yaml<BlitzStrings>(
`Data/Strings/${blitzLocale}.yaml`, `Data/Strings/${blitzLocale}.yaml`,
); );
const combined = { ...networkStrings, ...localStrings };
this.stringsI18n[locale] = { await mkdir(root, { recursive: true });
...cache, await writeFile(cachePath, JSON.stringify(combined));
...preInstalled,
};
console.log( this.stringsI18n[locale] = combined;
`Fetched localizations for ${locale} (${++fetchedLocalizations}/${locales.supported.length})`,
);
}), }),
); );
@@ -54,6 +54,7 @@ export const TIER_ROMAN_NUMERALS: Record<number, string> = {
8: "VIII", 8: "VIII",
9: "IX", 9: "IX",
10: "X", 10: "X",
11: "XI",
}; };
export const flags: Record<string, string> = { export const flags: Record<string, string> = {
+3
View File
@@ -215,6 +215,7 @@
"title": "Settings", "title": "Settings",
"description": "Change BlitzKit settings", "description": "Change BlitzKit settings",
"dev_mode": "Development mode", "dev_mode": "Development mode",
"auto_load_models": "Auto-load models",
"language": "Language" "language": "Language"
}, },
@@ -458,6 +459,8 @@
}, },
"sandbox": { "sandbox": {
"load_model": "Load model",
"poses": { "poses": {
"hull_down": "Hull down", "hull_down": "Hull down",
"face_hug": "Face hug", "face_hug": "Face hug",
+5 -4
View File
@@ -27,9 +27,10 @@ import {
TextField, TextField,
} from "@radix-ui/themes"; } from "@radix-ui/themes";
import fuzzysort from "fuzzysort"; import fuzzysort from "fuzzysort";
import { debounce, times } from "lodash-es"; import { debounce } from "lodash-es";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { awaitableTankNames } from "../core/awaitables/tankNames"; import { awaitableTankNames } from "../core/awaitables/tankNames";
import { awaitableTiers } from "../core/awaitables/tiers";
import { api } from "../core/blitzkit/api"; import { api } from "../core/blitzkit/api";
import { useLocale } from "../hooks/useLocale"; import { useLocale } from "../hooks/useLocale";
import { Guess, GuessState } from "../stores/guess"; import { Guess, GuessState } from "../stores/guess";
@@ -38,9 +39,10 @@ import { SearchResults } from "./SearchResults";
const { go } = fuzzysort; const { go } = fuzzysort;
const [tankNames, tankDefinitions] = await Promise.all([ const [tankNames, tankDefinitions, TIERS] = await Promise.all([
awaitableTankNames, awaitableTankNames,
api.tankDefinitions(), api.tankDefinitions(),
awaitableTiers,
]); ]);
const ids = Object.keys(tankDefinitions.tanks); const ids = Object.keys(tankDefinitions.tanks);
@@ -193,8 +195,7 @@ export function Guesser() {
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content> <DropdownMenu.Content>
{times(10, (index) => { {TIERS.map((tier) => {
const tier = 10 - index;
const isSelected = tiers.includes(tier); const isSelected = tiers.includes(tier);
return ( return (
@@ -25,8 +25,8 @@ import {
Tooltip, Tooltip,
type FlexProps, type FlexProps,
} from "@radix-ui/themes"; } from "@radix-ui/themes";
import { times } from "lodash-es";
import { Fragment, type ComponentProps, type ReactNode } from "react"; import { Fragment, type ComponentProps, type ReactNode } from "react";
import { awaitableTiers } from "../../../core/awaitables/tiers";
import { api } from "../../../core/blitzkit/api"; import { api } from "../../../core/blitzkit/api";
import { useLocale } from "../../../hooks/useLocale"; import { useLocale } from "../../../hooks/useLocale";
import { App } from "../../../stores/app"; import { App } from "../../../stores/app";
@@ -171,7 +171,7 @@ const GUN_TYPES = Object.keys(
const MAX_ICONS = 4; const MAX_ICONS = 4;
const TIERS = times(10, (i) => 10 - i); const TIERS = await awaitableTiers;
export function FilterControl() { export function FilterControl() {
return ( return (
@@ -1,10 +1,12 @@
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core"; import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { Flex, IconButton, Text } from "@radix-ui/themes"; import { Flex, IconButton, Text } from "@radix-ui/themes";
import { times } from "lodash-es";
import { memo } from "react"; import { memo } from "react";
import { awaitableTiers } from "../../../core/awaitables/tiers";
import { TankFilters } from "../../../stores/tankFilters"; import { TankFilters } from "../../../stores/tankFilters";
import { TankSort } from "../../../stores/tankopediaSort"; import { TankSort } from "../../../stores/tankopediaSort";
const TIERS = await awaitableTiers;
export const TierFilter = memo(() => { export const TierFilter = memo(() => {
const by = TankSort.use((state) => state.by); const by = TankSort.use((state) => state.by);
const search = TankFilters.use((state) => state.search); const search = TankFilters.use((state) => state.search);
@@ -19,8 +21,7 @@ export const TierFilter = memo(() => {
overflow="hidden" overflow="hidden"
style={{ borderRadius: "var(--radius-full)" }} style={{ borderRadius: "var(--radius-full)" }}
> >
{times(10, (index) => { {TIERS.map((tier) => {
const tier = 10 - index;
const selected = tiers.includes(tier); const selected = tiers.includes(tier);
return ( return (
@@ -13,6 +13,7 @@ import { modelTransformEvent } from "../../../../../../../core/blitzkit/modelTra
import { Var } from "../../../../../../../core/radix/var"; import { Var } from "../../../../../../../core/radix/var";
import { useLocale } from "../../../../../../../hooks/useLocale"; import { useLocale } from "../../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../../stores/duel"; import { Duel } from "../../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps"; import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps";
import { VisualizerCard } from "../VisualizerCard"; import { VisualizerCard } from "../VisualizerCard";
import { VisualizerCornerStat } from "../VisualizerCornerStat"; import { VisualizerCornerStat } from "../VisualizerCornerStat";
@@ -43,6 +44,7 @@ export function GunFlexibilityVisualizer({
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const turret = Duel.use((state) => state.protagonist.turret); const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun); const gun = Duel.use((state) => state.protagonist.gun);
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const tankModel = modelDefinition.models[tank.id]; const tankModel = modelDefinition.models[tank.id];
const turretModel = tankModel.turrets[turret.id]; const turretModel = tankModel.turrets[turret.id];
@@ -193,7 +195,7 @@ export function GunFlexibilityVisualizer({
top="0" top="0"
left="0" left="0"
> >
{!skeleton && <FlexibilityCanvas />} {!skeleton && modelRequested && <FlexibilityCanvas />}
<Box <Box
position="absolute" position="absolute"
@@ -77,6 +77,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const revealed = Tankopedia.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics); const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const equalize = Duel.use((state) => state.equalize); const equalize = Duel.use((state) => state.equalize);
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer); const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
@@ -283,7 +284,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<SpallLinerSwitcher /> <SpallLinerSwitcher />
</Flex> </Flex>
{!skeleton && ( {!skeleton && modelRequested && (
<Suspense> <Suspense>
<DynamicArmorSwitcher /> <DynamicArmorSwitcher />
</Suspense> </Suspense>
@@ -1,11 +1,14 @@
import { Flex, Heading } from "@radix-ui/themes"; import { UpdateIcon } from "@radix-ui/react-icons";
import { Button, Flex, Heading } from "@radix-ui/themes";
import { useLocale } from "../../../../../../hooks/useLocale"; import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../stores/duel"; import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia"; import { Tankopedia } from "../../../../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
import { Tracker } from "./components/Tracker"; import { Tracker } from "./components/Tracker";
export function Title() { export function Title({ skeleton }: MaybeSkeletonComponentProps) {
const { unwrap } = useLocale(); const { unwrap, strings } = useLocale();
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const protagonist = Duel.use((state) => state.protagonist.tank); const protagonist = Duel.use((state) => state.protagonist.tank);
const revealed = Tankopedia.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
@@ -21,6 +24,8 @@ export function Title() {
onPointerDown={(event) => { onPointerDown={(event) => {
event.preventDefault(); event.preventDefault();
if (!modelRequested) return;
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.revealed = true; draft.revealed = true;
}); });
@@ -60,7 +65,36 @@ export function Title() {
{name} {name}
</Heading> </Heading>
{!revealed && <Tracker fontSize={fontSize} />} {!skeleton && !revealed && modelRequested && (
<Tracker fontSize={fontSize} />
)}
{!skeleton && !modelRequested && (
<Flex
position="absolute"
left="50%"
top="50%"
width="min(100%, 16rem)"
justify="center"
style={{
transform: `translate(-50%, calc(${fontSize} * 0.75 + var(--space-4) - 50%))`,
}}
>
<Button
variant="solid"
highContrast
onPointerDown={(event) => event.stopPropagation()}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.modelRequested = true;
});
}}
>
<UpdateIcon />
{strings.website.tools.tankopedia.sandbox.load_model}
</Button>
</Flex>
)}
</Flex> </Flex>
); );
} }
@@ -1,6 +1,6 @@
import { Box, Flex } from "@radix-ui/themes"; import { Box, Flex } from "@radix-ui/themes";
import { times } from "lodash-es"; import { times } from "lodash-es";
import { useEffect, useMemo, useRef } from "react"; import { Suspense, useEffect, useMemo, useRef } from "react";
import { api } from "../../../core/blitzkit/api"; import { api } from "../../../core/blitzkit/api";
import { defaultEqualizer } from "../../../core/blitzkit/tankToDuelMember"; import { defaultEqualizer } from "../../../core/blitzkit/tankToDuelMember";
import { useFullScreen } from "../../../hooks/useFullScreen"; import { useFullScreen } from "../../../hooks/useFullScreen";
@@ -20,6 +20,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
const canvas = useRef<HTMLCanvasElement>(null!); const canvas = useRef<HTMLCanvasElement>(null!);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const protagonist = Duel.use((state) => state.protagonist.tank); const protagonist = Duel.use((state) => state.protagonist.tank);
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const thicknessRange = useMemo(() => { const thicknessRange = useMemo(() => {
const entries = Object.values(tankDefinitions.tanks); const entries = Object.values(tankDefinitions.tanks);
const filtered = entries.filter( const filtered = entries.filter(
@@ -131,13 +132,15 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
top="0" top="0"
left="0" left="0"
> >
{!skeleton && ( {!skeleton && modelRequested && (
<TankSandbox ref={canvas} thicknessRange={thicknessRange} /> <Suspense>
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
</Suspense>
)} )}
</Box> </Box>
</Box> </Box>
<Title /> <Title skeleton={skeleton} />
<Options <Options
skeleton={skeleton} skeleton={skeleton}
@@ -127,7 +127,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
</Text> </Text>
</Flex> </Flex>
{averages && nextTanks && tank.tier !== 10 && ( {averages && nextTanks && (
<Text color="gray" size="1" mt="2"> <Text color="gray" size="1" mt="2">
{skeleton && <Skeleton height="1em" width="4em" />} {skeleton && <Skeleton height="1em" width="4em" />}
{!skeleton && {!skeleton &&
@@ -191,7 +191,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
); );
})} })}
{master.tier < 10 && ( {master.successors.length > 0 && (
<> <>
<Arrow /> <Arrow />
{master.successors!.map((id, index) => ( {master.successors!.map((id, index) => (
@@ -0,0 +1,12 @@
import { times } from "lodash-es";
import { api } from "../blitzkit/api";
export const awaitableTiers = api.tankDefinitions().then((tankDefinitions) => {
let max = 0;
for (const tank of Object.values(tankDefinitions.tanks)) {
if (tank.tier > max) max = tank.tier;
}
return times(max, (index) => max - index);
});
@@ -3,11 +3,13 @@ import { useRef } from "react";
export function useStableProgress() { export function useStableProgress() {
const { loaded, total } = useProgress(); const { loaded, total } = useProgress();
const baseline = useRef(loaded);
const progress = useRef(0); const progress = useRef(0);
const pending = total - baseline.current;
progress.current = Math.max( progress.current = Math.max(
progress.current, progress.current,
total === 0 ? 0 : (loaded / total) * 100 pending <= 0 ? 0 : ((loaded - baseline.current) / pending) * 100
); );
return progress.current; return progress.current;
@@ -18,6 +18,10 @@ export function Page({ locale }: LocaleAcceptorProps) {
function Content({ locale }: LocaleAcceptorProps) { function Content({ locale }: LocaleAcceptorProps) {
const developerMode = App.useDeferred((state) => state.developerMode, false); const developerMode = App.useDeferred((state) => state.developerMode, false);
const autoLoadModels = App.useDeferred(
(state) => state.autoLoadModels,
true,
);
const { strings } = useLocale(); const { strings } = useLocale();
return ( return (
@@ -36,6 +40,19 @@ function Content({ locale }: LocaleAcceptorProps) {
/> />
</Flex> </Flex>
<Flex align="center" gap="2" justify="between">
<Text>{strings.website.settings.auto_load_models}</Text>
<Switch
variant="classic"
checked={autoLoadModels}
onCheckedChange={(checked) =>
App.mutate((draft) => {
draft.autoLoadModels = checked;
})
}
/>
</Flex>
<Flex align="center" gap="2" justify="between"> <Flex align="center" gap="2" justify="between">
<Text>{strings.website.settings.language}</Text> <Text>{strings.website.settings.language}</Text>
<LocaleSwitcher locale={locale} /> <LocaleSwitcher locale={locale} />
+2
View File
@@ -8,6 +8,7 @@ export interface WargamingLogin {
export interface App { export interface App {
developerMode: boolean; developerMode: boolean;
autoLoadModels: boolean;
policiesAgreementIndex: number; policiesAgreementIndex: number;
logins: { logins: {
wargaming?: WargamingLogin; wargaming?: WargamingLogin;
@@ -17,6 +18,7 @@ export interface App {
export const App = new Varuna<App>( export const App = new Varuna<App>(
{ {
developerMode: false, developerMode: false,
autoLoadModels: true,
policiesAgreementIndex: -1, policiesAgreementIndex: -1,
logins: {}, logins: {},
}, },
+4 -2
View File
@@ -1,6 +1,8 @@
import type { TankDefinition } from "@blitzkit/core"; import type { TankDefinition } from "@blitzkit/core";
import { times } from "lodash-es";
import { Varuna } from "varuna"; import { Varuna } from "varuna";
import { awaitableTiers } from "../core/awaitables/tiers";
const tiers = await awaitableTiers;
export enum GuessState { export enum GuessState {
Correct, Correct,
@@ -25,5 +27,5 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
correctGuesses: 0, correctGuesses: 0,
streak: 0, streak: 0,
helpingReveal: false, helpingReveal: false,
tiers: times(10, (index) => index + 1), tiers,
})); }));
@@ -9,6 +9,7 @@ import type { ArmorType } from "../components/Armor/components/SpacedArmorScene"
import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent"; import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent";
import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection"; import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection";
import { api } from "../core/blitzkit/api"; import { api } from "../core/blitzkit/api";
import { App } from "./app";
import { TankopediaDisplay } from "./tankopediaPersistent/constants"; import { TankopediaDisplay } from "./tankopediaPersistent/constants";
export interface ShotLayerBase { export interface ShotLayerBase {
@@ -79,6 +80,7 @@ export enum TankopediaRelativeAgainst {
interface Tankopedia { interface Tankopedia {
disturbed: boolean; disturbed: boolean;
revealed: boolean; revealed: boolean;
modelRequested: boolean;
shot?: Shot; shot?: Shot;
skills: Record<string, number>; skills: Record<string, number>;
relativeAgainst: TankopediaRelativeAgainst; relativeAgainst: TankopediaRelativeAgainst;
@@ -110,6 +112,7 @@ const skillDefinitions = await api.skillDefinitions();
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({ export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
disturbed: false, disturbed: false,
revealed: false, revealed: false,
modelRequested: App.state.autoLoadModels,
relativeAgainst: TankopediaRelativeAgainst.Class, relativeAgainst: TankopediaRelativeAgainst.Class,
editStatic: false, editStatic: false,
skills: createDefaultSkills(skillDefinitions), skills: createDefaultSkills(skillDefinitions),