Merge branch 'main' into ue

This commit is contained in:
TrèsAbhi committed 2025-12-28 12:11:40 -06:00
commit 496e127f64
9 files changed
+309 -76

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
# BlitzKit 1.29.8
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- Added an option to clear provisions and consumables from all tanks in Compare
- Added the ability to choose stock turrets and guns in Mixer if a tank has any
- Added streaks to Guesser
- Introduced an option to partially reveal tanks in Guesser
- This removes the darkness effect but keeps the name hidden
- This will reset your streak to 0
- Regardless of getting the right guess on the tank's name, your response will be counted as incorrect if partially revealed
## Fixes
- The Tankopedia now smoothly resizes when enlarging on interaction
- [Mark Greene] Stock turrets now correctly mount the corresponding stock gun
- The consumable and provision buttons in Compare are no longer tiny
+11 -3
View File
@@ -917,7 +917,13 @@
"next": "Next", "next": "Next",
"no_results": "No results" "no_results": "No results"
}, },
"stats": "{correct} / {total} guessed correctly" "stats": "{correct} / {total} correct, {streak} streak",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"reveal": "Reveal"
}
}, },
"tankopedia": { "tankopedia": {
@@ -1267,10 +1273,12 @@
"upgrade": "Upgrade" "upgrade": "Upgrade"
}, },
"provisions": { "provisions": {
"clear": "Clear" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
}, },
"consumables": { "consumables": {
"clear": "Clear" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
} }
} }
}, },
@@ -277,14 +277,14 @@ export function CompareTable({ stats }: CompareTableProps) {
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>
<Flex align="center" justify="center" gap="3"> <Flex justify="center" gap="3">
<Popover.Root> <Popover.Root>
<Popover.Trigger> <Popover.Trigger>
<Button <Button
variant="ghost" variant="ghost"
radius="large" radius="large"
style={{ style={{
height: "100%", height: "2rem",
width: 12, width: 12,
position: "relative", position: "relative",
}} }}
@@ -345,7 +345,24 @@ export function CompareTable({ stats }: CompareTableProps) {
}); });
}} }}
/> />
<Flex justify="end" mt="4">
<Flex justify="end" mt="4" gap="3">
<Button
variant="ghost"
onClick={() => {
CompareEphemeral.mutate((draft) => {
draft.members.map((member) => {
member.provisions = [];
});
});
}}
>
{
strings.website.tools.compare.table.provisions
.clear_all
}
</Button>
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
@@ -473,7 +490,7 @@ export function CompareTable({ stats }: CompareTableProps) {
variant="ghost" variant="ghost"
radius="large" radius="large"
style={{ style={{
height: "100%", height: "2rem",
width: 12, width: 12,
position: "relative", position: "relative",
}} }}
@@ -536,7 +553,23 @@ export function CompareTable({ stats }: CompareTableProps) {
} }
/> />
<Flex justify="end" mt="4"> <Flex justify="end" mt="4" gap="4">
<Button
variant="ghost"
onClick={() => {
CompareEphemeral.mutate((draft) => {
draft.members.map((member) => {
member.consumables = [];
});
});
}}
>
{
strings.website.tools.compare.table.consumables
.clear_all
}
</Button>
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
@@ -9,12 +9,15 @@ const CONTRAST = 2 ** 2;
export function GuessRenderer() { export function GuessRenderer() {
const guessState = Guess.use((state) => state.guessState); const guessState = Guess.use((state) => state.guessState);
const isRevealed = guessState !== GuessState.NotGuessed; const helpingReveal = Guess.use((state) => state.helpingReveal);
const isRevealed = helpingReveal || guessState !== GuessState.NotGuessed;
return ( return (
<SmartCanvas <SmartCanvas
style={{ style={{
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`, filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${
isRevealed ? 1 : CONTRAST
}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
transitionDuration: isRevealed ? "2s" : undefined, transitionDuration: isRevealed ? "2s" : undefined,
transitionProperty: "filter", transitionProperty: "filter",
}} }}
+52 -1
View File
@@ -7,14 +7,17 @@ import {
import { literals } from "@blitzkit/i18n"; import { literals } from "@blitzkit/i18n";
import { import {
ArrowRightIcon, ArrowRightIcon,
EyeOpenIcon,
MagnifyingGlassIcon, MagnifyingGlassIcon,
PaperPlaneIcon, PaperPlaneIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { import {
AlertDialog,
Box, Box,
Button, Button,
Card, Card,
Flex, Flex,
IconButton,
Spinner, Spinner,
Text, Text,
TextField, TextField,
@@ -43,6 +46,8 @@ export function Guesser() {
const guessState = Guess.use((state) => state.guessState); const guessState = Guess.use((state) => state.guessState);
const correctGuesses = Guess.use((state) => state.correctGuesses); const correctGuesses = Guess.use((state) => state.correctGuesses);
const totalGuesses = Guess.use((state) => state.totalGuesses); const totalGuesses = Guess.use((state) => state.totalGuesses);
const helpingReveal = Guess.use((state) => state.helpingReveal);
const streak = Guess.use((state) => state.streak);
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
@@ -150,6 +155,7 @@ export function Guesser() {
{literals(strings.website.tools.guess.stats, { {literals(strings.website.tools.guess.stats, {
correct: correctGuesses, correct: correctGuesses,
total: totalGuesses, total: totalGuesses,
streak: streak,
})} })}
</Text> </Text>
</Flex> </Flex>
@@ -169,6 +175,48 @@ export function Guesser() {
</TextField.Slot> </TextField.Slot>
</TextField.Root> </TextField.Root>
<AlertDialog.Root>
<AlertDialog.Trigger>
<IconButton
size="3"
disabled={guessState !== GuessState.NotGuessed}
>
<EyeOpenIcon />
</IconButton>
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Title>
{strings.website.tools.guess.help.title}
</AlertDialog.Title>
<AlertDialog.Description>
{strings.website.tools.guess.help.description}
</AlertDialog.Description>
<Flex justify="end" gap="2">
<AlertDialog.Cancel>
<Button variant="outline">
{strings.website.tools.guess.help.cancel}
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button
color="tomato"
onClick={() => {
Guess.mutate((draft) => {
draft.helpingReveal = true;
draft.streak = 0;
});
}}
>
{strings.website.tools.guess.help.reveal}
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<Button <Button
size="3" size="3"
color={ color={
@@ -178,7 +226,8 @@ export function Guesser() {
} }
onClick={() => { onClick={() => {
if (guessState === GuessState.NotGuessed) { if (guessState === GuessState.NotGuessed) {
const correct = selected !== null && selected.id === tank.id; const correct =
selected !== null && selected.id === tank.id && !helpingReveal;
Guess.mutate((draft) => { Guess.mutate((draft) => {
draft.guessState = correct draft.guessState = correct
@@ -186,6 +235,7 @@ export function Guesser() {
: GuessState.Incorrect; : GuessState.Incorrect;
draft.totalGuesses++; draft.totalGuesses++;
draft.correctGuesses += correct ? 1 : 0; draft.correctGuesses += correct ? 1 : 0;
draft.streak = correct ? draft.streak + 1 : 0;
}); });
} else { } else {
const id = Number(ids[Math.floor(Math.random() * ids.length)]); const id = Number(ids[Math.floor(Math.random() * ids.length)]);
@@ -196,6 +246,7 @@ export function Guesser() {
draft.tank = tank; draft.tank = tank;
draft.guessState = GuessState.NotGuessed; draft.guessState = GuessState.NotGuessed;
draft.helpingReveal = false;
input.current.value = ""; input.current.value = "";
}); });
@@ -176,11 +176,17 @@ export function Modules() {
(turret) => turret.id === unlock.id (turret) => turret.id === unlock.id
)!; )!;
if ( // Check if current gun exists in new turret
!draft.protagonist.turret.guns.some( const gunInNewTurret = draft.protagonist.turret.guns.find(
(gun) => gun.id === draft.protagonist.gun.id (gun) => gun.id === draft.protagonist.gun.id
) );
) {
if (gunInNewTurret) {
// Gun exists - get it FROM this turret
draft.protagonist.gun = gunInNewTurret;
draft.protagonist.shell = gunInNewTurret.shells[0];
} else {
// Gun doesn't exist - select top gun from new turret
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!; draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
draft.protagonist.shell = draft.protagonist.gun.shells[0]; draft.protagonist.shell = draft.protagonist.gun.shells[0];
} }
@@ -150,6 +150,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
return ( return (
<SmartCanvas <SmartCanvas
resize={{ debounce: 0 }}
ref={canvas} ref={canvas}
scene={{ fog: new Fog("black", 30, 50) }} scene={{ fog: new Fog("black", 30, 50) }}
gl={{ gl={{
@@ -1,23 +1,31 @@
import { asset } from "@blitzkit/core"; import {
import { CaretRightIcon, UpdateIcon } from "@radix-ui/react-icons"; asset,
GunDefinition,
TankDefinition,
TIER_ROMAN_NUMERALS,
TurretDefinition,
} from "@blitzkit/core";
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 { TankSearch } from "../../../components/TankSearch";
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 { curateMixer } from "../../../core/blitzkit/curateMixer"; import { curateMixer } from "../../../core/blitzkit/curateMixer";
import { import {
LocaleProvider, LocaleProvider,
@@ -31,6 +39,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 +59,147 @@ export function Page({
); );
} }
function ModuleButton({ type ModuleButtonProps = Omit<ButtonProps, "onSelect"> &
MaybeSkeletonComponentProps & {
tank: number;
onSelect: (
tank: TankDefinition,
turret?: TurretDefinition,
gun?: GunDefinition
) => void;
turret?: number;
gun?: number;
};
function ModuleChooser({
skeleton, skeleton,
tank, tank,
onSelect, onSelect,
turret,
gun,
...props ...props
}: Omit<ButtonProps, "onSelect"> & }: ModuleButtonProps) {
MaybeSkeletonComponentProps & {
tank: number;
onSelect: TankSearchProps["onSelect"];
}) {
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
variant={
turret.id === turretDefinition?.id ? "solid" : undefined
}
key={turret.id}
module="turret"
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
onClick={() => {
onSelect?.(tankDefinition, turret);
}}
/>
))}
</Flex>
)}
<TankSearch {turretDefinition && turretDefinition.guns.length > 1 && (
compact <Flex gap="2">
onSelect={(tank) => { {turretDefinition.guns.map((gun) => (
onSelect?.(tank); <ModuleButton
setOpen(false); variant={
}} gun.id === gunDefinition?.id ? "solid" : undefined
/> }
</Dialog.Content> key={gun.id}
</Dialog.Root> module="gun"
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
secondaryDiscriminator={
<Text style={{ fontSize: "0.75em" }}>
{literals(strings.common.units.mm, {
value: gun.shells[0].caliber.toFixed(0),
})}
</Text>
}
onClick={() => {
onSelect?.(tankDefinition, turretDefinition, gun);
}}
/>
))}
</Flex>
)}
</Flex>
</Popover.Content>
</Popover.Root>
)}
</Flex>
); );
} }
@@ -113,9 +212,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 +238,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 +262,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,28 +272,34 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
}} }}
/> />
<ModuleSeparator /> <ModuleSeparator />
<ModuleButton <ModuleChooser
skeleton={skeleton} skeleton={skeleton}
tank={turretId} tank={turretTankId}
onSelect={(tank) => { turret={turretTurretId}
onSelect={(tank, turret) => {
Mixer.mutate((draft) => { Mixer.mutate((draft) => {
draft.turret = { draft.turret = {
tank, tank,
turret: tank.turrets.at(-1)!, turret: turret ?? tank.turrets.at(-1)!,
}; };
}); });
}} }}
/> />
<ModuleSeparator /> <ModuleSeparator />
<ModuleButton <ModuleChooser
skeleton={skeleton} skeleton={skeleton}
tank={gunId} tank={gunTankId}
onSelect={(tank) => { turret={gunTurretId}
gun={gunGunId}
onSelect={(tank, turret, gun) => {
Mixer.mutate((draft) => { Mixer.mutate((draft) => {
draft.gun = { draft.gun = {
tank, tank,
turret: tank.turrets.at(-1)!, turret: turret ?? tank.turrets.at(-1)!,
gun: tank.turrets.at(-1)!.guns.at(-1)!, gun:
gun ??
turret?.guns.at(-1) ??
tank.turrets.at(-1)!.guns.at(-1)!,
}; };
}); });
}} }}
+4
View File
@@ -12,6 +12,8 @@ export interface Guess {
guessState: GuessState; guessState: GuessState;
totalGuesses: number; totalGuesses: number;
correctGuesses: number; correctGuesses: number;
streak: number;
helpingReveal: boolean;
} }
export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
@@ -19,4 +21,6 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
guessState: GuessState.NotGuessed, guessState: GuessState.NotGuessed,
totalGuesses: 0, totalGuesses: 0,
correctGuesses: 0, correctGuesses: 0,
streak: 0,
helpingReveal: false,
})); }));