mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-14 21:26:08 +01:00
Merge branch 'main' into animated-legendary-camos
This commit is contained in:
commit
99373872ed
15 files changed
+246
-42
No files matched your search
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"version": "1.33.0-beta.1",
|
"version": "1.33.0",
|
||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -79,14 +79,8 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
|||||||
I_HAT,
|
I_HAT,
|
||||||
-MathUtils.degToRad(turretRotation.pitch),
|
-MathUtils.degToRad(turretRotation.pitch),
|
||||||
)
|
)
|
||||||
.applyAxisAngle(
|
.applyAxisAngle(J_HAT, -MathUtils.degToRad(turretRotation.roll))
|
||||||
J_HAT,
|
.applyAxisAngle(K_HAT, -MathUtils.degToRad(turretRotation.yaw));
|
||||||
-MathUtils.degToRad(turretRotation.roll),
|
|
||||||
)
|
|
||||||
.applyAxisAngle(
|
|
||||||
K_HAT,
|
|
||||||
-MathUtils.degToRad(turretRotation.yaw),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const coordinates = position.toArray();
|
const coordinates = position.toArray();
|
||||||
@@ -298,23 +292,29 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
|||||||
|
|
||||||
turret.userString;
|
turret.userString;
|
||||||
|
|
||||||
const turretCollisionName = parsePath(
|
let turretCollisionName = parsePath(
|
||||||
turret.hitTester.collisionModel,
|
turret.hitTester.collisionModel,
|
||||||
).name.toLowerCase();
|
).name.toLowerCase();
|
||||||
const isFullYaw = -turretYaw[0] + turretYaw[1] === 360;
|
const isFullYaw = -turretYaw[0] + turretYaw[1] === 360;
|
||||||
|
|
||||||
if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) {
|
if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) {
|
||||||
throw new Error(
|
turretCollisionName = "turret_01";
|
||||||
`Missing turret collision mesh for ${nation}-${tankKey} ${turretKey}`,
|
|
||||||
|
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[
|
let turretBoundingBox = collisionBoundingBoxes[turretCollisionName];
|
||||||
turretCollisionName
|
|
||||||
] ?? {
|
if (turretBoundingBox === undefined) {
|
||||||
min: [0, 0, 0] as Vector3Tuple,
|
console.warn("HACK: missing collision; using 0's");
|
||||||
max: [0, 0, 0] as Vector3Tuple,
|
turretBoundingBox = { min: [0, 0, 0], max: [0, 0, 0] };
|
||||||
};
|
}
|
||||||
|
|
||||||
modelDefinitions.models[tankId].turrets[turretId] = {
|
modelDefinitions.models[tankId].turrets[turretId] = {
|
||||||
armor: turretArmor,
|
armor: turretArmor,
|
||||||
@@ -342,9 +342,18 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
|||||||
)
|
)
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map(Number) as [number, number];
|
.map(Number) as [number, number];
|
||||||
const gunModel = Number(
|
let gunModel = Number(
|
||||||
parsePath(gun.models.undamaged).name.split("_")[1],
|
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
|
const front = gun.extraPitchLimits?.front
|
||||||
? gun.extraPitchLimits.front.split(" ").map(Number)
|
? gun.extraPitchLimits.front.split(" ").map(Number)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -744,7 +744,8 @@
|
|||||||
"none": "No deltas",
|
"none": "No deltas",
|
||||||
"percentage": "Percentage",
|
"percentage": "Percentage",
|
||||||
"absolute": "Absolute"
|
"absolute": "Absolute"
|
||||||
}
|
},
|
||||||
|
"sync_shells": "Sync shells"
|
||||||
},
|
},
|
||||||
|
|
||||||
"no_tanks": {
|
"no_tanks": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { alias } from "@blitzkit/core";
|
|||||||
import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
|
import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
|
||||||
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||||
import { useLocale } from "../../hooks/useLocale";
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
import { CompareEphemeral, syncShellSlot } from "../../stores/compareEphemeral";
|
||||||
import { CompareCellDirection } from "../CompareCell";
|
import { CompareCellDirection } from "../CompareCell";
|
||||||
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { CompareRow } from "./CompareRow";
|
import { CompareRow } from "./CompareRow";
|
||||||
@@ -50,7 +50,11 @@ export function Body({ stats }: BodyProps) {
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
CompareEphemeral.mutate((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].shell = thisShell;
|
if (draft.syncShells) {
|
||||||
|
syncShellSlot(draft.members, shellIndex);
|
||||||
|
} else {
|
||||||
|
draft.members[index].shell = thisShell;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { PlusIcon, TrashIcon } from "@radix-ui/react-icons";
|
import { PlusIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes";
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
Flex,
|
||||||
|
SegmentedControl,
|
||||||
|
Text,
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import { DIALOG_MAX_HEIGHT } from "../../constants/dialog";
|
||||||
import { api } from "../../core/blitzkit/api";
|
import { api } from "../../core/blitzkit/api";
|
||||||
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||||
import { useLocale } from "../../hooks/useLocale";
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
import { CompareEphemeral, syncShellSlot } from "../../stores/compareEphemeral";
|
||||||
import {
|
import {
|
||||||
ComparePersistent,
|
ComparePersistent,
|
||||||
type DeltaMode,
|
type DeltaMode,
|
||||||
@@ -22,6 +30,7 @@ export function Controls({
|
|||||||
onAddTankDialogOpenChange,
|
onAddTankDialogOpenChange,
|
||||||
}: ControlsProps) {
|
}: ControlsProps) {
|
||||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||||
|
const syncShells = CompareEphemeral.use((state) => state.syncShells);
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,11 +45,11 @@ export function Controls({
|
|||||||
</Button>
|
</Button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
<Dialog.Content>
|
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}>
|
||||||
<Dialog.Title align="center">
|
<Dialog.Title align="center">
|
||||||
{strings.website.tools.compare.actions.add.title}
|
{strings.website.tools.compare.actions.add.title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Description align="center">
|
<Dialog.Description align="center" mb="4">
|
||||||
{strings.website.tools.compare.actions.add.description}
|
{strings.website.tools.compare.actions.add.description}
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
|
|
||||||
@@ -111,6 +120,31 @@ export function Controls({
|
|||||||
{strings.website.tools.compare.actions.deltas.absolute}
|
{strings.website.tools.compare.actions.deltas.absolute}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
</SegmentedControl.Root>
|
</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>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons";
|
import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
import { Dialog, Flex, IconButton } from "@radix-ui/themes";
|
import { Dialog, Flex, IconButton } from "@radix-ui/themes";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { DIALOG_MAX_HEIGHT } from "../../constants/dialog";
|
||||||
import { api } from "../../core/blitzkit/api";
|
import { api } from "../../core/blitzkit/api";
|
||||||
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||||
import { useLocale } from "../../hooks/useLocale";
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
@@ -43,7 +44,7 @@ export function TankControl({ index, slug }: TankControlProps) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
<Dialog.Content>
|
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}>
|
||||||
<Dialog.Title align="center">
|
<Dialog.Title align="center">
|
||||||
{strings.website.common.tank_search.swap_dialog_title}
|
{strings.website.common.tank_search.swap_dialog_title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { useEffect, useState, type ReactNode } from "react";
|
|||||||
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
|
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
|
||||||
import { InlineSkeleton } from "./InlineSkeleton";
|
import { InlineSkeleton } from "./InlineSkeleton";
|
||||||
|
|
||||||
const TIME = 1792598400 * 1000;
|
const TIME = 1793898000 * 1000;
|
||||||
|
const HREF = "https://preview.blitzkit.app/tanks";
|
||||||
|
|
||||||
export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
|
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
|
||||||
@@ -31,7 +32,7 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
|
|
||||||
string = (
|
string = (
|
||||||
<>
|
<>
|
||||||
revealed in{" "}
|
in{" "}
|
||||||
<Code highContrast>
|
<Code highContrast>
|
||||||
{hours}h {minutes}m {seconds}s
|
{hours}h {minutes}m {seconds}s
|
||||||
</Code>
|
</Code>
|
||||||
@@ -47,7 +48,7 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
color="gray"
|
color="gray"
|
||||||
highContrast
|
highContrast
|
||||||
style={{ display: "content" }}
|
style={{ display: "content" }}
|
||||||
href="https://preview.blitzkit.app/tanks"
|
href={timeLeft <= 0 ? HREF : undefined}
|
||||||
underline="none"
|
underline="none"
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -75,11 +76,11 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
|
|
||||||
<Flex direction="column">
|
<Flex direction="column">
|
||||||
<Heading size={{ initial: "4", sm: "6" }}>
|
<Heading size={{ initial: "4", sm: "6" }}>
|
||||||
Tier XIs {!skeleton && string}{" "}
|
More Tier XIs {!skeleton && string}{" "}
|
||||||
{skeleton && <InlineSkeleton width="10rem" />}
|
{skeleton && <InlineSkeleton width="10rem" />}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text size={{ initial: "2", sm: "3" }}>
|
<Text size={{ initial: "2", sm: "3" }}>
|
||||||
Get ready to go to the Tankopedia!
|
Catch the next 3 tanks on BlitzKit Preview
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
+30
-8
@@ -1,5 +1,5 @@
|
|||||||
import { createDefaultSkills } from "@blitzkit/core";
|
import { createDefaultSkills } from "@blitzkit/core";
|
||||||
import { Flex, Progress, Text } from "@radix-ui/themes";
|
import { Box, Flex, Progress, Text } from "@radix-ui/themes";
|
||||||
import { clamp } from "lodash-es";
|
import { clamp } from "lodash-es";
|
||||||
import { memo, useMemo, type ComponentProps, type ReactNode } from "react";
|
import { memo, useMemo, type ComponentProps, type ReactNode } from "react";
|
||||||
import { api } from "../../../../../../core/blitzkit/api";
|
import { api } from "../../../../../../core/blitzkit/api";
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
TankopediaRelativeAgainst,
|
TankopediaRelativeAgainst,
|
||||||
} from "../../../../../../stores/tankopedia";
|
} from "../../../../../../stores/tankopedia";
|
||||||
import { Info, type InfoProps } from "./Info";
|
import { Info, type InfoProps } from "./Info";
|
||||||
|
import { StatScale } from "./StatScale";
|
||||||
|
|
||||||
type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
|
type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
|
||||||
stats: TankCharacteristics;
|
stats: TankCharacteristics;
|
||||||
@@ -122,6 +123,15 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|||||||
return othersValue !== undefined;
|
return othersValue !== undefined;
|
||||||
}) as TankCharacteristics[];
|
}) as TankCharacteristics[];
|
||||||
}, [relativeAgainst, shellIndex, equipmentMatrix, equalize]);
|
}, [relativeAgainst, shellIndex, equipmentMatrix, equalize]);
|
||||||
|
const values = useMemo(
|
||||||
|
() =>
|
||||||
|
others.map((tank) =>
|
||||||
|
typeof props.value === "function"
|
||||||
|
? props.value(tank)!
|
||||||
|
: (tank[props.value] as number),
|
||||||
|
),
|
||||||
|
[others],
|
||||||
|
);
|
||||||
const betterTanks = others.filter((tank) => {
|
const betterTanks = others.filter((tank) => {
|
||||||
const othersValue =
|
const othersValue =
|
||||||
typeof props.value === "function"
|
typeof props.value === "function"
|
||||||
@@ -161,13 +171,25 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|||||||
|
|
||||||
{!noRanking && (
|
{!noRanking && (
|
||||||
<Flex pl={indent ? "2" : "0"} align="center" gap="2">
|
<Flex pl={indent ? "2" : "0"} align="center" gap="2">
|
||||||
<Progress
|
<Box position="relative" flexGrow="1">
|
||||||
variant="soft"
|
<StatScale
|
||||||
size="1"
|
values={values}
|
||||||
value={goodness * 100}
|
value={uhWhatDoICallThisVariable}
|
||||||
color={color}
|
lowerIsBetter={deltaType === "lowerIsBetter"}
|
||||||
style={{ height: "0.125rem", opacity: 0.5 }}
|
/>
|
||||||
/>
|
|
||||||
|
<Progress
|
||||||
|
variant="soft"
|
||||||
|
size="1"
|
||||||
|
value={goodness * 100}
|
||||||
|
color={color}
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
height: "0.125rem",
|
||||||
|
opacity: 0.75,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Text color="gray" size="1">
|
<Text color="gray" size="1">
|
||||||
{clamp(betterTanks.length + 1, 1, others.length)} /{" "}
|
{clamp(betterTanks.length + 1, 1, others.length)} /{" "}
|
||||||
|
|||||||
+91
@@ -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",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import { Suspense, useState, type RefObject } from "react";
|
import { Suspense, useState, type RefObject } from "react";
|
||||||
|
import { DIALOG_MAX_HEIGHT } from "../../../../../constants/dialog";
|
||||||
import { Pose, poseEvent } from "../../../../../core/blitzkit/pose";
|
import { Pose, poseEvent } from "../../../../../core/blitzkit/pose";
|
||||||
import { useEquipment } from "../../../../../hooks/useEquipment";
|
import { useEquipment } from "../../../../../hooks/useEquipment";
|
||||||
import { useFullScreen } from "../../../../../hooks/useFullScreen";
|
import { useFullScreen } from "../../../../../hooks/useFullScreen";
|
||||||
@@ -474,7 +475,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
<Dialog.Content>
|
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}>
|
||||||
<Dialog.Title align="center">
|
<Dialog.Title align="center">
|
||||||
{strings.website.common.tank_search.select_dialog_title}
|
{strings.website.common.tank_search.select_dialog_title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
|||||||
@@ -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)))`;
|
||||||
@@ -6,6 +6,7 @@ import { literals } from "@blitzkit/i18n/src/literals";
|
|||||||
import { Box, Flex, Link } from "@radix-ui/themes";
|
import { Box, Flex, Link } from "@radix-ui/themes";
|
||||||
import { AesonPlug } from "../../../components/AesonPlug";
|
import { AesonPlug } from "../../../components/AesonPlug";
|
||||||
import { Beg } from "../../../components/Beg";
|
import { Beg } from "../../../components/Beg";
|
||||||
|
import { Countdown } from "../../../components/Countdown";
|
||||||
import HomeHero from "../../../components/HomeHero.astro";
|
import HomeHero from "../../../components/HomeHero.astro";
|
||||||
import { Plugs } from "../../../components/Plugs";
|
import { Plugs } from "../../../components/Plugs";
|
||||||
import ToolCard from "../../../components/ToolCard.astro";
|
import ToolCard from "../../../components/ToolCard.astro";
|
||||||
@@ -66,6 +67,10 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
|
|||||||
<BaseLayout {locale}>
|
<BaseLayout {locale}>
|
||||||
<HomeHero />
|
<HomeHero />
|
||||||
|
|
||||||
|
<Countdown client:only="react">
|
||||||
|
<Countdown skeleton slot="fallback" />
|
||||||
|
</Countdown>
|
||||||
|
|
||||||
<Beg />
|
<Beg />
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { ModuleButton } from "../../../../components/ModuleButtons/ModuleButton"
|
|||||||
import { PageWrapper } from "../../../../components/PageWrapper";
|
import { PageWrapper } from "../../../../components/PageWrapper";
|
||||||
import { ScreenshotButton } from "../../../../components/ScreenshotButton";
|
import { ScreenshotButton } from "../../../../components/ScreenshotButton";
|
||||||
import { TankSearch } from "../../../../components/TankSearch";
|
import { TankSearch } from "../../../../components/TankSearch";
|
||||||
|
import { DIALOG_MAX_HEIGHT } from "../../../../constants/dialog";
|
||||||
import { api } from "../../../../core/blitzkit/api";
|
import { api } from "../../../../core/blitzkit/api";
|
||||||
import { curateMixer } from "../../../../core/blitzkit/curateMixer";
|
import { curateMixer } from "../../../../core/blitzkit/curateMixer";
|
||||||
import {
|
import {
|
||||||
@@ -117,7 +118,7 @@ function ModuleChooser({
|
|||||||
</Button>
|
</Button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
<Dialog.Content>
|
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}>
|
||||||
<Dialog.Title>
|
<Dialog.Title>
|
||||||
{strings.website.common.tank_search.select_dialog_title}
|
{strings.website.common.tank_search.select_dialog_title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface CompareMember extends DuelMember {
|
|||||||
export interface CompareEphemeral {
|
export interface CompareEphemeral {
|
||||||
crewSkills: Record<string, number>;
|
crewSkills: Record<string, number>;
|
||||||
equalize: boolean;
|
equalize: boolean;
|
||||||
|
syncShells: boolean;
|
||||||
members: CompareMember[];
|
members: CompareMember[];
|
||||||
sorting?: {
|
sorting?: {
|
||||||
direction: "ascending" | "descending";
|
direction: "ascending" | "descending";
|
||||||
@@ -21,5 +22,14 @@ export const CompareEphemeral = new Varuna<
|
|||||||
>((crewSkills) => ({
|
>((crewSkills) => ({
|
||||||
crewSkills,
|
crewSkills,
|
||||||
equalize: false,
|
equalize: false,
|
||||||
|
syncShells: false,
|
||||||
members: [],
|
members: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export function syncShellSlot(members: CompareMember[], slot: number) {
|
||||||
|
for (const member of members) {
|
||||||
|
const shell = member.gun.shells[slot];
|
||||||
|
|
||||||
|
if (shell) member.shell = shell;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user