Compare commits

...
14 Commits
17 changed files with 519 additions and 382 deletions

No files matched your search

+2 -1
View File
@@ -4,8 +4,9 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions
## Features
- Reworked tank search filters to be far more compact to make way for new filters
- [Mark Greene] Added the ability to filter tanks by consumables
- Also added the ability to filter tanks by game-mode abilities and provisions
- Added the ability to filter tanks by provisions and game-mode abilities and powers
## Fixes
@@ -11,56 +11,47 @@ export function checkConsumableProvisionInclusivity(
tank: TankDefinition,
gun: GunDefinition
) {
const included =
!consumableProvision.game_mode_exclusive &&
consumableProvision.include?.every((rule) => {
switch (rule.filter_type!.$case) {
case "tiers":
return (
rule.filter_type!.value.min <= tank.tier &&
tank.tier <= rule.filter_type!.value.max
);
const included = consumableProvision.include?.every((rule) => {
switch (rule.filter_type!.$case) {
case "tiers":
return (
rule.filter_type!.value.min <= tank.tier &&
tank.tier <= rule.filter_type!.value.max
);
case "ids":
return rule.filter_type!.value.ids.includes(tank.id);
case "ids":
return rule.filter_type!.value.ids.includes(tank.id);
case "nations":
return rule.filter_type!.value.nations.includes(tank.nation);
case "nations":
return rule.filter_type!.value.nations.includes(tank.nation);
case "categories":
throw new SyntaxError("Category filtering found in include rule");
}
});
const excluded =
consumableProvision.game_mode_exclusive ||
consumableProvision.exclude?.some((rule) => {
switch (rule.filter_type!.$case) {
case "tiers":
return (
rule.filter_type!.value.min <= tank.tier &&
tank.tier <= rule.filter_type!.value.max
);
case "categories":
throw new SyntaxError("Category filtering found in include rule");
}
});
const excluded = consumableProvision.exclude?.some((rule) => {
switch (rule.filter_type!.$case) {
case "tiers":
return (
rule.filter_type!.value.min <= tank.tier &&
tank.tier <= rule.filter_type!.value.max
);
case "ids":
return rule.filter_type!.value.ids.includes(tank.id);
case "ids":
return rule.filter_type!.value.ids.includes(tank.id);
case "nations":
return rule.filter_type!.value.nations.includes(tank.nation);
case "nations":
return rule.filter_type!.value.nations.includes(tank.nation);
case "categories":
return rule.filter_type!.value.categories.some((category) => {
switch (category) {
case ConsumableTankCategoryFilterCategory.CLIP:
return gun.gun_type!.$case !== "regular";
}
});
}
});
case "categories":
return rule.filter_type!.value.categories.some((category) => {
switch (category) {
case ConsumableTankCategoryFilterCategory.CLIP:
return gun.gun_type!.$case !== "regular";
}
});
}
});
return (
!consumableProvision.game_mode_exclusive &&
consumableProvision.include?.length > 0 &&
included &&
!excluded
);
return consumableProvision.include?.length > 0 && included && !excluded;
}
+17 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"deg_s": "{value}°/s",
"plus": "+{value}"
},
"gun_types": {
"auto_loader": "Auto cargador",
@@ -22,8 +23,8 @@
},
"shells": {
"ap": "AP",
"apcr": "APCR",
"heat": "AEAC",
"ap_cr": "APCR",
"hc": "HEAT",
"he": "AE"
},
"player_count_period": {
@@ -668,6 +669,8 @@
"count_singular": "1 tanque",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"clear": "Limpiar",
"tier": "Tier {tier}",
"no_tanks_found": {
"body": "No se encontraron tanques.",
"search_again": "Intenta buscar de nuevo.",
@@ -814,7 +817,13 @@
"next": "Next",
"no_results": "Sin resultados"
},
"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": "Cancelar",
"reveal": "Reveal"
}
},
"tankopedia": {
"search": "Search a stat...",
@@ -1135,10 +1144,12 @@
"upgrade": "Mejorar"
},
"provisions": {
"clear": "Limpiar"
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
},
"consumables": {
"clear": "Limpiar"
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
}
}
},
+17 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"deg_s": "{value}°/s",
"plus": "+{value}"
},
"gun_types": {
"auto_loader": "Chargement auto",
@@ -22,8 +23,8 @@
},
"shells": {
"ap": "AP",
"apcr": "APCR",
"heat": "HEAT",
"ap_cr": "APCR",
"hc": "HEAT",
"he": "HE"
},
"player_count_period": {
@@ -668,6 +669,8 @@
"count_singular": "1 tank",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"clear": "Clear",
"tier": "Tier {tier}",
"no_tanks_found": {
"body": "No tanks found.",
"search_again": "Try searching again.",
@@ -814,7 +817,13 @@
"next": "Next",
"no_results": "No results"
},
"stats": "{correct} / {total} guessed correctly"
"stats": "{correct} / {total} guessed correctly",
"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": {
"search": "Search a stat...",
@@ -1135,10 +1144,12 @@
"upgrade": "Upgrade"
},
"provisions": {
"clear": "Clear"
"clear": "Clear",
"clear_all": "Clear all tanks"
},
"consumables": {
"clear": "Clear"
"clear": "Clear",
"clear_all": "Clear all tanks"
}
}
},
+17 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"deg_s": "{value}°/s",
"plus": "+{value}"
},
"gun_types": {
"auto_loader": "自動装填砲",
@@ -22,8 +23,8 @@
},
"shells": {
"ap": "AP",
"apcr": "APCR",
"heat": "HEAT",
"ap_cr": "APCR",
"hc": "HEAT",
"he": "HE"
},
"player_count_period": {
@@ -668,6 +669,8 @@
"count_singular": "1 輌",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"clear": "クリア",
"tier": "Tier {tier}",
"no_tanks_found": {
"body": "該当する車輌が存在しません。",
"search_again": "もう一度検索してください。",
@@ -814,7 +817,13 @@
"next": "Next",
"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": {
"search": "Search a stat...",
@@ -1135,10 +1144,12 @@
"upgrade": "最終装備"
},
"provisions": {
"clear": "クリア"
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
},
"consumables": {
"clear": "クリア"
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
}
}
},
+17 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"deg_s": "{value}°/s",
"plus": "+{value}"
},
"gun_types": {
"auto_loader": "Auto loader",
@@ -22,8 +23,8 @@
},
"shells": {
"ap": "AP",
"apcr": "APCR",
"heat": "HEAT",
"ap_cr": "APCR",
"hc": "HEAT",
"he": "OB"
},
"player_count_period": {
@@ -668,6 +669,8 @@
"count_singular": "1 tank",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"clear": "Clear",
"tier": "Tier {tier}",
"no_tanks_found": {
"body": "No tanks found.",
"search_again": "Try searching again.",
@@ -814,7 +817,13 @@
"next": "Next",
"no_results": "Brak wyników"
},
"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": {
"search": "Search a stat...",
@@ -1135,10 +1144,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"
}
}
},
+17 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"deg_s": "{value}°/s",
"plus": "+{value}"
},
"gun_types": {
"auto_loader": "Autocarregador",
@@ -22,8 +23,8 @@
},
"shells": {
"ap": "AP",
"apcr": "APCR",
"heat": "HEAT",
"ap_cr": "APCR",
"hc": "HEAT",
"he": "HE"
},
"player_count_period": {
@@ -668,6 +669,8 @@
"count_singular": "1 tanque",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"clear": "Limpar",
"tier": "Tier {tier}",
"no_tanks_found": {
"body": "Nenhum tanque encontrado.",
"search_again": "Tente pesquisar novamente.",
@@ -814,7 +817,13 @@
"next": "Next",
"no_results": "Sem resultados"
},
"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": "Cancelar",
"reveal": "Reveal"
}
},
"tankopedia": {
"search": "Search a stat...",
@@ -1135,10 +1144,12 @@
"upgrade": "Melhorar"
},
"provisions": {
"clear": "Limpar"
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
},
"consumables": {
"clear": "Limpar"
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
}
}
},
+17 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}оп",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/с"
"deg_s": "{value}°/с",
"plus": "+{value}"
},
"gun_types": {
"auto_loader": "Барабан",
@@ -22,8 +23,8 @@
},
"shells": {
"ap": "ББ",
"apcr": "БП",
"heat": "КС",
"ap_cr": "APCR",
"hc": "HEAT",
"he": "ОФ"
},
"player_count_period": {
@@ -668,6 +669,8 @@
"count_singular": "1 танк",
"count_plural": "Танков: {count}",
"sorting_by": "Сортировать по {name}",
"clear": "Очистить",
"tier": " {tier} уровня",
"no_tanks_found": {
"body": "Танков не найдено.",
"search_again": "Попробуйте повторить поиск.",
@@ -814,7 +817,13 @@
"next": "Следующий",
"no_results": "Ничего не найденo"
},
"stats": "{correct} / {total} угадано верно"
"stats": "{correct} / {total} правильно, серия: {streak}",
"help": {
"title": "Частично раскрыть танк?",
"description": "Показывает танк не показывая название. Этот раунд будет считаться проигранным и прекратит серию!",
"cancel": "Отмена",
"reveal": "Раскрыть"
}
},
"tankopedia": {
"search": "Поиск характеристик...",
@@ -1135,10 +1144,12 @@
"upgrade": "Топ"
},
"provisions": {
"clear": "Очистить"
"clear": "Убрать",
"clear_all": "Убрать со всех"
},
"consumables": {
"clear": "Очистить"
"clear": "Убрать",
"clear_all": "Убрать со всех"
}
}
},
+17 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}од",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}град/c"
"deg_s": "{value}град/c",
"plus": "+{value}"
},
"gun_types": {
"auto_loader": "Магазинна система заряджання",
@@ -22,8 +23,8 @@
},
"shells": {
"ap": "ББ",
"apcr": "БП",
"heat": "КС",
"ap_cr": "APCR",
"hc": "HEAT",
"he": "ОФ"
},
"player_count_period": {
@@ -668,6 +669,8 @@
"count_singular": "1 танк",
"count_plural": "Танків: {count} ",
"sorting_by": "Відсортувати за {name}",
"clear": "Clear",
"tier": "Рівень {tier}",
"no_tanks_found": {
"body": "Нічого не знайдено.",
"search_again": "Спробуйте шукати заново.",
@@ -814,7 +817,13 @@
"next": "Next",
"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": {
"search": "Search a stat...",
@@ -1135,10 +1144,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"
}
}
},
+17 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value} 生命值",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}度°/秒"
"deg_s": "{value}度°/秒",
"plus": "+{value}"
},
"gun_types": {
"auto_loader": "弹夹型坦克",
@@ -22,8 +23,8 @@
},
"shells": {
"ap": "穿甲弹",
"apcr": "硬式复合穿甲弹",
"heat": "反战车高爆弹",
"ap_cr": "APCR",
"hc": "HEAT",
"he": "高爆弹"
},
"player_count_period": {
@@ -668,6 +669,8 @@
"count_singular": "1辆战车",
"count_plural": "{count} 辆坦克",
"sorting_by": "按 {name} 排序",
"clear": "清除",
"tier": "{tier} 阶",
"no_tanks_found": {
"body": "找不到战车。",
"search_again": "请尝试重新搜索。",
@@ -814,7 +817,13 @@
"next": "下一个",
"no_results": "沒有結果"
},
"stats": "猜对了 {correct} / {total}"
"stats": "猜对了 {correct} / {total}",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "取消",
"reveal": "Reveal"
}
},
"tankopedia": {
"search": "搜索属性……",
@@ -1135,10 +1144,12 @@
"upgrade": "顶级配置"
},
"provisions": {
"clear": "清除"
"clear": "清除",
"clear_all": "Clear all tanks"
},
"consumables": {
"clear": "清除"
"clear": "清除",
"clear_all": "Clear all tanks"
}
}
},
+134 -138
View File
@@ -1557,87 +1557,85 @@ export async function definitions() {
const includeRaw = consumable.vehicleFilter?.include.vehicle;
const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle;
if (!entry.game_mode_exclusive) {
if (includeRaw) {
entry.include = [];
if (includeRaw) {
entry.include = [];
if ("minLevel" in includeRaw) {
entry.include.push({
filter_type: {
$case: "tiers",
value: {
min: includeRaw.minLevel,
max: includeRaw.maxLevel,
},
if ("minLevel" in includeRaw) {
entry.include.push({
filter_type: {
$case: "tiers",
value: {
min: includeRaw.minLevel,
max: includeRaw.maxLevel,
},
});
} else if ("name" in includeRaw) {
entry.include.push({
filter_type: {
$case: "ids",
value: {
ids: includeRaw.name.split(/ +/).map((key) => {
return tankStringIdMap[key];
}),
},
},
});
} else if ("name" in includeRaw) {
entry.include.push({
filter_type: {
$case: "ids",
value: {
ids: includeRaw.name.split(/ +/).map((key) => {
return tankStringIdMap[key];
}),
},
});
} else throw new SyntaxError("Unhandled include type");
},
});
} else throw new SyntaxError("Unhandled include type");
if (consumable.vehicleFilter?.include.nations) {
entry.include.push({
filter_type: {
$case: "nations",
value: {
nations: consumable.vehicleFilter.include.nations.split(" "),
},
if (consumable.vehicleFilter?.include.nations) {
entry.include.push({
filter_type: {
$case: "nations",
value: {
nations: consumable.vehicleFilter.include.nations.split(" "),
},
});
}
},
});
}
}
if (excludeRaw) {
entry.exclude = [];
if (excludeRaw) {
entry.exclude = [];
if ("name" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "ids",
value: {
ids: excludeRaw.name.split(/ +/).map((key) => {
return tankStringIdMap[key];
}),
},
if ("name" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "ids",
value: {
ids: excludeRaw.name.split(/ +/).map((key) => {
return tankStringIdMap[key];
}),
},
});
} else if ("extendedTags" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "categories",
value: {
categories: excludeRaw.extendedTags
.split(" ")
.map(
(item) =>
blitzTankFilterDefinitionCategoryToBlitzkit[
item as BlitzTankFilterDefinitionCategory
]
),
},
},
});
} else if ("extendedTags" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "categories",
value: {
categories: excludeRaw.extendedTags
.split(" ")
.map(
(item) =>
blitzTankFilterDefinitionCategoryToBlitzkit[
item as BlitzTankFilterDefinitionCategory
]
),
},
});
} else throw new SyntaxError("Unhandled exclude type");
},
});
} else throw new SyntaxError("Unhandled exclude type");
if (consumable.vehicleFilter?.exclude?.nations) {
entry.exclude!.push({
filter_type: {
$case: "nations",
value: {
nations: consumable.vehicleFilter.exclude.nations.split(" "),
},
if (consumable.vehicleFilter?.exclude?.nations) {
entry.exclude!.push({
filter_type: {
$case: "nations",
value: {
nations: consumable.vehicleFilter.exclude.nations.split(" "),
},
});
}
},
});
}
}
});
@@ -1657,87 +1655,85 @@ export async function definitions() {
const includeRaw = provision.vehicleFilter?.include.vehicle;
const excludeRaw = provision.vehicleFilter?.exclude?.vehicle;
if (!entry.game_mode_exclusive) {
if (includeRaw) {
entry.include = [];
if (includeRaw) {
entry.include = [];
if ("minLevel" in includeRaw) {
entry.include.push({
filter_type: {
$case: "tiers",
value: {
min: includeRaw.minLevel,
max: includeRaw.maxLevel,
},
if ("minLevel" in includeRaw) {
entry.include.push({
filter_type: {
$case: "tiers",
value: {
min: includeRaw.minLevel,
max: includeRaw.maxLevel,
},
});
} else if ("name" in includeRaw) {
entry.include.push({
filter_type: {
$case: "ids",
value: {
ids: includeRaw.name.split(/ +/).map((key) => {
return tankStringIdMap[key];
}),
},
},
});
} else if ("name" in includeRaw) {
entry.include.push({
filter_type: {
$case: "ids",
value: {
ids: includeRaw.name.split(/ +/).map((key) => {
return tankStringIdMap[key];
}),
},
});
} else throw new SyntaxError("Unhandled include type");
},
});
} else throw new SyntaxError("Unhandled include type");
if (provision.vehicleFilter?.include.nations) {
entry.include.push({
filter_type: {
$case: "nations",
value: {
nations: provision.vehicleFilter.include.nations.split(" "),
},
if (provision.vehicleFilter?.include.nations) {
entry.include.push({
filter_type: {
$case: "nations",
value: {
nations: provision.vehicleFilter.include.nations.split(" "),
},
});
}
},
});
}
}
if (excludeRaw) {
entry.exclude = [];
if (excludeRaw) {
entry.exclude = [];
if ("name" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "ids",
value: {
ids: excludeRaw.name
.split(/ +/)
.map((key) => tankStringIdMap[key]),
},
if ("name" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "ids",
value: {
ids: excludeRaw.name
.split(/ +/)
.map((key) => tankStringIdMap[key]),
},
});
} else if ("extendedTags" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "categories",
value: {
categories: excludeRaw.extendedTags
.split(" ")
.map(
(item) =>
blitzTankFilterDefinitionCategoryToBlitzkit[
item as BlitzTankFilterDefinitionCategory
]
),
},
},
});
} else if ("extendedTags" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "categories",
value: {
categories: excludeRaw.extendedTags
.split(" ")
.map(
(item) =>
blitzTankFilterDefinitionCategoryToBlitzkit[
item as BlitzTankFilterDefinitionCategory
]
),
},
});
} else throw new SyntaxError("Unhandled exclude type");
},
});
} else throw new SyntaxError("Unhandled exclude type");
if (provision.vehicleFilter?.exclude?.nations) {
entry.exclude!.push({
filter_type: {
$case: "nations",
value: {
nations: provision.vehicleFilter.exclude.nations.split(" "),
},
if (provision.vehicleFilter?.exclude?.nations) {
entry.exclude!.push({
filter_type: {
$case: "nations",
value: {
nations: provision.vehicleFilter.exclude.nations.split(" "),
},
});
}
},
});
}
}
@@ -1,6 +1,6 @@
import type { Consumable } from '@blitzkit/core';
import { Flex, type FlexProps } from '@radix-ui/themes';
import { ConsumableButton } from './ModuleButtons/ConsumableButton';
import type { Consumable } from "@blitzkit/core";
import { Flex, type FlexProps } from "@radix-ui/themes";
import { ConsumableButton } from "./ModuleButtons/ConsumableButton";
type ConsumablesManagerProps = FlexProps & {
consumables: Consumable[];
@@ -27,6 +27,8 @@ export function ConsumablesManager({
return (
<Flex wrap="wrap" gap="2" {...props}>
{consumables.map((consumable) => {
if (consumable.game_mode_exclusive) return null;
const isSelected = selected.some((id) => id === consumable.id);
return (
@@ -1,5 +1,6 @@
import { Flex } from '@radix-ui/themes';
import { ProvisionButton } from './ModuleButtons/ProvisionButton';
import { Flex } from "@radix-ui/themes";
import { awaitableProvisionDefinitions } from "../core/awaitables/provisionDefinitions";
import { ProvisionButton } from "./ModuleButtons/ProvisionButton";
interface ProvisionsManagerProps {
provisions: number[];
@@ -8,6 +9,8 @@ interface ProvisionsManagerProps {
onChange?: (provisions: number[]) => void;
}
const provisionDefinitions = await awaitableProvisionDefinitions;
export function ProvisionsManager({
provisions,
selected,
@@ -15,15 +18,18 @@ export function ProvisionsManager({
disabled,
}: ProvisionsManagerProps) {
return (
<Flex wrap="wrap" gap="2" justify={{ initial: 'center', sm: 'start' }}>
{provisions.map((provision) => {
const isSelected = selected.includes(provision);
<Flex wrap="wrap" gap="2" justify={{ initial: "center", sm: "start" }}>
{provisions.map((id) => {
const isSelected = selected.includes(id);
const provision = provisionDefinitions.provisions[id];
if (provision.game_mode_exclusive) return null;
return (
<ProvisionButton
key={provision}
key={id}
disabled={disabled && !isSelected}
provision={provision}
provision={id}
selected={isSelected}
onClick={() => {
if (!onChange) return;
@@ -31,9 +37,9 @@ export function ProvisionsManager({
const draft = [...selected];
if (isSelected) {
draft.splice(draft.indexOf(provision), 1);
draft.splice(draft.indexOf(id), 1);
} else {
draft.push(provision);
draft.push(id);
}
onChange(draft);
@@ -12,6 +12,7 @@ import locales from "@blitzkit/i18n/locales.json";
import {
LockClosedIcon,
LockOpen2Icon,
ResetIcon,
TrashIcon,
} from "@radix-ui/react-icons";
import {
@@ -25,10 +26,11 @@ import {
type FlexProps,
} from "@radix-ui/themes";
import { times } from "lodash-es";
import type { ComponentProps, ReactNode } from "react";
import { Fragment, type ComponentProps, type ReactNode } from "react";
import { awaitableConsumableDefinitions } from "../../../core/awaitables/consumableDefinitions";
import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions";
import { awaitableProvisionDefinitions } from "../../../core/awaitables/provisionDefinitions";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { useLocale } from "../../../hooks/useLocale";
import { App } from "../../../stores/app";
import { TankFilters, type CaseType } from "../../../stores/tankFilters";
@@ -44,6 +46,59 @@ import { ScienceOffIcon } from "../../ScienceOffIcon";
const gameDefinitions = await awaitableGameDefinitions;
const consumableDefinitions = await awaitableConsumableDefinitions;
const provisionDefinitions = await awaitableProvisionDefinitions;
const tankDefinitions = await awaitableTankDefinitions;
const gameModeRoleSets: Record<number, Set<number>> = {};
for (const tankIdString in tankDefinitions.tanks) {
const gameMode = tankDefinitions.tanks[tankIdString];
for (const gameModeIdString in gameMode.roles) {
const role = gameMode.roles[gameModeIdString];
const gameModeId = Number(gameModeIdString);
if (gameModeId in gameModeRoleSets) {
gameModeRoleSets[gameModeId].add(role);
} else {
gameModeRoleSets[gameModeId] = new Set([role]);
}
}
}
const gameModeRoles: {
gameModeId: number;
consumables: number[];
provisions: number[];
}[] = [];
const allGameModeConsumables = new Set<number>();
const allGameModeProvisions = new Set<number>();
for (const gameModeIdString in gameModeRoleSets) {
const gameModeId = Number(gameModeIdString);
const roles = gameModeRoleSets[gameModeId];
const consumables = new Set<number>();
const provisions = new Set<number>();
for (const roleId of roles) {
const role = gameDefinitions.roles[roleId];
for (const id of role.consumables) {
consumables.add(id);
allGameModeConsumables.add(id);
}
for (const id of role.provisions) {
provisions.add(id);
allGameModeProvisions.add(id);
}
}
gameModeRoles.push({
gameModeId: gameModeId,
consumables: Array.from(consumables.values()),
provisions: Array.from(provisions.values()),
});
}
const consumablesArray = Object.values(consumableDefinitions.consumables)
.filter(
@@ -52,18 +107,6 @@ const consumablesArray = Object.values(consumableDefinitions.consumables)
consumable.name.locales[locales.default]
)
.map((consumable) => consumable.id);
const abilitiesArray = Array.from(
Object.values(consumableDefinitions.consumables)
.filter((ability) => ability.game_mode_exclusive)
.reduce((uniqueMap, consumable) => {
if (!uniqueMap.has(consumable.name.locales[locales.default])) {
uniqueMap.set(consumable.name.locales[locales.default], consumable.id);
}
return uniqueMap;
}, new Map<string, number>())
.values()
);
const provisionsArray = Array.from(
Object.values(provisionDefinitions.provisions)
.filter(
@@ -80,22 +123,6 @@ const provisionsArray = Array.from(
}, new Map<string, number>())
.values()
);
const powersArray = Array.from(
Object.values(provisionDefinitions.provisions)
.filter(
(provision) =>
provision.game_mode_exclusive && provision.name.locales[locales.default]
)
.reduce((uniqueMap, consumable) => {
if (!uniqueMap.has(consumable.name.locales[locales.default])) {
uniqueMap.set(consumable.name.locales[locales.default], consumable.id);
}
return uniqueMap;
}, new Map<string, number>())
.values()
);
const shellTypeIcons: Record<ShellType, string> = {
[ShellType.AP]: "ap",
[ShellType.APCR]: "ap_cr",
@@ -128,7 +155,7 @@ const TIERS = times(10, (i) => 10 - i);
export function FilterControl() {
return (
<Flex align="center" gap="2" wrap="wrap" justify="center">
<Flex align="center" gap="2" wrap="wrap">
<TiersFilter />
<ClassFilter />
<TypeFilter />
@@ -144,25 +171,22 @@ export function FilterControl() {
<AbilitiesFilter />
<PowersFilter />
<ClearButton />
<ResetButton />
</Flex>
);
}
function ClearButton() {
const { strings } = useLocale();
function ResetButton() {
return (
<Button
<IconButton
color="red"
variant="surface"
variant="solid"
onClick={() => {
TankFilters.set(TankFilters.initial);
}}
>
<TrashIcon />
{strings.website.common.tank_search.clear}
</Button>
<ResetIcon />
</IconButton>
);
}
@@ -1010,7 +1034,10 @@ function ProvisionsFilter() {
function PowersFilter() {
const { unwrap, strings } = useLocale();
const rawPowers = TankFilters.use((state) => state.powers);
const powers = rawPowers.length === 0 ? powersArray : rawPowers;
const powers =
rawPowers.length === 0
? Array.from(allGameModeProvisions.values())
: rawPowers;
return (
<DropdownMenu.Root modal={false}>
@@ -1043,37 +1070,54 @@ function PowersFilter() {
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{powersArray.map((power) => {
const selected = rawPowers.includes(power);
const powerDefinition = provisionDefinitions.provisions[power];
{gameModeRoles.map(({ gameModeId, provisions }) => {
const gameMode = gameDefinitions.gameModes[gameModeId];
if (provisions.length === 0) return null;
return (
<DropdownMenu.CheckboxItem
onClick={(event) => {
event.preventDefault();
<Fragment key={gameModeId}>
<DropdownMenu.Label>{unwrap(gameMode.name)}</DropdownMenu.Label>
TankFilters.mutate((draft) => {
if (selected) {
draft.powers = draft.powers.filter((n) => n !== power);
} else {
draft.powers = [...draft.powers, power];
}
});
}}
checked={selected}
key={power}
>
<img
style={{
width: "1.25em",
height: "1.25em",
objectFit: "contain",
}}
src={asset(`icons/provisions/${power}.webp`)}
/>
{provisions.map((provision) => {
{
const selected = rawPowers.includes(provision);
const powerDefinition =
provisionDefinitions.provisions[provision];
{unwrap(powerDefinition.name)}
</DropdownMenu.CheckboxItem>
return (
<DropdownMenu.CheckboxItem
onClick={(event) => {
event.preventDefault();
TankFilters.mutate((draft) => {
if (selected) {
draft.powers = draft.powers.filter(
(n) => n !== provision
);
} else {
draft.powers = [...draft.powers, provision];
}
});
}}
checked={selected}
key={provision}
>
<img
style={{
width: "1.25em",
height: "1.25em",
objectFit: "contain",
}}
src={asset(`icons/provisions/${provision}.webp`)}
/>
{unwrap(powerDefinition.name)}
</DropdownMenu.CheckboxItem>
);
}
})}
</Fragment>
);
})}
@@ -1100,7 +1144,10 @@ function PowersFilter() {
function AbilitiesFilter() {
const { unwrap, strings } = useLocale();
const rawAbilities = TankFilters.use((state) => state.abilities);
const abilities = rawAbilities.length === 0 ? abilitiesArray : rawAbilities;
const abilities =
rawAbilities.length === 0
? Array.from(allGameModeConsumables.values())
: rawAbilities;
return (
<DropdownMenu.Root modal={false}>
@@ -1133,39 +1180,54 @@ function AbilitiesFilter() {
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{abilitiesArray.map((ability) => {
const selected = rawAbilities.includes(ability);
const abilityDefinition = consumableDefinitions.consumables[ability];
{gameModeRoles.map(({ gameModeId, consumables }) => {
const gameMode = gameDefinitions.gameModes[gameModeId];
if (consumables.length === 0) return null;
return (
<DropdownMenu.CheckboxItem
onClick={(event) => {
event.preventDefault();
<Fragment key={gameModeId}>
<DropdownMenu.Label>{unwrap(gameMode.name)}</DropdownMenu.Label>
TankFilters.mutate((draft) => {
if (selected) {
draft.abilities = draft.abilities.filter(
(n) => n !== ability
);
} else {
draft.abilities = [...draft.abilities, ability];
}
});
}}
checked={selected}
key={ability}
>
<img
style={{
width: "1.25em",
height: "1.25em",
objectFit: "contain",
}}
src={asset(`icons/consumables/${ability}.webp`)}
/>
{consumables.map((consumable) => {
{
const selected = rawAbilities.includes(consumable);
const abilityDefinition =
consumableDefinitions.consumables[consumable];
{unwrap(abilityDefinition.name)}
</DropdownMenu.CheckboxItem>
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>
);
})}
@@ -1,10 +1,10 @@
import { asset } from '@blitzkit/core';
import { Box, Flex, Heading, Text } from '@radix-ui/themes';
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions';
import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { useLocale } from '../../hooks/useLocale';
import { Duel } from '../../stores/duel';
import { asset } from "@blitzkit/core";
import { Box, Flex, Heading, Text } from "@radix-ui/themes";
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
import { useLocale } from "../../hooks/useLocale";
import { Duel } from "../../stores/duel";
const [gameDefinitions, consumableDefinitions, provisionDefinitions] =
await Promise.all([
@@ -39,22 +39,22 @@ export function GameModeSection() {
overflow="hidden"
direction="column"
style={{
borderRadius: 'var(--radius-3)',
borderRadius: "var(--radius-3)",
background: `url(${asset(
`icons/game_mode_banners/${id}.webp`,
`icons/game_mode_banners/${id}.webp`
)})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<Box
flexGrow="1"
style={{
background: `url(${asset(
`icons/game_mode_banners/${id}.webp`,
`icons/game_mode_banners/${id}.webp`
)})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
@@ -64,8 +64,8 @@ export function GameModeSection() {
height="98px"
flexShrink="0"
style={{
backdropFilter: 'blur(4rem)',
WebkitBackdropFilter: 'blur(4rem)',
backdropFilter: "blur(4rem)",
WebkitBackdropFilter: "blur(4rem)",
}}
align="center"
justify="center"
@@ -83,9 +83,9 @@ export function GameModeSection() {
<Flex align="center" gap="1">
<img
style={{
width: '1.5em',
height: '1.5em',
objectFit: 'contain',
width: "1.5em",
height: "1.5em",
objectFit: "contain",
}}
src={asset(`icons/provisions/${id}.webp`)}
alt={unwrap(provisions.name)}
@@ -95,6 +95,7 @@ export function GameModeSection() {
</Text>
);
})}
{gameDefinitions.roles[value].consumables.map((id) => {
const consumable = consumableDefinitions.consumables[id];
@@ -103,9 +104,9 @@ export function GameModeSection() {
<Flex align="center" gap="1">
<img
style={{
width: '1.5em',
height: '1.5em',
objectFit: 'contain',
width: "1.5em",
height: "1.5em",
objectFit: "contain",
}}
src={asset(`icons/consumables/${id}.webp`)}
alt={unwrap(consumable.name)}
@@ -3,12 +3,14 @@ import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit
import { times } from "lodash-es";
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
import type { TankFilters } from "../../stores/tankFilters";
import { awaitableGameDefinitions } from "../awaitables/gameDefinitions";
import { awaitableProvisionDefinitions } from "../awaitables/provisionDefinitions";
const SHELLS = times(3, (index) => index);
const consumableDefinitions = await awaitableConsumableDefinitions;
const provisionDefinitions = await awaitableProvisionDefinitions;
const gameDefinitions = await awaitableGameDefinitions;
export async function filterTank(
filters: TankFilters,
@@ -72,27 +74,15 @@ export async function filterTank(
)
)) &&
(filters.abilities.length === 0 ||
filters.abilities.every((ability) =>
tank.turrets.some((turret) =>
turret.guns.some((gun) =>
checkConsumableProvisionInclusivity(
consumableDefinitions.consumables[ability],
tank,
gun
)
)
Object.values(tank.roles).some((id) =>
gameDefinitions.roles[id as unknown as number].consumables.some(
(consumable) => filters.abilities.includes(consumable)
)
)) &&
(filters.powers.length === 0 ||
filters.powers.every((power) =>
tank.turrets.some((turret) =>
turret.guns.some((gun) =>
checkConsumableProvisionInclusivity(
provisionDefinitions.provisions[power],
tank,
gun
)
)
Object.values(tank.roles).some((id) =>
gameDefinitions.roles[id as unknown as number].provisions.some(
(consumable) => filters.powers.includes(consumable)
)
))
);
@@ -12,7 +12,7 @@ export function Page({
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
return (
<LocaleProvider locale={locale}>
<PageWrapper color="purple" maxWidth="90rem">
<PageWrapper color="purple" maxWidth="80rem">
<TankSearch skeleton={skeleton} />
</PageWrapper>
</LocaleProvider>