feat: complete tankoepdia page

This commit is contained in:
TrèsAbhi committed 2024-09-22 22:04:03 -05:00
1 parent 2b5ffb2b84
commit a54ba78dbf
39 files changed
+790 -161

No files matched your search

@@ -65,7 +65,7 @@ export const averageDefinitionsArray = averageDefinitions.then((data) =>
({
id: Number(key),
...(value as AverageDefinitionsEntry),
} satisfies AverageDefinitionsEntryWithId),
}) satisfies AverageDefinitionsEntryWithId,
)
.filter((tank) => tank.mu.battles > 0),
);
+10 -10
View File
@@ -1,14 +1,14 @@
export * from './assertSecret';
export * from './asset';
export * from './availableProvisions';
// export * from './averageDefinitions';
export * from './averageDefinitions';
// export * from './camouflageDefinitions';
// export * from './checkConsumableProvisionInclusivity';
// export * from './coefficient';
// export * from './consumableDefinitions';
export * from './coefficient';
export * from './consumableDefinitions';
// export * from './context';
// export * from './createDefaultSkills';
// export * from './degressiveStat';
export * from './createDefaultSkills';
export * from './degressiveStat';
// export * from './discoveredIdDefinitions';
// export * from './emblemURL';
export * from './equipmentDefinitions';
@@ -29,13 +29,13 @@ export * from './normalizeBoundingBox';
// export * from './parseBkni';
// export * from './patientFetch';
// export * from './patientFetchJSON';
// export * from './progressiveStat';
export * from './progressiveStat';
export * from './provisionDefinitions';
export * from './resolveDpm';
export * from './resolvePenetrationCoefficient';
// export * from './reviews';
export * from './reviews';
export * from './skillDefinitions';
// export * from './sum';
export * from './sum';
export * from './superCompress';
export * from './superDecompress';
export * from './tankDefinitions';
@@ -44,6 +44,6 @@ export * from './tankNames';
export * from './unionBoundingBox';
export * from './uniqueGuns';
// export * from './unwrapBlitzkitResponse';
// export * from './votes';
export * from './votes';
// export * from './withCORSProxy';
// export * from './youtubers';
export * from './youtubers';
@@ -1,6 +1,6 @@
import protobuf from 'protobufjs';
import average_definitions from '../../protos/average_definitions.proto';
import reviews from '../../protos/reviews.proto';
import average_definitions from '../../protos/average_definitions.proto?raw';
import reviews from '../../protos/reviews.proto?raw';
export type ProtoSource = keyof typeof protos;
@@ -20,24 +20,23 @@ import {
Tooltip,
} from '@radix-ui/themes';
import { debounce } from 'lodash-es';
import { Fragment, use, useEffect, useRef, useState } from 'react';
import { lerp } from 'three/src/math/MathUtils';
import { Ad, AdType } from '../../../../../../components/Ad';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { tankCharacteristics } from '../../../../../../core/blitzkit/tankCharacteristics';
import { useAdExempt } from '../../../../../../hooks/useAdExempt';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
import * as Duel from '../../../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../../../stores/tankopediaEphemeral';
import { Info } from './components/Info';
import { InfoWithDelta } from './components/InfoWithDelta';
import { Fragment, useEffect, useRef, useState } from 'react';
import { Ad, AdType } from '../../../../../../../../website/src/components/Ad';
import { InfoWithDelta } from '../../../../../../../../website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta';
import { applyPitchYawLimits } from '../../../../../../../../website/src/core/blitz/applyPitchYawLimits';
import { useAdExempt } from '../../../../../../../../website/src/hooks/useAdExempt';
import { useEquipment } from '../../../../../../../../website/src/hooks/useEquipment';
import { useTankModelDefinition } from '../../../../../../../../website/src/hooks/useTankModelDefinition';
import { Duel } from '../../../../../../../../website/src/stores/duel';
import { TankopediaEphemeral } from '../../../../../../../../website/src/stores/tankopediaEphemeral';
import { Info } from '../../../../performance/components/Info';
const awaitedEquipmentDefinitions = await equipmentDefinitions;
const awaitedProvisionDefinitions = await provisionDefinitions;
export function Characteristics() {
const mutateDuel = Duel.useMutation();
const exempt = useAdExempt();
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
const awaitedProvisionDefinitions = use(provisionDefinitions);
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
const hasImprovedVentilation = useEquipment(102);
@@ -15,7 +15,7 @@ import { PageWrapper } from '../../../../../components/PageWrapper';
import { useAdExempt } from '../../../../../hooks/useAdExempt';
import * as Duel from '../../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../../stores/tankopediaEphemeral';
import { VideoSection } from './VideoSection';
import { VideoSection } from '../../../../../../../website/src/components/Tankopedia/VideoSection';
export function Content() {
const exempt = useAdExempt();
@@ -15,7 +15,7 @@ import { PageWrapper } from '../../../../components/PageWrapper';
import { useAdExempt } from '../../../../hooks/useAdExempt';
import * as Duel from '../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../stores/tankopediaEphemeral';
import { VideoSection } from './components/VideoSection';
import { VideoSection } from '../../../../../../website/src/components/Tankopedia/VideoSection';
export default function Page() {
const exempt = useAdExempt();
+1 -1
View File
@@ -18,5 +18,5 @@
"../website/src/components/Tankopedia/HistorySection.tsx",
"../website/src/components/Tankopedia/CalloutsSection.tsx",
"../website/src/components/Tankopedia/ShotDisplaySection.tsx"
]
, "../website/src/components/Tankopedia/CharacteristicsSection/components/ConfigurationChildWrapper.tsx", "../website/src/components/Tankopedia/VideoSection.tsx" ]
}
@@ -1,4 +1,4 @@
import { ConsumableEntry } from '@blitzkit/core';
import type { ConsumableEntry } from '@blitzkit/core/src/blitzkit/consumableDefinitions';
import { Flex, type FlexProps } from '@radix-ui/themes';
import { ConsumableButton } from './ModuleButtons/ConsumableButton';
@@ -1,7 +1,7 @@
import { EquipmentPreset } from '@blitzkit/core';
import type { EquipmentPreset } from '@blitzkit/core';
import { Flex } from '@radix-ui/themes';
import { cloneDeep } from 'lodash-es';
import { EquipmentMatrix } from '../../../website-legacy/src/stores/duel';
import type { EquipmentMatrix } from '../stores/duel';
import { EquipmentButton } from './ModuleButtons/EquipmentButton';
interface EquipmentManagerProps {
@@ -1,9 +1,10 @@
import { asset, consumableDefinitions } from '@blitzkit/core';
import { asset } from '@blitzkit/core';
import { consumableDefinitions } from '@blitzkit/core/src/blitzkit/consumableDefinitions';
import { ClockIcon, ReloadIcon } from '@radix-ui/react-icons';
import { Flex, Text } from '@radix-ui/themes';
import { useDelta } from '../../../../website-legacy/src/hooks/useDelta';
import { useDelta } from '../../hooks/useDelta';
import { GenericTankComponentButton } from './GenericTankComponentButton';
import { TankComponentButtonProps } from './TankComponentButton';
import type { TankComponentButtonProps } from './TankComponentButton';
interface ConsumableButtonProps extends TankComponentButtonProps {
consumable: number;
@@ -1,6 +1,6 @@
import { asset, equipmentDefinitions } from '@blitzkit/core';
import { GenericTankComponentButton } from './GenericTankComponentButton';
import { TankComponentButtonProps } from './TankComponentButton';
import type { TankComponentButtonProps } from './TankComponentButton';
interface EquipmentButtonProps extends TankComponentButtonProps {
equipment: number;
@@ -1,6 +1,6 @@
import { asset, provisionDefinitions } from '@blitzkit/core';
import { GenericTankComponentButton } from './GenericTankComponentButton';
import { TankComponentButtonProps } from './TankComponentButton';
import type { TankComponentButtonProps } from './TankComponentButton';
interface ProvisionButtonProps extends TankComponentButtonProps {
provision: number;
@@ -1,7 +1,7 @@
import { MinusCircledIcon } from '@radix-ui/react-icons';
import { Callout, Flex } from '@radix-ui/themes';
import { ExperimentIcon } from '../../../../../components/ExperimentIcon';
import * as Duel from '../../../../../stores/duel';
import { Duel } from '../../stores/duel';
import { ExperimentIcon } from '../ExperimentIcon';
export function CalloutsSection() {
const tank = Duel.use((state) => state.protagonist.tank);
@@ -1,6 +1,6 @@
import { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons';
import { Flex, Text } from '@radix-ui/themes';
import { ReactNode } from 'react';
import { Flex, Separator, Text } from '@radix-ui/themes';
import type { ReactNode } from 'react';
export interface InfoProps {
name: ReactNode;
@@ -41,15 +41,7 @@ export function Info({
)}
</Text>
{children !== undefined && (
<div
style={{
flex: 1,
height: 1,
backgroundColor: 'red',
}}
/>
)}
{children !== undefined && <Separator style={{ flex: 1 }} />}
{children !== undefined && (
<Flex align="center" gap="1">
@@ -1,5 +1,5 @@
import { useDelta } from '../../../../../../../hooks/useDelta';
import { Info, InfoProps } from './Info';
import { useDelta } from '../../../../../../hooks/useDelta';
import { type InfoProps, Info } from './Info';
interface InfoWithDeltaProps extends InfoProps {
children: number;
@@ -0,0 +1,632 @@
import {
asset,
coefficient,
CREW_MEMBER_NAMES,
equipmentDefinitions,
GUN_TYPE_NAMES,
isExplosive,
provisionDefinitions,
resolvePenetrationCoefficient,
} from '@blitzkit/core';
import { AccessibilityIcon, InfoCircledIcon } from '@radix-ui/react-icons';
import {
Flex,
Heading,
IconButton,
Popover,
Slider,
Text,
TextField,
Tooltip,
} from '@radix-ui/themes';
import { debounce } from 'lodash-es';
import { useEffect, useRef, useState } from 'react';
import { Fragment } from 'react/jsx-runtime';
import { lerp } from 'three/src/math/MathUtils.js';
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
import { useAdExempt } from '../../../../../hooks/useAdExempt';
import { useEquipment } from '../../../../../hooks/useEquipment';
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
import { Duel } from '../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import { Ad, AdType } from '../../../../Ad';
import { Info } from './components/Info';
import { InfoWithDelta } from './components/InfoWithDelta';
const awaitedEquipmentDefinitions = await equipmentDefinitions;
const awaitedProvisionDefinitions = await provisionDefinitions;
export function Characteristics() {
const mutateDuel = Duel.useMutation();
const exempt = useAdExempt();
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
const hasImprovedVentilation = useEquipment(102);
const crewMastery = Duel.use((state) => state.protagonist.crewMastery);
const [penetrationDistance, setPenetrationDistance] = useState(250);
const setPenetrationDistanceDebounced = debounce((value: number) => {
setPenetrationDistance(value);
}, 500);
const provisions = Duel.use((state) => state.protagonist.provisions);
const provisionCrewBonus =
provisions.reduce(
(total, provision) =>
awaitedProvisionDefinitions[provision].crew
? total + awaitedProvisionDefinitions[provision].crew! / 100
: total,
0,
) + (hasImprovedVentilation ? 0.08 : 0);
const commanderMastery = crewMastery + provisionCrewBonus;
const consumables = Duel.use((state) => state.protagonist.consumables);
const camouflage = Duel.use((state) => state.protagonist.camouflage);
const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix,
);
const { tank, turret, gun, engine, track, shell } = Duel.use(
(state) => state.protagonist,
);
const stockEngine = tank.engines[0];
const stockTrack = tank.tracks[0];
const stockTurret = tank.turrets[0];
const stockGun = stockTurret.guns[0];
const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const stats = tankCharacteristics(
{
tank,
camouflage,
consumables,
crewMastery,
crewSkills,
engine,
equipmentMatrix,
gun,
provisions,
shell,
turret,
track,
stockEngine,
stockGun,
stockTrack,
stockTurret,
applyReactiveArmor: false,
applyDynamicArmor: false,
applySpallLiner: false,
},
{
tankModelDefinition,
equipmentDefinitions: awaitedEquipmentDefinitions,
provisionDefinitions: awaitedProvisionDefinitions,
},
);
const hasSupercharger = useEquipment(107);
const hasCalibratedShells = useEquipment(103);
const hasImprovedVerticalStabilizer = useEquipment(122);
const penetrationCoefficient = coefficient([
hasCalibratedShells,
resolvePenetrationCoefficient(true, shell.type) - 1,
]);
const penetrationLossOverDistanceCoefficient = coefficient([
hasSupercharger,
-0.6,
]);
useEffect(() => {
if (penetrationDistanceInput.current) {
penetrationDistanceInput.current.value = `${penetrationDistance}`;
}
}, [penetrationDistance]);
useEffect(() => {
mutateDuel((draft) => {
[draft.protagonist.pitch, draft.protagonist.yaw] = applyPitchYawLimits(
draft.protagonist.pitch,
draft.protagonist.yaw,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
});
}, [hasImprovedVerticalStabilizer]);
return (
<Flex
gap="8"
direction={{
initial: 'column',
md: 'row',
}}
>
<Flex direction="column" gap="8" style={{ flex: 1 }}>
<Flex direction="column" gap="2">
<Flex align="center" gap="4">
<Heading size="5">Firepower</Heading>
<Flex>
{gun.shells.map((thisShell, shellIndex) => (
<Tooltip content={thisShell.name} key={thisShell.id}>
<IconButton
color={thisShell.id === shell.id ? undefined : 'gray'}
variant="soft"
style={{
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
borderBottomLeftRadius: shellIndex === 0 ? undefined : 0,
borderTopRightRadius:
shellIndex === gun.shells.length - 1 ? undefined : 0,
borderBottomRightRadius:
shellIndex === gun.shells.length - 1 ? undefined : 0,
marginLeft: shellIndex === 0 ? 0 : -1,
}}
onClick={() => {
mutateDuel((draft) => {
draft.protagonist.shell = thisShell;
});
}}
>
<img
alt={thisShell.name}
width={16}
height={16}
src={asset(`icons/shells/${thisShell.icon}.webp`)}
/>
</IconButton>
</Tooltip>
))}
</Flex>
</Flex>
<Info name="Gun type">{GUN_TYPE_NAMES[gun.type]}</Info>
<InfoWithDelta name="DPM" decimals={0} unit="hp / min">
{stats.dpm}
</InfoWithDelta>
{gun.type === 'autoReloader' && (
<InfoWithDelta
decimals={0}
indent
name="Effective at 60s"
unit="hp / min"
>
{stats.dpmEffective!}
</InfoWithDelta>
)}
{gun.type !== 'regular' && (
<InfoWithDelta name="Shells">{stats.shells}</InfoWithDelta>
)}
{gun.type === 'autoReloader' ? (
<>
<Info indent name="Most optimal shell index">
{stats.mostOptimalShellIndex! + 1}
</Info>
<Info name="Shell reloads" unit="s" />
{stats.shellReloads!.map((reload, index) => (
<InfoWithDelta
key={index}
indent
name={`Shell ${index + 1}`}
decimals={2}
deltaType="lowerIsBetter"
>
{reload}
</InfoWithDelta>
))}
</>
) : (
<InfoWithDelta
decimals={2}
name="Reload"
unit="s"
deltaType="lowerIsBetter"
>
{stats.shellReload!}
</InfoWithDelta>
)}
{(gun.type === 'autoLoader' || gun.type === 'autoReloader') && (
<InfoWithDelta
indent
decimals={2}
name="Intra-clip"
unit="s"
deltaType="lowerIsBetter"
>
{stats.intraClip!}
</InfoWithDelta>
)}
<InfoWithDelta decimals={0} name="Penetration" unit="mm">
{stats.penetration}
</InfoWithDelta>
{typeof shell.penetration !== 'number' && (
<>
<InfoWithDelta
indent
decimals={0}
name={`At ${penetrationDistance}m`}
>
{lerp(
shell.penetration[0],
shell.penetration[1],
(penetrationDistance *
penetrationLossOverDistanceCoefficient) /
500,
) * penetrationCoefficient}
</InfoWithDelta>
<Flex align="center" gap="2" style={{ paddingLeft: 24 }}>
<Text color="gray">Distance</Text>
<Slider
key={penetrationDistance}
min={0}
max={500}
style={{ flex: 1 }}
defaultValue={[penetrationDistance]}
onValueChange={([value]) => {
setPenetrationDistanceDebounced(value);
if (!penetrationDistanceInput.current) return;
penetrationDistanceInput.current.value = value.toString();
}}
/>
<TextField.Root
style={{ width: 64 }}
ref={penetrationDistanceInput}
defaultValue={penetrationDistance}
onBlur={() => {
setPenetrationDistance(
Math.min(
parseInt(penetrationDistanceInput.current!.value),
500,
),
);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.currentTarget.blur();
}
}}
>
<TextField.Slot side="right">m</TextField.Slot>
</TextField.Root>
</Flex>
</>
)}
<InfoWithDelta name="Damage" unit="hp" decimals={0}>
{stats.damage}
</InfoWithDelta>
{gun.type !== 'regular' && (
<InfoWithDelta name="Clipping potential" indent>
{stats.clipDamage!}
</InfoWithDelta>
)}
<InfoWithDelta name="Module damage" unit="hp" decimals={0}>
{stats.moduleDamage}
</InfoWithDelta>
{isExplosive(shell.type) && (
<InfoWithDelta name="Splash radius" unit="m" decimals={0}>
{stats.explosionRadius!}
</InfoWithDelta>
)}
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
{stats.caliber}
</InfoWithDelta>
<InfoWithDelta name="Normalization" decimals={0} unit="°">
{stats.shellNormalization}
</InfoWithDelta>
{!isExplosive(shell.type) && (
<InfoWithDelta
name="Ricochet"
decimals={0}
deltaType="lowerIsBetter"
unit="°"
>
{stats.shellRicochet!}
</InfoWithDelta>
)}
<InfoWithDelta name="Shell velocity" unit="m/s" decimals={0}>
{stats.shellVelocity}
</InfoWithDelta>
<InfoWithDelta
decimals={2}
name="Aim time"
unit="s"
deltaType="lowerIsBetter"
>
{stats.aimTime}
</InfoWithDelta>
<Info name="Dispersion at 100m" />
<InfoWithDelta
decimals={3}
indent
name="Still"
unit="m"
deltaType="lowerIsBetter"
>
{stats.dispersion}
</InfoWithDelta>
<InfoWithDelta
prefix="+"
decimals={3}
indent
name="Moving"
deltaType="lowerIsBetter"
>
{stats.dispersionMoving}
</InfoWithDelta>
<InfoWithDelta
decimals={3}
prefix="+"
indent
name="Hull traversing"
deltaType="lowerIsBetter"
>
{stats.dispersionHullTraversing}
</InfoWithDelta>
<InfoWithDelta
decimals={3}
prefix="+"
indent
name="Turret traversing"
deltaType="lowerIsBetter"
>
{stats.dispersionTurretTraversing}
</InfoWithDelta>
<InfoWithDelta
decimals={3}
prefix="+"
indent
name="After shooting"
deltaType="lowerIsBetter"
>
{stats.dispersionShooting}
</InfoWithDelta>
<InfoWithDelta
decimals={1}
prefix="x "
indent
name="Gun damaged"
unit="scalar"
deltaType="lowerIsBetter"
>
{stats.dispersionGunDamaged}
</InfoWithDelta>
<InfoWithDelta
decimals={3}
indent
name="Maximum"
unit="m"
deltaType="lowerIsBetter"
>
{stats.maxDispersion}
</InfoWithDelta>
<Info name="Gun flexibility" unit="°" />
<InfoWithDelta decimals={1} indent name="Depression">
{stats.gunDepression}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Elevation">
{stats.gunElevation}
</InfoWithDelta>
{gunModelDefinition.pitch.front && (
<>
<InfoWithDelta decimals={1} indent name="Frontal depression">
{stats.gunFrontalDepression!}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Frontal elevation">
{stats.gunFrontalElevation!}
</InfoWithDelta>
</>
)}
{gunModelDefinition.pitch.back && (
<>
<InfoWithDelta decimals={1} indent name="Rear depression">
{stats.gunRearDepression!}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Rear elevation">
{stats.gunRearElevation!}
</InfoWithDelta>
</>
)}
{turretModelDefinition.yaw && (
<>
<InfoWithDelta decimals={1} indent name="Azimuth left">
{stats.azimuthLeft!}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Azimuth right">
{stats.azimuthRight!}
</InfoWithDelta>
</>
)}
</Flex>
<Flex direction="column" gap="2">
<Flex gap="2" align="center">
<Heading size="5">Crew training</Heading>
<Popover.Root>
<Popover.Trigger>
<IconButton variant="ghost">
<InfoCircledIcon />
</IconButton>
</Popover.Trigger>
<Popover.Content>
<Flex gap="1" align="center">
<AccessibilityIcon />
<Text>
represents the other roles a crew member can take.
</Text>
</Flex>
</Popover.Content>
</Popover.Root>
</Flex>
{tank.crew.map((member) => {
const count = member.count ?? 1;
return (
<Fragment key={member.type}>
<InfoWithDelta
key={`${member.type}-root`}
name={`${CREW_MEMBER_NAMES[member.type]}${
count > 1 ? ` x ${count}` : ''
}`}
unit="%"
decimals={0}
>
{(member.type === 'commander'
? commanderMastery
: commanderMastery * 1.1) * 100}
</InfoWithDelta>
{member.substitute && (
<InfoWithDelta
key={`${member.type}-substitute`}
decimals={0}
unit="%"
indent
name={
<>
<Flex align="center" gap="1" display="inline-flex">
<AccessibilityIcon />
{member.substitute
.map((sub, index) =>
index === 0 ? CREW_MEMBER_NAMES[sub] : sub,
)
.join(', ')}
</Flex>
</>
}
>
{commanderMastery * 1.05 * 100}
</InfoWithDelta>
)}
</Fragment>
);
})}
</Flex>
</Flex>
{!exempt && (
<Flex
justify="center"
display={{
initial: 'flex',
xs: 'none',
}}
>
<Ad type={AdType.MediumRectangleHorizontalPurple} />
</Flex>
)}
<Flex direction="column" gap="8" style={{ flex: 1 }}>
<Flex direction="column" gap="2">
<Heading size="5">Maneuverability</Heading>
<Info name="Speed" unit="kph" />
<InfoWithDelta decimals={0} indent name="Forwards">
{stats.speedForwards}
</InfoWithDelta>
<InfoWithDelta decimals={0} indent name="Backwards">
{stats.speedBackwards}
</InfoWithDelta>
<InfoWithDelta decimals={0} name="Engine power" unit="hp">
{stats.enginePower}
</InfoWithDelta>
<InfoWithDelta
decimals={1}
name="Weight"
unit="tn"
deltaType="lowerIsBetter"
>
{stats.weight}
</InfoWithDelta>
<Info name="Terrain coefficients" />
<InfoWithDelta decimals={0} unit="%" indent name="Hard">
{stats.hardTerrainCoefficient}
</InfoWithDelta>
<InfoWithDelta decimals={0} unit="%" indent name="Medium">
{stats.mediumTerrainCoefficient}
</InfoWithDelta>
<InfoWithDelta decimals={0} unit="%" indent name="Soft">
{stats.softTerrainCoefficient}
</InfoWithDelta>
<Info name="Raw terrain coefficients" />
<InfoWithDelta decimals={2} unit="%" indent name="Hard">
{stats.hardTerrainCoefficientRaw}
</InfoWithDelta>
<InfoWithDelta decimals={2} unit="%" indent name="Medium">
{stats.mediumTerrainCoefficientRaw}
</InfoWithDelta>
<InfoWithDelta decimals={2} unit="%" indent name="Soft">
{stats.softTerrainCoefficientRaw}
</InfoWithDelta>
<Info name="Power to weight ratio" unit="hp/tn" />
<InfoWithDelta decimals={1} indent name="On hard terrain">
{stats.powerToWeightRatioHardTerrain}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On medium terrain">
{stats.powerToWeightRatioMediumTerrain}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On soft terrain">
{stats.powerToWeightRatioSoftTerrain}
</InfoWithDelta>
<InfoWithDelta name="Turret traverse speed" unit="°/s" decimals={1}>
{stats.turretTraverseSpeed}
</InfoWithDelta>
<Info name="Hull traverse speed" unit="°/s" />
<InfoWithDelta decimals={1} indent name="On hard terrain">
{stats.hullTraverseHardTerrain}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On medium terrain">
{stats.hullTraverseMediumTerrain}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On soft terrain">
{stats.hullTraverseSoftTerrain}
</InfoWithDelta>
</Flex>
<Flex direction="column" gap="2">
<Heading size="5">Survivability</Heading>
<InfoWithDelta name="Health" unit="hp" decimals={0}>
{stats.health}
</InfoWithDelta>
<InfoWithDelta
name="Fire chance"
unit="%"
deltaType="lowerIsBetter"
decimals={0}
>
{stats.fireChance * 100}
</InfoWithDelta>
<InfoWithDelta name="View range" unit="m" decimals={0}>
{stats.viewRange}
</InfoWithDelta>
<Info name="Camouflage" unit="%" />
<InfoWithDelta indent name="Still" decimals={0}>
{stats.camouflageStill * 100}
</InfoWithDelta>
<InfoWithDelta indent name="Moving" decimals={0}>
{stats.camouflageMoving * 100}
</InfoWithDelta>
<InfoWithDelta indent name="Shooting still" decimals={0}>
{stats.camouflageShootingStill * 100}
</InfoWithDelta>
<InfoWithDelta indent name="Shooting moving" decimals={0}>
{stats.camouflageShootingMoving * 100}
</InfoWithDelta>
<InfoWithDelta indent name="Caught on fire" decimals={0}>
{stats.camouflageCaughtOnFire * 100}
</InfoWithDelta>
<Info name="Width" unit="m" decimals={0} deltaType="lowerIsBetter">
{stats.width}
</Info>
<Info name="Height" unit="m" decimals={0} deltaType="lowerIsBetter">
{stats.height}
</Info>
<Info name="Length" unit="m" decimals={0} deltaType="lowerIsBetter">
{stats.length}
</Info>
<Info name="Volume" unit="m" decimals={0} deltaType="lowerIsBetter">
{stats.volume}
</Info>
</Flex>
</Flex>
</Flex>
);
}
@@ -1,5 +1,5 @@
import { Flex, FlexProps } from '@radix-ui/themes';
import { ReactNode } from 'react';
import { type FlexProps, Flex } from '@radix-ui/themes';
import type { ReactNode } from 'react';
type ConfigurationChildWrapperProps = FlexProps & {
children: ReactNode;
@@ -1,7 +1,6 @@
import {
checkConsumableProvisionInclusivity,
consumableDefinitions,
} from '@blitzkit/core';
import {} from '@blitzkit/core';
import { checkConsumableProvisionInclusivity } from '@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity';
import { consumableDefinitions } from '@blitzkit/core/src/blitzkit/consumableDefinitions';
import { InfoCircledIcon } from '@radix-ui/react-icons';
import {
Button,
@@ -11,16 +10,16 @@ import {
Popover,
Text,
} from '@radix-ui/themes';
import { use } from 'react';
import { ConsumablesManager } from '../../../../../../../components/ConsumablesManager';
import { useEquipment } from '../../../../../../../hooks/useEquipment';
import * as Duel from '../../../../../../../stores/duel';
import { useEquipment } from '../../../../hooks/useEquipment';
import { Duel } from '../../../../stores/duel';
import { ConsumablesManager } from '../../../ConsumablesManager';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
const awaitedConsumableDefinitions = await consumableDefinitions;
export function Consumables() {
const mutateDuel = Duel.useMutation();
const protagonist = Duel.use((state) => state.protagonist);
const awaitedConsumableDefinitions = use(consumableDefinitions);
const consumables = Duel.use((state) => state.protagonist.consumables);
const consumablesList = Object.values(awaitedConsumableDefinitions).filter(
(consumable) =>
@@ -1,14 +1,14 @@
import { equipmentDefinitions } from '@blitzkit/core';
import { Button, Flex, Heading } from '@radix-ui/themes';
import { use } from 'react';
import { EquipmentManager } from '../../../../../../../components/EquipmentManager';
import * as Duel from '../../../../../../../stores/duel';
import { Duel } from '../../../../stores/duel';
import { EquipmentManager } from '../../../EquipmentManager';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
const awaitedEquipmentDefinitions = await equipmentDefinitions;
export function Equipment() {
const mutateDuel = Duel.useMutation();
const protagonist = Duel.use((state) => state.protagonist);
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
const equipmentPreset =
awaitedEquipmentDefinitions.presets[protagonist.tank.equipment];
const equipmentMatrix = Duel.use(
@@ -2,8 +2,8 @@ import { asset } from '@blitzkit/core';
import { Flex, Heading, Slider, TextField } from '@radix-ui/themes';
import { clamp, debounce } from 'lodash-es';
import { useEffect, useMemo, useRef, useState } from 'react';
import { GenericTankComponentButton } from '../../../../../../../components/ModuleButtons/GenericTankComponentButton';
import * as Duel from '../../../../../../../stores/duel';
import { Duel } from '../../../../stores/duel';
import { GenericTankComponentButton } from '../../../ModuleButtons/GenericTankComponentButton';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
export function Miscellaneous() {
@@ -1,14 +1,14 @@
import {
asset,
formatCompact,
ModuleDefinition,
ModuleType,
TankDefinition,
tankDefinitions,
tankIcon,
Tier,
TIER_ROMAN_NUMERALS,
Unlock,
type ModuleDefinition,
type ModuleType,
type TankDefinition,
type Tier,
type Unlock,
} from '@blitzkit/core';
import {
Button,
@@ -19,9 +19,8 @@ import {
Link,
Text,
} from '@radix-ui/themes';
import { use } from 'react';
import * as Duel from '../../../../../../../stores/duel';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
import { Duel } from '../../../../stores/duel';
function ModuleButton({
tier,
@@ -112,9 +111,10 @@ function ModuleButton({
);
}
const awaitedTankDefinitions = await tankDefinitions;
export function Modules() {
const mutateDuel = Duel.useMutation();
const awaitedTankDefinitions = use(tankDefinitions);
const tank = Duel.use((state) => state.protagonist.tank);
const hasUpgrades =
tank.turrets.length > 1 ||
@@ -264,23 +264,23 @@ export function Modules() {
(unlock.type === 'turret'
? turret.id
: unlock.type === 'gun'
? gun.id
: unlock.type === 'engine'
? engine.id
: unlock.type === 'chassis'
? track.id
: -1) === unlock.id
? gun.id
: unlock.type === 'engine'
? engine.id
: unlock.type === 'chassis'
? track.id
: -1) === unlock.id
}
top={
unlock.type === 'turret'
? module.id == topTurret.id
: unlock.type === 'gun'
? module.id === topGun.id
: unlock.type === 'engine'
? module.id === topEngine.id
: unlock.type === 'chassis'
? module.id === topTrack.id
: false
? module.id === topGun.id
: unlock.type === 'engine'
? module.id === topEngine.id
: unlock.type === 'chassis'
? module.id === topTrack.id
: false
}
onClick={() => setByUnlock(unlock)}
/>
@@ -1,15 +1,15 @@
import { availableProvisions, provisionDefinitions } from '@blitzkit/core';
import { Button, Flex, Heading } from '@radix-ui/themes';
import { use } from 'react';
import { ProvisionsManager } from '../../../../../../../components/ProvisionsManager';
import * as Duel from '../../../../../../../stores/duel';
import { Duel } from '../../../../stores/duel';
import { ProvisionsManager } from '../../../ProvisionsManager';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
const awaitedProvisionDefinitions = await provisionDefinitions;
export function Provisions() {
const mutateDuel = Duel.useMutation();
const { tank, gun } = Duel.use((state) => state.protagonist);
const provisions = Duel.use((state) => state.protagonist.provisions);
const awaitedProvisionDefinitions = use(provisionDefinitions);
const provisionsList = availableProvisions(
tank,
gun,
@@ -9,13 +9,13 @@ import {
Popover,
Text,
} from '@radix-ui/themes';
import { use } from 'react';
import { CrewSkillManager } from '../../../../../../../../components/CrewSkillManager';
import * as TankopediaEphemeral from '../../../../../../../../stores/tankopediaEphemeral';
import { ConfigurationChildWrapper } from '../ConfigurationChildWrapper';
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
import { CrewSkillManager } from '../../../CrewSkillManager';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
const awaitedSkillDefinitions = await skillDefinitions;
export function Skills() {
const awaitedSkillDefinitions = use(skillDefinitions);
const skillLevels = TankopediaEphemeral.use((state) => state.skills);
const mutateTankopediaTemporary = TankopediaEphemeral.useMutation();
@@ -1,11 +1,11 @@
import { Flex, Heading } from '@radix-ui/themes';
import { Characteristics } from '../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Characteristics';
import { Consumables } from '../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Characteristics/components/Consumables';
import { Equipment } from '../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Characteristics/components/Equipment';
import { Miscellaneous } from '../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Characteristics/components/Miscellaneous';
import { Modules } from '../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Characteristics/components/Modules';
import { Provisions } from '../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Characteristics/components/Provisions';
import { Skills } from '../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Characteristics/components/Skills';
import { Characteristics } from './components/Characteristics';
import { Consumables } from './components/Consumables';
import { Equipment } from './components/Equipment';
import { Miscellaneous } from './components/Miscellaneous';
import { Modules } from './components/Modules';
import { Provisions } from './components/Provisions';
import { Skills } from './components/Skills';
export function CharacteristicsSection() {
return (
@@ -5,7 +5,7 @@ import {
provisionDefinitions,
} from '@blitzkit/core';
import { Box, Flex, Heading, Text } from '@radix-ui/themes';
import * as Duel from '../../../../../../stores/duel';
import { Duel } from '../../stores/duel';
const awaitedGameDefinitions = await gameDefinitions;
const awaitedConsumableDefinitions = await consumableDefinitions;
@@ -1,6 +1,6 @@
import { Heading, Text } from '@radix-ui/themes';
import { PageWrapper } from '../../../../../components/PageWrapper';
import * as Duel from '../../../../../stores/duel';
import { Duel } from '../../stores/duel';
import { PageWrapper } from '../PageWrapper';
export function HistorySection() {
const tank = Duel.use((state) => state.protagonist.tank);
@@ -1,4 +1,4 @@
import { Flex, Text, TextProps } from '@radix-ui/themes';
import { Flex, Text, type TextProps } from '@radix-ui/themes';
type ListingProps = TextProps & {
label: string;
@@ -1,5 +1,5 @@
import { StarsInt } from '@blitzkit/core';
import { Flex, Text, TextProps } from '@radix-ui/themes';
import type { StarsInt } from '@blitzkit/core';
import { type TextProps, Flex, Text } from '@radix-ui/themes';
import { times } from 'lodash-es';
import { PartialStar } from './components/PartialStar';
@@ -51,7 +51,7 @@ export function MetaSection() {
</Button>
</Link>
{assertSecret(process.env.PUBLIC_PROMOTE_OPENTEST) === 'true' && (
{assertSecret(import.meta.env.PUBLIC_PROMOTE_OPENTEST) === 'true' && (
<Link
href={`https://opentest.blitzkit.app/tools/tankopedia/${protagonist.id}`}
>
@@ -1,9 +1,9 @@
import { Flex } from '@radix-ui/themes';
import { ShotDisplayCard } from '../../../../../components/Armor/components/ShotDisplayCard';
import * as TankEphemeral from '../../../../../stores/tankopediaEphemeral';
import { TankopediaEphemeral } from '../../stores/tankopediaEphemeral';
import { ShotDisplayCard } from '../Armor/components/ShotDisplayCard';
export function ShotDisplaySection() {
const shot = TankEphemeral.use((state) => state.shot);
const shot = TankopediaEphemeral.use((state) => state.shot);
if (!shot) return null;
@@ -5,9 +5,8 @@ import {
tankDefinitions,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import { Box, Flex, Text } from '@radix-ui/themes';
import Link from 'next/link';
import * as TankopediaEphemeral from '../../../../../../../../stores/tankopediaEphemeral';
import { Box, Flex, Link, Text } from '@radix-ui/themes';
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
interface NodeProps {
id: number;
@@ -2,8 +2,8 @@ import { asset, imgur, tankDefinitions } from '@blitzkit/core';
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from '@radix-ui/react-icons';
import { Flex, Heading, IconButton, ScrollArea, Text } from '@radix-ui/themes';
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import * as Duel from '../../../../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../../../../stores/tankopediaEphemeral';
import { Duel } from '../../../stores/duel';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { Arrow } from './components/Arrow';
import { Node } from './components/Node';
@@ -168,6 +168,7 @@ export function TechTreeSection() {
return (
<Fragment key={id}>
{index > 0 && <Arrow key={`${id}-arrow`} />}
<Node
key={`${id}-node`}
id={id}
@@ -1,15 +1,14 @@
import { reviews, youtubers } from '@blitzkit/core';
import { Box, Flex, Heading, Text } from '@radix-ui/themes';
import { use } from 'react';
import { Link } from '../../../../../components/Link';
import { PageWrapper } from '../../../../../components/PageWrapper';
import { VerifiedIcon } from '../../../../../components/VerifiedIcon';
import { Var } from '../../../../../core/radix/var';
import * as Duel from '../../../../../stores/duel';
import { Box, Flex, Heading, Link, Text } from '@radix-ui/themes';
import { Var } from '../../core/radix/var';
import { Duel } from '../../stores/duel';
import { PageWrapper } from '../PageWrapper';
import { VerifiedIcon } from '../VerifiedIcon';
const awaitedReviews = await reviews;
export function VideoSection() {
const tank = Duel.use((state) => state.protagonist.tank);
const awaitedReviews = use(reviews);
const videos = awaitedReviews.reviews[tank.id]?.videos;
if (!videos) return null;
@@ -1,25 +1,25 @@
import {
coefficient,
degressiveStat,
EngineDefinition,
EquipmentDefinitions,
GunDefinition,
isExplosive,
ModelDefinition,
normalizeBoundingBox,
progressiveStat,
ProvisionDefinitions,
resolveDpm,
resolveNearPenetration,
resolvePenetrationCoefficient,
ShellDefinition,
sum,
TankDefinition,
TrackDefinition,
TurretDefinition,
unionBoundingBox,
type EngineDefinition,
type EquipmentDefinitions,
type GunDefinition,
type ModelDefinition,
type ProvisionDefinitions,
type ShellDefinition,
type TankDefinition,
type TrackDefinition,
type TurretDefinition,
} from '@blitzkit/core';
import { EquipmentMatrix } from '../../stores/duel';
import { coefficient } from '@blitzkit/core/src/blitzkit/coefficient';
import type { EquipmentMatrix } from '../../stores/duel';
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
+2 -2
View File
@@ -1,5 +1,5 @@
import { useStore } from '@nanostores/react';
import { useLayoutEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { $patreonLogin } from '../stores/patreonLogin';
let cache: Record<string, boolean | Promise<boolean>> = {};
@@ -8,7 +8,7 @@ export function useAdExempt() {
const patreon = useStore($patreonLogin);
const [exempt, setExempt] = useState(false);
useLayoutEffect(() => {
useEffect(() => {
(async () => {
if (!patreon.token) return setExempt(false);
@@ -1,9 +1,9 @@
import { useLayoutEffect, useState } from 'react';
import { useEffect, useState } from 'react';
export function useFullscreenAvailability() {
const [available, setAvailable] = useState(false);
useLayoutEffect(() => {
useEffect(() => {
setAvailable(document.fullscreenEnabled);
}, []);
@@ -1,4 +1,4 @@
import { type RefObject, useLayoutEffect, useRef } from 'react';
import { type RefObject, useEffect, useRef } from 'react';
interface UseIntersectionOptions extends IntersectionObserverInit {
disabled: boolean;
@@ -11,7 +11,7 @@ export function useIntersection(
) {
const observer = useRef<IntersectionObserver>();
useLayoutEffect(() => {
useEffect(() => {
if (options?.disabled || !ref.current) return;
const rect = ref.current.getBoundingClientRect();
@@ -4,7 +4,15 @@ import {
tankDefinitions,
} from '@blitzkit/core';
import { PageWrapper } from '../../../../components/PageWrapper';
import { CalloutsSection } from '../../../../components/Tankopedia/CalloutsSection';
import { CharacteristicsSection } from '../../../../components/Tankopedia/CharacteristicsSection';
import { GameModeSection } from '../../../../components/Tankopedia/GameModeSection';
import { HeroSection } from '../../../../components/Tankopedia/HeroSection';
import { HistorySection } from '../../../../components/Tankopedia/HistorySection';
import { MetaSection } from '../../../../components/Tankopedia/MetaSection';
import { ShotDisplaySection } from '../../../../components/Tankopedia/ShotDisplaySection';
import { TechTreeSection } from '../../../../components/Tankopedia/TechTreeSection';
import { VideoSection } from '../../../../components/Tankopedia/VideoSection';
import { App } from '../../../../stores/app';
import { Duel } from '../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
@@ -31,17 +39,17 @@ export function Page({ id }: PageProps) {
>
<PageWrapper p="0" noMaxWidth color="purple" size={1600} gap="9">
<HeroSection />
{/* <ShotDisplaySection />
<MetaSection />
<TechTreeSection />
{!exempt && <AdMidSectionResponsive />}
<CalloutsSection />
<CharacteristicsSection />
{!exempt && <AdMidSectionResponsive />}
<GameModeSection />
<VideoSection />
<HistorySection />
{!exempt && <AdMidSectionResponsive mb="6" />} */}
<ShotDisplaySection />
<CalloutsSection />
<MetaSection />
<TechTreeSection />
{/* {!exempt && <AdMidSectionResponsive />} */}
<CharacteristicsSection />
{/* {!exempt && <AdMidSectionResponsive />} */}
<GameModeSection />
<VideoSection />
<HistorySection />
{/* {!exempt && <AdMidSectionResponsive mb="6" />} */}
</PageWrapper>
</Duel.Provider>
</TankopediaPersistent.Provider>
@@ -1,7 +1,6 @@
import { TextProps } from '@radix-ui/themes';
import { EmbedConfig } from '.';
import * as EmbedState from './';
import { EmbedConfigItemType, EmbedItemType } from './constants';
import type { TextProps } from '@radix-ui/themes';
import type { EmbedConfig } from '.';
import type { EmbedConfigItemType, EmbedItemType } from './constants';
export function useEmbedStateCurry<Config extends EmbedConfig>() {
function useState<Key extends keyof Config>(key: Key) {