mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Merge branch 'main' into ue
This commit is contained in:
commit
17d1526b6f
16 files changed
+337
-188
No files matched your search
@@ -10,7 +10,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
name: Build bot
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
@@ -35,7 +35,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
name: Build website
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# BlitzKit 1.29.10
|
||||
|
||||
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||
|
||||
## Features
|
||||
|
||||
- Combined game mode passive and active abilities into a single dropdown
|
||||
- [Mark Greene] Added tier filters to the Guess game
|
||||
- Changing tiers will break your streak
|
||||
|
||||
## Fixes
|
||||
|
||||
- [Mark Greene] module damage is now properly shown when sorting with it in the tank search
|
||||
- Duplicate abilities in the tank filters are now removed
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "Limpiar",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "No se encontraron tanques.",
|
||||
"search_again": "Intenta buscar de nuevo.",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Cancelar",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "Limpiar",
|
||||
"select_all": "Seleccionar todo",
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "Clear",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "No tanks found.",
|
||||
"search_again": "Try searching again.",
|
||||
@@ -823,7 +825,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": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "クリア",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "該当する車輌が存在しません。",
|
||||
"search_again": "もう一度検索してください。",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Cancel",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "クリア",
|
||||
"select_all": "全て選択",
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "Clear",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "No tanks found.",
|
||||
"search_again": "Try searching again.",
|
||||
@@ -823,7 +825,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": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "Limpar",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "Nenhum tanque encontrado.",
|
||||
"search_again": "Tente pesquisar novamente.",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Cancelar",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "Limpar",
|
||||
"select_all": "Selecionar todos",
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Сортировать по {name}",
|
||||
"clear": "Очистить",
|
||||
"tier": " {tier} уровня",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "Танков не найдено.",
|
||||
"search_again": "Попробуйте повторить поиск.",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Показывает танк не показывая название. Этот раунд будет считаться проигранным и прекратит серию!",
|
||||
"cancel": "Отмена",
|
||||
"reveal": "Раскрыть"
|
||||
}
|
||||
},
|
||||
"clear": "Очистить",
|
||||
"select_all": "Выбрать всё",
|
||||
"tier": " {tier} уровня"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Поиск характеристик...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Відсортувати за {name}",
|
||||
"clear": "Clear",
|
||||
"tier": "Рівень {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "Нічого не знайдено.",
|
||||
"search_again": "Спробуйте шукати заново.",
|
||||
@@ -823,7 +825,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": "Вибрати все",
|
||||
"tier": "Рівень {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "按 {name} 排序",
|
||||
"clear": "清除",
|
||||
"tier": "{tier} 阶",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "找不到战车。",
|
||||
"search_again": "请尝试重新搜索。",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "取消",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "清除",
|
||||
"select_all": "全选",
|
||||
"tier": "{tier} 阶"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "搜索属性……",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
case 'fire.damage':
|
||||
return shell0.armor_damage.toFixed(0);
|
||||
case 'fire.moduleDamage':
|
||||
return shell0.armor_damage.toFixed(0);
|
||||
return shell0.module_damage.toFixed(0);
|
||||
case 'fire.dispersionMoving':
|
||||
return tracks.dispersion_move.toFixed(3);
|
||||
case 'fire.dispersionStill':
|
||||
|
||||
@@ -83,11 +83,36 @@ for (const gameModeIdString in gameModeRoleSets) {
|
||||
for (const roleId of roles) {
|
||||
const role = gameDefinitions.roles[roleId];
|
||||
|
||||
for (const id of role.consumables) {
|
||||
outerLoop: for (const id of role.consumables) {
|
||||
const name =
|
||||
consumableDefinitions.consumables[id].name.locales[locales.default];
|
||||
|
||||
for (const otherConsumable of consumables.values()) {
|
||||
const otherName =
|
||||
consumableDefinitions.consumables[otherConsumable].name.locales[
|
||||
locales.default
|
||||
];
|
||||
|
||||
if (name === otherName) continue outerLoop;
|
||||
}
|
||||
|
||||
consumables.add(id);
|
||||
allGameModeConsumables.add(id);
|
||||
}
|
||||
for (const id of role.provisions) {
|
||||
|
||||
outerLoop: for (const id of role.provisions) {
|
||||
const name =
|
||||
provisionDefinitions.provisions[id].name.locales[locales.default];
|
||||
|
||||
for (const otherProvision of provisions.values()) {
|
||||
const otherName =
|
||||
provisionDefinitions.provisions[otherProvision].name.locales[
|
||||
locales.default
|
||||
];
|
||||
|
||||
if (name === otherName) continue outerLoop;
|
||||
}
|
||||
|
||||
provisions.add(id);
|
||||
allGameModeProvisions.add(id);
|
||||
}
|
||||
@@ -155,7 +180,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 +193,7 @@ export function FilterControl() {
|
||||
<ShellFilter />
|
||||
<ConsumablesFilter />
|
||||
<ProvisionsFilter />
|
||||
<AbilitiesFilter />
|
||||
<PowersFilter />
|
||||
<GameModeAbilitiesFilter />
|
||||
|
||||
<ResetButton />
|
||||
</Flex>
|
||||
@@ -1031,22 +1055,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 +1089,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 +1105,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 +1225,7 @@ function PowersFilter() {
|
||||
event.preventDefault();
|
||||
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.abilities = [];
|
||||
draft.powers = [];
|
||||
});
|
||||
}}
|
||||
@@ -1140,113 +1237,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">
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
Reference in new issue
Block a user