Merge branch 'main' into filter-defaults

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-10-04 14:07:27 -05:00
commit 1bdd8bab9e
22 files changed
+450 -43

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
name: 🐞 Bug Report
description: BlitzKit is not behaving as expected.
labels: ["bug"]
type: bug
body:
- type: markdown
attributes:
value: Thank you so much for reporting an issue! Please fill out the form below in its entirety and we'll address your issue as soon as possible.
- type: textarea
validations:
required: true
attributes:
label: What happened?
description: What about BlitzKit's behavior did you find unexpected?
- type: textarea
attributes:
label: How do you reproduce the issue?
description: What steps one must follow to recreate your issue?
- type: dropdown
attributes:
label: Branch
description: What branch of BlitzKit were you using? Please see the "Branch" section of the footer at the very bottom of any page.
options:
- Production
- Preview
- OpenTest
default: 0
validations:
required: true
- type: input
attributes:
label: Discord username
description: Do you have a Discord account that we can reach out to for further information?
+10
View File
@@ -0,0 +1,10 @@
blank_issues_enabled: false
contact_links:
- name: 🎮 Discord Server
about: Got questions? We answer better on Discord!
url: https://discord.gg/nDt7AjGJQH
- name: ✉️ Email
about: Reach out to BlitzKit formally through email.
url: https://blitzkit.app/contact
+16
View File
@@ -0,0 +1,16 @@
name: 💡 Feature Request
description: BlitzKit could use a new idea.
labels: ["enhancement"]
type: feature
body:
- type: markdown
attributes:
value: Thank you so much for pitching in an idea!
- type: textarea
validations:
required: true
attributes:
label: What's your idea?
description: Please be descriptive and feel free to add drawings/screenshots if needed.
+71 -3
View File
@@ -1,7 +1,75 @@
# BlitzKit
Tools for everything and anything, World of Tanks Blitz.
Your Blitz experience, elevated. https://blitzkit.app/.
This GitHub repository contains the code for BlitzKit and all its services. If you are here, looking to use BlitzKit, go to https://blitzkit.app/.
## Developing: Prerequisites
If you intend to contribute to the code, note that you may need to check out sensitive, closed-source submodules to locally develop certain aspects of BlitzKit. Communicate with `@tresabhi` on Discord to get started or discuss sensitive content, such as environment variables or submodules.
There's a few things you'll need to get started. If this is your first time working with BlitzKit, it's completely safe to ignore all optional prerequisites below to get things running as quickly as possible.
### Bun
[Bun](https://bun.sh/) is the package manager and runtime that BlitzKit uses.
Ideally, you should also uninstall Node.js if you have it installed to avoid any cases where Node.js is used to run code instead of Bun which happens in some cases. But, this is not required and is unlikely to cause issues.
### Protocol Buffer
BlitzKit uses [Protocol Buffer's protoc CLI](https://protobuf.dev/installation/) to generate efficiently packed data structures to serve data to clients in place of JSON.
### World of Tanks Blitz
You will need a local installation of [World of Tanks Blitz](https://wotblitz.com/) for BlitzKit to build off.
### aria2 (Optional)
If you plan to use BlitzKit's client-management CLI for BlitzKit Previews, you will need to [get aria2](https://aria2.github.io/) to speed up your downloads.
### libarchive (Optional)
If you plan to use BlitzKit's client-management CLI for BlitzKit OpenTests, you will need to [get libarchive](https://www.libarchive.org/) to keep your downloads completely in-memory.
### Linux (Optional)
All work on BlitzKit is done on Linux and macOS. It is theoretically possible to work on Windows; please feel free to try and share your discoveries with us!
## Developing: Setting Up
Start by cloning the repository and changing directories.
```bash
git clone https://github.com/blitzkit/blitzkit.git
cd blitzkit
```
> [!NOTE]
> There is a closed source submodule under `packages/closed` that you can safely ignore. This sole purpose of this submodule is to provide `dvp.ts`, a proprietary Wargaming game asset tree compression tool. You will not need this at any point.
Then, download the dependencies with Bun.
```bash
bun install
```
Finally, compile the proto files into TypeScript.
```bash
build build-protos
```
## Developing: Environment
Go through the different packages in the `packages` directory and clone their respective `template.env`'s to `.env` files. Follow the instructions in the `.env` files to fill them out as much as you can.
## Developing: Running
Launching the website is as easy as running the `dev` script.
```bash
bun dev
```
If you ever make a change to any proto file, make sure to rebuild them.
```bash
build build-protos
```
@@ -350,6 +350,15 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
const tankClass =
this.blitzTankClassToBlitzkit[blitzTankClass as BlitzTankClass];
const canonicalFullName = this.getString(tank.userString);
const name =
(tank.shortUserString
? this.getString(tank.shortUserString)
: undefined) ?? canonicalFullName;
// only supply full name if it's different from the displayed short name to save some bytes
const name_full =
name === canonicalFullName ? undefined : canonicalFullName;
tankDefinitions.tanks[tankId] = {
ancestors: [],
successors: [],
@@ -367,10 +376,8 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
typeof equipment === "string" ? equipment : equipment.at(-1)!,
max_consumables: tankDefinition.root.consumableSlots,
max_provisions: tankDefinition.root.provisionSlots,
name:
(tank.shortUserString
? this.getString(tank.shortUserString)
: undefined) ?? this.getString(tank.userString),
name,
name_full,
slug,
nation,
type: tankTags.includes("collectible")
+13
View File
@@ -18,11 +18,22 @@ export async function fetchTankNames(api: BlitzKitAPI) {
searchableNameDeburr.locales[key] = deburr(value);
});
const searchableNameFull: I18nString = {
locales: { ...tank.name_full?.locales },
};
const searchableNameFullDeburr: I18nString = { locales: {} };
Object.entries(searchableNameFull.locales).forEach(([key, value]) => {
searchableNameFullDeburr.locales[key] = deburr(value);
});
return {
id: tank.id,
name: tank.name,
searchableName: tank.name,
searchableNameDeburr,
searchableNameFull,
searchableNameFullDeburr,
camouflages: tank.camouflages
?.map((id) =>
locales.supported.map(
@@ -45,6 +56,8 @@ export const SEARCH_KEYS = [
.map(({ locale }) => [
`searchableName.locales.${locale}`,
`searchableNameDeburr.locales.${locale}`,
`searchableNameFull.locales.${locale}`,
`searchableNameFullDeburr.locales.${locale}`,
])
.flat(),
"camouflages",
@@ -41,6 +41,7 @@ message TankDefinition {
required uint32 weight = 31;
required string dev_name = 32;
optional Equalizer equalizer = 33;
optional I18nString name_full = 34;
}
message Equalizer {
@@ -292,6 +292,7 @@ export interface TankDefinition {
weight: number;
dev_name: string;
equalizer?: Equalizer | undefined;
name_full?: I18nString | undefined;
}
export interface TankDefinition_RolesEntry {
@@ -651,6 +652,7 @@ function createBaseTankDefinition(): TankDefinition {
weight: 0,
dev_name: "",
equalizer: undefined,
name_full: undefined,
};
}
@@ -752,6 +754,9 @@ export const TankDefinition: MessageFns<TankDefinition> = {
if (message.equalizer !== undefined) {
Equalizer.encode(message.equalizer, writer.uint32(266).fork()).join();
}
if (message.name_full !== undefined) {
I18nString.encode(message.name_full, writer.uint32(274).fork()).join();
}
return writer;
},
@@ -1051,6 +1056,14 @@ export const TankDefinition: MessageFns<TankDefinition> = {
message.equalizer = Equalizer.decode(reader, reader.uint32());
continue;
}
case 34: {
if (tag !== 274) {
break;
}
message.name_full = I18nString.decode(reader, reader.uint32());
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@@ -1111,6 +1124,7 @@ export const TankDefinition: MessageFns<TankDefinition> = {
weight: isSet(object.weight) ? globalThis.Number(object.weight) : 0,
dev_name: isSet(object.dev_name) ? globalThis.String(object.dev_name) : "",
equalizer: isSet(object.equalizer) ? Equalizer.fromJSON(object.equalizer) : undefined,
name_full: isSet(object.name_full) ? I18nString.fromJSON(object.name_full) : undefined,
};
},
@@ -1218,6 +1232,9 @@ export const TankDefinition: MessageFns<TankDefinition> = {
if (message.equalizer !== undefined) {
obj.equalizer = Equalizer.toJSON(message.equalizer);
}
if (message.name_full !== undefined) {
obj.name_full = I18nString.toJSON(message.name_full);
}
return obj;
},
@@ -1271,6 +1288,9 @@ export const TankDefinition: MessageFns<TankDefinition> = {
message.equalizer = (object.equalizer !== undefined && object.equalizer !== null)
? Equalizer.fromPartial(object.equalizer)
: undefined;
message.name_full = (object.name_full !== undefined && object.name_full !== null)
? I18nString.fromPartial(object.name_full)
: undefined;
return message;
},
};
+2
View File
@@ -6,7 +6,9 @@ import { promisify } from "util";
const MAX_COMMAND_LENGTH = 2 ** 11;
const TS_PROTO_EXECUTABLE_LOCATIONS = [
"./node_modules/.bin/protoc-gen-ts_proto",
"./node_modules/.bin/protoc-gen-ts_proto.exe",
"../../node_modules/.bin/protoc-gen-ts_proto",
"../../node_modules/.bin/protoc-gen-ts_proto.exe",
];
const roots = [
@@ -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],
+5 -4
View File
@@ -12,6 +12,7 @@ export interface Props {
}
const gameDefinitions = await api.gameDefinitions();
const gameVersion = gameDefinitions.version.split("_")[0];
const environment = assertSecret(import.meta.env.PUBLIC_ENVIRONMENT);
const { locale } = Astro.props;
const strings = getStrings(locale);
@@ -141,7 +142,7 @@ const strings = getStrings(locale);
</Text>
)
}
<Text size="2" color="gray">CDN</Text>
<Text size="2" color="gray">Branch</Text>
</Flex>
<Flex direction="column" gap="1" flexGrow="1" flexBasis="0" align="start">
@@ -151,7 +152,7 @@ const strings = getStrings(locale);
{
environment !== "preview" && (
<Code size="2" color="gray">
{gameDefinitions.version}
{gameVersion}
</Code>
)
}
@@ -174,13 +175,13 @@ const strings = getStrings(locale);
WoTB
</Text>
<Code size="2" color="gray">
{gameDefinitions.version}
{gameVersion}
</Code>
</>
)
}
<Text size="2" color="gray">CDN</Text>
<Text size="2" color="gray">Branch</Text>
<Code size="2" color="gray">
{environment}
</Code>
@@ -33,7 +33,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
draft.search = sanitized.length === 0 ? null : sanitized;
});
}, 500),
[]
[],
);
const handleChange = useCallback(() => {
if (!searching) {
@@ -46,7 +46,14 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
}, [searching]);
const handleKeyDown = useCallback<KeyboardEventHandler>(
(event) => {
if (event.key !== "Enter" || !topResult || searching) return;
if (
event.key !== "Enter" ||
!topResult ||
!TankFilters.state.search ||
TankFilters.state.searching
) {
return;
}
event.preventDefault();
@@ -56,7 +63,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
window.location.href = `/tanks/${topResult.slug}`;
}
},
[topResult]
[topResult],
);
return (
@@ -17,7 +17,7 @@ import { AssaultRangesVisualizer } from "./AssaultRangesVisualizer";
import { GunFlexibilityVisualizer } from "./GunFlexibilityVisualizer";
import { Info } from "./Info";
import { InfoWithDelta } from "./InfoWithDelta";
import { ReloadVisualizer } from "./ReloadVisualizer";
import { RELOAD_PRECISION, ReloadVisualizer } from "./ReloadVisualizer";
import { RicochetVisualizer } from "./RicochetVisualizer";
import { StatsTableWrapper } from "./StatsTableWrapper";
import type { StatsAcceptorProps } from "./TraverseVisualizer";
@@ -163,7 +163,7 @@ export function Firepower({
.shell_index,
{ index: index + 1 },
)}
decimals={2}
decimals={RELOAD_PRECISION}
deltaType="lowerIsBetter"
noRanking
value={() => reload}
@@ -173,7 +173,7 @@ export function Firepower({
) : (
<InfoWithDelta
stats={stats}
decimals={2}
decimals={RELOAD_PRECISION}
deltaType="lowerIsBetter"
value="shellReload"
/>
@@ -183,7 +183,7 @@ export function Firepower({
<InfoWithDelta
stats={stats}
indent
decimals={2}
decimals={RELOAD_PRECISION}
deltaType="lowerIsBetter"
value="intraClip"
/>
@@ -1,6 +1,7 @@
import type { QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import type { Group } from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import { api } from "../../../../../../../../core/blitzkit/api";
import {
modelTransformEvent,
@@ -21,6 +22,13 @@ export function CanvasContent() {
const tankModel = modelDefinitions.models[tank.id];
const turretModel = tankModel.turrets[turret.id];
// baked-in turret mount tilt (e.g. Minotauro sits pitched 3deg forward);
// applied as a static base rotation so the live gun pitch composes on top
// of it, mirroring useTankTransform.ts's turretContainer/gunContainer nesting
const initialTurretPitch = -degToRad(
tankModel.initial_turret_rotation?.pitch ?? 0,
);
useEffect(() => {
function handleModelTransformEvent(
event: QuicklimeEvent<ModelTransformEventData>,
@@ -45,10 +53,23 @@ export function CanvasContent() {
</group>
</group>
<ModelChunk only="turret" />
<group
position={[0, turretModel.gun_origin!.y, turretModel.gun_origin!.z]}
rotation={[initialTurretPitch, 0, 0]}
>
<group
position={[
0,
-turretModel.gun_origin!.y,
-turretModel.gun_origin!.z,
]}
>
<ModelChunk only="turret" />
<group ref={gunWrapper}>
<ModelChunk only="gun" />
<group ref={gunWrapper}>
<ModelChunk only="gun" />
</group>
</group>
</group>
</>
);
@@ -48,13 +48,27 @@ export function GunFlexibilityVisualizer({
const turretModel = tankModel.turrets[turret.id];
const gunModel = turretModel.guns[gun.id];
// turret's own baked-in mount tilt (e.g. Minotauro's turret sits pitched
// 3deg forward on the hull); must be folded into displayed pitch values
// the same way tankCharacteristics.ts and QuickInputs do
const initialTurretPitch = degToRad(
tankModel.initial_turret_rotation?.pitch ?? 0,
);
// radial map is in the tilt-compensated frame so the neutrality circle sits
// at the displayed 0deg
const tiltDeg = radToDeg(initialTurretPitch);
const magTilted = (deg: number) => mag(deg + tiltDeg);
const [yaw, setYaw] = useState(0);
const [pitch, setPitch] = useState(0);
const [minPitch, setMinPitch] = useState(
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max),
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max) -
initialTurretPitch,
);
const [maxPitch, setMaxPitch] = useState(
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min),
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min) -
initialTurretPitch,
);
const container = useRef<HTMLDivElement>(null);
@@ -70,7 +84,7 @@ export function GunFlexibilityVisualizer({
let d = "";
// right
const mo = mag(p.max);
const mo = magTilted(p.max);
if (y) {
d += `M 0 0`;
d += `L ${c(90 - y.max, mo)}`;
@@ -87,7 +101,7 @@ export function GunFlexibilityVisualizer({
// front
if (f) {
const m = mag(f.max);
const m = magTilted(f.max);
d += `L ${c(90 - f.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
}
@@ -108,7 +122,7 @@ export function GunFlexibilityVisualizer({
// back
if (!y && b) {
const m = mag(b.max);
const m = magTilted(b.max);
d += `L ${c(-90 - b.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
}
@@ -116,7 +130,7 @@ export function GunFlexibilityVisualizer({
d += `Z`;
// right
const mi = mag(p.min);
const mi = magTilted(p.min);
if (y) {
d += `M 0 0`;
d += `L ${c(90 - y.max, mi)}`;
@@ -133,7 +147,7 @@ export function GunFlexibilityVisualizer({
// front
if (f) {
const m = mag(f.min);
const m = magTilted(f.min);
d += `L ${c(90 - f.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
}
@@ -154,14 +168,14 @@ export function GunFlexibilityVisualizer({
// back
if (!y && b) {
const m = mag(b.min);
const m = magTilted(b.min);
d += `L ${c(-90 - b.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
}
d += `Z`;
const r = mag(radToDeg(-pitch));
const r = mag(radToDeg(-(pitch - initialTurretPitch)));
const dotX = r * Math.sin(yaw);
const dotY = r * Math.cos(yaw);
@@ -251,13 +265,13 @@ export function GunFlexibilityVisualizer({
yaw = min[1];
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
t = clamp(t, 0, 1);
let pitch = degToRad(magInverse(-t));
let pitch = degToRad(magInverse(-t)) + initialTurretPitch;
pitch = clamp(pitch, min[0], max[0]);
setPitch(pitch);
setYaw(yaw);
setMinPitch(min[0]);
setMaxPitch(max[0]);
setMinPitch(min[0] - initialTurretPitch);
setMaxPitch(max[0] - initialTurretPitch);
modelTransformEvent.dispatch({ pitch, yaw });
}}
@@ -376,7 +390,7 @@ export function GunFlexibilityVisualizer({
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
value={literals(strings.common.units.deg, {
value: radToDeg(pitch).toFixed(0),
value: radToDeg(pitch - initialTurretPitch).toFixed(0),
})}
side="top-left"
/>
@@ -11,7 +11,7 @@ import { Shell } from './components/Shell';
import { Target } from './components/Target';
import './index.css';
const PRECISION = 2;
export const RELOAD_PRECISION = 1;
export interface ReloadUpdateData {
reload: number;
@@ -82,9 +82,9 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
);
}
totalTime.current.innerHTML = shellReload.toFixed(PRECISION);
totalTime.current.innerHTML = shellReload.toFixed(RELOAD_PRECISION);
progressTime.current.innerHTML = ((1 - reload) * shellReload).toFixed(
PRECISION,
RELOAD_PRECISION,
);
if (state.current >= reloadThreshold) shoot();
@@ -117,9 +117,9 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
shells = times(stats.shells, (index) => index > state.current - 1);
}
totalTime.current.innerHTML = interval.toFixed(PRECISION);
totalTime.current.innerHTML = interval.toFixed(RELOAD_PRECISION);
progressTime.current.innerHTML = ((1 - reload) * interval).toFixed(
PRECISION,
RELOAD_PRECISION,
);
if (lastState % 1 > state.current % 1) shoot();
@@ -262,7 +262,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
transform: 'translate(50%, -50%)',
}}
>
0.00
{(0).toFixed(RELOAD_PRECISION)}
</Code>
<Code
ref={totalTime}
@@ -277,7 +277,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
transform: 'translate(-50%, -50%)',
}}
>
0.00
{(0).toFixed(RELOAD_PRECISION)}
</Code>
{stats.shells > 1 &&
@@ -0,0 +1,128 @@
import { alias, availableProvisions } from "@blitzkit/core";
import { ShuffleIcon } from "@radix-ui/react-icons";
import { Flex, IconButton, Popover } 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";
const provisionDefinitions = await api.provisionDefinitions();
export function SpallLinerSwitcher() {
const { tank, gun, provisions } = Duel.use((state) => state.protagonist);
const { 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 = (
<IconButton
color={isSpallLinerActive ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
radius="medium"
onClick={() => {
if (isSpallLinerActive) {
unequip();
} else if (hasSlotAvailable) {
equip();
}
}}
>
<img
alt={unwrap(
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
)}
src={alias("api", `/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`)}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
);
if (isSpallLinerActive || hasSlotAvailable) return button;
return (
<Popover.Root>
<Popover.Trigger>{button}</Popover.Trigger>
<Popover.Content>
<Flex gap="2" align="center">
<img
alt={unwrap(
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
)}
src={alias(
"api",
`/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`,
)}
style={{
width: "1.25em",
height: "1.25em",
}}
/>
<ShuffleIcon color="var(--gray-9)" />
<Flex gap="2">
{provisions.map((id) => (
<Popover.Close key={id}>
<IconButton
color="gray"
variant="soft"
size="3"
radius="medium"
onClick={() => equip(id)}
>
<img
alt={unwrap(provisionDefinitions.provisions[id].name!)}
src={alias("api", `/icons/provisions/${id}.webp`)}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
</Popover.Close>
))}
</Flex>
</Flex>
</Popover.Content>
</Popover.Root>
);
}
@@ -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) {
}}
/>
</IconButton>
<SpallLinerSwitcher />
</Flex>
{!skeleton && (
@@ -15,7 +15,7 @@ export function Tracker({ fontSize }: Props) {
top="50%"
width="min(100%, 16rem)"
style={{
transform: `translate(-50%, calc(${fontSize} / 2 + var(--space-4)))`,
transform: `translate(-50%, calc(${fontSize} * 0.75 + var(--space-4)))`,
}}
>
<Progress color="gray" value={progress} />
@@ -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;
@@ -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<typeof tankCharacteristics>;
@@ -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)
+35
View File
@@ -0,0 +1,35 @@
# Point this to your local installation of the game (make sure you see the Data
# folder within the directory you're putting here); don't forget to use
# double quotation marks if your path has spaces
CLIENT_DIR=
# Should be set to * for local development to build all chunks at once
CHUNK=*
# Should always be development when using CHUNKS=*
PUBLIC_ENVIRONMENT=development
# GuidesBlitz is the official textual partner of BlitzKit, but, really any
# website that supports BlitzKit tank slugs like example.com/tank-slug works
# too
PUBLIC_GUIDE_WEBSITE=https://guidesblitz.com
# The tag manager doesn't do anything in localhost, but here's what BlitzKit
# uses for completeness sake
PUBLIC_GOOGLE_TAG_MANAGER_ID=G-GL2JVHCGPQ
# A very rare, but required variable, enabling the advertising of BlitzKit
# OpenTest; TODO: make this a string field to allow for cross-advertising for
# any branch of BlitzKit
PUBLIC_PROMOTE_OPENTEST=false
# Legacy variable; this will be removed soon
PUBLIC_STATIC_ASSET_BASE=https://assets.blitzkit.app
# Proprietary Wargaming fields; you shouldn't need these for 99% of the tasks
WOTB_GLOSSARY=
WOTB_GLOSSARY_DOMAIN=
WOTB_GLOSSARY_PATH=
# Generate your own application id: https://developers.wargaming.net
PUBLIC_WARGAMING_APPLICATION_ID=