Merge branch 'main' into remove-imgur-references

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-10-18 14:03:19 -05:00
commit 73080fd60a
39 files changed
+550 -569

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
@@ -1,7 +1,7 @@
import {
normalizeBoundingBox,
resolveDpm,
unionBoundingBox,
tankBoundingBox,
type TankDefinition,
} from "@blitzkit/core";
import { useMemo } from "react";
@@ -112,10 +112,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
case "survivability.volume": {
const dimensions = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
tankBoundingBox(tankModelDefinition, turretModelDefinition),
);
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
@@ -123,10 +120,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case "survivability.length": {
const bounds = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
tankBoundingBox(tankModelDefinition, turretModelDefinition),
);
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
@@ -3,7 +3,7 @@ import {
normalizeBoundingBox,
resolveDpm,
SEARCH_KEYS,
unionBoundingBox,
tankBoundingBox,
type TankDefinition,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
@@ -137,16 +137,10 @@ export const TankSearch = memo<TankSearchProps>(
const bTurretModelDefinition =
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox(
unionBoundingBox(
aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box!,
),
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition),
);
const bSize = normalizeBoundingBox(
unionBoundingBox(
bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box!,
),
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition),
);
const aVolume = aSize.x * aSize.y * aSize.z;
const bVolume = bSize.x * bSize.y * bSize.z;
@@ -164,16 +158,10 @@ export const TankSearch = memo<TankSearchProps>(
const bTurretModelDefinition =
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox(
unionBoundingBox(
aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box!,
),
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition),
);
const bSize = normalizeBoundingBox(
unionBoundingBox(
bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box!,
),
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition),
);
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
@@ -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",
}}