Merge pull request #198 from Hitjack007/dev

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-01-04 21:18:31 +00:00
commit d5526da1fb
5 files changed
+251 -176

No files matched your search

+6 -1
View File
@@ -762,6 +762,8 @@
"sorting_by": "Sorting by {name}",
"clear": "Clear",
"tier": "Tier {tier}",
"passive": "{name} (Passive)",
"active": "{name} (Active)",
"no_tanks_found": {
"body": "No tanks found.",
@@ -928,7 +930,10 @@
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"reveal": "Reveal"
}
},
"clear": "Clear",
"select_all": "Select all",
"tier": "Tier {tier}"
},
"tankopedia": {
+131 -51
View File
@@ -9,13 +9,17 @@ import {
ArrowRightIcon,
EyeOpenIcon,
MagnifyingGlassIcon,
MixerVerticalIcon,
PaperPlaneIcon,
StarFilledIcon,
TrashIcon,
} from "@radix-ui/react-icons";
import {
AlertDialog,
Box,
Button,
Card,
DropdownMenu,
Flex,
IconButton,
Spinner,
@@ -23,7 +27,7 @@ import {
TextField,
} from "@radix-ui/themes";
import fuzzysort from "fuzzysort";
import { debounce } from "lodash-es";
import { debounce, times } from "lodash-es";
import { useCallback, useEffect, useRef, useState } from "react";
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
import { awaitableTankNames } from "../core/awaitables/tankNames";
@@ -53,6 +57,7 @@ export function Guesser() {
const [searching, setSearching] = useState(false);
const [results, setResults] = useState<TankDefinition[] | null>(null);
const [selected, setSelected] = useState<TankDefinition | null>(null);
const tiers = Guess.use((state) => state.tiers);
const requestSearch = useCallback(() => {
setSelected(null);
@@ -97,58 +102,60 @@ export function Guesser() {
width="100%"
p="4"
maxWidth="25rem"
gap="4"
gap="3"
>
{results !== null && (
<Card variant="classic">
<Box py="2" px="3">
{results.length === 0 && (
<Flex justify="center">
<Text color="gray">
{strings.website.tools.guess.search.no_results}
</Text>
</Flex>
)}
<Box position="relative">
{results !== null && (
<Card variant="classic">
<Box py="2" px="3">
{results.length === 0 && (
<Flex justify="center">
<Text color="gray">
{strings.website.tools.guess.search.no_results}
</Text>
</Flex>
)}
<SearchResults.Root>
{results.map((result) => {
const Icon = classIcons[result.class];
<SearchResults.Root>
{results.map((result) => {
const Icon = classIcons[result.class];
return (
<SearchResults.Item
key={result.id}
onClick={() => {
if (!input.current) return;
return (
<SearchResults.Item
key={result.id}
onClick={() => {
if (!input.current) return;
input.current.value = unwrap(result.name);
setSelected(result);
setResults(null);
}}
text={unwrap(result.name)}
prefix={
<img
style={{ width: "1em", height: "1em" }}
src={asset(`flags/circle/${result.nation}.webp`)}
/>
}
discriminator={
<Flex
align="center"
gap="1"
width="38px"
justify="center"
>
<Icon width="1em" height="1em" />
{TIER_ROMAN_NUMERALS[result.tier]}
</Flex>
}
/>
);
})}
</SearchResults.Root>
</Box>
</Card>
)}
input.current.value = unwrap(result.name);
setSelected(result);
setResults(null);
}}
text={unwrap(result.name)}
prefix={
<img
style={{ width: "1em", height: "1em" }}
src={asset(`flags/circle/${result.nation}.webp`)}
/>
}
discriminator={
<Flex
align="center"
gap="1"
width="38px"
justify="center"
>
<Icon width="1em" height="1em" />
{TIER_ROMAN_NUMERALS[result.tier]}
</Flex>
}
/>
);
})}
</SearchResults.Root>
</Box>
</Card>
)}
</Box>
<Flex justify="center" style={{ userSelect: "none" }}>
<Text>
@@ -161,11 +168,78 @@ export function Guesser() {
</Flex>
<Flex gap="3">
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger>
<IconButton size="3" variant="surface" color="gray">
<MixerVerticalIcon />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{times(10, (index) => {
const tier = 10 - index;
const isSelected = tiers.includes(tier);
return (
<DropdownMenu.CheckboxItem
checked={isSelected}
key={tier}
onClick={(event) => {
event.preventDefault();
Guess.mutate((draft) => {
if (isSelected) {
if (draft.tiers.length === 1) return;
draft.tiers = draft.tiers.filter((t) => t !== tier);
} else {
draft.tiers = [...draft.tiers, tier];
}
draft.streak = 0;
});
}}
>
{literals(strings.website.tools.guess.tier, {
tier: TIER_ROMAN_NUMERALS[tier],
})}
</DropdownMenu.CheckboxItem>
);
})}
<DropdownMenu.Separator />
<DropdownMenu.Item
onClick={(event) => {
event.preventDefault();
Guess.mutate((draft) => {
draft.tiers = Guess.initial.tiers;
});
}}
>
<StarFilledIcon /> {strings.website.tools.guess.select_all}
</DropdownMenu.Item>
<DropdownMenu.Item
color="red"
onClick={(event) => {
event.preventDefault();
Guess.mutate((draft) => {
draft.tiers = [];
});
}}
>
<TrashIcon /> {strings.website.tools.guess.clear}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<TextField.Root
disabled={guessState !== GuessState.NotGuessed}
ref={input}
onChange={requestSearch}
style={{ flex: 1 }}
style={{ flex: 1, minWidth: "14rem" }}
placeholder={strings.website.tools.guess.search.placeholder}
size="3"
variant="classic"
@@ -238,7 +312,13 @@ export function Guesser() {
draft.streak = correct ? draft.streak + 1 : 0;
});
} else {
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
const filteredIds = ids.filter((id) => {
const tank = tankDefinitions.tanks[Number(id)];
return tiers.includes(tank.tier);
});
const id = Number(
filteredIds[Math.floor(Math.random() * filteredIds.length)]
);
const tank = tankDefinitions.tanks[id];
Guess.mutate((draft) => {
@@ -155,7 +155,7 @@ const TIERS = times(10, (i) => 10 - i);
export function FilterControl() {
return (
<Flex align="center" gap="2" wrap="wrap">
<Flex align="center" justify="center" gap="2" wrap="wrap">
<TiersFilter />
<ClassFilter />
<TypeFilter />
@@ -168,8 +168,7 @@ export function FilterControl() {
<ShellFilter />
<ConsumablesFilter />
<ProvisionsFilter />
<AbilitiesFilter />
<PowersFilter />
<GameModeAbilitiesFilter />
<ResetButton />
</Flex>
@@ -1031,22 +1030,33 @@ function ProvisionsFilter() {
);
}
function PowersFilter() {
function GameModeAbilitiesFilter() {
const { unwrap, strings } = useLocale();
const rawAbilities = TankFilters.use((state) => state.abilities);
const rawPowers = TankFilters.use((state) => state.powers);
const abilities =
rawAbilities.length === 0 && rawPowers.length === 0
? Array.from(allGameModeConsumables.values())
: rawAbilities;
const powers =
rawPowers.length === 0
rawPowers.length === 0 && rawAbilities.length === 0
? Array.from(allGameModeProvisions.values())
: rawPowers;
const icons = [
...abilities.map((ability) => `icons/consumables/${ability}.webp`),
...powers.map((power) => `icons/provisions/${power}.webp`),
];
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger>
<Button color="gray" variant="surface">
<Flex>
{powers.slice(0, MAX_ICONS).map((power, index) => (
{icons.slice(0, MAX_ICONS).map((icon, index) => (
<img
key={power}
key={icon}
style={{
filter: "drop-shadow(0 0 var(--space-1) var(--black-a11))",
marginLeft: index > 0 ? "-0.5em" : undefined,
@@ -1054,14 +1064,14 @@ function PowersFilter() {
height: "1.25em",
objectFit: "contain",
}}
src={asset(`icons/provisions/${power}.webp`)}
src={asset(icon)}
/>
))}
{powers.length > MAX_ICONS && (
{icons.length > MAX_ICONS && (
<Text size="1" ml="1">
{literals(strings.common.units.plus, {
value: powers.length - MAX_ICONS,
value: icons.length - MAX_ICONS,
})}
</Text>
)}
@@ -1070,14 +1080,75 @@ function PowersFilter() {
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{gameModeRoles.map(({ gameModeId, provisions }) => {
{gameModeRoles.map(({ gameModeId, consumables, provisions }) => {
const gameMode = gameDefinitions.gameModes[gameModeId];
const hasNeither =
consumables.length === 0 && provisions.length === 0;
if (provisions.length === 0) return null;
if (hasNeither) return null;
const hasBoth = consumables.length > 0 && provisions.length > 0;
return (
<Fragment key={gameModeId}>
<DropdownMenu.Label>{unwrap(gameMode.name)}</DropdownMenu.Label>
{consumables.length > 0 && (
<DropdownMenu.Label>
{hasBoth
? literals(strings.website.common.tank_search.active, {
name: unwrap(gameMode.name),
})
: unwrap(gameMode.name)}
</DropdownMenu.Label>
)}
{consumables.map((consumable) => {
{
const selected = rawAbilities.includes(consumable);
const abilityDefinition =
consumableDefinitions.consumables[consumable];
return (
<DropdownMenu.CheckboxItem
onClick={(event) => {
event.preventDefault();
TankFilters.mutate((draft) => {
if (selected) {
draft.abilities = draft.abilities.filter(
(n) => n !== consumable
);
} else {
draft.abilities = [...draft.abilities, consumable];
}
});
}}
checked={selected}
key={consumable}
>
<img
style={{
width: "1.25em",
height: "1.25em",
objectFit: "contain",
}}
src={asset(`icons/consumables/${consumable}.webp`)}
/>
{unwrap(abilityDefinition.name)}
</DropdownMenu.CheckboxItem>
);
}
})}
{provisions.length > 0 && (
<DropdownMenu.Label>
{hasBoth
? literals(strings.website.common.tank_search.passive, {
name: unwrap(gameMode.name),
})
: unwrap(gameMode.name)}
</DropdownMenu.Label>
)}
{provisions.map((provision) => {
{
@@ -1129,6 +1200,7 @@ function PowersFilter() {
event.preventDefault();
TankFilters.mutate((draft) => {
draft.abilities = [];
draft.powers = [];
});
}}
@@ -1140,113 +1212,3 @@ function PowersFilter() {
</DropdownMenu.Root>
);
}
function AbilitiesFilter() {
const { unwrap, strings } = useLocale();
const rawAbilities = TankFilters.use((state) => state.abilities);
const abilities =
rawAbilities.length === 0
? Array.from(allGameModeConsumables.values())
: rawAbilities;
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger>
<Button color="gray" variant="surface">
<Flex>
{abilities.slice(0, MAX_ICONS).map((ability, index) => (
<img
key={ability}
style={{
filter: "drop-shadow(0 0 var(--space-1) var(--black-a11))",
marginLeft: index > 0 ? "-0.5em" : undefined,
width: "1.25em",
height: "1.25em",
objectFit: "contain",
}}
src={asset(`icons/consumables/${ability}.webp`)}
/>
))}
{abilities.length > MAX_ICONS && (
<Text size="1" ml="1">
{literals(strings.common.units.plus, {
value: abilities.length - MAX_ICONS,
})}
</Text>
)}
</Flex>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{gameModeRoles.map(({ gameModeId, consumables }) => {
const gameMode = gameDefinitions.gameModes[gameModeId];
if (consumables.length === 0) return null;
return (
<Fragment key={gameModeId}>
<DropdownMenu.Label>{unwrap(gameMode.name)}</DropdownMenu.Label>
{consumables.map((consumable) => {
{
const selected = rawAbilities.includes(consumable);
const abilityDefinition =
consumableDefinitions.consumables[consumable];
return (
<DropdownMenu.CheckboxItem
onClick={(event) => {
event.preventDefault();
TankFilters.mutate((draft) => {
if (selected) {
draft.abilities = draft.abilities.filter(
(n) => n !== consumable
);
} else {
draft.abilities = [...draft.abilities, consumable];
}
});
}}
checked={selected}
key={consumable}
>
<img
style={{
width: "1.25em",
height: "1.25em",
objectFit: "contain",
}}
src={asset(`icons/consumables/${consumable}.webp`)}
/>
{unwrap(abilityDefinition.name)}
</DropdownMenu.CheckboxItem>
);
}
})}
</Fragment>
);
})}
<DropdownMenu.Separator />
<DropdownMenu.Item
color="red"
onClick={(event) => {
event.preventDefault();
TankFilters.mutate((draft) => {
draft.abilities = [];
});
}}
>
<TrashIcon />
{strings.website.common.tank_search.clear}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
);
}
@@ -1,5 +1,5 @@
import { Box, Flex, Heading } from "@radix-ui/themes";
import { Suspense } from "react";
import { Suspense, useEffect } from "react";
import { GuessBackground } from "../../../components/GuessBackground";
import { Guesser } from "../../../components/Guesser";
import { GuessRenderer } from "../../../components/GuessRenderer";
@@ -62,6 +62,31 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
const name = unwrap(tank.name);
const fontSize = `min(48vh, ${55 / name.length}vw)`;
const transitionDuration = isRevealed ? "2s" : undefined;
const tiers = Guess.use((state) => state.tiers);
useEffect(() => {
const isTankAlreadyCompliant = tiers.includes(tank.tier);
if (isTankAlreadyCompliant) return;
const filteredIds = ids.filter((id) => {
const tank = tankDefinitions.tanks[Number(id)];
return tiers.includes(tank.tier);
});
if (filteredIds.length === 0) return;
const id = Number(
filteredIds[Math.floor(Math.random() * filteredIds.length)]
);
const newTank = tankDefinitions.tanks[id];
Guess.mutate((draft) => {
draft.tank = newTank;
draft.guessState = GuessState.NotGuessed;
draft.helpingReveal = false;
});
}, [tiers]);
return (
<Flex flexGrow="1" position="relative" overflow="hidden">
+3
View File
@@ -1,4 +1,5 @@
import type { TankDefinition } from "@blitzkit/core";
import { times } from "lodash-es";
import { Varuna } from "varuna";
export enum GuessState {
@@ -14,6 +15,7 @@ export interface Guess {
correctGuesses: number;
streak: number;
helpingReveal: boolean;
tiers: number[];
}
export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
@@ -23,4 +25,5 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
correctGuesses: 0,
streak: 0,
helpingReveal: false,
tiers: times(10, (index) => index + 1),
}));