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",
"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": {
@@ -1267,10 +1273,12 @@
"upgrade": "Upgrade"
},
"provisions": {
"clear": "Clear"
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
},
"consumables": {
"clear": "Clear"
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
}
}
},
@@ -277,14 +277,14 @@ export function CompareTable({ stats }: CompareTableProps) {
</Popover.Content>
</Popover.Root>
<Flex align="center" justify="center" gap="3">
<Flex justify="center" gap="3">
<Popover.Root>
<Popover.Trigger>
<Button
variant="ghost"
radius="large"
style={{
height: "100%",
height: "2rem",
width: 12,
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
variant="ghost"
onClick={() => {
@@ -473,7 +490,7 @@ export function CompareTable({ stats }: CompareTableProps) {
variant="ghost"
radius="large"
style={{
height: "100%",
height: "2rem",
width: 12,
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
variant="ghost"
onClick={() => {
@@ -9,12 +9,15 @@ const CONTRAST = 2 ** 2;
export function GuessRenderer() {
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 (
<SmartCanvas
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,
transitionProperty: "filter",
}}
+52 -1
View File
@@ -7,14 +7,17 @@ import {
import { literals } from "@blitzkit/i18n";
import {
ArrowRightIcon,
EyeOpenIcon,
MagnifyingGlassIcon,
PaperPlaneIcon,
} from "@radix-ui/react-icons";
import {
AlertDialog,
Box,
Button,
Card,
Flex,
IconButton,
Spinner,
Text,
TextField,
@@ -43,6 +46,8 @@ export function Guesser() {
const guessState = Guess.use((state) => state.guessState);
const correctGuesses = Guess.use((state) => state.correctGuesses);
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 input = useRef<HTMLInputElement>(null);
const [searching, setSearching] = useState(false);
@@ -150,6 +155,7 @@ export function Guesser() {
{literals(strings.website.tools.guess.stats, {
correct: correctGuesses,
total: totalGuesses,
streak: streak,
})}
</Text>
</Flex>
@@ -169,6 +175,48 @@ export function Guesser() {
</TextField.Slot>
</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
size="3"
color={
@@ -178,7 +226,8 @@ export function Guesser() {
}
onClick={() => {
if (guessState === GuessState.NotGuessed) {
const correct = selected !== null && selected.id === tank.id;
const correct =
selected !== null && selected.id === tank.id && !helpingReveal;
Guess.mutate((draft) => {
draft.guessState = correct
@@ -186,6 +235,7 @@ export function Guesser() {
: GuessState.Incorrect;
draft.totalGuesses++;
draft.correctGuesses += correct ? 1 : 0;
draft.streak = correct ? draft.streak + 1 : 0;
});
} else {
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
@@ -196,6 +246,7 @@ export function Guesser() {
draft.tank = tank;
draft.guessState = GuessState.NotGuessed;
draft.helpingReveal = false;
input.current.value = "";
});
@@ -176,11 +176,17 @@ export function Modules() {
(turret) => turret.id === unlock.id
)!;
if (
!draft.protagonist.turret.guns.some(
(gun) => gun.id === draft.protagonist.gun.id
)
) {
// Check if current gun exists in new turret
const gunInNewTurret = draft.protagonist.turret.guns.find(
(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.shell = draft.protagonist.gun.shells[0];
}
@@ -150,6 +150,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
return (
<SmartCanvas
resize={{ debounce: 0 }}
ref={canvas}
scene={{ fog: new Fog("black", 30, 50) }}
gl={{
@@ -1,23 +1,31 @@
import { asset } from "@blitzkit/core";
import { CaretRightIcon, UpdateIcon } from "@radix-ui/react-icons";
import {
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 {
Box,
Button,
Dialog,
Flex,
IconButton,
Popover,
Skeleton,
Text,
type ButtonProps,
} from "@radix-ui/themes";
import { useRef, useState } from "react";
import { MixerScene } from "../../../components/MixerScene";
import { ModuleButton } from "../../../components/ModuleButtons/ModuleButton";
import { PageWrapper } from "../../../components/PageWrapper";
import { ScreenshotButton } from "../../../components/ScreenshotButton";
import {
TankSearch,
type TankSearchProps,
} from "../../../components/TankSearch";
import { TankSearch } from "../../../components/TankSearch";
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { curateMixer } from "../../../core/blitzkit/curateMixer";
import {
LocaleProvider,
@@ -31,6 +39,7 @@ import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonCo
const modelDefinition = await awaitableModelDefinitions.then(
({ models }) => models[1]
);
const tankDefinitions = await awaitableTankDefinitions;
export function Page({
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,
tank,
onSelect,
turret,
gun,
...props
}: Omit<ButtonProps, "onSelect"> &
MaybeSkeletonComponentProps & {
tank: number;
onSelect: TankSearchProps["onSelect"];
}) {
}: ModuleButtonProps) {
const { strings } = useLocale();
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 (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger>
<Button
variant="surface"
size="4"
style={{ overflow: "clip" }}
{...props}
>
{skeleton ? (
<Skeleton width="4em" height="2em" />
) : (
<img
<Flex>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger>
<Button
variant="surface"
size={{ initial: "3", md: "4" }}
style={{
overflow: "clip",
borderTopRightRadius: subSelector ? 0 : undefined,
borderBottomRightRadius: subSelector ? 0 : undefined,
}}
{...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={{
width: "4em",
height: "4em",
objectFit: "contain",
overflow: "clip",
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
marginLeft: -1,
}}
src={asset(`icons/tanks/big/${tank}.webp`)}
/>
)}
</Button>
</Dialog.Trigger>
{...props}
>
<CaretUpIcon />
</IconButton>
</Popover.Trigger>
<Dialog.Content>
<Dialog.Title>
{strings.website.common.tank_search.select_dialog_title}
</Dialog.Title>
<Popover.Content>
<Flex direction="column" gap="2">
{tankDefinition.turrets.length > 1 && (
<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
compact
onSelect={(tank) => {
onSelect?.(tank);
setOpen(false);
}}
/>
</Dialog.Content>
</Dialog.Root>
{turretDefinition && turretDefinition.guns.length > 1 && (
<Flex gap="2">
{turretDefinition.guns.map((gun) => (
<ModuleButton
variant={
gun.id === gunDefinition?.id ? "solid" : undefined
}
key={gun.id}
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) {
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 hullTankId = Mixer.use((state) => state.hull);
const turretTankId = Mixer.use((state) => state.turret.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 { strings } = useLocale();
@@ -136,7 +238,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
>
<Flex gap="2">
<ScreenshotButton
name={hull.slug}
name={hullTankId.slug}
canvas={canvas}
highContrast
variant="surface"
@@ -160,9 +262,9 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
</Flex>
<Flex gap="2" align="center">
<ModuleButton
<ModuleChooser
skeleton={skeleton}
tank={hull.id}
tank={hullTankId.id}
onSelect={(tank) => {
Mixer.mutate((draft) => {
draft.hull = tank;
@@ -170,28 +272,34 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
}}
/>
<ModuleSeparator />
<ModuleButton
<ModuleChooser
skeleton={skeleton}
tank={turretId}
onSelect={(tank) => {
tank={turretTankId}
turret={turretTurretId}
onSelect={(tank, turret) => {
Mixer.mutate((draft) => {
draft.turret = {
tank,
turret: tank.turrets.at(-1)!,
turret: turret ?? tank.turrets.at(-1)!,
};
});
}}
/>
<ModuleSeparator />
<ModuleButton
<ModuleChooser
skeleton={skeleton}
tank={gunId}
onSelect={(tank) => {
tank={gunTankId}
turret={gunTurretId}
gun={gunGunId}
onSelect={(tank, turret, gun) => {
Mixer.mutate((draft) => {
draft.gun = {
tank,
turret: tank.turrets.at(-1)!,
gun: tank.turrets.at(-1)!.guns.at(-1)!,
turret: turret ?? tank.turrets.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;
totalGuesses: number;
correctGuesses: number;
streak: number;
helpingReveal: boolean;
}
export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
@@ -19,4 +21,6 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
guessState: GuessState.NotGuessed,
totalGuesses: 0,
correctGuesses: 0,
streak: 0,
helpingReveal: false,
}));