mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 11:39:50 +02:00
feat: complete tankoepdia page
This commit is contained in:
1 parent
2b5ffb2b84
commit
a54ba78dbf
39 files changed
+790
-161
No files matched your search
-22
@@ -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>
|
||||
);
|
||||
}
|
||||
-91
@@ -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>
|
||||
);
|
||||
}
|
||||
-50
@@ -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>
|
||||
);
|
||||
}
|
||||
-90
@@ -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>
|
||||
);
|
||||
}
|
||||
-16
@@ -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>
|
||||
);
|
||||
}
|
||||
-130
@@ -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>
|
||||
);
|
||||
}
|
||||
-384
@@ -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>
|
||||
);
|
||||
}
|
||||
-48
@@ -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>
|
||||
);
|
||||
}
|
||||
-95
@@ -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>
|
||||
);
|
||||
}
|
||||
+13
-14
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user