mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-03 16:00:36 +01:00
Compare commits
27
Commits
220e4388d4
...
d42fc0e3f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d42fc0e3f6 | ||
|
|
dc15df2a40 | ||
|
|
17d1526b6f | ||
|
|
db8ac195d4 | ||
|
|
c5f5f69fd6 | ||
|
|
44203223fd | ||
|
|
bdabadcd49 | ||
|
|
7c838aaf06 | ||
|
|
f181c96518 | ||
|
|
6fbfa2c70b | ||
|
|
17dd50b902 | ||
|
|
9061335292 | ||
|
|
c27d3a3b6a | ||
|
|
c37acf4a93 | ||
|
|
0c1cef87f6 | ||
|
|
e8b777be8a | ||
|
|
2941348412 | ||
|
|
68b2ecfb6d | ||
|
|
d5526da1fb | ||
|
|
091305a20f | ||
|
|
85cd70b2d5 | ||
|
|
7997f490fc | ||
|
|
5a0b309b0b | ||
|
|
43c14665de | ||
|
|
e249408360 | ||
|
|
24e57d7125 | ||
|
|
636a2934a2 |
No files matched your search
@@ -10,7 +10,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
name: Build bot
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
@@ -35,7 +35,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
name: Build website
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"": {
|
||||
"name": "blitzkit",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@prisma/client": "^6.18.0",
|
||||
"prisma": "^6.18.0",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -545,6 +546,8 @@
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/[email protected]", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||
|
||||
"@fontsource-variable/jetbrains-mono": ["@fontsource-variable/[email protected]", "", {}, "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q=="],
|
||||
|
||||
"@fontsource-variable/roboto-flex": ["@fontsource-variable/[email protected]", "", {}, "sha512-Q9zbz+P2VrDeIxBu9YAIG+eYpLmQF5FIJEEwAWn8En9Lao1TOISnoYcywGV+jSv7LGsHEndCR8hveXPnvCBEQQ=="],
|
||||
|
||||
"@fontsource-variable/roboto-mono": ["@fontsource-variable/[email protected]", "", {}, "sha512-6M2U3wGIUxYNKRrUoKls8BRRIPDA57T8J0agqwyDkiEHrLEEAqptsxcUl3eTm6tnRNEn6yEm4pCefvtnujebDA=="],
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# BlitzKit 1.29.10
|
||||
|
||||
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||
|
||||
## Features
|
||||
|
||||
- Combined game mode passive and active abilities into a single dropdown
|
||||
- [Mark Greene] Added tier filters to the Guess game
|
||||
- Changing tiers will break your streak
|
||||
|
||||
## Fixes
|
||||
|
||||
- [Mark Greene] module damage is now properly shown when sorting with it in the tank search
|
||||
- Duplicate abilities in the tank filters are now removed
|
||||
@@ -43,6 +43,7 @@
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@prisma/client": "^6.18.0",
|
||||
"prisma": "^6.18.0",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export * from "./literals";
|
||||
export * from "./strings";
|
||||
export * from "./unwrapper";
|
||||
export * from "./wrapper";
|
||||
@@ -1,68 +1,3 @@
|
||||
import { merge } from "ts-deepmerge";
|
||||
import locales from "../locales.json";
|
||||
import en from "../strings/en.json";
|
||||
import es from "../strings/es.json";
|
||||
import fr from "../strings/fr.json";
|
||||
import ja from "../strings/ja.json";
|
||||
import pl from "../strings/pl.json";
|
||||
import pt from "../strings/pt.json";
|
||||
import ru from "../strings/ru.json";
|
||||
import uk from "../strings/uk.json";
|
||||
import zh from "../strings/zh.json";
|
||||
import type en from "../strings/en.json";
|
||||
|
||||
export type BlitzKitStrings = typeof en;
|
||||
|
||||
const stringsRaw: Record<string, TranslationTree> = {
|
||||
en,
|
||||
es,
|
||||
fr,
|
||||
ja,
|
||||
pl,
|
||||
pt,
|
||||
ru,
|
||||
uk,
|
||||
zh,
|
||||
};
|
||||
|
||||
export const STRINGS: Record<string, BlitzKitStrings> = {};
|
||||
|
||||
for (const locale in stringsRaw) {
|
||||
if (locale === locales.default) {
|
||||
STRINGS[locales.default] = stringsRaw[locales.default] as BlitzKitStrings;
|
||||
continue;
|
||||
}
|
||||
|
||||
const localeStrings = stringsRaw[locale];
|
||||
STRINGS[locale] = merge(
|
||||
stringsRaw[locales.default],
|
||||
localeStrings
|
||||
) as BlitzKitStrings;
|
||||
}
|
||||
|
||||
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<string, string> = {
|
||||
en: "en",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
ja: "ja",
|
||||
pt: "pt",
|
||||
ru: "ru",
|
||||
uk: "uk",
|
||||
zh: "zh-Hans",
|
||||
pl: "pl",
|
||||
};
|
||||
|
||||
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
|
||||
en: "🇺🇸",
|
||||
es: "🇪🇸",
|
||||
fr: "🇫🇷",
|
||||
ja: "🇯🇵",
|
||||
pt: "🇵🇹",
|
||||
ru: "🇷🇺",
|
||||
uk: "🇺🇦",
|
||||
zh: "🇨🇳",
|
||||
pl: "🇵🇱",
|
||||
};
|
||||
|
||||
export type TranslationTree = { [key: string]: TranslationNode };
|
||||
|
||||
export type TranslationNode = string | TranslationTree;
|
||||
export type Strings = typeof en;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { I18nString } from "../../core/src";
|
||||
import locales from "../locales.json";
|
||||
|
||||
export function unwrapper(locale = locales.default) {
|
||||
return function (i18nString: I18nString) {
|
||||
if (locale in i18nString.locales) {
|
||||
return i18nString.locales[locale];
|
||||
}
|
||||
|
||||
return i18nString.locales[locales.default];
|
||||
};
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import locales from "../locales.json";
|
||||
import { type BlitzKitStrings, STRINGS } from "./strings";
|
||||
|
||||
export function wrapper(slicer: (strings: BlitzKitStrings) => string) {
|
||||
const defaultString = slicer(STRINGS[locales.default] as BlitzKitStrings);
|
||||
|
||||
const object = { [locales.default]: defaultString };
|
||||
|
||||
for (const { locale } of locales.supported) {
|
||||
const string = slicer(STRINGS[locale] as BlitzKitStrings);
|
||||
if (string !== defaultString) object[locale] = string;
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
},
|
||||
|
||||
"home": {
|
||||
"search": "Search tanks, players, tools..."
|
||||
"search": "Search tanks, players, tools...",
|
||||
"credit": "by TrèsAbhi"
|
||||
}
|
||||
}
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "Limpiar",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "No se encontraron tanques.",
|
||||
"search_again": "Intenta buscar de nuevo.",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Cancelar",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "Limpiar",
|
||||
"select_all": "Seleccionar todo",
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "Clear",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "No tanks found.",
|
||||
"search_again": "Try searching again.",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Cancel",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "Clear",
|
||||
"select_all": "Select all",
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -187,11 +187,11 @@
|
||||
"bot": {
|
||||
"common": {
|
||||
"errors": {
|
||||
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
|
||||
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
|
||||
"clan_not_found": "",
|
||||
"player_not_found": "# ユーザーが見つかりませんでした\nユーザー「{search}」が見つかりませんでした。検索結果からユーザーを選択するか、有効なユーザー名を入力するか、/link コマンドを使用してください。",
|
||||
"player_not_linked": "# `/link`を使用して始めましょう\nはじめまして!あなたが誰か、私に教えてください。",
|
||||
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
|
||||
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
|
||||
"tank_id_not_found": "# 車両が見つかりません\nID「{search}」の戦車が見つかりませんでした。数字ではなく戦車の名前を入力してみてください。",
|
||||
"no_tank_stats": "# 利用可能な車輌統計がありません\nこのプレイヤーには利用可能な統計がありません。あなたが探しているプレイヤーではない可能性があります。",
|
||||
"get_help": "Discordサーバーに参加する",
|
||||
"uncaught_error": "# BlitzKitがエラーに遭遇しました!\n申し訳ありません。お気軽に[公式Discordサーバー](https://discord.gg/nDt7AjGJQH)にお問い合わせください。",
|
||||
@@ -395,7 +395,7 @@
|
||||
"wn8": "WN8",
|
||||
"winrate": "勝率",
|
||||
"damage": "ダメージ",
|
||||
"unknown_tank": "Unknown tank {id}"
|
||||
"unknown_tank": "不明な車両{id}"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
@@ -560,7 +560,7 @@
|
||||
"description": "Today's played tanks and statistics"
|
||||
},
|
||||
"stats": {
|
||||
"name": "stats",
|
||||
"name": "",
|
||||
"description": "Quick statistics for a period"
|
||||
},
|
||||
"owned_tanks": {
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "クリア",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "該当する車輌が存在しません。",
|
||||
"search_again": "もう一度検索してください。",
|
||||
@@ -781,7 +783,7 @@
|
||||
"tank": "車輌名",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"unrecorded": "",
|
||||
"wins": "勝利"
|
||||
},
|
||||
"large_warning": {
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Cancel",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "クリア",
|
||||
"select_all": "全て選択",
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "Clear",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "No tanks found.",
|
||||
"search_again": "Try searching again.",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Cancel",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "Clear",
|
||||
"select_all": "Select all",
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"clear": "Limpar",
|
||||
"tier": "Tier {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "Nenhum tanque encontrado.",
|
||||
"search_again": "Tente pesquisar novamente.",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Cancelar",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "Limpar",
|
||||
"select_all": "Selecionar todos",
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Сортировать по {name}",
|
||||
"clear": "Очистить",
|
||||
"tier": " {tier} уровня",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "Танков не найдено.",
|
||||
"search_again": "Попробуйте повторить поиск.",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Показывает танк не показывая название. Этот раунд будет считаться проигранным и прекратит серию!",
|
||||
"cancel": "Отмена",
|
||||
"reveal": "Раскрыть"
|
||||
}
|
||||
},
|
||||
"clear": "Очистить",
|
||||
"select_all": "Выбрать всё",
|
||||
"tier": " {tier} уровня"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Поиск характеристик...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "Відсортувати за {name}",
|
||||
"clear": "Clear",
|
||||
"tier": "Рівень {tier}",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "Нічого не знайдено.",
|
||||
"search_again": "Спробуйте шукати заново.",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Cancel",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "Clear",
|
||||
"select_all": "Вибрати все",
|
||||
"tier": "Рівень {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
@@ -671,6 +671,8 @@
|
||||
"sorting_by": "按 {name} 排序",
|
||||
"clear": "清除",
|
||||
"tier": "{tier} 阶",
|
||||
"passive": "{name} (Passive)",
|
||||
"active": "{name} (Active)",
|
||||
"no_tanks_found": {
|
||||
"body": "找不到战车。",
|
||||
"search_again": "请尝试重新搜索。",
|
||||
@@ -823,7 +825,10 @@
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "取消",
|
||||
"reveal": "Reveal"
|
||||
}
|
||||
},
|
||||
"clear": "清除",
|
||||
"select_all": "全选",
|
||||
"tier": "{tier} 阶"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "搜索属性……",
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
---
|
||||
import "@fontsource-variable/jetbrains-mono";
|
||||
import "@fontsource-variable/roboto-flex";
|
||||
import "@fontsource-variable/roboto-mono";
|
||||
---
|
||||
|
||||
<style is:global>
|
||||
@import "@radix-ui/colors/mauve-dark.css";
|
||||
@import "@radix-ui/colors/mauve-alpha.css";
|
||||
|
||||
<style>
|
||||
body {
|
||||
--space-0: 0.0625rem;
|
||||
--space-1: 0.125rem;
|
||||
@@ -19,6 +16,13 @@ import "@fontsource-variable/roboto-mono";
|
||||
--space-8: 16rem;
|
||||
--space-9: 32rem;
|
||||
|
||||
--max-size: max(100vw, 100vh);
|
||||
|
||||
--font-size-regular: var(--space-4);
|
||||
|
||||
--font-family-regular: "Roboto Flex Variable", sans-serif;
|
||||
--font-family-mono: "JetBrains Mono Variable", monospace;
|
||||
|
||||
--weight-thin: 100;
|
||||
--weight-extra-light: 200;
|
||||
--weight-light: 300;
|
||||
@@ -29,10 +33,6 @@ import "@fontsource-variable/roboto-mono";
|
||||
--weight-extra-bold: 800;
|
||||
--weight-black: 900;
|
||||
|
||||
--font-size-regular: var(--space-4);
|
||||
|
||||
--max-size: max(100vw, 100vh);
|
||||
|
||||
--outline-interactive: solid var(--space-1) var(--gray-5);
|
||||
--outline-focus: var(--space-1) solid var(--accent-6);
|
||||
|
||||
@@ -44,7 +44,7 @@ import "@fontsource-variable/roboto-mono";
|
||||
|
||||
background-color: var(--gray-1);
|
||||
color: var(--gray-12);
|
||||
font-family: "Roboto Flex";
|
||||
font-family: var(--font-family-regular);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.button {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import "./index.css";
|
||||
|
||||
export interface ButtonProps extends ComponentProps<"button"> {}
|
||||
|
||||
export function Button({ className, ...props }: ButtonProps) {
|
||||
return <button className={classNames("button", className)} {...props} />;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
.code {
|
||||
font-family: "Roboto Mono";
|
||||
color: red;
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import "./index.css";
|
||||
|
||||
export interface CodeProps extends ComponentProps<"code"> {}
|
||||
|
||||
export function Code({ className }: CodeProps) {
|
||||
return <code className={className?????} {...props} />;
|
||||
export function Code({ className, ...props }: CodeProps) {
|
||||
return <code className={classNames("code", className)} {...props} />;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import * as colors from "@radix-ui/colors";
|
||||
import type { Color } from "../pages/styles/colors/[accent].css";
|
||||
import type { Color } from "../core/ui/color";
|
||||
|
||||
interface Props {
|
||||
color: Color;
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
---
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { api } from "../core/api/dynamic";
|
||||
import { BlitzKitIcon } from "../icons/BlitzKit";
|
||||
import { Code } from "./Code";
|
||||
import { Text } from "./Text";
|
||||
import { TextField } from "./TextField";
|
||||
|
||||
const strings = { home: { search: "test" } };
|
||||
interface Props {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
const strings = await api.strings(locale);
|
||||
---
|
||||
|
||||
<style>
|
||||
@@ -39,7 +47,10 @@ const strings = { home: { search: "test" } };
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: linear-gradient(transparent, var(--gray-1));
|
||||
background: linear-gradient(
|
||||
color-mix(in srgb, transparent 50%, var(--gray-1)),
|
||||
var(--gray-1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +72,18 @@ const strings = { home: { search: "test" } };
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translateX(100%);
|
||||
transform: translate(100%, 0);
|
||||
padding-left: var(--space-2);
|
||||
|
||||
font-weight: var(--weight-bold);
|
||||
letter-spacing: -0.15em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-field {
|
||||
width: 100%;
|
||||
max-width: 25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -82,6 +98,10 @@ const strings = { home: { search: "test" } };
|
||||
<BlitzKitIcon className="icon" />
|
||||
<Code className="version">2.0</Code>
|
||||
</div>
|
||||
|
||||
<Text lowContrast>
|
||||
{strings.home.credit}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<TextField placeholder={strings.home.search}>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.text {
|
||||
font-size: var(--font-size-regular);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import type { Color } from "../../core/ui/color";
|
||||
import "./index.css";
|
||||
|
||||
export interface TextProps extends ComponentProps<"span"> {
|
||||
color?: Color;
|
||||
lowContrast?: boolean;
|
||||
}
|
||||
|
||||
export function Text({
|
||||
color = "gray",
|
||||
lowContrast,
|
||||
className,
|
||||
...props
|
||||
}: TextProps) {
|
||||
return (
|
||||
<span
|
||||
className={classNames("text", className)}
|
||||
style={{ color: `var(--${color}-${lowContrast ? 11 : 12})` }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import "./index.css";
|
||||
|
||||
export interface TextFieldProps extends ComponentProps<"input"> {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function TextField({ children, ...props }: TextFieldProps) {
|
||||
export function TextField({ children, className, ...props }: TextFieldProps) {
|
||||
return (
|
||||
<div className="text-field" data-has-icon={children !== undefined}>
|
||||
<div
|
||||
className={classNames("text-field", className)}
|
||||
data-has-icon={children !== undefined}
|
||||
>
|
||||
{children}
|
||||
<input {...props} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
import { CaretRightIcon } from "@radix-ui/react-icons";
|
||||
import type { Tool } from "../core/tools/tools";
|
||||
import { Button } from "./Button";
|
||||
import { Text } from "./Text";
|
||||
|
||||
interface Props {
|
||||
tool: Tool;
|
||||
}
|
||||
|
||||
const { tool } = Astro.props;
|
||||
---
|
||||
|
||||
<style>
|
||||
.tool-card {
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="tool-card">
|
||||
<div>
|
||||
<div>
|
||||
<Text>{tool.id}</Text>
|
||||
<Text>{tool.id}</Text>
|
||||
</div>
|
||||
|
||||
<Button>
|
||||
{tool.id}
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,15 +1,41 @@
|
||||
import type { CatalogItemAccessor } from "@blitzkit/closed";
|
||||
import type { Strings } from "@blitzkit/i18n";
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import type { Tank } from "../../protos/tank";
|
||||
import type { TankList } from "../../protos/tank_list";
|
||||
import type { Tanks } from "../../protos/tanks";
|
||||
|
||||
export abstract class AbstractAPI {
|
||||
abstract tanks(): Promise<Tanks>;
|
||||
|
||||
abstract tankList(): Promise<TankList>;
|
||||
|
||||
abstract strings(
|
||||
abstract _strings(locale: string): Promise<Strings>;
|
||||
abstract _gameStrings(
|
||||
locale: string,
|
||||
prefix?: string
|
||||
prefix: string
|
||||
): Promise<Record<string, string>>;
|
||||
|
||||
private assertLocale(locale: string) {
|
||||
const supported = locales.supported.find(
|
||||
(supported) => supported.locale === locale
|
||||
);
|
||||
|
||||
if (supported === undefined) {
|
||||
throw new Error(`Unsupported locale: ${locale}`);
|
||||
}
|
||||
|
||||
return supported;
|
||||
}
|
||||
async strings(locale: string) {
|
||||
this.assertLocale(locale);
|
||||
return await this._strings(locale);
|
||||
}
|
||||
async gameStrings(locale: string, prefix: string) {
|
||||
this.assertLocale(locale);
|
||||
return await this._gameStrings(locale, prefix);
|
||||
}
|
||||
|
||||
abstract tank(id: string): Promise<Tank>;
|
||||
|
||||
abstract avatars(): Promise<CatalogItemAccessor[]>;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { MetadataAccessor } from "@blitzkit/closed";
|
||||
import { sluggify } from "@blitzkit/core";
|
||||
import { sluggify, type DeepPartial } from "@blitzkit/core";
|
||||
import type { Strings } from "@blitzkit/i18n";
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import type { RemoteStorageComponent } from "@protos/blitz_static_remote_storage_component";
|
||||
import { merge } from "lodash-es";
|
||||
import { parse } from "yaml";
|
||||
import type { Tank } from "../../protos/tank";
|
||||
import type { TankList, TankListEntry } from "../../protos/tank_list";
|
||||
@@ -11,6 +14,10 @@ if (typeof window !== "undefined") {
|
||||
throw new Error("ServerAPI is being evaluated in the browser");
|
||||
}
|
||||
|
||||
const globbedStrings = import.meta.glob("../../../../i18n/strings/*.json", {
|
||||
import: "default",
|
||||
});
|
||||
|
||||
interface LocalizationConfig {
|
||||
namespaces: string[];
|
||||
}
|
||||
@@ -20,7 +27,7 @@ export class ServerAPI extends AbstractAPI {
|
||||
super();
|
||||
}
|
||||
|
||||
async strings(locale: string) {
|
||||
async _gameStrings(locale: string, prefix: string) {
|
||||
const group = this.metadata.group("ClientConfigsEntity");
|
||||
|
||||
if (group.length !== 1) {
|
||||
@@ -67,7 +74,7 @@ export class ServerAPI extends AbstractAPI {
|
||||
Object.assign(
|
||||
strings,
|
||||
await fetch(
|
||||
`${remoteStorage.url}/${remoteStorage.relative_path}/${namespace}/${locale}.yaml`
|
||||
`${remoteStorage.url}${remoteStorage.relative_path}/${namespace}/${locale}.yaml`
|
||||
)
|
||||
.then((response) => response.text())
|
||||
.then((text) => {
|
||||
@@ -89,6 +96,26 @@ export class ServerAPI extends AbstractAPI {
|
||||
);
|
||||
}
|
||||
|
||||
const filtered: Record<string, string> = {};
|
||||
|
||||
for (const key in strings) {
|
||||
if (key.startsWith(`${prefix}__`)) {
|
||||
filtered[key] = strings[key];
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
async _strings(locale: string) {
|
||||
const localized = (await globbedStrings[
|
||||
`../../../../i18n/strings/${locale}.json`
|
||||
]()) as DeepPartial<Strings>;
|
||||
const defaults = (await globbedStrings[
|
||||
`../../../../i18n/strings/${locales.default}.json`
|
||||
]()) as Strings;
|
||||
const strings = merge({}, defaults, localized);
|
||||
|
||||
return strings;
|
||||
}
|
||||
|
||||
@@ -97,7 +124,7 @@ export class ServerAPI extends AbstractAPI {
|
||||
if (this._tankList === undefined) {
|
||||
const group = this.metadata.group("TankEntity");
|
||||
const list: TankListEntry[] = [];
|
||||
const strings = await this.strings("en");
|
||||
const strings = await this.gameStrings("en", "TankEntity");
|
||||
|
||||
for (const item of group) {
|
||||
const tankCatalog = item.TankCatalog();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function classNames(...classes: (string | undefined)[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type * as colors from "@radix-ui/colors";
|
||||
|
||||
const ignoreEndings = ["A", "P3", "Dark", "DarkA", "DarkP3"] as const;
|
||||
export type Color = Exclude<
|
||||
keyof typeof colors,
|
||||
`${string}${(typeof ignoreEndings)[number]}` | "__esModule"
|
||||
>;
|
||||
@@ -4,6 +4,7 @@ import "@radix-ui/colors/amber-dark.css";
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import packageJson from "../../../../../package.json";
|
||||
import HomeHero from "../../components/HomeHero.astro";
|
||||
import ToolCard from "../../components/ToolCard.astro";
|
||||
import { tools } from "../../core/tools/tools";
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
|
||||
@@ -15,18 +16,14 @@ export const getStaticPaths = (() => {
|
||||
{ params: { locale: undefined }, props: { locale: locales.default } },
|
||||
];
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<BaseLayout accent="amber">
|
||||
<HomeHero />
|
||||
<HomeHero {locale} />
|
||||
|
||||
<h1>blitzkit {packageJson.version}</h1>
|
||||
|
||||
{
|
||||
Object.values(tools).map((tool) => (
|
||||
<p>
|
||||
<a href={`/${tool.id}`}>{tool.id}</a>
|
||||
</p>
|
||||
))
|
||||
}
|
||||
{Object.values(tools).map((tool) => <ToolCard {tool} />)}
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,15 @@
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import type { APIContext, GetStaticPaths } from "astro";
|
||||
import { api } from "../../../core/api/dynamic";
|
||||
|
||||
export const getStaticPaths = (() => {
|
||||
return locales.supported.map(({ locale }) => ({ params: { locale } }));
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
/**
|
||||
* BlitzKit strings.
|
||||
*/
|
||||
export async function GET({ params }: APIContext<never, { locale: string }>) {
|
||||
const strings = await api.strings(params.locale);
|
||||
return Response.json(strings);
|
||||
}
|
||||
@@ -9,13 +9,17 @@ import {
|
||||
ArrowRightIcon,
|
||||
EyeOpenIcon,
|
||||
MagnifyingGlassIcon,
|
||||
MixerVerticalIcon,
|
||||
PaperPlaneIcon,
|
||||
StarFilledIcon,
|
||||
TrashIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import {
|
||||
AlertDialog,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
DropdownMenu,
|
||||
Flex,
|
||||
IconButton,
|
||||
Spinner,
|
||||
@@ -23,7 +27,7 @@ import {
|
||||
TextField,
|
||||
} from "@radix-ui/themes";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { debounce } from "lodash-es";
|
||||
import { debounce, times } from "lodash-es";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
|
||||
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
||||
@@ -53,6 +57,7 @@ export function Guesser() {
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
||||
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
||||
const tiers = Guess.use((state) => state.tiers);
|
||||
|
||||
const requestSearch = useCallback(() => {
|
||||
setSelected(null);
|
||||
@@ -97,58 +102,60 @@ export function Guesser() {
|
||||
width="100%"
|
||||
p="4"
|
||||
maxWidth="25rem"
|
||||
gap="4"
|
||||
gap="3"
|
||||
>
|
||||
{results !== null && (
|
||||
<Card variant="classic">
|
||||
<Box py="2" px="3">
|
||||
{results.length === 0 && (
|
||||
<Flex justify="center">
|
||||
<Text color="gray">
|
||||
{strings.website.tools.guess.search.no_results}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
<Box position="relative">
|
||||
{results !== null && (
|
||||
<Card variant="classic">
|
||||
<Box py="2" px="3">
|
||||
{results.length === 0 && (
|
||||
<Flex justify="center">
|
||||
<Text color="gray">
|
||||
{strings.website.tools.guess.search.no_results}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<SearchResults.Root>
|
||||
{results.map((result) => {
|
||||
const Icon = classIcons[result.class];
|
||||
<SearchResults.Root>
|
||||
{results.map((result) => {
|
||||
const Icon = classIcons[result.class];
|
||||
|
||||
return (
|
||||
<SearchResults.Item
|
||||
key={result.id}
|
||||
onClick={() => {
|
||||
if (!input.current) return;
|
||||
return (
|
||||
<SearchResults.Item
|
||||
key={result.id}
|
||||
onClick={() => {
|
||||
if (!input.current) return;
|
||||
|
||||
input.current.value = unwrap(result.name);
|
||||
setSelected(result);
|
||||
setResults(null);
|
||||
}}
|
||||
text={unwrap(result.name)}
|
||||
prefix={
|
||||
<img
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
src={asset(`flags/circle/${result.nation}.webp`)}
|
||||
/>
|
||||
}
|
||||
discriminator={
|
||||
<Flex
|
||||
align="center"
|
||||
gap="1"
|
||||
width="38px"
|
||||
justify="center"
|
||||
>
|
||||
<Icon width="1em" height="1em" />
|
||||
{TIER_ROMAN_NUMERALS[result.tier]}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SearchResults.Root>
|
||||
</Box>
|
||||
</Card>
|
||||
)}
|
||||
input.current.value = unwrap(result.name);
|
||||
setSelected(result);
|
||||
setResults(null);
|
||||
}}
|
||||
text={unwrap(result.name)}
|
||||
prefix={
|
||||
<img
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
src={asset(`flags/circle/${result.nation}.webp`)}
|
||||
/>
|
||||
}
|
||||
discriminator={
|
||||
<Flex
|
||||
align="center"
|
||||
gap="1"
|
||||
width="38px"
|
||||
justify="center"
|
||||
>
|
||||
<Icon width="1em" height="1em" />
|
||||
{TIER_ROMAN_NUMERALS[result.tier]}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SearchResults.Root>
|
||||
</Box>
|
||||
</Card>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Flex justify="center" style={{ userSelect: "none" }}>
|
||||
<Text>
|
||||
@@ -161,11 +168,78 @@ export function Guesser() {
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3">
|
||||
<DropdownMenu.Root modal={false}>
|
||||
<DropdownMenu.Trigger>
|
||||
<IconButton size="3" variant="surface" color="gray">
|
||||
<MixerVerticalIcon />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{times(10, (index) => {
|
||||
const tier = 10 - index;
|
||||
const isSelected = tiers.includes(tier);
|
||||
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={isSelected}
|
||||
key={tier}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
Guess.mutate((draft) => {
|
||||
if (isSelected) {
|
||||
if (draft.tiers.length === 1) return;
|
||||
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||
} else {
|
||||
draft.tiers = [...draft.tiers, tier];
|
||||
}
|
||||
|
||||
draft.streak = 0;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{literals(strings.website.tools.guess.tier, {
|
||||
tier: TIER_ROMAN_NUMERALS[tier],
|
||||
})}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
);
|
||||
})}
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Item
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
Guess.mutate((draft) => {
|
||||
draft.tiers = Guess.initial.tiers;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StarFilledIcon /> {strings.website.tools.guess.select_all}
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
color="red"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
Guess.mutate((draft) => {
|
||||
draft.tiers = [];
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TrashIcon /> {strings.website.tools.guess.clear}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<TextField.Root
|
||||
disabled={guessState !== GuessState.NotGuessed}
|
||||
ref={input}
|
||||
onChange={requestSearch}
|
||||
style={{ flex: 1 }}
|
||||
style={{ flex: 1, minWidth: "14rem" }}
|
||||
placeholder={strings.website.tools.guess.search.placeholder}
|
||||
size="3"
|
||||
variant="classic"
|
||||
@@ -238,7 +312,13 @@ export function Guesser() {
|
||||
draft.streak = correct ? draft.streak + 1 : 0;
|
||||
});
|
||||
} else {
|
||||
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||
const filteredIds = ids.filter((id) => {
|
||||
const tank = tankDefinitions.tanks[Number(id)];
|
||||
return tiers.includes(tank.tier);
|
||||
});
|
||||
const id = Number(
|
||||
filteredIds[Math.floor(Math.random() * filteredIds.length)]
|
||||
);
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
|
||||
Guess.mutate((draft) => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
case 'fire.damage':
|
||||
return shell0.armor_damage.toFixed(0);
|
||||
case 'fire.moduleDamage':
|
||||
return shell0.armor_damage.toFixed(0);
|
||||
return shell0.module_damage.toFixed(0);
|
||||
case 'fire.dispersionMoving':
|
||||
return tracks.dispersion_move.toFixed(3);
|
||||
case 'fire.dispersionStill':
|
||||
|
||||
@@ -83,11 +83,36 @@ for (const gameModeIdString in gameModeRoleSets) {
|
||||
for (const roleId of roles) {
|
||||
const role = gameDefinitions.roles[roleId];
|
||||
|
||||
for (const id of role.consumables) {
|
||||
outerLoop: for (const id of role.consumables) {
|
||||
const name =
|
||||
consumableDefinitions.consumables[id].name.locales[locales.default];
|
||||
|
||||
for (const otherConsumable of consumables.values()) {
|
||||
const otherName =
|
||||
consumableDefinitions.consumables[otherConsumable].name.locales[
|
||||
locales.default
|
||||
];
|
||||
|
||||
if (name === otherName) continue outerLoop;
|
||||
}
|
||||
|
||||
consumables.add(id);
|
||||
allGameModeConsumables.add(id);
|
||||
}
|
||||
for (const id of role.provisions) {
|
||||
|
||||
outerLoop: for (const id of role.provisions) {
|
||||
const name =
|
||||
provisionDefinitions.provisions[id].name.locales[locales.default];
|
||||
|
||||
for (const otherProvision of provisions.values()) {
|
||||
const otherName =
|
||||
provisionDefinitions.provisions[otherProvision].name.locales[
|
||||
locales.default
|
||||
];
|
||||
|
||||
if (name === otherName) continue outerLoop;
|
||||
}
|
||||
|
||||
provisions.add(id);
|
||||
allGameModeProvisions.add(id);
|
||||
}
|
||||
@@ -155,7 +180,7 @@ const TIERS = times(10, (i) => 10 - i);
|
||||
|
||||
export function FilterControl() {
|
||||
return (
|
||||
<Flex align="center" gap="2" wrap="wrap">
|
||||
<Flex align="center" justify="center" gap="2" wrap="wrap">
|
||||
<TiersFilter />
|
||||
<ClassFilter />
|
||||
<TypeFilter />
|
||||
@@ -168,8 +193,7 @@ export function FilterControl() {
|
||||
<ShellFilter />
|
||||
<ConsumablesFilter />
|
||||
<ProvisionsFilter />
|
||||
<AbilitiesFilter />
|
||||
<PowersFilter />
|
||||
<GameModeAbilitiesFilter />
|
||||
|
||||
<ResetButton />
|
||||
</Flex>
|
||||
@@ -1031,22 +1055,33 @@ function ProvisionsFilter() {
|
||||
);
|
||||
}
|
||||
|
||||
function PowersFilter() {
|
||||
function GameModeAbilitiesFilter() {
|
||||
const { unwrap, strings } = useLocale();
|
||||
const rawAbilities = TankFilters.use((state) => state.abilities);
|
||||
const rawPowers = TankFilters.use((state) => state.powers);
|
||||
|
||||
const abilities =
|
||||
rawAbilities.length === 0 && rawPowers.length === 0
|
||||
? Array.from(allGameModeConsumables.values())
|
||||
: rawAbilities;
|
||||
const powers =
|
||||
rawPowers.length === 0
|
||||
rawPowers.length === 0 && rawAbilities.length === 0
|
||||
? Array.from(allGameModeProvisions.values())
|
||||
: rawPowers;
|
||||
|
||||
const icons = [
|
||||
...abilities.map((ability) => `icons/consumables/${ability}.webp`),
|
||||
...powers.map((power) => `icons/provisions/${power}.webp`),
|
||||
];
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root modal={false}>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button color="gray" variant="surface">
|
||||
<Flex>
|
||||
{powers.slice(0, MAX_ICONS).map((power, index) => (
|
||||
{icons.slice(0, MAX_ICONS).map((icon, index) => (
|
||||
<img
|
||||
key={power}
|
||||
key={icon}
|
||||
style={{
|
||||
filter: "drop-shadow(0 0 var(--space-1) var(--black-a11))",
|
||||
marginLeft: index > 0 ? "-0.5em" : undefined,
|
||||
@@ -1054,14 +1089,14 @@ function PowersFilter() {
|
||||
height: "1.25em",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
src={asset(`icons/provisions/${power}.webp`)}
|
||||
src={asset(icon)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{powers.length > MAX_ICONS && (
|
||||
{icons.length > MAX_ICONS && (
|
||||
<Text size="1" ml="1">
|
||||
{literals(strings.common.units.plus, {
|
||||
value: powers.length - MAX_ICONS,
|
||||
value: icons.length - MAX_ICONS,
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
@@ -1070,14 +1105,75 @@ function PowersFilter() {
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{gameModeRoles.map(({ gameModeId, provisions }) => {
|
||||
{gameModeRoles.map(({ gameModeId, consumables, provisions }) => {
|
||||
const gameMode = gameDefinitions.gameModes[gameModeId];
|
||||
const hasNeither =
|
||||
consumables.length === 0 && provisions.length === 0;
|
||||
|
||||
if (provisions.length === 0) return null;
|
||||
if (hasNeither) return null;
|
||||
|
||||
const hasBoth = consumables.length > 0 && provisions.length > 0;
|
||||
|
||||
return (
|
||||
<Fragment key={gameModeId}>
|
||||
<DropdownMenu.Label>{unwrap(gameMode.name)}</DropdownMenu.Label>
|
||||
{consumables.length > 0 && (
|
||||
<DropdownMenu.Label>
|
||||
{hasBoth
|
||||
? literals(strings.website.common.tank_search.active, {
|
||||
name: unwrap(gameMode.name),
|
||||
})
|
||||
: unwrap(gameMode.name)}
|
||||
</DropdownMenu.Label>
|
||||
)}
|
||||
|
||||
{consumables.map((consumable) => {
|
||||
{
|
||||
const selected = rawAbilities.includes(consumable);
|
||||
const abilityDefinition =
|
||||
consumableDefinitions.consumables[consumable];
|
||||
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
TankFilters.mutate((draft) => {
|
||||
if (selected) {
|
||||
draft.abilities = draft.abilities.filter(
|
||||
(n) => n !== consumable
|
||||
);
|
||||
} else {
|
||||
draft.abilities = [...draft.abilities, consumable];
|
||||
}
|
||||
});
|
||||
}}
|
||||
checked={selected}
|
||||
key={consumable}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "1.25em",
|
||||
height: "1.25em",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
src={asset(`icons/consumables/${consumable}.webp`)}
|
||||
/>
|
||||
|
||||
{unwrap(abilityDefinition.name)}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{provisions.length > 0 && (
|
||||
<DropdownMenu.Label>
|
||||
{hasBoth
|
||||
? literals(strings.website.common.tank_search.passive, {
|
||||
name: unwrap(gameMode.name),
|
||||
})
|
||||
: unwrap(gameMode.name)}
|
||||
</DropdownMenu.Label>
|
||||
)}
|
||||
|
||||
{provisions.map((provision) => {
|
||||
{
|
||||
@@ -1129,6 +1225,7 @@ function PowersFilter() {
|
||||
event.preventDefault();
|
||||
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.abilities = [];
|
||||
draft.powers = [];
|
||||
});
|
||||
}}
|
||||
@@ -1140,113 +1237,3 @@ function PowersFilter() {
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function AbilitiesFilter() {
|
||||
const { unwrap, strings } = useLocale();
|
||||
const rawAbilities = TankFilters.use((state) => state.abilities);
|
||||
const abilities =
|
||||
rawAbilities.length === 0
|
||||
? Array.from(allGameModeConsumables.values())
|
||||
: rawAbilities;
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root modal={false}>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button color="gray" variant="surface">
|
||||
<Flex>
|
||||
{abilities.slice(0, MAX_ICONS).map((ability, index) => (
|
||||
<img
|
||||
key={ability}
|
||||
style={{
|
||||
filter: "drop-shadow(0 0 var(--space-1) var(--black-a11))",
|
||||
marginLeft: index > 0 ? "-0.5em" : undefined,
|
||||
width: "1.25em",
|
||||
height: "1.25em",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
src={asset(`icons/consumables/${ability}.webp`)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{abilities.length > MAX_ICONS && (
|
||||
<Text size="1" ml="1">
|
||||
{literals(strings.common.units.plus, {
|
||||
value: abilities.length - MAX_ICONS,
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{gameModeRoles.map(({ gameModeId, consumables }) => {
|
||||
const gameMode = gameDefinitions.gameModes[gameModeId];
|
||||
|
||||
if (consumables.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Fragment key={gameModeId}>
|
||||
<DropdownMenu.Label>{unwrap(gameMode.name)}</DropdownMenu.Label>
|
||||
|
||||
{consumables.map((consumable) => {
|
||||
{
|
||||
const selected = rawAbilities.includes(consumable);
|
||||
const abilityDefinition =
|
||||
consumableDefinitions.consumables[consumable];
|
||||
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
TankFilters.mutate((draft) => {
|
||||
if (selected) {
|
||||
draft.abilities = draft.abilities.filter(
|
||||
(n) => n !== consumable
|
||||
);
|
||||
} else {
|
||||
draft.abilities = [...draft.abilities, consumable];
|
||||
}
|
||||
});
|
||||
}}
|
||||
checked={selected}
|
||||
key={consumable}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "1.25em",
|
||||
height: "1.25em",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
src={asset(`icons/consumables/${consumable}.webp`)}
|
||||
/>
|
||||
|
||||
{unwrap(abilityDefinition.name)}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Item
|
||||
color="red"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.abilities = [];
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
{strings.website.common.tank_search.clear}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Flex, Heading } from "@radix-ui/themes";
|
||||
import { Suspense } from "react";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { GuessBackground } from "../../../components/GuessBackground";
|
||||
import { Guesser } from "../../../components/Guesser";
|
||||
import { GuessRenderer } from "../../../components/GuessRenderer";
|
||||
@@ -62,6 +62,31 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const name = unwrap(tank.name);
|
||||
const fontSize = `min(48vh, ${55 / name.length}vw)`;
|
||||
const transitionDuration = isRevealed ? "2s" : undefined;
|
||||
const tiers = Guess.use((state) => state.tiers);
|
||||
|
||||
useEffect(() => {
|
||||
const isTankAlreadyCompliant = tiers.includes(tank.tier);
|
||||
|
||||
if (isTankAlreadyCompliant) return;
|
||||
|
||||
const filteredIds = ids.filter((id) => {
|
||||
const tank = tankDefinitions.tanks[Number(id)];
|
||||
return tiers.includes(tank.tier);
|
||||
});
|
||||
|
||||
if (filteredIds.length === 0) return;
|
||||
|
||||
const id = Number(
|
||||
filteredIds[Math.floor(Math.random() * filteredIds.length)]
|
||||
);
|
||||
const newTank = tankDefinitions.tanks[id];
|
||||
|
||||
Guess.mutate((draft) => {
|
||||
draft.tank = newTank;
|
||||
draft.guessState = GuessState.NotGuessed;
|
||||
draft.helpingReveal = false;
|
||||
});
|
||||
}, [tiers]);
|
||||
|
||||
return (
|
||||
<Flex flexGrow="1" position="relative" overflow="hidden">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TankDefinition } from "@blitzkit/core";
|
||||
import { times } from "lodash-es";
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
export enum GuessState {
|
||||
@@ -14,6 +15,7 @@ export interface Guess {
|
||||
correctGuesses: number;
|
||||
streak: number;
|
||||
helpingReveal: boolean;
|
||||
tiers: number[];
|
||||
}
|
||||
|
||||
export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
|
||||
@@ -23,4 +25,5 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
|
||||
correctGuesses: 0,
|
||||
streak: 0,
|
||||
helpingReveal: false,
|
||||
tiers: times(10, (index) => index + 1),
|
||||
}));
|
||||
Reference in new issue
Block a user