Merge branch 'main' into data-driven

This commit is contained in:
tresabhi committed 2026-11-05 16:46:42 -06:00
commit 5477be35e7
12 files changed
+201 -27

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
# BlitzKit 1.33.0
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
## Features
- Added legendary camouflages to Tankopedia [HitJack]
- The rank bars below every statistic in Tankopedia now shows the distribution of every other tank [HitJack]
- Added a Credits page for everyone who has contributed to BlitzKit
- Added an option to synchronize shells across tanks in Compare [HitJack]
## Fixes
- The tank selector popups are now properly sized vertically, stopping them from clipping the top of the page [HitJack]
- When the lights recede, there is no longer a 1-pixel spot where the light still shines
## Technical Changes
- The BlitzKit API is now split across `legendary.models.blitzkit.app`, `tanks.models.blitzkit.app`, `maps.models.blitzkit.app`, and `api.models.blitzkit.app`
- Added some compatibility for the new tier XI tanks
- More work is needed in this area to implement the new mechanics they bring
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkit",
"version": "1.33.0-beta.1",
"version": "1.33.0",
"license": "BlitzKit License",
"private": true,
"scripts": {
@@ -286,25 +286,31 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
this.assignArmor(armorRaw, armorId, turretArmor);
});
const turretCollisionName = turret.hitTester.collisionModel
.split("/")
.at(-1)!
.split(".")[0]
.toLowerCase();
turret.userString;
let turretCollisionName = this.parsePath!(
turret.hitTester.collisionModel,
).name.toLowerCase();
const isFullYaw = -turretYaw[0] + turretYaw[1] === 360;
if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) {
throw new Error(
`Missing turret collision mesh for ${nation}-${tankKey} ${turretKey}`,
turretCollisionName = "turret_01";
console.warn(
`HACK: Missing turret collision mesh for ${nation}/${tankKey}; turret key: ${
turretKey
}; collision model: ${turretCollisionName}; available options: ${Object.keys(
collisionBoundingBoxes,
).join(", ")}; using ${turretCollisionName} instead`,
);
}
const turretBoundingBox = collisionBoundingBoxes[
turretCollisionName
] ?? {
min: [0, 0, 0] as Vector3Tuple,
max: [0, 0, 0] as Vector3Tuple,
};
let turretBoundingBox = collisionBoundingBoxes[turretCollisionName];
if (turretBoundingBox === undefined) {
console.warn("HACK: missing collision; using 0's");
turretBoundingBox = { min: [0, 0, 0], max: [0, 0, 0] };
}
modelDefinitions.models[tankId].turrets[turretId] = {
armor: turretArmor,
@@ -332,9 +338,18 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
)
.split(" ")
.map(Number) as [number, number];
const gunModel = Number(
let gunModel = Number(
this.parsePath!(gun.models.undamaged).name.split("_")[1],
);
if (tankId === 30497) {
console.warn(
"HACK: using gun_02 for hacker tank instead of gun_01... hehe, that's quite appropriate for this tank haha",
);
gunModel = 2;
}
const front = gun.extraPitchLimits?.front
? gun.extraPitchLimits.front.split(" ").map(Number)
: undefined;
@@ -2,7 +2,7 @@ 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";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import { CompareEphemeral, syncShellSlot } from "../../stores/compareEphemeral";
import { CompareCellDirection } from "../CompareCell";
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
import { CompareRow } from "./CompareRow";
@@ -50,7 +50,11 @@ export function Body({ stats }: BodyProps) {
}}
onClick={() => {
CompareEphemeral.mutate((draft) => {
draft.members[index].shell = thisShell;
if (draft.syncShells) {
syncShellSlot(draft.members, shellIndex);
} else {
draft.members[index].shell = thisShell;
}
});
}}
>
@@ -3,7 +3,7 @@ import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes";
import { api } from "../../api/dynamic";
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import { CompareEphemeral, syncShellSlot } from "../../stores/compareEphemeral";
import {
ComparePersistent,
type DeltaMode,
@@ -22,6 +22,7 @@ export function Controls({
onAddTankDialogOpenChange,
}: ControlsProps) {
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
const syncShells = CompareEphemeral.use((state) => state.syncShells);
const { strings } = useLocale();
return (
@@ -36,11 +37,11 @@ export function Controls({
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}>
<Dialog.Title align="center">
{strings.website.tools.compare.actions.add.title}
</Dialog.Title>
<Dialog.Description align="center">
<Dialog.Description align="center" mb="4">
{strings.website.tools.compare.actions.add.description}
</Dialog.Description>
@@ -111,6 +112,31 @@ export function Controls({
{strings.website.tools.compare.actions.deltas.absolute}
</SegmentedControl.Item>
</SegmentedControl.Root>
<Flex
align="center"
gap="2"
style={{ cursor: "pointer" }}
onClick={() => {
CompareEphemeral.mutate((draft) => {
draft.syncShells = !draft.syncShells;
if (!draft.syncShells || draft.members.length === 0) return;
const [first] = draft.members;
syncShellSlot(
draft.members,
first.gun.shells.findIndex(({ id }) => id === first.shell.id),
);
});
}}
>
<Checkbox variant="classic" checked={syncShells} />
<Text size="2">
{strings.website.tools.compare.actions.sync_shells}
</Text>
</Flex>
</Flex>
);
}
@@ -43,7 +43,7 @@ export function TankControl({ index, slug }: TankControlProps) {
</IconButton>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}>
<Dialog.Title align="center">
{strings.website.common.tank_search.swap_dialog_title}
</Dialog.Title>
@@ -3,7 +3,8 @@ import { useEffect, useState, type ReactNode } from "react";
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
import { InlineSkeleton } from "./InlineSkeleton";
const TIME = 1792598400 * 1000;
const TIME = 1793898000 * 1000;
const HREF = "https://preview.blitzkit.app/tanks";
export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
@@ -31,7 +32,7 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
string = (
<>
revealed in{" "}
in{" "}
<Code highContrast>
{hours}h {minutes}m {seconds}s
</Code>
@@ -47,7 +48,7 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
color="gray"
highContrast
style={{ display: "content" }}
href="https://preview.blitzkit.app/tanks"
href={timeLeft <= 0 ? HREF : undefined}
underline="none"
>
<Flex
@@ -75,11 +76,11 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
<Flex direction="column">
<Heading size={{ initial: "4", sm: "6" }}>
Tier XIs {!skeleton && string}{" "}
More Tier XIs {!skeleton && string}{" "}
{skeleton && <InlineSkeleton width="10rem" />}
</Heading>
<Text size={{ initial: "2", sm: "3" }}>
Get ready to go to the Tankopedia!
Catch the next 3 tanks on BlitzKit Preview
</Text>
</Flex>
</Flex>
@@ -0,0 +1,91 @@
import { memo, useEffect, useMemo, useRef } from "react";
const RESOLUTION = 1000;
const HEIGHT = 8;
const DENSE_TICKS = 100;
interface StatScaleProps {
values: number[];
value: number;
lowerIsBetter?: boolean;
}
export const StatScale = memo<StatScaleProps>(
({ values, value, lowerIsBetter }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const ticks = useMemo(() => {
const min = Math.min(value, ...values);
const max = Math.max(value, ...values);
const range = max - min;
const positions = new Set<number>();
for (const other of values) {
const raw = range === 0 ? 0.5 : (other - min) / range;
const x = Math.round((lowerIsBetter ? 1 - raw : raw) * RESOLUTION);
if (x === 0 || x === RESOLUTION) continue;
positions.add(x);
}
return Array.from(positions, (x) => ({ x }));
}, [values, value, lowerIsBetter]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const draw = () => {
const { width } = canvas.getBoundingClientRect();
const pixelRatio = window.devicePixelRatio || 1;
const canvasWidth = Math.ceil(width * pixelRatio);
const canvasHeight = Math.ceil(HEIGHT * pixelRatio);
if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
const context = canvas.getContext("2d");
if (!context) return;
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
context.clearRect(0, 0, width, HEIGHT);
const styles = getComputedStyle(canvas);
for (const { x } of ticks) {
context.fillStyle = styles.getPropertyValue("--gray-a6").trim();
context.fillRect((x / RESOLUTION) * width, 0, 1, HEIGHT);
}
};
const observer = new ResizeObserver(draw);
observer.observe(canvas);
window.addEventListener("resize", draw);
draw();
return () => {
observer.disconnect();
window.removeEventListener("resize", draw);
};
}, [ticks]);
return (
<canvas
ref={canvasRef}
aria-hidden="true"
style={{
position: "absolute",
top: 0,
bottom: 0,
left: 0,
width: "100%",
height: HEIGHT,
margin: "auto 0",
opacity: Math.min(1, Math.sqrt(DENSE_TICKS / ticks.length)),
pointerEvents: "none",
}}
/>
);
},
);
+3
View File
@@ -0,0 +1,3 @@
import { NAVBAR_HEIGHT } from "./navbar";
export const DIALOG_MAX_HEIGHT = `calc(100svh - 2 * (${NAVBAR_HEIGHT}px + var(--space-4) + var(--space-2)))`;
@@ -5,6 +5,7 @@ import locales from "@blitzkit/i18n/locales.json";
import { api } from "../../../api/dynamic";
import { mixStaticPaths } from "../../../astro/mixStaticPaths";
import { AesonPlug } from "../../../components/AesonPlug";
import { Beg } from "../../../components/Beg";
import HelpTranslate from "../../../components/HelpTranslate.astro";
import HomeCosmetics from "../../../components/HomeCosmetics.astro";
import HomeGames from "../../../components/HomeGames.astro";
@@ -67,6 +68,8 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
<HelpTranslate {locale} />
<Beg />
{environment === "preview" && <AesonPlug />}
<Section>
@@ -117,7 +117,7 @@ function ModuleChooser({
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}>
<Dialog.Title>
{strings.website.common.tank_search.select_dialog_title}
</Dialog.Title>
@@ -8,6 +8,7 @@ export interface CompareMember extends DuelMember {
export interface CompareEphemeral {
crewSkills: Record<string, number>;
equalize: boolean;
syncShells: boolean;
members: CompareMember[];
sorting?: {
direction: "ascending" | "descending";
@@ -21,5 +22,14 @@ export const CompareEphemeral = new Soapstone<
>((crewSkills) => ({
crewSkills,
equalize: false,
syncShells: false,
members: [],
}));
export function syncShellSlot(members: CompareMember[], slot: number) {
for (const member of members) {
const shell = member.gun.shells[slot];
if (shell) member.shell = shell;
}
}