feat: quick controls

This commit is contained in:
tresabhi committed 2025-04-14 23:56:29 -05:00
1 parent 45938c875f
commit 2bef96475c
6 files changed
+288 -273

No files matched your search

BIN
View File
Binary file not shown.
+4 -4
View File
@@ -46,9 +46,9 @@
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.5.4", "@types/node": "^22.14.1",
"prettier": "^3.3.3", "prettier": "^3.5.3",
"prisma": "^6.0.0", "prisma": "^6.6.0",
"typescript": "^5.5.4" "typescript": "^5.8.3"
} }
} }
@@ -13,8 +13,8 @@ export function MissingShellIcon(props: ComponentProps<'svg'>) {
<path <path
d="M39.5461 16.5714L47.4286 24.4539M16.1429 37.5714L26.4286 47.8571M56 10.3802L53.6198 8C32.2378 20.0846 22.0873 28.6952 8 48.0661C8.13223 48.595 8.63471 50.2876 11.1736 52.8264C13.7124 55.3653 15.405 55.8678 15.9339 56C35.3048 41.9127 43.9154 31.7622 56 10.3802Z" d="M39.5461 16.5714L47.4286 24.4539M16.1429 37.5714L26.4286 47.8571M56 10.3802L53.6198 8C32.2378 20.0846 22.0873 28.6952 8 48.0661C8.13223 48.595 8.63471 50.2876 11.1736 52.8264C13.7124 55.3653 15.405 55.8678 15.9339 56C35.3048 41.9127 43.9154 31.7622 56 10.3802Z"
stroke="currentColor" stroke="currentColor"
stroke-width="4" strokeWidth="4"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" stroke-linejoin="round"
/> />
</svg> </svg>
@@ -5,14 +5,17 @@ import { Suspense } from 'react';
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from '../../../../../../hooks/useLocale';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps';
import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor'; import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor';
import { DynamicArmorSwitcher } from './DynamicArmorSwitcher'; import { DynamicArmorSwitcher } from './DynamicArmorSwitcher';
interface ThicknessesProps { type ThicknessesProps = MaybeSkeletonComponentProps & {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
} };
export function Thicknesses({ thicknessRange }: ThicknessesProps) { export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
const display = TankopediaEphemeral.use((state) => state.display);
const showExternalModules = TankopediaPersistent.use( const showExternalModules = TankopediaPersistent.use(
(state) => state.showExternalModules, (state) => state.showExternalModules,
); );
@@ -31,10 +34,14 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
return ( return (
<Flex <Flex
position="absolute" position="absolute"
right="0" right={display === TankopediaDisplay.StaticArmor ? '0' : '-6rem'}
top="50%" top="50%"
mr="4" mr="4"
style={{ transform: 'translateY(-50%)', userSelect: 'none' }} style={{
transform: 'translateY(-50%)',
userSelect: 'none',
transitionDuration: '200ms',
}}
direction="column" direction="column"
gap="5" gap="5"
align="end" align="end"
@@ -190,9 +197,11 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
</Button> </Button>
</Flex> </Flex>
<Suspense> {!skeleton && (
<DynamicArmorSwitcher /> <Suspense>
</Suspense> <DynamicArmorSwitcher />
</Suspense>
)}
</Flex> </Flex>
); );
} }
@@ -42,6 +42,7 @@ import {
} from '../../../../../stores/tankopediaEphemeral'; } from '../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
import type { ThicknessRange } from '../../../../Armor/components/StaticArmor'; import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton'; import { ModuleButton } from '../../../../ModuleButtons/ModuleButton';
import { SmallTankIcon } from '../../../../SmallTankIcon'; import { SmallTankIcon } from '../../../../SmallTankIcon';
@@ -51,12 +52,12 @@ import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
import { QuickInputs } from './components/QuickInputs'; import { QuickInputs } from './components/QuickInputs';
import { Thicknesses } from './components/Thicknesses'; import { Thicknesses } from './components/Thicknesses';
interface OptionsProps { type OptionsProps = MaybeSkeletonComponentProps & {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
canvas: RefObject<HTMLCanvasElement>; canvas: RefObject<HTMLCanvasElement>;
} };
export function Options({ thicknessRange, canvas }: OptionsProps) { export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const hasCustomShell = TankopediaEphemeral.use( const hasCustomShell = TankopediaEphemeral.use(
(state) => state.customShell !== undefined, (state) => state.customShell !== undefined,
); );
@@ -111,216 +112,212 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
/> />
)} )}
{display === TankopediaDisplay.StaticArmor && ( <Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
<Thicknesses thicknessRange={thicknessRange} />
)}
{display === TankopediaDisplay.DynamicArmor && ( <Flex
<Flex gap="2"
gap="2" direction="column"
direction="column" top="50%"
top="50%" right={display === TankopediaDisplay.DynamicArmor ? '3' : '-3rem'}
right="3" style={{
style={{ position: 'absolute', transform: 'translateY(-50%)' }} position: 'absolute',
align="end" transform: 'translateY(-50%)',
> transitionDuration: '200ms',
{!hasCustomShell && ( }}
<Text color="gray" size={{ initial: '1', sm: '2' }}> align="end"
{literals(strings.common.units.mm, [ >
( {!hasCustomShell && (
resolvePenetrationCoefficient( <Text color="gray" size={{ initial: '1', sm: '2' }}>
hasCalibratedShells, {literals(strings.common.units.mm, [
antagonistShell.type, (
) * antagonistShell.penetration.near resolvePenetrationCoefficient(
).toFixed(0), hasCalibratedShells,
])} antagonistShell.type,
</Text> ) * antagonistShell.penetration.near
)} ).toFixed(0),
{antagonistUniqueGuns.length > 1 && ( ])}
<Popover.Root> </Text>
<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 {antagonistUniqueGuns.length > 1 && (
alt="Antagonist Gun" <Popover.Root>
src={asset('icons/modules/gun.webp')} <Popover.Trigger>
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 <IconButton
color={ radius="full"
thisShell.id === antagonistShell.id && !hasCustomShell
? undefined
: 'gray'
}
variant="soft" variant="soft"
key={thisShell.id} color="gray"
size={{ initial: '2', sm: '3' }} size={{ initial: '2', sm: '3' }}
radius="none" style={{ position: 'relative' }}
onClick={() => {
mutateDuel((draft) => {
draft.antagonist.shell = thisShell;
});
mutateTankopediaEphemeral((draft) => {
draft.shot = undefined;
draft.customShell = undefined;
});
}}
> >
<img <Text
alt={unwrap(thisShell.name)} size="1"
src={asset(`icons/shells/${thisShell.icon}.webp`)} color="gray"
style={{ style={{
width: '50%', position: 'absolute',
height: '50%', 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> </IconButton>
))} </Popover.Trigger>
<CustomShellButton /> <Popover.Content side="left" align="center">
</Flex> <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 <Flex
direction="column" direction="column"
style={{ style={{
pointerEvents: 'auto', borderRadius: 'var(--radius-full)',
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;
});
}} }}
overflow="hidden"
> >
<IconButton <img
color={hasCalibratedShells ? undefined : 'gray'} alt="Calibrated Shells"
variant="soft" src={asset('icons/equipment/103.webp')}
size={{ initial: '2', sm: '3' }} style={{
radius="none" width: '50%',
onClick={() => { height: '50%',
mutateDuel((draft) => {
draft.antagonist.equipmentMatrix[0][0] = hasCalibratedShells
? 0
: 1;
});
mutateTankopediaEphemeral((draft) => {
draft.shot = undefined;
});
}} }}
> />
<img </IconButton>
alt="Calibrated Shells" <IconButton
src={asset('icons/equipment/103.webp')} color={hasEnhancedArmor ? undefined : 'gray'}
style={{ variant="soft"
width: '50%', size={{ initial: '2', sm: '3' }}
height: '50%', radius="none"
}} onClick={() => {
/> mutateDuel((draft) => {
</IconButton> draft.protagonist.equipmentMatrix[1][1] = hasEnhancedArmor
<IconButton ? 0
color={hasEnhancedArmor ? undefined : 'gray'} : -1;
variant="soft" });
size={{ initial: '2', sm: '3' }} mutateTankopediaEphemeral((draft) => {
radius="none" draft.shot = undefined;
onClick={() => { });
mutateDuel((draft) => { }}
draft.protagonist.equipmentMatrix[1][1] = hasEnhancedArmor >
? 0 <img
: -1; alt="Enhanced Armor"
}); src={asset('icons/equipment/110.webp')}
mutateTankopediaEphemeral((draft) => { style={{
draft.shot = undefined; width: '50%',
}); height: '50%',
}} }}
> />
<img </IconButton>
alt="Enhanced Armor" </Flex>
src={asset('icons/equipment/110.webp')}
style={{
width: '50%',
height: '50%',
}}
/>
</IconButton>
</Flex>
{!skeleton && (
<Suspense> <Suspense>
<DynamicArmorSwitcher /> <DynamicArmorSwitcher />
</Suspense> </Suspense>
</Flex> )}
)} </Flex>
<Flex <Flex
direction="column" direction="column"
@@ -330,72 +327,77 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
left="50%" left="50%"
style={{ transform: 'translateX(-50%)' }} style={{ transform: 'translateX(-50%)' }}
> >
{display === TankopediaDisplay.DynamicArmor && ( <Flex
<> direction="column"
<Flex align="center"
align="center" style={{ transitionDuration: '200ms' }}
gap="2" position="relative"
mb="1" bottom={display === TankopediaDisplay.DynamicArmor ? '0' : '-7rem'}
style={{ cursor: 'pointer' }} >
onClick={() => { <Flex
mutateTankopediaPersistent((draft) => { align="center"
draft.advancedHighlighting = !draft.advancedHighlighting; 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.tankopedia.sandbox.dynamic.advanced}
</Text>
</Flex>
<Flex align="center" gap="2">
<Text color="gray" size="2">
{strings.website.tools.tankopedia.sandbox.dynamic.shooter}
</Text>
<Dialog.Root
open={antagonistSelectorOpen}
onOpenChange={setAntagonistSelectorOpen}
> >
<Checkbox checked={advancedHighlighting} /> <Dialog.Trigger>
<Text color="gray" size="2"> <Button variant="ghost">
{strings.website.tools.tankopedia.sandbox.dynamic.advanced} <Flex gap="2" align="center">
</Text> <SmallTankIcon id={antagonistTank.id} size={16} />
</Flex> {unwrap(antagonistTank.name)}
</Flex>
</Button>
</Dialog.Trigger>
<Flex align="center" gap="2"> <Dialog.Content>
<Text color="gray" size="2"> <Dialog.Title align="center">Select tank</Dialog.Title>
{strings.website.tools.tankopedia.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> <Tabs.Root
<Dialog.Title align="center">Select tank</Dialog.Title> 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>
<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"> <Flex gap="3" align="center" mt="2">
<SegmentedControl.Root <SegmentedControl.Root
value={`${display}`} value={`${display}`}
@@ -101,7 +101,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
style={{ transform: 'translateY(-50%)', transitionDuration: '200ms' }} style={{ transform: 'translateY(-50%)', transitionDuration: '200ms' }}
direction="column" direction="column"
align={{ initial: 'center', md: 'start' }} align={{ initial: 'center', md: 'start' }}
gap={disturbed ? '0' : '2'} gap={disturbed ? '0' : { initial: '0', md: '2' }}
> >
<Flex align="center" gap="3"> <Flex align="center" gap="3">
<Heading <Heading
@@ -139,7 +139,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
weight="light" weight="light"
ml={{ ml={{
initial: '0', initial: '0',
md: '8', md: disturbed ? 'var(--font-size-7)' : 'var(--font-size-8)',
lg: disturbed ? 'var(--font-size-8)' : 'var(--font-size-9)', lg: disturbed ? 'var(--font-size-8)' : 'var(--font-size-9)',
}} }}
style={{ transitionDuration: '200ms' }} style={{ transitionDuration: '200ms' }}
@@ -161,7 +161,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
width="100%" width="100%"
height="100%" height="100%"
position="relative" position="relative"
left={disturbed ? '0' : '12.5%'} left={disturbed ? '0' : { initial: '0', md: '12.5%' }}
style={{ transitionDuration: '200ms' }} style={{ transitionDuration: '200ms' }}
> >
{skeleton && <TankSandboxLoader id={protagonist.id} />} {skeleton && <TankSandboxLoader id={protagonist.id} />}
@@ -171,7 +171,11 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
</Suspense> </Suspense>
</Box> </Box>
<Options canvas={canvas} thicknessRange={thicknessRange} /> <Options
skeleton={skeleton}
canvas={canvas}
thicknessRange={thicknessRange}
/>
</Box> </Box>
</Box> </Box>
</Flex> </Flex>