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];
|
2026-10-08 18:00:57 +13:00
|
|
|
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">
|
2026-10-08 18:00:57 +13:00
|
|
|
<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>
|
|
|
|
|
|
2026-10-08 18:00:57 +13:00
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
}
|