feat: mixer buttons

This commit is contained in:
TrèsAbhi committed 2025-09-22 13:50:46 -05:00
1 parent d107813c37
commit a646cc1e57
6 files changed
+213 -158

No files matched your search

+3 -1
View File
@@ -867,7 +867,9 @@
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
@@ -1,14 +1,15 @@
import { OrbitControls } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { Suspense } from "react";
import { forwardRef, Suspense } from "react";
import { Lighting } from "../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { SceneProps } from "../Tankopedia/HeroSection/components/TankSandbox/components/SceneProps";
import { MixerFallback } from "./components/MixerFallback";
import { Model } from "./components/Model";
export function MixerScene() {
export const MixerScene = forwardRef<HTMLCanvasElement>((_, ref) => {
return (
<Canvas
ref={ref}
shadows="soft"
style={{ background: "black" }}
camera={{
@@ -31,4 +32,4 @@ export function MixerScene() {
</Suspense>
</Canvas>
);
}
});
@@ -0,0 +1,92 @@
import { CameraIcon, CopyIcon, DownloadIcon } from "@radix-ui/react-icons";
import {
Button,
Flex,
IconButton,
Popover,
type ButtonProps,
} from "@radix-ui/themes";
import type { RefObject } from "react";
import { useLocale } from "../hooks/useLocale";
import { screenshotReadyEvent } from "./Tankopedia/HeroSection/components/TankSandbox/components/SceneProps";
interface Props extends ButtonProps {
canvas: RefObject<HTMLCanvasElement>;
name: string;
}
export function ScreenshotButton({ canvas, name, children, ...props }: Props) {
const { strings } = useLocale();
return (
<Popover.Root>
<Popover.Trigger>
{children ? (
<Button {...props}>
<CameraIcon />
{children}
</Button>
) : (
<IconButton {...props}>
<CameraIcon />
</IconButton>
)}
</Popover.Trigger>
<Popover.Content>
<Flex direction="column" gap="2">
<Popover.Close>
<Button
onClick={() => {
screenshotReadyEvent.dispatch(true);
requestAnimationFrame(() => {
if (!canvas.current) return;
const anchor = document.createElement("a");
anchor.setAttribute("download", `${name}.png`);
anchor.setAttribute(
"href",
canvas.current.toDataURL("image/png")
);
anchor.click();
screenshotReadyEvent.dispatch(false);
});
}}
>
<DownloadIcon />
{strings.website.tools.tankopedia.sandbox.screenshot.download}
</Button>
</Popover.Close>
<Popover.Close>
<Button
variant="outline"
onClick={() => {
screenshotReadyEvent.dispatch(true);
requestAnimationFrame(() => {
if (!canvas.current) return;
canvas.current.toBlob((blob) => {
if (!blob) return;
navigator.clipboard.write([
new ClipboardItem({ "image/png": blob }),
]);
});
screenshotReadyEvent.dispatch(false);
});
}}
>
<CopyIcon />
{strings.website.tools.tankopedia.sandbox.screenshot.copy}
</Button>
</Popover.Close>
</Flex>
</Popover.Content>
</Popover.Root>
);
}
@@ -6,9 +6,6 @@ import {
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import {
CameraIcon,
CopyIcon,
DownloadIcon,
EnterFullScreenIcon,
ExitFullScreenIcon,
EyeOpenIcon,
@@ -39,9 +36,9 @@ import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/co
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
import type { ThicknessRange } from "../../../../Armor/components/StaticArmor";
import { ModuleButton } from "../../../../ModuleButtons/ModuleButton";
import { ScreenshotButton } from "../../../../ScreenshotButton";
import { SmallTankIcon } from "../../../../SmallTankIcon";
import { TankSearch } from "../../../../TankSearch";
import { screenshotReadyEvent } from "../TankSandbox/components/SceneProps";
import { CustomShellButton } from "./components/CustomShellButton";
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
import { QuickInputs } from "./components/QuickInputs";
@@ -440,77 +437,12 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</DropdownMenu.Content>
</DropdownMenu.Root>
<Popover.Root>
<Popover.Trigger>
<IconButton color="gray" variant="ghost">
<CameraIcon />
</IconButton>
</Popover.Trigger>
<Popover.Content>
<Flex direction="column" gap="2">
<Popover.Close>
<Button
onClick={() => {
screenshotReadyEvent.dispatch(true);
requestAnimationFrame(() => {
if (!canvas.current) return;
const anchor = document.createElement("a");
anchor.setAttribute(
"download",
`${protagonistTank.name}.png`
);
anchor.setAttribute(
"href",
canvas.current.toDataURL("image/png")
);
anchor.click();
screenshotReadyEvent.dispatch(false);
});
}}
>
<DownloadIcon />
{
strings.website.tools.tankopedia.sandbox.screenshot
.download
}
</Button>
</Popover.Close>
<Popover.Close>
<Button
variant="outline"
onClick={() => {
screenshotReadyEvent.dispatch(true);
requestAnimationFrame(() => {
if (!canvas.current) return;
canvas.current.toBlob((blob) => {
if (!blob) return;
navigator.clipboard.write([
new ClipboardItem({ "image/png": blob }),
]);
});
screenshotReadyEvent.dispatch(false);
});
}}
>
<CopyIcon />
{
strings.website.tools.tankopedia.sandbox.screenshot
.copy
}
</Button>
</Popover.Close>
</Flex>
</Popover.Content>
</Popover.Root>
<ScreenshotButton
color="gray"
variant="ghost"
canvas={canvas}
name={protagonistTank.slug}
/>
{fullScreenAvailable && (
<IconButton
@@ -0,0 +1,33 @@
import { awaitableModelDefinitions } from "../awaitables/modelDefinitions";
import { awaitableTankDefinitions } from "../awaitables/tankDefinitions";
const tankDefinitions = await awaitableTankDefinitions;
const modelDefinitions = await awaitableModelDefinitions;
const tanks = Object.values(tankDefinitions.tanks).filter((tank) => {
const tankModel = modelDefinitions.models[tank.id];
const turret = tank.turrets.at(-1)!;
const turretModel = tankModel.turrets[turret.id];
return tank.tier >= 10 && !turretModel.yaw;
});
export function curateMixer() {
const hull = tanks[Math.floor(Math.random() * tanks.length)];
const turretTank = tanks[Math.floor(Math.random() * tanks.length)];
const turret =
turretTank.turrets[Math.floor(Math.random() * turretTank.turrets.length)];
const gunTank = tanks[Math.floor(Math.random() * tanks.length)];
const gunTurret =
gunTank.turrets[Math.floor(Math.random() * gunTank.turrets.length)];
const gun = gunTurret.guns[Math.floor(Math.random() * gunTurret.guns.length)];
return {
hull: hull,
turret: { tank: turretTank, turret: turret },
gun: {
tank: gunTank,
turret: gunTurret,
gun: gun,
},
};
}
@@ -1,5 +1,5 @@
import { asset } from "@blitzkit/core";
import { CaretRightIcon } from "@radix-ui/react-icons";
import { CaretRightIcon, UpdateIcon } from "@radix-ui/react-icons";
import {
Box,
Button,
@@ -12,12 +12,12 @@ import {
import { useRef, useState } from "react";
import { MixerScene } from "../../../components/MixerScene";
import { PageWrapper } from "../../../components/PageWrapper";
import { ScreenshotButton } from "../../../components/ScreenshotButton";
import {
TankSearch,
type TankSearchProps,
} from "../../../components/TankSearch";
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { curateMixer } from "../../../core/blitzkit/curateMixer";
import {
LocaleProvider,
useLocale,
@@ -26,48 +26,13 @@ import {
import { Mixer } from "../../../stores/mixer";
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
const tankDefinitions = await awaitableTankDefinitions;
const modelDefinitions = await awaitableModelDefinitions;
const tanks = Object.values(tankDefinitions.tanks).filter((tank) => {
const tankModel = modelDefinitions.models[tank.id];
const turret = tank.turrets.at(-1)!;
const turretModel = tankModel.turrets[turret.id];
return tank.tier >= 10 && !turretModel.yaw;
});
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(
turretTank.current.turrets[
Math.floor(Math.random() * turretTank.current.turrets.length)
]
);
const gunTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
const gunTurret = useRef(
gunTank.current.turrets[
Math.floor(Math.random() * gunTank.current.turrets.length)
]
);
const gun = useRef(
gunTurret.current.guns[
Math.floor(Math.random() * gunTurret.current.guns.length)
]
);
const initial = useRef(curateMixer());
Mixer.useInitialization({
hull: hull.current,
turret: { tank: turretTank.current, turret: turret.current },
gun: {
tank: gunTank.current,
turret: gunTurret.current,
gun: gun.current,
},
});
Mixer.useInitialization(initial.current);
return (
<PageWrapper color="gray" p="0" maxWidth="unset">
@@ -141,60 +106,90 @@ function ModuleSeparator() {
}
function Content({ skeleton }: MaybeSkeletonComponentProps) {
const hullId = Mixer.use((state) => state.hull.id);
const hull = Mixer.use((state) => state.hull);
const turretId = Mixer.use((state) => state.turret.tank.id);
const gunId = Mixer.use((state) => state.gun.tank.id);
const canvas = useRef<HTMLCanvasElement>(null);
const { strings } = useLocale();
return (
<Box flexGrow="1" position="relative">
<Box position="absolute" width="100%" height="100%" top="0" left="0">
{!skeleton && <MixerScene />}
{!skeleton && <MixerScene ref={canvas} />}
</Box>
<Flex
gap="2"
align="center"
direction="column"
position="absolute"
bottom="4"
left="50%"
style={{ transform: "translateX(-50%)" }}
gap="4"
>
<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 gap="2">
<ScreenshotButton
name={hull.slug}
canvas={canvas}
highContrast
variant="surface"
>
{strings.website.tools.mixer.screenshot}
</ScreenshotButton>
<Button
highContrast
variant="surface"
onClick={() => {
const mixed = curateMixer();
Mixer.mutate((draft) => {
Object.assign(draft, mixed);
});
}}
>
<UpdateIcon />
{strings.website.tools.mixer.randomize}
</Button>
</Flex>
<Flex gap="2" align="center">
<ModuleButton
skeleton={skeleton}
tank={hull.id}
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>
</Flex>
</Box>
);