Files
blitzkit/packages/website/src/components/ShotDisplayCard.tsx
T

274 lines
8.2 KiB
TypeScript
Raw Normal View History

2025-09-01 22:56:39 -05:00
import { literals } from "@blitzkit/i18n/src/literals";
2024-09-22 21:27:46 -05:00
import {
Card,
Flex,
Inset,
Separator,
Text,
type CardProps,
2025-09-01 22:56:39 -05:00
} from "@radix-ui/themes";
import type { ComponentProps } from "react";
import { radToDeg } from "three/src/math/MathUtils.js";
2026-09-27 16:46:22 -05:00
import { useLocale } from "../hooks/useLocale";
import type { Shot, ShotLayer } from "../stores/tankopedia";
2025-09-01 22:56:39 -05:00
import { shotStatusColors } from "./ShotDisplay";
import { ArmorType } from "./SpacedArmorScene";
2024-06-28 20:41:24 -05:00
2025-01-13 20:56:30 -06:00
export const layerTypeNames = {
2025-09-01 22:56:39 -05:00
[ArmorType.Primary]: "primary",
[ArmorType.Spaced]: "spaced",
[ArmorType.External]: "external",
null: "gap",
2025-01-13 20:56:30 -06:00
} as const;
2024-06-28 20:41:24 -05:00
function LayerEntry({
layerIndex,
shotStatusColor,
layerName,
layer,
angle,
}: {
layerIndex?: string;
2025-09-01 22:56:39 -05:00
shotStatusColor: ComponentProps<typeof Text>["color"];
2024-06-28 20:41:24 -05:00
layerName: string;
2024-09-22 21:12:18 -05:00
layer: ShotLayer;
2024-06-28 20:41:24 -05:00
angle?: number;
}) {
2025-01-13 20:56:30 -06:00
const { locale, strings } = useLocale();
2025-01-07 12:47:55 -06:00
2024-06-28 20:41:24 -05:00
return (
2025-08-29 23:24:12 -05:00
<tr>
<td>
<Text size="1" color="gray">
{layerIndex}
</Text>
</td>
2024-06-28 20:41:24 -05:00
2025-08-29 23:24:12 -05:00
<td>
<Text size="2" color={shotStatusColor}>
{layerName}
</Text>
</td>
<td>
{layer.type === null && (
2025-08-29 23:28:49 -05:00
<Text size="2" color={shotStatusColor}>
2025-10-02 10:15:38 -05:00
{literals(strings.common.units.mm, {
value: Math.round(layer.distance * 1000).toLocaleString(locale),
})}
2025-08-29 23:28:49 -05:00
</Text>
2025-08-29 23:24:12 -05:00
)}
{layer.type === ArmorType.External && (
<Text size="2" color={shotStatusColor}>
2025-10-02 10:15:38 -05:00
{literals(strings.common.units.mm, {
value: Math.round(layer.thickness).toLocaleString(locale),
})}
2024-06-28 20:41:24 -05:00
</Text>
2025-08-29 23:24:12 -05:00
)}
{(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
<Text size="2" color={shotStatusColor}>
2025-10-02 10:15:38 -05:00
{angle === undefined
? literals(strings.common.units.mm, {
value: Math.round(layer.thicknessAngled).toLocaleString(
locale
),
})
: literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
2025-01-13 20:56:30 -06:00
.stats.thickness_and_angle,
2025-10-02 10:15:38 -05:00
{
thickness: Math.round(layer.thicknessAngled).toLocaleString(
locale
),
angle: radToDeg(angle ?? 0).toFixed(0),
}
)}
2024-06-28 20:41:24 -05:00
</Text>
2025-08-29 23:24:12 -05:00
)}
</td>
2024-06-28 20:41:24 -05:00
2025-08-29 23:28:49 -05:00
<td>
{layer.type === null && (
<Text size="2" color="gray">
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.ricochet_loss,
2025-10-02 10:15:38 -05:00
{ loss: Math.max(-100, -50 * layer.distance).toFixed(0) }
2025-08-29 23:28:49 -05:00
)}
</Text>
)}
{(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
2024-06-28 20:41:24 -05:00
<Text size="2" color="gray">
2025-01-13 20:56:30 -06:00
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.nominal,
2025-10-02 10:15:38 -05:00
{ thickness: Math.round(layer.thickness).toLocaleString(locale) }
2025-01-13 20:56:30 -06:00
)}
2024-06-28 20:41:24 -05:00
</Text>
2025-08-29 23:28:49 -05:00
)}
</td>
2025-08-29 23:24:12 -05:00
</tr>
2024-06-28 20:41:24 -05:00
);
}
2024-07-29 20:18:31 -05:00
interface ShotDisplayCardProps extends CardProps {
2024-09-22 21:12:18 -05:00
shot: Shot;
2024-07-13 19:49:00 -05:00
}
2024-06-28 20:41:24 -05:00
2024-07-29 20:18:31 -05:00
export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
2025-01-13 20:56:30 -06:00
const { locale, strings } = useLocale();
2025-01-07 12:47:55 -06:00
2024-06-28 20:41:24 -05:00
if (!shot) return null;
const outTitleColor = shot.out
? shotStatusColors[shot.out.status]
: undefined;
const inTitleColor = shotStatusColors[shot.in.status];
const hasOutLayers = shot.out !== undefined && shot.out.layers.length > 0;
const penetrationChance = shot.penetrationChance !== undefined && (
<Text color="gray" weight="bold">
{literals(strings.common.units.percentage, {
value: (shot.penetrationChance * 100).toFixed(0),
})}
</Text>
);
2024-06-28 20:41:24 -05:00
return (
2025-08-29 23:24:12 -05:00
<Card variant="classic" {...props}>
2024-06-28 20:41:24 -05:00
<Flex direction="column" gap="2">
<Flex direction="column" gap="1">
<Flex justify="between" align="center" gap="4">
<Text color={inTitleColor} weight="bold">
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.status[shot.in.status],
{ damage: Math.round(shot.damage).toLocaleString(locale) }
)}
</Text>
{!hasOutLayers && penetrationChance}
</Flex>
2024-06-28 20:41:24 -05:00
2025-08-29 23:24:12 -05:00
<table
style={{
2025-09-01 22:56:39 -05:00
whiteSpace: "nowrap",
borderSpacing: "var(--space-1) 0",
2025-08-29 23:24:12 -05:00
}}
>
2024-06-28 20:41:24 -05:00
{shot.in.layers.map((layer, index) => {
const layerName =
2025-01-13 20:56:30 -06:00
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.element[
layer.type === ArmorType.External
? layer.variant
: layerTypeNames[`${layer.type}`]
];
2024-06-28 20:41:24 -05:00
const layerIndex =
layer.type === null
? undefined
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
const shotStatusColor =
2025-09-01 22:56:39 -05:00
shot.in.status === "splash"
2024-06-28 20:41:24 -05:00
? shotStatusColors.splash
: shotStatusColors[layer.status];
return (
<LayerEntry
key={index}
layer={layer}
layerIndex={layerIndex}
layerName={layerName}
shotStatusColor={shotStatusColor}
angle={
2024-07-29 20:46:57 -05:00
layer.type === ArmorType.Primary ||
2024-06-28 20:41:24 -05:00
layer.type === ArmorType.Spaced
? layer.angle
: undefined
}
/>
);
})}
2025-08-29 23:24:12 -05:00
</table>
2024-06-28 20:41:24 -05:00
</Flex>
{shot.out && shot.out.layers.length > 0 && (
<>
<Inset side="x">
<Flex align="center" gap="2">
<Separator
style={{
flex: 1,
}}
/>
<Text size="1" color="gray">
ricochet (-25% penetration)
</Text>
<Separator
style={{
flex: 1,
}}
/>
</Flex>
</Inset>
<Flex justify="between" align="center" gap="4">
<Text color={outTitleColor} weight="bold">
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.status[shot.out.status],
{ damage: Math.round(shot.damage).toLocaleString(locale) }
)}
</Text>
{penetrationChance}
</Flex>
2024-06-28 20:41:24 -05:00
<Flex direction="column">
{shot.out.layers.map((layer, index) => {
const layerName =
2025-01-13 20:56:30 -06:00
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.element[
layer.type === ArmorType.External
? layer.variant
: layerTypeNames[`${layer.type}`]
];
2024-06-28 20:41:24 -05:00
const shiftedIndex = index + shot.out!.layers.length;
const layerIndex =
layer.type === null
? undefined
2024-09-06 23:17:53 -05:00
: `${
(shot.containsGaps ? shiftedIndex / 2 : shiftedIndex) +
1
}.`;
2024-06-28 20:41:24 -05:00
const shotStatusColor =
2025-09-01 22:56:39 -05:00
shot.out!.status === "splash"
2024-06-28 20:41:24 -05:00
? shotStatusColors.splash
: shotStatusColors[layer.status];
return (
<LayerEntry
key={index}
layer={layer}
layerIndex={layerIndex}
layerName={layerName}
shotStatusColor={shotStatusColor}
angle={
2024-07-29 20:46:57 -05:00
layer.type === ArmorType.Primary ||
2024-06-28 20:41:24 -05:00
layer.type === ArmorType.Spaced
? layer.angle
: undefined
}
/>
);
})}
</Flex>
</>
)}
</Flex>
</Card>
);
}