mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: custom shell customizer ui
This commit is contained in:
1 parent
e364d1fda2
commit
9a1c66932a
3 files changed
+257
-57
No files matched your search
+220
@@ -0,0 +1,220 @@
|
||||
import { imgur, ShellType } from '@blitzkit/core';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
Flex,
|
||||
IconButton,
|
||||
Select,
|
||||
Slider,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { invalidate } from '@react-three/fiber';
|
||||
import { useEffect, useState, type ChangeEvent } from 'react';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
|
||||
export function CustomShellButton() {
|
||||
const customShell = TankopediaEphemeral.use((state) => state.customShell);
|
||||
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const [customShellDraft, setCustomShellDraft] = useState(
|
||||
customShell ?? antagonistShell,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomShellDraft(customShell ?? antagonistShell);
|
||||
}, [antagonistShell, customShell]);
|
||||
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<IconButton
|
||||
color={customShell ? undefined : 'gray'}
|
||||
variant="soft"
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
radius="none"
|
||||
>
|
||||
<img
|
||||
alt="custom shell"
|
||||
src={imgur('j2CoXak')}
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>Custom shell</Dialog.Title>
|
||||
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex align="center" justify="between">
|
||||
<Text>Type</Text>
|
||||
<Select.Root
|
||||
value={`${customShellDraft.type}`}
|
||||
onValueChange={(value) => {
|
||||
const type = Number(value) as ShellType;
|
||||
const modified = { ...customShellDraft, type };
|
||||
|
||||
switch (type) {
|
||||
case ShellType.AP: {
|
||||
modified.normalization = 5;
|
||||
modified.ricochet = 70;
|
||||
break;
|
||||
}
|
||||
case ShellType.APCR: {
|
||||
modified.normalization = 2;
|
||||
modified.ricochet = 70;
|
||||
break;
|
||||
}
|
||||
case ShellType.HEAT:
|
||||
case ShellType.HE: {
|
||||
modified.normalization = undefined;
|
||||
modified.ricochet = undefined;
|
||||
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>
|
||||
</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">{customShellDraft.ricochet ?? 90}°</Text>
|
||||
<Slider
|
||||
min={0}
|
||||
max={90}
|
||||
value={[customShellDraft.ricochet ?? 90]}
|
||||
onValueChange={([value]) =>
|
||||
setCustomShellDraft({
|
||||
...customShellDraft,
|
||||
ricochet: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex justify="end" mt="6" gap="2">
|
||||
<Dialog.Close>
|
||||
<Button color="red" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
<Dialog.Close>
|
||||
<Button
|
||||
onClick={() => {
|
||||
invalidate();
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.customShell = customShellDraft;
|
||||
});
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
+35
-57
@@ -42,6 +42,7 @@ 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';
|
||||
@@ -52,6 +53,7 @@ interface OptionsProps {
|
||||
}
|
||||
|
||||
export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
const customShell = TankopediaEphemeral.use((state) => state.customShell);
|
||||
const display = TankopediaPersistent.useDeferred(
|
||||
(state) => state.display,
|
||||
TankopediaDisplay.Model,
|
||||
@@ -184,64 +186,40 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
}}
|
||||
overflow="hidden"
|
||||
>
|
||||
{antagonistGun.gun_type!.value.base.shells.map(
|
||||
(thisShell, shellIndex) => (
|
||||
<IconButton
|
||||
color={
|
||||
thisShell.id === antagonistShell.id ? undefined : 'gray'
|
||||
}
|
||||
variant="soft"
|
||||
key={thisShell.id}
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
invalidate();
|
||||
mutateDuel((draft) => {
|
||||
draft.antagonist.shell = thisShell;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={thisShell.name}
|
||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
),
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
// color={
|
||||
// thisShell.id === antagonistShell.id ? undefined : 'gray'
|
||||
// }
|
||||
variant="soft"
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
invalidate();
|
||||
// mutateDuel((draft) => {
|
||||
// draft.antagonist.shell = thisShell;
|
||||
// });
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="custom shell"
|
||||
src={imgur('j2CoXak')}
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
{antagonistGun.gun_type!.value.base.shells.map((thisShell) => (
|
||||
<IconButton
|
||||
color={
|
||||
thisShell.id === antagonistShell.id && !customShell
|
||||
? undefined
|
||||
: 'gray'
|
||||
}
|
||||
variant="soft"
|
||||
key={thisShell.id}
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
invalidate();
|
||||
mutateDuel((draft) => {
|
||||
draft.antagonist.shell = thisShell;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.customShell = undefined;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
>
|
||||
<img
|
||||
alt={thisShell.name}
|
||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
))}
|
||||
|
||||
<CustomShellButton />
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
createDefaultSkills,
|
||||
fetchSkillDefinitions,
|
||||
ShellDefinition,
|
||||
type ModelDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import type { Vector3 } from 'three';
|
||||
@@ -91,6 +92,7 @@ interface TankopediaEphemeral {
|
||||
| { type: ArmorType.External }
|
||||
);
|
||||
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
||||
customShell?: ShellDefinition;
|
||||
}
|
||||
|
||||
const skillDefinitions = await fetchSkillDefinitions();
|
||||
|
||||
Reference in new issue
Block a user