mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
Merge branch 'main' into dev
This commit is contained in:
commit
d6ed395716
24 files changed
+654
-355
No files matched your search
Vendored
+1
@@ -91,6 +91,7 @@
|
|||||||
"malum",
|
"malum",
|
||||||
"mantlets",
|
"mantlets",
|
||||||
"metallicness",
|
"metallicness",
|
||||||
|
"metalness",
|
||||||
"minitelrose",
|
"minitelrose",
|
||||||
"Minotauro",
|
"Minotauro",
|
||||||
"MIPMAPCOUNT",
|
"MIPMAPCOUNT",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user