Merge branch 'main' into stale-info

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-10-04 14:06:06 -05:00
commit 95d6d8a1ae
21 files changed
+445 -39

No files matched your search

@@ -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],
@@ -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=