Merge branch 'main' into dev

This commit is contained in:
TrèsAbhi committed 2024-07-01 20:31:30 -05:00
commit d6ed395716
24 files changed
+654 -355

No files matched your search

+1
View File
@@ -91,6 +91,7 @@
"malum", "malum",
"mantlets", "mantlets",
"metallicness", "metallicness",
"metalness",
"minitelrose", "minitelrose",
"Minotauro", "Minotauro",
"MIPMAPCOUNT", "MIPMAPCOUNT",
+28
View File
@@ -0,0 +1,28 @@
# BlitzKit 1.20.5
Some QOL changes incoming!
## Changes
- The penetration of the shooter is now displayed above the shell switcher
- The shot display card is now shown below the tank instead of to the side on narrow screens
- Added a few more stats to Compare and Tankopedia
- Max dispersion
- Clipping potential for auto-loaders and auto-reloaders
- Raw terrain resistance coefficients
- Renamed the "Fire" section to "Firepower"
- The shell selector for the protagonist tank has been moved to the Firepower section's title
- The table layout of Compare has been compacted and indented in places
- The tank class and type has now displayed in the hero section of the Tankopedia
- Removed the default tier X filter
- Added draggable tracks
- Added the ability to sort by premium shell DPM
## Fixes
- Clicking enter no longer changes the tank you're viewing when selecting the shooter tank
- The open in new tab button in Compare now opens in a new tab
- HEAT shells no longer display ricochet angle
- The gun depression is now properly reported for the Minotauro line
- Stopped autofocus of the ghost button in the login card
- Patreon login now handles bad responses
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "blitzkit", "name": "blitzkit",
"version": "1.20.4", "version": "1.20.5",
"description": "🎉 Tools for everything World of Tanks Blitz", "description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts", "main": "src/server.ts",
"type": "module", "type": "module",
@@ -66,7 +66,7 @@ export function TankControl({ index, id }: TankControlProps) {
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>
<Link href={`/tools/tankopedia/${id}`}> <Link href={`/tools/tankopedia/${id}`} target="_blank">
<IconButton variant="ghost"> <IconButton variant="ghost">
<ExternalLinkIcon /> <ExternalLinkIcon />
</IconButton> </IconButton>
+145 -73
View File
@@ -178,6 +178,7 @@ export default function Page() {
deltaType = 'higherIsBetter', deltaType = 'higherIsBetter',
decimals, decimals,
deltaNominalDisplay, deltaNominalDisplay,
indent,
}: { }: {
name: string; name: string;
value: value:
@@ -191,6 +192,7 @@ export default function Page() {
deltaType?: 'higherIsBetter' | 'lowerIsBetter'; deltaType?: 'higherIsBetter' | 'lowerIsBetter';
decimals?: number; decimals?: number;
deltaNominalDisplay?: (delta: number) => string | number; deltaNominalDisplay?: (delta: number) => string | number;
indent?: boolean;
}) { }) {
const values = stats.map((stat) => const values = stats.map((stat) =>
typeof value === 'function' ? value(stat)! : (stat[value] as number), typeof value === 'function' ? value(stat)! : (stat[value] as number),
@@ -199,7 +201,11 @@ export default function Page() {
return ( return (
<Table.Row> <Table.Row>
<Table.RowHeaderCell> <Table.RowHeaderCell>
<Flex align="center" gap="2"> <Flex
align="center"
style={{ whiteSpace: 'nowrap' }}
gap={indent ? '6' : '2'}
>
<IconButton <IconButton
color={sorting?.by === name ? undefined : 'gray'} color={sorting?.by === name ? undefined : 'gray'}
variant="ghost" variant="ghost"
@@ -620,7 +626,7 @@ export default function Page() {
{members.length > 0 && ( {members.length > 0 && (
<Flex justify="center"> <Flex justify="center">
<Box maxWidth="100%" position="relative"> <Box maxWidth="100%" position="relative">
<Table.Root variant="surface" style={{ maxWidth: '100%' }}> <Table.Root size="1" variant="surface" style={{ maxWidth: '100%' }}>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.ColumnHeaderCell <Table.ColumnHeaderCell
@@ -1160,49 +1166,6 @@ export default function Page() {
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>
</Flex> </Flex>
<Flex mt="1">
{gun.shells.map((thisShell, shellIndex) => (
<IconButton
color={
thisShell.id === shell.id
? undefined
: 'gray'
}
variant="soft"
key={thisShell.id}
style={{
borderTopLeftRadius:
shellIndex === 0 ? undefined : 0,
borderBottomLeftRadius:
shellIndex === 0 ? undefined : 0,
borderTopRightRadius:
shellIndex === gun.shells.length - 1
? undefined
: 0,
borderBottomRightRadius:
shellIndex === gun.shells.length - 1
? undefined
: 0,
marginLeft: shellIndex === 0 ? 0 : -1,
}}
onClick={() => {
mutateCompareTemporary((draft) => {
draft.members[index].shell = thisShell;
});
}}
>
<img
alt={thisShell.name}
width={16}
height={16}
src={asset(
`icons/shells/${thisShell.icon}.webp`,
)}
/>
</IconButton>
))}
</Flex>
</Flex> </Flex>
</Table.Cell> </Table.Cell>
); );
@@ -1211,7 +1174,62 @@ export default function Page() {
</Table.Row> </Table.Row>
</Table.Body> </Table.Body>
<Title>Fire</Title> <Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>
<Flex height="100%" align="center">
<Heading size="4">Firepower</Heading>
</Flex>
</Table.ColumnHeaderCell>
{members.map(({ gun, shell, key }, index) => (
<Table.ColumnHeaderCell key={key}>
<Flex justify="center">
<Flex>
{gun.shells.map((thisShell, shellIndex) => (
<IconButton
color={
thisShell.id === shell.id ? undefined : 'gray'
}
variant="soft"
key={thisShell.id}
style={{
borderTopLeftRadius:
shellIndex === 0 ? undefined : 0,
borderBottomLeftRadius:
shellIndex === 0 ? undefined : 0,
borderTopRightRadius:
shellIndex === gun.shells.length - 1
? undefined
: 0,
borderBottomRightRadius:
shellIndex === gun.shells.length - 1
? undefined
: 0,
marginLeft: shellIndex === 0 ? 0 : -1,
}}
onClick={() => {
mutateCompareTemporary((draft) => {
draft.members[index].shell = thisShell;
});
}}
>
<img
alt={thisShell.name}
width={16}
height={16}
src={asset(
`icons/shells/${thisShell.icon}.webp`,
)}
/>
</IconButton>
))}
</Flex>
</Flex>
</Table.ColumnHeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body> <Table.Body>
<Row name="DPM" value="dpm" decimals={0} /> <Row name="DPM" value="dpm" decimals={0} />
<Row <Row
@@ -1243,6 +1261,14 @@ export default function Page() {
)} )}
<Row name="Penetration" value="penetration" decimals={0} /> <Row name="Penetration" value="penetration" decimals={0} />
<Row name="Damage" value="damage" decimals={0} /> <Row name="Damage" value="damage" decimals={0} />
{hasNonRegularGun && (
<Row
name="clipping potential"
indent
value="clipDamage"
decimals={0}
/>
)}
<Row name="Module damage" value="moduleDamage" decimals={0} /> <Row name="Module damage" value="moduleDamage" decimals={0} />
<Row name="Caliber" value="caliber" decimals={0} /> <Row name="Caliber" value="caliber" decimals={0} />
<Row <Row
@@ -1270,61 +1296,98 @@ export default function Page() {
decimals={3} decimals={3}
/> />
<Row <Row
name="Dispersion moving" name="moving"
indent
value="dispersionMoving" value="dispersionMoving"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
decimals={3} decimals={3}
/> />
<Row <Row
name="Dispersion hull traversing" name="hull traversing"
indent
value="dispersionHullTraversing" value="dispersionHullTraversing"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
decimals={3} decimals={3}
/> />
<Row <Row
name="Dispersion turret traversing" name="turret traversing"
indent
value="dispersionTurretTraversing" value="dispersionTurretTraversing"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
decimals={3} decimals={3}
/> />
<Row <Row
name="Dispersion shooting" name="shooting"
indent
value="dispersionShooting" value="dispersionShooting"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
decimals={3} decimals={3}
/> />
<Row <Row
name="Dispersion gun damaged" name="gun damaged"
indent
value="dispersionGunDamaged" value="dispersionGunDamaged"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
decimals={3} decimals={3}
/> />
<Row
name="maximum"
indent
value="maxDispersion"
deltaType="lowerIsBetter"
decimals={3}
/>
<Row name="Gun depression" value="gunDepression" decimals={1} /> <Row name="Gun depression" value="gunDepression" decimals={1} />
<Row name="Gun elevation" value="gunElevation" decimals={1} /> <Row
name="elevation"
indent
value="gunElevation"
decimals={1}
/>
</Table.Body> </Table.Body>
<Title>Maneuverability</Title> <Title>Maneuverability</Title>
<Table.Body> <Table.Body>
<Row name="Speed forwards" value="speedForwards" decimals={0} /> <Row name="Speed forwards" value="speedForwards" decimals={0} />
<Row <Row
name="Speed backwards" name="backwards"
indent
value="speedBackwards" value="speedBackwards"
decimals={0} decimals={0}
/> />
<Row name="Engine power" value="enginePower" decimals={0} /> <Row name="Engine power" value="enginePower" decimals={0} />
<Row name="Weight" value="weight" decimals={1} /> <Row name="Weight" value="weight" decimals={1} />
<Row <Row
name="Power to weight ratio on hard terrain" name="Terrain coefficient"
value="hardTerrainCoefficient"
decimals={1}
/>
<Row
name="medium terrain"
indent
value="mediumTerrainCoefficient"
decimals={1}
/>
<Row
name="soft terrain"
indent
value="softTerrainCoefficient"
decimals={1}
/>
<Row
name="Power to weight ratio"
value="powerToWeightRatioHardTerrain" value="powerToWeightRatioHardTerrain"
decimals={1} decimals={1}
/> />
<Row <Row
name="Power to weight ratio on medium terrain" name="medium terrain"
indent
value="powerToWeightRatioMediumTerrain" value="powerToWeightRatioMediumTerrain"
decimals={1} decimals={1}
/> />
<Row <Row
name="Power to weight ratio on soft terrain" name="soft terrain"
indent
value="powerToWeightRatioSoftTerrain" value="powerToWeightRatioSoftTerrain"
decimals={1} decimals={1}
/> />
@@ -1334,17 +1397,19 @@ export default function Page() {
decimals={1} decimals={1}
/> />
<Row <Row
name="Traverse speed on hard terrain" name="Traverse speed"
value="hullTraverseHardTerrain" value="hullTraverseHardTerrain"
decimals={1} decimals={1}
/> />
<Row <Row
name="Traverse speed on medium terrain" name="medium terrain"
indent
value="hullTraverseMediumTerrain" value="hullTraverseMediumTerrain"
decimals={1} decimals={1}
/> />
<Row <Row
name="Traverse speed on soft terrain" name="soft terrain"
indent
value="hullTraverseSoftTerrain" value="hullTraverseSoftTerrain"
decimals={1} decimals={1}
/> />
@@ -1362,19 +1427,21 @@ export default function Page() {
/> />
<Row name="View range" value="viewRange" decimals={0} /> <Row name="View range" value="viewRange" decimals={0} />
<Row <Row
name="Camouflage still" name="Camouflage"
value="camouflageStill" value="camouflageStill"
display={(stats) => (stats.camouflageStill * 100).toFixed(0)} display={(stats) => (stats.camouflageStill * 100).toFixed(0)}
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)} deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Camouflage moving" name="moving"
indent
value="camouflageMoving" value="camouflageMoving"
display={(stats) => (stats.camouflageMoving * 100).toFixed(0)} display={(stats) => (stats.camouflageMoving * 100).toFixed(0)}
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)} deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Camouflage shooting still" name="shooting"
indent
value="camouflageShootingStill" value="camouflageShootingStill"
display={(stats) => display={(stats) =>
(stats.camouflageShootingStill * 100).toFixed(0) (stats.camouflageShootingStill * 100).toFixed(0)
@@ -1382,7 +1449,8 @@ export default function Page() {
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)} deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Camouflage shooting moving" name="shooting & moving"
indent
value="camouflageShootingMoving" value="camouflageShootingMoving"
display={(stats) => display={(stats) =>
(stats.camouflageShootingMoving * 100).toFixed(0) (stats.camouflageShootingMoving * 100).toFixed(0)
@@ -1390,7 +1458,8 @@ export default function Page() {
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)} deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Camouflage caught on fire" name="caught on fire"
indent
value="camouflageCaughtOnFire" value="camouflageCaughtOnFire"
display={(stats) => display={(stats) =>
(stats.camouflageCaughtOnFire * 100).toFixed(0) (stats.camouflageCaughtOnFire * 100).toFixed(0)
@@ -1398,29 +1467,32 @@ export default function Page() {
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)} deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Width" name="Volume"
value="volume"
deltaType="lowerIsBetter"
decimals={1}
/>
<Row
name="width"
indent
value="width" value="width"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
decimals={1} decimals={1}
/> />
<Row <Row
name="Height" name="height"
indent
value="height" value="height"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
decimals={1} decimals={1}
/> />
<Row <Row
name="Length" name="length"
indent
value="length" value="length"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
decimals={1} decimals={1}
/> />
<Row
name="Volume"
value="volume"
deltaType="lowerIsBetter"
decimals={1}
/>
</Table.Body> </Table.Body>
</Table.Root> </Table.Root>
@@ -139,7 +139,7 @@ export function Characteristics() {
<Flex direction="column" gap="8" style={{ flex: 1 }}> <Flex direction="column" gap="8" style={{ flex: 1 }}>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Flex align="center" gap="4"> <Flex align="center" gap="4">
<Heading size="5">Fire</Heading> <Heading size="5">Firepower</Heading>
<Flex> <Flex>
{gun.shells.map((thisShell, shellIndex) => ( {gun.shells.map((thisShell, shellIndex) => (
@@ -297,6 +297,11 @@ export function Characteristics() {
<InfoWithDelta name="Damage" unit="hp" decimals={0}> <InfoWithDelta name="Damage" unit="hp" decimals={0}>
{stats.damage} {stats.damage}
</InfoWithDelta> </InfoWithDelta>
{gun.type !== 'regular' && (
<InfoWithDelta name="Clipping potential" indent>
{stats.clipDamage!}
</InfoWithDelta>
)}
<InfoWithDelta name="Module damage" unit="hp" decimals={0}> <InfoWithDelta name="Module damage" unit="hp" decimals={0}>
{stats.moduleDamage} {stats.moduleDamage}
</InfoWithDelta> </InfoWithDelta>
@@ -388,6 +393,15 @@ export function Characteristics() {
> >
{stats.dispersionGunDamaged} {stats.dispersionGunDamaged}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta
decimals={3}
indent
name="Maximum"
unit="m"
deltaType="lowerIsBetter"
>
{stats.maxDispersion}
</InfoWithDelta>
<Info name="Gun flexibility" unit="°" /> <Info name="Gun flexibility" unit="°" />
<InfoWithDelta decimals={1} indent name="Depression"> <InfoWithDelta decimals={1} indent name="Depression">
{stats.gunDepression} {stats.gunDepression}
@@ -520,6 +534,16 @@ export function Characteristics() {
> >
{stats.weight} {stats.weight}
</InfoWithDelta> </InfoWithDelta>
<Info name="Terrain coefficients" />
<InfoWithDelta decimals={1} indent name="Hard">
{stats.hardTerrainCoefficient}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Medium">
{stats.mediumTerrainCoefficient}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Soft">
{stats.softTerrainCoefficient}
</InfoWithDelta>
<Info name="Power to weight ratio" unit="hp/tn" /> <Info name="Power to weight ratio" unit="hp/tn" />
<InfoWithDelta decimals={1} indent name="On hard terrain"> <InfoWithDelta decimals={1} indent name="On hard terrain">
{stats.powerToWeightRatioHardTerrain} {stats.powerToWeightRatioHardTerrain}
@@ -9,7 +9,7 @@ import { modelDefinitions } from '../../../../../core/blitzkit/modelDefinitions'
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose'; import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
import { useAwait } from '../../../../../hooks/useAwait'; import { useAwait } from '../../../../../hooks/useAwait';
import { useDuel } from '../../../../../stores/duel'; import { useDuel } from '../../../../../stores/duel';
import { useTankopediaPersistent } from '../../../../../stores/tankopedia'; import { useTankopediaTemporary } from '../../../../../stores/tankopedia';
const poseDistances: Record<Pose, number> = { const poseDistances: Record<Pose, number> = {
[Pose.HullDown]: 15, [Pose.HullDown]: 15,
@@ -62,8 +62,8 @@ export function Controls() {
}, [camera]); }, [camera]);
useEffect(() => { useEffect(() => {
const unsubscribeTankopedia = useTankopediaPersistent.subscribe( const unsubscribeTankopedia = useTankopediaTemporary.subscribe(
(state) => state.model.visual.controlsEnabled, (state) => state.controlsEnabled,
(enabled) => { (enabled) => {
if (orbitControls.current) orbitControls.current.enabled = enabled; if (orbitControls.current) orbitControls.current.enabled = enabled;
}, },
@@ -182,7 +182,7 @@ export function Controls() {
maxDistance={20} maxDistance={20}
minDistance={5} minDistance={5}
ref={orbitControls} ref={orbitControls}
enabled={useTankopediaPersistent.getState().model.visual.controlsEnabled} enabled={useTankopediaTemporary.getState().controlsEnabled}
rotateSpeed={0.25} rotateSpeed={0.25}
enableDamping={false} enableDamping={false}
autoRotate={autoRotate} autoRotate={autoRotate}
@@ -1,6 +1,7 @@
import { ChevronLeftIcon, MixIcon } from '@radix-ui/react-icons'; import { ChevronLeftIcon, MixIcon } from '@radix-ui/react-icons';
import { Button, Flex, Heading, Text } from '@radix-ui/themes'; import { Button, Flex, Heading, Text } from '@radix-ui/themes';
import Link from 'next/link'; import Link from 'next/link';
import { classIcons } from '../../../../../../components/ClassIcon';
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkit/tankDefinitions/constants'; import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkit/tankDefinitions/constants';
import { useFullScreen } from '../../../../../../hooks/useFullScreen'; import { useFullScreen } from '../../../../../../hooks/useFullScreen';
import { useWideFormat } from '../../../../../../hooks/useWideFormat'; import { useWideFormat } from '../../../../../../hooks/useWideFormat';
@@ -11,14 +12,15 @@ import { TankSandbox } from './TankSandbox';
import { Options } from './components/Options'; import { Options } from './components/Options';
export function HeroSection() { export function HeroSection() {
const protagonistTank = useDuel((state) => state.protagonist!.tank); const protagonist = useDuel((state) => state.protagonist!.tank);
const antagonistTank = useDuel((state) => state.antagonist!.tank); const antagonist = useDuel((state) => state.antagonist!.tank);
const wideFormat = useWideFormat(880); const wideFormat = useWideFormat(880);
const compareTanks = const compareTanks =
protagonistTank.id === antagonistTank.id protagonist.id === antagonist.id
? [protagonistTank.id] ? [protagonist.id]
: [protagonistTank.id, antagonistTank.id]; : [protagonist.id, antagonist.id];
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const Icon = classIcons[protagonist.class];
return ( return (
<Flex <Flex
@@ -58,18 +60,26 @@ export function HeroSection() {
<Heading <Heading
size={wideFormat ? '9' : '8'} size={wideFormat ? '9' : '8'}
align={wideFormat ? undefined : 'center'} align={wideFormat ? undefined : 'center'}
color={
protagonist.treeType === 'collector'
? 'blue'
: protagonist.treeType === 'premium'
? 'amber'
: undefined
}
> >
{protagonistTank.nameFull ?? protagonistTank.name} <Icon style={{ width: '0.75em', height: '0.75em' }} />{' '}
{protagonist.nameFull ?? protagonist.name}
</Heading> </Heading>
<Text color="gray"> <Text color="gray">
Tier {TIER_ROMAN_NUMERALS[protagonistTank.tier]}{' '} Tier {TIER_ROMAN_NUMERALS[protagonist.tier]}{' '}
{ {
(strings.common.nations_adjectives as Record<string, string>)[ (strings.common.nations_adjectives as Record<string, string>)[
protagonistTank.nation protagonist.nation
] ]
}{' '} }{' '}
{strings.common.tank_class_short[protagonistTank.class]} {strings.common.tank_class_short[protagonist.class]}
</Text> </Text>
<Flex gap="4" mt="-1"> <Flex gap="4" mt="-1">
@@ -22,6 +22,7 @@ import { useState } from 'react';
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton'; import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
import { SmallTankIcon } from '../../../../../../../components/SmallTankIcon'; import { SmallTankIcon } from '../../../../../../../components/SmallTankIcon';
import { resolveNearPenetration } from '../../../../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../../../../core/blitz/resolveNearPenetration';
import { resolvePenetrationCoefficient } from '../../../../../../../core/blitz/resolvePenetrationCoefficient';
import { asset } from '../../../../../../../core/blitzkit/asset'; import { asset } from '../../../../../../../core/blitzkit/asset';
import { Pose, poseEvent } from '../../../../../../../core/blitzkit/pose'; import { Pose, poseEvent } from '../../../../../../../core/blitzkit/pose';
import { import {
@@ -113,7 +114,17 @@ export function Options() {
top: '50%', top: '50%',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
}} }}
align="end"
> >
<Text color="gray" size={{ initial: '1', sm: '2' }}>
{(
resolvePenetrationCoefficient(
hasCalibratedShells,
antagonistShell.type,
) * resolveNearPenetration(antagonistShell.penetration)
).toFixed(0)}
mm
</Text>
<Flex direction="column"> <Flex direction="column">
{antagonistGun.shells.map((thisShell, shellIndex) => ( {antagonistGun.shells.map((thisShell, shellIndex) => (
<IconButton <IconButton
@@ -157,7 +168,6 @@ export function Options() {
</IconButton> </IconButton>
))} ))}
</Flex> </Flex>
<Flex direction="column" style={{ pointerEvents: 'auto' }}> <Flex direction="column" style={{ pointerEvents: 'auto' }}>
<IconButton <IconButton
color={hasCalibratedShells ? undefined : 'gray'} color={hasCalibratedShells ? undefined : 'gray'}
@@ -1,6 +1,13 @@
import { ThreeEvent, useThree } from '@react-three/fiber'; import { ThreeEvent, useThree } from '@react-three/fiber';
import { memo, useEffect, useRef } from 'react'; import { memo, useEffect, useRef } from 'react';
import { Euler, Group, Vector2, Vector3 } from 'three'; import {
Euler,
Group,
Mesh,
MeshStandardMaterial,
Vector2,
Vector3,
} from 'three';
import { degToRad } from 'three/src/math/MathUtils'; import { degToRad } from 'three/src/math/MathUtils';
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis'; import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis';
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
@@ -15,9 +22,7 @@ import { normalizeAngleRad } from '../../../../../../../core/math/normalizeAngle
import { useAwait } from '../../../../../../../hooks/useAwait'; import { useAwait } from '../../../../../../../hooks/useAwait';
import { useModel } from '../../../../../../../hooks/useModel'; import { useModel } from '../../../../../../../hooks/useModel';
import { mutateDuel, useDuel } from '../../../../../../../stores/duel'; import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
import mutateTankopediaPersistent, { import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia';
mutateTankopediaTemporary,
} from '../../../../../../../stores/tankopedia';
export const TankModel = memo(() => { export const TankModel = memo(() => {
const awaitedModelDefinitions = useAwait(modelDefinitions); const awaitedModelDefinitions = useAwait(modelDefinitions);
@@ -117,11 +122,52 @@ export const TankModel = memo(() => {
const isWheel = node.name.startsWith('chassis_wheel_'); const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_'); const isTrack = node.name.startsWith('chassis_track_');
const isVisible = isHull || isWheel || isTrack; const isVisible = isHull || isWheel || isTrack;
const position = new Vector2();
const delta = new Vector2();
if (!isVisible) return null; if (!isVisible) return null;
function translateTexture(offset: number) {
const mesh = node.children[0] as Mesh;
const material = mesh?.material as MeshStandardMaterial;
if (!material) return;
if (material.map) material.map.offset.y += offset;
if (material.aoMap) material.aoMap.offset.y += offset;
if (material.normalMap) material.normalMap.offset.y += offset;
if (material.roughnessMap) material.roughnessMap.offset.y += offset;
if (material.metalnessMap) material.metalnessMap.offset.y += offset;
}
function onPointerDown(event: ThreeEvent<PointerEvent>) { function onPointerDown(event: ThreeEvent<PointerEvent>) {
if (!isTrack) return;
position.set(event.clientX, event.clientY);
event.stopPropagation(); event.stopPropagation();
mutateTankopediaTemporary((draft) => {
draft.controlsEnabled = false;
});
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
const deltaX = delta.x / window.innerWidth;
const deltaY = delta.y / window.innerHeight;
translateTexture(deltaX + deltaY);
}
function handlePointerUp() {
mutateTankopediaTemporary((draft) => {
draft.controlsEnabled = true;
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
} }
return jsxTree( return jsxTree(
@@ -158,8 +204,8 @@ export const TankModel = memo(() => {
yaw = protagonist.yaw; yaw = protagonist.yaw;
pitch = protagonist.pitch; pitch = protagonist.pitch;
mutateTankopediaPersistent((draft) => { mutateTankopediaTemporary((draft) => {
draft.model.visual.controlsEnabled = false; draft.controlsEnabled = false;
}); });
mutateTankopediaTemporary((draft) => { mutateTankopediaTemporary((draft) => {
draft.shot = undefined; draft.shot = undefined;
@@ -194,8 +240,8 @@ export const TankModel = memo(() => {
draft.protagonist!.pitch = normalizeAngleRad(pitch); draft.protagonist!.pitch = normalizeAngleRad(pitch);
draft.protagonist!.yaw = normalizeAngleRad(yaw); draft.protagonist!.yaw = normalizeAngleRad(yaw);
}); });
mutateTankopediaPersistent((draft) => { mutateTankopediaTemporary((draft) => {
draft.model.visual.controlsEnabled = true; draft.controlsEnabled = true;
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointerup', handlePointerUp);
@@ -234,8 +280,8 @@ export const TankModel = memo(() => {
function onPointerDown(event: ThreeEvent<PointerEvent>) { function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation(); event.stopPropagation();
mutateTankopediaPersistent((draft) => { mutateTankopediaTemporary((draft) => {
draft.model.visual.controlsEnabled = false; draft.controlsEnabled = false;
}); });
mutateTankopediaTemporary((draft) => { mutateTankopediaTemporary((draft) => {
draft.shot = undefined; draft.shot = undefined;
@@ -269,8 +315,8 @@ export const TankModel = memo(() => {
modelTransformEvent.emit({ pitch, yaw }); modelTransformEvent.emit({ pitch, yaw });
} }
function handlePointerUp() { function handlePointerUp() {
mutateTankopediaPersistent((draft) => { mutateTankopediaTemporary((draft) => {
draft.model.visual.controlsEnabled = true; draft.controlsEnabled = true;
}); });
mutateDuel((draft) => { mutateDuel((draft) => {
draft.protagonist!.pitch = normalizeAngleRad(pitch); draft.protagonist!.pitch = normalizeAngleRad(pitch);
@@ -38,7 +38,9 @@ export function RotationInputs() {
function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) { function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) {
if (!pitchInput.current || !yawInput.current) return; if (!pitchInput.current || !yawInput.current) return;
pitchInput.current.value = (-radToDeg(pitch)).toFixed(1); pitchInput.current.value = (
-radToDeg(pitch) + (tankModelDefinition.turretRotation?.pitch ?? 0)
).toFixed(1);
if (yaw === undefined) return; if (yaw === undefined) return;
@@ -0,0 +1,16 @@
import { Flex } from '@radix-ui/themes';
import { ShotDisplayCard } from '../../../../../components/Armor/components/ShotDisplayCard';
export function ShotDisplaySection() {
return (
<Flex
justify="center"
display={{
initial: 'flex',
sm: 'none',
}}
>
<ShotDisplayCard />
</Flex>
);
}
+2
View File
@@ -11,6 +11,7 @@ import { HistorySection } from './components/HistorySection';
import { CharacteristicsSection } from './components/Model/CharacteristicsSection'; import { CharacteristicsSection } from './components/Model/CharacteristicsSection';
import { HeroSection } from './components/Model/HeroSection'; import { HeroSection } from './components/Model/HeroSection';
import { TechTreeSection } from './components/Model/TechTreeSection'; import { TechTreeSection } from './components/Model/TechTreeSection';
import { ShotDisplaySection } from './components/ShotDisplaySection';
import { VideoSection } from './components/VideoSection'; import { VideoSection } from './components/VideoSection';
export default function Page({ params }: { params: { id: string } }) { export default function Page({ params }: { params: { id: string } }) {
@@ -77,6 +78,7 @@ export default function Page({ params }: { params: { id: string } }) {
{assigned && ( {assigned && (
<> <>
<HeroSection /> <HeroSection />
<ShotDisplaySection />
{!exempt && <AdMidSectionResponsive />} {!exempt && <AdMidSectionResponsive />}
<CharacteristicsSection /> <CharacteristicsSection />
{!exempt && <AdMidSectionResponsive />} {!exempt && <AdMidSectionResponsive />}
@@ -10,9 +10,10 @@ import { Sort } from './Sort';
interface SearchBarProps { interface SearchBarProps {
topResult?: TankDefinition; topResult?: TankDefinition;
onSelect?: (tank: TankDefinition) => void;
} }
export function SearchBar({ topResult }: SearchBarProps) { export function SearchBar({ topResult, onSelect }: SearchBarProps) {
const router = useRouter(); const router = useRouter();
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
const searching = useTankopediaFilters((state) => state.searching); const searching = useTankopediaFilters((state) => state.searching);
@@ -42,7 +43,12 @@ export function SearchBar({ topResult }: SearchBarProps) {
if (event.key !== 'Enter' || !topResult || searching) return; if (event.key !== 'Enter' || !topResult || searching) return;
event.preventDefault(); event.preventDefault();
router.push(`/tools/tankopedia/${topResult.id}`);
if (onSelect) {
onSelect(topResult);
} else {
router.push(`/tools/tankopedia/${topResult.id}`);
}
}, },
[topResult], [topResult],
); );
+11 -1
View File
@@ -43,7 +43,17 @@ export function Sort() {
}); });
}} }}
> >
DPM Standard DPM
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={sort.by === 'fire.dpmPremium'}
onClick={() => {
mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.dpmPremium';
});
}}
>
Premium DPM
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.reload'} checked={sort.by === 'fire.reload'}
@@ -51,6 +51,8 @@ export function TankCard({ tank, onSelect }: TankCardProps) {
return gun.dispersion.base.toFixed(3); return gun.dispersion.base.toFixed(3);
case 'fire.dpm': case 'fire.dpm':
return resolveDpm(gun, shell0).toFixed(0); return resolveDpm(gun, shell0).toFixed(0);
case 'fire.dpmPremium':
return shell1 ? resolveDpm(gun, shell1).toFixed(0) : '--';
case 'fire.reload': case 'fire.reload':
return resolveReload(gun).toFixed(2); return resolveReload(gun).toFixed(2);
case 'fire.standardPenetration': case 'fire.standardPenetration':
@@ -69,7 +69,8 @@ export const TankSearch = memo<TankSearchProps>(
(a, b) => (a, b) =>
awaitedGameDefinitions.nations.indexOf(b.nation) - awaitedGameDefinitions.nations.indexOf(b.nation) -
awaitedGameDefinitions.nations.indexOf(a.nation), awaitedGameDefinitions.nations.indexOf(a.nation),
); )
.sort((a, b) => a.tier - b.tier);
break; break;
case 'survivability.health': { case 'survivability.health': {
@@ -179,6 +180,24 @@ export const TankSearch = memo<TankSearchProps>(
); );
break; break;
case 'fire.dpmPremium':
sorted = filtered.sort(
(a, b) =>
(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],
)
: 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],
)
: 0),
);
break;
case 'fire.reload': case 'fire.reload':
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
@@ -360,7 +379,7 @@ export const TankSearch = memo<TankSearchProps>(
return ( return (
<Flex direction="column" gap="4" flexGrow="1" {...props}> <Flex direction="column" gap="4" flexGrow="1" {...props}>
<SearchBar topResult={tanks?.[0]} /> <SearchBar topResult={tanks?.[0]} onSelect={onSelect} />
{!filters.search && !filters.searching && ( {!filters.search && !filters.searching && (
<FilterControl compact={compact} /> <FilterControl compact={compact} />
+15 -227
View File
@@ -1,39 +1,17 @@
import { Card, Flex, Inset, Separator, Text } from '@radix-ui/themes'; import { Box, Flex, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei'; import { Html } from '@react-three/drei';
import { useFrame } from '@react-three/fiber'; import { useFrame } from '@react-three/fiber';
import { ComponentProps, useRef } from 'react'; import { ComponentProps, useRef } from 'react';
import { Euler, Mesh, Quaternion } from 'three'; import { Euler, Mesh, Quaternion } from 'three';
import { radToDeg } from 'three/src/math/MathUtils';
import { J_HAT } from '../../../constants/axis'; import { J_HAT } from '../../../constants/axis';
import { import {
ShotLayer,
ShotLayerNonExternal, ShotLayerNonExternal,
ShotStatus, ShotStatus,
useTankopediaTemporary, useTankopediaTemporary,
} from '../../../stores/tankopedia'; } from '../../../stores/tankopedia';
import { ArmorType } from './SpacedArmorScene'; import { ShotDisplayCard } from './ShotDisplayCard';
import { ExternalModuleVariant } from './SpacedArmorSceneComponent';
const layerTypeNames: Record<ArmorType | 'null', string> = { export const shotStatusColors: Record<
[ArmorType.Core]: 'Core',
[ArmorType.Spaced]: 'Spaced',
[ArmorType.External]: 'External',
null: 'Gap',
};
const externalLayerNames: Record<ExternalModuleVariant, string> = {
gun: 'Gun',
track: 'Tracks',
};
const shotStatusNames: Record<ShotStatus, string> = {
blocked: 'Blocked',
ricochet: 'Ricochet',
penetration: 'penetration',
splash: 'splash',
};
const shotStatusColors: Record<
ShotStatus, ShotStatus,
ComponentProps<typeof Text>['color'] ComponentProps<typeof Text>['color']
> = { > = {
@@ -44,93 +22,10 @@ const shotStatusColors: Record<
}; };
const SURFACE_POINT_SIZE = 12; const SURFACE_POINT_SIZE = 12;
const LENGTH_INFINITY = 4; export const SHOT_DISPLAY_LENGTH_INFINITY = 4;
const TRACER_THICK = 1 / 32; const TRACER_THICK = 1 / 32;
const TRACER_THIN = TRACER_THICK / 2; const TRACER_THIN = TRACER_THICK / 2;
function LayerEntry({
layerIndex,
shotStatusColor,
layerName,
layer,
angle,
}: {
layerIndex?: string;
shotStatusColor: ComponentProps<typeof Text>['color'];
layerName: string;
layer: ShotLayer;
angle?: number;
}) {
return (
<Flex align="center" gap="2">
<Text
size="1"
color="gray"
style={{
width: 12,
}}
>
{layerIndex}
</Text>
<Text size="2" color={shotStatusColor} style={{ width: 47 }}>
{layerName}
</Text>
{layer.type === null && (
<>
<Text
size="2"
color={shotStatusColor}
style={{
width: 64,
}}
>
{Math.round(layer.distance * 1000).toLocaleString()}
<Text size="1">mm</Text>
</Text>
<Text size="2" color="gray">
{Math.max(-100, Math.round(-50 * layer.distance))}% penetration
</Text>
</>
)}
{layer.type === ArmorType.External && (
<Text
size="2"
color={shotStatusColor}
style={{
width: 64,
}}
>
{Math.round(layer.thickness).toLocaleString()}
<Text size="1">mm</Text>
</Text>
)}
{(layer.type === ArmorType.Core || layer.type === ArmorType.Spaced) && (
<>
<Text
size="2"
color={shotStatusColor}
style={{
width: 80,
}}
>
{Math.round(layer.thicknessAngled).toLocaleString()}
<Text size="1">{`mm${angle === undefined ? '' : ` ${Math.round(radToDeg(angle))}°`}`}</Text>
</Text>
<Text size="2" color="gray">
{Math.round(layer.thickness).toLocaleString()}
<Text size="1">mm</Text> nominal
</Text>
</>
)}
</Flex>
);
}
export function ShotDisplay() { export function ShotDisplay() {
const shot = useTankopediaTemporary((state) => state.shot); const shot = useTankopediaTemporary((state) => state.shot);
const inTracer = useRef<Mesh>(null); const inTracer = useRef<Mesh>(null);
@@ -157,13 +52,13 @@ export function ShotDisplay() {
shot.in.layers.reduce((accumulator, layer) => { shot.in.layers.reduce((accumulator, layer) => {
if (layer.type === null) return accumulator + layer.distance; if (layer.type === null) return accumulator + layer.distance;
return accumulator; return accumulator;
}, 0) + LENGTH_INFINITY; }, 0) + SHOT_DISPLAY_LENGTH_INFINITY;
const outLength = const outLength =
shot.out && shot.out.layers.length > 0 shot.out && shot.out.layers.length > 0
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo( ? (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,
) )
: LENGTH_INFINITY; : SHOT_DISPLAY_LENGTH_INFINITY;
const inLast = shot.in.layers.findLast( const inLast = shot.in.layers.findLast(
(layer) => layer.type !== null, (layer) => layer.type !== null,
) as ShotLayerNonExternal; ) as ShotLayerNonExternal;
@@ -181,122 +76,15 @@ export function ShotDisplay() {
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
}} }}
> >
<Card ml="9" style={{ width: 300 }}> <Box
<Flex direction="column" gap="2"> pl="9"
<Flex direction="column" gap="1"> display={{
<Text color={inTitleColor} weight="bold"> initial: 'none',
{shot.in.status !== 'blocked' && sm: 'block',
shot.in.status !== 'ricochet' && ( }}
<> >
{Math.round(shot.damage).toLocaleString()} <ShotDisplayCard />
<Text size="1">hp</Text> </Box>
</>
)}
{` ${shotStatusNames[shot.in.status]} `}
</Text>
<Flex direction="column">
{shot.in.layers.map((layer, index) => {
const layerName =
layer.type === ArmorType.External
? externalLayerNames[layer.variant]
: layerTypeNames[`${layer.type}`];
const layerIndex =
layer.type === null
? undefined
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
const shotStatusColor =
shot.in.status === 'splash'
? shotStatusColors.splash
: shotStatusColors[layer.status];
return (
<LayerEntry
key={index}
layer={layer}
layerIndex={layerIndex}
layerName={layerName}
shotStatusColor={shotStatusColor}
angle={
layer.type === ArmorType.Core ||
layer.type === ArmorType.Spaced
? layer.angle
: undefined
}
/>
);
})}
</Flex>
</Flex>
{shot.out && shot.out.layers.length > 0 && (
<>
<Inset side="x">
<Flex align="center" gap="2">
<Separator
style={{
flex: 1,
}}
/>
<Text size="1" color="gray">
ricochet (-25% penetration)
</Text>
<Separator
style={{
flex: 1,
}}
/>
</Flex>
</Inset>
<Text color={outTitleColor} weight="bold">
{shot.out.status !== 'blocked' &&
shot.out.status !== 'ricochet' && (
<>
{Math.round(shot.damage).toLocaleString()}
<Text size="1">hp</Text>
</>
)}
{` ${shotStatusNames[shot.out.status]} `}
</Text>
<Flex direction="column">
{shot.out.layers.map((layer, index) => {
const layerName =
layer.type === ArmorType.External
? externalLayerNames[layer.variant]
: layerTypeNames[`${layer.type}`];
const shiftedIndex = index + shot.out!.layers.length;
const layerIndex =
layer.type === null
? undefined
: `${(shot.containsGaps ? shiftedIndex / 2 : shiftedIndex) + 1}.`;
const shotStatusColor =
shot.out!.status === 'splash'
? shotStatusColors.splash
: shotStatusColors[layer.status];
return (
<LayerEntry
key={index}
layer={layer}
layerIndex={layerIndex}
layerName={layerName}
shotStatusColor={shotStatusColor}
angle={
layer.type === ArmorType.Core ||
layer.type === ArmorType.Spaced
? layer.angle
: undefined
}
/>
);
})}
</Flex>
</>
)}
</Flex>
</Card>
</Html> </Html>
{[...shot.in.layers, ...(shot.out?.layers ?? [])].map((layer, index) => { {[...shot.in.layers, ...(shot.out?.layers ?? [])].map((layer, index) => {
@@ -0,0 +1,242 @@
import { Card, Flex, Inset, Separator, Text } from '@radix-ui/themes';
import { ComponentProps } from 'react';
import { radToDeg } from 'three/src/math/MathUtils';
import {
ShotLayer,
ShotStatus,
useTankopediaTemporary,
} from '../../../stores/tankopedia';
import { shotStatusColors } from './ShotDisplay';
import { ArmorType } from './SpacedArmorScene';
import { ExternalModuleVariant } from './SpacedArmorSceneComponent';
const layerTypeNames: Record<ArmorType | 'null', string> = {
[ArmorType.Core]: 'Core',
[ArmorType.Spaced]: 'Spaced',
[ArmorType.External]: 'External',
null: 'Gap',
};
const externalLayerNames: Record<ExternalModuleVariant, string> = {
gun: 'Gun',
track: 'Tracks',
};
const shotStatusNames: Record<ShotStatus, string> = {
blocked: 'Blocked',
ricochet: 'Ricochet',
penetration: 'penetration',
splash: 'splash',
};
function LayerEntry({
layerIndex,
shotStatusColor,
layerName,
layer,
angle,
}: {
layerIndex?: string;
shotStatusColor: ComponentProps<typeof Text>['color'];
layerName: string;
layer: ShotLayer;
angle?: number;
}) {
return (
<Flex align="center" gap="2">
<Text
size="1"
color="gray"
style={{
width: 12,
}}
>
{layerIndex}
</Text>
<Text size="2" color={shotStatusColor} style={{ width: 47 }}>
{layerName}
</Text>
{layer.type === null && (
<>
<Text
size="2"
color={shotStatusColor}
style={{
width: 64,
}}
>
{Math.round(layer.distance * 1000).toLocaleString()}
<Text size="1">mm</Text>
</Text>
<Text size="2" color="gray">
{Math.max(-100, Math.round(-50 * layer.distance))}% penetration
</Text>
</>
)}
{layer.type === ArmorType.External && (
<Text
size="2"
color={shotStatusColor}
style={{
width: 64,
}}
>
{Math.round(layer.thickness).toLocaleString()}
<Text size="1">mm</Text>
</Text>
)}
{(layer.type === ArmorType.Core || layer.type === ArmorType.Spaced) && (
<>
<Text
size="2"
color={shotStatusColor}
style={{
width: 80,
}}
>
{Math.round(layer.thicknessAngled).toLocaleString()}
<Text size="1">{`mm${angle === undefined ? '' : ` ${Math.round(radToDeg(angle))}°`}`}</Text>
</Text>
<Text size="2" color="gray">
{Math.round(layer.thickness).toLocaleString()}
<Text size="1">mm</Text> nominal
</Text>
</>
)}
</Flex>
);
}
export function ShotDisplayCard() {
const shot = useTankopediaTemporary((state) => state.shot);
if (!shot) return null;
const outTitleColor = shot.out
? shotStatusColors[shot.out.status]
: undefined;
const inTitleColor = shotStatusColors[shot.in.status];
return (
<Card style={{ width: 300 }}>
<Flex direction="column" gap="2">
<Flex direction="column" gap="1">
<Text color={inTitleColor} weight="bold">
{shot.in.status !== 'blocked' && shot.in.status !== 'ricochet' && (
<>
{Math.round(shot.damage).toLocaleString()}
<Text size="1">hp</Text>
</>
)}
{` ${shotStatusNames[shot.in.status]} `}
</Text>
<Flex direction="column">
{shot.in.layers.map((layer, index) => {
const layerName =
layer.type === ArmorType.External
? externalLayerNames[layer.variant]
: layerTypeNames[`${layer.type}`];
const layerIndex =
layer.type === null
? undefined
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
const shotStatusColor =
shot.in.status === 'splash'
? shotStatusColors.splash
: shotStatusColors[layer.status];
return (
<LayerEntry
key={index}
layer={layer}
layerIndex={layerIndex}
layerName={layerName}
shotStatusColor={shotStatusColor}
angle={
layer.type === ArmorType.Core ||
layer.type === ArmorType.Spaced
? layer.angle
: undefined
}
/>
);
})}
</Flex>
</Flex>
{shot.out && shot.out.layers.length > 0 && (
<>
<Inset side="x">
<Flex align="center" gap="2">
<Separator
style={{
flex: 1,
}}
/>
<Text size="1" color="gray">
ricochet (-25% penetration)
</Text>
<Separator
style={{
flex: 1,
}}
/>
</Flex>
</Inset>
<Text color={outTitleColor} weight="bold">
{shot.out.status !== 'blocked' &&
shot.out.status !== 'ricochet' && (
<>
{Math.round(shot.damage).toLocaleString()}
<Text size="1">hp</Text>
</>
)}
{` ${shotStatusNames[shot.out.status]} `}
</Text>
<Flex direction="column">
{shot.out.layers.map((layer, index) => {
const layerName =
layer.type === ArmorType.External
? externalLayerNames[layer.variant]
: layerTypeNames[`${layer.type}`];
const shiftedIndex = index + shot.out!.layers.length;
const layerIndex =
layer.type === null
? undefined
: `${(shot.containsGaps ? shiftedIndex / 2 : shiftedIndex) + 1}.`;
const shotStatusColor =
shot.out!.status === 'splash'
? shotStatusColors.splash
: shotStatusColors[layer.status];
return (
<LayerEntry
key={index}
layer={layer}
layerIndex={layerIndex}
layerName={layerName}
shotStatusColor={shotStatusColor}
angle={
layer.type === ArmorType.Core ||
layer.type === ArmorType.Spaced
? layer.angle
: undefined
}
/>
);
})}
</Flex>
</>
)}
</Flex>
</Card>
);
}
+5 -1
View File
@@ -143,7 +143,11 @@ export default function Navbar() {
<PersonIcon style={{ cursor: 'pointer' }} /> <PersonIcon style={{ cursor: 'pointer' }} />
</Popover.Trigger> </Popover.Trigger>
<Popover.Content align="end" width="320px"> <Popover.Content
align="end"
width="320px"
onOpenAutoFocus={(event) => event.preventDefault()}
>
{(logins.patreon || logins.wargaming) && ( {(logins.patreon || logins.wargaming) && (
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Text align="center" color="gray"> <Text align="center" color="gray">
+28 -16
View File
@@ -1,4 +1,5 @@
import { EquipmentMatrix } from '../../stores/duel'; import { EquipmentMatrix } from '../../stores/duel';
import { isExplosive } from '../blitz/isExplosive';
import { resolveNearPenetration } from '../blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../blitz/resolveNearPenetration';
import { resolvePenetrationCoefficient } from '../blitz/resolvePenetrationCoefficient'; import { resolvePenetrationCoefficient } from '../blitz/resolvePenetrationCoefficient';
import { coefficient } from './coefficient'; import { coefficient } from './coefficient';
@@ -359,11 +360,20 @@ export function tankCharacteristics(
const penetration = const penetration =
resolveNearPenetration(shell.penetration) * penetrationCoefficient; resolveNearPenetration(shell.penetration) * penetrationCoefficient;
const damage = shell.damage.armor * damageCoefficient; const damage = shell.damage.armor * damageCoefficient;
const clipDamage = gun.type === 'regular' ? undefined : gun.count * damage;
const moduleDamage = shell.damage.module * damageCoefficient; const moduleDamage = shell.damage.module * damageCoefficient;
const explosionRadius = shell.explosionRadius; const explosionRadius = shell.explosionRadius;
const shellVelocity = shell.speed * shellVelocityCoefficient; const shellVelocity = shell.speed * shellVelocityCoefficient;
const aimTime = gun.aimTime * aimTimeCoefficient; const aimTime = gun.aimTime * aimTimeCoefficient;
const dispersion = gun.dispersion.base * dispersionStillCoefficient; const dispersion = gun.dispersion.base * dispersionStillCoefficient;
const maxDispersion =
gun.dispersion.base *
Math.sqrt(
((gun.dispersion.traverse * turret.traverseSpeed) ** 2 +
(track.dispersion.traverse * track.traverseSpeed) ** 2 +
track.dispersion.move * tank.speed.forwards) **
2,
);
const dispersionMoving = track.dispersion.move * dispersionMovingCoefficient; const dispersionMoving = track.dispersion.move * dispersionMovingCoefficient;
const dispersionHullTraversing = const dispersionHullTraversing =
track.dispersion.traverse * dispersionHullTraverseCoefficient; track.dispersion.traverse * dispersionHullTraverseCoefficient;
@@ -396,40 +406,37 @@ export function tankCharacteristics(
const speedForwards = tank.speed.forwards + speedForwardsSum; const speedForwards = tank.speed.forwards + speedForwardsSum;
const speedBackwards = tank.speed.backwards + speedBackwardsSum; const speedBackwards = tank.speed.backwards + speedBackwardsSum;
const enginePower = resolvedEnginePower; const enginePower = resolvedEnginePower;
const hardTerrainCoefficient =
track.resistance.hard * resistanceHardCoefficient;
const mediumTerrainCoefficient =
track.resistance.medium * resistanceMediumCoefficient;
const softTerrainCoefficient =
track.resistance.soft * resistanceSoftCoefficient;
const powerToWeightRatioHardTerrain = const powerToWeightRatioHardTerrain =
resolvedEnginePower / resolvedEnginePower / weightTons / hardTerrainCoefficient;
weightTons /
(track.resistance.hard * resistanceHardCoefficient);
const powerToWeightRatioMediumTerrain = const powerToWeightRatioMediumTerrain =
resolvedEnginePower / resolvedEnginePower / weightTons / mediumTerrainCoefficient;
weightTons /
(track.resistance.medium * resistanceMediumCoefficient);
const powerToWeightRatioSoftTerrain = const powerToWeightRatioSoftTerrain =
resolvedEnginePower / resolvedEnginePower / weightTons / softTerrainCoefficient;
weightTons /
(track.resistance.soft * resistanceSoftCoefficient);
const weight = weightTons; const weight = weightTons;
const turretTraverseSpeed = turret.traverseSpeed * turretTraverseCoefficient; const turretTraverseSpeed = turret.traverseSpeed * turretTraverseCoefficient;
const hullTraverseHardTerrain = const hullTraverseHardTerrain =
(resolvedEnginePower / stockEngine.power) * (resolvedEnginePower / stockEngine.power) *
track.traverseSpeed * track.traverseSpeed *
hullTraverseCoefficient * hullTraverseCoefficient *
(track.resistance.hard / (track.resistance.hard / hardTerrainCoefficient) *
(track.resistance.hard * resistanceHardCoefficient)) *
(stockWeight / weightKg); (stockWeight / weightKg);
const hullTraverseMediumTerrain = const hullTraverseMediumTerrain =
(resolvedEnginePower / stockEngine.power) * (resolvedEnginePower / stockEngine.power) *
track.traverseSpeed * track.traverseSpeed *
hullTraverseCoefficient * hullTraverseCoefficient *
(track.resistance.hard / (track.resistance.hard / mediumTerrainCoefficient) *
(track.resistance.medium * resistanceMediumCoefficient)) *
(stockWeight / weightKg); (stockWeight / weightKg);
const hullTraverseSoftTerrain = const hullTraverseSoftTerrain =
(resolvedEnginePower / stockEngine.power) * (resolvedEnginePower / stockEngine.power) *
track.traverseSpeed * track.traverseSpeed *
hullTraverseCoefficient * hullTraverseCoefficient *
(track.resistance.hard / (track.resistance.hard / softTerrainCoefficient) *
(track.resistance.soft * resistanceSoftCoefficient)) *
(stockWeight / weightKg); (stockWeight / weightKg);
const health = (tank.health + turret.health) * healthCoefficient; const health = (tank.health + turret.health) * healthCoefficient;
const fireChance = engine.fireChance * fireChanceCoefficient; const fireChance = engine.fireChance * fireChanceCoefficient;
@@ -453,7 +460,7 @@ export function tankCharacteristics(
const length = size[1]; const length = size[1];
const volume = width * height * length; const volume = width * height * length;
const shellNormalization = shell.normalization ?? 0; const shellNormalization = shell.normalization ?? 0;
const shellRicochet = shell.ricochet; const shellRicochet = isExplosive(shell.type) ? undefined : shell.ricochet;
return { return {
shellNormalization, shellNormalization,
@@ -469,11 +476,13 @@ export function tankCharacteristics(
caliber, caliber,
penetration, penetration,
damage, damage,
clipDamage,
moduleDamage, moduleDamage,
explosionRadius, explosionRadius,
shellVelocity, shellVelocity,
aimTime, aimTime,
dispersion, dispersion,
maxDispersion,
dispersionMoving, dispersionMoving,
dispersionHullTraversing, dispersionHullTraversing,
dispersionTurretTraversing, dispersionTurretTraversing,
@@ -489,6 +498,9 @@ export function tankCharacteristics(
azimuthRight, azimuthRight,
speedForwards, speedForwards,
speedBackwards, speedBackwards,
hardTerrainCoefficient,
mediumTerrainCoefficient,
softTerrainCoefficient,
enginePower, enginePower,
powerToWeightRatioHardTerrain, powerToWeightRatioHardTerrain,
powerToWeightRatioMediumTerrain, powerToWeightRatioMediumTerrain,
+3
View File
@@ -15,6 +15,9 @@ export function useAdExempt() {
`/api/patreon/membership/${patreon.token}`, `/api/patreon/membership/${patreon.token}`,
{ cache: 'force-cache' }, { cache: 'force-cache' },
); );
if (!response.ok) return resolve(false);
const json = await response.json(); const json = await response.json();
const exempt = typeof json === 'boolean' && json; const exempt = typeof json === 'boolean' && json;
cache[patreon.token] = exempt; cache[patreon.token] = exempt;
+5 -2
View File
@@ -20,7 +20,6 @@ export interface TankopediaPersistent {
wireframe: boolean; wireframe: boolean;
opaque: boolean; opaque: boolean;
environment: (typeof ENVIRONMENTS)[number]; environment: (typeof ENVIRONMENTS)[number];
controlsEnabled: boolean;
showGrid: boolean; showGrid: boolean;
greenPenetration: boolean; greenPenetration: boolean;
showEnvironment: boolean; showEnvironment: boolean;
@@ -45,6 +44,7 @@ export interface TankopediaPersistent {
interface TankopediaTemporary { interface TankopediaTemporary {
shot?: Shot; shot?: Shot;
skills: Record<string, number>; skills: Record<string, number>;
controlsEnabled: boolean;
} }
export interface ShotLayerBase { export interface ShotLayerBase {
@@ -114,7 +114,8 @@ export const SORT_NAMES = {
'survivability.camouflageShooting': 'camouflage shooting', 'survivability.camouflageShooting': 'camouflage shooting',
'survivability.volume': 'volume', 'survivability.volume': 'volume',
'survivability.length': 'length', 'survivability.length': 'length',
'fire.dpm': 'DPM', 'fire.dpm': 'standard DPM',
'fire.dpmPremium': 'premium DPM',
'fire.reload': 'reload', 'fire.reload': 'reload',
'fire.caliber': 'caliber', 'fire.caliber': 'caliber',
'fire.standardPenetration': 'standard penetration', 'fire.standardPenetration': 'standard penetration',
@@ -143,6 +144,7 @@ export const SORT_UNITS: Record<TankopediaSortBy, string | undefined> = {
'fire.dispersionMoving': 'm', 'fire.dispersionMoving': 'm',
'fire.dispersionStill': 'm', 'fire.dispersionStill': 'm',
'fire.dpm': undefined, 'fire.dpm': undefined,
'fire.dpmPremium': undefined,
'fire.gunDepression': '°', 'fire.gunDepression': '°',
'fire.gunElevation': '°', 'fire.gunElevation': '°',
'fire.premiumPenetration': 'mm', 'fire.premiumPenetration': 'mm',
@@ -199,6 +201,7 @@ export const useTankopediaPersistent = create<TankopediaPersistent>()(
export const useTankopediaTemporary = create<TankopediaTemporary>()( export const useTankopediaTemporary = create<TankopediaTemporary>()(
subscribeWithSelector<TankopediaTemporary>(() => ({ subscribeWithSelector<TankopediaTemporary>(() => ({
skills: {}, skills: {},
controlsEnabled: true,
})), })),
); );
-1
View File
@@ -24,7 +24,6 @@ export const useTankopediaFilters = create<TankopediaFilters>()(() => ({
by: 'meta.none', by: 'meta.none',
direction: 'descending', direction: 'descending',
}, },
tier: 10,
searching: false, searching: false,
})); }));