mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
Merge branch 'main' into data-driven
This commit is contained in:
commit
6ebb2810ad
37 files changed
+1404
-350
No files matched your search
@@ -27,8 +27,6 @@ export const DynamicArmor = memo(() => {
|
||||
const newRenderSize = new Vector2();
|
||||
|
||||
useFrame(({ gl, camera, scene }) => {
|
||||
gl.autoClear = true;
|
||||
|
||||
gl.getSize(newRenderSize).multiplyScalar(gl.getPixelRatio());
|
||||
|
||||
if (!newRenderSize.equals(renderSize)) {
|
||||
@@ -50,6 +48,8 @@ export const DynamicArmor = memo(() => {
|
||||
|
||||
gl.clearDepth();
|
||||
gl.render(primaryArmorScene, camera);
|
||||
|
||||
gl.autoClear = true;
|
||||
}, 1);
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
import { assertSecret } from "@blitzkit/core";
|
||||
import { Box, Code, Flex, Heading, Link, Text } from "@radix-ui/themes";
|
||||
import packageJson from "../../../../package.json";
|
||||
import { api } from "../core/blitzkit/api";
|
||||
import { getStrings } from "../core/i18n/getStrings";
|
||||
import { api } from "../api/dynamic";
|
||||
import { LinkI18n } from "./LinkI18n";
|
||||
import { LocaleSwitcherThemeWrapper } from "./LocaleSwitcher";
|
||||
|
||||
@@ -12,9 +11,10 @@ export interface Props {
|
||||
}
|
||||
|
||||
const gameDefinitions = await api.game();
|
||||
const gameVersion = gameDefinitions.version.split("_")[0];
|
||||
const environment = assertSecret(import.meta.env.PUBLIC_ENVIRONMENT);
|
||||
const { locale } = Astro.props;
|
||||
const strings = getStrings(locale);
|
||||
const strings = await api.strings(locale);
|
||||
---
|
||||
|
||||
<style>
|
||||
@@ -141,7 +141,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 +151,7 @@ const strings = getStrings(locale);
|
||||
{
|
||||
environment !== "preview" && (
|
||||
<Code size="2" color="gray">
|
||||
{gameDefinitions.version}
|
||||
{gameVersion}
|
||||
</Code>
|
||||
)
|
||||
}
|
||||
@@ -174,13 +174,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>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@blitzkit/core";
|
||||
import { invalidate, useFrame } from "@react-three/fiber";
|
||||
import type { QuicklimeEvent } from "quicklime";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
MeshBasicMaterial,
|
||||
NormalBlending,
|
||||
@@ -43,59 +43,61 @@ export function PrimaryArmorSceneComponent({
|
||||
node,
|
||||
thickness,
|
||||
}: PrimaryArmorSceneComponentProps) {
|
||||
const material = new ShaderMaterial({
|
||||
fragmentShader,
|
||||
vertexShader,
|
||||
const material = useRef(
|
||||
new ShaderMaterial({
|
||||
fragmentShader,
|
||||
vertexShader,
|
||||
|
||||
fog: true,
|
||||
transparent: true,
|
||||
fog: true,
|
||||
transparent: true,
|
||||
|
||||
blending: isHalloween ? SubtractiveBlending : NormalBlending,
|
||||
blending: isHalloween ? SubtractiveBlending : NormalBlending,
|
||||
|
||||
uniforms: UniformsUtils.merge([
|
||||
UniformsLib.common,
|
||||
UniformsLib.fog,
|
||||
uniforms: UniformsUtils.merge([
|
||||
UniformsLib.common,
|
||||
UniformsLib.fog,
|
||||
|
||||
{
|
||||
thickness: { value: null },
|
||||
penetration: { value: null },
|
||||
caliber: { value: null },
|
||||
ricochet: { value: null },
|
||||
normalization: { value: null },
|
||||
isExplosive: { value: null },
|
||||
canSplash: { value: null },
|
||||
damage: { value: null },
|
||||
explosionRadius: { value: null },
|
||||
greenPenetration: { value: null },
|
||||
advancedHighlighting: { value: null },
|
||||
opaque: { value: null },
|
||||
{
|
||||
thickness: { value: null },
|
||||
penetration: { value: null },
|
||||
caliber: { value: null },
|
||||
ricochet: { value: null },
|
||||
normalization: { value: null },
|
||||
isExplosive: { value: null },
|
||||
canSplash: { value: null },
|
||||
damage: { value: null },
|
||||
explosionRadius: { value: null },
|
||||
greenPenetration: { value: null },
|
||||
advancedHighlighting: { value: null },
|
||||
opaque: { value: null },
|
||||
|
||||
inverseProjectionMatrix: { value: null },
|
||||
resolution: { value: new Vector2() },
|
||||
spacedArmorBuffer: { value: null },
|
||||
spacedArmorDepth: { value: null },
|
||||
inverseProjectionMatrix: { value: null },
|
||||
resolution: { value: new Vector2() },
|
||||
spacedArmorBuffer: { value: null },
|
||||
spacedArmorDepth: { value: null },
|
||||
|
||||
opacity: { value: 0 },
|
||||
},
|
||||
]),
|
||||
});
|
||||
opacity: { value: 0 },
|
||||
},
|
||||
]),
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function handleShellChange() {
|
||||
const shell =
|
||||
Tankopedia.state.customShell ?? getDuel("antagonist", DuelModule.Shell);
|
||||
|
||||
material.uniforms.caliber.value = shell.caliber;
|
||||
material.uniforms.ricochet.value = degToRad(
|
||||
material.current.uniforms.caliber.value = shell.caliber;
|
||||
material.current.uniforms.ricochet.value = degToRad(
|
||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
||||
);
|
||||
material.uniforms.normalization.value = degToRad(
|
||||
material.current.uniforms.normalization.value = degToRad(
|
||||
shell.normalization ?? 0,
|
||||
);
|
||||
material.uniforms.isExplosive.value = isExplosive(shell.type);
|
||||
material.uniforms.canSplash.value = canSplash(shell.type);
|
||||
material.uniforms.damage.value = shell.armor_damage;
|
||||
material.uniforms.explosionRadius.value = shell.explosion_radius;
|
||||
material.current.uniforms.isExplosive.value = isExplosive(shell.type);
|
||||
material.current.uniforms.canSplash.value = canSplash(shell.type);
|
||||
material.current.uniforms.damage.value = shell.armor_damage;
|
||||
material.current.uniforms.explosionRadius.value = shell.explosion_radius;
|
||||
|
||||
handleProtagonistEquipmentChange(true);
|
||||
handleAntagonistEquipmentChange(true);
|
||||
@@ -103,17 +105,18 @@ export function PrimaryArmorSceneComponent({
|
||||
invalidate();
|
||||
}
|
||||
function handleGreenPenetrationChange(greenPenetration: boolean) {
|
||||
material.uniforms.greenPenetration.value = greenPenetration;
|
||||
material.current.uniforms.greenPenetration.value = greenPenetration;
|
||||
}
|
||||
function handleAdvancedHighlightingChange(advancedHighlighting: boolean) {
|
||||
material.uniforms.advancedHighlighting.value = advancedHighlighting;
|
||||
material.current.uniforms.advancedHighlighting.value =
|
||||
advancedHighlighting;
|
||||
invalidate();
|
||||
}
|
||||
function handleOpaqueChange(opaque: boolean) {
|
||||
material.uniforms.opaque.value = opaque;
|
||||
material.current.uniforms.opaque.value = opaque;
|
||||
}
|
||||
function handleWireframeChange(wireframe: boolean) {
|
||||
material.wireframe = wireframe;
|
||||
material.current.wireframe = wireframe;
|
||||
}
|
||||
function handleProtagonistEquipmentChange(noInvalidate = false) {
|
||||
const equipment = Tankopedia.state.protagonist.equipment;
|
||||
@@ -128,7 +131,7 @@ export function PrimaryArmorSceneComponent({
|
||||
? protagonist.equalizer?.armor
|
||||
: undefined) ?? 1;
|
||||
|
||||
material.uniforms.thickness.value =
|
||||
material.current.uniforms.thickness.value =
|
||||
thickness * (hasEnhancedArmor ? 1.04 : 1) * equalizer;
|
||||
|
||||
if (!noInvalidate) invalidate();
|
||||
@@ -148,7 +151,7 @@ export function PrimaryArmorSceneComponent({
|
||||
equipment,
|
||||
);
|
||||
|
||||
material.uniforms.penetration.value =
|
||||
material.current.uniforms.penetration.value =
|
||||
penetration *
|
||||
resolvePenetrationCoefficient(
|
||||
hasCalibratedShells,
|
||||
@@ -161,7 +164,7 @@ export function PrimaryArmorSceneComponent({
|
||||
}
|
||||
|
||||
function handleTransitionEvent(event: QuicklimeEvent<number>) {
|
||||
material.uniforms.opacity.value = event.data;
|
||||
material.current.uniforms.opacity.value = event.data;
|
||||
}
|
||||
|
||||
handleShellChange();
|
||||
@@ -212,13 +215,14 @@ export function PrimaryArmorSceneComponent({
|
||||
}, []);
|
||||
|
||||
useFrame(({ gl, camera }) => {
|
||||
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||
gl.getSize(material.current.uniforms.resolution.value).multiplyScalar(
|
||||
gl.getPixelRatio(),
|
||||
);
|
||||
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
||||
material.uniforms.spacedArmorDepth.value =
|
||||
material.current.uniforms.spacedArmorBuffer.value =
|
||||
spacedArmorRenderTarget.texture;
|
||||
material.current.uniforms.spacedArmorDepth.value =
|
||||
spacedArmorRenderTarget.depthTexture;
|
||||
material.uniforms.inverseProjectionMatrix.value =
|
||||
material.current.uniforms.inverseProjectionMatrix.value =
|
||||
camera.projectionMatrixInverse;
|
||||
});
|
||||
|
||||
@@ -243,7 +247,7 @@ export function PrimaryArmorSceneComponent({
|
||||
{...props}
|
||||
key={`${key}-include`}
|
||||
renderOrder={1}
|
||||
material={material}
|
||||
material={material.current}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -88,7 +88,7 @@ export function SpacedArmorSceneComponent({
|
||||
|
||||
const { customShell } = Tankopedia.state;
|
||||
const shell = customShell ?? getDuel("protagonist", DuelModule.Shell);
|
||||
const { equalize } = Tankopedia.state;
|
||||
const { equalize } = Tankopedia.state.environment;
|
||||
const protagonistEqualizer =
|
||||
(equalize ? protagonist.equalizer : undefined) ?? defaultEqualizer;
|
||||
const antagonistEqualizer =
|
||||
@@ -382,6 +382,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],
|
||||
|
||||
@@ -746,6 +746,7 @@ function OwnershipFilterInternal() {
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.showOwned = !draft.showOwned;
|
||||
if (draft.showOwned) draft.showUnowned = false;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -765,6 +766,7 @@ function OwnershipFilterInternal() {
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.showUnowned = !draft.showUnowned;
|
||||
if (draft.showUnowned) draft.showOwned = false;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -791,6 +793,7 @@ function TestFilter() {
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.showNonTesting = !draft.showNonTesting;
|
||||
if (draft.showNonTesting) draft.showTesting = false;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -811,6 +814,7 @@ function TestFilter() {
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.showTesting = !draft.showTesting;
|
||||
if (draft.showTesting) draft.showNonTesting = false;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
+47
-5
@@ -1,6 +1,14 @@
|
||||
import { alias, isExplosive } from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n";
|
||||
import { Flex, Heading, IconButton, Tooltip } from "@radix-ui/themes";
|
||||
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Popover,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
@@ -9,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";
|
||||
@@ -77,6 +85,40 @@ export function Firepower({
|
||||
{strings.common.gun_types[gun.gun_type!.$case]}
|
||||
</Info>
|
||||
<InfoWithDelta stats={stats} decimals={0} value="dpm" />
|
||||
{gun.gun_type!.$case === "regular" && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={0}
|
||||
noRanking
|
||||
name={
|
||||
<Flex align="center" gap="1" display="inline-flex">
|
||||
{
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.peekabooDpm
|
||||
}
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton variant="ghost" size="1">
|
||||
<InfoCircledIcon />
|
||||
</IconButton>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content style={{ maxWidth: 280 }}>
|
||||
<Text size="2">
|
||||
{
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.peekabooDpmInfo
|
||||
}
|
||||
</Text>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Flex>
|
||||
}
|
||||
value={(tank) => tank.peekabooDpm}
|
||||
/>
|
||||
)}
|
||||
<InfoWithDelta stats={stats} decimals={0} value="damage" />
|
||||
{gun.gun_type!.$case !== "regular" && (
|
||||
<InfoWithDelta stats={stats} indent decimals={0} value="clipDamage" />
|
||||
@@ -121,7 +163,7 @@ export function Firepower({
|
||||
.shell_index,
|
||||
{ index: index + 1 },
|
||||
)}
|
||||
decimals={2}
|
||||
decimals={RELOAD_PRECISION}
|
||||
deltaType="lowerIsBetter"
|
||||
noRanking
|
||||
value={() => reload}
|
||||
@@ -131,7 +173,7 @@ export function Firepower({
|
||||
) : (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
decimals={RELOAD_PRECISION}
|
||||
deltaType="lowerIsBetter"
|
||||
value="shellReload"
|
||||
/>
|
||||
@@ -141,7 +183,7 @@ export function Firepower({
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={2}
|
||||
decimals={RELOAD_PRECISION}
|
||||
deltaType="lowerIsBetter"
|
||||
value="intraClip"
|
||||
/>
|
||||
|
||||
+19
-4
@@ -6,7 +6,6 @@ import {
|
||||
modelTransformEvent,
|
||||
type ModelTransformEventData,
|
||||
} from "../../../../../../../../core/blitzkit/modelTransform";
|
||||
import { Duel } from "../../../../../../../../stores/duel";
|
||||
import { ModelChunk } from "./ModelChunk";
|
||||
|
||||
const modelDefinitions = await api.models();
|
||||
@@ -21,6 +20,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 +51,19 @@ 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>
|
||||
</>
|
||||
);
|
||||
|
||||
+27
-13
@@ -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"
|
||||
/>
|
||||
|
||||
+7
-7
@@ -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 &&
|
||||
|
||||
+4
-9
@@ -1,5 +1,5 @@
|
||||
import { alias } from "@blitzkit/core";
|
||||
import { Badge, Box, Flex, Heading, Switch, Text } from "@radix-ui/themes";
|
||||
import { Box, Flex, Heading, Switch, Text } from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||
@@ -12,12 +12,7 @@ export function Equalizer() {
|
||||
return (
|
||||
<ConfigurationChildWrapper>
|
||||
<Heading size="4">
|
||||
<Flex gap="2" align="center">
|
||||
{strings.website.tools.tankopedia.configuration.equalizer.title}{" "}
|
||||
<Badge variant="outline">
|
||||
{strings.website.tools.tankopedia.configuration.equalizer.beta}
|
||||
</Badge>
|
||||
</Flex>
|
||||
{strings.website.tools.tankopedia.configuration.equalizer.title}
|
||||
</Heading>
|
||||
|
||||
<Box
|
||||
@@ -34,9 +29,9 @@ export function Equalizer() {
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
borderRadius: "var(--radius-3)",
|
||||
backgroundImage: `url(${alias("api", `/gamemodes/45/banner.webp`)})`,
|
||||
backgroundImage: `url(${alias("api", `/gamemodes/Insanity/banner.webp`)})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundPosition: "50% 25%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
|
||||
+128
@@ -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 && (
|
||||
|
||||
+1
-1
@@ -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} />
|
||||
|
||||
Reference in new issue
Block a user