mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 12:09:51 +02:00
feat: popover custom shell
This commit is contained in:
1 parent
dbc3d0df3a
commit
3e2bfc453a
2 files changed
+187
-194
No files matched your search
+172
-183
@@ -1,16 +1,15 @@
|
|||||||
import { imgur, ShellType } from '@blitzkit/core';
|
import { imgur, ShellType } from '@blitzkit/core';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
|
||||||
Flex,
|
Flex,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Popover,
|
||||||
Select,
|
Select,
|
||||||
Slider,
|
Slider,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { invalidate } from '@react-three/fiber';
|
import { type ChangeEvent } from 'react';
|
||||||
import { useEffect, useState, type ChangeEvent } from 'react';
|
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||||
|
|
||||||
@@ -18,17 +17,18 @@ export function CustomShellButton() {
|
|||||||
const customShell = TankopediaEphemeral.use((state) => state.customShell);
|
const customShell = TankopediaEphemeral.use((state) => state.customShell);
|
||||||
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||||
const [customShellDraft, setCustomShellDraft] = useState(
|
|
||||||
customShell ?? antagonistShell,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCustomShellDraft(customShell ?? antagonistShell);
|
|
||||||
}, [antagonistShell, customShell]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root>
|
<Popover.Root
|
||||||
<Dialog.Trigger>
|
onOpenChange={(open) => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
mutateTankopediaEphemeral((draft) => {
|
||||||
|
draft.customShell = antagonistShell;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover.Trigger>
|
||||||
<IconButton
|
<IconButton
|
||||||
color={customShell ? undefined : 'gray'}
|
color={customShell ? undefined : 'gray'}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@@ -44,185 +44,174 @@ export function CustomShellButton() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Dialog.Trigger>
|
</Popover.Trigger>
|
||||||
|
|
||||||
<Dialog.Content>
|
<Popover.Content>
|
||||||
<Dialog.Title>Custom shell</Dialog.Title>
|
{/* <Popover.Title>Custom shell</Popover.Title> */}
|
||||||
|
|
||||||
<Flex direction="column" gap="2">
|
{customShell && (
|
||||||
<Flex align="center" justify="between">
|
<>
|
||||||
<Text>Type</Text>
|
<Flex direction="column" gap="2">
|
||||||
<Select.Root
|
<Flex align="center" justify="between" gap="4">
|
||||||
value={`${customShellDraft.type}`}
|
<Text>Type</Text>
|
||||||
onValueChange={(value) => {
|
<Select.Root
|
||||||
const type = Number(value) as ShellType;
|
value={`${customShell.type}`}
|
||||||
const modified = { ...customShellDraft, type };
|
onValueChange={(value) => {
|
||||||
|
const type = Number(value) as ShellType;
|
||||||
|
const modified = { ...customShell, type };
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ShellType.AP: {
|
case ShellType.AP: {
|
||||||
modified.normalization = 5;
|
modified.normalization = 5;
|
||||||
modified.ricochet = 70;
|
modified.ricochet = 70;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ShellType.APCR: {
|
case ShellType.APCR: {
|
||||||
modified.normalization = 2;
|
modified.normalization = 2;
|
||||||
modified.ricochet = 70;
|
modified.ricochet = 70;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ShellType.HEAT:
|
case ShellType.HEAT:
|
||||||
case ShellType.HE: {
|
case ShellType.HE: {
|
||||||
modified.normalization = undefined;
|
modified.normalization = undefined;
|
||||||
modified.ricochet = undefined;
|
modified.ricochet = undefined;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
setCustomShellDraft(modified);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Select.Trigger />
|
|
||||||
|
|
||||||
<Select.Content>
|
|
||||||
<Select.Item value={`${ShellType.AP}`}>AP</Select.Item>
|
|
||||||
<Select.Item value={`${ShellType.APCR}`}>APCR</Select.Item>
|
|
||||||
<Select.Item value={`${ShellType.HEAT}`}>HEAT</Select.Item>
|
|
||||||
<Select.Item value={`${ShellType.HE}`}>HE</Select.Item>
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex align="center" justify="between">
|
|
||||||
<Text>Caliber</Text>
|
|
||||||
<TextField.Root
|
|
||||||
style={{ maxWidth: '6rem' }}
|
|
||||||
defaultValue={customShellDraft.caliber}
|
|
||||||
type="number"
|
|
||||||
onBlur={(event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setCustomShellDraft({
|
|
||||||
...customShellDraft,
|
|
||||||
caliber: event.target.valueAsNumber,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onFocus={(event) => event.target.select()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
(event.target as HTMLInputElement).blur();
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
(event.target as HTMLInputElement).valueAsNumber =
|
|
||||||
customShellDraft.caliber;
|
|
||||||
(event.target as HTMLInputElement).blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField.Slot side="right">mm</TextField.Slot>
|
|
||||||
</TextField.Root>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex align="center" justify="between">
|
|
||||||
<Text>Penetration</Text>
|
|
||||||
<TextField.Root
|
|
||||||
style={{ maxWidth: '6rem' }}
|
|
||||||
defaultValue={customShellDraft.penetration.near}
|
|
||||||
type="number"
|
|
||||||
onBlur={(event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setCustomShellDraft({
|
|
||||||
...customShellDraft,
|
|
||||||
penetration: {
|
|
||||||
near: event.target.valueAsNumber,
|
|
||||||
far: event.target.valueAsNumber,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onFocus={(event) => event.target.select()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
(event.target as HTMLInputElement).blur();
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
(event.target as HTMLInputElement).valueAsNumber =
|
|
||||||
customShellDraft.penetration.near;
|
|
||||||
(event.target as HTMLInputElement).blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField.Slot side="right">mm</TextField.Slot>
|
|
||||||
</TextField.Root>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{(customShellDraft.type === ShellType.AP ||
|
|
||||||
customShellDraft.type === ShellType.APCR) && (
|
|
||||||
<>
|
|
||||||
<Flex align="center" justify="between" gap="4" py="1">
|
|
||||||
<Text>Normalization</Text>
|
|
||||||
|
|
||||||
<Flex align="center" gap="2" flexGrow="1" maxWidth="15rem">
|
|
||||||
<Text color="gray">
|
|
||||||
{customShellDraft.normalization ?? 0}°
|
|
||||||
</Text>
|
|
||||||
<Slider
|
|
||||||
min={0}
|
|
||||||
max={90}
|
|
||||||
value={[customShellDraft.normalization ?? 0]}
|
|
||||||
onValueChange={([value]) =>
|
|
||||||
setCustomShellDraft({
|
|
||||||
...customShellDraft,
|
|
||||||
normalization: value,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
</Flex>
|
mutateTankopediaEphemeral((draft) => {
|
||||||
|
draft.customShell = modified;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Trigger />
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value={`${ShellType.AP}`}>AP</Select.Item>
|
||||||
|
<Select.Item value={`${ShellType.APCR}`}>APCR</Select.Item>
|
||||||
|
<Select.Item value={`${ShellType.HEAT}`}>HEAT</Select.Item>
|
||||||
|
<Select.Item value={`${ShellType.HE}`}>HE</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex align="center" justify="between" gap="4" py="1">
|
<Flex align="center" justify="between" gap="4">
|
||||||
<Text>Ricochet</Text>
|
<Text>Caliber</Text>
|
||||||
|
<TextField.Root
|
||||||
<Flex align="center" gap="2" flexGrow="1" maxWidth="15rem">
|
style={{ maxWidth: '6rem' }}
|
||||||
<Text color="gray">{customShellDraft.ricochet ?? 90}°</Text>
|
defaultValue={customShell.caliber}
|
||||||
<Slider
|
type="number"
|
||||||
min={0}
|
onChange={(event) => {
|
||||||
max={90}
|
mutateTankopediaEphemeral((draft) => {
|
||||||
value={[customShellDraft.ricochet ?? 90]}
|
draft.customShell!.caliber = event.target.valueAsNumber;
|
||||||
onValueChange={([value]) =>
|
});
|
||||||
setCustomShellDraft({
|
}}
|
||||||
...customShellDraft,
|
onFocus={(event) => event.target.select()}
|
||||||
ricochet: value,
|
onKeyDown={(event) => {
|
||||||
})
|
if (event.key === 'Enter') {
|
||||||
|
(event.target as HTMLInputElement).blur();
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
(event.target as HTMLInputElement).valueAsNumber =
|
||||||
|
customShell.caliber;
|
||||||
|
(event.target as HTMLInputElement).blur();
|
||||||
}
|
}
|
||||||
/>
|
}}
|
||||||
</Flex>
|
>
|
||||||
|
<TextField.Slot side="right">mm</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex justify="end" mt="6" gap="2">
|
<Flex align="center" justify="between" gap="4">
|
||||||
<Dialog.Close>
|
<Text>Penetration</Text>
|
||||||
<Button color="red" variant="outline">
|
<TextField.Root
|
||||||
Cancel
|
style={{ maxWidth: '6rem' }}
|
||||||
</Button>
|
defaultValue={customShell.penetration.near}
|
||||||
</Dialog.Close>
|
type="number"
|
||||||
<Button
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
color="red"
|
mutateTankopediaEphemeral((draft) => {
|
||||||
onClick={() => {
|
draft.customShell!.penetration.near =
|
||||||
setCustomShellDraft(antagonistShell);
|
event.target.valueAsNumber;
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
Reset
|
onFocus={(event) => event.target.select()}
|
||||||
</Button>
|
onKeyDown={(event) => {
|
||||||
<Dialog.Close>
|
if (event.key === 'Enter') {
|
||||||
<Button
|
(event.target as HTMLInputElement).blur();
|
||||||
onClick={() => {
|
} else if (event.key === 'Escape') {
|
||||||
invalidate();
|
(event.target as HTMLInputElement).valueAsNumber =
|
||||||
mutateTankopediaEphemeral((draft) => {
|
customShell.penetration.near;
|
||||||
draft.shot = undefined;
|
(event.target as HTMLInputElement).blur();
|
||||||
draft.customShell = customShellDraft;
|
}
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<TextField.Slot side="right">mm</TextField.Slot>
|
||||||
Apply
|
</TextField.Root>
|
||||||
</Button>
|
</Flex>
|
||||||
</Dialog.Close>
|
|
||||||
</Flex>
|
{(customShell.type === ShellType.AP ||
|
||||||
</Dialog.Content>
|
customShell.type === ShellType.APCR) && (
|
||||||
</Dialog.Root>
|
<>
|
||||||
|
<Flex align="center" justify="between" gap="4" py="1">
|
||||||
|
<Text>Normalization</Text>
|
||||||
|
|
||||||
|
<Flex align="center" gap="2" flexGrow="1" maxWidth="15rem">
|
||||||
|
<Text color="gray">
|
||||||
|
{customShell.normalization ?? 0}°
|
||||||
|
</Text>
|
||||||
|
<Slider
|
||||||
|
min={0}
|
||||||
|
max={90}
|
||||||
|
value={[customShell.normalization ?? 0]}
|
||||||
|
onValueChange={([value]) => {
|
||||||
|
mutateTankopediaEphemeral((draft) => {
|
||||||
|
draft.customShell!.normalization = value;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex align="center" justify="between" gap="4" py="1">
|
||||||
|
<Text>Ricochet</Text>
|
||||||
|
|
||||||
|
<Flex align="center" gap="2" flexGrow="1" maxWidth="15rem">
|
||||||
|
<Text color="gray">{customShell.ricochet ?? 90}°</Text>
|
||||||
|
<Slider
|
||||||
|
min={0}
|
||||||
|
max={90}
|
||||||
|
value={[customShell.ricochet ?? 90]}
|
||||||
|
onValueChange={([value]) => {
|
||||||
|
mutateTankopediaEphemeral((draft) => {
|
||||||
|
draft.customShell!.ricochet = value;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex justify="end" mt="6" gap="2">
|
||||||
|
<Popover.Close>
|
||||||
|
<Button color="red" variant="outline">
|
||||||
|
Exit
|
||||||
|
</Button>
|
||||||
|
</Popover.Close>
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
mutateTankopediaEphemeral((draft) => {
|
||||||
|
draft.customShell = antagonistShell;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+15
-11
@@ -53,7 +53,9 @@ interface OptionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Options({ thicknessRange, canvas }: OptionsProps) {
|
export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||||
const customShell = TankopediaEphemeral.use((state) => state.customShell);
|
const hasCustomShell = TankopediaEphemeral.use(
|
||||||
|
(state) => state.customShell !== undefined,
|
||||||
|
);
|
||||||
const display = TankopediaPersistent.useDeferred(
|
const display = TankopediaPersistent.useDeferred(
|
||||||
(state) => state.display,
|
(state) => state.display,
|
||||||
TankopediaDisplay.Model,
|
TankopediaDisplay.Model,
|
||||||
@@ -101,15 +103,17 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
|||||||
style={{ position: 'absolute', transform: 'translateY(-50%)' }}
|
style={{ position: 'absolute', transform: 'translateY(-50%)' }}
|
||||||
align="end"
|
align="end"
|
||||||
>
|
>
|
||||||
<Text color="gray" size={{ initial: '1', sm: '2' }}>
|
{!hasCustomShell && (
|
||||||
{(
|
<Text color="gray" size={{ initial: '1', sm: '2' }}>
|
||||||
resolvePenetrationCoefficient(
|
{(
|
||||||
hasCalibratedShells,
|
resolvePenetrationCoefficient(
|
||||||
antagonistShell.type,
|
hasCalibratedShells,
|
||||||
) * antagonistShell.penetration.near
|
antagonistShell.type,
|
||||||
).toFixed(0)}
|
) * antagonistShell.penetration.near
|
||||||
mm
|
).toFixed(0)}
|
||||||
</Text>
|
mm
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
{antagonistUniqueGuns.length > 1 && (
|
{antagonistUniqueGuns.length > 1 && (
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<Popover.Trigger>
|
<Popover.Trigger>
|
||||||
@@ -189,7 +193,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
|||||||
{antagonistGun.gun_type!.value.base.shells.map((thisShell) => (
|
{antagonistGun.gun_type!.value.base.shells.map((thisShell) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
color={
|
color={
|
||||||
thisShell.id === antagonistShell.id && !customShell
|
thisShell.id === antagonistShell.id && !hasCustomShell
|
||||||
? undefined
|
? undefined
|
||||||
: 'gray'
|
: 'gray'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user