feat: module selection

This commit is contained in:
TrèsAbhi committed 2025-12-27 19:21:22 -06:00
1 parent 6a280982d0
commit 581d889e9b
1 file changed
+138 -52
@@ -1,16 +1,20 @@
import { asset } from "@blitzkit/core"; import { asset, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { CaretRightIcon, UpdateIcon } from "@radix-ui/react-icons"; import { literals } from "@blitzkit/i18n";
import { CaretRightIcon, CaretUpIcon, UpdateIcon } from "@radix-ui/react-icons";
import { import {
Box, Box,
Button, Button,
Dialog, Dialog,
Flex, Flex,
IconButton,
Popover,
Skeleton, Skeleton,
Text, Text,
type ButtonProps, type ButtonProps,
} from "@radix-ui/themes"; } from "@radix-ui/themes";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { MixerScene } from "../../../components/MixerScene"; import { MixerScene } from "../../../components/MixerScene";
import { ModuleButton } from "../../../components/ModuleButtons/ModuleButton";
import { PageWrapper } from "../../../components/PageWrapper"; import { PageWrapper } from "../../../components/PageWrapper";
import { ScreenshotButton } from "../../../components/ScreenshotButton"; import { ScreenshotButton } from "../../../components/ScreenshotButton";
import { import {
@@ -18,6 +22,7 @@ import {
type TankSearchProps, type TankSearchProps,
} from "../../../components/TankSearch"; } from "../../../components/TankSearch";
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions"; import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { curateMixer } from "../../../core/blitzkit/curateMixer"; import { curateMixer } from "../../../core/blitzkit/curateMixer";
import { import {
LocaleProvider, LocaleProvider,
@@ -31,6 +36,7 @@ import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonCo
const modelDefinition = await awaitableModelDefinitions.then( const modelDefinition = await awaitableModelDefinitions.then(
({ models }) => models[1] ({ models }) => models[1]
); );
const tankDefinitions = await awaitableTankDefinitions;
export function Page({ export function Page({
locale, locale,
@@ -50,57 +56,131 @@ export function Page({
); );
} }
function ModuleButton({ type ModuleButtonProps = Omit<ButtonProps, "onSelect"> &
skeleton,
tank,
onSelect,
...props
}: Omit<ButtonProps, "onSelect"> &
MaybeSkeletonComponentProps & { MaybeSkeletonComponentProps & {
tank: number; tank: number;
onSelect: TankSearchProps["onSelect"]; onSelect: TankSearchProps["onSelect"];
}) { turret?: number;
gun?: number;
};
function ModuleChooser({
skeleton,
tank,
onSelect,
turret,
gun,
...props
}: ModuleButtonProps) {
const { strings } = useLocale(); const { strings } = useLocale();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const tankDefinition = tankDefinitions.tanks[tank];
const turretDefinition = tankDefinition.turrets.find((t) => t.id === turret);
const gunDefinition = turretDefinition?.guns.find((g) => g.id === gun);
const subSelector =
turret !== undefined &&
(tankDefinition.turrets.length > 1 ||
(gun !== undefined && turretDefinition!.guns.length > 1));
return ( return (
<Dialog.Root open={open} onOpenChange={setOpen}> <Flex>
<Dialog.Trigger> <Dialog.Root open={open} onOpenChange={setOpen}>
<Button <Dialog.Trigger>
variant="surface" <Button
size="4" variant="surface"
style={{ overflow: "clip" }} size={{ initial: "3", md: "4" }}
{...props} style={{
> overflow: "clip",
{skeleton ? ( borderTopRightRadius: subSelector ? 0 : undefined,
<Skeleton width="4em" height="2em" /> borderBottomRightRadius: subSelector ? 0 : undefined,
) : ( }}
<img {...props}
>
{skeleton ? (
<Skeleton width="4em" height="2em" />
) : (
<img
style={{
width: "4em",
height: "4em",
objectFit: "contain",
}}
src={asset(`icons/tanks/big/${tank}.webp`)}
/>
)}
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>
{strings.website.common.tank_search.select_dialog_title}
</Dialog.Title>
<TankSearch
compact
onSelect={(tank) => {
onSelect?.(tank);
setOpen(false);
}}
/>
</Dialog.Content>
</Dialog.Root>
{subSelector && (
<Popover.Root>
<Popover.Trigger>
<IconButton
variant="surface"
size={{ initial: "3", md: "4" }}
style={{ style={{
width: "4em", overflow: "clip",
height: "4em", borderTopLeftRadius: 0,
objectFit: "contain", borderBottomLeftRadius: 0,
marginLeft: -1,
}} }}
src={asset(`icons/tanks/big/${tank}.webp`)} {...props}
/> >
)} <CaretUpIcon />
</Button> </IconButton>
</Dialog.Trigger> </Popover.Trigger>
<Dialog.Content> <Popover.Content>
<Dialog.Title> <Flex direction="column" gap="2">
{strings.website.common.tank_search.select_dialog_title} {tankDefinition.turrets.length > 1 && (
</Dialog.Title> <Flex gap="2">
{tankDefinition.turrets.map((turret) => (
<ModuleButton
key={turret.id}
module="turret"
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
/>
))}
</Flex>
)}
<TankSearch {turretDefinition && turretDefinition.guns.length > 1 && (
compact <Flex gap="2">
onSelect={(tank) => { {turretDefinition.guns.map((gun) => (
onSelect?.(tank); <ModuleButton
setOpen(false); key={gun.id}
}} module="gun"
/> discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
</Dialog.Content> secondaryDiscriminator={
</Dialog.Root> <Text style={{ fontSize: "0.75em" }}>
{literals(strings.common.units.mm, {
value: gun.shells[0].caliber.toFixed(0),
})}
</Text>
}
/>
))}
</Flex>
)}
</Flex>
</Popover.Content>
</Popover.Root>
)}
</Flex>
); );
} }
@@ -113,9 +193,12 @@ function ModuleSeparator() {
} }
function Content({ skeleton }: MaybeSkeletonComponentProps) { function Content({ skeleton }: MaybeSkeletonComponentProps) {
const hull = Mixer.use((state) => state.hull); const hullTankId = Mixer.use((state) => state.hull);
const turretId = Mixer.use((state) => state.turret.tank.id); const turretTankId = Mixer.use((state) => state.turret.tank.id);
const gunId = Mixer.use((state) => state.gun.tank.id); const turretTurretId = Mixer.use((state) => state.turret.turret.id);
const gunTankId = Mixer.use((state) => state.gun.tank.id);
const gunTurretId = Mixer.use((state) => state.gun.turret.id);
const gunGunId = Mixer.use((state) => state.gun.gun.id);
const canvas = useRef<HTMLCanvasElement>(null!); const canvas = useRef<HTMLCanvasElement>(null!);
const { strings } = useLocale(); const { strings } = useLocale();
@@ -136,7 +219,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
> >
<Flex gap="2"> <Flex gap="2">
<ScreenshotButton <ScreenshotButton
name={hull.slug} name={hullTankId.slug}
canvas={canvas} canvas={canvas}
highContrast highContrast
variant="surface" variant="surface"
@@ -160,9 +243,9 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
</Flex> </Flex>
<Flex gap="2" align="center"> <Flex gap="2" align="center">
<ModuleButton <ModuleChooser
skeleton={skeleton} skeleton={skeleton}
tank={hull.id} tank={hullTankId.id}
onSelect={(tank) => { onSelect={(tank) => {
Mixer.mutate((draft) => { Mixer.mutate((draft) => {
draft.hull = tank; draft.hull = tank;
@@ -170,9 +253,10 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
}} }}
/> />
<ModuleSeparator /> <ModuleSeparator />
<ModuleButton <ModuleChooser
skeleton={skeleton} skeleton={skeleton}
tank={turretId} tank={turretTankId}
turret={turretTurretId}
onSelect={(tank) => { onSelect={(tank) => {
Mixer.mutate((draft) => { Mixer.mutate((draft) => {
draft.turret = { draft.turret = {
@@ -183,9 +267,11 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
}} }}
/> />
<ModuleSeparator /> <ModuleSeparator />
<ModuleButton <ModuleChooser
skeleton={skeleton} skeleton={skeleton}
tank={gunId} tank={gunTankId}
turret={gunTurretId}
gun={gunGunId}
onSelect={(tank) => { onSelect={(tank) => {
Mixer.mutate((draft) => { Mixer.mutate((draft) => {
draft.gun = { draft.gun = {