Merge pull request #377 from Hitjack007/armor-penetration-chance

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-10-09 17:28:05 -05:00
commit b6ae34dafa
3 files changed
+74 -15

No files matched your search

@@ -128,19 +128,30 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
? shotStatusColors[shot.out.status] ? shotStatusColors[shot.out.status]
: undefined; : undefined;
const inTitleColor = shotStatusColors[shot.in.status]; 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 ( return (
<Card variant="classic" {...props}> <Card variant="classic" {...props}>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Flex direction="column" gap="1"> <Flex direction="column" gap="1">
<Text color={inTitleColor} weight="bold"> <Flex justify="between" align="center" gap="4">
{literals( <Text color={inTitleColor} weight="bold">
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[ {literals(
shot.in.status strings.website.tools.tankopedia.sandbox.dynamic.shot_card
], .status[shot.in.status],
{ damage: Math.round(shot.damage).toLocaleString(locale) } { damage: Math.round(shot.damage).toLocaleString(locale) }
)} )}
</Text> </Text>
{!hasOutLayers && penetrationChance}
</Flex>
<table <table
style={{ style={{
@@ -204,13 +215,17 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
</Flex> </Flex>
</Inset> </Inset>
<Text color={outTitleColor} weight="bold"> <Flex justify="between" align="center" gap="4">
{literals( <Text color={outTitleColor} weight="bold">
strings.website.tools.tankopedia.sandbox.dynamic.shot_card {literals(
.status[shot.out.status], strings.website.tools.tankopedia.sandbox.dynamic.shot_card
{ damage: Math.round(shot.damage).toLocaleString(locale) } .status[shot.out.status],
)} { damage: Math.round(shot.damage).toLocaleString(locale) }
</Text> )}
</Text>
{penetrationChance}
</Flex>
<Flex direction="column"> <Flex direction="column">
{shot.out.layers.map((layer, index) => { {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({ const omitMaterial = new MeshBasicMaterial({
colorWrite: false, colorWrite: false,
depthWrite: true, depthWrite: true,
@@ -89,6 +119,7 @@ export function SpacedArmorSceneComponent({
intersections: Intersection[], intersections: Intersection[],
allowRicochet: boolean, allowRicochet: boolean,
remainingPenetrationInput?: number, remainingPenetrationInput?: number,
penetrationScale = 1,
) => { ) => {
const { customShell } = Tankopedia.state; const { customShell } = Tankopedia.state;
const shell = customShell ?? Duel.state.antagonist.shell; const shell = customShell ?? Duel.state.antagonist.shell;
@@ -125,6 +156,7 @@ export function SpacedArmorSceneComponent({
Duel.state.protagonist.equipmentMatrix, Duel.state.protagonist.equipmentMatrix,
); );
const penetration = const penetration =
penetrationScale *
shell.penetration!.near * shell.penetration!.near *
resolvePenetrationCoefficient( resolvePenetrationCoefficient(
hasCalibratedShells, hasCalibratedShells,
@@ -367,6 +399,7 @@ export function SpacedArmorSceneComponent({
ricochetIntersections, ricochetIntersections,
false, false,
remainingPenetration, remainingPenetration,
penetrationScale,
); );
shot.in.status = "ricochet"; shot.in.status = "ricochet";
@@ -422,6 +455,16 @@ export function SpacedArmorSceneComponent({
const shot = shoot(event.point, event.intersections, true)!; const shot = shoot(event.point, event.intersections, true)!;
shot.penetrationChance = resolvePenetrationChance((scale) =>
shoot(
event.point,
event.intersections,
true,
undefined,
scale,
),
);
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = shot; draft.shot = shot;
}); });
@@ -43,6 +43,7 @@ export type Shot = {
containsGaps: boolean; containsGaps: boolean;
damage: number; damage: number;
splashRadius?: number; splashRadius?: number;
penetrationChance?: number;
in: { in: {
surfaceNormal: Vector3; surfaceNormal: Vector3;