applySpallLiner

This commit is contained in:
tresabhi committed 2026-10-04 17:53:57 -05:00
1 parent 1a19cff268
commit 16637dac71
10 files changed
+133 -36

No files matched your search

+1
View File
@@ -83,6 +83,7 @@
"dynamics": "Dynamics", "dynamics": "Dynamics",
"status": "Status", "status": "Status",
"consumables": "Consumables", "consumables": "Consumables",
"provisions": "Provisions",
"modules": "Modules", "modules": "Modules",
"equipment": "Equipment", "equipment": "Equipment",
"alternative": "Alt" "alternative": "Alt"
@@ -21,6 +21,10 @@
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
} }
&[data-size="major"] {
font-size: 2em;
}
&[data-list-child="true"]:not(:first-child), &[data-list-child="true"]:not(:first-child),
*:not(:first-child) > &[data-parent-list-child="true"] { *:not(:first-child) > &[data-parent-list-child="true"] {
border-top-left-radius: 0; border-top-left-radius: 0;
@@ -7,7 +7,7 @@ export interface ButtonProps extends ComponentProps<"button"> {
color?: Color; color?: Color;
radius?: `${1 | 2 | 3}` | "max"; radius?: `${1 | 2 | 3}` | "max";
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline"; variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
size?: "regular" | "minor"; size?: "regular" | "minor" | "major";
highContrast?: boolean; // TODO: add high contrast highContrast?: boolean; // TODO: add high contrast
listChild?: boolean; listChild?: boolean;
@@ -1,6 +1,9 @@
.section { .section {
max-width: 21rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start;
gap: var(--space-4); gap: var(--space-4);
.header { .header {
@@ -41,9 +44,15 @@
} }
} }
.provisions {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
}
.consumables { .consumables {
display: grid; display: flex;
grid-template-columns: repeat(3, minmax(0, 1fr)); flex-wrap: wrap;
gap: var(--space-3); gap: var(--space-3);
.consumable-wrapper { .consumable-wrapper {
@@ -100,8 +109,6 @@
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
padding: var(--space-3); padding: var(--space-3);
font-size: var(--space-5);
&[data-selected="true"] { &[data-selected="true"] {
z-index: 1; z-index: 1;
} }
@@ -1,12 +1,12 @@
import { alias } from "@blitzkit/core"; import { alias } from "@blitzkit/core";
import type { Consumable, EquipmentSlot } from "@blitzkit/protos"; import type { Consumable, EquipmentSlot, Provision } from "@blitzkit/protos";
import { ClockIcon, ResetIcon, TrashIcon } from "@radix-ui/react-icons"; import { ClockIcon, ResetIcon, TrashIcon } from "@radix-ui/react-icons";
import { chunk } from "lodash-es"; import { chunk } from "lodash-es";
import { api } from "../../api/dynamic"; import { api } from "../../api/dynamic";
import { equipmentColumns } from "../../config/equipment"; import { equipmentColumns } from "../../config/equipment";
import { useConsumables } from "../../hooks/useConsumables";
import { DuelModule, useDuel } from "../../hooks/useDuel"; import { DuelModule, useDuel } from "../../hooks/useDuel";
import { useEquipmentPreset } from "../../hooks/useEquipmentPreset"; import { useEquipmentPreset } from "../../hooks/useEquipmentPreset";
import { useProvision } from "../../hooks/useProvision";
import { useStrings } from "../../hooks/useStrings"; import { useStrings } from "../../hooks/useStrings";
import { useTankCompatibility } from "../../hooks/useTankCompatibility"; import { useTankCompatibility } from "../../hooks/useTankCompatibility";
import { useUnwrapper } from "../../hooks/useUnwrapper"; import { useUnwrapper } from "../../hooks/useUnwrapper";
@@ -22,6 +22,8 @@ import { Tooltip } from "../Tooltip";
import styles from "./index.module.css"; import styles from "./index.module.css";
const equipment = await api.equipment(); const equipment = await api.equipment();
const provisions = await api.provisions();
const consumables = await api.consumables();
interface TankopediaLoadoutProps { interface TankopediaLoadoutProps {
characteristics: ComputedCharacteristics; characteristics: ComputedCharacteristics;
@@ -35,6 +37,7 @@ export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
<> <>
{/* {showModules && <Modules />} */} {/* {showModules && <Modules />} */}
<Equipment /> <Equipment />
<Provisions />
<Consumables characteristics={characteristics} /> <Consumables characteristics={characteristics} />
<State /> <State />
<Status /> <Status />
@@ -122,13 +125,88 @@ function Status() {
); );
} }
function Provisions() {
const strings = useStrings();
const tank = useDuel("protagonist", DuelModule.Tank);
const gun = useDuel("protagonist", DuelModule.Gun);
const isCompatible = useTankCompatibility(tank, gun);
return (
<div className={styles.section}>
<div className={styles.header}>
<Heading size="4">{strings.tanks.loadout.provisions}</Heading>
<IconButton
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.provisions = [];
});
}}
>
<TrashIcon />
</IconButton>
</div>
<div className={styles.provisions}>
{Object.entries(provisions.provisions).map(([id, provision]) => {
if (
provision.game_mode_exclusive ||
!isCompatible(provision.include, provision.exclude)
) {
return null;
}
return (
<ProvisionEntry key={id} id={Number(id)} provision={provision} />
);
})}
</div>
</div>
);
}
interface ProvisionEntryProps {
id: number;
provision: Provision;
}
function ProvisionEntry({ id, provision }: ProvisionEntryProps) {
const isSelected = useProvision("protagonist", id);
const unwrap = useUnwrapper();
return (
<Tooltip tooltip={unwrap(provision.name)}>
<IconButton
size="major"
radius="1"
variant={isSelected ? "surface" : "soft"}
color={isSelected ? undefined : "gray"}
className={styles.consumable}
onClick={() => {
Tankopedia.mutate((draft) => {
const index = draft.protagonist.provisions.indexOf(id);
if (index === -1) {
draft.protagonist.provisions.push(id);
} else {
draft.protagonist.provisions.splice(index, 1);
}
});
}}
>
<img src={alias("api", `/icons/provisions/${provision.id}.webp`)} />
</IconButton>
</Tooltip>
);
}
interface ConsumablesProps { interface ConsumablesProps {
characteristics: ComputedCharacteristics; characteristics: ComputedCharacteristics;
} }
function Consumables({ characteristics }: ConsumablesProps) { function Consumables({ characteristics }: ConsumablesProps) {
const strings = useStrings(); const strings = useStrings();
const consumables = useConsumables();
const tank = useDuel("protagonist", DuelModule.Tank); const tank = useDuel("protagonist", DuelModule.Tank);
const gun = useDuel("protagonist", DuelModule.Gun); const gun = useDuel("protagonist", DuelModule.Gun);
const isCompatible = useTankCompatibility(tank, gun); const isCompatible = useTankCompatibility(tank, gun);
@@ -152,7 +230,10 @@ function Consumables({ characteristics }: ConsumablesProps) {
<div className={styles.consumables}> <div className={styles.consumables}>
{Object.entries(consumables.consumables).map(([id, consumable]) => { {Object.entries(consumables.consumables).map(([id, consumable]) => {
if (!isCompatible(consumable.include, consumable.exclude)) { if (
consumable.game_mode_exclusive ||
!isCompatible(consumable.include, consumable.exclude)
) {
return null; return null;
} }
@@ -174,7 +255,6 @@ function ConsumableEntry({ id, consumable }: ConsumableProps) {
const isSelected = Tankopedia.use((state) => const isSelected = Tankopedia.use((state) =>
state.protagonist.consumables.includes(id), state.protagonist.consumables.includes(id),
); );
const tank = useDuel("protagonist", DuelModule.Tank);
const unwrap = useUnwrapper(); const unwrap = useUnwrapper();
return ( return (
@@ -338,6 +418,7 @@ function EquipmentOption({
return ( return (
<Tooltip tooltip={unwrap(specificEquipment.name)}> <Tooltip tooltip={unwrap(specificEquipment.name)}>
<IconButton <IconButton
size="major"
color={isSelected ? undefined : "gray"} color={isSelected ? undefined : "gray"}
variant={isSelected ? "surface" : "soft"} variant={isSelected ? "surface" : "soft"}
data-selected={isSelected} data-selected={isSelected}
+19 -9
View File
@@ -33,10 +33,28 @@ export const characteristics = {
if (environment.equalize) coefficient *= equalizer.damage; if (environment.equalize) coefficient *= equalizer.damage;
// tungsten shells
script("protagonist", "TungstenTip", (effect) => { script("protagonist", "TungstenTip", (effect) => {
coefficient *= (effect.minDamageFactor + effect.maxDamageFactor) / 2; coefficient *= (effect.minDamageFactor + effect.maxDamageFactor) / 2;
}); });
// reactive armor
script("antagonist", "ShieldKit", (effect) => {
if (shell.type === ShellType.SHELL_TYPE_HE) return;
coefficient *= 1 - effect.shieldCoef;
});
// dynamic armour system
script("antagonist", "ArmorMover", (effect) => {
coefficient *= 1 - effect.bonusValues!.damageReductionPercent / 100;
});
// spall liner
script("antagonist", "AntiHighExplosive", (effect) => {
if (shell.type !== ShellType.SHELL_TYPE_HE) return;
coefficient *= effect.bonusValues!.factor;
});
return coefficient * shell.armor_damage; return coefficient * shell.armor_damage;
}, },
}, },
@@ -46,19 +64,11 @@ export const characteristics = {
compute({ shell, script }) { compute({ shell, script }) {
let coefficient = 1; let coefficient = 1;
// tungsten shells
script("protagonist", "TungstenTip", (effect) => { script("protagonist", "TungstenTip", (effect) => {
coefficient *= (effect.minDamageFactor + effect.maxDamageFactor) / 2; coefficient *= (effect.minDamageFactor + effect.maxDamageFactor) / 2;
}); });
script("antagonist", "ShieldKit", (effect) => {
if (shell.type === ShellType.SHELL_TYPE_HE) return;
coefficient *= 1 - effect.shieldCoef;
});
script("antagonist", "ArmorMover", (effect) => {
coefficient *= 1 - effect.bonusValues!.damageReductionPercent / 100;
});
return coefficient * shell.module_damage; return coefficient * shell.module_damage;
}, },
}, },
@@ -11,7 +11,6 @@ import {
import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient"; import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient";
import { import {
CrewType, CrewType,
ShellType,
TankClass, TankClass,
type EngineDefinition, type EngineDefinition,
type EquipmentDefinitions, type EquipmentDefinitions,
@@ -24,7 +23,6 @@ import {
type TurretDefinition, type TurretDefinition,
} from "@blitzkit/protos"; } from "@blitzkit/protos";
import type { EquipmentMatrix } from "../../stores/duel"; import type { EquipmentMatrix } from "../../stores/duel";
import { SPALL_LINER_HE_DAMAGE_DELTA } from "./spallLiner";
import { defaultEqualizer } from "./tankToDuelMember"; import { defaultEqualizer } from "./tankToDuelMember";
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>; export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
@@ -168,10 +166,6 @@ export function tankCharacteristics(
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]); const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
const burstShells = gun.burst?.count ?? 1; const burstShells = gun.burst?.count ?? 1;
const burstInterShell = gun.burst?.interval; const burstInterShell = gun.burst?.interval;
const armorDamageCoefficient = coefficient([
applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE,
SPALL_LINER_HE_DAMAGE_DELTA,
]);
const assaultDamageCoefficient = const assaultDamageCoefficient =
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type) gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
? (gun.assault_ranges.ranges.find( ? (gun.assault_ranges.ranges.find(
@@ -59,6 +59,13 @@ export function computeCharacteristics(
scriptsMap.protagonist.set(script["#text"], script); scriptsMap.protagonist.set(script["#text"], script);
} }
for (const id of states[side].provisions) {
if (!(id in scripts.provisions)) continue;
const script = scripts.provisions[id];
scriptsMap.protagonist.set(script["#text"], script);
}
for (const index in states[side].equipment) { for (const index in states[side].equipment) {
const choice = states[side].equipment[index]; const choice = states[side].equipment[index];
const id = equipmentPreset.slots[index].options[choice]; const id = equipmentPreset.slots[index].options[choice];
@@ -1,7 +0,0 @@
import { api } from "../api/dynamic";
import { useAwait } from "./useAwait";
export function useConsumables() {
const consumables = useAwait(() => api.consumables(), "consumables");
return consumables;
}
+4 -4
View File
@@ -1,6 +1,6 @@
import { Duel } from '../stores/duel'; import { Tankopedia } from "../stores/tankopedia";
import type { DuelSide } from "./useEquipment";
export function useProvision(id: number) { export function useProvision(side: DuelSide, id: number) {
const provisions = Duel.use((state) => state.protagonist.provisions); return Tankopedia.use((state) => state[side].provisions.includes(id));
return provisions.includes(id);
} }