mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 11:39:50 +02:00
626 lines
22 KiB
TypeScript
626 lines
22 KiB
TypeScript
import {
|
|
asset,
|
|
imgur,
|
|
resolvePenetrationCoefficient,
|
|
TIER_ROMAN_NUMERALS,
|
|
uniqueGuns,
|
|
} from '@blitzkit/core';
|
|
import { literals } from '@blitzkit/i18n/src/literals';
|
|
import {
|
|
CameraIcon,
|
|
CopyIcon,
|
|
DownloadIcon,
|
|
EnterFullScreenIcon,
|
|
ExitFullScreenIcon,
|
|
EyeOpenIcon,
|
|
GearIcon,
|
|
} from '@radix-ui/react-icons';
|
|
import {
|
|
Box,
|
|
Button,
|
|
Checkbox,
|
|
Dialog,
|
|
DropdownMenu,
|
|
Flex,
|
|
IconButton,
|
|
Popover,
|
|
SegmentedControl,
|
|
Tabs,
|
|
Text,
|
|
Tooltip,
|
|
} from '@radix-ui/themes';
|
|
import { Suspense, useState, type RefObject } from 'react';
|
|
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
|
|
import { useEquipment } from '../../../../../hooks/useEquipment';
|
|
import { useFullScreen } from '../../../../../hooks/useFullScreen';
|
|
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability';
|
|
import { useLocale } from '../../../../../hooks/useLocale';
|
|
import { App } from '../../../../../stores/app';
|
|
import { Duel } from '../../../../../stores/duel';
|
|
import {
|
|
ShootingRangeZoom,
|
|
TankopediaEphemeral,
|
|
} from '../../../../../stores/tankopediaEphemeral';
|
|
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
|
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
|
import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
|
|
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton';
|
|
import { SmallTankIcon } from '../../../../SmallTankIcon';
|
|
import { TankSearch } from '../../../../TankSearch';
|
|
import { CustomShellButton } from './components/CustomShellButton';
|
|
import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
|
|
import { QuickInputs } from './components/QuickInputs';
|
|
import { Thicknesses } from './components/Thicknesses';
|
|
|
|
interface OptionsProps {
|
|
thicknessRange: ThicknessRange;
|
|
canvas: RefObject<HTMLCanvasElement>;
|
|
}
|
|
|
|
export function Options({ thicknessRange, canvas }: OptionsProps) {
|
|
const hasCustomShell = TankopediaEphemeral.use(
|
|
(state) => state.customShell !== undefined,
|
|
);
|
|
const display = TankopediaEphemeral.use((state) => state.display);
|
|
const isFullScreen = useFullScreen();
|
|
const showGrid = TankopediaPersistent.use((state) => state.showGrid);
|
|
const greenPenetration = TankopediaPersistent.use(
|
|
(state) => state.greenPenetration,
|
|
);
|
|
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
|
(state) => state.hideTankModelUnderArmor,
|
|
);
|
|
const advancedHighlighting = TankopediaPersistent.use(
|
|
(state) => state.advancedHighlighting,
|
|
);
|
|
const wireframe = TankopediaPersistent.use((state) => state.wireframe);
|
|
const opaque = TankopediaPersistent.use((state) => state.opaque);
|
|
const fullScreenAvailable = useFullscreenAvailability();
|
|
const developerMode = App.use((state) => state.developerMode);
|
|
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
|
const antagonistGun = Duel.use((state) => state.antagonist.gun);
|
|
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
|
const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false);
|
|
const antagonistTank = Duel.use((state) => state.antagonist.tank);
|
|
const hasCalibratedShells = useEquipment(103, true);
|
|
const [tab, setTab] = useState('search');
|
|
const mutateDuel = Duel.useMutation();
|
|
const hasEnhancedArmor = useEquipment(110);
|
|
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
|
|
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
const { strings, unwrap } = useLocale();
|
|
|
|
return (
|
|
<>
|
|
<QuickInputs />
|
|
|
|
{display === TankopediaDisplay.ShootingRange && (
|
|
<Box
|
|
width="1rem"
|
|
height="1rem"
|
|
position="absolute"
|
|
top="50%"
|
|
left="50%"
|
|
style={{
|
|
zIndex: 2,
|
|
backgroundImage: `url(${imgur('27Gwth4')})`,
|
|
backgroundSize: 'contain',
|
|
transform: 'translateX(-50%)',
|
|
borderRadius: '50%',
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{display === TankopediaDisplay.StaticArmor && (
|
|
<Thicknesses thicknessRange={thicknessRange} />
|
|
)}
|
|
|
|
{display === TankopediaDisplay.DynamicArmor && (
|
|
<Flex
|
|
gap="2"
|
|
direction="column"
|
|
top="50%"
|
|
right="3"
|
|
style={{ position: 'absolute', transform: 'translateY(-50%)' }}
|
|
align="end"
|
|
>
|
|
{!hasCustomShell && (
|
|
<Text color="gray" size={{ initial: '1', sm: '2' }}>
|
|
{literals(strings.common.units.mm, [
|
|
(
|
|
resolvePenetrationCoefficient(
|
|
hasCalibratedShells,
|
|
antagonistShell.type,
|
|
) * antagonistShell.penetration.near
|
|
).toFixed(0),
|
|
])}
|
|
</Text>
|
|
)}
|
|
{antagonistUniqueGuns.length > 1 && (
|
|
<Popover.Root>
|
|
<Popover.Trigger>
|
|
<IconButton
|
|
radius="full"
|
|
variant="soft"
|
|
color="gray"
|
|
size={{ initial: '2', sm: '3' }}
|
|
style={{ position: 'relative' }}
|
|
>
|
|
<Text
|
|
size="1"
|
|
color="gray"
|
|
style={{
|
|
position: 'absolute',
|
|
right: 0,
|
|
bottom: 0,
|
|
}}
|
|
mr="2"
|
|
mb="1"
|
|
>
|
|
{
|
|
TIER_ROMAN_NUMERALS[
|
|
antagonistGun.gun_type!.value.base.tier
|
|
]
|
|
}
|
|
</Text>
|
|
|
|
<img
|
|
alt="Antagonist Gun"
|
|
src={asset('icons/modules/gun.webp')}
|
|
style={{
|
|
width: '65%',
|
|
height: '65%',
|
|
objectFit: 'contain',
|
|
}}
|
|
/>
|
|
</IconButton>
|
|
</Popover.Trigger>
|
|
|
|
<Popover.Content side="left" align="center">
|
|
<Flex gap="2">
|
|
{[...antagonistUniqueGuns]
|
|
.reverse()
|
|
.map(({ gun, turret }) => (
|
|
<ModuleButton
|
|
module="gun"
|
|
key={gun.gun_type!.value.base.id}
|
|
onClick={() =>
|
|
mutateDuel((draft) => {
|
|
draft.antagonist.turret = turret;
|
|
draft.antagonist.gun = gun;
|
|
draft.antagonist.shell =
|
|
gun.gun_type!.value.base.shells[0];
|
|
})
|
|
}
|
|
selected={
|
|
gun.gun_type!.value.base.id ===
|
|
antagonistGun.gun_type!.value.base.id
|
|
}
|
|
discriminator={
|
|
TIER_ROMAN_NUMERALS[gun.gun_type!.value.base.tier]
|
|
}
|
|
secondaryDiscriminator={
|
|
<Text style={{ fontSize: '0.75em' }}>
|
|
{literals(strings.common.units.mm, [
|
|
gun.gun_type!.value.base.shells[0].caliber.toFixed(
|
|
0,
|
|
),
|
|
])}
|
|
</Text>
|
|
}
|
|
/>
|
|
))}
|
|
</Flex>
|
|
</Popover.Content>
|
|
</Popover.Root>
|
|
)}
|
|
<Flex
|
|
direction="column"
|
|
style={{
|
|
borderRadius: 'var(--radius-full)',
|
|
}}
|
|
overflow="hidden"
|
|
>
|
|
{antagonistGun.gun_type!.value.base.shells.map((thisShell) => (
|
|
<IconButton
|
|
color={
|
|
thisShell.id === antagonistShell.id && !hasCustomShell
|
|
? undefined
|
|
: 'gray'
|
|
}
|
|
variant="soft"
|
|
key={thisShell.id}
|
|
size={{ initial: '2', sm: '3' }}
|
|
radius="none"
|
|
onClick={() => {
|
|
mutateDuel((draft) => {
|
|
draft.antagonist.shell = thisShell;
|
|
});
|
|
mutateTankopediaEphemeral((draft) => {
|
|
draft.shot = undefined;
|
|
draft.customShell = undefined;
|
|
});
|
|
}}
|
|
>
|
|
<img
|
|
alt={unwrap(thisShell.name)}
|
|
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
|
style={{
|
|
width: '50%',
|
|
height: '50%',
|
|
}}
|
|
/>
|
|
</IconButton>
|
|
))}
|
|
|
|
<CustomShellButton />
|
|
</Flex>
|
|
|
|
<Flex
|
|
direction="column"
|
|
style={{
|
|
pointerEvents: 'auto',
|
|
borderRadius: 'var(--radius-full)',
|
|
}}
|
|
overflow="hidden"
|
|
>
|
|
<IconButton
|
|
color={hasCalibratedShells ? undefined : 'gray'}
|
|
variant="soft"
|
|
size={{ initial: '2', sm: '3' }}
|
|
radius="none"
|
|
onClick={() => {
|
|
mutateDuel((draft) => {
|
|
draft.antagonist.equipmentMatrix[0][0] = hasCalibratedShells
|
|
? 0
|
|
: 1;
|
|
});
|
|
mutateTankopediaEphemeral((draft) => {
|
|
draft.shot = undefined;
|
|
});
|
|
}}
|
|
>
|
|
<img
|
|
alt="Calibrated Shells"
|
|
src={asset('icons/equipment/103.webp')}
|
|
style={{
|
|
width: '50%',
|
|
height: '50%',
|
|
}}
|
|
/>
|
|
</IconButton>
|
|
<IconButton
|
|
color={hasEnhancedArmor ? undefined : 'gray'}
|
|
variant="soft"
|
|
size={{ initial: '2', sm: '3' }}
|
|
radius="none"
|
|
onClick={() => {
|
|
mutateDuel((draft) => {
|
|
draft.protagonist.equipmentMatrix[1][1] = hasEnhancedArmor
|
|
? 0
|
|
: -1;
|
|
});
|
|
mutateTankopediaEphemeral((draft) => {
|
|
draft.shot = undefined;
|
|
});
|
|
}}
|
|
>
|
|
<img
|
|
alt="Enhanced Armor"
|
|
src={asset('icons/equipment/110.webp')}
|
|
style={{
|
|
width: '50%',
|
|
height: '50%',
|
|
}}
|
|
/>
|
|
</IconButton>
|
|
</Flex>
|
|
|
|
<Suspense>
|
|
<DynamicArmorSwitcher />
|
|
</Suspense>
|
|
</Flex>
|
|
)}
|
|
|
|
<Flex
|
|
direction="column"
|
|
align="center"
|
|
position="absolute"
|
|
bottom="4"
|
|
left="50%"
|
|
style={{ transform: 'translateX(-50%)' }}
|
|
>
|
|
{display === TankopediaDisplay.DynamicArmor && (
|
|
<>
|
|
<Flex
|
|
align="center"
|
|
gap="2"
|
|
mb="1"
|
|
style={{ cursor: 'pointer' }}
|
|
onClick={() => {
|
|
mutateTankopediaPersistent((draft) => {
|
|
draft.advancedHighlighting = !draft.advancedHighlighting;
|
|
});
|
|
}}
|
|
>
|
|
<Checkbox checked={advancedHighlighting} />
|
|
<Text color="gray" size="2">
|
|
{strings.website.tools.tanks.sandbox.dynamic.advanced}
|
|
</Text>
|
|
</Flex>
|
|
|
|
<Flex align="center" gap="2">
|
|
<Text color="gray" size="2">
|
|
{strings.website.tools.tanks.sandbox.dynamic.shooter}
|
|
</Text>
|
|
<Dialog.Root
|
|
open={antagonistSelectorOpen}
|
|
onOpenChange={setAntagonistSelectorOpen}
|
|
>
|
|
<Dialog.Trigger>
|
|
<Button variant="ghost">
|
|
<Flex gap="2" align="center">
|
|
<SmallTankIcon id={antagonistTank.id} size={16} />
|
|
{unwrap(antagonistTank.name)}
|
|
</Flex>
|
|
</Button>
|
|
</Dialog.Trigger>
|
|
|
|
<Dialog.Content>
|
|
<Dialog.Title align="center">Select tank</Dialog.Title>
|
|
|
|
<Tabs.Root
|
|
value={tab}
|
|
onValueChange={setTab}
|
|
style={{ position: 'relative' }}
|
|
>
|
|
<TankSearch
|
|
compact
|
|
onSelect={(tank) => {
|
|
mutateDuel((draft) => {
|
|
draft.antagonist.tank = tank;
|
|
draft.antagonist.engine = tank.engines.at(-1)!;
|
|
draft.antagonist.track = tank.tracks.at(-1)!;
|
|
draft.antagonist.turret = tank.turrets.at(-1)!;
|
|
draft.antagonist.gun =
|
|
draft.antagonist.turret.guns.at(-1)!;
|
|
draft.antagonist.shell =
|
|
draft.antagonist.gun.gun_type!.value.base.shells[0];
|
|
});
|
|
setAntagonistSelectorOpen(false);
|
|
}}
|
|
/>
|
|
</Tabs.Root>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|
|
</Flex>
|
|
</>
|
|
)}
|
|
<Flex gap="3" align="center" mt="2">
|
|
<SegmentedControl.Root
|
|
value={`${display}`}
|
|
onValueChange={(value) => {
|
|
mutateTankopediaEphemeral((draft) => {
|
|
draft.display = Number(value);
|
|
draft.shootingRangeZoom = ShootingRangeZoom.Arcade;
|
|
});
|
|
}}
|
|
>
|
|
<SegmentedControl.Item value={`${TankopediaDisplay.Model}`}>
|
|
<Tooltip content={strings.website.tools.tanks.sandbox.model.name}>
|
|
<Flex height="100%" align="center">
|
|
<img src={imgur('jAdYf0m')} style={{ height: '1.25em' }} />
|
|
</Flex>
|
|
</Tooltip>
|
|
</SegmentedControl.Item>
|
|
<SegmentedControl.Item value={`${TankopediaDisplay.DynamicArmor}`}>
|
|
<Tooltip
|
|
content={strings.website.tools.tanks.sandbox.dynamic.name}
|
|
>
|
|
<Flex height="100%" align="center">
|
|
<img src={imgur('oe4Cq0g')} style={{ height: '1.25em' }} />
|
|
</Flex>
|
|
</Tooltip>
|
|
</SegmentedControl.Item>
|
|
<SegmentedControl.Item value={`${TankopediaDisplay.StaticArmor}`}>
|
|
<Tooltip
|
|
content={strings.website.tools.tanks.sandbox.static.name}
|
|
>
|
|
<Flex height="100%" align="center">
|
|
<img src={imgur('VQ4uDno')} style={{ height: '1.25em' }} />
|
|
</Flex>
|
|
</Tooltip>
|
|
</SegmentedControl.Item>
|
|
{/* <SegmentedControl.Item value={`${TankopediaDisplay.ShootingRange}`}>
|
|
<Tooltip content="Shooting range">
|
|
<Flex height="100%" align="center">
|
|
<img src={imgur('q8ZPfzG')} style={{ height: '1.25em' }} />
|
|
</Flex>
|
|
</Tooltip>
|
|
</SegmentedControl.Item> */}
|
|
</SegmentedControl.Root>
|
|
|
|
<DropdownMenu.Root>
|
|
<DropdownMenu.Trigger>
|
|
<IconButton variant="ghost" color="gray">
|
|
<EyeOpenIcon />
|
|
</IconButton>
|
|
</DropdownMenu.Trigger>
|
|
|
|
<DropdownMenu.Content>
|
|
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.HullDown)}>
|
|
{strings.website.tools.tanks.sandbox.poses.hull_down}
|
|
</DropdownMenu.Item>
|
|
|
|
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.FaceHug)}>
|
|
{strings.website.tools.tanks.sandbox.poses.face_hug}
|
|
</DropdownMenu.Item>
|
|
|
|
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.Default)}>
|
|
{strings.website.tools.tanks.sandbox.poses.default}
|
|
</DropdownMenu.Item>
|
|
</DropdownMenu.Content>
|
|
</DropdownMenu.Root>
|
|
|
|
<DropdownMenu.Root>
|
|
<DropdownMenu.Trigger>
|
|
<IconButton variant="ghost" color="gray">
|
|
<GearIcon />
|
|
</IconButton>
|
|
</DropdownMenu.Trigger>
|
|
|
|
<DropdownMenu.Content>
|
|
{display === TankopediaDisplay.DynamicArmor && (
|
|
<>
|
|
<DropdownMenu.Label>
|
|
{strings.website.tools.tanks.sandbox.settings.armor}
|
|
</DropdownMenu.Label>
|
|
|
|
<DropdownMenu.CheckboxItem
|
|
checked={greenPenetration}
|
|
onCheckedChange={(checked) => {
|
|
mutateTankopediaPersistent((draft) => {
|
|
draft.greenPenetration = checked;
|
|
});
|
|
}}
|
|
>
|
|
{
|
|
strings.website.tools.tanks.sandbox.settings
|
|
.green_penetration
|
|
}
|
|
</DropdownMenu.CheckboxItem>
|
|
|
|
<DropdownMenu.CheckboxItem
|
|
checked={hideTankModelUnderArmor}
|
|
onCheckedChange={(checked) => {
|
|
mutateTankopediaPersistent((draft) => {
|
|
draft.hideTankModelUnderArmor = checked;
|
|
});
|
|
}}
|
|
>
|
|
{
|
|
strings.website.tools.tanks.sandbox.settings
|
|
.hide_model_under_armor
|
|
}
|
|
</DropdownMenu.CheckboxItem>
|
|
|
|
<DropdownMenu.CheckboxItem
|
|
checked={opaque}
|
|
onCheckedChange={(checked) => {
|
|
mutateTankopediaPersistent((draft) => {
|
|
draft.opaque = checked;
|
|
});
|
|
}}
|
|
>
|
|
{strings.website.tools.tanks.sandbox.settings.opaque}
|
|
</DropdownMenu.CheckboxItem>
|
|
|
|
{developerMode && (
|
|
<DropdownMenu.CheckboxItem
|
|
checked={wireframe}
|
|
onCheckedChange={(checked) => {
|
|
mutateTankopediaPersistent((draft) => {
|
|
draft.wireframe = checked;
|
|
});
|
|
}}
|
|
>
|
|
{
|
|
strings.website.tools.tanks.sandbox.settings
|
|
.dev_wireframe
|
|
}
|
|
</DropdownMenu.CheckboxItem>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<DropdownMenu.Label>
|
|
{strings.website.tools.tanks.sandbox.settings.environment}
|
|
</DropdownMenu.Label>
|
|
|
|
<DropdownMenu.CheckboxItem
|
|
checked={showGrid}
|
|
onCheckedChange={(checked) => {
|
|
mutateTankopediaPersistent((draft) => {
|
|
draft.showGrid = checked;
|
|
});
|
|
}}
|
|
>
|
|
{strings.website.tools.tanks.sandbox.settings.show_grid}
|
|
</DropdownMenu.CheckboxItem>
|
|
</DropdownMenu.Content>
|
|
</DropdownMenu.Root>
|
|
|
|
<Popover.Root>
|
|
<Popover.Trigger>
|
|
<IconButton color="gray" variant="ghost">
|
|
<CameraIcon />
|
|
</IconButton>
|
|
</Popover.Trigger>
|
|
|
|
<Popover.Content>
|
|
<Flex direction="column" gap="2">
|
|
<Popover.Close>
|
|
<Button
|
|
onClick={() => {
|
|
if (!canvas.current) return;
|
|
|
|
const anchor = document.createElement('a');
|
|
|
|
anchor.setAttribute(
|
|
'download',
|
|
`${protagonistTank.name}.png`,
|
|
);
|
|
anchor.setAttribute(
|
|
'href',
|
|
canvas.current.toDataURL('image/png'),
|
|
);
|
|
anchor.click();
|
|
}}
|
|
>
|
|
<DownloadIcon />
|
|
{strings.website.tools.tanks.sandbox.screenshot.download}
|
|
</Button>
|
|
</Popover.Close>
|
|
<Popover.Close>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
if (!canvas.current) return;
|
|
|
|
canvas.current.toBlob((blob) => {
|
|
if (!blob) return;
|
|
|
|
navigator.clipboard.write([
|
|
new ClipboardItem({ 'image/png': blob }),
|
|
]);
|
|
});
|
|
}}
|
|
>
|
|
<CopyIcon />
|
|
{strings.website.tools.tanks.sandbox.screenshot.copy}
|
|
</Button>
|
|
</Popover.Close>
|
|
</Flex>
|
|
</Popover.Content>
|
|
</Popover.Root>
|
|
|
|
{fullScreenAvailable && (
|
|
<IconButton
|
|
color="gray"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
if (isFullScreen) {
|
|
document.exitFullscreen();
|
|
} else document.body.requestFullscreen();
|
|
}}
|
|
>
|
|
{isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
|
|
</IconButton>
|
|
)}
|
|
</Flex>
|
|
</Flex>
|
|
</>
|
|
);
|
|
}
|