diff --git a/docs/changelogs/1.33.0.md b/docs/changelogs/1.33.0.md new file mode 100644 index 000000000..731256d06 --- /dev/null +++ b/docs/changelogs/1.33.0.md @@ -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 diff --git a/package.json b/package.json index 399d17fd9..79ebe1629 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkit", - "version": "1.33.0-beta.1", + "version": "1.33.0", "license": "BlitzKit License", "private": true, "scripts": { diff --git a/packages/core/src/blitzkit/api/server/3_models.ts b/packages/core/src/blitzkit/api/server/3_models.ts index 97c821c3e..3693770a8 100644 --- a/packages/core/src/blitzkit/api/server/3_models.ts +++ b/packages/core/src/blitzkit/api/server/3_models.ts @@ -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; diff --git a/packages/website/src/components/Compare/Body.tsx b/packages/website/src/components/Compare/Body.tsx index ee88cd4e4..e9648106e 100644 --- a/packages/website/src/components/Compare/Body.tsx +++ b/packages/website/src/components/Compare/Body.tsx @@ -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; + } }); }} > diff --git a/packages/website/src/components/Compare/Controls.tsx b/packages/website/src/components/Compare/Controls.tsx index 8f0030eea..e31db45ff 100644 --- a/packages/website/src/components/Compare/Controls.tsx +++ b/packages/website/src/components/Compare/Controls.tsx @@ -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({ - + {strings.website.tools.compare.actions.add.title} - + {strings.website.tools.compare.actions.add.description} @@ -111,6 +112,31 @@ export function Controls({ {strings.website.tools.compare.actions.deltas.absolute} + + { + 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), + ); + }); + }} + > + + + {strings.website.tools.compare.actions.sync_shells} + + ); } diff --git a/packages/website/src/components/Compare/TankControl.tsx b/packages/website/src/components/Compare/TankControl.tsx index f30800cb9..4951b9173 100644 --- a/packages/website/src/components/Compare/TankControl.tsx +++ b/packages/website/src/components/Compare/TankControl.tsx @@ -43,7 +43,7 @@ export function TankControl({ index, slug }: TankControlProps) { - + {strings.website.common.tank_search.swap_dialog_title} diff --git a/packages/website/src/components/Countdown.tsx b/packages/website/src/components/Countdown.tsx index 77468ccc8..bfc878efe 100644 --- a/packages/website/src/components/Countdown.tsx +++ b/packages/website/src/components/Countdown.tsx @@ -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(TIME - Date.now()); @@ -31,7 +32,7 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) { string = ( <> - revealed in{" "} + in{" "} {hours}h {minutes}m {seconds}s @@ -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" > - Tier XIs {!skeleton && string}{" "} + More Tier XIs {!skeleton && string}{" "} {skeleton && } - Get ready to go to the Tankopedia! + Catch the next 3 tanks on BlitzKit Preview diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx new file mode 100644 index 000000000..a032c46d2 --- /dev/null +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx @@ -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( + ({ values, value, lowerIsBetter }) => { + const canvasRef = useRef(null); + const ticks = useMemo(() => { + const min = Math.min(value, ...values); + const max = Math.max(value, ...values); + const range = max - min; + const positions = new Set(); + + 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 ( +