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

@@ -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);
@@ -0,0 +1,82 @@
import { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons';
import { Flex, Separator, Text } from '@radix-ui/themes';
import type { ReactNode } from 'react';
export interface InfoProps {
name: ReactNode;
children?: ReactNode;
unit?: string;
indent?: boolean;
delta?: number;
decimals?: number;
prefix?: string;
deltaType?: 'higherIsBetter' | 'lowerIsBetter';
}
export function Info({
name,
children,
unit,
indent = false,
delta,
decimals,
prefix,
deltaType = 'higherIsBetter',
}: InfoProps) {
return (
<Flex
align="center"
style={{ width: '100%', paddingLeft: indent ? 24 : 0 }}
gap="4"
>
<Text color="gray">
{name}
{unit != undefined && (
<>
{' '}
<Text color="gray" size="1">
{unit}
</Text>
</>
)}
</Text>
{children !== undefined && <Separator style={{ flex: 1 }} />}
{children !== undefined && (
<Flex align="center" gap="1">
{delta !== undefined && delta !== 0 && !isNaN(delta) && (
<>
<Text
color={
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0
? 'green'
: 'tomato'
}
>
{decimals !== undefined
? Math.abs(delta).toFixed(decimals)
: Math.abs(delta)}
</Text>
<Text
color={
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0
? 'green'
: 'tomato'
}
>
{delta > 0 ? <CaretUpIcon /> : <CaretDownIcon />}
</Text>
</>
)}
<Text>
{prefix}
{decimals !== undefined && typeof children === 'number'
? children.toFixed(decimals)
: children}
</Text>
</Flex>
)}
</Flex>
);
}
@@ -0,0 +1,16 @@
import { useDelta } from '../../../../../../hooks/useDelta';
import { type InfoProps, Info } from './Info';
interface InfoWithDeltaProps extends InfoProps {
children: number;
}
export function InfoWithDelta({ children, ...props }: InfoWithDeltaProps) {
const delta = useDelta(children);
return (
<Info {...props} delta={delta}>
{children}
</Info>
);
}
@@ -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>
);
}
@@ -0,0 +1,22 @@
import { type FlexProps, Flex } from '@radix-ui/themes';
import type { ReactNode } from 'react';
type ConfigurationChildWrapperProps = FlexProps & {
children: ReactNode;
};
export function ConfigurationChildWrapper({
children,
...props
}: ConfigurationChildWrapperProps) {
return (
<Flex
gap="2"
direction="column"
align={{ initial: 'center', sm: 'start' }}
{...props}
>
{children}
</Flex>
);
}
@@ -0,0 +1,90 @@
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,
Flex,
Heading,
IconButton,
Popover,
Text,
} from '@radix-ui/themes';
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 consumables = Duel.use((state) => state.protagonist.consumables);
const consumablesList = Object.values(awaitedConsumableDefinitions).filter(
(consumable) =>
checkConsumableProvisionInclusivity(
consumable,
protagonist.tank,
protagonist.gun,
),
);
const cooldownBooster = Duel.use(
(state) => state.protagonist.cooldownBooster,
);
const hasConsumableDeliverySystem = useEquipment(118);
const hasHighEndConsumables = useEquipment(101);
return (
<ConfigurationChildWrapper>
<Flex gap="4" align="center">
<Flex gap="2" align="center">
<Heading size="4">Consumables</Heading>
<Popover.Root>
<Popover.Trigger>
<IconButton variant="ghost">
<InfoCircledIcon />
</IconButton>
</Popover.Trigger>
<Popover.Content>
<Text>
<Text color="amber">Yellow consumables</Text> do not affect the
statistics.
</Text>
</Popover.Content>
</Popover.Root>
</Flex>
<Button
variant="ghost"
color="red"
onClick={() => {
mutateDuel((draft) => {
draft.protagonist.consumables = [];
});
}}
>
Clear
</Button>
</Flex>
<ConsumablesManager
consumables={consumablesList}
selected={consumables}
cooldownBooster={cooldownBooster}
disabled={protagonist.tank.consumables === consumables.length}
hasConsumableDeliverySystem={hasConsumableDeliverySystem}
hasHighEndConsumables={hasHighEndConsumables}
timers
onConsumablesChange={(consumables) => {
mutateDuel((draft) => {
draft.protagonist.consumables = consumables;
});
}}
justify={{ initial: 'center', sm: 'start' }}
/>
</ConfigurationChildWrapper>
);
}
@@ -0,0 +1,50 @@
import { equipmentDefinitions } from '@blitzkit/core';
import { Button, Flex, Heading } from '@radix-ui/themes';
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 equipmentPreset =
awaitedEquipmentDefinitions.presets[protagonist.tank.equipment];
const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix,
);
return (
<ConfigurationChildWrapper>
<Flex gap="4" align="center">
<Heading size="4">Equipment</Heading>
<Button
variant="ghost"
color="red"
onClick={() => {
mutateDuel((draft) => {
draft.protagonist.equipmentMatrix.forEach((row) => {
row.forEach((_, index) => {
row[index] = 0;
});
});
});
}}
>
Clear
</Button>
</Flex>
<EquipmentManager
matrix={equipmentMatrix}
preset={equipmentPreset}
onChange={(matrix) => {
mutateDuel((draft) => {
draft.protagonist.equipmentMatrix = matrix;
});
}}
/>
</ConfigurationChildWrapper>
);
}
@@ -0,0 +1,130 @@
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 { Duel } from '../../../../stores/duel';
import { GenericTankComponentButton } from '../../../ModuleButtons/GenericTankComponentButton';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
export function Miscellaneous() {
const mutateDuel = Duel.useMutation();
const camouflage = Duel.use((state) => state.protagonist.camouflage);
const cooldownBooster = Duel.use(
(state) => state.protagonist.cooldownBooster,
);
const crewMastery = Duel.use((state) => state.protagonist.crewMastery);
const crewInput = useRef<HTMLInputElement>(null);
const [crewMasteryDraft, setCrewMasteryDraft] = useState(crewMastery);
const { tank } = Duel.use((state) => state.protagonist);
const debouncedApplyDraft = useMemo(
() =>
debounce((value) => {
mutateDuel((draft) => {
draft.protagonist.crewMastery = value;
});
}, 500),
[],
);
useEffect(() => {
setCrewMasteryDraft(crewMastery);
if (!crewInput.current) return;
crewInput.current.value = `${Math.round(crewMastery * 100)}`;
}, [crewMastery]);
return (
<ConfigurationChildWrapper>
<Heading size="4">Miscellaneous</Heading>
<Flex gap="2" align="center">
{!tank.fixedCamouflage && (
<GenericTankComponentButton
icon={asset('icons/camo.webp')}
selected={camouflage}
iconStyles={{
top: '50%',
left: '50%',
transform: 'translate(calc(-50% + 4px), calc(-50% + 4px))',
}}
onClick={() => {
mutateDuel((draft) => {
draft.protagonist.camouflage = !camouflage;
});
}}
/>
)}
<GenericTankComponentButton
icon={asset('icons/boosters/equipment.webp')}
selected={cooldownBooster > 0}
banner={
cooldownBooster === 0
? undefined
: cooldownBooster === 1
? '#afb0abc0'
: cooldownBooster === 2
? '#5f72cbc0'
: '#9b3cc0c0'
}
iconStyles={{
transform: 'translate(-50%, -50%) scale(0.8)',
}}
onClick={() => {
mutateDuel((draft) => {
draft.protagonist.cooldownBooster++;
if (draft.protagonist.cooldownBooster === 4)
draft.protagonist.cooldownBooster = 0;
});
}}
/>
{tank.treeType === 'researchable' && (
<Flex
align="center"
direction="column"
gap="1"
justify="end"
style={{ width: 120 }}
>
<TextField.Root
ref={crewInput}
defaultValue={Math.round(crewMastery * 100)}
style={{ textAlign: 'right' }}
onBlur={() => {
const newValue = Number(crewInput.current!.value) / 100;
if (isNaN(newValue)) {
crewInput.current!.value = `${Math.round(crewMastery * 100)}`;
return;
}
mutateDuel((draft) => {
draft.protagonist.crewMastery = clamp(newValue / 100, 0.5, 1);
});
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
crewInput.current?.blur();
}
}}
>
<TextField.Slot>Crew</TextField.Slot>
<TextField.Slot>%</TextField.Slot>
</TextField.Root>
<Slider
min={50}
max={100}
value={[crewMasteryDraft * 100]}
style={{ width: '100%' }}
onValueChange={([value]) => {
setCrewMasteryDraft(value / 100);
debouncedApplyDraft(value / 100);
}}
/>
</Flex>
)}
</Flex>
</ConfigurationChildWrapper>
);
}
@@ -0,0 +1,384 @@
import {
asset,
formatCompact,
tankDefinitions,
tankIcon,
TIER_ROMAN_NUMERALS,
type ModuleDefinition,
type ModuleType,
type TankDefinition,
type Tier,
type Unlock,
} from '@blitzkit/core';
import {
Button,
ChevronDownIcon,
Flex,
Heading,
IconButton,
Link,
Text,
} from '@radix-ui/themes';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
import { Duel } from '../../../../stores/duel';
function ModuleButton({
tier,
selected,
unlock,
top,
onClick,
}: {
unlock: Unlock;
tier: Tier;
selected: boolean;
top: boolean;
onClick: () => void;
}) {
const isTank = unlock.type === 'vehicle';
const button = (
<IconButton
size="4"
radius="small"
variant={selected ? 'surface' : 'soft'}
color={top ? 'amber' : selected ? undefined : 'gray'}
onClick={onClick}
style={{
position: 'relative',
width: isTank ? 72 : undefined,
height: isTank ? 72 : undefined,
}}
>
<Text
size="1"
color="gray"
style={{
position: 'absolute',
left: isTank ? 8 : 4,
top: isTank ? 8 : 4,
}}
>
{TIER_ROMAN_NUMERALS[tier]}
</Text>
{unlock.cost.value > 0 && (
<Flex
style={{
position: 'absolute',
left: '50%',
bottom: 4,
transform: 'translateX(-50%)',
}}
align="center"
gap={isTank ? '1' : undefined}
>
<Text size="1" color="gray">
{formatCompact(unlock.cost.value)}
</Text>
<img
alt={unlock.cost.type}
src={asset(`icons/currencies/${unlock.cost.type}.webp`)}
style={{
width: 12,
height: 12,
objectFit: 'contain',
}}
/>
</Flex>
)}
<img
alt={unlock.type}
src={
isTank
? tankIcon(unlock.id)
: asset(`icons/modules/${unlock.type}.webp`)
}
style={{
width: isTank ? 64 : 32,
height: isTank ? 64 : 32,
objectFit: 'contain',
}}
/>
</IconButton>
);
return isTank ? (
<Link href={`/tools/tankopedia/${unlock.id}`}>{button}</Link>
) : (
button
);
}
const awaitedTankDefinitions = await tankDefinitions;
export function Modules() {
const mutateDuel = Duel.useMutation();
const tank = Duel.use((state) => state.protagonist.tank);
const hasUpgrades =
tank.turrets.length > 1 ||
tank.turrets[0].guns.length > 1 ||
tank.engines.length > 1 ||
tank.tracks.length > 1;
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
const engine = Duel.use((state) => state.protagonist.engine);
const track = Duel.use((state) => state.protagonist.track);
const turret0 = tank.turrets[0];
const gun0 = turret0.guns[0];
const engine0 = tank.engines[0];
const track0 = tank.tracks[0];
const topTurret = tank.turrets.at(-1)!;
const topGun = topTurret.guns.at(-1)!;
const topEngine = tank.engines.at(-1)!;
const topTrack = tank.tracks.at(-1)!;
function setByUnlock(unlock: Unlock) {
mutateDuel((draft) => {
if (unlock.type === 'turret') {
draft.protagonist.turret = draft.protagonist.tank.turrets.find(
(turret) => turret.id === unlock.id,
)!;
if (
!draft.protagonist.turret.guns.some(
(gun) => gun.id === draft.protagonist.gun.id,
)
) {
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
draft.protagonist.shell = draft.protagonist.gun.shells[0];
}
} else if (unlock.type === 'gun') {
const gunInTurret = draft.protagonist.turret.guns.find(
(gun) => gun.id === unlock.id,
);
if (gunInTurret) {
draft.protagonist.gun = gunInTurret;
draft.protagonist.shell = gunInTurret.shells[0];
} else {
// TODO: warn somehow?
const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
turret.guns.some((gun) => gun.id === unlock.id),
)!;
const gunInSuitableTurret = suitableTurret.guns.find(
(gun) => gun.id === unlock.id,
)!;
draft.protagonist.turret = suitableTurret;
draft.protagonist.gun = gunInSuitableTurret;
draft.protagonist.shell = gunInSuitableTurret.shells[0];
}
} else if (unlock.type === 'engine') {
draft.protagonist.engine = draft.protagonist.tank.engines.find(
(engine) => engine.id === unlock.id,
)!;
} else if (unlock.type === 'chassis') {
draft.protagonist.track = draft.protagonist.tank.tracks.find(
(track) => track.id === unlock.id,
)!;
}
});
}
function tree(type: ModuleType, unlocks: Unlock[]) {
return (
<Flex gap="2">
{unlocks.map((unlock, index) => {
const first = index === 0;
const last = index === unlocks.length - 1;
const central = !first && !last;
let module: ModuleDefinition | TankDefinition | undefined = undefined;
if (unlock.type === 'chassis') {
module = tank.tracks.find((track) => track.id === unlock.id)!;
} else if (unlock.type === 'engine') {
module = tank.engines.find((engine) => engine.id === unlock.id)!;
} else if (unlock.type === 'turret') {
module = tank.turrets.find((turret) => turret.id === unlock.id)!;
} else if (unlock.type === 'gun') {
module = tank.turrets
.find((turret) =>
turret.guns.some((gun) => gun.id === unlock.id),
)!
.guns.find((gun) => gun.id === unlock.id)!;
} else if (unlock.type === 'vehicle') {
module = awaitedTankDefinitions[unlock.id];
}
if (module === undefined) return null;
return (
<Flex
key={unlock.id}
align="center"
direction="column"
gap="2"
position="relative"
>
{unlock.cost.value > 0 && (
<>
{unlocks.length > 1 && (
<div
style={{
backgroundColor: 'currentcolor',
width: `calc(${central ? 100 : 50}% + ${
central ? 8 : 4
}px)`,
height: 1,
position: 'absolute',
right: first ? 0 : undefined,
left: last ? 0 : undefined,
transform: `translateX(${first ? 4 : last ? -4 : 0}px)`,
}}
/>
)}
<Flex
direction="column"
align="center"
style={{
marginBottom: -7,
}}
>
<div
style={{
backgroundColor: 'currentcolor',
width: 1,
height: 8,
}}
/>
<ChevronDownIcon
style={{
transform: 'translateY(-7px)',
}}
/>
</Flex>
</>
)}
<ModuleButton
unlock={unlock}
tier={module.tier}
selected={
(unlock.type === 'turret'
? turret.id
: unlock.type === 'gun'
? 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
}
onClick={() => setByUnlock(unlock)}
/>
{(module as ModuleDefinition).unlocks &&
(module as ModuleDefinition).unlocks!.length > 1 && (
<div
style={{
backgroundColor: 'currentcolor',
width: 1,
height: 8,
marginBottom: -8,
}}
/>
)}
{(module as ModuleDefinition).unlocks &&
tree(type, (module as ModuleDefinition).unlocks!)}
</Flex>
);
})}
</Flex>
);
}
return (
<ConfigurationChildWrapper>
<Flex gap="4" align="center">
<Heading size="4">Modules</Heading>
{hasUpgrades && (
<>
<Button
variant="ghost"
color="red"
onClick={() => {
mutateDuel((draft) => {
draft.protagonist.turret = draft.protagonist.tank.turrets[0];
draft.protagonist.gun = draft.protagonist.turret.guns[0];
draft.protagonist.shell = draft.protagonist.gun.shells[0];
draft.protagonist.engine = draft.protagonist.tank.engines[0];
draft.protagonist.track = draft.protagonist.tank.tracks[0];
});
}}
>
Stock
</Button>
<Button
variant="ghost"
onClick={() => {
mutateDuel((draft) => {
draft.protagonist.turret =
draft.protagonist.tank.turrets.at(-1)!;
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
draft.protagonist.shell = draft.protagonist.gun.shells[0];
draft.protagonist.engine =
draft.protagonist.tank.engines.at(-1)!;
draft.protagonist.track =
draft.protagonist.tank.tracks.at(-1)!;
});
}}
>
Upgrade
</Button>
</>
)}
</Flex>
<Flex gap="2" wrap="wrap" gapY="6">
{tree('engine', [
{
cost: { type: 'xp', value: 0 },
id: engine0.id,
type: 'engine',
},
])}
{tree('chassis', [
{
cost: { type: 'xp', value: 0 },
id: track0.id,
type: 'chassis',
},
])}
{tree('turret', [
{
cost: { type: 'xp', value: 0 },
id: turret0.id,
type: 'turret',
},
])}
{tree('gun', [
{
cost: { type: 'xp', value: 0 },
id: gun0.id,
type: 'gun',
},
])}
</Flex>
</ConfigurationChildWrapper>
);
}
@@ -0,0 +1,48 @@
import { availableProvisions, provisionDefinitions } from '@blitzkit/core';
import { Button, Flex, Heading } from '@radix-ui/themes';
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 provisionsList = availableProvisions(
tank,
gun,
awaitedProvisionDefinitions,
);
return (
<ConfigurationChildWrapper>
<Flex gap="4" align="center">
<Heading size="4">Provisions</Heading>
<Button
variant="ghost"
color="red"
onClick={() => {
mutateDuel((draft) => {
draft.protagonist.provisions = [];
});
}}
>
Clear
</Button>
</Flex>
<ProvisionsManager
provisions={provisionsList.map((provision) => provision.id)}
selected={provisions}
disabled={tank.provisions === provisions.length}
onChange={(provisions) => {
mutateDuel((draft) => {
draft.protagonist.provisions = provisions;
});
}}
/>
</ConfigurationChildWrapper>
);
}
@@ -0,0 +1,95 @@
import { createDefaultSkills, skillDefinitions } from '@blitzkit/core';
import { InfoCircledIcon } from '@radix-ui/react-icons';
import {
Button,
Flex,
Heading,
IconButton,
Kbd,
Popover,
Text,
} from '@radix-ui/themes';
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
import { CrewSkillManager } from '../../../CrewSkillManager';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
const awaitedSkillDefinitions = await skillDefinitions;
export function Skills() {
const skillLevels = TankopediaEphemeral.use((state) => state.skills);
const mutateTankopediaTemporary = TankopediaEphemeral.useMutation();
if (Object.keys(skillLevels).length === 0) {
mutateTankopediaTemporary((draft) => {
draft.skills = createDefaultSkills(awaitedSkillDefinitions);
});
return null;
}
return (
<ConfigurationChildWrapper>
<Flex gap="4" align="center">
<Flex gap="2" align="center">
<Heading size="4">Crew skills</Heading>
<Popover.Root>
<Popover.Trigger>
<IconButton variant="ghost">
<InfoCircledIcon />
</IconButton>
</Popover.Trigger>
<Popover.Content>
<Flex direction="column" gap="2">
<Text>
<Text color="amber">Yellow skills</Text> only apply under
special circumstances.
</Text>
<Text>
Hold <Kbd>Shift</Kbd> to quickly toggle between level 0 and 7.
</Text>
</Flex>
</Popover.Content>
</Popover.Root>
</Flex>
<Button
variant="ghost"
color="red"
onClick={() => {
mutateTankopediaTemporary((draft) => {
Object.keys(draft.skills).forEach((skill) => {
draft.skills[skill] = 0;
});
});
}}
>
Clear
</Button>
<Button
variant="ghost"
onClick={() => {
mutateTankopediaTemporary((draft) => {
Object.keys(draft.skills).forEach((skill) => {
draft.skills[skill] = 7;
});
});
}}
>
Maximize
</Button>
</Flex>
<CrewSkillManager
skillLevels={skillLevels}
onChange={(skills) => {
mutateTankopediaTemporary((draft) => {
draft.skills = skills;
});
}}
/>
</ConfigurationChildWrapper>
);
}
@@ -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}
@@ -0,0 +1,114 @@
import { reviews, youtubers } from '@blitzkit/core';
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 videos = awaitedReviews.reviews[tank.id]?.videos;
if (!videos) return null;
return (
<PageWrapper noFlex1>
<Flex direction="column" gap="6" align="center">
<Flex direction="column" align="center">
<Heading size="6">Review videos</Heading>
<Text color="gray">BlitzKit does not endorse any review videos.</Text>
</Flex>
<Flex gap="3" wrap="wrap" justify="center">
{videos.map((video) => {
const image = `https://i.ytimg.com/vi/${video.id}/hqdefault.jpg`;
const youtuber = youtubers.find(
(youtuber) => youtuber.id === video.author,
);
if (!youtuber) return null;
return (
<Link
color="gray"
highContrast
underline="hover"
key={video.id}
href={`https://www.youtube.com/watch?v=${video.id}`}
target="_blank"
>
<Flex
direction="column"
style={{
borderRadius: 'var(--radius-3)',
overflow: 'hidden',
background: `url(${image})`,
backgroundPosition: '0 18px',
backgroundSize: 'cover',
}}
>
<Box
style={{
background: `url(${image})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
height: 128,
aspectRatio: '16 / 9',
}}
/>
<Flex
p="2"
width="100%"
justify="center"
style={{
backdropFilter: 'blur(4rem)',
WebkitBackdropFilter: 'blur(4rem)',
}}
align="center"
gap="1"
>
<Text>{youtuber.name}</Text>
<VerifiedIcon width="1em" height="1em" />
</Flex>
</Flex>
</Link>
);
})}
<Link
href="https://discord.gg/nDt7AjGJQH"
target="_blank"
color="gray"
highContrast
>
<Flex
align="center"
justify="center"
gap="2"
p="3"
direction="column"
height="100%"
style={{
backgroundColor: Var('color-panel-solid'),
borderRadius: Var('radius-3'),
}}
>
<VerifiedIcon />
<Text
align="center"
style={{
width: '9rem',
}}
>
Apply to BlitzKit YouTuber program
</Text>
</Flex>
</Link>
</Flex>
</Flex>
</PageWrapper>
);
}
@@ -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) {