From 541da65bfc910bfa1e80a33cdeb9699fb812894a Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Mon, 5 Oct 2026 19:52:46 +1300 Subject: [PATCH 1/4] Feature: option to turn off automatic tank model loading Lets people on limited data stop the Tankopedia from downloading 3D models automatically. - Added an Automatically load 3D models switch to the settings page, on by default - When off, the tank page hero shows a Load 3D model button where the loading bar sits - Held back every model download on the tank page until requested, including the dynamic armor switcher and the gun flexibility visualizer - Wrapped the tank sandbox in its own suspense boundary so the loading bar shows real progress instead of the whole page waiting on the model - Measured loading progress from when each loading bar appears so it always starts empty - Added the English strings; other languages fall back until translated Fixes #311 --- packages/i18n/strings/en.json | 3 ++ .../GunFlexibilityVisualizer/index.tsx | 4 +- .../HeroSection/components/Options/index.tsx | 3 +- .../components/TankSandbox/Title/index.tsx | 43 +++++++++++++++++-- .../Tankopedia/HeroSection/index.tsx | 11 +++-- .../website/src/hooks/useStableProgress.ts | 4 +- .../[...locale]/settings/_index.tsx | 17 ++++++++ packages/website/src/stores/app/index.ts | 2 + packages/website/src/stores/tankopedia.ts | 3 ++ 9 files changed, 79 insertions(+), 11 deletions(-) diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index fe4ed47b1..c610ad57e 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -215,6 +215,7 @@ "title": "Settings", "description": "Change BlitzKit settings", "dev_mode": "Development mode", + "auto_load_models": "Automatically load 3D models", "language": "Language" }, @@ -458,6 +459,8 @@ }, "sandbox": { + "load_model": "Load 3D model", + "poses": { "hull_down": "Hull down", "face_hug": "Face hug", diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx index 93d7a5e1a..f28050abc 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx @@ -13,6 +13,7 @@ import { modelTransformEvent } from "../../../../../../../core/blitzkit/modelTra import { Var } from "../../../../../../../core/radix/var"; import { useLocale } from "../../../../../../../hooks/useLocale"; import { Duel } from "../../../../../../../stores/duel"; +import { Tankopedia } from "../../../../../../../stores/tankopedia"; import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps"; import { VisualizerCard } from "../VisualizerCard"; import { VisualizerCornerStat } from "../VisualizerCornerStat"; @@ -43,6 +44,7 @@ export function GunFlexibilityVisualizer({ const tank = Duel.use((state) => state.protagonist.tank); const turret = Duel.use((state) => state.protagonist.turret); const gun = Duel.use((state) => state.protagonist.gun); + const modelRequested = Tankopedia.use((state) => state.modelRequested); const tankModel = modelDefinition.models[tank.id]; const turretModel = tankModel.turrets[turret.id]; @@ -193,7 +195,7 @@ export function GunFlexibilityVisualizer({ top="0" left="0" > - {!skeleton && } + {!skeleton && modelRequested && } state.revealed); const disturbed = Tankopedia.use((state) => state.disturbed); const highGraphics = TankopediaPersistent.use((state) => state.highGraphics); + const modelRequested = Tankopedia.use((state) => state.modelRequested); const equalize = Duel.use((state) => state.equalize); const equalizer = Duel.use((state) => state.antagonist.tank.equalizer); @@ -283,7 +284,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) { - {!skeleton && ( + {!skeleton && modelRequested && ( diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx index 694d24f12..b3dab3aaf 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx @@ -1,11 +1,14 @@ -import { Flex, Heading } from "@radix-ui/themes"; +import { DownloadIcon } from "@radix-ui/react-icons"; +import { Button, Flex, Heading } from "@radix-ui/themes"; import { useLocale } from "../../../../../../hooks/useLocale"; import { Duel } from "../../../../../../stores/duel"; import { Tankopedia } from "../../../../../../stores/tankopedia"; +import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps"; import { Tracker } from "./components/Tracker"; -export function Title() { - const { unwrap } = useLocale(); +export function Title({ skeleton }: MaybeSkeletonComponentProps) { + const { unwrap, strings } = useLocale(); + const modelRequested = Tankopedia.use((state) => state.modelRequested); const protagonist = Duel.use((state) => state.protagonist.tank); const revealed = Tankopedia.use((state) => state.revealed); const disturbed = Tankopedia.use((state) => state.disturbed); @@ -21,6 +24,8 @@ export function Title() { onPointerDown={(event) => { event.preventDefault(); + if (!modelRequested) return; + Tankopedia.mutate((draft) => { draft.revealed = true; }); @@ -60,7 +65,37 @@ export function Title() { {name} - {!revealed && } + {!skeleton && !revealed && modelRequested && ( + + )} + + {!skeleton && !modelRequested && ( + + + + )} ); } diff --git a/packages/website/src/components/Tankopedia/HeroSection/index.tsx b/packages/website/src/components/Tankopedia/HeroSection/index.tsx index 11f13d58e..51205fb49 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/index.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/index.tsx @@ -1,6 +1,6 @@ import { Box, Flex } from "@radix-ui/themes"; 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 { defaultEqualizer } from "../../../core/blitzkit/tankToDuelMember"; import { useFullScreen } from "../../../hooks/useFullScreen"; @@ -20,6 +20,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) { const canvas = useRef(null!); const isFullScreen = useFullScreen(); const protagonist = Duel.use((state) => state.protagonist.tank); + const modelRequested = Tankopedia.use((state) => state.modelRequested); const thicknessRange = useMemo(() => { const entries = Object.values(tankDefinitions.tanks); const filtered = entries.filter( @@ -131,13 +132,15 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) { top="0" left="0" > - {!skeleton && ( - + {!skeleton && modelRequested && ( + + + )} - + <Title skeleton={skeleton} /> <Options skeleton={skeleton} diff --git a/packages/website/src/hooks/useStableProgress.ts b/packages/website/src/hooks/useStableProgress.ts index 332ded1d0..396136e51 100644 --- a/packages/website/src/hooks/useStableProgress.ts +++ b/packages/website/src/hooks/useStableProgress.ts @@ -3,11 +3,13 @@ import { useRef } from "react"; export function useStableProgress() { const { loaded, total } = useProgress(); + const baseline = useRef(loaded); const progress = useRef(0); + const pending = total - baseline.current; progress.current = Math.max( progress.current, - total === 0 ? 0 : (loaded / total) * 100 + pending <= 0 ? 0 : ((loaded - baseline.current) / pending) * 100 ); return progress.current; diff --git a/packages/website/src/pages/[...chunk_default]/[...locale]/settings/_index.tsx b/packages/website/src/pages/[...chunk_default]/[...locale]/settings/_index.tsx index c5054b676..374d24198 100644 --- a/packages/website/src/pages/[...chunk_default]/[...locale]/settings/_index.tsx +++ b/packages/website/src/pages/[...chunk_default]/[...locale]/settings/_index.tsx @@ -18,6 +18,10 @@ export function Page({ locale }: LocaleAcceptorProps) { function Content({ locale }: LocaleAcceptorProps) { const developerMode = App.useDeferred((state) => state.developerMode, false); + const autoLoadModels = App.useDeferred( + (state) => state.autoLoadModels, + true, + ); const { strings } = useLocale(); return ( @@ -36,6 +40,19 @@ function Content({ locale }: LocaleAcceptorProps) { /> </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"> <Text>{strings.website.settings.language}</Text> <LocaleSwitcher locale={locale} /> diff --git a/packages/website/src/stores/app/index.ts b/packages/website/src/stores/app/index.ts index 6b15394f2..ba975056c 100644 --- a/packages/website/src/stores/app/index.ts +++ b/packages/website/src/stores/app/index.ts @@ -8,6 +8,7 @@ export interface WargamingLogin { export interface App { developerMode: boolean; + autoLoadModels: boolean; policiesAgreementIndex: number; logins: { wargaming?: WargamingLogin; @@ -17,6 +18,7 @@ export interface App { export const App = new Varuna<App>( { developerMode: false, + autoLoadModels: true, policiesAgreementIndex: -1, logins: {}, }, diff --git a/packages/website/src/stores/tankopedia.ts b/packages/website/src/stores/tankopedia.ts index d2eb8a066..023ab0e87 100644 --- a/packages/website/src/stores/tankopedia.ts +++ b/packages/website/src/stores/tankopedia.ts @@ -9,6 +9,7 @@ import type { ArmorType } from "../components/Armor/components/SpacedArmorScene" import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent"; import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection"; import { api } from "../core/blitzkit/api"; +import { App } from "./app"; import { TankopediaDisplay } from "./tankopediaPersistent/constants"; export interface ShotLayerBase { @@ -79,6 +80,7 @@ export enum TankopediaRelativeAgainst { interface Tankopedia { disturbed: boolean; revealed: boolean; + modelRequested: boolean; shot?: Shot; skills: Record<string, number>; relativeAgainst: TankopediaRelativeAgainst; @@ -110,6 +112,7 @@ const skillDefinitions = await api.skillDefinitions(); export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({ disturbed: false, revealed: false, + modelRequested: App.state.autoLoadModels, relativeAgainst: TankopediaRelativeAgainst.Class, editStatic: false, skills: createDefaultSkills(skillDefinitions), From 00149a3d9d8a0d9aad1ebbbfb372d703ff4885bc Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:03:41 -0500 Subject: [PATCH 2/4] simplify strings --- packages/i18n/strings/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index c610ad57e..04125f138 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -215,7 +215,7 @@ "title": "Settings", "description": "Change BlitzKit settings", "dev_mode": "Development mode", - "auto_load_models": "Automatically load 3D models", + "auto_load_models": "Auto-load models", "language": "Language" }, @@ -459,7 +459,7 @@ }, "sandbox": { - "load_model": "Load 3D model", + "load_model": "Load model", "poses": { "hull_down": "Hull down", From e1b079ca02c233de57ddbb5047f5e763bd68dc67 Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:06:52 -0500 Subject: [PATCH 3/4] high contrast button --- .../HeroSection/components/TankSandbox/Title/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx index b3dab3aaf..69f7a0193 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx @@ -81,8 +81,7 @@ export function Title({ skeleton }: MaybeSkeletonComponentProps) { }} > <Button - variant="surface" - color="gray" + variant="solid" highContrast onPointerDown={(event) => event.stopPropagation()} onClick={() => { From 16be5faae6516e4ba1a5bf8adc68b1ff4a5ef615 Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:08:56 -0500 Subject: [PATCH 4/4] use UpdateIcon --- .../HeroSection/components/TankSandbox/Title/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx index 69f7a0193..d31db7a1f 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/Title/index.tsx @@ -1,4 +1,4 @@ -import { DownloadIcon } from "@radix-ui/react-icons"; +import { UpdateIcon } from "@radix-ui/react-icons"; import { Button, Flex, Heading } from "@radix-ui/themes"; import { useLocale } from "../../../../../../hooks/useLocale"; import { Duel } from "../../../../../../stores/duel"; @@ -90,7 +90,7 @@ export function Title({ skeleton }: MaybeSkeletonComponentProps) { }); }} > - <DownloadIcon /> + <UpdateIcon /> {strings.website.tools.tankopedia.sandbox.load_model} </Button> </Flex>