Adds track repair time to the Tankopedia survivability stats, matching the value the game lists.
- Added a required `repair_time` to track definitions, filled from each chassis's hysteresis health divided by its health regen per second
- Applied the toolbox, the repair crew skill and the protective kit as multiplicative repair speed bonuses
- Added a Track repair time row under fire chance; it appears once the definitions are regenerated on the next build
Fixes#308
Shows the chance a shot penetrates in the dynamic armor shot card, right-aligned on the shot's result line.
- Penetration varies by ±5% (the same randomisation the armor highlighter shader uses), so the chance is the share of that range where the shot's final outcome is a penetration
- Re-runs the existing shot calculation at different penetration values, so spaced armor, HEAT gap losses and ricochets are taken into account
- For ricochets the chance is shown on the ricocheted shot's line; HE splashes don't count as penetrations
- Adds the `shot_card.stats.penetration_chance` string
Fixes#342
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
Prepares BlitzKit for tier XI tanks ahead of their release. Tier X was hardcoded as the maximum in the tank search filters, the guesser and the tech tree, and had no roman numeral beyond X.
- Added `XI` to `TIER_ROMAN_NUMERALS`
- Added an awaitable tier list derived from the highest tier in the tank definitions, so nothing changes until tier XI tanks exist
- Used it for the tank search tier filters and the guesser tier picker and defaults
- Made the tech tree check for successors instead of assuming tier X is the last tier
Fixes#302
Contact shadows under the tank disappeared when the camera orbited low enough to look up at it. The shadow plane and the ground disc are both transparent and centred on the origin, so three.js sorts them by camera depth. Looking upward flips that order, and because the shadow doesn't write depth, the ground disc was drawn over it.
- Gave `ContactShadows` a `renderOrder` of 1 so it always renders after the ground disc
Fixes#299
On some browsers a thin sliver of the Insanity banner showed through along the rounded edges of the equalizer toggle, most visibly on the right where the gradient is opaque. The banner and the gradient were painted on two separately clipped elements, so their anti-aliased edges didn't line up.
- Moved the gradient onto the outer box as the top layer of its `backgroundImage`, above the banner
- Removed the separate gradient layer from the inner flex
Fixes#297
The fade flag images are 2:1 but the flag only fills the left part, with the rest transparent. That made `cover` + `center` in the tech tree node crop and offset the visible flag.
- Trim the transparent border from the fade flag in the `/flags/fade/[id].webp` route using `sharp`
- Revert the tech tree node back to `cover` + `center`, since the image is now just the flag
The filters on the playlist page stretched into one long strip beside the tank pool, squeezing the pool into the remaining space.
**Changes**
- Place the filters and the generate button in a fixed-width column (`22rem`) on medium screens and up so the filter buttons wrap compactly
- Stretch the generate button to the column width
- Let the tank pool take the remaining width
- Tighten the gap between the two columns
- Keep the existing stacked layout on small screens
Dragging a tank's track scrolled its roughness/metalness texture at twice the speed of the base color and normal textures, so highlights drifted off the track links.
**Cause**
- `translateTexture` added the scroll offset to `roughnessMap` and `metalnessMap` separately
- glTF has a single metallic-roughness texture, and the loader assigns that same texture object to both slots, so it received the offset twice
**Changes**
- Collect the track's maps in a `Set` before scrolling so a texture shared between slots is offset exactly once
Users were no longer sent to their preferred locale. `LocaleRedirector` was emptied in `115fa5aca` after its previous versions caused repeated redirects, and nothing replaced it.
**Cause**
- The earlier logic compared the best locale (a string) against the current locale entry (an object), so the check was always true on non-default locale pages
- That made the redirect fire repeatedly, which led to the logic being removed entirely
**Changes**
- Derive the current locale from the URL, treating the default locale as unprefixed to match how pages are generated
- Redirect only when the best locale differs from the current locale, so it stops after one hop
- Redirect users with a saved `preferred-locale` from any page
- Redirect users without a saved preference from default-locale URLs only, using their browser language, so shared locale links are respected
- Preserve the rest of the path when swapping the locale prefix
- Skip `/api` paths
The mixer's hull/turret/gun row is about 440px wide at its fixed sizes and is centered with `left: 50%` and `translateX(-50%)`, so on phones narrower than that it overflowed past both edges.
Use smaller button sizes on phones and scale the tank images with the viewport (`clamp(2rem, 11vw, 4em)`), with smaller separators and gaps. From 520px up the layout is unchanged.
The static armor panel hid itself with a fixed `right: -6rem`. In languages with longer labels (e.g. Spanish) the panel is wider than that, so part of it stayed visible at the edge.
Anchor the panel at `right: 0` and slide it out with a transform based on its own width (`translateX(calc(100% + var(--space-4)))`) when static armor isn't active, so it always clears the edge regardless of label length.
The "Help translate BlitzKit" link on the home page had only a bottom margin, so it sat flush against the hero above while the tool grid below added extra space.
Add a top margin so the space above matches the space below, leaving the bottom margin unchanged.