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,22 +0,0 @@
import { Flex, FlexProps } from '@radix-ui/themes';
import { 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>
);
}
@@ -1,91 +0,0 @@
import {
checkConsumableProvisionInclusivity,
consumableDefinitions,
} from '@blitzkit/core';
import { InfoCircledIcon } from '@radix-ui/react-icons';
import {
Button,
Flex,
Heading,
IconButton,
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 { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
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) =>
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>
);
}
@@ -1,50 +0,0 @@
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 { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
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(
(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>
);
}
@@ -1,90 +0,0 @@
import { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons';
import { Flex, Text } from '@radix-ui/themes';
import { 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 && (
<div
style={{
flex: 1,
height: 1,
backgroundColor: 'red',
}}
/>
)}
{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>
);
}
@@ -1,16 +0,0 @@
import { useDelta } from '../../../../../../../hooks/useDelta';
import { Info, InfoProps } 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>
);
}
@@ -1,130 +0,0 @@
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 { 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>
);
}
@@ -1,384 +0,0 @@
import {
asset,
formatCompact,
ModuleDefinition,
ModuleType,
TankDefinition,
tankDefinitions,
tankIcon,
Tier,
TIER_ROMAN_NUMERALS,
Unlock,
} from '@blitzkit/core';
import {
Button,
ChevronDownIcon,
Flex,
Heading,
IconButton,
Link,
Text,
} from '@radix-ui/themes';
import { use } from 'react';
import * as Duel from '../../../../../../../stores/duel';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
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
);
}
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 ||
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>
);
}
@@ -1,48 +0,0 @@
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 { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
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,
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>
);
}
@@ -1,95 +0,0 @@
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 { use } from 'react';
import { CrewSkillManager } from '../../../../../../../../components/CrewSkillManager';
import * as TankopediaEphemeral from '../../../../../../../../stores/tankopediaEphemeral';
import { ConfigurationChildWrapper } from '../ConfigurationChildWrapper';
export function Skills() {
const awaitedSkillDefinitions = use(skillDefinitions);
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>
);
}
@@ -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();
@@ -1,115 +0,0 @@
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';
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;
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>
);
}
@@ -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();