Merge branch 'main' into ue

This commit is contained in:
TrèsAbhi committed 2026-04-04 20:25:51 -05:00
commit c0376182f8
164 files changed
+5145 -4079

No files matched your search

@@ -11,6 +11,7 @@ import { Html } from "@react-three/drei";
import { useRef } from "react";
import { radToDeg } from "three/src/math/MathUtils.js";
import { resolveArmorIndex } from "../../../core/blitzkit/resolveArmorIndex";
import { LocaleProvider, useLocale } from "../../../hooks/useLocale";
import { App } from "../../../stores/app";
import { Duel } from "../../../stores/duel";
import { Tankopedia } from "../../../stores/tankopedia";
@@ -21,6 +22,7 @@ export function ArmorPlateDisplay() {
const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
const developerMode = App.useDeferred((state) => state.developerMode, false);
const input = useRef<HTMLInputElement>(null);
const { locale } = useLocale();
if (highlightArmor === undefined) return null;
@@ -30,154 +32,164 @@ export function ArmorPlateDisplay() {
center
style={{ pointerEvents: highlightArmor.editingPlate ? "auto" : "none" }}
>
<Card
variant="classic"
mb="4"
style={{
whiteSpace: "nowrap",
color: highlightArmor.color,
transform: "translateY(-50%)",
}}
>
<Flex direction="column">
<Text weight="bold">
{layerTypeNames[highlightArmor.type]}{" "}
{highlightArmor.thickness.toFixed(0)}
<Text size="1" weight="regular">
mm
</Text>
</Text>
{highlightArmor.type !== ArmorType.External && (
<Text size="2" color="gray">
{highlightArmor.thicknessAngled.toFixed(0)}
<Text size="1">
mm {radToDeg(highlightArmor.angle).toFixed(0)}°
<LocaleProvider locale={locale}>
<Card
variant="classic"
mb="4"
style={{
whiteSpace: "nowrap",
color: highlightArmor.color,
transform: "translateY(-50%)",
}}
>
<Flex direction="column">
<Text weight="bold">
{layerTypeNames[highlightArmor.type]}{" "}
{highlightArmor.thickness.toFixed(0)}
<Text size="1" weight="regular">
mm
</Text>
</Text>
)}
{developerMode && (
<>
<Text mt="2" color="gray" size="2">
<b>DEV:</b> <Code>{highlightArmor.name}</Code>
{highlightArmor.type !== ArmorType.External && (
<Text size="2" color="gray">
{highlightArmor.thicknessAngled.toFixed(0)}
<Text size="1">
mm {radToDeg(highlightArmor.angle).toFixed(0)}°
</Text>
</Text>
</>
)}
)}
{highlightArmor.editingPlate && (
<Flex gap="2" align="center">
<TextField.Root
variant="classic"
style={{ width: 96 }}
mt="2"
ref={input}
key={highlightArmor.name}
type="number"
autoFocus
onFocus={(event) => event.target.select()}
onKeyDown={(event) => {
if (!highlightArmor.editingPlate) return;
{developerMode && (
<>
<Text mt="2" color="gray" size="2">
<b>DEV:</b>{" "}
<Code variant="soft" highContrast color="gray">
{highlightArmor.name}
</Code>
</Text>
</>
)}
const index = resolveArmorIndex(highlightArmor.name);
if (index === undefined) return;
const thickness = event.currentTarget.valueAsNumber;
if (isNaN(thickness) || thickness < 0) return;
if (event.key === "Enter") {
input.current?.blur();
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
const { protagonist } = Duel.state;
const tank = draft.model;
const turret = tank.turrets[protagonist.turret.id];
const gun = turret.guns[protagonist.gun.id];
const track = tank.tracks[protagonist.track.id];
if (highlightArmor.name.startsWith("hull_")) {
tank.armor.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith("turret_")) {
turret.armor.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor.thickness[index] = thickness;
} else {
gun.thickness = thickness;
}
} else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = thickness;
}
});
} else if (event.key === "Escape") {
input.current?.blur();
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
}}
defaultValue={highlightArmor.thickness.toFixed(0)}
>
<TextField.Slot side="right">mm</TextField.Slot>
</TextField.Root>
<IconButton
mt="2"
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
{highlightArmor.editingPlate && (
<Flex gap="2" align="center">
<TextField.Root
variant="classic"
style={{ width: 96 }}
mt="2"
ref={input}
key={highlightArmor.name}
type="number"
autoFocus
onFocus={(event) => event.target.select()}
onKeyDown={(event) => {
if (!highlightArmor.editingPlate) return;
const index = resolveArmorIndex(highlightArmor.name);
if (index === undefined) return;
draft.highlightArmor = undefined;
const initialState = Tankopedia.initial;
const { protagonist } = Duel.state;
const tank = draft.model;
const initialTank = initialState.model;
const turret = tank.turrets[protagonist.turret.id];
const initialTurret =
initialTank.turrets[protagonist.turret.id];
const gun = turret.guns[protagonist.gun.id];
const initialGun = initialTurret.guns[protagonist.gun.id];
const track = tank.tracks[protagonist.track.id];
const initialTrack =
initialTank.tracks[protagonist.track.id];
const thickness = event.currentTarget.valueAsNumber;
if (highlightArmor.name.startsWith("hull_")) {
tank.armor.thickness[index] =
initialTank.armor.thickness[index];
} else if (highlightArmor.name.startsWith("turret_")) {
turret.armor.thickness[index] =
initialTurret.armor.thickness[index];
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor.thickness[index] =
initialGun.armor.thickness[index];
} else {
gun.thickness = initialGun.thickness;
}
} else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = initialTrack.thickness;
if (isNaN(thickness) || thickness < 0) return;
if (event.key === "Enter") {
input.current?.blur();
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
const { protagonist } = Duel.state;
const tank = draft.model;
const turret = tank.turrets[protagonist.turret.id];
const gun = turret.guns[protagonist.gun.id];
const track = tank.tracks[protagonist.track.id];
if (highlightArmor.name.startsWith("hull_")) {
tank.armor!.thickness[index] = thickness;
} else if (
highlightArmor.name.startsWith("turret_")
) {
turret.armor!.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor!.thickness[index] = thickness;
} else {
gun.thickness = thickness;
}
} else if (
highlightArmor.name.startsWith("chassis_")
) {
track.thickness = thickness;
}
});
} else if (event.key === "Escape") {
input.current?.blur();
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}
});
}}
>
<ReloadIcon />
</IconButton>
</Flex>
)}
</Flex>
</Card>
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
}}
defaultValue={highlightArmor.thickness.toFixed(0)}
>
<TextField.Slot side="right">mm</TextField.Slot>
</TextField.Root>
<IconButton
mt="2"
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
if (!highlightArmor.editingPlate) return;
const index = resolveArmorIndex(highlightArmor.name);
if (index === undefined) return;
draft.highlightArmor = undefined;
const initialState = Tankopedia.initial;
const { protagonist } = Duel.state;
const tank = draft.model;
const initialTank = initialState.model;
const turret = tank.turrets[protagonist.turret.id];
const initialTurret =
initialTank.turrets[protagonist.turret.id];
const gun = turret.guns[protagonist.gun.id];
const initialGun =
initialTurret.guns[protagonist.gun.id];
const track = tank.tracks[protagonist.track.id];
const initialTrack =
initialTank.tracks[protagonist.track.id];
if (highlightArmor.name.startsWith("hull_")) {
tank.armor!.thickness[index] =
initialTank.armor!.thickness[index];
} else if (highlightArmor.name.startsWith("turret_")) {
turret.armor!.thickness[index] =
initialTurret.armor!.thickness[index];
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor!.thickness[index] =
initialGun.armor!.thickness[index];
} else {
gun.thickness = initialGun.thickness;
}
} else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = initialTrack.thickness;
}
});
}}
>
<ReloadIcon />
</IconButton>
</Flex>
)}
</Flex>
</Card>
</LocaleProvider>
</Html>
</group>
);
@@ -24,11 +24,11 @@ export const PrimaryArmorScene = memo(() => {
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73)
state.protagonist.consumables.includes(73),
);
useTankTransform(track, turret, turretContainer, gunContainer);
@@ -41,8 +41,8 @@ export const PrimaryArmorScene = memo(() => {
const isVisible = isHull;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor,
armorId
tankModelDefinition.armor!,
armorId,
);
if (
@@ -69,13 +69,13 @@ export const PrimaryArmorScene = memo(() => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id
.toString()
.padStart(2, "0")}`
.padStart(2, "0")}`,
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor,
armorId
turretModelDefinition.armor!,
armorId,
);
if (
@@ -103,13 +103,13 @@ export const PrimaryArmorScene = memo(() => {
<group ref={gunContainer}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`,
);
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor,
armorId
gunModelDefinition.armor!,
armorId,
);
if (
@@ -19,7 +19,7 @@ import {
import { degToRad } from "three/src/math/MathUtils.js";
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
import { Duel } from "../../../../stores/duel";
import { Tankopedia } from "../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
import { transitionEvent } from "../../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
@@ -88,24 +88,18 @@ export function PrimaryArmorSceneComponent({
material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!
isExplosive(shell.type) ? 90 : shell.ricochet!,
);
material.uniforms.normalization.value = degToRad(
shell.normalization ?? 0
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;
handleProtagonistEquipmentChange(
Duel.state.protagonist.equipmentMatrix,
true
);
handleAntagonistEquipmentChange(
Duel.state.antagonist.equipmentMatrix,
true
);
handleProtagonistEquipmentChange(true);
handleAntagonistEquipmentChange(true);
invalidate();
}
@@ -122,38 +116,43 @@ export function PrimaryArmorSceneComponent({
function handleWireframeChange(wireframe: boolean) {
material.wireframe = wireframe;
}
function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix,
noInvalidate = false
) {
function handleProtagonistEquipmentChange(noInvalidate = false) {
const equipment = Duel.state.protagonist.equipmentMatrix;
const hasEnhancedArmor = hasEquipment(
110,
Duel.state.protagonist.tank.equipment_preset,
equipment
equipment,
);
material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03
: thickness;
const equalizer =
(Duel.state.equalize
? Duel.state.protagonist.tank.equalizer?.armor
: undefined) ?? 1;
material.uniforms.thickness.value =
thickness * (hasEnhancedArmor ? 1.03 : 1) * equalizer;
if (!noInvalidate) invalidate();
}
function handleAntagonistEquipmentChange(
equipment: EquipmentMatrix,
noInvalidate = false
) {
function handleAntagonistEquipmentChange(noInvalidate = false) {
const equipment = Duel.state.antagonist.equipmentMatrix;
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near;
const penetration = shell.penetration!.near;
const hasCalibratedShells = hasEquipment(
103,
Duel.state.antagonist.tank.equipment_preset,
equipment
equipment,
);
material.uniforms.penetration.value =
penetration *
resolvePenetrationCoefficient(hasCalibratedShells, shell.type);
resolvePenetrationCoefficient(
hasCalibratedShells,
Duel.state.equalize,
shell.type,
Duel.state.antagonist.tank.equalizer,
);
if (!noInvalidate) invalidate();
}
@@ -165,12 +164,12 @@ export function PrimaryArmorSceneComponent({
handleShellChange();
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
handleAdvancedHighlightingChange(
TankopediaPersistent.state.advancedHighlighting
TankopediaPersistent.state.advancedHighlighting,
);
handleOpaqueChange(TankopediaPersistent.state.opaque);
handleWireframeChange(TankopediaPersistent.state.wireframe);
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
handleProtagonistEquipmentChange();
handleAntagonistEquipmentChange();
transitionEvent.on(handleTransitionEvent);
@@ -179,20 +178,27 @@ export function PrimaryArmorSceneComponent({
Tankopedia.on((state) => state.customShell, handleShellChange),
TankopediaPersistent.on(
(state) => state.greenPenetration,
handleGreenPenetrationChange
handleGreenPenetrationChange,
),
TankopediaPersistent.on(
(state) => state.advancedHighlighting,
handleAdvancedHighlightingChange
handleAdvancedHighlightingChange,
),
TankopediaPersistent.on((state) => state.opaque, handleOpaqueChange),
Duel.on(
(state) => state.protagonist.equipmentMatrix,
(equipment) => handleProtagonistEquipmentChange(equipment)
() => handleProtagonistEquipmentChange(),
),
Duel.on(
(state) => state.antagonist.equipmentMatrix,
(equipment) => handleAntagonistEquipmentChange(equipment)
() => handleAntagonistEquipmentChange(),
),
Duel.on(
(state) => state.equalize,
() => {
handleProtagonistEquipmentChange();
handleAntagonistEquipmentChange();
},
),
() => transitionEvent.off(handleTransitionEvent),
];
@@ -204,7 +210,7 @@ export function PrimaryArmorSceneComponent({
useFrame(({ gl, camera }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
gl.getPixelRatio()
gl.getPixelRatio(),
);
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
material.uniforms.spacedArmorDepth.value =
@@ -52,7 +52,7 @@ export function ShotDisplay() {
useEffect(() => {
if (shot?.splashRadius !== undefined) {
const outlineGeometry = new BufferGeometry().setFromPoints(
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50)
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50),
);
const outlineMaterial = new LineDashedMaterial({
color: "orange",
@@ -61,7 +61,7 @@ export function ShotDisplay() {
});
const outline = new Line(
outlineGeometry,
outlineMaterial
outlineMaterial,
).computeLineDistances();
splashRadiusWrapper.current?.add(outline);
@@ -134,11 +134,11 @@ export function ShotDisplay() {
const outLength =
shot.out && shot.out.layers.length > 0
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
(shot.out.layers.at(-1) as ShotLayerNonExternal).point
(shot.out.layers.at(-1) as ShotLayerNonExternal).point,
)
: SHOT_DISPLAY_LENGTH_INFINITY;
const inLast = shot.in.layers.findLast(
(layer) => layer.type !== null
(layer) => layer.type !== null,
) as ShotLayerNonExternal;
const outTitleColor = shot.out
? shotStatusColors[shot.out.status]
@@ -154,7 +154,7 @@ export function ShotDisplay() {
position={inLast.point}
ref={splashRadiusWrapper}
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal)
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal),
)}
>
<mesh>
@@ -225,7 +225,7 @@ export function ShotDisplay() {
<group
position={inLast.point}
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal)
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal),
)}
>
<mesh position={[0, inLength / 2, 0]} renderOrder={2}>
@@ -253,8 +253,8 @@ export function ShotDisplay() {
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(
J_HAT,
shot.out.surfaceNormal.clone()
)
shot.out.surfaceNormal.clone(),
),
)}
>
<mesh position={[0, outLength / 2, 0]} renderOrder={2}>
@@ -37,15 +37,15 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const maskOrigin =
gunModelDefinition.mask === undefined
? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73)
state.protagonist.consumables.includes(73),
);
useTankTransform(track, turret, turretContainer, gunContainer);
@@ -58,8 +58,8 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
const isVisible = isHull;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor,
armorId
tankModelDefinition.armor!,
armorId,
);
if (
@@ -104,13 +104,13 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
<group ref={turretContainer}>
{armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`,
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor,
armorId
turretModelDefinition.armor!,
armorId,
);
if (
@@ -139,13 +139,13 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
<group ref={gunContainer}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`,
);
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor,
armorId
gunModelDefinition.armor!,
armorId,
);
if (
@@ -10,7 +10,7 @@ import {
import type { ArmorUserData, ExternalModuleVariant } from "../..";
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { ArmorType } from "../../../SpacedArmorScene";
import fragmentShader from "./shaders/fragment.glsl?raw";
@@ -50,48 +50,71 @@ export function SpacedArmorSubExternal({
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near;
material.uniforms.penetration.value = shell.penetration!.near;
}
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
function handleProtagonistEquipmentChange() {
const equipment = Duel.state.protagonist.equipmentMatrix;
const hasEnhancedArmor = hasEquipment(
110,
Duel.state.protagonist.tank.equipment_preset,
equipment
equipment,
);
material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03
: thickness;
const equalizer =
(Duel.state.equalize
? Duel.state.protagonist.tank.equalizer?.armor
: undefined) ?? 1;
material.uniforms.thickness.value =
thickness * (hasEnhancedArmor ? 1.03 : 1) * equalizer;
}
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
function handleAntagonistEquipmentChange() {
const equipment = Duel.state.antagonist.equipmentMatrix;
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near;
const penetration = shell.penetration!.near;
const hasCalibratedShells = hasEquipment(
103,
Duel.state.antagonist.tank.equipment_preset,
equipment
equipment,
);
const equalize =
(Duel.state.equalize
? Duel.state.antagonist.tank.equalizer?.penetration
: undefined) ?? 1;
material.uniforms.penetration.value =
penetration *
resolvePenetrationCoefficient(hasCalibratedShells, shell.type);
resolvePenetrationCoefficient(
hasCalibratedShells,
Duel.state.equalize,
shell.type,
Duel.state.antagonist.tank.equalizer,
) *
equalize;
}
handleShellChange();
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
handleProtagonistEquipmentChange();
handleAntagonistEquipmentChange();
const unsubscribes = [
Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange),
Duel.on(
(state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange
handleProtagonistEquipmentChange,
),
Duel.on(
(state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange
handleAntagonistEquipmentChange,
),
Duel.on(
(state) => state.equalize,
() => {
handleProtagonistEquipmentChange();
handleAntagonistEquipmentChange();
},
),
];
@@ -10,7 +10,7 @@ import { degToRad } from "three/src/math/MathUtils.js";
import type { ArmorUserData } from "../..";
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { ArmorType } from "../../../SpacedArmorScene";
import fragmentShader from "./shaders/fragment.glsl?raw";
@@ -53,55 +53,78 @@ export function SpacedArmorSubSpaced({
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near;
material.uniforms.penetration.value = shell.penetration!.near;
material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!
isExplosive(shell.type) ? 90 : shell.ricochet!,
);
material.uniforms.normalization.value = degToRad(
shell.normalization ?? 0
shell.normalization ?? 0,
);
}
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
function handleProtagonistEquipmentChange() {
const equipment = Duel.state.protagonist.equipmentMatrix;
const hasEnhancedArmor = hasEquipment(
110,
Duel.state.protagonist.tank.equipment_preset,
equipment
equipment,
);
material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03
: thickness;
const equalizer =
(Duel.state.equalize
? Duel.state.protagonist.tank.equalizer?.armor
: undefined) ?? 1;
material.uniforms.thickness.value =
thickness * (hasEnhancedArmor ? 1.03 : 1) * equalizer;
}
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
function handleAntagonistEquipmentChange() {
const equipment = Duel.state.antagonist.equipmentMatrix;
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near;
const penetration = shell.penetration!.near;
const hasCalibratedShells = hasEquipment(
103,
Duel.state.antagonist.tank.equipment_preset,
equipment
equipment,
);
const equalize =
(Duel.state.equalize
? Duel.state.antagonist.tank.equalizer?.penetration
: undefined) ?? 1;
material.uniforms.penetration.value =
penetration *
resolvePenetrationCoefficient(hasCalibratedShells, shell.type);
resolvePenetrationCoefficient(
hasCalibratedShells,
Duel.state.equalize,
shell.type,
Duel.state.antagonist.tank.equalizer,
) *
equalize;
}
handleShellChange();
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
handleProtagonistEquipmentChange();
handleAntagonistEquipmentChange();
const unsubscribes = [
Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange),
Duel.on(
(state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange
handleProtagonistEquipmentChange,
),
Duel.on(
(state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange
handleAntagonistEquipmentChange,
),
Duel.on(
(state) => state.equalize,
() => {
handleProtagonistEquipmentChange();
handleAntagonistEquipmentChange();
},
),
];
@@ -18,6 +18,7 @@ import {
import { degToRad } from "three/src/math/MathUtils.js";
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { defaultEqualizer } from "../../../../core/blitzkit/tankToDuelMember";
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
import { Duel } from "../../../../stores/duel";
import {
@@ -83,16 +84,25 @@ export function SpacedArmorSceneComponent({
point: Vector3,
intersections: Intersection[],
allowRicochet: boolean,
remainingPenetrationInput?: number
remainingPenetrationInput?: number,
) => {
const { customShell } = Tankopedia.state;
const shell = customShell ?? Duel.state.antagonist.shell;
const { equalize } = Duel.state;
const protagonistEqualizer =
(equalize ? Duel.state.protagonist.tank.equalizer : undefined) ??
defaultEqualizer;
const antagonistEqualizer =
(equalize ? Duel.state.antagonist.tank.equalizer : undefined) ??
defaultEqualizer;
const cameraNormal = camera.position.clone().sub(point).normalize();
const shot: Shot = {
splashRadius:
shell.type === ShellType.HE ? shell.explosion_radius : undefined,
shell.type === ShellType.SHELL_TYPE_HE
? shell.explosion_radius
: undefined,
damage: -1,
containsGaps: shell.type === ShellType.HEAT,
containsGaps: shell.type === ShellType.SHELL_TYPE_HEAT,
in: {
surfaceNormal: cameraNormal,
layers: [],
@@ -103,21 +113,27 @@ export function SpacedArmorSceneComponent({
const hasCalibratedShells = hasEquipment(
103,
Duel.state.antagonist.tank.equipment_preset,
Duel.state.antagonist.equipmentMatrix
Duel.state.antagonist.equipmentMatrix,
);
const hasEnhancedArmor = hasEquipment(
110,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const penetration =
shell.penetration.near *
resolvePenetrationCoefficient(hasCalibratedShells, shell.type);
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
shell.penetration!.near *
resolvePenetrationCoefficient(
hasCalibratedShells,
equalize,
shell.type,
antagonistEqualizer,
);
const thicknessCoefficient =
(hasEnhancedArmor ? 1.03 : 1) * protagonistEqualizer.armor;
const filteredIntersections = intersections.filter(
(intersection) =>
"type" in intersection.object.userData &&
!discardClippingPlane(intersection.object, intersection.point)
!discardClippingPlane(intersection.object, intersection.point),
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
const noDuplicateIntersections: typeof filteredIntersections = [];
@@ -126,12 +142,12 @@ export function SpacedArmorSceneComponent({
if (intersection.object.userData.type === ArmorType.External) {
if (
!encounteredExternalModuleVariants.includes(
intersection.object.userData.variant
intersection.object.userData.variant,
)
) {
noDuplicateIntersections.push(intersection);
encounteredExternalModuleVariants.push(
intersection.object.userData.variant
intersection.object.userData.variant,
);
}
} else {
@@ -159,10 +175,10 @@ export function SpacedArmorSceneComponent({
const surfaceNormal = intersection
.normal!.clone()
.applyQuaternion(
intersection.object.getWorldQuaternion(new Quaternion())
intersection.object.getWorldQuaternion(new Quaternion()),
);
if (shell.type === ShellType.HEAT && index > 0) {
if (shell.type === ShellType.SHELL_TYPE_HEAT && index > 0) {
const lastLayer = shot.in.layers.at(-1) as ShotLayerBase;
const distance = lastLayer.point.distanceTo(intersection.point);
remainingPenetration -= 0.5 * remainingPenetration * distance;
@@ -181,7 +197,7 @@ export function SpacedArmorSceneComponent({
const thickness = layer.thickness * thicknessCoefficient;
remainingPenetration -= thickness;
const blocked =
remainingPenetration <= 0 || shell.type === ShellType.HE;
remainingPenetration <= 0 || shell.type === ShellType.SHELL_TYPE_HE;
shot.in.layers.push({
type: ArmorType.External,
@@ -190,7 +206,7 @@ export function SpacedArmorSceneComponent({
point: intersection.point,
surfaceNormal,
status:
shell.type === ShellType.HE
shell.type === ShellType.SHELL_TYPE_HE
? "blocked"
: blocked
? "blocked"
@@ -200,7 +216,7 @@ export function SpacedArmorSceneComponent({
} else {
const thickness = layer.thickness * thicknessCoefficient;
const ricochet = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!
isExplosive(shell.type) ? 90 : shell.ricochet!,
);
const angle = surfaceNormal.angleTo(cameraNormal);
@@ -241,7 +257,8 @@ export function SpacedArmorSceneComponent({
point: intersection.point,
surfaceNormal: intersection.face!.normal,
status:
shell.type === ShellType.HE && layer.type !== ArmorType.Primary
shell.type === ShellType.SHELL_TYPE_HE &&
layer.type !== ArmorType.Primary
? "blocked"
: blocked
? "blocked"
@@ -251,7 +268,10 @@ export function SpacedArmorSceneComponent({
}
}
if (shell.type !== ShellType.HE && remainingPenetration <= 0) {
if (
shell.type !== ShellType.SHELL_TYPE_HE &&
remainingPenetration <= 0
) {
break;
}
@@ -266,7 +286,7 @@ export function SpacedArmorSceneComponent({
if (!lastLayer || !firstLayer) {
return null;
} else {
if (shell.type === ShellType.HE) {
if (shell.type === ShellType.SHELL_TYPE_HE) {
const totalSpacedArmorThickness = shot.in.layers.reduce(
(acc, layer) => {
if (layer.type === null || layer.type === ArmorType.Primary) {
@@ -279,10 +299,10 @@ export function SpacedArmorSceneComponent({
return acc + layer.thicknessAngled;
},
0
0,
);
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
lastLayer.point
lastLayer.point,
);
const finalDamage = Math.max(
0,
@@ -291,7 +311,7 @@ export function SpacedArmorSceneComponent({
(1 - distanceFromSpacedArmor / shell.explosion_radius!) -
1.1 *
(lastLayer.thicknessAngled +
Math.min(penetration, totalSpacedArmorThickness))
Math.min(penetration, totalSpacedArmorThickness)),
);
/**
@@ -328,20 +348,20 @@ export function SpacedArmorSceneComponent({
.sub(
lastLayer.surfaceNormal
.clone()
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal))
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)),
);
caster.set(lastLayer.point, ricochetNormal);
const ricochetIntersections = caster.intersectObjects(
props.scene.children,
true
true,
);
const outShot = shoot(
lastLayer.point,
ricochetIntersections,
false,
remainingPenetration
remainingPenetration,
);
shot.in.status = "ricochet";
@@ -361,9 +381,11 @@ export function SpacedArmorSceneComponent({
}
}
shot.damage *= antagonistEqualizer.damage;
return shot;
},
[camera]
[camera],
);
return (
@@ -43,9 +43,9 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const canvas = useThree((state) => state.gl.domElement);
const maskOrigin =
@@ -53,16 +53,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.showPrimaryArmor
(state) => state.showPrimaryArmor,
);
const showSpacedArmor = TankopediaPersistent.use(
(state) => state.showSpacedArmor
(state) => state.showSpacedArmor,
);
const showExternalModules = TankopediaPersistent.use(
(state) => state.showExternalModules
(state) => state.showExternalModules,
);
const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73)
state.protagonist.consumables.includes(73),
);
useTankTransform(track, turret, turretContainer, gunContainer);
@@ -75,8 +75,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const isVisible = isHull;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor,
armorId
tankModelDefinition.armor!,
armorId,
);
if (
@@ -127,13 +127,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<group ref={turretContainer}>
{armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`,
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor,
armorId
turretModelDefinition.armor!,
armorId,
);
if (
@@ -168,12 +168,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
@@ -184,10 +184,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}
@@ -216,13 +216,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<group ref={gunContainer}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`,
);
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor,
armorId
gunModelDefinition.armor!,
armorId,
);
if (
@@ -256,12 +256,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
@@ -272,10 +272,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
delta.y * (Math.PI / boundingRect.height),
modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}
@@ -332,12 +332,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
@@ -348,10 +348,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
delta.y * (Math.PI / boundingRect.height),
modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}
@@ -22,7 +22,10 @@ import {
modelTransformEvent,
type ModelTransformEventData,
} from "../../../core/blitzkit/modelTransform";
import { defaultEqualizer } from "../../../core/blitzkit/tankToDuelMember";
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
import { useEquipment } from "../../../hooks/useEquipment";
import { Duel } from "../../../stores/duel";
import { Tankopedia } from "../../../stores/tankopedia";
import { transitionEvent } from "../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { ArmorType } from "./SpacedArmorScene";
@@ -64,46 +67,68 @@ const externalModuleColor = new Color(192 / 255, 192 / 255, 192 / 255);
export function StaticArmorSceneComponent({
name,
thickness,
thickness: thicknessRaw,
thicknessRange,
node,
onPointerDown,
...props
}: StaticArmorSceneComponentProps) {
const camera = useThree((state) => state.camera);
const hasEnhancedArmor = useEquipment(110);
const tank = Duel.use((state) => state.protagonist.tank);
const equalize = Duel.use((state) => state.equalize);
const equalizer = (equalize ? tank.equalizer : undefined) ?? defaultEqualizer;
const thicknessCoefficient = (hasEnhancedArmor ? 1.03 : 1) * equalizer.armor;
const thickness = thicknessRaw * thicknessCoefficient;
const x = thickness / thicknessRange.value;
const xClamped = clamp(x, 0, 1);
let color: Color;
let opacity: number;
let depthWrite = true;
switch (props.type) {
case ArmorType.Primary:
if (x > 1) {
color = new Color(clamp(2 - x, 0.5, 1), 0, 0);
} else {
color = new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0);
const color = useMemo(() => {
switch (props.type) {
case ArmorType.Primary: {
if (x > 1) {
return new Color(clamp(2 - x, 0.5, 1), 0, 0);
} else {
return new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0);
}
}
opacity = 1;
break;
case ArmorType.Spaced:
color = new Color(
clamp(1 - (7 / 8) * xClamped, 0, 1),
0,
clamp(1 - (1 / 8) * xClamped, 0, 1),
);
opacity = clamp(x + 1 / 2, 0, 1);
break;
case ArmorType.Spaced: {
return new Color(
clamp(1 - (7 / 8) * xClamped, 0, 1),
0,
clamp(1 - (1 / 8) * xClamped, 0, 1),
);
}
case ArmorType.External:
color = externalModuleColor;
opacity = 1 / 8;
depthWrite = false;
break;
}
case ArmorType.External: {
return externalModuleColor;
}
}
}, [props.type, xClamped]);
const opacity = useMemo(() => {
switch (props.type) {
case ArmorType.Primary:
return 1;
opacity = clamp(opacity, 0, 1);
case ArmorType.Spaced:
return clamp(x + 1 / 2, 0, 1);
case ArmorType.External:
return 1 / 8;
}
}, [props.type, x]);
const depthWrite = useMemo(() => {
switch (props.type) {
case ArmorType.Primary:
return true;
case ArmorType.Spaced:
return true;
case ArmorType.External:
return false;
}
}, [props.type]);
const surfaceMaterial = useMemo(
() =>
@@ -115,7 +140,7 @@ export function StaticArmorSceneComponent({
opacity: 0,
userData: { opacity0: opacity },
}),
[thickness],
[hasEnhancedArmor, equalize],
);
const outlineMaterial = useMemo(
() =>
@@ -126,7 +151,7 @@ export function StaticArmorSceneComponent({
opacity: 0,
transparent: true,
}),
[thickness],
[hasEnhancedArmor, equalize],
);
useEffect(() => {
@@ -229,7 +254,7 @@ export function StaticArmorSceneComponent({
return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe());
};
}, [thickness]);
}, [hasEnhancedArmor, equalize]);
return (
<>
@@ -1,16 +1,16 @@
import { TankClass } from '@blitzkit/core';
import type { ComponentProps, ReactNode } from 'react';
import { ClassHeavy } from './components/ClassHeavy';
import { ClassLight } from './components/ClassLight';
import { ClassMedium } from './components/ClassMedium';
import { ClassTankDestroyer } from './components/ClassTankDestroyer';
import { TankClass } from "@blitzkit/core";
import type { ComponentProps, ReactNode } from "react";
import { ClassHeavy } from "./components/ClassHeavy";
import { ClassLight } from "./components/ClassLight";
import { ClassMedium } from "./components/ClassMedium";
import { ClassTankDestroyer } from "./components/ClassTankDestroyer";
export const classIcons: Record<
TankClass,
(props: ComponentProps<'svg'>) => ReactNode
(props: ComponentProps<"svg">) => ReactNode
> = {
[TankClass.TANK_DESTROYER]: ClassTankDestroyer,
[TankClass.HEAVY]: ClassHeavy,
[TankClass.LIGHT]: ClassLight,
[TankClass.MEDIUM]: ClassMedium,
[TankClass.TANK_CLASS_TANK_DESTROYER]: ClassTankDestroyer,
[TankClass.TANK_CLASS_HEAVY]: ClassHeavy,
[TankClass.TANK_CLASS_LIGHT]: ClassLight,
[TankClass.TANK_CLASS_MEDIUM]: ClassMedium,
};
@@ -15,7 +15,7 @@ interface BodyProps {
export function Body({ stats }: BodyProps) {
const members = CompareEphemeral.use((state) => state.members);
const hasNonRegularGun = members.some(
({ gun }) => gun.gun_type!.$case !== "regular"
({ gun }) => gun.gun_type!.$case !== "regular",
);
const { unwrap, strings } = useLocale();
@@ -55,7 +55,7 @@ export function Body({ stats }: BodyProps) {
}}
>
<img
alt={unwrap(thisShell.name)}
alt={unwrap(thisShell.name!)}
width={16}
height={16}
src={asset(`icons/shells/${thisShell.icon}.webp`)}
@@ -14,7 +14,7 @@ import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
type CompareRowProps = {
display?: (
member: Awaited<TankCharacteristics>
member: Awaited<TankCharacteristics>,
) => number | string | undefined;
deltaType?: CompareCellDirection;
decimals?: number;
@@ -46,7 +46,7 @@ export function CompareRow({
const values = stats.map((stat) =>
typeof props.value === "function"
? props.value(stat)!
: (stat[props.value] as number)
: (stat[props.value] as number),
);
const id = useRef(Math.random());
const name =
@@ -56,10 +56,10 @@ export function CompareRow({
return (
<Table.Row>
<StickyRowHeaderCell>
<StickyRowHeaderCell left="-1.25rem">
<Flex
align="center"
style={{ whiteSpace: "nowrap" }}
maxWidth={{ initial: "6rem", sm: "unset" }}
gap={indent ? { initial: "4", sm: "6" } : { initial: "1", sm: "2" }}
>
<IconButton
@@ -111,8 +111,8 @@ export function CompareRow({
const resolvedDisplayValue = display
? display(stats[index])
: decimals === undefined
? value
: value?.toFixed(decimals);
? value
: value?.toFixed(decimals);
return (
<CompareCell
@@ -145,8 +145,8 @@ export function CompareRow({
deltaNominalDisplay
? deltaNominalDisplay(delta)
: decimals === undefined
? delta
: delta.toFixed(decimals)
? delta
: delta.toFixed(decimals)
}`}
)
</Text>
@@ -156,7 +156,7 @@ export function CompareRow({
<Text color="gray">
(
{`${deltaPercentage > 0 ? "+" : ""}${Math.round(
deltaPercentage * 100
deltaPercentage * 100,
)}%`}
)
</Text>
@@ -1,7 +1,7 @@
import { Heading, Table } from '@radix-ui/themes';
import { times } from 'lodash-es';
import { CompareEphemeral } from '../../stores/compareEphemeral';
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
import { Heading, Table } from "@radix-ui/themes";
import { times } from "lodash-es";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
export function CompareSectionTitle({ children }: { children: string }) {
const memberCount = CompareEphemeral.use((state) => state.members.length);
@@ -1,7 +1,15 @@
import { asset, availableProvisions, permanentSkills } from "@blitzkit/core";
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
import { Button, Flex, IconButton, Popover, Table } from "@radix-ui/themes";
import {
Button,
Flex,
IconButton,
Popover,
Switch,
Table,
Text,
} from "@radix-ui/themes";
import { times } from "lodash-es";
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
import { awaitableEquipmentDefinitions } from "../../core/awaitables/equipmentDefinitions";
@@ -44,6 +52,7 @@ export function CompareTable({ stats }: CompareTableProps) {
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
const members = CompareEphemeral.use((state) => state.members);
const { unwrap, strings } = useLocale();
const equalize = CompareEphemeral.use((state) => state.equalize);
return (
<StickyTableRoot
@@ -80,10 +89,12 @@ export function CompareTable({ stats }: CompareTableProps) {
<Table.Row>
<Table.Cell style={{ height: "100%" }}>
<Flex
gap="2"
style={{
width: "100%",
height: "100%",
}}
direction="column"
align="center"
justify="center"
>
@@ -105,13 +116,13 @@ export function CompareTable({ stats }: CompareTableProps) {
crewSkills[skill] === 0
? Var("gray-11")
: permanentSkills.includes(skill)
? Var("crimson-11")
: Var("amber-11"),
? Var("crimson-11")
: Var("amber-11"),
}}
/>
))}
</Flex>
)
),
)}
</Flex>
</Button>
@@ -155,6 +166,21 @@ export function CompareTable({ stats }: CompareTableProps) {
</Flex>
</Popover.Content>
</Popover.Root>
<Flex align="center" gap="1">
<Text>
{strings.website.tools.compare.table.skills.equalize}
</Text>
<Switch
size="1"
checked={equalize}
onCheckedChange={(checked) => {
CompareEphemeral.mutate((draft) => {
draft.equalize = checked;
});
}}
/>
</Flex>
</Flex>
</Table.Cell>
@@ -173,19 +199,19 @@ export function CompareTable({ stats }: CompareTableProps) {
consumables,
camouflage,
},
index
index,
) => {
const equipmentPreset =
equipmentDefinitions.presets[tank.equipment_preset];
const provisionsList = availableProvisions(
tank,
gun,
provisionDefinitions
provisionDefinitions,
);
const consumablesList = Object.values(
consumableDefinitions.consumables
consumableDefinitions.consumables,
).filter((consumable) =>
checkConsumableProvisionInclusivity(consumable, tank, gun)
checkConsumableProvisionInclusivity(consumable, tank, gun),
);
return (
@@ -294,11 +320,11 @@ export function CompareTable({ stats }: CompareTableProps) {
<img
alt={unwrap(
provisionDefinitions.provisions[provision]
.name
.name!,
)}
key={provision}
src={asset(
`/icons/provisions/${provision}.webp`
`/icons/provisions/${provision}.webp`,
)}
style={{
left: "50%",
@@ -335,7 +361,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<Popover.Content>
<ProvisionsManager
provisions={provisionsList.map(
(provision) => provision.id
(provision) => provision.id,
)}
selected={provisions}
disabled={tank.max_provisions === provisions.length}
@@ -452,7 +478,7 @@ export function CompareTable({ stats }: CompareTableProps) {
CompareEphemeral.mutate((draft) => {
draft.members[index].equipmentMatrix = times(
3,
() => times(3, () => 0)
() => times(3, () => 0),
) as EquipmentMatrix;
draft.sorting = undefined;
});
@@ -501,11 +527,11 @@ export function CompareTable({ stats }: CompareTableProps) {
alt={unwrap(
consumableDefinitions.consumables[
consumable
].name
].name!,
)}
key={consumable}
src={asset(
`/icons/consumables/${consumable}.webp`
`/icons/consumables/${consumable}.webp`,
)}
style={{
left: "50%",
@@ -610,7 +636,7 @@ export function CompareTable({ stats }: CompareTableProps) {
</Flex>
</Table.Cell>
);
}
},
)}
</Table.Row>
</Table.Body>
@@ -55,7 +55,7 @@ export function TankCard({ index, tank }: TankCardProps) {
};
});
const closest = distances.reduce((a, b) =>
a.distance < b.distance ? a : b
a.distance < b.distance ? a : b,
);
dropIndex = closest.index;
@@ -85,8 +85,8 @@ export function TankCard({ index, tank }: TankCardProps) {
CompareEphemeral.mutate((draft) => {
const fromIndex = index;
const toIndex = dropIndex > index ? dropIndex - 1 : dropIndex;
const member = draft.members[fromIndex];
draft.members.splice(fromIndex, 1);
draft.members.splice(toIndex, 0, member);
});
@@ -99,7 +99,7 @@ export function TankCard({ index, tank }: TankCardProps) {
return () => {
draggable.current?.removeEventListener("pointerdown", handlePointerDown);
};
}, []);
}, [index]);
return (
<StickyColumnHeaderCell width="0">
@@ -119,7 +119,7 @@ export function TankCard({ index, tank }: TankCardProps) {
}}
>
<img
alt={unwrap(tank.name)}
alt={unwrap(tank.name!)}
src={tankIcon(tank.id)}
width={64}
height={64}
@@ -137,11 +137,10 @@ export function TankCard({ index, tank }: TankCardProps) {
textOverflow: "ellipsis",
}}
>
{unwrap(tank.name)}
{unwrap(tank.name!)}
</Text>
</Flex>
</Flex>
<InsertionMarker index={index + 1} />
</StickyColumnHeaderCell>
);
@@ -36,9 +36,8 @@ export function ConsumablesManager({
special={consumable.duration === undefined}
duration={
timers
? consumable.duration === undefined
? undefined
: consumable.duration * (hasHighEndConsumables ? 1.33 : 1)
? (consumable.duration ?? 0) *
(hasHighEndConsumables ? 1.33 : 1) || undefined
: undefined
}
cooldown={
@@ -0,0 +1,40 @@
import { Code, Tooltip, type CodeProps } from "@radix-ui/themes";
import { useState } from "react";
import { useLocale } from "../hooks/useLocale";
export interface CopyableCodeProps extends CodeProps {
copy?: string;
}
export function CopyableCode({
copy,
onClick,
onPointerLeave,
style,
children,
...props
}: CopyableCodeProps) {
const [copied, setCopied] = useState(false);
const { strings } = useLocale();
return (
<Tooltip open={copied} content={strings.website.common.copy_button.copied}>
<Code
{...props}
style={{ ...style, cursor: "pointer" }}
onClick={(event) => {
onClick?.(event);
if (!copy) return;
navigator.clipboard.writeText(copy);
setCopied(true);
}}
onPointerLeave={(event) => {
onPointerLeave?.(event);
setCopied(false);
}}
>
{children}
</Code>
</Tooltip>
);
}
@@ -55,7 +55,7 @@ export const fakeCompositeStats = compositeStats(
wins: 30,
xp: 1230 * 50,
},
averageDefinitions.averages[7297].mu
averageDefinitions.averages[7297].mu!,
);
export function BreakdownPreview() {
@@ -88,9 +88,9 @@ export function BreakdownRenderer() {
const diff = useMemo(
() =>
deltaTankStats(tankStatsA, tankStatsB).sort(
(a, b) => b.last_battle_time - a.last_battle_time
(a, b) => b.last_battle_time - a.last_battle_time,
),
[tankStatsB]
[tankStatsB],
);
const compositeStats = diff
.map((diff) => {
@@ -100,14 +100,14 @@ export function BreakdownRenderer() {
const compositeStats = calculateCompositeStats(
{ battle_life_time: diff.battle_life_time, ...diff.all },
average.mu
average.mu!,
);
return { id: diff.tank_id, compositeStats };
})
.filter((stats) => stats !== null);
const sum = sumCompositeStats(
compositeStats.map(({ compositeStats }) => compositeStats)
compositeStats.map(({ compositeStats }) => compositeStats),
);
useEffect(() => {
@@ -0,0 +1,21 @@
import type { ComponentProps } from "react";
export function EqualIcon(props: ComponentProps<"svg">) {
return (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.5 5C1.22386 5 1 5.22386 1 5.5C1 5.77614 1.22386 6 1.5 6H13.5C13.7761 6 14 5.77614 14 5.5C14 5.22386 13.7761 5 13.5 5H1.5ZM1 9.5C1 9.22386 1.22386 9 1.5 9H13.5C13.7761 9 14 9.22386 14 9.5C14 9.77614 13.7761 10 13.5 10H1.5C1.22386 10 1 9.77614 1 9.5Z"
fill="currentColor"
/>
</svg>
);
}
@@ -19,7 +19,7 @@ type GalleryCardProps = MaybeSkeletonComponentProps<{
export function GalleryCard(props: GalleryCardProps) {
const src = asset(
`gallery/avatars/${!props.skeleton && `${props.avatar.id}${props.avatar.extension}`}`
`gallery/avatars/${!props.skeleton && `${props.avatar.id}${props.avatar.extension}`}`,
);
const card = useRef<HTMLDivElement>(null!);
const { unwrap } = useLocale();
@@ -54,11 +54,11 @@ export function GalleryCard(props: GalleryCardProps) {
objectFit: "cover",
}}
src={src}
alt={unwrap(props.avatar.name)}
alt={unwrap(props.avatar.name!)}
/>
<Text size="1" align="center" style={{ maxWidth: "100%" }}>
{unwrap(props.avatar.name)}
{unwrap(props.avatar.name!)}
</Text>
</>
)}
@@ -90,12 +90,12 @@ export function GalleryCard(props: GalleryCardProps) {
</Flex>
<Flex px="4" justify="center" mt="3">
<img src={src} alt={unwrap(props.avatar.name)} />
<img src={src} alt={unwrap(props.avatar.name!)} />
</Flex>
<Flex justify="center" mt="4">
<Dialog.Title align="center" style={{ maxWidth: "10rem" }}>
{unwrap(props.avatar.name)}
{unwrap(props.avatar.name!)}
</Dialog.Title>
</Flex>
</Dialog.Content>
+5 -5
View File
@@ -82,10 +82,10 @@ export function Guesser() {
});
setResults(
searchResults.map((result) => tankDefinitions.tanks[result.obj.id])
searchResults.map((result) => tankDefinitions.tanks[result.obj.id]),
);
}, 500),
[]
[],
);
useEffect(() => {
@@ -126,11 +126,11 @@ export function Guesser() {
onClick={() => {
if (!input.current) return;
input.current.value = unwrap(result.name);
input.current.value = unwrap(result.name!);
setSelected(result);
setResults(null);
}}
text={unwrap(result.name)}
text={unwrap(result.name!)}
prefix={
<img
style={{ width: "1em", height: "1em" }}
@@ -321,7 +321,7 @@ export function Guesser() {
return tiers.includes(tank.tier);
});
const id = Number(
filteredIds[Math.floor(Math.random() * filteredIds.length)]
filteredIds[Math.floor(Math.random() * filteredIds.length)],
);
const tank = tankDefinitions.tanks[id];
@@ -97,7 +97,7 @@ const strings = getStrings(locale);
window.innerHeight) **
2,
0,
1
1,
);
// https://www.desmos.com/calculator/ndz2poxk32
@@ -145,11 +145,9 @@ const strings = getStrings(locale);
</Flex>
</Heading>
<Text color="gray" align="center" size="4"
>{
literals(strings.website.home.hero.credit, { name: "TrèsAbhi" })
}</Text
>
<Text color="gray" align="center" size="4">
{literals(strings.website.home.hero.credit, { name: "TrèsAbhi" })}
</Text>
</Flex>
</Flex>
</Flex>
@@ -84,7 +84,7 @@ export function Model() {
useEffect(() => {
function handleModelTransform(
event: QuicklimeEvent<ModelTransformEventData>
event: QuicklimeEvent<ModelTransformEventData>,
) {
turretGroup.current.rotation.z = event.data.yaw;
gunGroup.current.rotation.x = event.data.pitch;
@@ -118,16 +118,16 @@ export function Model() {
ref={turretGroup}
rotation={[0, 0, modelTransformEvent.last!.yaw]}
position={[
tankModel.turret_origin.x + trackModel.origin.x,
tankModel.turret_origin.z + trackModel.origin.z,
tankModel.turret_origin.y + trackModel.origin.y,
tankModel.turret_origin!.x + trackModel.origin!.x,
tankModel.turret_origin!.z + trackModel.origin!.z,
tankModel.turret_origin!.y + trackModel.origin!.y,
]}
>
<group
position={[
-turretTankModel.turret_origin.x - turretTrackModel.origin.x,
-turretTankModel.turret_origin.z - turretTrackModel.origin.z,
-turretTankModel.turret_origin.y - turretTrackModel.origin.y,
-turretTankModel.turret_origin!.x - turretTrackModel.origin!.x,
-turretTankModel.turret_origin!.z - turretTrackModel.origin!.z,
-turretTankModel.turret_origin!.y - turretTrackModel.origin!.y,
]}
>
{turretNodes.map((node) => {
@@ -160,38 +160,81 @@ export function Model() {
ref={gunGroup}
rotation={[modelTransformEvent.last!.pitch, 0, 0]}
position={[
turretTankModel.turret_origin.x +
turretTrackModel.origin.x +
turretModel.gun_origin.x,
turretTankModel.turret_origin.z +
turretTrackModel.origin.z +
turretModel.gun_origin.z,
turretTankModel.turret_origin.y +
turretTrackModel.origin.y +
turretModel.gun_origin.y,
turretTankModel.turret_origin!.x +
turretTrackModel.origin!.x +
turretModel.gun_origin!.x,
turretTankModel.turret_origin!.z +
turretTrackModel.origin!.z +
turretModel.gun_origin!.z,
turretTankModel.turret_origin!.y +
turretTrackModel.origin!.y +
turretModel.gun_origin!.y,
]}
>
<group
position={[
-gunTankModel.turret_origin.x -
gunTrackModel.origin.x -
gunTurretModel.gun_origin.x,
-gunTankModel.turret_origin.z -
gunTrackModel.origin.z -
gunTurretModel.gun_origin.z,
-gunTankModel.turret_origin.y -
gunTrackModel.origin.y -
gunTurretModel.gun_origin.y,
-turretTankModel.turret_origin!.x -
turretTrackModel.origin!.x -
turretModel.gun_origin!.x,
-turretTankModel.turret_origin!.z -
turretTrackModel.origin!.z -
turretModel.gun_origin!.z,
-turretTankModel.turret_origin!.y -
turretTrackModel.origin!.y -
turretModel.gun_origin!.y,
]}
>
{turretNodes.map((node) => {
const isCurrentMantlet =
node.name ===
`gun_${turretModel.guns[turret.turret.guns.at(-1)!.id].model_id.toString().padStart(2, "0")}_mask`;
// const isCurrentGun =
// node.name ===
// `gun_${gunModel.model_id.toString().padStart(2, "0")}`;
const isVisible = isCurrentMantlet;
if (!isVisible) return null;
return jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
castShadow
receiveShadow
onPointerDown={(event) => {
enablePitchRotation.current = true;
handlePointerDown(event);
}}
/>
);
},
});
})}
</group>
<group
position={[
-gunTankModel.turret_origin!.x -
gunTrackModel.origin!.x -
gunTurretModel.gun_origin!.x,
-gunTankModel.turret_origin!.z -
gunTrackModel.origin!.z -
gunTurretModel.gun_origin!.z,
-gunTankModel.turret_origin!.y -
gunTrackModel.origin!.y -
gunTurretModel.gun_origin!.y,
]}
>
{gunNodes.map((node) => {
const isCurrentMantlet =
node.name ===
`gun_${gunModel.model_id.toString().padStart(2, "0")}_mask`;
// const isCurrentMantlet =
// node.name ===
// `gun_${gunModel.model_id.toString().padStart(2, "0")}_mask`;
const isCurrentGun =
node.name ===
`gun_${gunModel.model_id.toString().padStart(2, "0")}`;
const isVisible = isCurrentGun || isCurrentMantlet;
const isVisible = isCurrentGun;
if (!isVisible) return null;
@@ -1,11 +1,11 @@
import { asset } from '@blitzkit/core';
import { ClockIcon, ReloadIcon } from '@radix-ui/react-icons';
import { Flex, Text } from '@radix-ui/themes';
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions';
import { useDelta } from '../../hooks/useDelta';
import { useLocale } from '../../hooks/useLocale';
import { GenericTankComponentButton } from './GenericTankComponentButton';
import type { TankComponentButtonProps } from './TankComponentButton';
import { asset } from "@blitzkit/core";
import { ClockIcon, ReloadIcon } from "@radix-ui/react-icons";
import { Flex, Text } from "@radix-ui/themes";
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
import { useDelta } from "../../hooks/useDelta";
import { useLocale } from "../../hooks/useLocale";
import { GenericTankComponentButton } from "./GenericTankComponentButton";
import type { TankComponentButtonProps } from "./TankComponentButton";
interface ConsumableButtonProps extends TankComponentButtonProps {
consumable: number;
@@ -27,8 +27,8 @@ export function ConsumableButton({
return (
<GenericTankComponentButton
style={{ width: '6rem' }}
tooltip={unwrap(consumableDefinitions.consumables[consumable].name)}
style={{ width: "6rem" }}
tooltip={unwrap(consumableDefinitions.consumables[consumable].name!)}
icon={asset(`icons/consumables/${consumable}.webp`)}
{...props}
>
@@ -39,9 +39,9 @@ export function ConsumableButton({
size="1"
color={
durationDelta > 0
? 'green'
? "green"
: durationDelta < 0
? 'tomato'
? "tomato"
: undefined
}
>
@@ -56,9 +56,9 @@ export function ConsumableButton({
size="1"
color={
cooldownDelta < 0
? 'green'
? "green"
: cooldownDelta > 0
? 'tomato'
? "tomato"
: undefined
}
>
@@ -1,8 +1,8 @@
import { asset } from '@blitzkit/core';
import { awaitableEquipmentDefinitions } from '../../core/awaitables/equipmentDefinitions';
import { useLocale } from '../../hooks/useLocale';
import { GenericTankComponentButton } from './GenericTankComponentButton';
import type { TankComponentButtonProps } from './TankComponentButton';
import { asset } from "@blitzkit/core";
import { awaitableEquipmentDefinitions } from "../../core/awaitables/equipmentDefinitions";
import { useLocale } from "../../hooks/useLocale";
import { GenericTankComponentButton } from "./GenericTankComponentButton";
import type { TankComponentButtonProps } from "./TankComponentButton";
interface EquipmentButtonProps extends TankComponentButtonProps {
equipment: number;
@@ -15,7 +15,7 @@ export function EquipmentButton({ equipment, ...props }: EquipmentButtonProps) {
return (
<GenericTankComponentButton
tooltip={unwrap(equipmentDefinitions.equipments[equipment].name)}
tooltip={unwrap(equipmentDefinitions.equipments[equipment].name!)}
icon={asset(`icons/equipment/${equipment}.webp`)}
{...props}
/>
@@ -1,8 +1,8 @@
import { asset } from '@blitzkit/core';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { useLocale } from '../../hooks/useLocale';
import { GenericTankComponentButton } from './GenericTankComponentButton';
import type { TankComponentButtonProps } from './TankComponentButton';
import { asset } from "@blitzkit/core";
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
import { useLocale } from "../../hooks/useLocale";
import { GenericTankComponentButton } from "./GenericTankComponentButton";
import type { TankComponentButtonProps } from "./TankComponentButton";
interface ProvisionButtonProps extends TankComponentButtonProps {
provision: number;
@@ -15,7 +15,7 @@ export function ProvisionButton({ provision, ...props }: ProvisionButtonProps) {
return (
<GenericTankComponentButton
tooltip={unwrap(provisionDefinitions.provisions[provision].name)}
tooltip={unwrap(provisionDefinitions.provisions[provision].name!)}
icon={asset(`icons/provisions/${provision}.webp`)}
{...props}
/>
@@ -6,11 +6,11 @@ import {
type TankDefinition,
type TrackDefinition,
type TurretDefinition,
} from '@blitzkit/core';
import { DropdownMenu, Flex } from '@radix-ui/themes';
import { useState } from 'react';
import { useLocale } from '../hooks/useLocale';
import { ModuleButton } from './ModuleButtons/ModuleButton';
} from "@blitzkit/core";
import { DropdownMenu, Flex } from "@radix-ui/themes";
import { useState } from "react";
import { useLocale } from "../hooks/useLocale";
import { ModuleButton } from "./ModuleButtons/ModuleButton";
interface ModuleManagerProps {
tank: TankDefinition;
@@ -72,8 +72,8 @@ export function ModuleManager({
variant="ghost"
module="turret"
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
/>{' '}
{unwrap(turret.name)}
/>{" "}
{unwrap(turret.name!)}
</DropdownMenu.RadioItem>
);
})}
@@ -112,8 +112,8 @@ export function ModuleManager({
variant="ghost"
module="gun"
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
/>{' '}
{unwrap(gun.name)}
/>{" "}
{unwrap(gun.name!)}
</DropdownMenu.RadioItem>
);
})}
@@ -151,8 +151,8 @@ export function ModuleManager({
variant="ghost"
module="engine"
discriminator={TIER_ROMAN_NUMERALS[engine.tier]}
/>{' '}
{unwrap(engine.name)}
/>{" "}
{unwrap(engine.name!)}
</DropdownMenu.RadioItem>
);
})}
@@ -190,8 +190,8 @@ export function ModuleManager({
variant="ghost"
module="chassis"
discriminator={TIER_ROMAN_NUMERALS[track.tier]}
/>{' '}
{unwrap(track.name)}
/>{" "}
{unwrap(track.name!)}
</DropdownMenu.RadioItem>
);
})}
@@ -20,7 +20,7 @@ export const TankRow = memo<TankRowProps>(
const averages = averageDefinitions.averages[tank.id];
const ratio = useAveragesExclusionRatio();
const playerCountPeriod = Performance.use(
(state) => state.playerCountPeriod
(state) => state.playerCountPeriod,
);
const { locale } = useLocale();
@@ -29,107 +29,109 @@ export const TankRow = memo<TankRowProps>(
<TankRowHeaderCell tank={tank} />
<CompareCell
value={averages.mu.wins / averages.mu.battles}
value={averages.mu!.wins / averages.mu!.battles}
truth={average.winrate}
align="center"
>
{((averages.mu.wins / averages.mu.battles) * 100).toFixed(1)}%
{((averages.mu!.wins / averages.mu!.battles) * 100).toFixed(1)}%
</CompareCell>
<Table.Cell align="center">
{formatCompact(
locale,
Math.round(ratio * averages.samples[playerCountPeriod])
Math.round(ratio * averages.samples![playerCountPeriod]),
)}
</Table.Cell>
<CompareCell
value={averages.mu.damage_dealt / averages.mu.battles}
value={averages.mu!.damage_dealt / averages.mu!.battles}
truth={average.damage}
align="center"
>
{Math.round(
averages.mu.damage_dealt / averages.mu.battles
averages.mu!.damage_dealt / averages.mu!.battles,
).toLocaleString(locale)}
</CompareCell>
<CompareCell
value={averages.mu.survived_battles / averages.mu.battles}
value={averages.mu!.survived_battles / averages.mu!.battles}
truth={average.survival}
align="center"
>
{Math.round(
(averages.mu.survived_battles / averages.mu.battles) * 100
(averages.mu!.survived_battles / averages.mu!.battles) * 100,
)}
%
</CompareCell>
<CompareCell
value={averages.mu.xp / averages.mu.battles}
value={averages.mu!.xp / averages.mu!.battles}
truth={average.xp}
align="center"
>
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString(
locale
{Math.round(averages.mu!.xp / averages.mu!.battles).toLocaleString(
locale,
)}
</CompareCell>
<CompareCell
value={averages.mu.frags / averages.mu.battles}
value={averages.mu!.frags / averages.mu!.battles}
truth={average.kills}
align="center"
>
{(averages.mu.frags / averages.mu.battles).toFixed(2)}
{(averages.mu!.frags / averages.mu!.battles).toFixed(2)}
</CompareCell>
<CompareCell
value={averages.mu.spotted / averages.mu.battles}
value={averages.mu!.spotted / averages.mu!.battles}
truth={average.spots}
align="center"
>
{(averages.mu.spotted / averages.mu.battles).toFixed(2)}
{(averages.mu!.spotted / averages.mu!.battles).toFixed(2)}
</CompareCell>
<CompareCell
value={averages.mu.hits / averages.mu.shots}
value={averages.mu!.hits / averages.mu!.shots}
truth={average.accuracy}
align="center"
>
{Math.round((averages.mu.hits / averages.mu.shots) * 100)}%
{Math.round((averages.mu!.hits / averages.mu!.shots) * 100)}%
</CompareCell>
<CompareCell
value={averages.mu.shots / averages.mu.battles}
value={averages.mu!.shots / averages.mu!.battles}
truth={average.shots}
align="center"
>
{(averages.mu.shots / averages.mu.battles).toFixed(1)}
{(averages.mu!.shots / averages.mu!.battles).toFixed(1)}
</CompareCell>
<CompareCell
value={averages.mu.hits / averages.mu.battles}
value={averages.mu!.hits / averages.mu!.battles}
truth={average.hits}
align="center"
>
{(averages.mu.hits / averages.mu.battles).toFixed(1)}
{(averages.mu!.hits / averages.mu!.battles).toFixed(1)}
</CompareCell>
<CompareCell
value={averages.mu.damage_dealt / averages.mu.damage_received}
value={averages.mu!.damage_dealt / averages.mu!.damage_received}
truth={average.damageRatio}
align="center"
>
{(averages.mu.damage_dealt / averages.mu.damage_received).toFixed(2)}
{(averages.mu!.damage_dealt / averages.mu!.damage_received).toFixed(
2,
)}
</CompareCell>
<CompareCell
value={averages.mu.damage_received / averages.mu.battles}
value={averages.mu!.damage_received / averages.mu!.battles}
truth={average.damageReceived}
direction={CompareCellDirection.LOWER_IS_BETTER}
align="center"
>
{Math.round(
averages.mu.damage_received / averages.mu.battles
averages.mu!.damage_received / averages.mu!.battles,
).toLocaleString(locale)}
</CompareCell>
<CompareCell
value={averages.mu.capture_points / averages.mu.battles}
value={averages.mu!.capture_points / averages.mu!.battles}
truth={average.capturePoints}
align="center"
>
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)}
{(averages.mu!.capture_points / averages.mu!.battles).toFixed(2)}
</CompareCell>
</Table.Row>
);
},
(prev, next) => prev.tank.id === next.tank.id
(prev, next) => prev.tank.id === next.tank.id,
);
@@ -49,10 +49,10 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
const filteredTanks = usePromise(
() =>
filterTanks(filters, tankList).then((tanks) =>
tanks.filter(({ id }) => id in averageDefinitions.averages)
tanks.filter(({ id }) => id in averageDefinitions.averages),
),
// react-promise-suspense has awful type annotations
[filters] as any
[filters] as any,
);
const tanksMapped = useMemo(
() =>
@@ -60,7 +60,7 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
id,
...averageDefinitions.averages[id],
})),
[filters]
[filters],
);
const tanksSorted = useMemo(() => {
const { playerCountPeriod } = Performance.state;
@@ -69,83 +69,85 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
case "accuracy":
return tanksMapped.sort(
(a, b) =>
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots)
sort.direction *
(a.mu!.hits / a.mu!.shots - b.mu!.hits / b.mu!.shots),
);
case "capture_points":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.capture_points / a.mu.battles -
b.mu.capture_points / b.mu.battles)
(a.mu!.capture_points / a.mu!.battles -
b.mu!.capture_points / b.mu!.battles),
);
case "damage":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.damage_dealt / a.mu.battles -
b.mu.damage_dealt / b.mu.battles)
(a.mu!.damage_dealt / a.mu!.battles -
b.mu!.damage_dealt / b.mu!.battles),
);
case "damage_ratio":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.damage_dealt / a.mu.damage_received -
b.mu.damage_dealt / b.mu.damage_received)
(a.mu!.damage_dealt / a.mu!.damage_received -
b.mu!.damage_dealt / b.mu!.damage_received),
);
case "damage_taken":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.damage_received / a.mu.battles -
b.mu.damage_received / b.mu.battles)
(a.mu!.damage_received / a.mu!.battles -
b.mu!.damage_received / b.mu!.battles),
);
case "hits":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles)
(a.mu!.hits / a.mu!.battles - b.mu!.hits / b.mu!.battles),
);
case "kills":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles)
(a.mu!.frags / a.mu!.battles - b.mu!.frags / b.mu!.battles),
);
case "shots":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles)
(a.mu!.shots / a.mu!.battles - b.mu!.shots / b.mu!.battles),
);
case "players":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod])
(a.samples![playerCountPeriod] - b.samples![playerCountPeriod]),
);
case "spots":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles)
(a.mu!.spotted / a.mu!.battles - b.mu!.spotted / b.mu!.battles),
);
case "survival":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.survived_battles / a.mu.battles -
b.mu.survived_battles / b.mu.battles)
(a.mu!.survived_battles / a.mu!.battles -
b.mu!.survived_battles / b.mu!.battles),
);
case "winrate":
return tanksMapped.sort(
(a, b) =>
sort.direction *
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles)
(a.mu!.wins / a.mu!.battles - b.mu!.wins / b.mu!.battles),
);
case "xp":
return tanksMapped.sort(
(a, b) =>
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles)
sort.direction *
(a.mu!.xp / a.mu!.battles - b.mu!.xp / b.mu!.battles),
);
}
}, [filters, sort]);
@@ -161,11 +163,11 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
const sum = useCallback(
(slice: (tank: BlitzkitStats) => number) => {
return tanksSorted.reduce(
(acc, tank) => acc + tank.samples[playerCountPeriod] * slice(tank.mu),
0
(acc, tank) => acc + tank.samples![playerCountPeriod] * slice(tank.mu!),
0,
);
},
[tanksSorted]
[tanksSorted],
);
const battles = sum(({ battles }) => battles);
@@ -209,7 +211,7 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
<Table.Cell align="center">
{formatCompact(
locale,
ratio * averageDefinitions.samples[playerCountPeriod]
ratio * averageDefinitions.samples![playerCountPeriod],
)}
</Table.Cell>
<Table.Cell align="center">
@@ -255,12 +257,12 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
key={index}
onIntersection={() => {
setLoadedRows((state) =>
Math.min(state + 2, tanksSorted.length)
Math.min(state + 2, tanksSorted.length),
);
}}
/>
);
}
},
)}
</Table.Body>
);
@@ -1,12 +1,16 @@
import { Table } from "@radix-ui/themes";
import type { RowHeaderCellProps } from "@radix-ui/themes/src/components/table.js";
export function StickyRowHeaderCell({ style, ...props }: RowHeaderCellProps) {
interface Props extends RowHeaderCellProps {
left?: string | number;
}
export function StickyRowHeaderCell({ style, left = 0, ...props }: Props) {
return (
<Table.RowHeaderCell
style={{
position: "sticky",
left: 0,
left,
backgroundColor: "var(--color-panel)",
...style,
}}
@@ -33,7 +33,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
style,
...props
}: TankCardProps,
ref
ref,
) => {
const { unwrap, locale } = useLocale();
const provideLink = !noLink && onSelect === undefined;
@@ -46,13 +46,13 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
tabIndex={onSelect ? 0 : undefined}
size="2"
color={
tank.type === TankType.COLLECTOR
tank.type === TankType.TANK_TYPE_COLLECTOR
? "blue"
: tank.type === TankType.PREMIUM
: tank.type === TankType.TANK_TYPE_PREMIUM
? "amber"
: "gray"
}
highContrast={tank.type === TankType.RESEARCHABLE}
highContrast={tank.type === TankType.TANK_TYPE_RESEARCHABLE}
onClick={() => {
onSelect?.(tank);
TankopediaPersistent.mutate((draft) => {
@@ -79,7 +79,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
}}
>
<img
alt={unwrap(tank.name)}
alt={unwrap(tank.name!)}
src={asset(`icons/tanks/big/${tank.id}.webp`)}
className="image"
draggable={false}
@@ -95,7 +95,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
>
<Icon className="class-icon" />
<Text align="center" className="name">
{unwrap(tank.name)}
{unwrap(tank.name!)}
</Text>
</Flex>
@@ -115,5 +115,5 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
</LinkI18n>
</Text>
);
}
},
);
@@ -19,13 +19,13 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
<LinkI18n
locale={locale}
color={
tank.type === TankType.COLLECTOR
tank.type === TankType.TANK_TYPE_COLLECTOR
? "blue"
: tank.type === TankType.PREMIUM
: tank.type === TankType.TANK_TYPE_PREMIUM
? "amber"
: "gray"
}
highContrast={tank.type === TankType.RESEARCHABLE}
highContrast={tank.type === TankType.TANK_TYPE_RESEARCHABLE}
underline="hover"
wrap="nowrap"
href={`/tanks/${tank.slug}`}
@@ -55,7 +55,7 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
)}
{unwrap(tank.name)}
{unwrap(tank.name!)}
</Flex>
</LinkI18n>
</StickyRowHeaderCell>
@@ -53,11 +53,11 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case "fire.reload":
return resolveReload(gun).toFixed(2);
case "fire.standardPenetration":
return shell0?.penetration.near.toFixed(0);
return shell0?.penetration!.near.toFixed(0);
case "fire.premiumPenetration":
return shell1 ? shell1?.penetration.near.toFixed(0) : "--";
return shell1 ? shell1?.penetration!.near.toFixed(0) : "--";
case "fire.tertiaryPenetration":
return shell1 ? shell2?.penetration.near.toFixed(0) : "--";
return shell1 ? shell2?.penetration!.near.toFixed(0) : "--";
case "fire.shellVelocity":
return shell0.velocity.toFixed(0);
case "fire.shellCapacity":
@@ -66,12 +66,12 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
return shell0.range.toFixed(0);
case "fire.gunDepression":
return (
gunModelDefinition.pitch.max +
gunModelDefinition.pitch!.max +
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
).toFixed(1);
case "fire.gunElevation":
return (
-gunModelDefinition.pitch.min -
-gunModelDefinition.pitch!.min -
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
).toFixed(1);
case "maneuverability.forwardsSpeed":
@@ -113,8 +113,8 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case "survivability.volume": {
const dimensions = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box,
turretModelDefinition.bounding_box,
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
);
@@ -124,8 +124,8 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case "survivability.length": {
const bounds = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box,
turretModelDefinition.bounding_box,
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
);
@@ -85,11 +85,11 @@ for (const gameModeIdString in gameModeRoleSets) {
outerLoop: for (const id of role.consumables) {
const name =
consumableDefinitions.consumables[id].name.locales[locales.default];
consumableDefinitions.consumables[id].name!.locales[locales.default];
for (const otherConsumable of consumables.values()) {
const otherName =
consumableDefinitions.consumables[otherConsumable].name.locales[
consumableDefinitions.consumables[otherConsumable].name!.locales[
locales.default
];
@@ -102,11 +102,11 @@ for (const gameModeIdString in gameModeRoleSets) {
outerLoop: for (const id of role.provisions) {
const name =
provisionDefinitions.provisions[id].name.locales[locales.default];
provisionDefinitions.provisions[id].name!.locales[locales.default];
for (const otherProvision of provisions.values()) {
const otherName =
provisionDefinitions.provisions[otherProvision].name.locales[
provisionDefinitions.provisions[otherProvision].name!.locales[
locales.default
];
@@ -129,7 +129,7 @@ const consumablesArray = Object.values(consumableDefinitions.consumables)
.filter(
(consumable) =>
!consumable.game_mode_exclusive &&
consumable.name.locales[locales.default],
consumable.name!.locales[locales.default],
)
.map((consumable) => consumable.id);
const provisionsArray = Array.from(
@@ -137,11 +137,11 @@ const provisionsArray = Array.from(
.filter(
(provision) =>
!provision.game_mode_exclusive &&
provision.name.locales[locales.default],
provision.name!.locales[locales.default],
)
.reduce((uniqueMap, consumable) => {
if (!uniqueMap.has(consumable.name.locales[locales.default])) {
uniqueMap.set(consumable.name.locales[locales.default], consumable.id);
if (!uniqueMap.has(consumable.name!.locales[locales.default])) {
uniqueMap.set(consumable.name!.locales[locales.default], consumable.id);
}
return uniqueMap;
@@ -149,10 +149,10 @@ const provisionsArray = Array.from(
.values(),
);
const shellTypeIcons: Record<ShellType, string> = {
[ShellType.AP]: "ap",
[ShellType.APCR]: "ap_cr",
[ShellType.HE]: "he",
[ShellType.HEAT]: "hc",
[ShellType.SHELL_TYPE_AP]: "ap",
[ShellType.SHELL_TYPE_APCR]: "ap_cr",
[ShellType.SHELL_TYPE_HE]: "he",
[ShellType.SHELL_TYPE_HEAT]: "hc",
};
const GUN_TYPE_ICONS: Record<
@@ -165,9 +165,9 @@ const GUN_TYPE_ICONS: Record<
};
const TANK_TYPE_COLORS: Record<TankType, string> = {
[TankType.RESEARCHABLE]: "gray",
[TankType.PREMIUM]: "amber",
[TankType.COLLECTOR]: "blue",
[TankType.TANK_TYPE_RESEARCHABLE]: "gray",
[TankType.TANK_TYPE_PREMIUM]: "amber",
[TankType.TANK_TYPE_COLLECTOR]: "blue",
};
const GUN_TYPES = Object.keys(
@@ -566,12 +566,12 @@ function TypeFilter() {
<DropdownMenu.Trigger>
<Button color="gray" variant="surface">
<Flex gap="1">
{types.map((tankType, index) => {
{types.map((tankType) => {
return (
<ResearchedIcon
style={{
color: `var(--${TANK_TYPE_COLORS[tankType]}-${
tankType === TankType.RESEARCHABLE ? "12" : "11"
tankType === TankType.TANK_TYPE_RESEARCHABLE ? "12" : "11"
})`,
filter: "drop-shadow(0 0 var(--space-1) var(--black-a11))",
opacity: 1,
@@ -609,7 +609,7 @@ function TypeFilter() {
<ResearchedIcon
style={{
color: `var(--${TANK_TYPE_COLORS[tankType]}-${
tankType === TankType.RESEARCHABLE ? "12" : "11"
tankType === TankType.TANK_TYPE_RESEARCHABLE ? "12" : "11"
})`,
opacity: 1,
width: "1.25em",
@@ -941,7 +941,7 @@ function ConsumablesFilter() {
src={asset(`icons/consumables/${consumable}.webp`)}
/>
{unwrap(consumableDefinition.name)}
{unwrap(consumableDefinition.name!)}
</DropdownMenu.CheckboxItem>
);
})}
@@ -1035,7 +1035,7 @@ function ProvisionsFilter() {
src={asset(`icons/provisions/${provision}.webp`)}
/>
{unwrap(provisionDefinition.name)}
{unwrap(provisionDefinition.name!)}
</DropdownMenu.CheckboxItem>
);
})}
@@ -1125,9 +1125,9 @@ function GameModeAbilitiesFilter() {
<DropdownMenu.Label>
{hasBoth
? literals(strings.website.common.tank_search.active, {
name: unwrap(gameMode.name),
name: unwrap(gameMode.name!),
})
: unwrap(gameMode.name)}
: unwrap(gameMode.name!)}
</DropdownMenu.Label>
)}
@@ -1164,7 +1164,7 @@ function GameModeAbilitiesFilter() {
src={asset(`icons/consumables/${consumable}.webp`)}
/>
{unwrap(abilityDefinition.name)}
{unwrap(abilityDefinition.name!)}
</DropdownMenu.CheckboxItem>
);
}
@@ -1174,9 +1174,9 @@ function GameModeAbilitiesFilter() {
<DropdownMenu.Label>
{hasBoth
? literals(strings.website.common.tank_search.passive, {
name: unwrap(gameMode.name),
name: unwrap(gameMode.name!),
})
: unwrap(gameMode.name)}
: unwrap(gameMode.name!)}
</DropdownMenu.Label>
)}
@@ -1213,7 +1213,7 @@ function GameModeAbilitiesFilter() {
src={asset(`icons/provisions/${provision}.webp`)}
/>
{unwrap(powerDefinition.name)}
{unwrap(powerDefinition.name!)}
</DropdownMenu.CheckboxItem>
);
}
@@ -20,7 +20,7 @@ export function QuickLink({ topResult }: QuickLinkProps) {
<TextField.Slot>
<LinkI18n locale={locale} href={`/tanks/${topResult.slug}`}>
<Button variant="ghost">
{unwrap(topResult.name)} <CaretRightIcon />
{unwrap(topResult.name!)} <CaretRightIcon />
</Button>
</LinkI18n>
</TextField.Slot>
@@ -67,7 +67,7 @@ export const TankSearch = memo<TankSearchProps>(
return filterTanks(
tankFilters,
awaitedTanksDefinitionsArray,
wargaming?.id
wargaming?.id,
);
} else {
return Promise.resolve(
@@ -75,12 +75,12 @@ export const TankSearch = memo<TankSearchProps>(
.go(tankFilters.search, tankNames, {
keys: SEARCH_KEYS,
})
.map((result) => tankDefinitions.tanks[result.obj.id])
.map((result) => tankDefinitions.tanks[result.obj.id]),
);
}
},
// react-promise-suspense has awful type annotations
[tankFilters] as any
[tankFilters] as any,
);
const sorted = useMemo(() => {
if (tankFilters.search === null) {
@@ -104,19 +104,19 @@ export const TankSearch = memo<TankSearchProps>(
case "survivability.viewRange":
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range,
);
break;
case "survivability.camouflageStill":
sorted = filtered.sort(
(a, b) => a.camouflage_still - b.camouflage_still
(a, b) => a.camouflage_still - b.camouflage_still,
);
break;
case "survivability.camouflageMoving":
sorted = filtered.sort(
(a, b) => a.camouflage_moving - b.camouflage_moving
(a, b) => a.camouflage_moving - b.camouflage_moving,
);
break;
@@ -126,7 +126,7 @@ export const TankSearch = memo<TankSearchProps>(
a.camouflage_still *
a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss -
b.camouflage_still *
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss,
);
break;
@@ -140,15 +140,15 @@ export const TankSearch = memo<TankSearchProps>(
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox(
unionBoundingBox(
aTankModelDefinition.bounding_box,
aTurretModelDefinition.bounding_box
)
aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box!,
),
);
const bSize = normalizeBoundingBox(
unionBoundingBox(
bTankModelDefinition.bounding_box,
bTurretModelDefinition.bounding_box
)
bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box!,
),
);
const aVolume = aSize.x * aSize.y * aSize.z;
const bVolume = bSize.x * bSize.y * bSize.z;
@@ -167,15 +167,15 @@ export const TankSearch = memo<TankSearchProps>(
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox(
unionBoundingBox(
aTankModelDefinition.bounding_box,
aTurretModelDefinition.bounding_box
)
aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box!,
),
);
const bSize = normalizeBoundingBox(
unionBoundingBox(
bTankModelDefinition.bounding_box,
bTurretModelDefinition.bounding_box
)
bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box!,
),
);
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
@@ -189,12 +189,12 @@ export const TankSearch = memo<TankSearchProps>(
(a, b) =>
resolveDpm(
a.turrets.at(-1)!.guns.at(-1)!,
a.turrets.at(-1)!.guns.at(-1)!.shells[0]
a.turrets.at(-1)!.guns.at(-1)!.shells[0],
) -
resolveDpm(
b.turrets.at(-1)!.guns.at(-1)!,
b.turrets.at(-1)!.guns.at(-1)!.shells[0]
)
b.turrets.at(-1)!.guns.at(-1)!.shells[0],
),
);
break;
@@ -204,15 +204,15 @@ export const TankSearch = memo<TankSearchProps>(
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]
? resolveDpm(
a.turrets.at(-1)!.guns.at(-1)!,
a.turrets.at(-1)!.guns.at(-1)!.shells[1]
a.turrets.at(-1)!.guns.at(-1)!.shells[1],
)
: 0) -
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]
? resolveDpm(
b.turrets.at(-1)!.guns.at(-1)!,
b.turrets.at(-1)!.guns.at(-1)!.shells[1]
b.turrets.at(-1)!.guns.at(-1)!.shells[1],
)
: 0)
: 0),
);
break;
@@ -220,7 +220,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
resolveReload(a.turrets.at(-1)!.guns.at(-1)!) -
resolveReload(b.turrets.at(-1)!.guns.at(-1)!)
resolveReload(b.turrets.at(-1)!.guns.at(-1)!),
);
break;
@@ -228,35 +228,35 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber,
);
break;
case "fire.standardPenetration":
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near,
);
break;
case "fire.premiumPenetration":
sorted = filtered.sort(
(a, b) =>
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration!.near ??
0) -
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
0)
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration!.near ??
0),
);
break;
case "fire.tertiaryPenetration":
sorted = filtered.sort(
(a, b) =>
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration!.near ??
0) -
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
0)
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration!.near ??
0),
);
break;
@@ -264,7 +264,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage
b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage,
);
break;
@@ -272,7 +272,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage
b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage,
);
break;
@@ -280,7 +280,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity
b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity,
);
break;
@@ -288,7 +288,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].range -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].range
b.turrets.at(-1)!.guns.at(-1)!.shells[0].range,
);
break;
@@ -296,7 +296,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shell_capacity -
b.turrets.at(-1)!.guns.at(-1)!.shell_capacity
b.turrets.at(-1)!.guns.at(-1)!.shell_capacity,
);
break;
@@ -304,7 +304,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.aim_time -
b.turrets.at(-1)!.guns.at(-1)!.aim_time
b.turrets.at(-1)!.guns.at(-1)!.aim_time,
);
break;
@@ -312,7 +312,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.dispersion_base -
b.turrets.at(-1)!.guns.at(-1)!.dispersion_base
b.turrets.at(-1)!.guns.at(-1)!.dispersion_base,
);
break;
@@ -320,7 +320,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.tracks.at(-1)!.dispersion_move -
b.tracks.at(-1)!.dispersion_move
b.tracks.at(-1)!.dispersion_move,
);
break;
@@ -328,13 +328,13 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.max +
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch!.max +
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
0) -
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.max -
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch!.max -
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
0)
0),
);
break;
@@ -342,31 +342,31 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.min +
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch!.min +
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
0) -
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.min -
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch!.min -
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
0)
0),
);
break;
case "maneuverability.forwardsSpeed":
sorted = filtered.sort(
(a, b) => a.speed_forwards - b.speed_forwards
(a, b) => a.speed_forwards - b.speed_forwards,
);
break;
case "maneuverability.backwardsSpeed":
sorted = filtered.sort(
(a, b) => a.speed_backwards - b.speed_backwards
(a, b) => a.speed_backwards - b.speed_backwards,
);
break;
case "maneuverability.power":
sorted = filtered.sort(
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power,
);
break;
@@ -384,7 +384,7 @@ export const TankSearch = memo<TankSearchProps>(
b.engines.at(-1)!.weight +
b.tracks.at(-1)!.weight +
b.turrets.at(-1)!.weight +
b.turrets.at(-1)!.guns.at(-1)!.weight)
b.turrets.at(-1)!.guns.at(-1)!.weight),
);
break;
@@ -400,7 +400,7 @@ export const TankSearch = memo<TankSearchProps>(
b.engines.at(-1)!.weight +
b.tracks.at(-1)!.weight +
b.turrets.at(-1)!.weight +
b.turrets.at(-1)!.guns.at(-1)!.weight)
b.turrets.at(-1)!.guns.at(-1)!.weight),
);
break;
@@ -408,7 +408,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort(
(a, b) =>
a.tracks.at(-1)!.traverse_speed -
b.tracks.at(-1)!.traverse_speed
b.tracks.at(-1)!.traverse_speed,
);
break;
}
@@ -520,12 +520,12 @@ export const TankSearch = memo<TankSearchProps>(
key={index}
onIntersection={() => {
setLoadedCards((state) =>
Math.min(state + 2, sorted.length)
Math.min(state + 2, sorted.length),
);
}}
/>
);
}
},
)}
</TankCardWrapper>
)}
@@ -538,15 +538,15 @@ export const TankSearch = memo<TankSearchProps>(
<TankCardWrapper>
{times(
Math.round(
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random()
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random(),
),
(index) => (
<SkeletonTankCard key={index} />
)
),
)}
</TankCardWrapper>
)}
</Flex>
);
}
},
);
@@ -24,7 +24,7 @@ export function Crew({ stats }: StatsAcceptorProps) {
provisionDefinitions.provisions[provision].crew
? total + provisionDefinitions.provisions[provision].crew! / 100
: total,
0
0,
) + (hasImprovedVentilation ? 0.08 : 0);
const commanderMastery = 1 + provisionCrewBonus;
@@ -71,7 +71,7 @@ export function Crew({ stats }: StatsAcceptorProps) {
decimals={0}
noRanking
value={() =>
(member.type === CrewType.COMMANDER
(member.type === CrewType.CREW_TYPE_COMMANDER
? commanderMastery
: commanderMastery * 1.1) * 100
}
@@ -93,7 +93,7 @@ export function Crew({ stats }: StatsAcceptorProps) {
? strings.website.tools.tankopedia.crew[
CREW_MEMBER_NAMES[sub]
]
: sub
: sub,
)
.join(", ")}
</Flex>
@@ -38,7 +38,7 @@ export function Firepower({
const selected = thisShell.id === shell.id;
return (
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
<Tooltip content={unwrap(thisShell.name!)} key={thisShell.id}>
<IconButton
color={selected ? undefined : "gray"}
variant={selected ? "solid" : "soft"}
@@ -59,7 +59,7 @@ export function Firepower({
}}
>
<img
alt={unwrap(thisShell.name)}
alt={unwrap(thisShell.name!)}
width={16}
height={16}
src={asset(`icons/shells/${thisShell.icon}.webp`)}
@@ -119,7 +119,7 @@ export function Firepower({
name={literals(
strings.website.tools.tankopedia.characteristics.values
.shell_index,
{ index: index + 1 }
{ index: index + 1 },
)}
decimals={2}
deltaType="lowerIsBetter"
@@ -191,7 +191,7 @@ export function Firepower({
name={literals(
strings.website.tools.tankopedia.characteristics.values
.dispersion_at_distance,
{ distance: 100 }
{ distance: 100 },
)}
/>
<InfoWithDelta
@@ -249,7 +249,7 @@ export function Firepower({
/>
<InfoWithDelta value="gunDepression" stats={stats} decimals={1} indent />
<InfoWithDelta stats={stats} decimals={1} indent value="gunElevation" />
{gunModelDefinition.pitch.front && (
{gunModelDefinition.pitch!.front && (
<>
<InfoWithDelta
stats={stats}
@@ -265,7 +265,7 @@ export function Firepower({
/>
</>
)}
{gunModelDefinition.pitch.back && (
{gunModelDefinition.pitch!.back && (
<>
<InfoWithDelta
stats={stats}
@@ -1,13 +1,13 @@
import type { QuicklimeEvent } from 'quicklime';
import { useEffect, useRef } from 'react';
import type { Group } from 'three';
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
import type { QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import type { Group } from "three";
import { awaitableModelDefinitions } from "../../../../../../../../core/awaitables/modelDefinitions";
import {
modelTransformEvent,
type ModelTransformEventData,
} from '../../../../../../../../core/blitzkit/modelTransform';
import { Duel } from '../../../../../../../../stores/duel';
import { ModelChunk } from './ModelChunk';
} from "../../../../../../../../core/blitzkit/modelTransform";
import { Duel } from "../../../../../../../../stores/duel";
import { ModelChunk } from "./ModelChunk";
const modelDefinitions = await awaitableModelDefinitions;
@@ -39,8 +39,8 @@ export function CanvasContent() {
}, []);
return (
<>
<group ref={hullWrapper} position={[0, 0, turretModel.gun_origin.z]}>
<group position={[0, 0, -turretModel.gun_origin.z]}>
<group ref={hullWrapper} position={[0, 0, turretModel.gun_origin!.z]}>
<group position={[0, 0, -turretModel.gun_origin!.z]}>
<ModelChunk only="body" />
</group>
</group>
@@ -1,13 +1,13 @@
import { mauveDark } from '@radix-ui/colors';
import { LineBasicMaterial, MeshBasicMaterial } from 'three';
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
import { jsxTree } from '../../../../../../../../core/blitzkit/jsxTree';
import { useModel } from '../../../../../../../../hooks/useModel';
import { Duel } from '../../../../../../../../stores/duel';
import { mauveDark } from "@radix-ui/colors";
import { LineBasicMaterial, MeshBasicMaterial } from "three";
import { awaitableModelDefinitions } from "../../../../../../../../core/awaitables/modelDefinitions";
import { jsxTree } from "../../../../../../../../core/blitzkit/jsxTree";
import { useModel } from "../../../../../../../../hooks/useModel";
import { Duel } from "../../../../../../../../stores/duel";
const modelDefinitions = await awaitableModelDefinitions;
const surfaceMaterial: Record<Props['only'], MeshBasicMaterial> = {
const surfaceMaterial: Record<Props["only"], MeshBasicMaterial> = {
gun: new MeshBasicMaterial({
color: mauveDark.mauve8,
toneMapped: false,
@@ -23,7 +23,7 @@ const surfaceMaterial: Record<Props['only'], MeshBasicMaterial> = {
opacity: 0.5,
}),
};
const outlineMaterial: Record<Props['only'], LineBasicMaterial> = {
const outlineMaterial: Record<Props["only"], LineBasicMaterial> = {
gun: new LineBasicMaterial({
color: mauveDark.mauve11,
toneMapped: false,
@@ -41,7 +41,7 @@ const outlineMaterial: Record<Props['only'], LineBasicMaterial> = {
};
interface Props {
only: 'gun' | 'turret' | 'body';
only: "gun" | "turret" | "body";
}
export function ModelChunk({ only }: Props) {
@@ -53,8 +53,8 @@ export function ModelChunk({ only }: Props) {
const turretModel = tankModel.turrets[turret.id];
const gunModel = turretModel.guns[gun.id];
const originZ = tankModel.turret_origin.z + turretModel.gun_origin.z;
const originY = tankModel.turret_origin.y + turretModel.gun_origin.y;
const originZ = tankModel.turret_origin!.z + turretModel.gun_origin!.z;
const originY = tankModel.turret_origin!.y + turretModel.gun_origin!.y;
const { gltf } = useModel(tank.id);
const nodes = Object.values(gltf.nodes);
@@ -64,25 +64,25 @@ export function ModelChunk({ only }: Props) {
{nodes.map((node) => {
const isCurrentMantlet =
node.name ===
`gun_${gunModel.model_id.toString().padStart(2, '0')}_mask`;
`gun_${gunModel.model_id.toString().padStart(2, "0")}_mask`;
const isCurrentGun =
node.name === `gun_${gunModel.model_id.toString().padStart(2, '0')}`;
node.name === `gun_${gunModel.model_id.toString().padStart(2, "0")}`;
const isGun = isCurrentGun || isCurrentMantlet;
const isCurrentTurret =
node.name ===
`turret_${turretModel.model_id.toString().padStart(2, '0')}`;
`turret_${turretModel.model_id.toString().padStart(2, "0")}`;
const isTurret = isCurrentTurret;
const isHull = node.name === 'hull';
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
const isHull = node.name === "hull";
const isWheel = node.name.startsWith("chassis_wheel_");
const isTrack = node.name.startsWith("chassis_track_");
const isBody = isHull || isWheel || isTrack;
if (
(only === 'gun' && !isGun) ||
(only === 'turret' && !isTurret) ||
(only === 'body' && !isBody)
(only === "gun" && !isGun) ||
(only === "turret" && !isTurret) ||
(only === "body" && !isBody)
) {
return null;
}
@@ -51,21 +51,21 @@ export function GunFlexibilityVisualizer({
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),
);
const [maxPitch, setMaxPitch] = useState(
-degToRad(gunModel.pitch.front?.min ?? gunModel.pitch.min)
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min),
);
const container = useRef<HTMLDivElement>(null);
const highlighter = useRef<HTMLDivElement>(null);
const pointer = useRef<HTMLDivElement>(null);
const p = gunModel.pitch;
const p = gunModel.pitch!;
const y = turretModel.yaw;
const f = p.front;
const b = p.back;
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION;
const t = gunModel.pitch!.transition ?? DEFAULT_PITCH_TRANSITION;
let d = "";
@@ -223,12 +223,12 @@ export function GunFlexibilityVisualizer({
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const min = applyPitchYawLimits(
@@ -237,7 +237,7 @@ export function GunFlexibilityVisualizer({
p,
y,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
const max = applyPitchYawLimits(
Math.PI,
@@ -245,7 +245,7 @@ export function GunFlexibilityVisualizer({
p,
y,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
yaw = min[1];
@@ -59,11 +59,12 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
const delta = useDelta(uhWhatDoICallThisVariable);
const protagonistTank = Duel.use((state) => state.protagonist.tank);
const shellIndex = Duel.use((state) =>
state.protagonist.gun.shells.indexOf(state.protagonist.shell)
state.protagonist.gun.shells.indexOf(state.protagonist.shell),
);
const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix
(state) => state.protagonist.equipmentMatrix,
);
const equalize = Duel.use((state) => state.equalize);
const others = useMemo(() => {
const defaultSkills = createDefaultSkills(skillDefinitions);
@@ -75,7 +76,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
tank.class === protagonistTank.class) ||
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
tank.tier === protagonistTank.tier) ||
relativeAgainst === TankopediaRelativeAgainst.All
relativeAgainst === TankopediaRelativeAgainst.All,
)
.map((tank) => {
const member = tankToDuelMember(tank, provisionDefinitions);
@@ -105,12 +106,13 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
track: member.track,
turret: member.turret,
assaultDistance: member.assaultDistance,
equalize,
},
{
equipmentDefinitions,
provisionDefinitions,
tankModelDefinition: modelDefinitions.models[tank.id],
}
},
);
})
.filter((tank) => {
@@ -123,7 +125,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
return othersValue !== undefined;
}) as TankCharacteristics[];
}, [relativeAgainst, shellIndex, equipmentMatrix]);
}, [relativeAgainst, shellIndex, equipmentMatrix, equalize]);
const betterTanks = others.filter((tank) => {
const othersValue =
typeof props.value === "function"
@@ -183,5 +185,5 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
(a, b) =>
(typeof a.value === "function" ? a.value(a.stats) : a.stats[a.value]) ===
(typeof b.value === "function" ? b.value(b.stats) : b.stats[b.value])
(typeof b.value === "function" ? b.value(b.stats) : b.stats[b.value]),
);
@@ -15,7 +15,7 @@ export function ShellSwitcher() {
const selected = thisShell.id === shell.id;
return (
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
<Tooltip content={unwrap(thisShell.name!)} key={thisShell.id}>
<IconButton
size="3"
color={selected ? undefined : "gray"}
@@ -36,7 +36,7 @@ export function ShellSwitcher() {
}}
>
<img
alt={unwrap(thisShell.name)}
alt={unwrap(thisShell.name!)}
style={{ width: "1.125rem", height: "1.125rem" }}
src={asset(`icons/shells/${thisShell.icon}.webp`)}
/>
@@ -26,13 +26,14 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
const consumables = Duel.use((state) => state.protagonist.consumables);
const camouflage = Duel.use((state) => state.protagonist.camouflage);
const assaultDistance = Duel.use(
(state) => state.protagonist.assaultDistance
(state) => state.protagonist.assaultDistance,
);
const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix
(state) => state.protagonist.equipmentMatrix,
);
const equalize = Duel.use((state) => state.equalize);
const { tank, turret, gun, engine, track, shell } = Duel.use(
(state) => state.protagonist
(state) => state.protagonist,
);
const stockEngine = tank.engines[0];
const stockTrack = tank.tracks[0];
@@ -62,12 +63,13 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
applyDynamicArmor: false,
applySpallLiner: false,
assaultDistance,
equalize,
},
{
tankModelDefinition,
equipmentDefinitions: equipmentDefinitions,
provisionDefinitions: provisionDefinitions,
}
},
);
const hasImprovedVerticalStabilizer = useEquipment(122);
@@ -77,10 +79,10 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw,
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
@@ -1,5 +1,5 @@
import { type FlexProps, Flex } from '@radix-ui/themes';
import type { ReactNode } from 'react';
import { type FlexProps, Flex } from "@radix-ui/themes";
import type { ReactNode } from "react";
type ConfigurationChildWrapperProps = FlexProps & {
children: ReactNode;
@@ -7,13 +7,15 @@ type ConfigurationChildWrapperProps = FlexProps & {
export function ConfigurationChildWrapper({
children,
style,
...props
}: ConfigurationChildWrapperProps) {
return (
<Flex
gap="2"
direction="column"
align={{ initial: 'center', sm: 'start' }}
align={{ initial: "center", sm: "start" }}
style={{ width: "100%", ...style }}
{...props}
>
{children}
@@ -0,0 +1,106 @@
import { asset } from "@blitzkit/core";
import { InfoCircledIcon } from "@radix-ui/react-icons";
import {
Badge,
Box,
Flex,
Heading,
Link,
Popover,
Switch,
Text,
} from "@radix-ui/themes";
import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from "../../../../stores/duel";
import { Tankopedia } from "../../../../stores/tankopedia";
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
export function Equalizer() {
const { strings } = useLocale();
const equalize = Duel.use((state) => state.equalize);
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>
</Heading>
<Box
mt="2"
onClick={() => {
Duel.mutate((draft) => {
draft.equalize = !draft.equalize;
});
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}}
width="100%"
style={{
cursor: "pointer",
borderRadius: "var(--radius-3)",
backgroundImage: `url(${asset("icons/game_mode_banners/45.webp")})`,
backgroundSize: "cover",
backgroundPosition: "center",
overflow: "hidden",
}}
>
<Flex
align="center"
width="100%"
justify="between"
p="4"
style={{
background:
"linear-gradient(90deg, var(--black-a3), var(--gray-1))",
}}
>
<Text
style={{
textShadow: "0 0 var(--space-1) var(--black-a12)",
}}
>
{strings.website.tools.tankopedia.configuration.equalizer.equalize}
</Text>
<Switch checked={equalize} />
</Flex>
</Box>
<Popover.Root>
<Popover.Trigger>
<Text style={{ cursor: "pointer" }} size="2" color="gray">
<Flex align="center" gap="2">
Brought to you by{" "}
<img
src="/assets/images/third-party/wotb-news.png"
style={{
width: "1rem",
height: "1rem",
objectFit: "cover",
borderRadius: "50%",
}}
/>{" "}
WoTB News <InfoCircledIcon />
</Flex>
</Text>
</Popover.Trigger>
<Popover.Content>
<Text>
This feature was brought to you early, ahead of the release of the
game mode, thanks to{" "}
<Link href="https://discord.gg/WHdER7ZPAD" target="_blank">
WoTB News
</Link>
.
</Text>
</Popover.Content>
</Popover.Root>
</ConfigurationChildWrapper>
);
}
@@ -43,11 +43,11 @@ interface ModuleWithUnlocks {
}
const moduleTypeTypeIconName: Record<ModuleType, string> = {
[ModuleType.VEHICLE]: "",
[ModuleType.ENGINE]: "engine",
[ModuleType.TRACKS]: "chassis",
[ModuleType.TURRET]: "turret",
[ModuleType.GUN]: "gun",
[ModuleType.MODULE_TYPE_VEHICLE]: "",
[ModuleType.MODULE_TYPE_ENGINE]: "engine",
[ModuleType.MODULE_TYPE_TRACKS]: "chassis",
[ModuleType.MODULE_TYPE_TURRET]: "turret",
[ModuleType.MODULE_TYPE_GUN]: "gun",
};
function ModuleButton({
@@ -63,7 +63,7 @@ function ModuleButton({
top: boolean;
onClick: () => void;
}) {
const isTank = unlock.type === ModuleType.VEHICLE;
const isTank = unlock.type === ModuleType.MODULE_TYPE_VEHICLE;
const { locale } = useLocale();
const button = (
@@ -91,7 +91,7 @@ function ModuleButton({
{TIER_ROMAN_NUMERALS[tier]}
</Text>
{unlock.cost.value > 0 && (
{unlock.cost!.value > 0 && (
<Flex
style={{
position: "absolute",
@@ -103,12 +103,12 @@ function ModuleButton({
gap={isTank ? "1" : undefined}
>
<Text size="1" color="gray">
{formatCompact(locale, unlock.cost.value)}
{formatCompact(locale, unlock.cost!.value)}
</Text>
<img
alt={unlock.cost.type}
src={asset(`icons/currencies/${unlock.cost.type}.webp`)}
alt={unlock.cost!.type}
src={asset(`icons/currencies/${unlock.cost!.type}.webp`)}
style={{
width: 12,
height: 12,
@@ -171,16 +171,16 @@ export function Modules() {
function setByUnlock(unlock: Unlock) {
Duel.mutate((draft) => {
if (unlock.type === ModuleType.TURRET) {
if (unlock.type === ModuleType.MODULE_TYPE_TURRET) {
draft.protagonist.turret = draft.protagonist.tank.turrets.find(
(turret) => turret.id === unlock.id
(turret) => turret.id === unlock.id,
)!;
// Check if current gun exists in new turret
const gunInNewTurret = draft.protagonist.turret.guns.find(
(gun) => gun.id === draft.protagonist.gun.id
(gun) => gun.id === draft.protagonist.gun.id,
);
if (gunInNewTurret) {
// Gun exists - get it FROM this turret
draft.protagonist.gun = gunInNewTurret;
@@ -190,9 +190,9 @@ export function Modules() {
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
draft.protagonist.shell = draft.protagonist.gun.shells[0];
}
} else if (unlock.type === ModuleType.GUN) {
} else if (unlock.type === ModuleType.MODULE_TYPE_GUN) {
const gunInTurret = draft.protagonist.turret.guns.find(
(gun) => gun.id === unlock.id
(gun) => gun.id === unlock.id,
);
if (gunInTurret) {
draft.protagonist.gun = gunInTurret;
@@ -200,23 +200,23 @@ export function Modules() {
} else {
// TODO: warn somehow?
const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
turret.guns.some((gun) => gun.id === unlock.id)
turret.guns.some((gun) => gun.id === unlock.id),
)!;
const gunInSuitableTurret = suitableTurret.guns.find(
(gun) => gun.id === unlock.id
(gun) => gun.id === unlock.id,
)!;
draft.protagonist.turret = suitableTurret;
draft.protagonist.gun = gunInSuitableTurret;
draft.protagonist.shell = gunInSuitableTurret.shells[0];
}
} else if (unlock.type === ModuleType.ENGINE) {
} else if (unlock.type === ModuleType.MODULE_TYPE_ENGINE) {
draft.protagonist.engine = draft.protagonist.tank.engines.find(
(engine) => engine.id === unlock.id
(engine) => engine.id === unlock.id,
)!;
} else if (unlock.type === ModuleType.TRACKS) {
} else if (unlock.type === ModuleType.MODULE_TYPE_TRACKS) {
draft.protagonist.track = draft.protagonist.tank.tracks.find(
(track) => track.id === unlock.id
(track) => track.id === unlock.id,
)!;
}
});
@@ -231,19 +231,19 @@ export function Modules() {
const central = !first && !last;
let module: ModuleDefinition | TankDefinition | undefined = undefined;
if (unlock.type === ModuleType.TRACKS) {
if (unlock.type === ModuleType.MODULE_TYPE_TRACKS) {
module = tank.tracks.find((track) => track.id === unlock.id)!;
} else if (unlock.type === ModuleType.ENGINE) {
} else if (unlock.type === ModuleType.MODULE_TYPE_ENGINE) {
module = tank.engines.find((engine) => engine.id === unlock.id)!;
} else if (unlock.type === ModuleType.TURRET) {
} else if (unlock.type === ModuleType.MODULE_TYPE_TURRET) {
module = tank.turrets.find((turret) => turret.id === unlock.id)!;
} else if (unlock.type === ModuleType.GUN) {
} else if (unlock.type === ModuleType.MODULE_TYPE_GUN) {
module = tank.turrets
.find((turret) =>
turret.guns.some((gun) => gun.id === unlock.id)
turret.guns.some((gun) => gun.id === unlock.id),
)!
.guns.find((gun) => gun.id === unlock.id)!;
} else if (unlock.type === ModuleType.VEHICLE) {
} else if (unlock.type === ModuleType.MODULE_TYPE_VEHICLE) {
module = tankDefinitions.tanks[unlock.id];
}
@@ -257,7 +257,7 @@ export function Modules() {
gap="2"
position="relative"
>
{unlock.cost.value > 0 && (
{unlock.cost!.value > 0 && (
<>
{unlocks.length > 1 && (
<div
@@ -302,24 +302,24 @@ export function Modules() {
unlock={unlock}
tier={extractModule(module).tier}
selected={
(unlock.type === ModuleType.TURRET
(unlock.type === ModuleType.MODULE_TYPE_TURRET
? turret.id
: unlock.type === ModuleType.GUN
: unlock.type === ModuleType.MODULE_TYPE_GUN
? gun.id
: unlock.type === ModuleType.ENGINE
: unlock.type === ModuleType.MODULE_TYPE_ENGINE
? engine.id
: unlock.type === ModuleType.TRACKS
: unlock.type === ModuleType.MODULE_TYPE_TRACKS
? track.id
: -1) === unlock.id
}
top={
unlock.type === ModuleType.TURRET
unlock.type === ModuleType.MODULE_TYPE_TURRET
? extractModule(module).id == topTurret.id
: unlock.type === ModuleType.GUN
: unlock.type === ModuleType.MODULE_TYPE_GUN
? extractModule(module).id === topGun.id
: unlock.type === ModuleType.ENGINE
: unlock.type === ModuleType.MODULE_TYPE_ENGINE
? extractModule(module).id === topEngine.id
: unlock.type === ModuleType.TRACKS
: unlock.type === ModuleType.MODULE_TYPE_TRACKS
? extractModule(module).id === topTrack.id
: false
}
@@ -342,7 +342,7 @@ export function Modules() {
{(extractModule(module) as ModuleWithUnlocks).unlocks &&
tree(
type,
(extractModule(module) as ModuleWithUnlocks).unlocks!
(extractModule(module) as ModuleWithUnlocks).unlocks!,
)}
</Flex>
);
@@ -397,32 +397,32 @@ export function Modules() {
</Flex>
<Flex gap="2" wrap="wrap" gapY="6">
{tree(ModuleType.ENGINE, [
{tree(ModuleType.MODULE_TYPE_ENGINE, [
{
cost: { type: "xp", value: 0 },
id: engine0.id,
type: ModuleType.ENGINE,
type: ModuleType.MODULE_TYPE_ENGINE,
},
])}
{tree(ModuleType.TRACKS, [
{tree(ModuleType.MODULE_TYPE_TRACKS, [
{
cost: { type: "xp", value: 0 },
id: track0.id,
type: ModuleType.TRACKS,
type: ModuleType.MODULE_TYPE_TRACKS,
},
])}
{tree(ModuleType.TURRET, [
{tree(ModuleType.MODULE_TYPE_TURRET, [
{
cost: { type: "xp", value: 0 },
id: turret0.id,
type: ModuleType.TURRET,
type: ModuleType.MODULE_TYPE_TURRET,
},
])}
{tree(ModuleType.GUN, [
{tree(ModuleType.MODULE_TYPE_GUN, [
{
cost: { type: "xp", value: 0 },
id: gun0.id,
type: ModuleType.GUN,
type: ModuleType.MODULE_TYPE_GUN,
},
])}
</Flex>
@@ -1,14 +1,15 @@
import { Flex } from '@radix-ui/themes';
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
import { Characteristics } from './components/Characteristics';
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
import { StatSearch } from './components/Characteristics/components/StatSearch';
import { Consumables } from './components/Consumables';
import { Equipment } from './components/Equipment';
import { Miscellaneous } from './components/Miscellaneous';
import { Modules } from './components/Modules';
import { Provisions } from './components/Provisions';
import { Skills } from './components/Skills';
import { Flex } from "@radix-ui/themes";
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
import { Characteristics } from "./components/Characteristics";
import { CompareOptions } from "./components/Characteristics/components/CompareOptions";
import { StatSearch } from "./components/Characteristics/components/StatSearch";
import { Consumables } from "./components/Consumables";
import { Equalizer } from "./components/Equalizer";
import { Equipment } from "./components/Equipment";
import { Miscellaneous } from "./components/Miscellaneous";
import { Modules } from "./components/Modules";
import { Provisions } from "./components/Provisions";
import { Skills } from "./components/Skills";
export function CharacteristicsSection({
skeleton,
@@ -16,34 +17,35 @@ export function CharacteristicsSection({
return (
<Flex direction="column" gap="3">
<Flex
display={{ initial: 'none', sm: 'flex' }}
direction={{ initial: 'column', md: 'row' }}
display={{ initial: "none", sm: "flex" }}
direction={{ initial: "column", md: "row" }}
align="center"
justify="center"
mb="8"
gap={{ initial: '4', md: '8' }}
gap={{ initial: "4", md: "8" }}
>
<CompareOptions />
<StatSearch />
</Flex>
<Flex mt="4" px="4" style={{ boxSizing: 'border-box' }} gap="6">
<Flex mt="4" px="4" style={{ boxSizing: "border-box" }} gap="6">
<Flex
flexGrow="1"
gap="9"
justify="center"
align={{ initial: 'center', sm: 'start' }}
direction={{ initial: 'column', sm: 'row' }}
align={{ initial: "center", sm: "start" }}
direction={{ initial: "column", sm: "row" }}
>
<Flex
direction="column"
gap="6"
maxWidth="19rem"
align={{ initial: 'center', sm: 'start' }}
align={{ initial: "center", sm: "start" }}
>
<Modules />
<Equipment />
<Provisions />
<Equalizer />
<Consumables />
<Skills />
<Miscellaneous />
@@ -52,11 +54,11 @@ export function CharacteristicsSection({
<Flex
direction="column"
gap="6"
align={{ initial: 'stretch', sm: 'center' }}
width={{ initial: '100%', sm: 'auto' }}
align={{ initial: "stretch", sm: "center" }}
width={{ initial: "100%", sm: "auto" }}
>
<Flex
display={{ initial: 'flex', sm: 'none' }}
display={{ initial: "flex", sm: "none" }}
direction="column"
align="center"
justify="center"
@@ -41,7 +41,7 @@ export function GameModeSection() {
style={{
borderRadius: "var(--radius-3)",
background: `url(${asset(
`icons/game_mode_banners/${id}.webp`
`icons/game_mode_banners/${id}.webp`,
)})`,
backgroundSize: "cover",
backgroundPosition: "center",
@@ -51,7 +51,7 @@ export function GameModeSection() {
flexGrow="1"
style={{
background: `url(${asset(
`icons/game_mode_banners/${id}.webp`
`icons/game_mode_banners/${id}.webp`,
)})`,
backgroundSize: "cover",
backgroundPosition: "center",
@@ -71,7 +71,7 @@ export function GameModeSection() {
justify="center"
>
<Text size="4" weight="bold">
{unwrap(gameMode.name)}
{unwrap(gameMode.name!)}
</Text>
<Flex direction="column">
@@ -88,9 +88,9 @@ export function GameModeSection() {
objectFit: "contain",
}}
src={asset(`icons/provisions/${id}.webp`)}
alt={unwrap(provisions.name)}
alt={unwrap(provisions.name!)}
/>
{unwrap(provisions.name)}
{unwrap(provisions.name!)}
</Flex>
</Text>
);
@@ -109,9 +109,9 @@ export function GameModeSection() {
objectFit: "contain",
}}
src={asset(`icons/consumables/${id}.webp`)}
alt={unwrap(consumable.name)}
alt={unwrap(consumable.name!)}
/>
{unwrap(consumable.name)}
{unwrap(consumable.name!)}
</Flex>
</Text>
);
@@ -68,29 +68,29 @@ export function CustomShellButton() {
const modified = { ...customShell, type };
switch (type) {
case ShellType.AP: {
case ShellType.SHELL_TYPE_AP: {
modified.normalization = 5;
modified.ricochet = 70;
modified.explosion_radius = undefined;
break;
}
case ShellType.APCR: {
case ShellType.SHELL_TYPE_APCR: {
modified.normalization = 2;
modified.ricochet = 70;
modified.explosion_radius = undefined;
break;
}
case ShellType.HEAT: {
case ShellType.SHELL_TYPE_HEAT: {
modified.normalization = undefined;
modified.ricochet = undefined;
modified.explosion_radius = undefined;
}
case ShellType.HE: {
case ShellType.SHELL_TYPE_HE: {
modified.normalization = undefined;
modified.ricochet = undefined;
modified.explosion_radius =
gun.shells.find(
(shell) => shell.type === ShellType.HE
(shell) => shell.type === ShellType.SHELL_TYPE_HE,
)?.explosion_radius ?? 1;
break;
}
@@ -104,16 +104,16 @@ export function CustomShellButton() {
<Select.Trigger variant="classic" />
<Select.Content>
<Select.Item value={`${ShellType.AP}`}>
<Select.Item value={`${ShellType.SHELL_TYPE_AP}`}>
{strings.common.shells.ap}
</Select.Item>
<Select.Item value={`${ShellType.APCR}`}>
<Select.Item value={`${ShellType.SHELL_TYPE_APCR}`}>
{strings.common.shells.ap_cr}
</Select.Item>
<Select.Item value={`${ShellType.HEAT}`}>
<Select.Item value={`${ShellType.SHELL_TYPE_HEAT}`}>
{strings.common.shells.hc}
</Select.Item>
<Select.Item value={`${ShellType.HE}`}>
<Select.Item value={`${ShellType.SHELL_TYPE_HE}`}>
{strings.common.shells.he}
</Select.Item>
</Select.Content>
@@ -166,11 +166,11 @@ export function CustomShellButton() {
<TextField.Root
variant="classic"
style={{ maxWidth: "6rem" }}
defaultValue={customShell.penetration.near}
defaultValue={customShell.penetration!.near}
type="number"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
Tankopedia.mutate((draft) => {
draft.customShell!.penetration.near =
draft.customShell!.penetration!.near =
event.target.valueAsNumber;
});
}}
@@ -182,7 +182,7 @@ export function CustomShellButton() {
(event.target as HTMLInputElement).blur();
} else if (event.key === "Escape") {
(event.target as HTMLInputElement).valueAsNumber =
customShell.penetration.near;
customShell.penetration!.near;
(event.target as HTMLInputElement).blur();
}
}}
@@ -193,8 +193,8 @@ export function CustomShellButton() {
</TextField.Root>
</Flex>
{(customShell.type === ShellType.AP ||
customShell.type === ShellType.APCR) && (
{(customShell.type === ShellType.SHELL_TYPE_AP ||
customShell.type === ShellType.SHELL_TYPE_APCR) && (
<>
<Flex align="center" justify="between" gap="4" py="1">
<Text>
@@ -264,7 +264,7 @@ export function CustomShellButton() {
</>
)}
{customShell.type === ShellType.HE && (
{customShell.type === ShellType.SHELL_TYPE_HE && (
<Flex align="center" justify="between" gap="4">
<Text>Explosion radius</Text>
<TextField.Root
@@ -27,27 +27,29 @@ export function QuickInputs() {
const hasDownImprovedVerticalStabilizer = useEquipment(124);
useEffect(() => {
function handleTransformEvent(
event: QuicklimeEvent<ModelTransformEventData>
) {
function syncInputValues(data: ModelTransformEventData) {
if (!pitchInput.current || !yawInput.current) return;
pitchInput.current.value = (
radToDeg(event.data.pitch) +
radToDeg(data.pitch) +
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
).toFixed(1);
yawInput.current.value = radToDeg(data.yaw).toFixed(1);
}
if (event.data.yaw === undefined) return;
yawInput.current.value = radToDeg(event.data.yaw).toFixed(1);
function handleTransformEvent(
event: QuicklimeEvent<ModelTransformEventData>,
) {
syncInputValues(event.data);
}
modelTransformEvent.on(handleTransformEvent);
syncInputValues(modelTransformEvent.last!);
return () => {
modelTransformEvent.off(handleTransformEvent);
};
}, []);
}, [initialGunPitch]);
return (
<Flex
@@ -69,13 +71,13 @@ export function QuickInputs() {
size="1"
radius="full"
style={{ flex: 1, textAlign: "right" }}
defaultValue="0.0"
defaultValue={radToDeg(modelTransformEvent.last!.yaw).toFixed(1)}
onBlur={() => {
const value = Number(yawInput.current!.value);
if (isNaN(value)) {
yawInput.current!.value = radToDeg(
modelTransformEvent.last!.yaw
modelTransformEvent.last!.yaw,
).toFixed(1);
return;
}
@@ -83,15 +85,15 @@ export function QuickInputs() {
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
degToRad(value),
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
yawInput.current!.value = radToDeg(
modelTransformEvent.last!.yaw
modelTransformEvent.last!.yaw,
).toFixed(1);
}}
onKeyDown={(event) => {
@@ -113,7 +115,9 @@ export function QuickInputs() {
size="1"
radius="full"
style={{ flex: 1, textAlign: "right" }}
defaultValue="0.0"
defaultValue={(
radToDeg(modelTransformEvent.last!.pitch) + initialGunPitch
).toFixed(1)}
onBlur={() => {
const value = Number(pitchInput.current!.value);
if (isNaN(value)) {
@@ -125,10 +129,10 @@ export function QuickInputs() {
const [pitch, yaw] = applyPitchYawLimits(
degToRad(value + initialGunPitch),
modelTransformEvent.last!.yaw,
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
pitchInput.current!.value = (
@@ -38,6 +38,7 @@ import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent
import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/constants";
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
import type { ThicknessRange } from "../../../../Armor/components/StaticArmor";
import { EqualIcon } from "../../../../EqualIcon";
import { ModuleButton } from "../../../../ModuleButtons/ModuleButton";
import { ScreenshotButton } from "../../../../ScreenshotButton";
import { SmallTankIcon } from "../../../../SmallTankIcon";
@@ -54,12 +55,12 @@ type OptionsProps = MaybeSkeletonComponentProps & {
export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const hasCustomShell = Tankopedia.use(
(state) => state.customShell !== undefined
(state) => state.customShell !== undefined,
);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const isFullScreen = useFullScreen();
const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting
(state) => state.advancedHighlighting,
);
const fullScreenAvailable = useFullscreenAvailability(true);
const protagonistTank = Duel.use((state) => state.protagonist.tank);
@@ -75,6 +76,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
const equalize = Duel.use((state) => state.equalize);
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
return (
<>
@@ -102,8 +105,10 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
value: (
resolvePenetrationCoefficient(
hasCalibratedShells,
antagonistShell.type
) * antagonistShell.penetration.near
equalize,
antagonistShell.type,
equalizer,
) * antagonistShell.penetration!.near
).toFixed(0),
})}
</Text>
@@ -203,7 +208,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
}}
>
<img
alt={unwrap(thisShell.name)}
alt={unwrap(thisShell.name!)}
src={asset(`icons/shells/${thisShell.icon}.webp`)}
style={{
width: "50%",
@@ -212,7 +217,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
/>
</IconButton>
))}
<CustomShellButton />
</Flex>
@@ -281,6 +285,22 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<DynamicArmorSwitcher />
</Suspense>
)}
<IconButton
variant="soft"
size={{ initial: "2", sm: "3" }}
color={equalize ? undefined : "gray"}
onClick={() => {
Duel.mutate((draft) => {
draft.equalize = !draft.equalize;
});
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}}
>
<EqualIcon />
</IconButton>
</Flex>
<Flex
@@ -456,7 +476,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Dialog.Trigger>
<Button variant="solid" highContrast>
<Crosshair1Icon />
{unwrap(antagonistTank.name)}
{unwrap(antagonistTank.name!)}
<SmallTankIcon id={antagonistTank.id} size={16} />
</Button>
</Dialog.Trigger>
@@ -9,7 +9,7 @@ export function Title() {
const protagonist = Duel.use((state) => state.protagonist.tank);
const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed);
const name = unwrap(protagonist.name);
const name = unwrap(protagonist.name!);
const fontSize = revealed
? disturbed
? "1.5rem"
@@ -53,24 +53,24 @@ export function Controls({
const protagonistGunModelDefinition =
protagonistTurretModelDefinition.guns[protagonistGun.id];
const protagonistHullOrigin = new Vector3(
protagonistTrackModelDefinition.origin.x,
protagonistTrackModelDefinition.origin.y,
-protagonistTrackModelDefinition.origin.z,
protagonistTrackModelDefinition.origin!.x,
protagonistTrackModelDefinition.origin!.y,
-protagonistTrackModelDefinition.origin!.z,
);
const protagonistTurretOrigin = new Vector3(
protagonistModelDefinition.turret_origin.x,
protagonistModelDefinition.turret_origin.y,
-protagonistModelDefinition.turret_origin.z,
protagonistModelDefinition.turret_origin!.x,
protagonistModelDefinition.turret_origin!.y,
-protagonistModelDefinition.turret_origin!.z,
);
const protagonistGunOrigin = new Vector3(
protagonistTurretModelDefinition.gun_origin.x,
protagonistTurretModelDefinition.gun_origin.y,
-protagonistTurretModelDefinition.gun_origin.z,
protagonistTurretModelDefinition.gun_origin!.x,
protagonistTurretModelDefinition.gun_origin!.y,
-protagonistTurretModelDefinition.gun_origin!.z,
);
const antagonistGunHeight =
protagonistTrackModelDefinition.origin.y +
antagonistModelDefinition.turret_origin.y +
antagonistTurretModelDefinition.gun_origin.y;
protagonistTrackModelDefinition.origin!.y +
antagonistModelDefinition.turret_origin!.y +
antagonistTurretModelDefinition.gun_origin!.y;
const disturbed = Tankopedia.use((state) => state.disturbed);
const doAutoRotate = autoRotate && !disturbed;
const gunHeight =
@@ -119,7 +119,7 @@ export function Controls({
const [pitch] = applyPitchYawLimits(
-Infinity,
0,
protagonistGunModelDefinition.pitch,
protagonistGunModelDefinition.pitch!,
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
@@ -152,7 +152,7 @@ export function Controls({
const [pitch] = applyPitchYawLimits(
0,
0,
protagonistGunModelDefinition.pitch,
protagonistGunModelDefinition.pitch!,
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
@@ -1,9 +1,9 @@
import { useEffect } from 'react';
import { degToRad } from 'three/src/math/MathUtils.js';
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
import { Duel } from '../../../../../../stores/duel';
import { useEffect } from "react";
import { degToRad } from "three/src/math/MathUtils.js";
import { awaitableModelDefinitions } from "../../../../../../core/awaitables/modelDefinitions";
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
import { Duel } from "../../../../../../stores/duel";
const modelDefinitions = await awaitableModelDefinitions;
@@ -20,7 +20,7 @@ export function InitialAligner() {
const [pitch, yaw] = applyPitchYawLimits(
degToRad(8),
degToRad(25),
gunModel.pitch,
gunModel.pitch!,
turretModel.yaw,
);
@@ -134,12 +134,12 @@ export function TankModel() {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
@@ -150,10 +150,10 @@ export function TankModel() {
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}
@@ -217,12 +217,12 @@ export function TankModel() {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
@@ -231,10 +231,10 @@ export function TankModel() {
[pitch, yaw] = applyPitchYawLimits(
pitch - delta.y * (Math.PI / boundingRect.height),
yaw + delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}
@@ -37,9 +37,9 @@ export function TransitionSkeleton() {
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const wrapper = useRef<Group>(null);
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
useTankTransform(track, turret, turretContainer, gunContainer);
@@ -77,7 +77,7 @@ export function TransitionSkeleton() {
<group ref={turretContainer}>
{armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`,
);
const isVisible = isCurrentTurret;
if (!isVisible) {
@@ -102,7 +102,7 @@ export function TransitionSkeleton() {
<group ref={gunContainer}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`,
);
const isVisible = isCurrentGun;
@@ -73,7 +73,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const [pitch, yaw] = applyPitchYawLimits(
-Infinity,
0,
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
@@ -88,7 +88,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const [pitch, yaw] = applyPitchYawLimits(
Infinity,
0,
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
@@ -102,7 +102,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const [pitch, yaw] = applyPitchYawLimits(
0,
0,
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
@@ -124,7 +124,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw,
gunModelDefinition.pitch,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
@@ -2,6 +2,7 @@ import { Box, Flex } from "@radix-ui/themes";
import { times } from "lodash-es";
import { useEffect, useMemo, useRef } from "react";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { defaultEqualizer } from "../../../core/blitzkit/tankToDuelMember";
import { useFullScreen } from "../../../hooks/useFullScreen";
import { Duel } from "../../../stores/duel";
import { Tankopedia } from "../../../stores/tankopedia";
@@ -14,7 +15,7 @@ import { Title } from "./components/TankSandbox/Title";
const tankDefinitions = await awaitableTankDefinitions;
export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
const revealed = Tankopedia.use((state) => state.revealed);
const equalize = Duel.use((state) => state.equalize);
const disturbed = Tankopedia.use((state) => state.disturbed);
const canvas = useRef<HTMLCanvasElement>(null!);
const isFullScreen = useFullScreen();
@@ -22,20 +23,22 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
const thicknessRange = useMemo(() => {
const entries = Object.values(tankDefinitions.tanks);
const filtered = entries.filter(
(thisTank) => thisTank.tier === protagonist.tier
(thisTank) => thisTank.tier === protagonist.tier,
);
const value =
(filtered.reduce((accumulator, thisTank) => {
return (
accumulator +
thisTank.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near
thisTank.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near *
((equalize ? thisTank.equalizer : undefined) ?? defaultEqualizer)
.penetration
);
}, 0) /
filtered.length) *
(3 / 4);
return { value } satisfies ThicknessRange;
}, [protagonist]);
}, [protagonist, equalize]);
useEffect(() => {
if (disturbed) {
@@ -99,11 +102,11 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
isFullScreen
? "100vh"
: disturbed
? "calc(100svh - 8rem)"
: {
initial: "28rem",
md: "calc(100svh - 14rem)",
}
? "calc(100svh - 8rem)"
: {
initial: "28rem",
md: "calc(100svh - 14rem)",
}
}
maxWidth={isFullScreen ? undefined : "120rem"}
flexGrow="1"
@@ -36,7 +36,7 @@ export function VoteCaster() {
</Dialog.Trigger>
<Dialog.Content maxWidth="23rem">
<Dialog.Title>Voting for {unwrap(tank.name)}</Dialog.Title>
<Dialog.Title>Voting for {unwrap(tank.name!)}</Dialog.Title>
<Flex direction="column" mt="4">
<Flex align="center" justify="between">
@@ -118,10 +118,10 @@ export function VoteCaster() {
survivability === null
? ""
: `survivability=${survivability}&`
}player=${wargaming.id}&token=${wargaming.token}`
}player=${wargaming.id}&token=${wargaming.token}`,
)
.then(
(response) => response.json() as Promise<BlitzkitResponse>
(response) => response.json() as Promise<BlitzkitResponse>,
)
.then((json) => {
setShowDialogue(false);
@@ -3,19 +3,20 @@ import {
TankPriceType,
TankType,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import { Box, Code, Flex } from '@radix-ui/themes';
import { Var } from '../../../core/radix/var';
import { useLocale } from '../../../hooks/useLocale';
import { App } from '../../../stores/app';
import { Duel } from '../../../stores/duel';
import { classIcons } from '../../ClassIcon';
import { Listing } from './components/Listing';
} from "@blitzkit/core";
import { Box, Flex } from "@radix-ui/themes";
import { Var } from "../../../core/radix/var";
import { useLocale } from "../../../hooks/useLocale";
import { App } from "../../../stores/app";
import { Duel } from "../../../stores/duel";
import { classIcons } from "../../ClassIcon";
import { CopyableCode } from "../../CopyableCode";
import { Listing } from "./components/Listing";
const NATIONAL_BANNER_POSITION_OVERRIDES: Record<string, string> = {
germany: '50% 35%',
france: '50% 35%',
other: '50% 35%',
germany: "50% 35%",
france: "50% 35%",
other: "50% 35%",
};
export function MetaSection() {
@@ -29,10 +30,10 @@ export function MetaSection() {
<Box
style={{
background: `url(/assets/images/national-wallpapers/${protagonist.nation}.jpg)`,
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
backgroundPosition:
NATIONAL_BANNER_POSITION_OVERRIDES[protagonist.nation] ?? 'center',
NATIONAL_BANNER_POSITION_OVERRIDES[protagonist.nation] ?? "center",
}}
flexGrow="1"
maxWidth="120rem"
@@ -40,9 +41,9 @@ export function MetaSection() {
<Box
style={{
background: `url(${asset(`flags/scratched/${protagonist.nation}.webp`)})`,
backgroundRepeat: 'no-repeat',
backgroundSize: 'min(53rem, 80vw)',
backgroundPosition: '-8rem 50%',
backgroundRepeat: "no-repeat",
backgroundSize: "min(53rem, 80vw)",
backgroundPosition: "-8rem 50%",
}}
>
<Flex
@@ -50,19 +51,19 @@ export function MetaSection() {
align="center"
py="6rem"
style={{
background: `linear-gradient(${Var('black-a8')}, ${Var('black-a8')}, ${Var('gray-1')})`,
background: `linear-gradient(${Var("black-a8")}, ${Var("black-a8")}, ${Var("gray-1")})`,
}}
>
<Flex direction="column" align="center" gap="6">
<Flex
gap={{ initial: '2', sm: '8' }}
direction={{ initial: 'column', sm: 'row' }}
gap={{ initial: "2", sm: "8" }}
direction={{ initial: "column", sm: "row" }}
>
<Flex direction="column" width="100%" gap="2">
<Listing label={strings.website.tools.tankopedia.meta.nation}>
<Flex align="center" gap="1">
<img
style={{ width: '1em', height: '1em' }}
style={{ width: "1em", height: "1em" }}
alt={protagonist.nation}
src={asset(`flags/circle/${protagonist.nation}.webp`)}
/>
@@ -85,10 +86,10 @@ export function MetaSection() {
<Listing
label={strings.website.tools.tankopedia.meta.type}
color={
protagonist.type === TankType.COLLECTOR
? 'blue'
: protagonist.type === TankType.PREMIUM
? 'amber'
protagonist.type === TankType.TANK_TYPE_COLLECTOR
? "blue"
: protagonist.type === TankType.TANK_TYPE_PREMIUM
? "amber"
: undefined
}
>
@@ -101,40 +102,62 @@ export function MetaSection() {
<Listing
label={strings.website.tools.tankopedia.meta.dev_id}
>
<Code>{protagonist.id}</Code>
<CopyableCode
copy={`${protagonist.id}`}
variant="soft"
highContrast
color="gray"
>
{protagonist.id}
</CopyableCode>
</Listing>
)}
{protagonist.type === TankType.PREMIUM && (
{developerMode && (
<Listing
label={strings.website.tools.tankopedia.meta.dev_name}
>
<CopyableCode
copy={protagonist.dev_name}
variant="soft"
highContrast
color="gray"
>
{protagonist.dev_name}
</CopyableCode>
</Listing>
)}
{protagonist.type === TankType.TANK_TYPE_PREMIUM && (
<Listing
label={strings.website.tools.tankopedia.meta.purchase}
>
<Flex align="center" gap="1">
{protagonist.price.value / 400}
{protagonist.price!.value / 400}
<img
style={{ width: '1em', height: '1em' }}
style={{ width: "1em", height: "1em" }}
alt="gold"
src={asset('icons/currencies/gold.webp')}
src={asset("icons/currencies/gold.webp")}
/>
</Flex>
</Listing>
)}
<Listing
label={
protagonist.type === TankType.RESEARCHABLE
protagonist.type === TankType.TANK_TYPE_RESEARCHABLE
? strings.website.tools.tankopedia.meta.purchase
: strings.website.tools.tankopedia.meta.restoration
}
>
<Flex align="center" gap="1">
{protagonist.price.value.toLocaleString(locale)}
{protagonist.price!.value.toLocaleString(locale)}
<img
style={{ width: '1em', height: '1em' }}
alt={TankPriceType[protagonist.price.type]}
style={{ width: "1em", height: "1em" }}
alt={TankPriceType[protagonist.price!.type]}
src={asset(
`icons/currencies/${
protagonist.price.type === TankPriceType.GOLD
? 'gold'
: 'silver'
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`,
)}
/>
@@ -142,15 +165,16 @@ export function MetaSection() {
</Listing>
<Listing label={strings.website.tools.tankopedia.meta.sale}>
<Flex align="center" gap="1">
{(protagonist.price.value / 2).toLocaleString(locale)}
{(protagonist.price!.value / 2).toLocaleString(locale)}
<img
style={{ width: '1em', height: '1em' }}
alt={TankPriceType[protagonist.price.type]}
style={{ width: "1em", height: "1em" }}
alt={TankPriceType[protagonist.price!.type]}
src={asset(
`icons/currencies/${
protagonist.price.type === TankPriceType.GOLD
? 'gold'
: 'silver'
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`,
)}
/>
@@ -166,9 +190,9 @@ export function MetaSection() {
.value as number
).toLocaleString(locale)}
<img
style={{ width: '1em', height: '1em' }}
style={{ width: "1em", height: "1em" }}
alt="xp"
src={asset('icons/currencies/xp.webp')}
src={asset("icons/currencies/xp.webp")}
/>
</Flex>
</Listing>
@@ -36,7 +36,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
? 0
: (tank.research_cost.research_cost_type!.value as number) /
xpMultiplier),
0
0,
);
const averages = averageDefinitions.averages[tank.id]?.mu;
const averageXp = averages ? averages.xp / averages.battles : undefined;
@@ -78,7 +78,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
}}
>
<img
alt={unwrap(tank.name)}
alt={unwrap(tank.name!)}
src={asset(`icons/tanks/big/${id}.webp`)}
width={64}
height={64}
@@ -92,7 +92,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
<Text color="gray" size="1">
{TIER_ROMAN_NUMERALS[tank.tier]}
</Text>
<Text wrap="nowrap">{unwrap(tank.name)}</Text>
<Text wrap="nowrap">{unwrap(tank.name!)}</Text>
</Flex>
<Flex gap="2" align="center">
<Text color="gray" size="1" wrap="nowrap">
@@ -122,7 +122,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
objectPosition: "center",
}}
/>
{formatCompact(locale, tank.price.value)}
{formatCompact(locale, tank.price!.value)}
</Flex>
</Text>
</Flex>
@@ -135,7 +135,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
? strings.website.tools.tankopedia.tech_tree.battle
: literals(
strings.website.tools.tankopedia.tech_tree.battles,
{ count: games }
{ count: games },
))}
</Text>
)}
@@ -26,14 +26,18 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
function extend(line: Line): Line[] {
const root = tankDefinitions.tanks[line.at(-1)!];
if (root.ancestors === undefined || root.tier === 1) {
if (
root.ancestors === undefined ||
root.ancestors.length === 0 ||
root.tier === 1
) {
return [line];
} else {
if (root.ancestors.length === 1 || root.tier === 2) {
line.push(
root.ancestors.find(
(ancestor) => !tankDefinitions.tanks[ancestor].deprecated
) ?? root.ancestors[0]
(ancestor) => !tankDefinitions.tanks[ancestor].deprecated,
) ?? root.ancestors[0],
);
return extend(line);
} else {
@@ -52,7 +56,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
const [lineIndex, setLineIndex] = useState(0);
const line = useMemo(
() => (lines.length === 0 ? [master.id] : [...lines[lineIndex]].reverse()),
[master, lineIndex]
[master, lineIndex],
);
const totalXp = line.reduce(
(xp, id) =>
@@ -62,11 +66,11 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
? 0
: (tankDefinitions.tanks[id].research_cost.research_cost_type!
.value as number)),
0
0,
);
const totalCredits = line.reduce(
(credits, id) => credits + tankDefinitions.tanks[id].price.value,
0
(credits, id) => credits + tankDefinitions.tanks[id].price!.value,
0,
);
useEffect(() => {
@@ -75,7 +79,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
});
if (
master.type !== TankType.RESEARCHABLE ||
master.type !== TankType.TANK_TYPE_RESEARCHABLE ||
master.ancestors === undefined ||
master.ancestors.length === 0
) {
@@ -1,12 +1,12 @@
import { youtubers } from '@blitzkit/core';
import { Box, Flex, Heading, Link, Skeleton, Text } from '@radix-ui/themes';
import { awaitableReviews } from '../../core/awaitables/reviews';
import { Var } from '../../core/radix/var';
import { useLocale } from '../../hooks/useLocale';
import { Duel } from '../../stores/duel';
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
import { PageWrapper } from '../PageWrapper';
import { VerifiedIcon } from '../VerifiedIcon';
import { youtubers } from "@blitzkit/core";
import { Box, Flex, Heading, Link, Skeleton, Text } from "@radix-ui/themes";
import { awaitableReviews } from "../../core/awaitables/reviews";
import { Var } from "../../core/radix/var";
import { useLocale } from "../../hooks/useLocale";
import { Duel } from "../../stores/duel";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { PageWrapper } from "../PageWrapper";
import { VerifiedIcon } from "../VerifiedIcon";
const reviews = await awaitableReviews;
@@ -47,22 +47,22 @@ export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) {
<Flex
direction="column"
style={{
borderRadius: 'var(--radius-3)',
overflow: 'hidden',
borderRadius: "var(--radius-3)",
overflow: "hidden",
background: skeleton
? Var('color-panel-solid')
? Var("color-panel-solid")
: `url(${image})`,
backgroundPosition: '0 18px',
backgroundSize: 'cover',
backgroundPosition: "0 18px",
backgroundSize: "cover",
}}
>
<Box
style={{
background: skeleton ? 'transparent' : `url(${image})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
background: skeleton ? "transparent" : `url(${image})`,
backgroundSize: "cover",
backgroundPosition: "center",
height: 128,
aspectRatio: '16 / 9',
aspectRatio: "16 / 9",
}}
/>
<Flex
@@ -70,8 +70,8 @@ export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) {
width="100%"
justify="center"
style={{
backdropFilter: 'blur(4rem)',
WebkitBackdropFilter: 'blur(4rem)',
backdropFilter: "blur(4rem)",
WebkitBackdropFilter: "blur(4rem)",
}}
align="center"
gap="1"
+36 -36
View File
@@ -5,20 +5,20 @@ import {
TankType,
type CompositeStats,
type CompositeStatsKey,
} from '@blitzkit/core';
import strings from '@blitzkit/i18n/strings/en.json';
import { amberDark, blueDark } from '@radix-ui/colors';
import { Flex, Text, type FlexProps } from '@radix-ui/themes';
import { times } from 'lodash-es';
import type { ReactNode } from 'react';
import { breakdownConfig } from '../constants/embeds';
import { toRadiusVar } from '../core/radix/utils';
import { useLocale } from '../hooks/useLocale';
import { useEmbedStateCurry } from '../stores/embedState/utilities';
import { ClassHeavy } from './ClassIcon/components/ClassHeavy';
import { ClassLight } from './ClassIcon/components/ClassLight';
import { ClassMedium } from './ClassIcon/components/ClassMedium';
import { ClassTankDestroyer } from './ClassIcon/components/ClassTankDestroyer';
} from "@blitzkit/core";
import strings from "@blitzkit/i18n/strings/en.json";
import { amberDark, blueDark } from "@radix-ui/colors";
import { Flex, Text, type FlexProps } from "@radix-ui/themes";
import { times } from "lodash-es";
import type { ReactNode } from "react";
import { breakdownConfig } from "../constants/embeds";
import { toRadiusVar } from "../core/radix/utils";
import { useLocale } from "../hooks/useLocale";
import { useEmbedStateCurry } from "../stores/embedState/utilities";
import { ClassHeavy } from "./ClassIcon/components/ClassHeavy";
import { ClassLight } from "./ClassIcon/components/ClassLight";
import { ClassMedium } from "./ClassIcon/components/ClassMedium";
import { ClassTankDestroyer } from "./ClassIcon/components/ClassTankDestroyer";
type BreakdownEmbedWrapperProps = FlexProps & {
children: ReactNode;
@@ -33,7 +33,7 @@ export function BreakdownEmbedWrapper({
return (
<Flex
direction="column"
gap={useEmbedState('listGap')}
gap={useEmbedState("listGap")}
height="100%"
{...props}
>
@@ -60,65 +60,65 @@ export function BreakdownEmbedCard({
flexShrink="0"
direction="column"
style={{
borderRadius: toRadiusVar(useEmbedState('cardRadius')),
overflow: 'hidden',
backgroundColor: useEmbedState('cardBodyBackgroundColor'),
borderRadius: toRadiusVar(useEmbedState("cardRadius")),
overflow: "hidden",
backgroundColor: useEmbedState("cardBodyBackgroundColor"),
}}
>
<Flex
justify="center"
style={{
backgroundColor: useEmbedState('cardHeaderBackgroundColor'),
backgroundColor: useEmbedState("cardHeaderBackgroundColor"),
}}
p={useEmbedState('cardHeaderPadding')}
p={useEmbedState("cardHeaderPadding")}
>
<Text
{...{
...useRichText('cardTitle'),
...useRichText("cardTitle"),
style:
tank === null
? undefined
: tank === undefined ||
tank.type === TankType.RESEARCHABLE ||
!useEmbedState('cardTitleTypeColor')
tank.type === TankType.TANK_TYPE_RESEARCHABLE ||
!useEmbedState("cardTitleTypeColor")
? undefined
: {
color:
tank.type === TankType.PREMIUM
tank.type === TankType.TANK_TYPE_PREMIUM
? amberDark.amber11
: blueDark.blue11,
},
}}
>
<Flex align="center" gap="1">
{useEmbedState('cardTitleClassIcon') &&
{useEmbedState("cardTitleClassIcon") &&
tank !== null &&
tank !== undefined && (
<>
{tank.class === TankClass.LIGHT && (
{tank.class === TankClass.TANK_CLASS_LIGHT && (
<ClassLight width="1em" height="1em" />
)}
{tank.class === TankClass.MEDIUM && (
{tank.class === TankClass.TANK_CLASS_MEDIUM && (
<ClassMedium width="1em" height="1em" />
)}
{tank.class === TankClass.HEAVY && (
{tank.class === TankClass.TANK_CLASS_HEAVY && (
<ClassHeavy width="1em" height="1em" />
)}
{tank.class === TankClass.TANK_DESTROYER && (
{tank.class === TankClass.TANK_CLASS_TANK_DESTROYER && (
<ClassTankDestroyer width="1em" height="1em" />
)}
</>
)}{' '}
)}{" "}
{tank === null
? 'Total'
? "Total"
: tank === undefined
? `Unknown tank`
: unwrap(tank.name)}
: unwrap(tank.name!)}
</Flex>
</Text>
</Flex>
<Flex p={useEmbedState('cardBodyPadding')}>
<Flex p={useEmbedState("cardBodyPadding")}>
{times(4, (index) => {
const column = (index + 1) as 1 | 2 | 3 | 4;
const statKey = useEmbedState(`column${column}`) as CompositeStatsKey;
@@ -129,13 +129,13 @@ export function BreakdownEmbedCard({
key={column}
direction="column"
align="center"
gap={useEmbedState('columnGap')}
gap={useEmbedState("columnGap")}
flexGrow="1"
>
<Text {...useRichText('columnValue')}>
<Text {...useRichText("columnValue")}>
{formatCompositeStat(composite[statKey], statKey, composite)}
</Text>
<Text {...useRichText('columnLabel')}>
<Text {...useRichText("columnLabel")}>
{customLabel.length === 0
? strings.common.composite_stats[statKey]
: customLabel}
-5
View File
@@ -55,11 +55,6 @@ export const tools: Record<string, Tool> = {
id: "session",
button: "blue",
},
discord: {
id: "discord",
href: "https://discord.com/application-directory/1097673957865443370",
button: "indigo",
},
tier_list: {
id: "tier-list",
strings: "tier_list",
@@ -8,7 +8,7 @@ const tanks = Object.values(tankDefinitions.tanks).filter((tank) => {
const turret = tank.turrets.at(-1)!;
const turretModel = tankModel.turrets[turret.id];
return tank.tier >= 10 && !turretModel.yaw;
return !turretModel.yaw;
});
export function curateMixer() {
@@ -22,10 +22,13 @@ import {
} from "@blitzkit/core";
import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient";
import type { EquipmentMatrix } from "../../stores/duel";
import { defaultEqualizer } from "./tankToDuelMember";
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
export type TankCharacteristicsKey = keyof TankCharacteristics;
const EQUALIZER_HEALTH_EPSILON = 50;
export function tankCharacteristics(
{
tank,
@@ -47,6 +50,7 @@ export function tankCharacteristics(
applyDynamicArmor,
applySpallLiner,
assaultDistance,
equalize,
}: {
tank: TankDefinition;
turret: TurretDefinition;
@@ -67,6 +71,7 @@ export function tankCharacteristics(
applyDynamicArmor: boolean;
applySpallLiner: boolean;
assaultDistance: number;
equalize: boolean;
},
{
tankModelDefinition,
@@ -76,11 +81,12 @@ export function tankCharacteristics(
tankModelDefinition: ModelDefinition;
equipmentDefinitions: EquipmentDefinitions;
provisionDefinitions: ProvisionDefinitions;
}
},
) {
const preset = equipmentDefinitions.presets[tank.equipment_preset];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const equalizer = (equalize ? tank.equalizer : undefined) ?? defaultEqualizer;
function equipment(id: number) {
return preset.slots.some((slot, index) => {
@@ -138,56 +144,64 @@ export function tankCharacteristics(
provisionDefinitions.provisions[provision].crew
? total + provisionDefinitions.provisions[provision].crew! / 100
: total,
0
0,
) + (hasImprovedVentilation ? 0.08 : 0);
const commanderMastery = 1 + provisionCrewBonus;
const loaderMastery =
commanderMastery *
(tank.crew.some(({ type }) => type === CrewType.LOADER) ? 1.1 : 1.05);
(tank.crew.some(({ type }) => type === CrewType.CREW_TYPE_LOADER)
? 1.1
: 1.05);
const gunnerMastery =
commanderMastery *
(tank.crew.some(({ type }) => type === CrewType.GUNNER) ? 1.1 : 1.05);
(tank.crew.some(({ type }) => type === CrewType.CREW_TYPE_GUNNER)
? 1.1
: 1.05);
const driverMastery =
commanderMastery *
(tank.crew.some(({ type }) => type === CrewType.DRIVER) ? 1.1 : 1.05);
(tank.crew.some(({ type }) => type === CrewType.CREW_TYPE_DRIVER)
? 1.1
: 1.05);
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
const burstShells = gun.burst?.count ?? 1;
const burstInterShell = gun.burst?.interval;
const armorDamageCoefficient =
coefficient([hasTungsten, 0.15]) *
coefficient(
[applyReactiveArmor && shell.type !== ShellType.HE, -0.27],
[applyReactiveArmor && shell.type !== ShellType.SHELL_TYPE_HE, -0.27],
[applyDynamicArmor, -0.1],
[applySpallLiner && shell.type === ShellType.HE, -0.2]
[applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE, -0.2],
);
const assaultDamageCoefficient =
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
? gun.assault_ranges.ranges.find(
({ distance }) => distance >= assaultDistance
)?.factor ?? 0
? (gun.assault_ranges.ranges.find(
({ distance }) => distance >= assaultDistance,
)?.factor ?? 0)
: 1;
const moduleDamageCoefficient = coefficient([hasTungsten, 0.15]);
const reloadCoefficient =
(coefficient([hasGunRammer, -0.05]) *
coefficient([true, degressiveStat(loaderMastery)])) /
coefficient([hasAdrenaline && gun.gun_type!.$case === "regular", 0.17]);
const penetrationCoefficient = coefficient([
hasCalibratedShells,
resolvePenetrationCoefficient(true, shell.type) - 1,
]);
const healthCoefficient = coefficient(
[hasSandbagArmor, 0.03],
[hasEnhancedSandbagArmor, 0.06],
[hasImprovedAssembly, 0.04]
);
const penetrationCoefficient =
coefficient([
hasCalibratedShells,
resolvePenetrationCoefficient(true, equalize, shell.type, equalizer) - 1,
]) * equalizer.penetration;
const healthCoefficient =
coefficient(
[hasSandbagArmor, 0.03],
[hasEnhancedSandbagArmor, 0.06],
[hasImprovedAssembly, 0.04],
) * equalizer.health;
const shellVelocityCoefficient = coefficient(
[hasSupercharger, 0.35],
[hasImprovedGunPowder, 0.3]
[hasImprovedGunPowder, 0.3],
);
const aimTimeCoefficient =
coefficient(
[hasEnhancedGunLayingDrive, -0.15],
[hasReticleCalibration, -0.3]
[hasReticleCalibration, -0.3],
) * coefficient([true, degressiveStat(gunnerMastery)]);
const dispersionStillCoefficient =
coefficient([hasRefinedGun, -0.15], [hasReticleCalibration, -0.3]) *
@@ -197,25 +211,26 @@ export function tankCharacteristics(
-0.12,
]);
const dispersionHullTraverseCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_turn / 100]
[hasVerticalStabilizer, -0.12],
[true, -crewSkills.smooth_turn / 100],
);
const dispersionAfterShotCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.soft_recoil / 100]
[hasVerticalStabilizer, -0.12],
[true, -crewSkills.soft_recoil / 100],
);
const dispersionMovingCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_driving / 100]
[hasVerticalStabilizer, -0.12],
[true, -crewSkills.smooth_driving / 100],
);
const dispersionTurretTraverseCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_turret * (2 / 100)]
[hasVerticalStabilizer, -0.12],
[true, -crewSkills.smooth_turret * (2 / 100)],
);
const enginePowerCoefficient = coefficient(
[
hasEngineAccelerator,
tank.class === TankClass.HEAVY || tank.class === TankClass.TANK_DESTROYER
tank.class === TankClass.TANK_CLASS_HEAVY ||
tank.class === TankClass.TANK_CLASS_TANK_DESTROYER
? 0.05
: 0.04,
],
@@ -225,7 +240,7 @@ export function tankCharacteristics(
[hasStandardFuel, 0.03],
[hasImprovedFuel, 0.1],
[hasGearOil, 0.03],
[hasImprovedGearOil, 0.06]
[hasImprovedGearOil, 0.06],
);
const turretTraverseCoefficient =
coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) *
@@ -234,7 +249,7 @@ export function tankCharacteristics(
coefficient(
[hasImprovedControl, 0.11],
[hasImprovedEnginePowerBoost, 0.01],
[true, crewSkills.virtuoso / 100]
[true, crewSkills.virtuoso / 100],
) * coefficient([true, progressiveStat(driverMastery)]);
const resistanceHardCoefficient =
@@ -250,48 +265,48 @@ export function tankCharacteristics(
const viewRangeCoefficient =
coefficient([
hasImprovedOptics,
tank.class === TankClass.HEAVY
tank.class === TankClass.TANK_CLASS_HEAVY
? 0.06
: tank.class === TankClass.MEDIUM
? 0.08
: tank.class === TankClass.LIGHT
? 0.11
: 0.04,
: tank.class === TankClass.TANK_CLASS_MEDIUM
? 0.08
: tank.class === TankClass.TANK_CLASS_LIGHT
? 0.11
: 0.04,
]) * coefficient([true, progressiveStat(commanderMastery)]);
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
const speedForwardsSum = sum(
[hasImprovedEnginePowerBoost, 4],
[hasGearOil, 2],
[hasImprovedGearOil, 4]
[hasImprovedGearOil, 4],
);
const speedBackwardsSum = sum(
[hasImprovedEnginePowerBoost, 8],
[hasGearOil, 2],
[hasImprovedGearOil, 4]
[hasImprovedGearOil, 4],
);
const camouflageCoefficientMoving = sum(
[
true,
coefficient([
camouflage,
tank.class === TankClass.TANK_DESTROYER
tank.class === TankClass.TANK_CLASS_TANK_DESTROYER
? 0.04
: tank.class === TankClass.HEAVY
? 0.03
: 0.02,
: tank.class === TankClass.TANK_CLASS_HEAVY
? 0.03
: 0.02,
]),
],
[
hasCamouflageNet,
tank.class === TankClass.HEAVY
tank.class === TankClass.TANK_CLASS_HEAVY
? 0.02
: tank.class === TankClass.MEDIUM
? 0.04
: tank.class === TankClass.LIGHT
? 0.07
: 0.1,
]
: tank.class === TankClass.TANK_CLASS_MEDIUM
? 0.04
: tank.class === TankClass.TANK_CLASS_LIGHT
? 0.07
: 0.1,
],
);
const camouflageCoefficientStill = sum(
[
@@ -299,32 +314,32 @@ export function tankCharacteristics(
coefficient(
[
camouflage,
tank.class === TankClass.TANK_DESTROYER
tank.class === TankClass.TANK_CLASS_TANK_DESTROYER
? 0.04
: tank.class === TankClass.HEAVY
? 0.03
: 0.02,
: tank.class === TankClass.TANK_CLASS_HEAVY
? 0.03
: 0.02,
],
[true, crewSkills.camouflage * (3 / 100)]
[true, crewSkills.camouflage * (3 / 100)],
),
],
[
hasCamouflageNet,
tank.class === TankClass.HEAVY
tank.class === TankClass.TANK_CLASS_HEAVY
? 0.04
: tank.class === TankClass.MEDIUM
? 0.08
: tank.class === TankClass.LIGHT
? 0.14
: 0.2,
]
: tank.class === TankClass.TANK_CLASS_MEDIUM
? 0.08
: tank.class === TankClass.TANK_CLASS_LIGHT
? 0.14
: 0.2,
],
);
const size = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box,
turretModelDefinition.bounding_box
)
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
);
const weightKg =
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
@@ -336,7 +351,8 @@ export function tankCharacteristics(
stockTurret.weight +
stockGun.weight;
const resolvedEnginePower = engine.power * enginePowerCoefficient;
const damageCoefficientWithoutAssault = armorDamageCoefficient;
const damageCoefficientWithoutAssault =
armorDamageCoefficient * equalizer.damage;
const damageCoefficient =
damageCoefficientWithoutAssault * assaultDamageCoefficient;
const dpm = resolveDpm(
@@ -344,7 +360,7 @@ export function tankCharacteristics(
shell,
damageCoefficient,
reloadCoefficient,
intraClipCoefficient
intraClipCoefficient,
);
const intraClip =
gun.gun_type?.$case === "regular"
@@ -398,7 +414,7 @@ export function tankCharacteristics(
const shellReloads =
gun.gun_type!.$case === "auto_reloader"
? gun.gun_type!.value.shell_reloads.map(
(reload) => reload * reloadCoefficient
(reload) => reload * reloadCoefficient,
)
: undefined;
const shellReload =
@@ -408,7 +424,7 @@ export function tankCharacteristics(
? gun.gun_type!.value.reload
: gun.gun_type!.value.clip_reload) * reloadCoefficient;
const caliber = shell.caliber;
const penetration = shell.penetration.near * penetrationCoefficient;
const penetration = shell.penetration!.near * penetrationCoefficient;
const clipDamage =
gun.gun_type!.$case === "regular"
? undefined
@@ -428,21 +444,21 @@ export function tankCharacteristics(
const dispersionGunDamaged =
gun.dispersion_damaged * dispersionDamagedCoefficient;
const gunDepression =
gunModelDefinition.pitch.max +
gunModelDefinition.pitch!.max +
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0) +
(hasDownImprovedVerticalStabilizer ? 3 : 0);
const gunElevation =
-gunModelDefinition.pitch.min -
-gunModelDefinition.pitch!.min -
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 4 : 0);
const gunFrontalDepression = gunModelDefinition.pitch.front?.max;
const gunFrontalElevation = gunModelDefinition.pitch.front
? -gunModelDefinition.pitch.front.min
const gunFrontalDepression = gunModelDefinition.pitch!.front?.max;
const gunFrontalElevation = gunModelDefinition.pitch!.front
? -gunModelDefinition.pitch!.front.min
: undefined;
const gunRearDepression = gunModelDefinition.pitch.back?.max;
const gunRearElevation = gunModelDefinition.pitch.back
? -gunModelDefinition.pitch.back.min
const gunRearDepression = gunModelDefinition.pitch!.back?.max;
const gunRearElevation = gunModelDefinition.pitch!.back
? -gunModelDefinition.pitch!.back.min
: undefined;
const azimuthLeft = turretModelDefinition.yaw
? -turretModelDefinition.yaw.min
@@ -491,23 +507,31 @@ export function tankCharacteristics(
hullTraverseCoefficient *
(track.resistance_hard / softTerrainCoefficientRaw) *
(stockWeight / weightKg);
const health = (tank.health + turret.health) * healthCoefficient;
let health = (tank.health + turret.health) * healthCoefficient;
if (equalize) {
health =
EQUALIZER_HEALTH_EPSILON * Math.round(health / EQUALIZER_HEALTH_EPSILON);
}
const fireChance = engine.fire_chance * fireChanceCoefficient;
const viewRange = turret.view_range * viewRangeCoefficient;
const camouflageStill = tank.camouflage_still * camouflageCoefficientStill;
const camouflageMoving =
tank.class === TankClass.LIGHT
tank.class === TankClass.TANK_CLASS_LIGHT
? tank.camouflage_still * camouflageCoefficientStill
: tank.camouflage_moving * camouflageCoefficientMoving;
const camouflageShootingStill =
tank.camouflage_still * gun.camouflage_loss * camouflageCoefficientStill;
const camouflageShootingMoving =
(tank.class === TankClass.LIGHT
(tank.class === TankClass.TANK_CLASS_LIGHT
? tank.camouflage_still * camouflageCoefficientStill
: tank.camouflage_moving * camouflageCoefficientMoving) *
gun.camouflage_loss;
const camouflageCaughtOnFire =
tank.camouflage_onFire * tank.camouflage_still * camouflageCoefficientStill;
tank.camouflage_on_fire *
tank.camouflage_still *
camouflageCoefficientStill;
const width = size.z;
const height = size.x;
const length = size.y;
@@ -518,7 +542,7 @@ export function tankCharacteristics(
const shellRange = shell.range;
const shellCapacity = gun.shell_capacity;
const gunType = gun.gun_type!.$case;
const penetrationAt250m = shell.penetration.far;
const penetrationAt250m = shell.penetration!.far * penetrationCoefficient;
return {
crewCount,
@@ -645,13 +669,3 @@ export const characteristicsOrder: {
items: ["crewCount"],
},
];
// export const characteristicsStyling: Record<
// TankCharacteristicsKey,
// {
// name: string;
// indent?: boolean;
// condition?: TankCharacteristicsCondition[];
// }
// > = {
// };
@@ -1,10 +1,11 @@
import {
createDefaultProvisions,
Equalizer,
type ProvisionDefinitions,
type TankDefinition,
} from '@blitzkit/core';
import type { DuelMember } from '../../stores/duel';
import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
} from "@blitzkit/core";
import type { DuelMember } from "../../stores/duel";
import { genericDefaultEquipmentMatrix } from "../../stores/duel/constants";
export function tankToDuelMember(
tank: TankDefinition,
@@ -28,3 +29,11 @@ export function tankToDuelMember(
assaultDistance: (gun.assault_ranges?.ranges[0].distance ?? 0) / 2,
} satisfies DuelMember;
}
export const defaultEqualizer: Equalizer = {
armor: 1,
damage: 1,
health: 1,
module_health: 1,
penetration: 1,
};
@@ -1,5 +1,5 @@
import { awaitableAverageDefinitions } from '../core/awaitables/averageDefinitions';
import { awaitableDiscoveredIdsDefinitions } from '../core/awaitables/discoveredIdsDefinitions';
import { awaitableAverageDefinitions } from "../core/awaitables/averageDefinitions";
import { awaitableDiscoveredIdsDefinitions } from "../core/awaitables/discoveredIdsDefinitions";
const [discoveredIdsDefinitions, averageDefinitions] = await Promise.all([
awaitableDiscoveredIdsDefinitions,
@@ -7,5 +7,5 @@ const [discoveredIdsDefinitions, averageDefinitions] = await Promise.all([
]);
export function useAveragesExclusionRatio() {
return discoveredIdsDefinitions.count / averageDefinitions.samples.total;
return discoveredIdsDefinitions.count / averageDefinitions.samples!.total;
}
@@ -21,23 +21,23 @@ export function useTankTransform(
track: TrackDefinition,
turret: TurretDefinition,
turretContainer: RefObject<Group>,
gunContainer: RefObject<Group>
gunContainer: RefObject<Group>,
) {
const tankModelDefinition = useTankModelDefinition();
useEffect(() => {
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const turretPosition = new Vector3();
const turretRotation = new Euler();
const gunPosition = new Vector3();
const gunRotation = new Euler();
function handleModelTransform(
event: QuicklimeEvent<ModelTransformEventData>
event: QuicklimeEvent<ModelTransformEventData>,
) {
handleModelTransformInner(event.data);
}
@@ -70,13 +70,13 @@ export function useTankTransform(
if (tankModelDefinition.initial_turret_rotation) {
const initialPitch = -degToRad(
tankModelDefinition.initial_turret_rotation.pitch
tankModelDefinition.initial_turret_rotation.pitch,
);
const initialYaw = -degToRad(
tankModelDefinition.initial_turret_rotation.yaw
tankModelDefinition.initial_turret_rotation.yaw,
);
const initialRoll = -degToRad(
tankModelDefinition.initial_turret_rotation.roll
tankModelDefinition.initial_turret_rotation.roll,
);
turretPosition
@@ -45,6 +45,7 @@ export function Page({ locale }: LocaleAcceptorProps) {
function Content() {
const members = CompareEphemeral.use((state) => state.members);
const equalize = CompareEphemeral.use((state) => state.equalize);
const [addTankDialogOpen, setAddTankDialogOpen] = useState(false);
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
const { strings } = useLocale();
@@ -61,26 +62,30 @@ function Content() {
stockTurret: thisMember.tank.turrets[0],
applyReactiveArmor: members.some(
(member) =>
member.key !== thisMember.key && member.consumables.includes(33)
member.key !== thisMember.key &&
member.consumables.includes(33),
),
applyDynamicArmor: members.some(
(member) =>
member.key !== thisMember.key && member.consumables.includes(73)
member.key !== thisMember.key &&
member.consumables.includes(73),
),
applySpallLiner: members.some(
(member) =>
member.key !== thisMember.key && member.provisions.includes(101)
member.key !== thisMember.key &&
member.provisions.includes(101),
),
assaultDistance: thisMember.assaultDistance,
equalize,
},
{
equipmentDefinitions: equipmentDefinitions,
provisionDefinitions: provisionDefinitions,
tankModelDefinition: modelDefinitions.models[thisMember.tank.id],
}
)
},
),
),
[members, crewSkills]
[members, crewSkills, equalize],
);
useEffect(() => {
@@ -112,7 +117,7 @@ function Content() {
window.history.replaceState(
{},
"",
`${window.location.pathname}?${search.toString()}`
`${window.location.pathname}?${search.toString()}`,
);
}, [members]);
@@ -59,7 +59,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
const tank = Guess.use((state) => state.tank);
const guessState = Guess.use((state) => state.guessState);
const isRevealed = guessState !== GuessState.NotGuessed;
const name = unwrap(tank.name);
const name = unwrap(tank.name!);
const fontSize = `min(48vh, ${55 / name.length}vw)`;
const transitionDuration = isRevealed ? "2s" : undefined;
const tiers = Guess.use((state) => state.tiers);
@@ -77,7 +77,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
if (filteredIds.length === 0) return;
const id = Number(
filteredIds[Math.floor(Math.random() * filteredIds.length)]
filteredIds[Math.floor(Math.random() * filteredIds.length)],
);
const newTank = tankDefinitions.tanks[id];
@@ -6,7 +6,6 @@ import { literals } from "@blitzkit/i18n/src/literals";
import { Box, Flex, Link } from "@radix-ui/themes";
import type { GetStaticPaths } from "astro";
import { AesonPlug } from "../../components/AesonPlug";
import { Giveaway } from "../../components/Giveaway";
import HomeHero from "../../components/HomeHero.astro";
import { Plugs } from "../../components/Plugs";
import ToolCard from "../../components/ToolCard.astro";
@@ -29,7 +28,7 @@ for (const id in tools) {
const compatibleRow = rows.find(
(row) =>
row.length < MAX_TOOLS_PER_ROW &&
!("significant" in row[0] && row[0].significant)
!("significant" in row[0] && row[0].significant),
);
if (compatibleRow) {
@@ -69,7 +69,7 @@ function Content() {
const input = useRef<HTMLInputElement>(null);
const session = Session.use();
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[] | null>(
null
null,
);
const [accountInfo, setAccountInfo] = useState<
IndividualAccountInfo | null | undefined
@@ -81,7 +81,7 @@ function Content() {
if (!id) return setAccountInfo(null);
setAccountInfo(
session.tracking ? await getAccountInfo(idToRegion(id), id) : null
session.tracking ? await getAccountInfo(idToRegion(id), id) : null,
);
})();
}, [session.tracking && session.player.id]);
@@ -95,20 +95,20 @@ function Content() {
const average = averageDefinitions.averages[tank.id];
const composite = compositeStats(
{ ...entry.all, battle_life_time: entry.battle_life_time },
average?.mu
average?.mu!,
);
return { tank, composite };
})
: undefined,
[session.tracking && session.player, tankStatsB]
[session.tracking && session.player, tankStatsB],
);
const total = useMemo(
() =>
delta
? sumCompositeStats(delta.map(({ composite }) => composite))
: undefined,
[delta]
[delta],
);
const search = debounce(async () => {
if (!input.current) return;
@@ -150,7 +150,7 @@ function Content() {
const tankStats = await getTankStats(
idToRegion(session.player.id),
session.player.id
session.player.id,
);
setTankStatsB(tankStats);
}
@@ -294,7 +294,7 @@ function Content() {
onClick={async () => {
const stats = await getTankStats(
session.player.region,
session.player.id
session.player.id,
);
if (stats === null) {
@@ -365,7 +365,7 @@ function Content() {
onClick={() => {
Session.mutate((draft) => {
(draft as SessionTracking).columns.unshift(
key
key,
);
});
}}
@@ -402,7 +402,8 @@ function Content() {
{compositeStatsKeys
.filter(
(item) =>
!session.columns.includes(item) || item === column
!session.columns.includes(item) ||
item === column,
)
.map((key) => (
<Select.Item key={key} value={key}>
@@ -452,7 +453,7 @@ function Content() {
{formatCompositeStat(
composite[column],
column,
composite
composite,
)}
</Table.Cell>
))}
@@ -43,7 +43,7 @@ export function Page({ id, skeleton, locale, guide }: PageProps) {
<HeroSection skeleton={skeleton} />
<MetaSection />
<CalloutsSection />
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
{tank.type === TankType.TANK_TYPE_RESEARCHABLE && !tank.deprecated && (
<TechTreeSection skeleton={skeleton} />
)}
<CharacteristicsSection skeleton={skeleton} />
@@ -33,7 +33,7 @@ export const getStaticPaths = (async () => {
parentPaths.map((parent) => ({
params: { ...parent.params, slug: tank.slug },
props: { ...parent.props, id: tank.id },
}))
})),
)
.flat();
}) satisfies GetStaticPaths;
@@ -50,7 +50,7 @@ const tank = tankDefinitions.tanks[id];
const tankModelDefinition = tankModelDefinitions.models[tank.id];
const unwrap = unwrapper(locale);
const title = literals(strings.website.tools.tankopedia.tank_seo_title, {
name: unwrap(tank.name),
name: unwrap(tank.name!),
tier: TIER_ROMAN_NUMERALS[tank.tier],
nationality:
strings.common.nations_adjectives[
@@ -86,19 +86,20 @@ const characteristics = tankCharacteristics(
stockTrack: tank.tracks[0],
track: tank.tracks.at(-1)!,
assaultDistance: 0,
equalize: false,
},
{ equipmentDefinitions, provisionDefinitions, tankModelDefinition }
{ equipmentDefinitions, provisionDefinitions, tankModelDefinition },
);
const description = literals(
strings.website.tools.tankopedia.tank_seo_description,
{
name: unwrap(tank.name),
name: unwrap(tank.name!),
dpm: Math.round(characteristics.dpm).toLocaleString(locale),
alpha: Math.round(characteristics.damage).toLocaleString(locale),
penetration: Math.round(characteristics.penetration).toLocaleString(locale),
speed: Math.round(characteristics.speedForwards).toLocaleString(locale),
health: Math.round(characteristics.health).toLocaleString(locale),
}
},
);
const guideUrl = `${assertSecret(import.meta.env.PUBLIC_GUIDE_WEBSITE)}/${tank.slug}`;
@@ -7,6 +7,7 @@ export interface CompareMember extends DuelMember {
export interface CompareEphemeral {
crewSkills: Record<string, number>;
equalize: boolean;
members: CompareMember[];
sorting?: {
direction: "ascending" | "descending";
@@ -19,5 +20,6 @@ export const CompareEphemeral = new Varuna<
Record<string, number>
>((crewSkills) => ({
crewSkills,
equalize: false,
members: [],
}));
+2 -1
View File
@@ -41,6 +41,7 @@ export interface DuelMember {
export interface DuelStore {
protagonist: DuelMember;
antagonist: DuelMember;
equalize: boolean;
}
const provisionDefinitions = await awaitableProvisionDefinitions;
@@ -51,5 +52,5 @@ export const Duel = new Varuna<
>(({ tank, model }) => {
const protagonist = tankToDuelMember(tank, provisionDefinitions);
return { protagonist, antagonist: protagonist, model };
return { protagonist, antagonist: protagonist, model, equalize: false };
});