diff --git a/packages/website/src/components/Armor/components/ShotDisplayCard.tsx b/packages/website/src/components/Armor/components/ShotDisplayCard.tsx
index e27c1de5c..6720458f3 100644
--- a/packages/website/src/components/Armor/components/ShotDisplayCard.tsx
+++ b/packages/website/src/components/Armor/components/ShotDisplayCard.tsx
@@ -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 && (
+
+ {literals(strings.common.units.percentage, {
+ value: (shot.penetrationChance * 100).toFixed(0),
+ })}
+
+ );
return (
-
- {literals(
- strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
- shot.in.status
- ],
- { damage: Math.round(shot.damage).toLocaleString(locale) }
- )}
-
+
+
+ {literals(
+ strings.website.tools.tankopedia.sandbox.dynamic.shot_card
+ .status[shot.in.status],
+ { damage: Math.round(shot.damage).toLocaleString(locale) }
+ )}
+
+
+ {!hasOutLayers && penetrationChance}
+
-
- {literals(
- strings.website.tools.tankopedia.sandbox.dynamic.shot_card
- .status[shot.out.status],
- { damage: Math.round(shot.damage).toLocaleString(locale) }
- )}
-
+
+
+ {literals(
+ strings.website.tools.tankopedia.sandbox.dynamic.shot_card
+ .status[shot.out.status],
+ { damage: Math.round(shot.damage).toLocaleString(locale) }
+ )}
+
+
+ {penetrationChance}
+
{shot.out.layers.map((layer, index) => {
diff --git a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx
index 784836464..57149cbe4 100644
--- a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx
+++ b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx
@@ -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;
});
diff --git a/packages/website/src/stores/tankopedia.ts b/packages/website/src/stores/tankopedia.ts
index 023ab0e87..d6e374a0a 100644
--- a/packages/website/src/stores/tankopedia.ts
+++ b/packages/website/src/stores/tankopedia.ts
@@ -43,6 +43,7 @@ export type Shot = {
containsGaps: boolean;
damage: number;
splashRadius?: number;
+ penetrationChance?: number;
in: {
surfaceNormal: Vector3;