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
This commit is contained in:
Mark Greene committed 2026-10-05 19:52:46 +13:00
1 parent 116c30be88
commit 541da65bfc
9 files changed
+79 -11

No files matched your search

@@ -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 && <FlexibilityCanvas />}
{!skeleton && modelRequested && <FlexibilityCanvas />}
<Box
position="absolute"
@@ -77,6 +77,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const revealed = Tankopedia.use((state) => 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) {
<SpallLinerSwitcher />
</Flex>
{!skeleton && (
{!skeleton && modelRequested && (
<Suspense>
<DynamicArmorSwitcher />
</Suspense>
@@ -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}
</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="surface"
color="gray"
highContrast
onPointerDown={(event) => event.stopPropagation()}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.modelRequested = true;
});
}}
>
<DownloadIcon />
{strings.website.tools.tankopedia.sandbox.load_model}
</Button>
</Flex>
)}
</Flex>
);
}
@@ -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<HTMLCanvasElement>(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 && (
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
{!skeleton && modelRequested && (
<Suspense>
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
</Suspense>
)}
</Box>
</Box>
<Title />
<Title skeleton={skeleton} />
<Options
skeleton={skeleton}