diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json
index 534e2fa8d..9ac4145ee 100644
--- a/packages/i18n/strings/en.json
+++ b/packages/i18n/strings/en.json
@@ -527,6 +527,10 @@
"label": "Edit",
"reset": "Reset"
}
+ },
+
+ "spall_liner": {
+ "swap_prompt": "Spall Liner will replace one of your active provisions. Choose which one to remove:"
}
},
diff --git a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx
index f2c770593..784836464 100644
--- a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx
+++ b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx
@@ -18,6 +18,10 @@ import {
import { degToRad } from "three/src/math/MathUtils.js";
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
+import {
+ SPALL_LINER_HE_DAMAGE_DELTA,
+ SPALL_LINER_PROVISION_ID,
+} from "../../../../core/blitzkit/spallLiner";
import { defaultEqualizer } from "../../../../core/blitzkit/tankToDuelMember";
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
import { Duel } from "../../../../stores/duel";
@@ -384,6 +388,13 @@ export function SpacedArmorSceneComponent({
shot.damage *= antagonistEqualizer.damage;
+ const hasSpallLiner = Duel.state.protagonist.provisions.includes(
+ SPALL_LINER_PROVISION_ID,
+ );
+ if (hasSpallLiner && shell.type === ShellType.SHELL_TYPE_HE) {
+ shot.damage *= 1 + SPALL_LINER_HE_DAMAGE_DELTA;
+ }
+
return shot;
},
[camera],
diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/Options/components/SpallLinerSwitcher.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/Options/components/SpallLinerSwitcher.tsx
new file mode 100644
index 000000000..3b66d75dc
--- /dev/null
+++ b/packages/website/src/components/Tankopedia/HeroSection/components/Options/components/SpallLinerSwitcher.tsx
@@ -0,0 +1,103 @@
+import { alias, availableProvisions } from "@blitzkit/core";
+import { Flex, IconButton, Popover, Text } from "@radix-ui/themes";
+import { api } from "../../../../../../core/blitzkit/api";
+import { SPALL_LINER_PROVISION_ID } from "../../../../../../core/blitzkit/spallLiner";
+import { useLocale } from "../../../../../../hooks/useLocale";
+import { Duel } from "../../../../../../stores/duel";
+import { Tankopedia } from "../../../../../../stores/tankopedia";
+import { ProvisionButton } from "../../../../../ModuleButtons/ProvisionButton";
+
+const provisionDefinitions = await api.provisionDefinitions();
+
+export function SpallLinerSwitcher() {
+ const { tank, gun, provisions } = Duel.use((state) => state.protagonist);
+ const { strings, unwrap } = useLocale();
+ const isSpallLinerActive = provisions.includes(SPALL_LINER_PROVISION_ID);
+ const hasSlotAvailable = provisions.length < tank.max_provisions;
+
+ const canEquipSpallLiner = availableProvisions(
+ tank,
+ gun,
+ provisionDefinitions,
+ ).some((provision) => provision.id === SPALL_LINER_PROVISION_ID);
+
+ if (!canEquipSpallLiner) return null;
+
+ function equip(replacing?: number) {
+ Duel.mutate((draft) => {
+ if (replacing !== undefined) {
+ const provisions = draft.protagonist.provisions;
+ provisions[provisions.indexOf(replacing)] = SPALL_LINER_PROVISION_ID;
+ } else {
+ draft.protagonist.provisions.push(SPALL_LINER_PROVISION_ID);
+ }
+ });
+ Tankopedia.mutate((draft) => {
+ draft.shot = undefined;
+ });
+ }
+
+ function unequip() {
+ Duel.mutate((draft) => {
+ draft.protagonist.provisions = draft.protagonist.provisions.filter(
+ (id) => id !== SPALL_LINER_PROVISION_ID,
+ );
+ });
+ Tankopedia.mutate((draft) => {
+ draft.shot = undefined;
+ });
+ }
+
+ const button = (
+ {
+ if (isSpallLinerActive) {
+ unequip();
+ } else if (hasSlotAvailable) {
+ equip();
+ }
+ }}
+ >
+
+
+ );
+
+ if (isSpallLinerActive || hasSlotAvailable) return button;
+
+ return (
+
+ {button}
+
+
+
+
+ {strings.website.tools.tankopedia.sandbox.spall_liner.swap_prompt}
+
+
+
+ {provisions.map((id) => (
+
+ equip(id)}
+ />
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx
index 05fc67d92..5856c66d0 100644
--- a/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx
+++ b/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx
@@ -46,6 +46,7 @@ import { TankSearch } from "../../../../TankSearch";
import { CustomShellButton } from "./components/CustomShellButton";
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
import { QuickInputs } from "./components/QuickInputs";
+import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
import { Thicknesses } from "./components/Thicknesses";
type OptionsProps = MaybeSkeletonComponentProps & {
@@ -278,6 +279,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
}}
/>
+
+
{!skeleton && (
diff --git a/packages/website/src/core/blitzkit/spallLiner.ts b/packages/website/src/core/blitzkit/spallLiner.ts
new file mode 100644
index 000000000..80598d5c1
--- /dev/null
+++ b/packages/website/src/core/blitzkit/spallLiner.ts
@@ -0,0 +1,8 @@
+export const SPALL_LINER_PROVISION_ID = 101;
+
+/**
+ * Matches the in-game reduction Spall Liner grants against HE splash
+ * damage. Shared between the characteristics panel and the Dynamic
+ * Armor 3D viewer's damage readout so the figure isn't duplicated.
+ */
+export const SPALL_LINER_HE_DAMAGE_DELTA = -0.2;
diff --git a/packages/website/src/core/blitzkit/tankCharacteristics.ts b/packages/website/src/core/blitzkit/tankCharacteristics.ts
index d9d041098..f03bbf3e7 100644
--- a/packages/website/src/core/blitzkit/tankCharacteristics.ts
+++ b/packages/website/src/core/blitzkit/tankCharacteristics.ts
@@ -22,6 +22,7 @@ import {
} from "@blitzkit/core";
import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient";
import type { EquipmentMatrix } from "../../stores/duel";
+import { SPALL_LINER_HE_DAMAGE_DELTA } from "./spallLiner";
import { defaultEqualizer } from "./tankToDuelMember";
export type TankCharacteristics = ReturnType;
@@ -170,7 +171,10 @@ export function tankCharacteristics(
coefficient(
[applyReactiveArmor && shell.type !== ShellType.SHELL_TYPE_HE, -0.27],
[applyDynamicArmor, -0.1],
- [applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE, -0.2],
+ [
+ applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE,
+ SPALL_LINER_HE_DAMAGE_DELTA,
+ ],
);
const assaultDamageCoefficient =
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)