mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: isolate stat sections
This commit is contained in:
1 parent
c3d2b063bc
commit
df1bc56a4c
8 files changed
+807
-791
No files matched your search
+112
@@ -0,0 +1,112 @@
|
||||
import { CREW_MEMBER_NAMES, CrewType } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
import { AccessibilityIcon, InfoCircledIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Heading, IconButton, Popover, Text } from '@radix-ui/themes';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions';
|
||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import type { StatsAcceptorProps } from './HullTraverseVisualizer';
|
||||
import { InfoWithDelta } from './InfoWithDelta';
|
||||
|
||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
|
||||
export function Crew({ stats }: StatsAcceptorProps) {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const { strings } = useLocale();
|
||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||
const hasImprovedVentilation = useEquipment(102);
|
||||
const provisionCrewBonus =
|
||||
provisions.reduce(
|
||||
(total, provision) =>
|
||||
provisionDefinitions.provisions[provision].crew
|
||||
? total + provisionDefinitions.provisions[provision].crew! / 100
|
||||
: total,
|
||||
0,
|
||||
) + (hasImprovedVentilation ? 0.08 : 0);
|
||||
const commanderMastery = 1 + provisionCrewBonus;
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex gap="2" align="center">
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.crew.title}
|
||||
</Heading>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton variant="ghost">
|
||||
<InfoCircledIcon />
|
||||
</IconButton>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content>
|
||||
<Flex gap="1" align="center">
|
||||
<AccessibilityIcon />
|
||||
<Text>{strings.website.tools.tankopedia.crew.info}</Text>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Flex>
|
||||
|
||||
<InfoWithDelta value="crewCount" stats={stats} decimals={0} />
|
||||
|
||||
{tank.crew.map((member) => {
|
||||
return (
|
||||
<Fragment key={member.type}>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
key={`${member.type}-root`}
|
||||
name={`${
|
||||
strings.website.tools.tankopedia.crew[
|
||||
CREW_MEMBER_NAMES[member.type]
|
||||
]
|
||||
}${
|
||||
member.count > 1
|
||||
? ` ${literals(strings.common.units.x, [`${member.count}`])}`
|
||||
: ''
|
||||
}`}
|
||||
unit="%"
|
||||
decimals={0}
|
||||
noRanking
|
||||
value={() =>
|
||||
(member.type === CrewType.COMMANDER
|
||||
? commanderMastery
|
||||
: commanderMastery * 1.1) * 100
|
||||
}
|
||||
/>
|
||||
|
||||
{member.substitute.length > 0 && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
key={`${member.type}-substitute`}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
name={
|
||||
<>
|
||||
<Flex align="center" gap="1" display="inline-flex">
|
||||
<AccessibilityIcon />
|
||||
{member.substitute
|
||||
.map((sub, index) =>
|
||||
index === 0
|
||||
? strings.website.tools.tankopedia.crew[
|
||||
CREW_MEMBER_NAMES[sub]
|
||||
]
|
||||
: sub,
|
||||
)
|
||||
.join(', ')}
|
||||
</Flex>
|
||||
</>
|
||||
}
|
||||
noRanking
|
||||
value={() => commanderMastery * 1.05 * 100}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
import {
|
||||
asset,
|
||||
coefficient,
|
||||
isExplosive,
|
||||
resolvePenetrationCoefficient,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Slider,
|
||||
Text,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { lerp } from 'three/src/math/MathUtils.js';
|
||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import type { StatsAcceptorProps } from './HullTraverseVisualizer';
|
||||
import { Info } from './Info';
|
||||
import { InfoWithDelta } from './InfoWithDelta';
|
||||
import { RicochetVisualizer } from './RicochetVisualizer';
|
||||
|
||||
export function Firepower({ stats }: StatsAcceptorProps) {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const { strings, unwrap } = useLocale();
|
||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
||||
const setPenetrationDistanceDebounced = debounce((value: number) => {
|
||||
setPenetrationDistance(value);
|
||||
}, 500);
|
||||
const { gun, shell, turret } = Duel.use((state) => state.protagonist);
|
||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
||||
const hasSupercharger = useEquipment(107);
|
||||
const hasCalibratedShells = useEquipment(103);
|
||||
const penetrationCoefficient = coefficient([
|
||||
hasCalibratedShells,
|
||||
resolvePenetrationCoefficient(true, shell.type) - 1,
|
||||
]);
|
||||
const penetrationLossOverDistanceCoefficient = coefficient([
|
||||
hasSupercharger,
|
||||
-0.6,
|
||||
]);
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||
const gunModelDefinition =
|
||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
||||
|
||||
useEffect(() => {
|
||||
if (penetrationDistanceInput.current) {
|
||||
penetrationDistanceInput.current.value = `${penetrationDistance}`;
|
||||
}
|
||||
}, [penetrationDistance]);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex align="center" gap="4">
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.firepower.title}
|
||||
</Heading>
|
||||
|
||||
<Flex>
|
||||
{gun.gun_type!.value.base.shells.map((thisShell, shellIndex) => (
|
||||
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
|
||||
<IconButton
|
||||
color={thisShell.id === shell.id ? undefined : 'gray'}
|
||||
variant="soft"
|
||||
style={{
|
||||
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
shellIndex === gun.gun_type!.value.base.shells.length - 1
|
||||
? undefined
|
||||
: 0,
|
||||
borderBottomRightRadius:
|
||||
shellIndex === gun.gun_type!.value.base.shells.length - 1
|
||||
? undefined
|
||||
: 0,
|
||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||
}}
|
||||
onClick={() => {
|
||||
mutateDuel((draft) => {
|
||||
draft.protagonist.shell = thisShell;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={unwrap(thisShell.name)}
|
||||
width={16}
|
||||
height={16}
|
||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Info
|
||||
name={strings.website.tools.tankopedia.characteristics.values.gunType}
|
||||
>
|
||||
{strings.common.gun_types[gun.gun_type!.$case]}
|
||||
</Info>
|
||||
<InfoWithDelta stats={stats} decimals={0} unit="hp / min" value="dpm" />
|
||||
{gun.gun_type!.$case === 'auto_reloader' && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
indent
|
||||
unit="hp / min"
|
||||
value="dpmEffective"
|
||||
/>
|
||||
)}
|
||||
{gun.gun_type!.$case !== 'regular' && (
|
||||
<InfoWithDelta stats={stats} value="shells" />
|
||||
)}
|
||||
{gun.gun_type!.$case === 'auto_reloader' ? (
|
||||
<>
|
||||
<Info
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.mostOptimalShellIndex
|
||||
}
|
||||
>
|
||||
{stats.mostOptimalShellIndex! + 1}
|
||||
</Info>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.shellReloads
|
||||
}
|
||||
unit="s"
|
||||
/>
|
||||
{stats.shellReloads!.map((reload, index) => (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
key={index}
|
||||
indent
|
||||
name={literals(
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.shell_index,
|
||||
[`${index + 1}`],
|
||||
)}
|
||||
decimals={2}
|
||||
deltaType="lowerIsBetter"
|
||||
noRanking
|
||||
value={() => reload}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="s"
|
||||
deltaType="lowerIsBetter"
|
||||
value="shellReload"
|
||||
/>
|
||||
)}
|
||||
{(gun.gun_type!.$case === 'auto_loader' ||
|
||||
gun.gun_type!.$case === 'auto_reloader') && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={2}
|
||||
unit="s"
|
||||
deltaType="lowerIsBetter"
|
||||
value="intraClip"
|
||||
/>
|
||||
)}
|
||||
<InfoWithDelta stats={stats} decimals={0} unit="mm" value="penetration" />
|
||||
{typeof shell.penetration !== 'number' && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={0}
|
||||
name={literals(
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.at_distance,
|
||||
[`${penetrationDistance}`],
|
||||
)}
|
||||
noRanking
|
||||
value={() =>
|
||||
lerp(
|
||||
shell.penetration.near,
|
||||
shell.penetration.far,
|
||||
(penetrationDistance * penetrationLossOverDistanceCoefficient) /
|
||||
500,
|
||||
) * penetrationCoefficient
|
||||
}
|
||||
/>
|
||||
<Flex align="center" gap="2" style={{ paddingLeft: 24 }}>
|
||||
<Text color="gray">
|
||||
{
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.at_distance_slider_label
|
||||
}
|
||||
</Text>
|
||||
<Slider
|
||||
variant="classic"
|
||||
key={penetrationDistance}
|
||||
min={0}
|
||||
max={500}
|
||||
style={{ flex: 1 }}
|
||||
defaultValue={[penetrationDistance]}
|
||||
onValueChange={([value]) => {
|
||||
setPenetrationDistanceDebounced(value);
|
||||
|
||||
if (!penetrationDistanceInput.current) return;
|
||||
|
||||
penetrationDistanceInput.current.value = value.toString();
|
||||
}}
|
||||
/>
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
style={{ width: 64 }}
|
||||
ref={penetrationDistanceInput}
|
||||
defaultValue={penetrationDistance}
|
||||
onBlur={() => {
|
||||
setPenetrationDistance(
|
||||
Math.min(
|
||||
parseInt(penetrationDistanceInput.current!.value),
|
||||
500,
|
||||
),
|
||||
);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TextField.Slot side="right">m</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
<InfoWithDelta stats={stats} unit="hp" decimals={0} value="damage" />
|
||||
{gun.gun_type!.$case !== 'regular' && (
|
||||
<InfoWithDelta stats={stats} indent decimals={0} value="clipDamage" />
|
||||
)}
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
unit="hp"
|
||||
decimals={0}
|
||||
value="moduleDamage"
|
||||
/>
|
||||
{isExplosive(shell.type) && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
unit="m"
|
||||
noRanking
|
||||
decimals={0}
|
||||
value="explosionRadius"
|
||||
/>
|
||||
)}
|
||||
<InfoWithDelta stats={stats} decimals={0} unit="mm" value="caliber" />
|
||||
|
||||
{!isExplosive(shell.type) && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="°"
|
||||
noRanking
|
||||
value="shellNormalization"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
noRanking
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
unit="°"
|
||||
value="shellRicochet"
|
||||
/>
|
||||
<RicochetVisualizer stats={stats} />
|
||||
</>
|
||||
)}
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
unit="m/s"
|
||||
decimals={0}
|
||||
value="shellVelocity"
|
||||
/>
|
||||
<InfoWithDelta stats={stats} unit="m" decimals={0} value="shellRange" />
|
||||
<InfoWithDelta stats={stats} decimals={0} value="shellCapacity" />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="s"
|
||||
deltaType="lowerIsBetter"
|
||||
value="aimTime"
|
||||
/>
|
||||
<Info
|
||||
name={literals(
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.dispersion_at_distance,
|
||||
['100'],
|
||||
)}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={3}
|
||||
indent
|
||||
unit="m"
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersion"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
prefix="+"
|
||||
decimals={3}
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionMoving"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={3}
|
||||
prefix="+"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionHullTraversing"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={3}
|
||||
prefix="+"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionTurretTraversing"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={3}
|
||||
prefix="+"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionShooting"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
prefix="x "
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionGunDamaged"
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.gun_flexibility
|
||||
}
|
||||
unit="°"
|
||||
/>
|
||||
<InfoWithDelta value="gunDepression" stats={stats} decimals={1} indent />
|
||||
<InfoWithDelta stats={stats} decimals={1} indent value="gunElevation" />
|
||||
{gunModelDefinition.pitch.front && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="gunFrontalDepression"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="gunFrontalElevation"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{gunModelDefinition.pitch.back && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="gunRearDepression"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="gunRearElevation"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{turretModelDefinition.yaw && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="azimuthLeft"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="azimuthRight"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -4,13 +4,13 @@ import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tan
|
||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||
import './index.css';
|
||||
|
||||
export interface VisualizerProps {
|
||||
export interface StatsAcceptorProps {
|
||||
stats: TankCharacteristics;
|
||||
}
|
||||
|
||||
type Terrain = 'Hard' | 'Medium' | 'Soft';
|
||||
|
||||
export function HullTraverseVisualizer({ stats }: VisualizerProps) {
|
||||
export function HullTraverseVisualizer({ stats }: StatsAcceptorProps) {
|
||||
const hull = useRef<HTMLDivElement>(null);
|
||||
const turret = useRef<HTMLImageElement>(null);
|
||||
const [terrain, setTerrain] = useState<Terrain>('Hard');
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import { Flex, Heading } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import {
|
||||
HullTraverseVisualizer,
|
||||
type StatsAcceptorProps,
|
||||
} from './HullTraverseVisualizer';
|
||||
import { Info } from './Info';
|
||||
import { InfoWithDelta } from './InfoWithDelta';
|
||||
|
||||
export function Maneuverability({ stats }: StatsAcceptorProps) {
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.maneuverability.title}
|
||||
</Heading>
|
||||
<Info
|
||||
name={strings.website.tools.tankopedia.characteristics.values.speed}
|
||||
unit="kph"
|
||||
/>
|
||||
<InfoWithDelta value="speedForwards" stats={stats} decimals={0} indent />
|
||||
<InfoWithDelta value="speedBackwards" stats={stats} decimals={0} indent />
|
||||
<InfoWithDelta stats={stats} decimals={0} value="enginePower" />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
unit="tn"
|
||||
deltaType="lowerIsBetter"
|
||||
value="weight"
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.terrain_coefficients
|
||||
}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="hardTerrainCoefficient"
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
value="mediumTerrainCoefficient"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="softTerrainCoefficient"
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.raw_terrain_coefficients
|
||||
}
|
||||
deltaType="lowerIsBetter"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="hardTerrainCoefficientRaw"
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="%"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="%"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="mediumTerrainCoefficientRaw"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="%"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="softTerrainCoefficientRaw"
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.power_to_weight_ratio
|
||||
}
|
||||
unit="hp/tn"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="powerToWeightRatioHardTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="powerToWeightRatioMediumTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="powerToWeightRatioSoftTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
unit="°/s"
|
||||
decimals={1}
|
||||
value="turretTraverseSpeed"
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.hull_traverse_speed
|
||||
}
|
||||
unit="°/s"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="hullTraverseHardTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="hullTraverseMediumTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="hullTraverseSoftTerrain"
|
||||
/>
|
||||
|
||||
<HullTraverseVisualizer stats={stats} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -6,9 +6,9 @@ import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||
import { Var } from '../../../../../../core/radix/var';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import type { VisualizerProps } from './HullTraverseVisualizer';
|
||||
import type { StatsAcceptorProps } from './HullTraverseVisualizer';
|
||||
|
||||
export function RicochetVisualizer({ stats }: VisualizerProps) {
|
||||
export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||
const shell = Duel.use((state) => state.protagonist.shell);
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const normalization = degToRad(stats.shellNormalization);
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import { Flex, Heading } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import type { StatsAcceptorProps } from './HullTraverseVisualizer';
|
||||
import { Info } from './Info';
|
||||
import { InfoWithDelta } from './InfoWithDelta';
|
||||
import { ViewRangeVisualizer } from './ViewRangeVisualizer';
|
||||
|
||||
export function Survivability({ stats }: StatsAcceptorProps) {
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.survivability.title}
|
||||
</Heading>
|
||||
<InfoWithDelta value="health" stats={stats} unit="hp" decimals={0} />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values.fireChance
|
||||
}
|
||||
unit="%"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={0}
|
||||
value={(stats) => stats.fireChance * 100}
|
||||
/>
|
||||
<InfoWithDelta value="viewRange" stats={stats} unit="m" decimals={0} />
|
||||
<ViewRangeVisualizer stats={stats} />
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values.camouflage
|
||||
}
|
||||
unit="%"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value={(stats) => stats.camouflageStill * 100}
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageStill
|
||||
}
|
||||
decimals={2}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value={(stats) => stats.camouflageMoving * 100}
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageMoving
|
||||
}
|
||||
decimals={2}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageShootingStill
|
||||
}
|
||||
decimals={2}
|
||||
value={(stats) => stats.camouflageShootingStill * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageShootingMoving
|
||||
}
|
||||
decimals={2}
|
||||
value={(stats) => stats.camouflageShootingMoving * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageCaughtOnFire
|
||||
}
|
||||
decimals={2}
|
||||
value={(stats) => stats.camouflageCaughtOnFire * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
stats={stats}
|
||||
value="width"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
stats={stats}
|
||||
value="height"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
stats={stats}
|
||||
value="length"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
stats={stats}
|
||||
value="volume"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { Box, Card, Code, Flex, Slider, Tabs, Text } from '@radix-ui/themes';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Var } from '../../../../../../core/radix/var';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import type { VisualizerProps } from './HullTraverseVisualizer';
|
||||
import type { StatsAcceptorProps } from './HullTraverseVisualizer';
|
||||
|
||||
const maps = [
|
||||
{
|
||||
@@ -19,7 +19,7 @@ const maps = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function ViewRangeVisualizer({ stats }: VisualizerProps) {
|
||||
export function ViewRangeVisualizer({ stats }: StatsAcceptorProps) {
|
||||
const [mapIndex, setMapIndex] = useState(0);
|
||||
const [camouflage, setCamouflage] = useState(0);
|
||||
const map = maps[mapIndex];
|
||||
|
||||
+10
-785
@@ -1,41 +1,17 @@
|
||||
import {
|
||||
asset,
|
||||
coefficient,
|
||||
CREW_MEMBER_NAMES,
|
||||
CrewType,
|
||||
isExplosive,
|
||||
resolvePenetrationCoefficient,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { AccessibilityIcon, InfoCircledIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Popover,
|
||||
Slider,
|
||||
Text,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
import { lerp } from 'three/src/math/MathUtils.js';
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import { useEffect } from 'react';
|
||||
import { awaitableEquipmentDefinitions } from '../../../../../core/awaitables/equipmentDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
|
||||
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||
import { useLocale } from '../../../../../hooks/useLocale';
|
||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import { HullTraverseVisualizer } from './components/HullTraverseVisualizer';
|
||||
import { Info } from './components/Info';
|
||||
import { InfoWithDelta } from './components/InfoWithDelta';
|
||||
import { RicochetVisualizer } from './components/RicochetVisualizer';
|
||||
import { ViewRangeVisualizer } from './components/ViewRangeVisualizer';
|
||||
import { Crew } from './components/Crew';
|
||||
import { Firepower } from './components/Firepower';
|
||||
import { Maneuverability } from './components/Maneuverability';
|
||||
import { Survivability } from './components/Survivability';
|
||||
|
||||
const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||
awaitableEquipmentDefinitions,
|
||||
@@ -43,26 +19,9 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||
]);
|
||||
|
||||
export function Characteristics() {
|
||||
const { unwrap, strings } = useLocale();
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
||||
const hasImprovedVentilation = useEquipment(102);
|
||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
||||
const setPenetrationDistanceDebounced = debounce((value: number) => {
|
||||
setPenetrationDistance(value);
|
||||
}, 500);
|
||||
|
||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||
const provisionCrewBonus =
|
||||
provisions.reduce(
|
||||
(total, provision) =>
|
||||
provisionDefinitions.provisions[provision].crew
|
||||
? total + provisionDefinitions.provisions[provision].crew! / 100
|
||||
: total,
|
||||
0,
|
||||
) + (hasImprovedVentilation ? 0.08 : 0);
|
||||
const commanderMastery = 1 + provisionCrewBonus;
|
||||
const consumables = Duel.use((state) => state.protagonist.consumables);
|
||||
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
||||
const equipmentMatrix = Duel.use(
|
||||
@@ -107,24 +66,9 @@ export function Characteristics() {
|
||||
},
|
||||
);
|
||||
|
||||
const hasSupercharger = useEquipment(107);
|
||||
const hasCalibratedShells = useEquipment(103);
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||
const penetrationCoefficient = coefficient([
|
||||
hasCalibratedShells,
|
||||
resolvePenetrationCoefficient(true, shell.type) - 1,
|
||||
]);
|
||||
const penetrationLossOverDistanceCoefficient = coefficient([
|
||||
hasSupercharger,
|
||||
-0.6,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (penetrationDistanceInput.current) {
|
||||
penetrationDistanceInput.current.value = `${penetrationDistance}`;
|
||||
}
|
||||
}, [penetrationDistance]);
|
||||
useEffect(() => {
|
||||
mutateDuel((draft) => {
|
||||
[draft.protagonist.pitch, draft.protagonist.yaw] = applyPitchYawLimits(
|
||||
@@ -147,732 +91,13 @@ export function Characteristics() {
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" gap="8" style={{ flex: 1 }}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex align="center" gap="4">
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.firepower.title}
|
||||
</Heading>
|
||||
|
||||
<Flex>
|
||||
{gun.gun_type!.value.base.shells.map((thisShell, shellIndex) => (
|
||||
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
|
||||
<IconButton
|
||||
color={thisShell.id === shell.id ? undefined : 'gray'}
|
||||
variant="soft"
|
||||
style={{
|
||||
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
shellIndex ===
|
||||
gun.gun_type!.value.base.shells.length - 1
|
||||
? undefined
|
||||
: 0,
|
||||
borderBottomRightRadius:
|
||||
shellIndex ===
|
||||
gun.gun_type!.value.base.shells.length - 1
|
||||
? undefined
|
||||
: 0,
|
||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||
}}
|
||||
onClick={() => {
|
||||
mutateDuel((draft) => {
|
||||
draft.protagonist.shell = thisShell;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={unwrap(thisShell.name)}
|
||||
width={16}
|
||||
height={16}
|
||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values.gunType
|
||||
}
|
||||
>
|
||||
{strings.common.gun_types[gun.gun_type!.$case]}
|
||||
</Info>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="hp / min"
|
||||
value="dpm"
|
||||
/>
|
||||
{gun.gun_type!.$case === 'auto_reloader' && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
indent
|
||||
unit="hp / min"
|
||||
value="dpmEffective"
|
||||
/>
|
||||
)}
|
||||
{gun.gun_type!.$case !== 'regular' && (
|
||||
<InfoWithDelta stats={stats} value="shells" />
|
||||
)}
|
||||
{gun.gun_type!.$case === 'auto_reloader' ? (
|
||||
<>
|
||||
<Info
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.mostOptimalShellIndex
|
||||
}
|
||||
>
|
||||
{stats.mostOptimalShellIndex! + 1}
|
||||
</Info>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.shellReloads
|
||||
}
|
||||
unit="s"
|
||||
/>
|
||||
{stats.shellReloads!.map((reload, index) => (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
key={index}
|
||||
indent
|
||||
name={literals(
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.shell_index,
|
||||
[`${index + 1}`],
|
||||
)}
|
||||
decimals={2}
|
||||
deltaType="lowerIsBetter"
|
||||
noRanking
|
||||
value={() => reload}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="s"
|
||||
deltaType="lowerIsBetter"
|
||||
value="shellReload"
|
||||
/>
|
||||
)}
|
||||
{(gun.gun_type!.$case === 'auto_loader' ||
|
||||
gun.gun_type!.$case === 'auto_reloader') && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={2}
|
||||
unit="s"
|
||||
deltaType="lowerIsBetter"
|
||||
value="intraClip"
|
||||
/>
|
||||
)}
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="mm"
|
||||
value="penetration"
|
||||
/>
|
||||
{typeof shell.penetration !== 'number' && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={0}
|
||||
name={literals(
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.at_distance,
|
||||
[`${penetrationDistance}`],
|
||||
)}
|
||||
noRanking
|
||||
value={() =>
|
||||
lerp(
|
||||
shell.penetration.near,
|
||||
shell.penetration.far,
|
||||
(penetrationDistance *
|
||||
penetrationLossOverDistanceCoefficient) /
|
||||
500,
|
||||
) * penetrationCoefficient
|
||||
}
|
||||
/>
|
||||
<Flex align="center" gap="2" style={{ paddingLeft: 24 }}>
|
||||
<Text color="gray">
|
||||
{
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.at_distance_slider_label
|
||||
}
|
||||
</Text>
|
||||
<Slider
|
||||
variant="classic"
|
||||
key={penetrationDistance}
|
||||
min={0}
|
||||
max={500}
|
||||
style={{ flex: 1 }}
|
||||
defaultValue={[penetrationDistance]}
|
||||
onValueChange={([value]) => {
|
||||
setPenetrationDistanceDebounced(value);
|
||||
|
||||
if (!penetrationDistanceInput.current) return;
|
||||
|
||||
penetrationDistanceInput.current.value = value.toString();
|
||||
}}
|
||||
/>
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
style={{ width: 64 }}
|
||||
ref={penetrationDistanceInput}
|
||||
defaultValue={penetrationDistance}
|
||||
onBlur={() => {
|
||||
setPenetrationDistance(
|
||||
Math.min(
|
||||
parseInt(penetrationDistanceInput.current!.value),
|
||||
500,
|
||||
),
|
||||
);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TextField.Slot side="right">m</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
<InfoWithDelta stats={stats} unit="hp" decimals={0} value="damage" />
|
||||
{gun.gun_type!.$case !== 'regular' && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={0}
|
||||
value="clipDamage"
|
||||
/>
|
||||
)}
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
unit="hp"
|
||||
decimals={0}
|
||||
value="moduleDamage"
|
||||
/>
|
||||
{isExplosive(shell.type) && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
unit="m"
|
||||
noRanking
|
||||
decimals={0}
|
||||
value="explosionRadius"
|
||||
/>
|
||||
)}
|
||||
<InfoWithDelta stats={stats} decimals={0} unit="mm" value="caliber" />
|
||||
|
||||
{!isExplosive(shell.type) && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="°"
|
||||
noRanking
|
||||
value="shellNormalization"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
noRanking
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
unit="°"
|
||||
value="shellRicochet"
|
||||
/>
|
||||
<RicochetVisualizer stats={stats} />
|
||||
</>
|
||||
)}
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
unit="m/s"
|
||||
decimals={0}
|
||||
value="shellVelocity"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
unit="m"
|
||||
decimals={0}
|
||||
value="shellRange"
|
||||
/>
|
||||
<InfoWithDelta stats={stats} decimals={0} value="shellCapacity" />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="s"
|
||||
deltaType="lowerIsBetter"
|
||||
value="aimTime"
|
||||
/>
|
||||
<Info
|
||||
name={literals(
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.dispersion_at_distance,
|
||||
['100'],
|
||||
)}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={3}
|
||||
indent
|
||||
unit="m"
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersion"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
prefix="+"
|
||||
decimals={3}
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionMoving"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={3}
|
||||
prefix="+"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionHullTraversing"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={3}
|
||||
prefix="+"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionTurretTraversing"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={3}
|
||||
prefix="+"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionShooting"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
prefix="x "
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionGunDamaged"
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.gun_flexibility
|
||||
}
|
||||
unit="°"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="gunDepression"
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="gunElevation"
|
||||
/>
|
||||
{gunModelDefinition.pitch.front && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="gunFrontalDepression"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="gunFrontalElevation"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{gunModelDefinition.pitch.back && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="gunRearDepression"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="gunRearElevation"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{turretModelDefinition.yaw && (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="azimuthLeft"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="azimuthRight"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex gap="2" align="center">
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.crew.title}
|
||||
</Heading>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton variant="ghost">
|
||||
<InfoCircledIcon />
|
||||
</IconButton>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content>
|
||||
<Flex gap="1" align="center">
|
||||
<AccessibilityIcon />
|
||||
<Text>{strings.website.tools.tankopedia.crew.info}</Text>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Flex>
|
||||
|
||||
<InfoWithDelta value="crewCount" stats={stats} decimals={0} />
|
||||
|
||||
{tank.crew.map((member) => {
|
||||
return (
|
||||
<Fragment key={member.type}>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
key={`${member.type}-root`}
|
||||
name={`${
|
||||
strings.website.tools.tankopedia.crew[
|
||||
CREW_MEMBER_NAMES[member.type]
|
||||
]
|
||||
}${
|
||||
member.count > 1
|
||||
? ` ${literals(strings.common.units.x, [
|
||||
`${member.count}`,
|
||||
])}`
|
||||
: ''
|
||||
}`}
|
||||
unit="%"
|
||||
decimals={0}
|
||||
noRanking
|
||||
value={() =>
|
||||
(member.type === CrewType.COMMANDER
|
||||
? commanderMastery
|
||||
: commanderMastery * 1.1) * 100
|
||||
}
|
||||
/>
|
||||
|
||||
{member.substitute.length > 0 && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
key={`${member.type}-substitute`}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
name={
|
||||
<>
|
||||
<Flex align="center" gap="1" display="inline-flex">
|
||||
<AccessibilityIcon />
|
||||
{member.substitute
|
||||
.map((sub, index) =>
|
||||
index === 0
|
||||
? strings.website.tools.tankopedia.crew[
|
||||
CREW_MEMBER_NAMES[sub]
|
||||
]
|
||||
: sub,
|
||||
)
|
||||
.join(', ')}
|
||||
</Flex>
|
||||
</>
|
||||
}
|
||||
noRanking
|
||||
value={() => commanderMastery * 1.05 * 100}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
<Firepower stats={stats} />
|
||||
<Crew stats={stats} />
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" gap="8" style={{ flex: 1 }}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.maneuverability.title}
|
||||
</Heading>
|
||||
<Info
|
||||
name={strings.website.tools.tankopedia.characteristics.values.speed}
|
||||
unit="kph"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="speedForwards"
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
indent
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="speedBackwards"
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
indent
|
||||
/>
|
||||
<InfoWithDelta stats={stats} decimals={0} value="enginePower" />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
unit="tn"
|
||||
deltaType="lowerIsBetter"
|
||||
value="weight"
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.terrain_coefficients
|
||||
}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="hardTerrainCoefficient"
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
value="mediumTerrainCoefficient"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="softTerrainCoefficient"
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.raw_terrain_coefficients
|
||||
}
|
||||
deltaType="lowerIsBetter"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="hardTerrainCoefficientRaw"
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="%"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="%"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="mediumTerrainCoefficientRaw"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="%"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="softTerrainCoefficientRaw"
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.power_to_weight_ratio
|
||||
}
|
||||
unit="hp/tn"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="powerToWeightRatioHardTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="powerToWeightRatioMediumTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="powerToWeightRatioSoftTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
unit="°/s"
|
||||
decimals={1}
|
||||
value="turretTraverseSpeed"
|
||||
/>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.hull_traverse_speed
|
||||
}
|
||||
unit="°/s"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="hullTraverseHardTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="hullTraverseMediumTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
value="hullTraverseSoftTerrain"
|
||||
/>
|
||||
|
||||
<HullTraverseVisualizer stats={stats} />
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.survivability.title}
|
||||
</Heading>
|
||||
<InfoWithDelta value="health" stats={stats} unit="hp" decimals={0} />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values.fireChance
|
||||
}
|
||||
unit="%"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={0}
|
||||
value={(stats) => stats.fireChance * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="viewRange"
|
||||
stats={stats}
|
||||
unit="m"
|
||||
decimals={0}
|
||||
/>
|
||||
<ViewRangeVisualizer stats={stats} />
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values.camouflage
|
||||
}
|
||||
unit="%"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value={(stats) => stats.camouflageStill * 100}
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageStill
|
||||
}
|
||||
decimals={2}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value={(stats) => stats.camouflageMoving * 100}
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageMoving
|
||||
}
|
||||
decimals={2}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageShootingStill
|
||||
}
|
||||
decimals={2}
|
||||
value={(stats) => stats.camouflageShootingStill * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageShootingMoving
|
||||
}
|
||||
decimals={2}
|
||||
value={(stats) => stats.camouflageShootingMoving * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageCaughtOnFire
|
||||
}
|
||||
decimals={2}
|
||||
value={(stats) => stats.camouflageCaughtOnFire * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
stats={stats}
|
||||
value="width"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
stats={stats}
|
||||
value="height"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
stats={stats}
|
||||
value="length"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
stats={stats}
|
||||
value="volume"
|
||||
/>
|
||||
</Flex>
|
||||
<Maneuverability stats={stats} />
|
||||
<Survivability stats={stats} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user