diff --git a/docs/changelogs/1.29.9.md b/docs/changelogs/1.29.9.md new file mode 100644 index 000000000..5f8d4a78e --- /dev/null +++ b/docs/changelogs/1.29.9.md @@ -0,0 +1,14 @@ +# BlitzKit 1.29.9 + +Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update. + +## 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 +- Added the ability to filter tanks by provisions and game-mode abilities and powers + +## Fixes + +- Consumable and provision icons in Compare are now properly sized +- [Mark Greene] Provisions now properly affect terrain resistances diff --git a/packages/core/src/blitzkit/checkConsumableProvisionInclusivity.ts b/packages/core/src/blitzkit/checkConsumableProvisionInclusivity.ts index 471b76482..ad1a90ac8 100644 --- a/packages/core/src/blitzkit/checkConsumableProvisionInclusivity.ts +++ b/packages/core/src/blitzkit/checkConsumableProvisionInclusivity.ts @@ -4,63 +4,54 @@ import { GunDefinition, Provision, TankDefinition, -} from '../protos'; +} from "../protos"; export function checkConsumableProvisionInclusivity( consumableProvision: Consumable | Provision, tank: TankDefinition, - gun: GunDefinition, + 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; } diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 8ccbe5487..94def8954 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -15,7 +15,8 @@ "hp": "{value}hp", "x": "x{value}", "percentage": "{value}%", - "deg_s": "{value}°/s" + "deg_s": "{value}°/s", + "plus": "+{value}" }, "gun_types": { @@ -26,8 +27,8 @@ "shells": { "ap": "AP", - "apcr": "APCR", - "heat": "HEAT", + "ap_cr": "APCR", + "hc": "HEAT", "he": "HE" }, @@ -759,11 +760,15 @@ "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.", "clear_filters": "Try clearing filters." }, + "sort": { "meta.none": "none", "survivability.health": "health", diff --git a/packages/i18n/strings/es.json b/packages/i18n/strings/es.json index ee04808ee..c0f7e18df 100644 --- a/packages/i18n/strings/es.json +++ b/packages/i18n/strings/es.json @@ -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" } } }, diff --git a/packages/i18n/strings/fr.json b/packages/i18n/strings/fr.json index 9cceeb33c..1dcc88113 100644 --- a/packages/i18n/strings/fr.json +++ b/packages/i18n/strings/fr.json @@ -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" } } }, diff --git a/packages/i18n/strings/ja.json b/packages/i18n/strings/ja.json index fa5ee2138..1dca6f499 100644 --- a/packages/i18n/strings/ja.json +++ b/packages/i18n/strings/ja.json @@ -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" } } }, diff --git a/packages/i18n/strings/pl.json b/packages/i18n/strings/pl.json index 1d5b0f9ed..67acd7f24 100644 --- a/packages/i18n/strings/pl.json +++ b/packages/i18n/strings/pl.json @@ -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" } } }, diff --git a/packages/i18n/strings/pt.json b/packages/i18n/strings/pt.json index 5fa7ec4d1..29c85cc3c 100644 --- a/packages/i18n/strings/pt.json +++ b/packages/i18n/strings/pt.json @@ -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" } } }, diff --git a/packages/i18n/strings/ru.json b/packages/i18n/strings/ru.json index ff72db6df..29283091c 100644 --- a/packages/i18n/strings/ru.json +++ b/packages/i18n/strings/ru.json @@ -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": "Убрать со всех" } } }, diff --git a/packages/i18n/strings/uk.json b/packages/i18n/strings/uk.json index 5607cc26e..8e43fdf26 100644 --- a/packages/i18n/strings/uk.json +++ b/packages/i18n/strings/uk.json @@ -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" } } }, diff --git a/packages/i18n/strings/zh.json b/packages/i18n/strings/zh.json index f56585bf1..42a479522 100644 --- a/packages/i18n/strings/zh.json +++ b/packages/i18n/strings/zh.json @@ -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" } } }, diff --git a/packages/scripts/src/buildAssets/definitions.ts b/packages/scripts/src/buildAssets/definitions.ts index 94608ef77..ba83125ae 100644 --- a/packages/scripts/src/buildAssets/definitions.ts +++ b/packages/scripts/src/buildAssets/definitions.ts @@ -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(" "), }, - }); - } + }, + }); } } diff --git a/packages/website/src/components/ConsumablesManager.tsx b/packages/website/src/components/ConsumablesManager.tsx index 932f4d5cf..8ddff5c06 100644 --- a/packages/website/src/components/ConsumablesManager.tsx +++ b/packages/website/src/components/ConsumablesManager.tsx @@ -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 ( {consumables.map((consumable) => { + if (consumable.game_mode_exclusive) return null; + const isSelected = selected.some((id) => id === consumable.id); return ( diff --git a/packages/website/src/components/ProvisionsManager.tsx b/packages/website/src/components/ProvisionsManager.tsx index 4a8a4b740..7ede38889 100644 --- a/packages/website/src/components/ProvisionsManager.tsx +++ b/packages/website/src/components/ProvisionsManager.tsx @@ -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 ( - - {provisions.map((provision) => { - const isSelected = selected.includes(provision); + + {provisions.map((id) => { + const isSelected = selected.includes(id); + const provision = provisionDefinitions.provisions[id]; + + if (provision.game_mode_exclusive) return null; return ( { 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); diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 68ae1e60f..fc51ff253 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,30 +1,39 @@ import { asset, + GunDefinition, ShellType, TANK_CLASSES, + TANK_TYPES, TankType, TIER_ROMAN_NUMERALS, } from "@blitzkit/core"; -import { LockClosedIcon, LockOpen2Icon } from "@radix-ui/react-icons"; +import { literals } from "@blitzkit/i18n"; +import locales from "@blitzkit/i18n/locales.json"; +import { + LockClosedIcon, + LockOpen2Icon, + ResetIcon, + TrashIcon, +} from "@radix-ui/react-icons"; import { Box, + Button, + DropdownMenu, Flex, IconButton, - Inset, - Link, - Popover, Text, Tooltip, - type ButtonProps, type FlexProps, } from "@radix-ui/themes"; -import { isEqual, times } from "lodash-es"; +import { times } from "lodash-es"; +import { Fragment, type ComponentProps, type ReactNode } from "react"; +import { awaitableConsumableDefinitions } from "../../../core/awaitables/consumableDefinitions"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; -import { Var } from "../../../core/radix/var"; +import { awaitableProvisionDefinitions } from "../../../core/awaitables/provisionDefinitions"; +import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions"; import { useLocale } from "../../../hooks/useLocale"; import { App } from "../../../stores/app"; -import { TankFilters } from "../../../stores/tankFilters"; -import { TankSort } from "../../../stores/tankopediaSort"; +import { TankFilters, type CaseType } from "../../../stores/tankFilters"; import { classIcons } from "../../ClassIcon"; import { GunAutoloaderIcon } from "../../GunAutoloaderIcon"; import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon"; @@ -35,7 +44,85 @@ import { ScienceIcon } from "../../ScienceIcon"; import { ScienceOffIcon } from "../../ScienceOffIcon"; const gameDefinitions = await awaitableGameDefinitions; +const consumableDefinitions = await awaitableConsumableDefinitions; +const provisionDefinitions = await awaitableProvisionDefinitions; +const tankDefinitions = await awaitableTankDefinitions; +const gameModeRoleSets: Record> = {}; + +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(); +const allGameModeProvisions = new Set(); + +for (const gameModeIdString in gameModeRoleSets) { + const gameModeId = Number(gameModeIdString); + const roles = gameModeRoleSets[gameModeId]; + const consumables = new Set(); + const provisions = new Set(); + + 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( + (consumable) => + !consumable.game_mode_exclusive && + consumable.name.locales[locales.default] + ) + .map((consumable) => consumable.id); +const provisionsArray = 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()) + .values() +); const shellTypeIcons: Record = { [ShellType.AP]: "ap", [ShellType.APCR]: "ap_cr", @@ -43,18 +130,521 @@ const shellTypeIcons: Record = { [ShellType.HEAT]: "hc", }; -const size: ButtonProps["size"] = { initial: "1", xs: "2" }; +const GUN_TYPE_ICONS: Record< + CaseType, + (props: ComponentProps<"svg">) => ReactNode +> = { + regular: GunRegularIcon, + auto_loader: GunAutoloaderIcon, + auto_reloader: GunAutoreloaderIcon, +}; -function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { +const TANK_TYPE_COLORS: Record = { + [TankType.RESEARCHABLE]: "gray", + [TankType.PREMIUM]: "amber", + [TankType.COLLECTOR]: "blue", +}; + +const GUN_TYPES = Object.keys( + GUN_TYPE_ICONS +) as (keyof typeof GUN_TYPE_ICONS)[]; + +const MAX_ICONS = 4; + +const TIERS = times(10, (i) => 10 - i); + +export function FilterControl() { + return ( + + + + + + + + + + + + + + + + + + + ); +} + +function ResetButton() { + return ( + { + TankFilters.set(TankFilters.initial); + }} + > + + + ); +} + +function TiersFilter() { + const { strings } = useLocale(); + const tiersRaw = TankFilters.use((state) => state.tiers); + const tiers = tiersRaw.length === 0 ? TIERS : tiersRaw; + + return ( + + + + + + + {TIERS.map((tier) => { + const selected = tiersRaw.includes(tier); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.tiers = draft.tiers.filter((t) => t !== tier); + } else { + draft.tiers = [...draft.tiers, tier].sort((a, b) => b - a); + } + }); + }} + checked={selected} + key={tier} + > + {literals(strings.website.common.tank_search.tier, { + tier: TIER_ROMAN_NUMERALS[tier], + })} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.tiers = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + +function NationsFilter() { + const rawNations = TankFilters.use((state) => state.nations); + const nations = + rawNations.length === 0 ? gameDefinitions.nations : rawNations; + const { strings } = useLocale(); + + return ( + + + + + + + {gameDefinitions.nations.map((nation) => { + const selected = rawNations.includes(nation); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.nations = draft.nations.filter((n) => n !== nation); + } else { + draft.nations = [...draft.nations, nation]; + } + }); + }} + checked={selected} + key={nation} + style={{ + position: "relative", + }} + > + + + + + + { + strings.common.nations[ + nation as keyof typeof strings.common.nations + ] + } + + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.nations = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + +function ClassFilter() { + const { strings } = useLocale(); + const rawClasses = TankFilters.use((state) => state.classes); + const classes = rawClasses.length === 0 ? TANK_CLASSES : rawClasses; + + return ( + + + + + + + {TANK_CLASSES.map((tankClass) => { + const selected = rawClasses.includes(tankClass); + const Icon = classIcons[tankClass]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.classes = draft.classes.filter( + (c) => c !== tankClass + ); + } else { + draft.classes = [...draft.classes, tankClass]; + } + }); + }} + checked={selected} + > + + + {strings.common.tank_class_medium[tankClass]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.classes = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + +function GunTypeFilter() { + const rawGunTypes = TankFilters.use((state) => state.gunType); + const gunTypes = rawGunTypes.length === 0 ? GUN_TYPES : rawGunTypes; + const { strings } = useLocale(); + + return ( + + + + + + + {GUN_TYPES.map((gunType) => { + const selected = rawGunTypes.includes(gunType); + const Icon = GUN_TYPE_ICONS[gunType]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.gunType = draft.gunType.filter((c) => c !== gunType); + } else { + draft.gunType = [...draft.gunType, gunType]; + } + }); + }} + checked={selected} + key={gunType} + > + + + {strings.common.gun_types[gunType]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.gunType = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + +function TypeFilter() { + const { strings } = useLocale(); + const rawTypes = TankFilters.use((state) => state.types); + const types = rawTypes.length === 0 ? TANK_TYPES : rawTypes; + + return ( + + + + + + + {TANK_TYPES.map((tankType) => { + const selected = rawTypes.includes(tankType); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.types = draft.types.filter((c) => c !== tankType); + } else { + draft.types = [...draft.types, tankType]; + } + }); + }} + checked={selected} + key={tankType} + > + + + {strings.common.tree_type[tankType]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.types = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + +function ShellFilter() { + return ( + + + + + + ); +} + +function IndividualShellFilter({ + index, + premium, +}: { + index: number; + premium?: boolean; +}) { + const { strings } = useLocale(); const shells = TankFilters.use((state) => state.shells); return ( - - + + @@ -74,70 +664,105 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { /> )} - + - - - - {Object.values(ShellType).map((shellType) => { - if (typeof shellType === "string") return null; + + + {Object.values(ShellType).map((shellType) => { + if (typeof shellType === "string") return null; - const selected = shells[index] === shellType; + const selected = shells[index] === shellType; - return ( - { - const mutated = [...shells] as TankFilters["shells"]; + return ( + { + event.preventDefault(); - mutated[index] = selected ? null : shellType; + const mutated = [...shells] as TankFilters["shells"]; - TankFilters.mutate((draft) => { - draft.shells = mutated; - }); - }} - highContrast={selected} - color={selected ? undefined : "gray"} - > - - - ); - })} - - - - + mutated[index] = selected ? null : shellType; + + TankFilters.mutate((draft) => { + draft.shells = mutated; + }); + }} + color={selected ? undefined : "gray"} + > + + + { + strings.common.shells[ + shellTypeIcons[ + shellType + ] as keyof typeof strings.common.shells + ] + } + + ); + })} + + + + { + event.preventDefault(); + + const mutated = [...shells] as TankFilters["shells"]; + + mutated[index] = null; + + TankFilters.mutate((draft) => { + draft.shells = mutated; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); } -function OwnershipFilter(props: FlexProps) { - const tankFilters = TankFilters.use(); +function OwnershipFilter() { const wargaming = App.use((state) => state.logins.wargaming); + const { strings } = useLocale(); + + return wargaming ? ( + + ) : ( + + + + ); +} + +function OwnershipFilterInternal(props: FlexProps) { + const wargaming = App.use((state) => state.logins.wargaming); + const showOwned = TankFilters.use((state) => state.showOwned); + const showUnowned = TankFilters.use((state) => state.showUnowned); return ( - + { TankFilters.mutate((draft) => { @@ -149,11 +774,14 @@ function OwnershipFilter(props: FlexProps) { { TankFilters.mutate((draft) => { @@ -167,459 +795,458 @@ function OwnershipFilter(props: FlexProps) { ); } -export function FilterControl() { - const tankFilters = TankFilters.use(); - const { strings } = useLocale(); - const clearable = !isEqual(tankFilters, TankFilters.initial); - const wargaming = App.use((state) => state.logins.wargaming); +function TestFilter() { + const showTesting = TankFilters.use((state) => state.showTesting); + const showNonTesting = TankFilters.use((state) => state.showNonTesting); return ( - - - - - {times(5, (index) => { - const tier = index + 1; - const selected = tankFilters.tiers?.includes(tier); + + { + TankFilters.mutate((draft) => { + draft.showNonTesting = !draft.showNonTesting; + }); + }} + > + + - return ( - { - if (tankFilters.tiers.includes(tier)) { - TankFilters.mutate((draft) => { - draft.tiers = draft.tiers.filter((t) => t !== tier); - }); - } else { - TankFilters.mutate((draft) => { - draft.tiers = [...draft.tiers, tier]; - }); - } - }} - > - {TIER_ROMAN_NUMERALS[tier]} - - ); - })} - - - - - - {times(5, (index) => { - const tier = index + 6; - const selected = tankFilters.tiers?.includes(tier); - - return ( - { - if (tankFilters.tiers.includes(tier)) { - TankFilters.mutate((draft) => { - draft.tiers = draft.tiers.filter((t) => t !== tier); - }); - } else { - TankFilters.mutate((draft) => { - draft.tiers = [...draft.tiers, tier]; - }); - } - }} - > - {TIER_ROMAN_NUMERALS[tier]} - - ); - })} - - - - - - {gameDefinitions.nations - .slice(0, Math.ceil(gameDefinitions.nations.length / 2)) - .map((nation) => { - const selected = tankFilters.nations?.includes(nation); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.nations.includes(nation)) { - draft.nations = draft.nations.filter( - (n) => n !== nation - ); - } else { - draft.nations = [...draft.nations, nation]; - } - }); - }} - > - - - ); - })} - - - - - - {gameDefinitions.nations - .slice(Math.ceil(gameDefinitions.nations.length / 2)) - .map((nation) => { - const selected = tankFilters.nations?.includes(nation); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.nations.includes(nation)) { - draft.nations = draft.nations.filter( - (n) => n !== nation - ); - } else { - draft.nations = [...draft.nations, nation]; - } - }); - }} - > - - - ); - })} - - - - - {TANK_CLASSES.map((tankClass) => { - const Icon = classIcons[tankClass]; - const selected = tankFilters.classes?.includes(tankClass); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.classes.includes(tankClass)) { - draft.classes = draft.classes.filter( - (c) => c !== tankClass - ); - } else { - draft.classes = [...draft.classes, tankClass]; - } - }); - }} - > - - - ); - })} - - - - { - TankFilters.mutate((draft) => { - if (tankFilters.gunType.includes("regular")) { - draft.gunType = draft.gunType.filter((t) => t !== "regular"); - } else { - draft.gunType = [...draft.gunType, "regular"]; - } - }); - }} - > - - - { - TankFilters.mutate((draft) => { - if (tankFilters.gunType.includes("auto_loader")) { - draft.gunType = draft.gunType.filter( - (t) => t !== "auto_loader" - ); - } else { - draft.gunType = [...draft.gunType, "auto_loader"]; - } - }); - }} - > - - - { - TankFilters.mutate((draft) => { - if (tankFilters.gunType.includes("auto_reloader")) { - draft.gunType = draft.gunType.filter( - (t) => t !== "auto_reloader" - ); - } else { - draft.gunType = [...draft.gunType, "auto_reloader"]; - } - }); - }} - > - - - - - - - - - - - - { - TankFilters.mutate((draft) => { - if (tankFilters.types.includes(TankType.RESEARCHABLE)) { - draft.types = draft.types.filter( - (t) => t !== TankType.RESEARCHABLE - ); - } else { - draft.types = [...draft.types, TankType.RESEARCHABLE]; - } - }); - }} - > - - - { - TankFilters.mutate((draft) => { - if (tankFilters.types.includes(TankType.PREMIUM)) { - draft.types = draft.types.filter( - (t) => t !== TankType.PREMIUM - ); - } else { - draft.types = [...draft.types, TankType.PREMIUM]; - } - }); - }} - > - - - - - { - TankFilters.mutate((draft) => { - if (tankFilters.types.includes(TankType.COLLECTOR)) { - draft.types = draft.types.filter( - (t) => t !== TankType.COLLECTOR - ); - } else { - draft.types = [...draft.types, TankType.COLLECTOR]; - } - }); - }} - > - - - - - - - - { - TankFilters.mutate((draft) => { - draft.showNonTesting = !draft.showNonTesting; - }); - }} - > - - - - { - TankFilters.mutate((draft) => { - draft.showTesting = !draft.showTesting; - }); - }} - > - - - - - {wargaming ? ( - - ) : ( - - - - )} - - - {clearable && ( - { - event.preventDefault(); - TankFilters.set(TankFilters.initial); - TankSort.set(TankSort.initial); - }} - > - {strings.website.common.tank_search.clear_filters} - - )} + { + TankFilters.mutate((draft) => { + draft.showTesting = !draft.showTesting; + }); + }} + > + + ); } + +function ConsumablesFilter() { + const { unwrap, strings } = useLocale(); + const rawConsumables = TankFilters.use((state) => state.consumables); + const consumables = + rawConsumables.length === 0 ? consumablesArray : rawConsumables; + + return ( + + + + + + + {consumablesArray.map((consumable) => { + const selected = rawConsumables.includes(consumable); + const consumableDefinition = + consumableDefinitions.consumables[consumable]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.consumables = draft.consumables.filter( + (n) => n !== consumable + ); + } else { + draft.consumables = [...draft.consumables, consumable]; + } + }); + }} + checked={selected} + key={consumable} + > + + + {unwrap(consumableDefinition.name)} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.consumables = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + +function ProvisionsFilter() { + const { unwrap, strings } = useLocale(); + const rawProvisions = TankFilters.use((state) => state.provisions); + const provisions = + rawProvisions.length === 0 ? provisionsArray : rawProvisions; + + return ( + + + + + + + {provisionsArray.map((provision) => { + const selected = rawProvisions.includes(provision); + const provisionDefinition = + provisionDefinitions.provisions[provision]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.provisions = draft.provisions.filter( + (n) => n !== provision + ); + } else { + draft.provisions = [...draft.provisions, provision]; + } + }); + }} + checked={selected} + key={provision} + > + + + {unwrap(provisionDefinition.name)} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.provisions = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + +function PowersFilter() { + const { unwrap, strings } = useLocale(); + const rawPowers = TankFilters.use((state) => state.powers); + const powers = + rawPowers.length === 0 + ? Array.from(allGameModeProvisions.values()) + : rawPowers; + + return ( + + + + + + + {gameModeRoles.map(({ gameModeId, provisions }) => { + const gameMode = gameDefinitions.gameModes[gameModeId]; + + if (provisions.length === 0) return null; + + return ( + + {unwrap(gameMode.name)} + + {provisions.map((provision) => { + { + const selected = rawPowers.includes(provision); + const powerDefinition = + provisionDefinitions.provisions[provision]; + + return ( + { + 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} + > + + + {unwrap(powerDefinition.name)} + + ); + } + })} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.powers = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} + +function AbilitiesFilter() { + const { unwrap, strings } = useLocale(); + const rawAbilities = TankFilters.use((state) => state.abilities); + const abilities = + rawAbilities.length === 0 + ? Array.from(allGameModeConsumables.values()) + : rawAbilities; + + return ( + + + + + + + {gameModeRoles.map(({ gameModeId, consumables }) => { + const gameMode = gameDefinitions.gameModes[gameModeId]; + + if (consumables.length === 0) return null; + + return ( + + {unwrap(gameMode.name)} + + {consumables.map((consumable) => { + { + const selected = rawAbilities.includes(consumable); + const abilityDefinition = + consumableDefinitions.consumables[consumable]; + + return ( + { + 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} + > + + + {unwrap(abilityDefinition.name)} + + ); + } + })} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.abilities = []; + }); + }} + > + + {strings.website.common.tank_search.clear} + + + + ); +} diff --git a/packages/website/src/components/TankSearch/index.tsx b/packages/website/src/components/TankSearch/index.tsx index 581833c1c..4008e0366 100644 --- a/packages/website/src/components/TankSearch/index.tsx +++ b/packages/website/src/components/TankSearch/index.tsx @@ -429,7 +429,7 @@ export const TankSearch = memo( }, [tankFilters, tankopediaSort]); return ( - + @@ -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() { {unwrap(provisions.name)} ); })} + {gameDefinitions.roles[value].consumables.map((id) => { const consumable = consumableDefinitions.consumables[id]; @@ -103,9 +104,9 @@ export function GameModeSection() { {unwrap(consumable.name)} - {strings.common.shells.apcr} + {strings.common.shells.ap_cr} - {strings.common.shells.heat} + {strings.common.shells.hc} {strings.common.shells.he} diff --git a/packages/website/src/core/blitzkit/filterTank.ts b/packages/website/src/core/blitzkit/filterTank.ts index 029505948..02bea5262 100644 --- a/packages/website/src/core/blitzkit/filterTank.ts +++ b/packages/website/src/core/blitzkit/filterTank.ts @@ -1,9 +1,17 @@ import type { TankDefinition } from "@blitzkit/core"; +import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity"; 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, tank: TankDefinition, @@ -40,6 +48,42 @@ export async function filterTank( gun.shells[index].type === filters.shells[index] ) ) - ) + ) && + (filters.consumables.length === 0 || + filters.consumables.every((consumable) => + tank.turrets.some((turret) => + turret.guns.some((gun) => + checkConsumableProvisionInclusivity( + consumableDefinitions.consumables[consumable], + tank, + gun + ) + ) + ) + )) && + (filters.provisions.length === 0 || + filters.provisions.every((provision) => + tank.turrets.some((turret) => + turret.guns.some((gun) => + checkConsumableProvisionInclusivity( + provisionDefinitions.provisions[provision], + tank, + gun + ) + ) + ) + )) && + (filters.abilities.length === 0 || + Object.values(tank.roles).some((id) => + gameDefinitions.roles[id as unknown as number].consumables.some( + (consumable) => filters.abilities.includes(consumable) + ) + )) && + (filters.powers.length === 0 || + Object.values(tank.roles).some((id) => + gameDefinitions.roles[id as unknown as number].provisions.some( + (consumable) => filters.powers.includes(consumable) + ) + )) ); } diff --git a/packages/website/src/core/blitzkit/tankCharacteristics.ts b/packages/website/src/core/blitzkit/tankCharacteristics.ts index 86fb8ca5b..dcab1976c 100644 --- a/packages/website/src/core/blitzkit/tankCharacteristics.ts +++ b/packages/website/src/core/blitzkit/tankCharacteristics.ts @@ -19,9 +19,9 @@ import { type TankDefinition, type TrackDefinition, type TurretDefinition, -} from '@blitzkit/core'; -import { coefficient } from '@blitzkit/core/src/blitzkit/coefficient'; -import type { EquipmentMatrix } from '../../stores/duel'; +} from "@blitzkit/core"; +import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient"; +import type { EquipmentMatrix } from "../../stores/duel"; export type TankCharacteristics = ReturnType; export type TankCharacteristicsKey = keyof TankCharacteristics; @@ -76,7 +76,7 @@ export function tankCharacteristics( tankModelDefinition: ModelDefinition; equipmentDefinitions: EquipmentDefinitions; provisionDefinitions: ProvisionDefinitions; - }, + } ) { const preset = equipmentDefinitions.presets[tank.equipment_preset]; const turretModelDefinition = tankModelDefinition.turrets[turret.id]; @@ -86,7 +86,7 @@ export function tankCharacteristics( return preset.slots.some((slot, index) => { const choice = equipmentMatrix[Math.floor(index / 3)][index % 3]; if (choice === 0) return false; - const equipped = slot[choice === -1 ? 'left' : 'right']; + const equipped = slot[choice === -1 ? "left" : "right"]; return equipped === id; }); } @@ -138,7 +138,7 @@ export function tankCharacteristics( provisionDefinitions.provisions[provision].crew ? total + provisionDefinitions.provisions[provision].crew! / 100 : total, - 0, + 0 ) + (hasImprovedVentilation ? 0.08 : 0); const commanderMastery = 1 + provisionCrewBonus; const loaderMastery = @@ -158,19 +158,19 @@ export function tankCharacteristics( coefficient( [applyReactiveArmor && shell.type !== ShellType.HE, -0.27], [applyDynamicArmor, -0.1], - [applySpallLiner && shell.type === ShellType.HE, -0.2], + [applySpallLiner && shell.type === ShellType.HE, -0.2] ); const assaultDamageCoefficient = gun.assault_ranges && gun.assault_ranges.types.includes(shell.type) - ? (gun.assault_ranges.ranges.find( - ({ distance }) => distance >= assaultDistance, - )?.factor ?? 0) + ? gun.assault_ranges.ranges.find( + ({ distance }) => distance >= assaultDistance + )?.factor ?? 0 : 1; const moduleDamageCoefficient = coefficient([hasTungsten, 0.15]); const reloadCoefficient = (coefficient([hasGunRammer, -0.05]) * coefficient([true, degressiveStat(loaderMastery)])) / - coefficient([hasAdrenaline && gun.gun_type!.$case === 'regular', 0.17]); + coefficient([hasAdrenaline && gun.gun_type!.$case === "regular", 0.17]); const penetrationCoefficient = coefficient([ hasCalibratedShells, resolvePenetrationCoefficient(true, shell.type) - 1, @@ -178,16 +178,16 @@ export function tankCharacteristics( const healthCoefficient = coefficient( [hasSandbagArmor, 0.03], [hasEnhancedSandbagArmor, 0.06], - [hasImprovedAssembly, 0.04], + [hasImprovedAssembly, 0.04] ); const shellVelocityCoefficient = coefficient( [hasSupercharger, 0.35], - [hasImprovedGunPowder, 0.3], + [hasImprovedGunPowder, 0.3] ); const aimTimeCoefficient = coefficient( [hasEnhancedGunLayingDrive, -0.15], - [hasReticleCalibration, -0.3], + [hasReticleCalibration, -0.3] ) * coefficient([true, degressiveStat(gunnerMastery)]); const dispersionStillCoefficient = coefficient([hasRefinedGun, -0.15], [hasReticleCalibration, -0.3]) * @@ -198,19 +198,19 @@ export function tankCharacteristics( ]); const dispersionHullTraverseCoefficient = coefficient( [hasVerticalStabilizer, -0.15], - [true, -crewSkills.smooth_turn / 100], + [true, -crewSkills.smooth_turn / 100] ); const dispersionAfterShotCoefficient = coefficient( [hasVerticalStabilizer, -0.15], - [true, -crewSkills.soft_recoil / 100], + [true, -crewSkills.soft_recoil / 100] ); const dispersionMovingCoefficient = coefficient( [hasVerticalStabilizer, -0.15], - [true, -crewSkills.smooth_driving / 100], + [true, -crewSkills.smooth_driving / 100] ); const dispersionTurretTraverseCoefficient = coefficient( [hasVerticalStabilizer, -0.15], - [true, -crewSkills.smooth_turret * (2 / 100)], + [true, -crewSkills.smooth_turret * (2 / 100)] ); const enginePowerCoefficient = coefficient( [ @@ -225,7 +225,7 @@ export function tankCharacteristics( [hasStandardFuel, 0.03], [hasImprovedFuel, 0.1], [hasGearOil, 0.03], - [hasImprovedGearOil, 0.06], + [hasImprovedGearOil, 0.06] ); const turretTraverseCoefficient = coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) * @@ -234,36 +234,41 @@ export function tankCharacteristics( coefficient( [hasImprovedControl, 0.11], [hasImprovedEnginePowerBoost, 0.01], - [true, crewSkills.virtuoso / 100], + [true, crewSkills.virtuoso / 100] ) * coefficient([true, progressiveStat(driverMastery)]); - const resistanceHardCoefficient = coefficient([hasImprovedSuspension, -0.2]); - const resistanceMediumCoefficient = coefficient([ - hasImprovedSuspension, - -0.15, - ]); - const resistanceSoftCoefficient = coefficient([hasImprovedSuspension, -0.3]); + + const resistanceHardCoefficient = + coefficient([hasImprovedSuspension, -0.2]) * + coefficient([true, degressiveStat(driverMastery)]); + const resistanceMediumCoefficient = + coefficient([hasImprovedSuspension, -0.15]) * + coefficient([true, degressiveStat(driverMastery)]); + const resistanceSoftCoefficient = + coefficient([hasImprovedSuspension, -0.3]) * + coefficient([true, degressiveStat(driverMastery)]); + const viewRangeCoefficient = coefficient([ hasImprovedOptics, tank.class === TankClass.HEAVY ? 0.06 : tank.class === TankClass.MEDIUM - ? 0.08 - : tank.class === TankClass.LIGHT - ? 0.11 - : 0.04, + ? 0.08 + : tank.class === TankClass.LIGHT + ? 0.11 + : 0.04, ]) * coefficient([true, progressiveStat(commanderMastery)]); const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]); const speedForwardsSum = sum( [hasImprovedEnginePowerBoost, 4], [hasGearOil, 2], - [hasImprovedGearOil, 4], + [hasImprovedGearOil, 4] ); const speedBackwardsSum = sum( [hasImprovedEnginePowerBoost, 8], [hasGearOil, 2], - [hasImprovedGearOil, 4], + [hasImprovedGearOil, 4] ); const camouflageCoefficientMoving = sum( [ @@ -273,8 +278,8 @@ export function tankCharacteristics( tank.class === TankClass.TANK_DESTROYER ? 0.04 : tank.class === TankClass.HEAVY - ? 0.03 - : 0.02, + ? 0.03 + : 0.02, ]), ], [ @@ -282,11 +287,11 @@ export function tankCharacteristics( tank.class === TankClass.HEAVY ? 0.02 : tank.class === TankClass.MEDIUM - ? 0.04 - : tank.class === TankClass.LIGHT - ? 0.07 - : 0.1, - ], + ? 0.04 + : tank.class === TankClass.LIGHT + ? 0.07 + : 0.1, + ] ); const camouflageCoefficientStill = sum( [ @@ -297,10 +302,10 @@ export function tankCharacteristics( tank.class === TankClass.TANK_DESTROYER ? 0.04 : tank.class === TankClass.HEAVY - ? 0.03 - : 0.02, + ? 0.03 + : 0.02, ], - [true, crewSkills.camouflage * (3 / 100)], + [true, crewSkills.camouflage * (3 / 100)] ), ], [ @@ -308,18 +313,18 @@ export function tankCharacteristics( tank.class === TankClass.HEAVY ? 0.04 : tank.class === TankClass.MEDIUM - ? 0.08 - : tank.class === TankClass.LIGHT - ? 0.14 - : 0.2, - ], + ? 0.08 + : tank.class === TankClass.LIGHT + ? 0.14 + : 0.2, + ] ); const size = normalizeBoundingBox( unionBoundingBox( tankModelDefinition.bounding_box, - turretModelDefinition.bounding_box, - ), + turretModelDefinition.bounding_box + ) ); const weightKg = tank.weight + engine.weight + track.weight + turret.weight + gun.weight; @@ -339,14 +344,14 @@ export function tankCharacteristics( shell, damageCoefficient, reloadCoefficient, - intraClipCoefficient, + intraClipCoefficient ); const intraClip = - gun.gun_type?.$case === 'regular' + gun.gun_type?.$case === "regular" ? undefined : gun.gun_type!.value.intra_clip * intraClipCoefficient; const mostOptimalShellIndex = - gun.gun_type!.$case === 'auto_reloader' + gun.gun_type!.$case === "auto_reloader" ? gun.gun_type!.value.shell_reloads.reduce gun.gun_type!.value.shell_reloads[1] + gun.gun_type!.value.intra_clip ? ((armorDamageCoefficient * shell.armor_damage) / @@ -389,23 +394,23 @@ export function tankCharacteristics( gun.gun_type!.value.shell_reloads.slice(1).length : undefined; const shells = - gun.gun_type!.$case === 'regular' ? 1 : gun.gun_type!.value.shell_count; + gun.gun_type!.$case === "regular" ? 1 : gun.gun_type!.value.shell_count; const shellReloads = - gun.gun_type!.$case === 'auto_reloader' + gun.gun_type!.$case === "auto_reloader" ? gun.gun_type!.value.shell_reloads.map( - (reload) => reload * reloadCoefficient, + (reload) => reload * reloadCoefficient ) : undefined; const shellReload = - gun.gun_type!.$case === 'auto_reloader' + gun.gun_type!.$case === "auto_reloader" ? undefined - : (gun.gun_type!.$case === 'regular' + : (gun.gun_type!.$case === "regular" ? gun.gun_type!.value.reload : gun.gun_type!.value.clip_reload) * reloadCoefficient; const caliber = shell.caliber; const penetration = shell.penetration.near * penetrationCoefficient; const clipDamage = - gun.gun_type!.$case === 'regular' + gun.gun_type!.$case === "regular" ? undefined : gun.gun_type!.value.shell_count * damage; const moduleDamage = shell.module_damage * moduleDamageCoefficient; @@ -599,45 +604,45 @@ export const characteristicsOrder: { items: TankCharacteristicsKey[]; }[] = [ { - name: 'Firepower', + name: "Firepower", items: [ - 'gunType', - 'dpm', - 'shells', - 'shellReload', - 'shellReloads', - 'intraClip', - 'penetration', - 'penetrationAt250m', - 'damage', - 'clipDamage', - 'moduleDamage', - 'caliber', - 'shellNormalization', - 'shellRicochet', - 'shellVelocity', - 'shellRange', - 'shellCapacity', - 'aimTime', - 'dispersion', - 'dispersionMoving', - 'dispersionHullTraversing', - 'dispersionTurretTraversing', - 'dispersionShooting', - 'dispersionGunDamaged', - 'gunDepression', - 'gunElevation', - 'gunFrontalDepression', - 'gunFrontalElevation', - 'gunRearDepression', - 'gunRearElevation', - 'azimuthLeft', - 'azimuthRight', + "gunType", + "dpm", + "shells", + "shellReload", + "shellReloads", + "intraClip", + "penetration", + "penetrationAt250m", + "damage", + "clipDamage", + "moduleDamage", + "caliber", + "shellNormalization", + "shellRicochet", + "shellVelocity", + "shellRange", + "shellCapacity", + "aimTime", + "dispersion", + "dispersionMoving", + "dispersionHullTraversing", + "dispersionTurretTraversing", + "dispersionShooting", + "dispersionGunDamaged", + "gunDepression", + "gunElevation", + "gunFrontalDepression", + "gunFrontalElevation", + "gunRearDepression", + "gunRearElevation", + "azimuthLeft", + "azimuthRight", ], }, { - name: 'Crew', - items: ['crewCount'], + name: "Crew", + items: ["crewCount"], }, ]; diff --git a/packages/website/src/stores/tankFilters.ts b/packages/website/src/stores/tankFilters.ts index 68999b562..7d1c1dcdb 100644 --- a/packages/website/src/stores/tankFilters.ts +++ b/packages/website/src/stores/tankFilters.ts @@ -21,6 +21,10 @@ export interface TankFilters { searching: boolean; gunType: CaseType[]; shells: [ShellType | null, ShellType | null, ShellType | null]; + consumables: number[]; + provisions: number[]; + abilities: number[]; + powers: number[]; showTesting: boolean; showNonTesting: boolean; @@ -37,6 +41,10 @@ export const TankFilters = new Varuna({ searching: false, gunType: [], shells: [null, null, null], + consumables: [], + provisions: [], + abilities: [], + powers: [], showTesting: true, showNonTesting: true,