mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: mixer tank selector
This commit is contained in:
1 parent
7afce6cbcb
commit
6e0429332d
9 files changed
+161
-45
No files matched your search
@@ -763,6 +763,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Search tanks...",
|
"search_bar_hint": "Search tanks...",
|
||||||
"recent": "Recently viewed",
|
"recent": "Recently viewed",
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export class Varuna<Type, Arguments = void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useInitialization(...args: Arguments[]) {
|
useInitialization(...args: Arguments[]) {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
const initializedThisMount = useRef(false);
|
const initializedThisMount = useRef(false);
|
||||||
|
|
||||||
if (initializedThisMount.current) return;
|
if (initializedThisMount.current) return;
|
||||||
@@ -53,6 +55,7 @@ export class Varuna<Type, Arguments = void> {
|
|||||||
|
|
||||||
private dispatch() {
|
private dispatch() {
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
|
console.log("dispatched");
|
||||||
this.listeners.forEach((callback) => callback(state));
|
this.listeners.forEach((callback) => callback(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface TankControlProps {
|
|||||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||||
|
|
||||||
export function TankControl({ index, slug }: TankControlProps) {
|
export function TankControl({ index, slug }: TankControlProps) {
|
||||||
const { locale } = useLocale();
|
const { locale, strings } = useLocale();
|
||||||
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -44,7 +44,9 @@ export function TankControl({ index, slug }: TankControlProps) {
|
|||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
<Dialog.Content>
|
<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 gap="4" direction="column">
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Html } from "@react-three/drei";
|
|
||||||
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||||
import { jsxTree } from "../../../core/blitzkit/jsxTree";
|
import { jsxTree } from "../../../core/blitzkit/jsxTree";
|
||||||
import { useModel } from "../../../hooks/useModel";
|
import { useModel } from "../../../hooks/useModel";
|
||||||
import { Mixer } from "../../../stores/mixer";
|
import { Mixer } from "../../../stores/mixer";
|
||||||
import { Selector } from "./Selector";
|
|
||||||
|
|
||||||
const modelDefinitions = await awaitableModelDefinitions;
|
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
|
<group
|
||||||
position={[
|
position={[
|
||||||
tankModel.turret_origin.x +
|
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) {
|
export function TankCardWrapper(props: GridProps) {
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
py="4"
|
py="4"
|
||||||
columns={{
|
columns="repeat(auto-fill, minmax(7rem, 1fr))"
|
||||||
initial: 'repeat(auto-fill, minmax(6rem, 1fr))',
|
|
||||||
sm: 'repeat(auto-fill, minmax(10rem, 1fr))',
|
|
||||||
}}
|
|
||||||
flow="dense"
|
flow="dense"
|
||||||
gap="4"
|
gap="4"
|
||||||
gapY="8"
|
gapY="8"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { SkeletonTankCard } from "./components/SkeletonTankCard";
|
|||||||
import { TankCardWrapper } from "./components/TankCardWrapper";
|
import { TankCardWrapper } from "./components/TankCardWrapper";
|
||||||
import { MAX_RECENTLY_VIEWED } from "./constants";
|
import { MAX_RECENTLY_VIEWED } from "./constants";
|
||||||
|
|
||||||
type TankSearchProps = MaybeSkeletonComponentProps &
|
export type TankSearchProps = MaybeSkeletonComponentProps &
|
||||||
Omit<FlexProps, "onSelect"> & {
|
Omit<FlexProps, "onSelect"> & {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
onSelect?: (tank: TankDefinition) => void;
|
onSelect?: (tank: TankDefinition) => void;
|
||||||
|
|||||||
@@ -331,7 +331,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
<Dialog.Content>
|
<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
|
<Tabs.Root
|
||||||
value={tab}
|
value={tab}
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
import { Box } from "@radix-ui/themes";
|
import { asset } from "@blitzkit/core";
|
||||||
import { useRef } from "react";
|
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 { MixerScene } from "../../../components/MixerScene";
|
||||||
|
import { PageWrapper } from "../../../components/PageWrapper";
|
||||||
|
import {
|
||||||
|
TankSearch,
|
||||||
|
type TankSearchProps,
|
||||||
|
} from "../../../components/TankSearch";
|
||||||
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||||
import {
|
import {
|
||||||
LocaleProvider,
|
LocaleProvider,
|
||||||
|
useLocale,
|
||||||
type LocaleAcceptorProps,
|
type LocaleAcceptorProps,
|
||||||
} from "../../../hooks/useLocale";
|
} from "../../../hooks/useLocale";
|
||||||
import { Mixer } from "../../../stores/mixer";
|
import { Mixer } from "../../../stores/mixer";
|
||||||
|
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||||
|
|
||||||
const tankDefinitions = await awaitableTankDefinitions;
|
const tankDefinitions = await awaitableTankDefinitions;
|
||||||
const modelDefinitions = await awaitableModelDefinitions;
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
@@ -19,7 +36,10 @@ const tanks = Object.values(tankDefinitions.tanks).filter((tank) => {
|
|||||||
return tank.tier >= 10 && !turretModel.yaw;
|
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 hull = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
||||||
const turretTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
const turretTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
||||||
const turret = useRef(
|
const turret = useRef(
|
||||||
@@ -50,18 +70,132 @@ export function Page({ locale }: LocaleAcceptorProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocaleProvider locale={locale}>
|
<PageWrapper color="gray" p="0" maxWidth="unset">
|
||||||
<Content />
|
<LocaleProvider locale={locale}>
|
||||||
</LocaleProvider>
|
<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 (
|
return (
|
||||||
<Box flexGrow="1" position="relative">
|
<Box flexGrow="1" position="relative">
|
||||||
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
||||||
<MixerScene />
|
{!skeleton && <MixerScene />}
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ const { locale } = Astro.params;
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout integrated>
|
<BaseLayout integrated>
|
||||||
<PageWrapper color="gray" p="0" maxWidth="unset">
|
<Page {locale} client:only="react">
|
||||||
<Page {locale} client:load />
|
<Page {locale} skeleton slot="fallback" />
|
||||||
</PageWrapper>
|
</Page>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
Reference in new issue
Block a user