Merge branch 'main' into calculated-bounding-boxes

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-10-18 13:59:49 -05:00
commit 8132f2d91e
31 files changed
+349 -487

No files matched your search

@@ -128,19 +128,30 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
? 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>
);
return (
<Card variant="classic" {...props}>
<Flex direction="column" gap="2">
<Flex direction="column" gap="1">
<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>
<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>
<table
style={{
@@ -204,13 +215,17 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
</Flex>
</Inset>
<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>
<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>
<Flex direction="column">
{shot.out.layers.map((layer, index) => {
@@ -63,6 +63,36 @@ export type ArmorUserData = {
}
);
const PENETRATION_RANDOMIZATION = 0.05;
function resolvePenetrationChance(shoot: (scale: number) => Shot | null) {
function penetrates(scale: number) {
const shot = shoot(scale);
return (shot?.out ?? shot?.in)?.status === "penetration";
}
let low = 1 - PENETRATION_RANDOMIZATION;
let high = 1 + PENETRATION_RANDOMIZATION;
if (penetrates(low)) return 1;
if (!penetrates(high)) return 0;
for (let iteration = 0; iteration < 16; iteration++) {
const middle = (low + high) / 2;
if (penetrates(middle)) {
high = middle;
} else {
low = middle;
}
}
return (
(1 + PENETRATION_RANDOMIZATION - high) / (2 * PENETRATION_RANDOMIZATION)
);
}
const omitMaterial = new MeshBasicMaterial({
colorWrite: false,
depthWrite: true,
@@ -89,6 +119,7 @@ export function SpacedArmorSceneComponent({
intersections: Intersection[],
allowRicochet: boolean,
remainingPenetrationInput?: number,
penetrationScale = 1,
) => {
const { customShell } = Tankopedia.state;
const shell = customShell ?? Duel.state.antagonist.shell;
@@ -125,6 +156,7 @@ export function SpacedArmorSceneComponent({
Duel.state.protagonist.equipmentMatrix,
);
const penetration =
penetrationScale *
shell.penetration!.near *
resolvePenetrationCoefficient(
hasCalibratedShells,
@@ -367,6 +399,7 @@ export function SpacedArmorSceneComponent({
ricochetIntersections,
false,
remainingPenetration,
penetrationScale,
);
shot.in.status = "ricochet";
@@ -422,6 +455,16 @@ export function SpacedArmorSceneComponent({
const shot = shoot(event.point, event.intersections, true)!;
shot.penetrationChance = resolvePenetrationChance((scale) =>
shoot(
event.point,
event.intersections,
true,
undefined,
scale,
),
);
Tankopedia.mutate((draft) => {
draft.shot = shot;
});
@@ -25,6 +25,7 @@ import { FilterControl } from "./TankSearch/components/FilterControl";
const tankDefinitions = await api.tankDefinitions();
const POOL_HEIGHT = "16rem";
const FILTERS_WIDTH = "32rem";
export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
const [largeWarningOpen, setLargeWarningOpen] = useState(false);
@@ -80,13 +81,19 @@ export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
<Flex justify="center" width="100%" align="center" px="4">
<Flex
flexGrow="1"
gap="9"
gap="6"
align="center"
justify="center"
maxWidth="80rem"
direction={{ initial: "column-reverse", md: "row" }}
>
<Flex direction="column" gap="4" align="center">
<Flex
direction="column"
gap="4"
align="center"
flexShrink="0"
width={{ initial: "100%", md: FILTERS_WIDTH }}
>
<FilterControl />
<Button
@@ -24,6 +24,14 @@ export function Survivability({ stats }: StatsAcceptorProps) {
decimals={0}
value={(stats) => stats.fireChance * 100}
/>
{stats.trackRepairTime !== undefined && (
<InfoWithDelta
value="trackRepairTime"
stats={stats}
deltaType="lowerIsBetter"
decimals={2}
/>
)}
<InfoWithDelta value="viewRange" stats={stats} decimals={0} />
<ViewRangeVisualizer stats={stats} />
<Info
@@ -31,11 +31,11 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
return (
<Flex
position="absolute"
right={display === TankopediaDisplay.StaticArmor ? "0" : "-6rem"}
right="0"
top="50%"
mr="4"
style={{
transform: "translateY(-50%)",
transform: `translate(${display === TankopediaDisplay.StaticArmor ? "0" : "calc(100% + var(--space-4))"}, -50%)`,
userSelect: "none",
transitionDuration: "200ms",
}}