feat: mixer tank selector

This commit is contained in:
TrèsAbhi committed 2025-09-18 10:41:42 -05:00
1 parent 7afce6cbcb
commit 6e0429332d
9 files changed
+161 -45

No files matched your search

+2
View File
@@ -763,6 +763,8 @@
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
+3
View File
@@ -39,6 +39,8 @@ export class Varuna<Type, Arguments = void> {
}
useInitialization(...args: Arguments[]) {
if (this.initialized) return;
const initializedThisMount = useRef(false);
if (initializedThisMount.current) return;
@@ -53,6 +55,7 @@ export class Varuna<Type, Arguments = void> {
private dispatch() {
const state = this.state;
console.log("dispatched");
this.listeners.forEach((callback) => callback(state));
}
@@ -16,7 +16,7 @@ interface TankControlProps {
const provisionDefinitions = await awaitableProvisionDefinitions;
export function TankControl({ index, slug }: TankControlProps) {
const { locale } = useLocale();
const { locale, strings } = useLocale();
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
return (
@@ -44,7 +44,9 @@ export function TankControl({ index, slug }: TankControlProps) {
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title align="center">Switch tanks</Dialog.Title>
<Dialog.Title align="center">
{strings.website.common.tank_search.swap_dialog_title}
</Dialog.Title>
<Flex gap="4" direction="column">
<Flex
@@ -1,9 +1,7 @@
import { Html } from "@react-three/drei";
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
import { jsxTree } from "../../../core/blitzkit/jsxTree";
import { useModel } from "../../../hooks/useModel";
import { Mixer } from "../../../stores/mixer";
import { Selector } from "./Selector";
const modelDefinitions = await awaitableModelDefinitions;
@@ -50,28 +48,6 @@ export function Model() {
});
})}
<Html
position={[
0,
trackModel.origin.z + tankModel.turret_origin.z,
(trackModel.origin.y + tankModel.turret_origin.y) / 2,
]}
>
<Selector>test hull</Selector>
</Html>
<Html
position={[
0,
trackModel.origin.z + tankModel.turret_origin.z,
trackModel.origin.y +
tankModel.turret_origin.y +
turretModel.gun_origin.y,
]}
>
<Selector>test turret</Selector>
</Html>
<group
position={[
tankModel.turret_origin.x +
@@ -1,13 +1,10 @@
import { Grid, type GridProps } from '@radix-ui/themes';
import { Grid, type GridProps } from "@radix-ui/themes";
export function TankCardWrapper(props: GridProps) {
return (
<Grid
py="4"
columns={{
initial: 'repeat(auto-fill, minmax(6rem, 1fr))',
sm: 'repeat(auto-fill, minmax(10rem, 1fr))',
}}
columns="repeat(auto-fill, minmax(7rem, 1fr))"
flow="dense"
gap="4"
gapY="8"
@@ -33,7 +33,7 @@ import { SkeletonTankCard } from "./components/SkeletonTankCard";
import { TankCardWrapper } from "./components/TankCardWrapper";
import { MAX_RECENTLY_VIEWED } from "./constants";
type TankSearchProps = MaybeSkeletonComponentProps &
export type TankSearchProps = MaybeSkeletonComponentProps &
Omit<FlexProps, "onSelect"> & {
compact?: boolean;
onSelect?: (tank: TankDefinition) => void;
@@ -331,7 +331,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title align="center">Select tank</Dialog.Title>
<Dialog.Title align="center">
{strings.website.common.tank_search.select_dialog_title}
</Dialog.Title>
<Tabs.Root
value={tab}
@@ -1,13 +1,30 @@
import { Box } from "@radix-ui/themes";
import { useRef } from "react";
import { asset } from "@blitzkit/core";
import { CaretRightIcon } from "@radix-ui/react-icons";
import {
Box,
Button,
Dialog,
Flex,
Skeleton,
Text,
type ButtonProps,
} from "@radix-ui/themes";
import { useRef, useState } from "react";
import { MixerScene } from "../../../components/MixerScene";
import { PageWrapper } from "../../../components/PageWrapper";
import {
TankSearch,
type TankSearchProps,
} from "../../../components/TankSearch";
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import {
LocaleProvider,
useLocale,
type LocaleAcceptorProps,
} from "../../../hooks/useLocale";
import { Mixer } from "../../../stores/mixer";
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
const tankDefinitions = await awaitableTankDefinitions;
const modelDefinitions = await awaitableModelDefinitions;
@@ -19,7 +36,10 @@ const tanks = Object.values(tankDefinitions.tanks).filter((tank) => {
return tank.tier >= 10 && !turretModel.yaw;
});
export function Page({ locale }: LocaleAcceptorProps) {
export function Page({
locale,
skeleton,
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
const hull = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
const turretTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
const turret = useRef(
@@ -50,18 +70,132 @@ export function Page({ locale }: LocaleAcceptorProps) {
});
return (
<LocaleProvider locale={locale}>
<Content />
</LocaleProvider>
<PageWrapper color="gray" p="0" maxWidth="unset">
<LocaleProvider locale={locale}>
<Content skeleton={skeleton} />
</LocaleProvider>
</PageWrapper>
);
}
function Content() {
function ModuleButton({
skeleton,
tank,
onSelect,
...props
}: Omit<ButtonProps, "onSelect"> &
MaybeSkeletonComponentProps & {
tank: number;
onSelect: TankSearchProps["onSelect"];
}) {
const { strings } = useLocale();
const [open, setOpen] = useState(false);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger>
<Button
variant="surface"
size="4"
style={{ overflow: "clip" }}
{...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>
);
}
function ModuleSeparator() {
return (
<Text size="5" color="gray" trim="end">
<CaretRightIcon width="1em" height="1em" />
</Text>
);
}
function Content({ skeleton }: MaybeSkeletonComponentProps) {
const hullId = Mixer.use((state) => state.hull.id);
const turretId = Mixer.use((state) => state.turret.tank.id);
const gunId = Mixer.use((state) => state.gun.tank.id);
return (
<Box flexGrow="1" position="relative">
<Box position="absolute" width="100%" height="100%" top="0" left="0">
<MixerScene />
{!skeleton && <MixerScene />}
</Box>
<Flex
gap="2"
align="center"
position="absolute"
bottom="4"
left="50%"
style={{ transform: "translateX(-50%)" }}
>
<ModuleButton
skeleton={skeleton}
tank={hullId}
onSelect={(tank) => {
Mixer.mutate((draft) => {
draft.hull = tank;
});
}}
/>
<ModuleSeparator />
<ModuleButton
skeleton={skeleton}
tank={turretId}
onSelect={(tank) => {
Mixer.mutate((draft) => {
draft.turret = {
tank,
turret: tank.turrets.at(-1)!,
};
});
}}
/>
<ModuleSeparator />
<ModuleButton
skeleton={skeleton}
tank={gunId}
onSelect={(tank) => {
Mixer.mutate((draft) => {
draft.gun = {
tank,
turret: tank.turrets.at(-1)!,
gun: tank.turrets.at(-1)!.guns.at(-1)!,
};
});
}}
/>
</Flex>
</Box>
);
}
@@ -9,7 +9,7 @@ const { locale } = Astro.params;
---
<BaseLayout integrated>
<PageWrapper color="gray" p="0" maxWidth="unset">
<Page {locale} client:load />
</PageWrapper>
</BaseLayout>
<Page {locale} client:only="react">
<Page {locale} skeleton slot="fallback" />
</Page>
</BaseLayout>