mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-09 18:56:11 +01:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eed9d23646 | ||
|
|
2101dba7c6 | ||
|
|
1b398a5f66 | ||
|
|
d8f9a2633d | ||
|
|
2e79ccf7eb | ||
|
|
be778acf39 | ||
|
|
8d10d7cffa | ||
|
|
5b9041a972 | ||
|
|
1e60b1f61f | ||
|
|
13a636c023 | ||
|
|
8bc0693f3b | ||
|
|
9b842c501c | ||
|
|
8aadda4fb8 | ||
|
|
58c70f98e1 | ||
|
|
54b73d11ce | ||
|
|
d1ac5644a4 | ||
|
|
ae35c1516c | ||
|
|
fd364d434f | ||
|
|
fd37f72f8c | ||
|
|
1427f61c52 | ||
|
|
cd4fa366a0 | ||
|
|
344e7caa81 | ||
|
|
fe7b2aea02 | ||
|
|
fced1e96ef | ||
|
|
778efb2372 | ||
|
|
b67fd5b58c | ||
|
|
2559782a28 | ||
|
|
fb829d4b81 | ||
|
|
a692d1ee0b | ||
|
|
6c8fbf7245 | ||
|
|
2ea22512d4 | ||
|
|
697d1fa1b4 | ||
|
|
2c03b530d2 | ||
|
|
182471c4d6 | ||
|
|
aecc2adb07 | ||
|
|
f8ee2b64a6 | ||
|
|
7c4e28bd09 |
No files matched your search
@@ -0,0 +1,11 @@
|
|||||||
|
# BlitzKit 1.29.5
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added new lighting to be a bit softer on tanks
|
||||||
|
- Added a button with the sun icon to toggle high graphics
|
||||||
|
- Added a new reveal and transition animation between tabs
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Track wheels on some tanks no longer look translucent
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# BlitzKit 1.29.6
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Simplified the ownership and test tank filters in the tank search
|
||||||
|
- Renamed "Guess the tank" to simply "Guess"
|
||||||
|
- The player count value in the Performance tool is now set to Total by default
|
||||||
|
- Added Compare-like red/green highlighting to the Performance tool
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Removed the last updated info bar in Performance for some more screen real estate
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The remove played tanks in the Playlist tool now shows even when logged out
|
||||||
|
- The Guess game now has a page title
|
||||||
|
- The compare table now occupies the minimum width possible instead of expanding all columns excessively
|
||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"version": "1.29.4",
|
"version": "1.29.6",
|
||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"build:website": "cd packages/website && bun run build",
|
"build:website": "cd packages/website && bun run build",
|
||||||
"build:prisma": "prisma generate",
|
"build:prisma": "prisma generate",
|
||||||
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
|
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
|
||||||
|
"build:catalogitemaccesssor": "cd packages/scripts && bun run build:catalogitemaccesssor",
|
||||||
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
|
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
|
||||||
"build:app": "cd packages/app && bun run build",
|
"build:app": "cd packages/app && bun run build",
|
||||||
"dev:bot": "bun run build:bot --dev && bun start:bot",
|
"dev:bot": "bun run build:bot --dev && bun start:bot",
|
||||||
|
|||||||
+1
-1
Submodule packages/closed updated: 33b7d22419...5b4a85d503.
@@ -906,8 +906,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess",
|
||||||
"button": "Guess away",
|
"button": "What could it be?",
|
||||||
"description": "Silhouette mini-game",
|
"description": "Silhouette mini-game",
|
||||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -803,8 +803,8 @@
|
|||||||
"randomize": "Randomize"
|
"randomize": "Randomize"
|
||||||
},
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess",
|
||||||
"button": "Guess away",
|
"button": "What could it be?",
|
||||||
"description": "Silhouette mini-game",
|
"description": "Silhouette mini-game",
|
||||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -803,8 +803,8 @@
|
|||||||
"randomize": "Randomize"
|
"randomize": "Randomize"
|
||||||
},
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess",
|
||||||
"button": "Guess away",
|
"button": "What could it be?",
|
||||||
"description": "Silhouette mini-game",
|
"description": "Silhouette mini-game",
|
||||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -803,8 +803,8 @@
|
|||||||
"randomize": "Randomize"
|
"randomize": "Randomize"
|
||||||
},
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess",
|
||||||
"button": "Guess away",
|
"button": "What could it be?",
|
||||||
"description": "Silhouette mini-game",
|
"description": "Silhouette mini-game",
|
||||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -803,8 +803,8 @@
|
|||||||
"randomize": "Randomize"
|
"randomize": "Randomize"
|
||||||
},
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess",
|
||||||
"button": "Guess away",
|
"button": "What could it be?",
|
||||||
"description": "Silhouette mini-game",
|
"description": "Silhouette mini-game",
|
||||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -803,8 +803,8 @@
|
|||||||
"randomize": "Randomize"
|
"randomize": "Randomize"
|
||||||
},
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess",
|
||||||
"button": "Guess away",
|
"button": "What could it be?",
|
||||||
"description": "Silhouette mini-game",
|
"description": "Silhouette mini-game",
|
||||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -803,8 +803,8 @@
|
|||||||
"randomize": "Случайно"
|
"randomize": "Случайно"
|
||||||
},
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Угадай танк",
|
"name": "Угадать",
|
||||||
"button": "Угадать",
|
"button": "Что это за танк?",
|
||||||
"description": "Мини-игра «Силуэт»",
|
"description": "Мини-игра «Силуэт»",
|
||||||
"seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz",
|
"seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"test": {
|
"test": {
|
||||||
"default": "тест",
|
"default": "тест",
|
||||||
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
|
"long": "{fruit}",
|
||||||
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
|
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"name": "BlitzKit",
|
"name": "BlitzKit",
|
||||||
"units": {
|
"units": {
|
||||||
"m": "{value}m",
|
"m": "{value}м",
|
||||||
"mm": "{value}mm",
|
"mm": "{value}мм",
|
||||||
"deg": "{value}°",
|
"deg": "{value}°",
|
||||||
"hp": "{value}hp",
|
"hp": "{value}од",
|
||||||
"x": "x{value}",
|
"x": "x{value}",
|
||||||
"percentage": "{value}%",
|
"percentage": "{value}%",
|
||||||
"deg_s": "{value}°/s"
|
"deg_s": "{value}град/c"
|
||||||
},
|
},
|
||||||
"gun_types": {
|
"gun_types": {
|
||||||
"auto_loader": "Магазинна система заряджання",
|
"auto_loader": "Магазинна система заряджання",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"nations": {
|
"nations": {
|
||||||
"ussr": "СРСР",
|
"ussr": "СРСР",
|
||||||
"germany": "Німеччина",
|
"germany": "Німеччина",
|
||||||
"usa": "США",
|
"usa": "ЗША",
|
||||||
"china": "Китай",
|
"china": "Китай",
|
||||||
"uk": "Велика Британія",
|
"uk": "Велика Британія",
|
||||||
"france": "Франція",
|
"france": "Франція",
|
||||||
@@ -186,11 +186,11 @@
|
|||||||
"bot": {
|
"bot": {
|
||||||
"common": {
|
"common": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
|
"clan_not_found": "# Не вдалося знайти клан\nНе вдалося знайти \"{search}\". Спробуйте обрати клан з результату пошуку.",
|
||||||
"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.",
|
"player_not_found": "# Не вдалося знайти користувача\nНе вдалося знайти користувача \"{search}\". Спробуйте вибрати користувача із результатів пошуку, ввівши правильне ім'я користувача, або скористайтеся командою `/link`.",
|
||||||
"player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!",
|
"player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!",
|
||||||
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
|
"tank_name_not_found": "# Танк не знайдено\nНе вдалося знайти танк із назвою «{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": "# Танк не знайдено\nНе вдалося знайти танк з ID «{search}». Спробуйте ввести назву танка замість номера.",
|
||||||
"no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.",
|
"no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.",
|
||||||
"get_help": "Допомогти Discord серверу",
|
"get_help": "Допомогти Discord серверу",
|
||||||
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
|
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
|
||||||
@@ -284,19 +284,19 @@
|
|||||||
"name": "рівень",
|
"name": "рівень",
|
||||||
"description": "Рівень",
|
"description": "Рівень",
|
||||||
"choices": {
|
"choices": {
|
||||||
"tier": "Tier {tier_1} - {tier_2}"
|
"tier": "Рівень {tier_1} - {tier_2}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"periods": {
|
"periods": {
|
||||||
"career": "Кар'єра",
|
"career": "Кар'єра",
|
||||||
"today": "Сьогодні",
|
"today": "Сьогодні",
|
||||||
"days": "{days} days",
|
"days": "{days} днів",
|
||||||
"custom": "{day_1} - {day_2} days"
|
"custom": "{day_1} - {day_2} днів"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"none": "Немає фільтрів",
|
"none": "Немає фільтрів",
|
||||||
"tier": "Tier {tier}"
|
"tier": "Рівень {tier}"
|
||||||
},
|
},
|
||||||
"no_data": {
|
"no_data": {
|
||||||
"battles_in_period": "За цей період боїв не було",
|
"battles_in_period": "За цей період боїв не було",
|
||||||
@@ -339,10 +339,10 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"subtitle": "Дослідження",
|
"subtitle": "Дослідження",
|
||||||
"total": "Загалом",
|
"total": "Загалом",
|
||||||
"research": "Research: {cost}",
|
"research": "Дослідження: {cost}",
|
||||||
"upgrades": "Upgrades: {cost}",
|
"upgrades": "Upgrades: {cost}",
|
||||||
"purchase": "Purchase: {cost}",
|
"purchase": "Вартість: {cost}",
|
||||||
"equipment": "Equipment: {cost}",
|
"equipment": "Спорядження: {cost}",
|
||||||
"estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди."
|
"estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди."
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -435,7 +435,7 @@
|
|||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"subtitle": "Inactive for {days}+ days",
|
"subtitle": "Inactive for {days}+ days",
|
||||||
"listing": "{days} days"
|
"listing": "{days} днів"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"full_stats": {
|
"full_stats": {
|
||||||
@@ -549,7 +549,7 @@
|
|||||||
"top_players": "Top {count} players",
|
"top_players": "Top {count} players",
|
||||||
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
|
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
|
||||||
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
|
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
|
||||||
"deleted_player": "Deleted {id}",
|
"deleted_player": "",
|
||||||
"neighbors": "Rating neighbors",
|
"neighbors": "Rating neighbors",
|
||||||
"more": "+ {count} more"
|
"more": "+ {count} more"
|
||||||
}
|
}
|
||||||
@@ -643,9 +643,9 @@
|
|||||||
"body": "This page has moved. Redirecting..."
|
"body": "This page has moved. Redirecting..."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Налаштування",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Змінити налаштування BlitzKit ",
|
||||||
"dev_mode": "Development mode",
|
"dev_mode": "Режим розробника",
|
||||||
"language": "Мова"
|
"language": "Мова"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -658,16 +658,16 @@
|
|||||||
"copied": "Скопійовано!"
|
"copied": "Скопійовано!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
"select_dialog_title": "Search for tanks",
|
"select_dialog_title": "Знайти танк",
|
||||||
"swap_dialog_title": "Swap tank",
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Очистити",
|
||||||
"login": "Login to use this",
|
"login": "Увійдіть, щоб скористатися",
|
||||||
"search_bar_hint": "Шукайте техніку...",
|
"search_bar_hint": "Шукайте техніку...",
|
||||||
"recent": "Недавно переглянуті",
|
"recent": "Недавно переглянуті",
|
||||||
"select_all": "Вибрати все",
|
"select_all": "Вибрати все",
|
||||||
"count_singular": "1 танк",
|
"count_singular": "1 танк",
|
||||||
"count_plural": "{count} tanks",
|
"count_plural": "Танків: {count} ",
|
||||||
"sorting_by": "Sorting by {name}",
|
"sorting_by": "Відсортувати за {name}",
|
||||||
"no_tanks_found": {
|
"no_tanks_found": {
|
||||||
"body": "Нічого не знайдено.",
|
"body": "Нічого не знайдено.",
|
||||||
"search_again": "Спробуйте шукати заново.",
|
"search_again": "Спробуйте шукати заново.",
|
||||||
@@ -731,7 +731,7 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"pwa": {
|
"pwa": {
|
||||||
"title": "Install the BlitzKit App",
|
"title": "Install the BlitzKit App",
|
||||||
"new": "NEW",
|
"new": "",
|
||||||
"checking": "Checking compatibility...",
|
"checking": "Checking compatibility...",
|
||||||
"instructions": "Instructions for Apple devices.",
|
"instructions": "Instructions for Apple devices.",
|
||||||
"install": "Install",
|
"install": "Install",
|
||||||
@@ -763,20 +763,20 @@
|
|||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"playlist": {
|
"playlist": {
|
||||||
"reset": "Reset",
|
"reset": "Скинути",
|
||||||
"shuffle": "Shuffle",
|
"shuffle": "Перемішати",
|
||||||
"remove": "Remove played",
|
"remove": "Видалити зіграні",
|
||||||
"tracking": "BlitzKit is tracking your battles",
|
"tracking": "BlitzKit is tracking your battles",
|
||||||
"sort": {
|
"sort": {
|
||||||
"button": "Сортувати за",
|
"button": "Сортувати за",
|
||||||
"last_played": "Last played",
|
"last_played": "Остання гра",
|
||||||
"least_played": "Least played"
|
"least_played": "Найменше зіграно"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"index": "#",
|
"index": "#",
|
||||||
"never": "Never",
|
"never": "Ніколи",
|
||||||
"tank": "Танк",
|
"tank": "Танк",
|
||||||
"last_played": "Last played",
|
"last_played": "Остання гра",
|
||||||
"battles": "Career battles",
|
"battles": "Career battles",
|
||||||
"unrecorded": "--",
|
"unrecorded": "--",
|
||||||
"wins": "Перемоги"
|
"wins": "Перемоги"
|
||||||
@@ -784,14 +784,14 @@
|
|||||||
"large_warning": {
|
"large_warning": {
|
||||||
"title": "That's a lot of tanks!",
|
"title": "That's a lot of tanks!",
|
||||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||||
"cancel": "Cancel",
|
"cancel": "Скасувати",
|
||||||
"proceed": "Proceed"
|
"proceed": "Продовжити"
|
||||||
},
|
},
|
||||||
"generate": "Generate playlist",
|
"generate": "Згенерувати плейлист",
|
||||||
"name": "Playlist",
|
"name": "Плейлист",
|
||||||
"description": "Tank playlist generator",
|
"description": "Tank playlist generator",
|
||||||
"seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!",
|
"seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!",
|
||||||
"button": "Generate",
|
"button": "Створити",
|
||||||
"pool_size": "{count} possible tanks"
|
"pool_size": "{count} possible tanks"
|
||||||
},
|
},
|
||||||
"mixer": {
|
"mixer": {
|
||||||
@@ -803,8 +803,8 @@
|
|||||||
"randomize": "Randomize"
|
"randomize": "Randomize"
|
||||||
},
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess",
|
||||||
"button": "Guess away",
|
"button": "What could it be?",
|
||||||
"description": "Silhouette mini-game",
|
"description": "Silhouette mini-game",
|
||||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -803,8 +803,8 @@
|
|||||||
"randomize": "随机搭配"
|
"randomize": "随机搭配"
|
||||||
},
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "猜坦克",
|
"name": "猜测",
|
||||||
"button": "猜猜看",
|
"button": "What could it be?",
|
||||||
"description": "坦克剪影小游戏",
|
"description": "坦克剪影小游戏",
|
||||||
"seo_description": "看剪影猜坦克",
|
"seo_description": "看剪影猜坦克",
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"build:bot": "bun src/buildBot.ts",
|
"build:bot": "bun src/buildBot.ts",
|
||||||
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts",
|
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts",
|
||||||
"build:proxyclient": "bun src/buildProxyClient.ts",
|
"build:proxyclient": "bun src/buildProxyClient.ts",
|
||||||
|
"build:catalogitemaccesssor": "bun src/buildCatalogItemAccessor.ts",
|
||||||
"prunestrings": "bun src/pruneStrings.ts",
|
"prunestrings": "bun src/pruneStrings.ts",
|
||||||
"build:protos": "bun src/protos.ts",
|
"build:protos": "bun src/protos.ts",
|
||||||
"patchnotes": "bun src/patchNotes.ts"
|
"patchnotes": "bun src/patchNotes.ts"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { argv } from "process";
|
|||||||
const projectDir = "../..";
|
const projectDir = "../..";
|
||||||
const srcRoot = `${projectDir}/packages/bot`;
|
const srcRoot = `${projectDir}/packages/bot`;
|
||||||
const distRoot = `${projectDir}/dist/bot`;
|
const distRoot = `${projectDir}/dist/bot`;
|
||||||
const modulesRoot = `${projectDir}/packages/bot/node_modules`;
|
const modulesRoot = "../../node_modules";
|
||||||
const workers = await readdir(`${srcRoot}/src/workers`);
|
const workers = await readdir(`${srcRoot}/src/workers`);
|
||||||
const dev = argv.includes("--dev");
|
const dev = argv.includes("--dev");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { readdir, readFile, writeFile } from "fs/promises";
|
||||||
|
|
||||||
|
const SOURCES = "../../packages/closed/protos";
|
||||||
|
const TARGET = "../../packages/closed/src/unreal/catalogItemAccessor.ts";
|
||||||
|
|
||||||
|
const componentPattern = /^message (\w+)Component {/gm;
|
||||||
|
|
||||||
|
const files = await readdir(SOURCES).then((files) =>
|
||||||
|
files.filter(
|
||||||
|
(file) =>
|
||||||
|
file.startsWith("blitz_static_") && file.endsWith("_component.proto")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const imports: { name: string; file: string }[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = await readFile(`${SOURCES}/${file}`).then((buffer) =>
|
||||||
|
buffer.toString()
|
||||||
|
);
|
||||||
|
const components = content.matchAll(componentPattern);
|
||||||
|
|
||||||
|
for (const match of components) {
|
||||||
|
const name = match[1];
|
||||||
|
const duplicate = imports.find((_import) => _import.name === name);
|
||||||
|
|
||||||
|
if (duplicate) {
|
||||||
|
console.warn(
|
||||||
|
`Duplicate component ${name} across ${duplicate.file}.proto and ${file}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const importFile = file.replace(".proto", "");
|
||||||
|
|
||||||
|
imports.push({ name, file: importFile });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = "";
|
||||||
|
|
||||||
|
for (const _import of imports) {
|
||||||
|
content += `import { ${_import.name}Component } from "@protos/${_import.file}";\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content +=
|
||||||
|
'import { BaseCatalogItemAccessor } from "./baseCatalogItemAccessor";\n\n';
|
||||||
|
content +=
|
||||||
|
"export class CatalogItemAccessor extends BaseCatalogItemAccessor {\n";
|
||||||
|
|
||||||
|
for (const _import of imports) {
|
||||||
|
const strippedName = _import.name.replace("BlitzStatic", "");
|
||||||
|
const defaultName = strippedName[0].toLowerCase() + strippedName.slice(1);
|
||||||
|
|
||||||
|
content += ` ${strippedName}(name = "${defaultName}Component") {\n`;
|
||||||
|
content += ` return this.component(name, ${_import.name}Component);\n`;
|
||||||
|
content += " }\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
content += "}\n";
|
||||||
|
|
||||||
|
await writeFile(TARGET, content);
|
||||||
@@ -4,21 +4,18 @@ const IMPORT = "@protos/auto_legacy_proxy_client";
|
|||||||
const SOURCE = "../../packages/closed/protos/auto_legacy_proxy_client.proto";
|
const SOURCE = "../../packages/closed/protos/auto_legacy_proxy_client.proto";
|
||||||
const TARGET = "../../packages/closed/src/unreal/methodPopulatedProxyClient.ts";
|
const TARGET = "../../packages/closed/src/unreal/methodPopulatedProxyClient.ts";
|
||||||
|
|
||||||
const requestPattern = /message (\w+)Request {/g;
|
const requestPattern = /^message (\w+)Request {/gm;
|
||||||
const source = await readFile(SOURCE).then((buffer) => buffer.toString());
|
const source = await readFile(SOURCE).then((buffer) => buffer.toString());
|
||||||
const requests = source.matchAll(requestPattern);
|
const requests = source.matchAll(requestPattern);
|
||||||
|
|
||||||
const imports: string[] = [];
|
const imports: string[] = [];
|
||||||
const methods: { name: string; method: string }[] = [];
|
const names: string[] = [];
|
||||||
|
|
||||||
for (const match of requests) {
|
for (const match of requests) {
|
||||||
const name = match[1];
|
const name = match[1];
|
||||||
// const method =
|
|
||||||
// name.charAt(0).toLowerCase() + name.slice(1).replace(/Async$/, "");
|
|
||||||
const method = name;
|
|
||||||
|
|
||||||
imports.push(`${name}Request`, `${name}Response`);
|
imports.push(`${name}Request`, `${name}Response`);
|
||||||
methods.push({ name, method });
|
names.push(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = "";
|
let content = "";
|
||||||
@@ -31,9 +28,9 @@ content += 'import { BaseProxyClient } from "./baseProxyClient";\n\n';
|
|||||||
|
|
||||||
content +=
|
content +=
|
||||||
"export abstract class MethodPopulatedProxyClient extends BaseProxyClient {\n";
|
"export abstract class MethodPopulatedProxyClient extends BaseProxyClient {\n";
|
||||||
content += methods
|
content += names
|
||||||
.map(({ name, method }) => {
|
.map((name) => {
|
||||||
return ` ${method}(data: ${name}Request) {
|
return ` ${name}(data: ${name}Request) {
|
||||||
return this.request("${name}Request", ${name}Request, data, ${name}Response);
|
return this.request("${name}Request", ${name}Request, data, ${name}Response);
|
||||||
}\n`;
|
}\n`;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { readdir, rm } from "fs/promises";
|
|||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
const MAX_COMMAND_LENGTH = 2 ** 11;
|
const MAX_COMMAND_LENGTH = 2 ** 11;
|
||||||
|
const TS_PROTO_EXECUTABLE_LOCATIONS = [
|
||||||
|
"./node_modules/.bin/protoc-gen-ts_proto",
|
||||||
|
"../../node_modules/.bin/protoc-gen-ts_proto",
|
||||||
|
];
|
||||||
|
|
||||||
const roots = [
|
const roots = [
|
||||||
// "../../packages/core/src/protos",
|
// "../../packages/core/src/protos",
|
||||||
@@ -12,12 +16,24 @@ const roots = [
|
|||||||
|
|
||||||
const exec = promisify(execSync);
|
const exec = promisify(execSync);
|
||||||
|
|
||||||
let executableFileExtension: string;
|
let tsProtoExecutableLocation: string | undefined = undefined;
|
||||||
|
|
||||||
if (existsSync("../../node_modules/.bin/protoc-gen-ts_proto.exe")) {
|
for (const location of TS_PROTO_EXECUTABLE_LOCATIONS) {
|
||||||
executableFileExtension = ".exe";
|
if (existsSync(location)) {
|
||||||
} else {
|
tsProtoExecutableLocation = location;
|
||||||
executableFileExtension = "";
|
break;
|
||||||
|
} else if (existsSync(`${location}.exe`)) {
|
||||||
|
tsProtoExecutableLocation = `${location}.exe`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tsProtoExecutableLocation) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find ts-proto executable. Checked locations: ${TS_PROTO_EXECUTABLE_LOCATIONS.join(
|
||||||
|
", "
|
||||||
|
)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const root of roots) {
|
for (const root of roots) {
|
||||||
@@ -32,7 +48,7 @@ for (const root of roots) {
|
|||||||
while (files.length > 0) {
|
while (files.length > 0) {
|
||||||
let command = [
|
let command = [
|
||||||
"protoc",
|
"protoc",
|
||||||
`--plugin=../../node_modules/.bin/protoc-gen-ts_proto${executableFileExtension}`,
|
`--plugin=${tsProtoExecutableLocation}`,
|
||||||
"--ts_proto_opt=esModuleInterop=true",
|
"--ts_proto_opt=esModuleInterop=true",
|
||||||
"--ts_proto_opt=oneof=unions-value",
|
"--ts_proto_opt=oneof=unions-value",
|
||||||
// "--ts_proto_opt=removeEnumPrefix=true",
|
// "--ts_proto_opt=removeEnumPrefix=true",
|
||||||
|
|||||||
Binary file not shown.
@@ -67,7 +67,9 @@ export const PrimaryArmorScene = memo(() => {
|
|||||||
<group ref={turretContainer}>
|
<group ref={turretContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentTurret = node.name.startsWith(
|
const isCurrentTurret = node.name.startsWith(
|
||||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
|
`turret_${turretModelDefinition.model_id
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`
|
||||||
);
|
);
|
||||||
const isVisible = isCurrentTurret;
|
const isVisible = isCurrentTurret;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
|
|||||||
+12
-1
@@ -4,6 +4,7 @@ import {
|
|||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import { invalidate, useFrame } from "@react-three/fiber";
|
import { invalidate, useFrame } from "@react-three/fiber";
|
||||||
|
import type { QuicklimeEvent } from "quicklime";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
@@ -21,6 +22,7 @@ import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
|||||||
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
|
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
|
||||||
import { Tankopedia } from "../../../../stores/tankopedia";
|
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||||
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
|
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
|
||||||
|
import { transitionEvent } from "../../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||||
import fragmentShader from "./shaders/fragment.glsl?raw";
|
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||||
import vertexShader from "./shaders/vertex.glsl?raw";
|
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||||
import { spacedArmorRenderTarget } from "./target";
|
import { spacedArmorRenderTarget } from "./target";
|
||||||
@@ -72,6 +74,8 @@ export function PrimaryArmorSceneComponent({
|
|||||||
resolution: { value: new Vector2() },
|
resolution: { value: new Vector2() },
|
||||||
spacedArmorBuffer: { value: null },
|
spacedArmorBuffer: { value: null },
|
||||||
spacedArmorDepth: { value: null },
|
spacedArmorDepth: { value: null },
|
||||||
|
|
||||||
|
opacity: { value: 0 },
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
@@ -154,6 +158,10 @@ export function PrimaryArmorSceneComponent({
|
|||||||
if (!noInvalidate) invalidate();
|
if (!noInvalidate) invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTransitionEvent(event: QuicklimeEvent<number>) {
|
||||||
|
material.uniforms.opacity.value = event.data;
|
||||||
|
}
|
||||||
|
|
||||||
handleShellChange();
|
handleShellChange();
|
||||||
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
|
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
|
||||||
handleAdvancedHighlightingChange(
|
handleAdvancedHighlightingChange(
|
||||||
@@ -164,6 +172,8 @@ export function PrimaryArmorSceneComponent({
|
|||||||
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
|
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
|
||||||
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
|
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
|
||||||
|
|
||||||
|
transitionEvent.on(handleTransitionEvent);
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
Duel.on((state) => state.antagonist.shell, handleShellChange),
|
Duel.on((state) => state.antagonist.shell, handleShellChange),
|
||||||
Tankopedia.on((state) => state.customShell, handleShellChange),
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
@@ -184,12 +194,13 @@ export function PrimaryArmorSceneComponent({
|
|||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
(equipment) => handleAntagonistEquipmentChange(equipment)
|
(equipment) => handleAntagonistEquipmentChange(equipment)
|
||||||
),
|
),
|
||||||
|
() => transitionEvent.off(handleTransitionEvent),
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
};
|
};
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
useFrame(({ gl, camera }) => {
|
useFrame(({ gl, camera }) => {
|
||||||
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||||
|
|||||||
+2
-5
@@ -1,12 +1,9 @@
|
|||||||
precision mediump float;
|
precision mediump float;
|
||||||
|
|
||||||
#define USE_FOG
|
|
||||||
|
|
||||||
#include <fog_pars_fragment>
|
|
||||||
|
|
||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
varying vec3 vViewPosition;
|
varying vec3 vViewPosition;
|
||||||
|
|
||||||
|
uniform float opacity;
|
||||||
uniform float thickness;
|
uniform float thickness;
|
||||||
uniform float penetration;
|
uniform float penetration;
|
||||||
uniform float caliber;
|
uniform float caliber;
|
||||||
@@ -119,5 +116,5 @@ void main() {
|
|||||||
gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
|
gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
#include <fog_fragment>
|
gl_FragColor.a *= opacity;
|
||||||
}
|
}
|
||||||
-6
@@ -1,9 +1,5 @@
|
|||||||
precision mediump float;
|
precision mediump float;
|
||||||
|
|
||||||
#define USE_FOG
|
|
||||||
|
|
||||||
#include <fog_pars_vertex>
|
|
||||||
|
|
||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
varying vec3 vViewPosition;
|
varying vec3 vViewPosition;
|
||||||
|
|
||||||
@@ -14,6 +10,4 @@ void main() {
|
|||||||
vNormal = normalMatrix * normal;
|
vNormal = normalMatrix * normal;
|
||||||
|
|
||||||
gl_Position = projectionMatrix * mvPosition;
|
gl_Position = projectionMatrix * mvPosition;
|
||||||
|
|
||||||
#include <fog_vertex>
|
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from "../../../core/blitzkit/modelTransform";
|
} from "../../../core/blitzkit/modelTransform";
|
||||||
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
|
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
|
||||||
import { Tankopedia } from "../../../stores/tankopedia";
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
|
import { transitionEvent } from "../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||||
import { ArmorType } from "./SpacedArmorScene";
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
import type {
|
import type {
|
||||||
ArmorUserData,
|
ArmorUserData,
|
||||||
@@ -108,10 +109,11 @@ export function StaticArmorSceneComponent({
|
|||||||
() =>
|
() =>
|
||||||
new MeshBasicMaterial({
|
new MeshBasicMaterial({
|
||||||
color,
|
color,
|
||||||
opacity,
|
transparent: true,
|
||||||
transparent: opacity < 1,
|
|
||||||
depthWrite,
|
depthWrite,
|
||||||
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
||||||
|
opacity: 0,
|
||||||
|
userData: { opacity0: opacity },
|
||||||
}),
|
}),
|
||||||
[thickness]
|
[thickness]
|
||||||
);
|
);
|
||||||
@@ -121,6 +123,8 @@ export function StaticArmorSceneComponent({
|
|||||||
color: color
|
color: color
|
||||||
.clone()
|
.clone()
|
||||||
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
||||||
|
opacity: 0,
|
||||||
|
transparent: true,
|
||||||
}),
|
}),
|
||||||
[thickness]
|
[thickness]
|
||||||
);
|
);
|
||||||
@@ -163,11 +167,22 @@ export function StaticArmorSceneComponent({
|
|||||||
surfaceMaterial.needsUpdate = true;
|
surfaceMaterial.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTransitionEvent(event: QuicklimeEvent<number>) {
|
||||||
|
surfaceMaterial.opacity = surfaceMaterial.userData.opacity0 * event.data;
|
||||||
|
outlineMaterial.opacity = event.data;
|
||||||
|
|
||||||
|
surfaceMaterial.transparent = surfaceMaterial.opacity < 1;
|
||||||
|
outlineMaterial.transparent = outlineMaterial.opacity < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionEvent.on(handleTransitionEvent);
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
Tankopedia.on(
|
Tankopedia.on(
|
||||||
(state) => state.highlightArmor?.name,
|
(state) => state.highlightArmor?.name,
|
||||||
handleHighlightArmor
|
handleHighlightArmor
|
||||||
),
|
),
|
||||||
|
() => transitionEvent.off(handleTransitionEvent),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (props.clip) {
|
if (props.clip) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
|
|||||||
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||||
import { useLocale } from "../../hooks/useLocale";
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
|
import { CompareCellDirection } from "../CompareCell";
|
||||||
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { CompareRow } from "./CompareRow";
|
import { CompareRow } from "./CompareRow";
|
||||||
import { CompareSectionTitle } from "./CompareSectionTitle";
|
import { CompareSectionTitle } from "./CompareSectionTitle";
|
||||||
@@ -79,7 +80,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
name={strings.website.tools.tankopedia.characteristics.values.reload}
|
name={strings.website.tools.tankopedia.characteristics.values.reload}
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
value={(stats) =>
|
value={(stats) =>
|
||||||
stats.shellReload ??
|
stats.shellReload ??
|
||||||
stats.shellReloads!.reduce((a, b) => a + b, 0) /
|
stats.shellReloads!.reduce((a, b) => a + b, 0) /
|
||||||
@@ -95,7 +96,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<>
|
<>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
value="intraClip"
|
value="intraClip"
|
||||||
decimals={2}
|
decimals={2}
|
||||||
/>
|
/>
|
||||||
@@ -107,7 +108,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
value="shellRicochet"
|
value="shellRicochet"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={0}
|
decimals={0}
|
||||||
/>
|
/>
|
||||||
<CompareRow stats={stats} value="shellVelocity" decimals={0} />
|
<CompareRow stats={stats} value="shellVelocity" decimals={0} />
|
||||||
@@ -116,48 +117,48 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
value="aimTime"
|
value="aimTime"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={2}
|
decimals={2}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
value="dispersion"
|
value="dispersion"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={3}
|
decimals={3}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="dispersionMoving"
|
value="dispersionMoving"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={3}
|
decimals={3}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="dispersionHullTraversing"
|
value="dispersionHullTraversing"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={3}
|
decimals={3}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="dispersionTurretTraversing"
|
value="dispersionTurretTraversing"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={3}
|
decimals={3}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="dispersionShooting"
|
value="dispersionShooting"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={3}
|
decimals={3}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="dispersionGunDamaged"
|
value="dispersionGunDamaged"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={3}
|
decimals={3}
|
||||||
/>
|
/>
|
||||||
<CompareRow stats={stats} value="gunDepression" decimals={1} />
|
<CompareRow stats={stats} value="gunDepression" decimals={1} />
|
||||||
@@ -186,21 +187,21 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
value="hardTerrainCoefficientRaw"
|
value="hardTerrainCoefficientRaw"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={2}
|
decimals={2}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="mediumTerrainCoefficientRaw"
|
value="mediumTerrainCoefficientRaw"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={2}
|
decimals={2}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="softTerrainCoefficientRaw"
|
value="softTerrainCoefficientRaw"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={2}
|
decimals={2}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
@@ -246,7 +247,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
value="fireChance"
|
value="fireChance"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
display={(stats) => (stats.fireChance * 100).toFixed(0)}
|
display={(stats) => (stats.fireChance * 100).toFixed(0)}
|
||||||
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
|
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
|
||||||
/>
|
/>
|
||||||
@@ -288,28 +289,28 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
value="volume"
|
value="volume"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={1}
|
decimals={1}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="width"
|
value="width"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={1}
|
decimals={1}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="height"
|
value="height"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={1}
|
decimals={1}
|
||||||
/>
|
/>
|
||||||
<CompareRow
|
<CompareRow
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
value="length"
|
value="length"
|
||||||
deltaType="lowerIsBetter"
|
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
decimals={1}
|
decimals={1}
|
||||||
/>
|
/>
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import {
|
|||||||
import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
|
import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||||
import { Var } from "../../core/radix/var";
|
|
||||||
import { useLocale } from "../../hooks/useLocale";
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { ComparePersistent } from "../../stores/comparePersistent";
|
import { ComparePersistent } from "../../stores/comparePersistent";
|
||||||
|
import { CompareCell, CompareCellDirection } from "../CompareCell";
|
||||||
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
||||||
|
|
||||||
type CompareRowProps = {
|
type CompareRowProps = {
|
||||||
display?: (
|
display?: (
|
||||||
member: Awaited<TankCharacteristics>
|
member: Awaited<TankCharacteristics>
|
||||||
) => number | string | undefined;
|
) => number | string | undefined;
|
||||||
deltaType?: "higherIsBetter" | "lowerIsBetter";
|
deltaType?: CompareCellDirection;
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
deltaNominalDisplay?: (delta: number) => string | number;
|
deltaNominalDisplay?: (delta: number) => string | number;
|
||||||
indent?: boolean;
|
indent?: boolean;
|
||||||
@@ -33,7 +33,7 @@ type CompareRowProps = {
|
|||||||
|
|
||||||
export function CompareRow({
|
export function CompareRow({
|
||||||
display,
|
display,
|
||||||
deltaType = "higherIsBetter",
|
deltaType = CompareCellDirection.HIGHER_IS_BETTER,
|
||||||
decimals,
|
decimals,
|
||||||
deltaNominalDisplay,
|
deltaNominalDisplay,
|
||||||
indent,
|
indent,
|
||||||
@@ -108,33 +108,18 @@ export function CompareRow({
|
|||||||
{values.map((value, index) => {
|
{values.map((value, index) => {
|
||||||
const delta = value - values[0];
|
const delta = value - values[0];
|
||||||
const deltaPercentage = value / values[0] - 1;
|
const deltaPercentage = value / values[0] - 1;
|
||||||
const normalizedDeltaPercentage = Math.round(
|
|
||||||
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25)
|
|
||||||
);
|
|
||||||
const resolvedDisplayValue = display
|
const resolvedDisplayValue = display
|
||||||
? display(stats[index])
|
? display(stats[index])
|
||||||
: decimals === undefined
|
: decimals === undefined
|
||||||
? value
|
? value
|
||||||
: value?.toFixed(decimals);
|
: value?.toFixed(decimals);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Cell
|
<CompareCell
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
truth={values[0]}
|
||||||
backgroundColor:
|
value={value}
|
||||||
index === 0 ||
|
direction={deltaType}
|
||||||
value === undefined ||
|
|
||||||
values[0] === undefined ||
|
|
||||||
value === values[0]
|
|
||||||
? undefined
|
|
||||||
: (
|
|
||||||
deltaType === "higherIsBetter"
|
|
||||||
? value > values[0]
|
|
||||||
: value < values[0]
|
|
||||||
)
|
|
||||||
? `color-mix(in srgb, ${Var("green-7")} ${normalizedDeltaPercentage}%, ${Var("green-3")})`
|
|
||||||
: `color-mix(in srgb, ${Var("red-7")} ${normalizedDeltaPercentage}%, ${Var("red-3")})`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
@@ -160,8 +145,8 @@ export function CompareRow({
|
|||||||
deltaNominalDisplay
|
deltaNominalDisplay
|
||||||
? deltaNominalDisplay(delta)
|
? deltaNominalDisplay(delta)
|
||||||
: decimals === undefined
|
: decimals === undefined
|
||||||
? delta
|
? delta
|
||||||
: delta.toFixed(decimals)
|
: delta.toFixed(decimals)
|
||||||
}`}
|
}`}
|
||||||
)
|
)
|
||||||
</Text>
|
</Text>
|
||||||
@@ -179,7 +164,7 @@ export function CompareRow({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
|
|||||||
@@ -51,19 +51,17 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="surface"
|
variant="surface"
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
width: "100%",
|
maxWidth: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
borderRadius: 0,
|
borderBottomRightRadius: 0,
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
|
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
|
||||||
<Flex
|
<Flex
|
||||||
style={{
|
style={{ width: "100%", height: "100%" }}
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
@@ -107,8 +105,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
crewSkills[skill] === 0
|
crewSkills[skill] === 0
|
||||||
? Var("gray-11")
|
? Var("gray-11")
|
||||||
: permanentSkills.includes(skill)
|
: permanentSkills.includes(skill)
|
||||||
? Var("crimson-11")
|
? Var("crimson-11")
|
||||||
: Var("amber-11"),
|
: Var("amber-11"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StickyColumnHeaderCell>
|
<StickyColumnHeaderCell width="0">
|
||||||
<Flex direction="column" align="center" justify="between" gap="2">
|
<Flex direction="column" align="center" justify="between" gap="2">
|
||||||
<TankControl index={index} key={tank.id} slug={tank.slug} />
|
<TankControl index={index} key={tank.id} slug={tank.slug} />
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Table } from "@radix-ui/themes";
|
||||||
|
import type { CellProps } from "@radix-ui/themes/components/table";
|
||||||
|
|
||||||
|
export enum CompareCellDirection {
|
||||||
|
HIGHER_IS_BETTER,
|
||||||
|
LOWER_IS_BETTER,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompareCellProps extends CellProps {
|
||||||
|
value: number;
|
||||||
|
truth: number;
|
||||||
|
direction?: CompareCellDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompareCell({
|
||||||
|
value,
|
||||||
|
truth,
|
||||||
|
style,
|
||||||
|
direction = CompareCellDirection.HIGHER_IS_BETTER,
|
||||||
|
...props
|
||||||
|
}: CompareCellProps) {
|
||||||
|
const percentage = value / truth - 1;
|
||||||
|
const shiftedPercentage = Math.round(
|
||||||
|
Math.min(100, Math.abs(percentage) * 2 * 100 + 25)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Cell
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
value === truth
|
||||||
|
? undefined
|
||||||
|
: (direction === CompareCellDirection.HIGHER_IS_BETTER) ===
|
||||||
|
value > truth
|
||||||
|
? `color-mix(in srgb, var(--green-7) ${shiftedPercentage}%, var(--green-3))`
|
||||||
|
: `color-mix(in srgb, var(--red-7) ${shiftedPercentage}%, var(--red-3))`,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Box, Text } from "@radix-ui/themes";
|
||||||
|
import { Html } from "@react-three/drei";
|
||||||
import { memo, type ComponentProps } from "react";
|
import { memo, type ComponentProps } from "react";
|
||||||
|
|
||||||
interface Props extends ComponentProps<"spotLight"> {
|
interface Props extends ComponentProps<"spotLight"> {
|
||||||
@@ -8,10 +10,20 @@ export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{debug && (
|
{debug && (
|
||||||
<mesh position={props.position}>
|
<group position={props.position}>
|
||||||
<icosahedronGeometry args={[0.1, 2]} />
|
<mesh>
|
||||||
<meshBasicMaterial color={props.color} />
|
<icosahedronGeometry args={[0.1, 2]} />
|
||||||
</mesh>
|
<meshBasicMaterial color={props.color} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<Html center>
|
||||||
|
<Box pb="9">
|
||||||
|
<Text wrap="nowrap">
|
||||||
|
{props.intensity} @ {JSON.stringify(props.userData)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Html>
|
||||||
|
</group>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<spotLight {...props} />
|
<spotLight {...props} />
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { Flex } from '@radix-ui/themes';
|
import { Flex } from "@radix-ui/themes";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
import { StickyTableRoot } from '../StickyTableRoot';
|
import { StickyTableRoot } from "../StickyTableRoot";
|
||||||
import { Header } from './Header';
|
import { Header } from "./Header";
|
||||||
import { Tanks } from './Tanks';
|
import { Tanks } from "./Tanks";
|
||||||
|
|
||||||
export function TankTable({ skeleton }: MaybeSkeletonComponentProps) {
|
export function TankTable({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
return (
|
return (
|
||||||
<Flex justify="center" maxWidth="100%" flexGrow="1" position="relative">
|
<Flex justify="center" maxWidth="100%" flexGrow="1" position="relative">
|
||||||
<StickyTableRoot
|
<StickyTableRoot
|
||||||
size={{ initial: '1', sm: '2' }}
|
size={{ initial: "1", sm: "2" }}
|
||||||
variant="surface"
|
variant="surface"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
maxHeight: '100%',
|
maxHeight: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
|
borderBottomRightRadius: 0,
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import { TankDefinition, formatCompact } from '@blitzkit/core';
|
import { TankDefinition, formatCompact } from "@blitzkit/core";
|
||||||
import { Table } from '@radix-ui/themes';
|
import { Table } from "@radix-ui/themes";
|
||||||
import { memo } from 'react';
|
import { memo } from "react";
|
||||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { Performance } from '../../stores/performance';
|
import { Performance } from "../../stores/performance";
|
||||||
import { TankRowHeaderCell } from '../TankRowHeaderCell';
|
import { CompareCell, CompareCellDirection } from "../CompareCell";
|
||||||
|
import { TankRowHeaderCell } from "../TankRowHeaderCell";
|
||||||
|
|
||||||
interface TankRowProps {
|
interface TankRowProps {
|
||||||
tank: TankDefinition;
|
tank: TankDefinition;
|
||||||
|
average: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const averageDefinitions = await awaitableAverageDefinitions;
|
const averageDefinitions = await awaitableAverageDefinitions;
|
||||||
|
|
||||||
export const TankRow = memo<TankRowProps>(
|
export const TankRow = memo<TankRowProps>(
|
||||||
({ tank }) => {
|
({ tank, average }) => {
|
||||||
const averages = averageDefinitions.averages[tank.id];
|
const averages = averageDefinitions.averages[tank.id];
|
||||||
const ratio = useAveragesExclusionRatio();
|
const ratio = useAveragesExclusionRatio();
|
||||||
const playerCountPeriod = Performance.use(
|
const playerCountPeriod = Performance.use(
|
||||||
(state) => state.playerCountPeriod,
|
(state) => state.playerCountPeriod
|
||||||
);
|
);
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|
||||||
@@ -26,59 +28,108 @@ export const TankRow = memo<TankRowProps>(
|
|||||||
<Table.Row>
|
<Table.Row>
|
||||||
<TankRowHeaderCell tank={tank} />
|
<TankRowHeaderCell tank={tank} />
|
||||||
|
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.wins / averages.mu.battles}
|
||||||
|
truth={average.winrate}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{((averages.mu.wins / averages.mu.battles) * 100).toFixed(1)}%
|
{((averages.mu.wins / averages.mu.battles) * 100).toFixed(1)}%
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<Table.Cell align="center">
|
||||||
{formatCompact(
|
{formatCompact(
|
||||||
locale,
|
locale,
|
||||||
Math.round(ratio * averages.samples[playerCountPeriod]),
|
Math.round(ratio * averages.samples[playerCountPeriod])
|
||||||
)}
|
)}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.damage_dealt / averages.mu.battles}
|
||||||
|
truth={average.damage}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{Math.round(
|
{Math.round(
|
||||||
averages.mu.damage_dealt / averages.mu.battles,
|
averages.mu.damage_dealt / averages.mu.battles
|
||||||
).toLocaleString(locale)}
|
).toLocaleString(locale)}
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.survived_battles / averages.mu.battles}
|
||||||
|
truth={average.survival}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{Math.round(
|
{Math.round(
|
||||||
(averages.mu.survived_battles / averages.mu.battles) * 100,
|
(averages.mu.survived_battles / averages.mu.battles) * 100
|
||||||
)}
|
)}
|
||||||
%
|
%
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.xp / averages.mu.battles}
|
||||||
|
truth={average.xp}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString(
|
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString(
|
||||||
locale,
|
locale
|
||||||
)}
|
)}
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.frags / averages.mu.battles}
|
||||||
|
truth={average.kills}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{(averages.mu.frags / averages.mu.battles).toFixed(2)}
|
{(averages.mu.frags / averages.mu.battles).toFixed(2)}
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.spotted / averages.mu.battles}
|
||||||
|
truth={average.spots}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{(averages.mu.spotted / averages.mu.battles).toFixed(2)}
|
{(averages.mu.spotted / averages.mu.battles).toFixed(2)}
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.hits / averages.mu.shots}
|
||||||
|
truth={average.accuracy}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{Math.round((averages.mu.hits / averages.mu.shots) * 100)}%
|
{Math.round((averages.mu.hits / averages.mu.shots) * 100)}%
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.shots / averages.mu.battles}
|
||||||
|
truth={average.shots}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{(averages.mu.shots / averages.mu.battles).toFixed(1)}
|
{(averages.mu.shots / averages.mu.battles).toFixed(1)}
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.hits / averages.mu.battles}
|
||||||
|
truth={average.hits}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{(averages.mu.hits / averages.mu.battles).toFixed(1)}
|
{(averages.mu.hits / averages.mu.battles).toFixed(1)}
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.damage_dealt / averages.mu.damage_received}
|
||||||
|
truth={average.damageRatio}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{(averages.mu.damage_dealt / averages.mu.damage_received).toFixed(2)}
|
{(averages.mu.damage_dealt / averages.mu.damage_received).toFixed(2)}
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.damage_received / averages.mu.battles}
|
||||||
|
truth={average.damageReceived}
|
||||||
|
direction={CompareCellDirection.LOWER_IS_BETTER}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{Math.round(
|
{Math.round(
|
||||||
averages.mu.damage_received / averages.mu.battles,
|
averages.mu.damage_received / averages.mu.battles
|
||||||
).toLocaleString(locale)}
|
).toLocaleString(locale)}
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
<Table.Cell align="center">
|
<CompareCell
|
||||||
|
value={averages.mu.capture_points / averages.mu.battles}
|
||||||
|
truth={average.capturePoints}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)}
|
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)}
|
||||||
</Table.Cell>
|
</CompareCell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(prev, next) => prev.tank.id === next.tank.id,
|
(prev, next) => prev.tank.id === next.tank.id
|
||||||
);
|
);
|
||||||
@@ -1,17 +1,27 @@
|
|||||||
|
import { formatCompact, type BlitzkitStats } from "@blitzkit/core";
|
||||||
import { Table } from "@radix-ui/themes";
|
import { Table } from "@radix-ui/themes";
|
||||||
import { times } from "lodash-es";
|
import { times } from "lodash-es";
|
||||||
import { Fragment, Suspense, useEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
Fragment,
|
||||||
|
Suspense,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import usePromise from "react-promise-suspense";
|
import usePromise from "react-promise-suspense";
|
||||||
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||||
import { filterTanks } from "../../core/blitzkit/filterTanks";
|
import { filterTanks } from "../../core/blitzkit/filterTanks";
|
||||||
|
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
|
||||||
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { Performance } from "../../stores/performance";
|
import { Performance } from "../../stores/performance";
|
||||||
import { TankFilters } from "../../stores/tankFilters";
|
import { TankFilters } from "../../stores/tankFilters";
|
||||||
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
|
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
|
||||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
|
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
||||||
import { RowLoader } from "./RowLoader";
|
import { RowLoader } from "./RowLoader";
|
||||||
import { TankRow } from "./TankRow";
|
import { TankRow } from "./TankRow";
|
||||||
import { Total } from "./Total";
|
|
||||||
|
|
||||||
const PREVIEW_COUNT = 10;
|
const PREVIEW_COUNT = 10;
|
||||||
const DEFAULT_LOADED_ROWS = 25;
|
const DEFAULT_LOADED_ROWS = 25;
|
||||||
@@ -145,9 +155,83 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
setLoadedRows(DEFAULT_LOADED_ROWS);
|
setLoadedRows(DEFAULT_LOADED_ROWS);
|
||||||
}, [filters, sort]);
|
}, [filters, sort]);
|
||||||
|
|
||||||
|
const playerCountPeriod = Performance.use((state) => state.playerCountPeriod);
|
||||||
|
const { strings, locale } = useLocale();
|
||||||
|
const ratio = useAveragesExclusionRatio();
|
||||||
|
const sum = useCallback(
|
||||||
|
(slice: (tank: BlitzkitStats) => number) => {
|
||||||
|
return tanksSorted.reduce(
|
||||||
|
(acc, tank) => acc + tank.samples[playerCountPeriod] * slice(tank.mu),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[tanksSorted]
|
||||||
|
);
|
||||||
|
|
||||||
|
const battles = sum(({ battles }) => battles);
|
||||||
|
const winrate = sum(({ wins }) => wins) / battles;
|
||||||
|
const damage = sum(({ damage_dealt }) => damage_dealt) / battles;
|
||||||
|
const survival = sum(({ survived_battles }) => survived_battles) / battles;
|
||||||
|
const xp = sum(({ xp }) => xp) / battles;
|
||||||
|
const kills = sum(({ frags }) => frags) / battles;
|
||||||
|
const spots = sum(({ spotted }) => spotted) / battles;
|
||||||
|
const hits = sum(({ hits }) => hits) / battles;
|
||||||
|
const shots = sum(({ shots }) => shots) / battles;
|
||||||
|
const accuracy = hits / shots;
|
||||||
|
const damageReceived =
|
||||||
|
sum(({ damage_received }) => damage_received) / battles;
|
||||||
|
const damageRatio = damage / damageReceived;
|
||||||
|
const capturePoints = sum(({ capture_points }) => capture_points) / battles;
|
||||||
|
|
||||||
|
const average = {
|
||||||
|
battles,
|
||||||
|
winrate,
|
||||||
|
damage,
|
||||||
|
survival,
|
||||||
|
xp,
|
||||||
|
kills,
|
||||||
|
spots,
|
||||||
|
hits,
|
||||||
|
shots,
|
||||||
|
accuracy,
|
||||||
|
damageReceived,
|
||||||
|
damageRatio,
|
||||||
|
capturePoints,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
<Total tanks={tanksSorted} />
|
<Table.Row>
|
||||||
|
<StickyRowHeaderCell style={{ overflow: "hidden", display: "flex" }}>
|
||||||
|
{strings.website.tools.performance.table.tanks.total}
|
||||||
|
</StickyRowHeaderCell>
|
||||||
|
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
|
||||||
|
<Table.Cell align="center">
|
||||||
|
{formatCompact(
|
||||||
|
locale,
|
||||||
|
ratio * averageDefinitions.samples[playerCountPeriod]
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell align="center">
|
||||||
|
{Math.round(damage).toLocaleString(locale)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell align="center">
|
||||||
|
{Math.round(survival * 100).toFixed(1)}%
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell align="center">
|
||||||
|
{Math.round(xp).toLocaleString(locale)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell align="center">{kills.toFixed(2)}</Table.Cell>
|
||||||
|
<Table.Cell align="center">{spots.toFixed(2)}</Table.Cell>
|
||||||
|
<Table.Cell align="center">{(accuracy * 100).toFixed(0)}%</Table.Cell>
|
||||||
|
<Table.Cell align="center">{shots.toFixed(1)}</Table.Cell>
|
||||||
|
<Table.Cell align="center">{hits.toFixed(1)}</Table.Cell>
|
||||||
|
<Table.Cell align="center">{damageRatio.toFixed(2)}</Table.Cell>
|
||||||
|
<Table.Cell align="center">
|
||||||
|
{Math.round(damageReceived).toLocaleString(locale)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
|
||||||
{tanksSorted.slice(0, loadedRows).map((averages) => {
|
{tanksSorted.slice(0, loadedRows).map((averages) => {
|
||||||
const tank = tankDefinitions.tanks[averages.id];
|
const tank = tankDefinitions.tanks[averages.id];
|
||||||
@@ -157,7 +241,7 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
return (
|
return (
|
||||||
<Fragment key={tank.id}>
|
<Fragment key={tank.id}>
|
||||||
<Suspense fallback={<RowLoader />}>
|
<Suspense fallback={<RowLoader />}>
|
||||||
<TankRow tank={tank} />
|
<TankRow average={average} tank={tank} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import {
|
|
||||||
AverageDefinitionsEntry,
|
|
||||||
formatCompact,
|
|
||||||
type BlitzkitStats,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import { Table } from "@radix-ui/themes";
|
|
||||||
import { memo, useCallback } from "react";
|
|
||||||
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
|
||||||
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
|
|
||||||
import { useLocale } from "../../hooks/useLocale";
|
|
||||||
import { Performance } from "../../stores/performance";
|
|
||||||
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
|
||||||
|
|
||||||
export interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TotalProps {
|
|
||||||
tanks: AverageDefinitionsEntryWithId[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const averageDefinitions = await awaitableAverageDefinitions;
|
|
||||||
|
|
||||||
export const Total = memo<TotalProps>(
|
|
||||||
({ tanks }) => {
|
|
||||||
const playerCountPeriod = Performance.use(
|
|
||||||
(state) => state.playerCountPeriod
|
|
||||||
);
|
|
||||||
const { strings, locale } = useLocale();
|
|
||||||
const ratio = useAveragesExclusionRatio();
|
|
||||||
const sum = useCallback(
|
|
||||||
(slice: (tank: BlitzkitStats) => number) => {
|
|
||||||
return tanks.reduce(
|
|
||||||
(acc, tank) => acc + tank.samples[playerCountPeriod] * slice(tank.mu),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[tanks]
|
|
||||||
);
|
|
||||||
|
|
||||||
const battles = sum(({ battles }) => battles);
|
|
||||||
const winrate = sum(({ wins }) => wins) / battles;
|
|
||||||
const damage = sum(({ damage_dealt }) => damage_dealt) / battles;
|
|
||||||
const survival = sum(({ survived_battles }) => survived_battles) / battles;
|
|
||||||
const xp = sum(({ xp }) => xp) / battles;
|
|
||||||
const kills = sum(({ frags }) => frags) / battles;
|
|
||||||
const spots = sum(({ spotted }) => spotted) / battles;
|
|
||||||
const hits = sum(({ hits }) => hits) / battles;
|
|
||||||
const shots = sum(({ shots }) => shots) / battles;
|
|
||||||
const accuracy = hits / shots;
|
|
||||||
const damageReceived =
|
|
||||||
sum(({ damage_received }) => damage_received) / battles;
|
|
||||||
const damageRatio = damage / damageReceived;
|
|
||||||
const capturePoints = sum(({ capture_points }) => capture_points) / battles;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table.Row>
|
|
||||||
<StickyRowHeaderCell style={{ overflow: "hidden", display: "flex" }}>
|
|
||||||
{strings.website.tools.performance.table.tanks.total}
|
|
||||||
</StickyRowHeaderCell>
|
|
||||||
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
|
|
||||||
<Table.Cell align="center">
|
|
||||||
{formatCompact(
|
|
||||||
locale,
|
|
||||||
ratio * averageDefinitions.samples[playerCountPeriod]
|
|
||||||
)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell align="center">
|
|
||||||
{Math.round(damage).toLocaleString(locale)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell align="center">
|
|
||||||
{Math.round(survival * 100).toFixed(1)}%
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell align="center">
|
|
||||||
{Math.round(xp).toLocaleString(locale)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell align="center">{kills.toFixed(2)}</Table.Cell>
|
|
||||||
<Table.Cell align="center">{spots.toFixed(2)}</Table.Cell>
|
|
||||||
<Table.Cell align="center">{(accuracy * 100).toFixed(0)}%</Table.Cell>
|
|
||||||
<Table.Cell align="center">{shots.toFixed(1)}</Table.Cell>
|
|
||||||
<Table.Cell align="center">{hits.toFixed(1)}</Table.Cell>
|
|
||||||
<Table.Cell align="center">{damageRatio.toFixed(2)}</Table.Cell>
|
|
||||||
<Table.Cell align="center">
|
|
||||||
{Math.round(damageReceived).toLocaleString(locale)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(prev, next) => prev.tanks === next.tanks
|
|
||||||
);
|
|
||||||
@@ -37,26 +37,26 @@ export function PlaylistActions() {
|
|||||||
{strings.website.tools.playlist.shuffle}
|
{strings.website.tools.playlist.shuffle}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size={{ initial: "1", xs: "2" }}
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
Playlist.mutate((draft) => {
|
||||||
|
draft.list = draft.list?.filter(
|
||||||
|
(entry) =>
|
||||||
|
(!entry.now ||
|
||||||
|
!entry.then ||
|
||||||
|
entry.now.battles === entry.then.battles) &&
|
||||||
|
!entry.checked
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strings.website.tools.playlist.remove}
|
||||||
|
</Button>
|
||||||
|
|
||||||
{wargaming && (
|
{wargaming && (
|
||||||
<>
|
<>
|
||||||
<Button
|
|
||||||
size={{ initial: "1", xs: "2" }}
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
Playlist.mutate((draft) => {
|
|
||||||
draft.list = draft.list?.filter(
|
|
||||||
(entry) =>
|
|
||||||
(!entry.now ||
|
|
||||||
!entry.then ||
|
|
||||||
entry.now.battles === entry.then.battles) &&
|
|
||||||
!entry.checked
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.playlist.remove}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<Button size={{ initial: "1", xs: "2" }}>
|
<Button size={{ initial: "1", xs: "2" }}>
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import {
|
|||||||
TankType,
|
TankType,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import {
|
import { LockClosedIcon, LockOpen2Icon } from "@radix-ui/react-icons";
|
||||||
ComponentBooleanIcon,
|
|
||||||
LockClosedIcon,
|
|
||||||
LockOpen2Icon,
|
|
||||||
} from "@radix-ui/react-icons";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -71,7 +67,9 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
|||||||
<img
|
<img
|
||||||
style={{ width: "1em", height: "1em" }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
src={asset(
|
src={asset(
|
||||||
`icons/shells/${shellTypeIcons[shells[index]]}${premium ? "_premium" : ""}.webp`
|
`icons/shells/${shellTypeIcons[shells[index]]}${
|
||||||
|
premium ? "_premium" : ""
|
||||||
|
}.webp`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -107,7 +105,9 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={asset(
|
src={asset(
|
||||||
`icons/shells/${shellTypeIcons[shellType]}${premium ? "_premium" : ""}.webp`
|
`icons/shells/${shellTypeIcons[shellType]}${
|
||||||
|
premium ? "_premium" : ""
|
||||||
|
}.webp`
|
||||||
)}
|
)}
|
||||||
style={{ width: "1em", height: "1em" }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
/>
|
/>
|
||||||
@@ -135,29 +135,13 @@ function OwnershipFilter(props: FlexProps) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size={size}
|
size={size}
|
||||||
disabled={!wargaming}
|
disabled={!wargaming}
|
||||||
variant={tankFilters.ownership === "all" ? "solid" : "soft"}
|
variant={tankFilters.showOwned ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.ownership === "all" ? undefined : "gray"}
|
color={tankFilters.showOwned ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.ownership = "all";
|
draft.showOwned = !draft.showOwned;
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ComponentBooleanIcon />
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
size={size}
|
|
||||||
disabled={!wargaming}
|
|
||||||
variant={tankFilters.ownership === "owned" ? "solid" : "soft"}
|
|
||||||
radius="none"
|
|
||||||
color={tankFilters.ownership === "owned" ? undefined : "gray"}
|
|
||||||
highContrast
|
|
||||||
onClick={() => {
|
|
||||||
TankFilters.mutate((draft) => {
|
|
||||||
draft.ownership = "owned";
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -167,13 +151,13 @@ function OwnershipFilter(props: FlexProps) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size={size}
|
size={size}
|
||||||
disabled={!wargaming}
|
disabled={!wargaming}
|
||||||
variant={tankFilters.ownership === "unowned" ? "solid" : "soft"}
|
variant={tankFilters.showUnowned ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.ownership === "unowned" ? undefined : "gray"}
|
color={tankFilters.showUnowned ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.ownership = "unowned";
|
draft.showUnowned = !draft.showUnowned;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -579,27 +563,13 @@ export function FilterControl() {
|
|||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size={size}
|
size={size}
|
||||||
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
variant={tankFilters.showNonTesting ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === "include" ? undefined : "gray"}
|
color={tankFilters.showNonTesting ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.testing = "include";
|
draft.showNonTesting = !draft.showNonTesting;
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ComponentBooleanIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
size={size}
|
|
||||||
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
|
||||||
radius="none"
|
|
||||||
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
|
||||||
highContrast
|
|
||||||
onClick={() => {
|
|
||||||
TankFilters.mutate((draft) => {
|
|
||||||
draft.testing = "exclude";
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -607,15 +577,16 @@ export function FilterControl() {
|
|||||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size={size}
|
size={size}
|
||||||
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
variant={tankFilters.showTesting ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === "only" ? undefined : "gray"}
|
color={tankFilters.showTesting ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.testing = "only";
|
draft.showTesting = !draft.showTesting;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -487,7 +487,7 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{tankFilters.testing === "only" && (
|
{tankFilters.showTesting && !tankFilters.showNonTesting && (
|
||||||
<Flex justify="center" mt="4">
|
<Flex justify="center" mt="4">
|
||||||
<Callout.Root color="amber">
|
<Callout.Root color="amber">
|
||||||
<Callout.Icon>
|
<Callout.Icon>
|
||||||
|
|||||||
+24
-6
@@ -12,6 +12,7 @@ import {
|
|||||||
EyeOpenIcon,
|
EyeOpenIcon,
|
||||||
ShadowInnerIcon,
|
ShadowInnerIcon,
|
||||||
ShadowNoneIcon,
|
ShadowNoneIcon,
|
||||||
|
SunIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -55,7 +56,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
const hasCustomShell = Tankopedia.use(
|
const hasCustomShell = Tankopedia.use(
|
||||||
(state) => state.customShell !== undefined
|
(state) => state.customShell !== undefined
|
||||||
);
|
);
|
||||||
const display = Tankopedia.use((state) => state.display);
|
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||||
const isFullScreen = useFullScreen();
|
const isFullScreen = useFullScreen();
|
||||||
const advancedHighlighting = TankopediaPersistent.use(
|
const advancedHighlighting = TankopediaPersistent.use(
|
||||||
(state) => state.advancedHighlighting
|
(state) => state.advancedHighlighting
|
||||||
@@ -73,6 +74,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
|
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -84,7 +86,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
gap="2"
|
gap="2"
|
||||||
direction="column"
|
direction="column"
|
||||||
top="50%"
|
top="50%"
|
||||||
right={display === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"}
|
right={
|
||||||
|
requestedDisplay === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
transform: "translateY(-50%)",
|
transform: "translateY(-50%)",
|
||||||
@@ -296,7 +300,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
>
|
>
|
||||||
{disturbed && (
|
{disturbed && (
|
||||||
<>
|
<>
|
||||||
{display === TankopediaDisplay.DynamicArmor && (
|
{requestedDisplay === TankopediaDisplay.DynamicArmor && (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="2"
|
size="2"
|
||||||
highContrast
|
highContrast
|
||||||
@@ -349,6 +353,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
size="2"
|
||||||
|
highContrast
|
||||||
|
onClick={() => {
|
||||||
|
TankopediaPersistent.mutate((draft) => {
|
||||||
|
draft.highGraphics = !draft.highGraphics;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
color={highGraphics ? undefined : "gray"}
|
||||||
|
variant={highGraphics ? "solid" : "surface"}
|
||||||
|
>
|
||||||
|
<SunIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
<ScreenshotButton
|
<ScreenshotButton
|
||||||
color="gray"
|
color="gray"
|
||||||
size="2"
|
size="2"
|
||||||
@@ -384,11 +402,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Flex gap="3" align="center" mt="2">
|
<Flex gap="3" align="center" mt="2">
|
||||||
<SegmentedControl.Root
|
<SegmentedControl.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
value={`${disturbed ? display : -1}`}
|
value={`${disturbed ? requestedDisplay : -1}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.disturbed = true;
|
draft.disturbed = true;
|
||||||
draft.display = Number(value);
|
draft.requestedDisplay = Number(value);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -430,7 +448,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
</SegmentedControl.Root>
|
</SegmentedControl.Root>
|
||||||
|
|
||||||
{display === TankopediaDisplay.DynamicArmor && (
|
{requestedDisplay === TankopediaDisplay.DynamicArmor && (
|
||||||
<Dialog.Root
|
<Dialog.Root
|
||||||
open={antagonistSelectorOpen}
|
open={antagonistSelectorOpen}
|
||||||
onOpenChange={setAntagonistSelectorOpen}
|
onOpenChange={setAntagonistSelectorOpen}
|
||||||
|
|||||||
+3
-13
@@ -22,14 +22,12 @@ const poseDistances: Record<Pose, number> = {
|
|||||||
const modelDefinitions = await awaitableModelDefinitions;
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
naked?: boolean;
|
|
||||||
autoRotate?: boolean;
|
autoRotate?: boolean;
|
||||||
zoomable?: boolean;
|
zoomable?: boolean;
|
||||||
enableRotate?: boolean;
|
enableRotate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Controls({
|
export function Controls({
|
||||||
naked,
|
|
||||||
autoRotate = true,
|
autoRotate = true,
|
||||||
zoomable = true,
|
zoomable = true,
|
||||||
enableRotate = true,
|
enableRotate = true,
|
||||||
@@ -79,11 +77,7 @@ export function Controls({
|
|||||||
protagonistHullOrigin.y +
|
protagonistHullOrigin.y +
|
||||||
protagonistTurretOrigin.y +
|
protagonistTurretOrigin.y +
|
||||||
protagonistGunOrigin.y;
|
protagonistGunOrigin.y;
|
||||||
const inspectModeInitialPosition = new Vector3(
|
const inspectModeInitialPosition = new Vector3(-8, 2, -13);
|
||||||
-8,
|
|
||||||
naked ? gunHeight + 10 : 2,
|
|
||||||
-13
|
|
||||||
);
|
|
||||||
|
|
||||||
const t0 = useRef(Date.now() / 1000);
|
const t0 = useRef(Date.now() / 1000);
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
@@ -236,14 +230,10 @@ export function Controls({
|
|||||||
orbitControls.current.rotateSpeed = 0.25;
|
orbitControls.current.rotateSpeed = 0.25;
|
||||||
|
|
||||||
camera.position.copy(inspectModeInitialPosition);
|
camera.position.copy(inspectModeInitialPosition);
|
||||||
orbitControls.current.target.set(
|
orbitControls.current.target.set(0, gunHeight * 0.6, 0);
|
||||||
0,
|
|
||||||
gunHeight * (naked ? 1 / 4 : 3 / 4),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
orbitControls.current.enablePan = true;
|
orbitControls.current.enablePan = true;
|
||||||
orbitControls.current.enableZoom = true;
|
orbitControls.current.enableZoom = true;
|
||||||
camera.fov = naked ? 20 : 25;
|
camera.fov = 25;
|
||||||
|
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
}
|
}
|
||||||
|
|||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
import { invalidate, useThree } from "@react-three/fiber";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import type { Fog } from "three";
|
|
||||||
import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from "..";
|
|
||||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
|
||||||
|
|
||||||
export function InitialFogReveal() {
|
|
||||||
const fog = useThree((state) => state.scene.fog as Fog | null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.revealed = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fog) return;
|
|
||||||
|
|
||||||
const t0 = Date.now();
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
const t = (Date.now() - t0) / 1000;
|
|
||||||
const x = t / fogAnimationTime;
|
|
||||||
const near = forNear1 + (forNear0 - forNear1) * x;
|
|
||||||
const far = fogFar1 + (fogFar0 - fogFar1) * x;
|
|
||||||
|
|
||||||
fog.near = near;
|
|
||||||
fog.far = far;
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
if (x >= 1) {
|
|
||||||
fog.near = forNear0;
|
|
||||||
fog.far = fogFar0;
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, 1000 / 60);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
+77
-41
@@ -1,82 +1,118 @@
|
|||||||
import { useHelper } from "@react-three/drei";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { useFrame } from "@react-three/fiber";
|
|
||||||
import { times } from "lodash-es";
|
import { times } from "lodash-es";
|
||||||
import { useRef, useState } from "react";
|
import { Quicklime } from "quicklime";
|
||||||
import { Color, SpotLight, SpotLightHelper } from "three";
|
import { useEffect, useRef } from "react";
|
||||||
import { degToRad } from "three/src/math/MathUtils.js";
|
import { HemisphereLight, SpotLight, type Group } from "three";
|
||||||
import { isHalloween } from "../../../../../../core/blitzkit/isHalloween";
|
import { clamp, degToRad, lerp } from "three/src/math/MathUtils.js";
|
||||||
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
|
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
|
||||||
import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
|
import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
|
||||||
|
|
||||||
|
const ANGLE = Math.PI * 2 ** -2;
|
||||||
|
const REVEAL_ANIMATION_TIME = 3;
|
||||||
|
const TRANSITION_ANIMATION_TIME = 0.5;
|
||||||
|
|
||||||
const LIGHTS_COUNT = 5;
|
const LIGHTS_COUNT = 5;
|
||||||
|
const THETA_OFFSET = degToRad(-152);
|
||||||
const LIGHT_DISTANCE = 13;
|
const LIGHT_DISTANCE = 13;
|
||||||
const LIGHT_HEIGHT_0 = 4;
|
const LIGHT_HEIGHT_0 = 4;
|
||||||
const LIGHT_HEIGHT_1 = 2;
|
const LIGHT_HEIGHT_1 = 2;
|
||||||
const INTENSITY = 50;
|
const INTENSITY_0 = 3.5;
|
||||||
|
const INTENSITY_1 = 1.5;
|
||||||
|
const HEMISPHERE_INTENSITY = 2;
|
||||||
|
|
||||||
const N = 128;
|
export const transitionEvent = new Quicklime<number>(0);
|
||||||
const TARGET_FRAME_RATE = 30;
|
|
||||||
|
|
||||||
export function Lighting() {
|
export function Lighting() {
|
||||||
const [cast, setCast] = useState(true);
|
const wrapper = useRef<Group>(null!);
|
||||||
|
|
||||||
const t0 = useRef(0);
|
const t0 = useRef(0);
|
||||||
const topLight = useRef<SpotLight>(null!);
|
const transition = useRef(false);
|
||||||
const frontLight = useRef<SpotLight>(null!);
|
const clock = useThree((state) => state.clock);
|
||||||
|
const display = Tankopedia.use((state) => state.display);
|
||||||
|
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||||
|
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
||||||
|
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||||
|
|
||||||
useHelper(topLight, SpotLightHelper, "red");
|
useEffect(() => {
|
||||||
useHelper(frontLight, SpotLightHelper, "green");
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.revealed = true;
|
||||||
|
});
|
||||||
|
|
||||||
const last = useRef<number>(0);
|
t0.current = clock.elapsedTime - REVEAL_ANIMATION_TIME;
|
||||||
const samples = useRef<number[]>([]);
|
}, []);
|
||||||
|
|
||||||
useFrame(({ clock, frameloop }) => {
|
useFrame(({ invalidate }) => {
|
||||||
if (frameloop === "never") {
|
if (transition.current) {
|
||||||
|
transition.current = false;
|
||||||
t0.current = clock.elapsedTime;
|
t0.current = clock.elapsedTime;
|
||||||
samples.current = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!cast || clock.elapsedTime - t0.current < TARGET_FRAME_RATE * N) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dt = clock.elapsedTime - last.current;
|
const x = clamp(
|
||||||
last.current = clock.elapsedTime;
|
(clock.elapsedTime - t0.current) / animationTime.current,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
|
||||||
|
|
||||||
const fps = 1 / dt;
|
if (t !== transitionEvent.last) {
|
||||||
samples.current.push(fps);
|
transitionEvent.dispatch(t);
|
||||||
|
}
|
||||||
|
|
||||||
if (samples.current.length > N) samples.current.shift();
|
for (const child of wrapper.current.children) {
|
||||||
|
if (child instanceof SpotLight) {
|
||||||
|
child.angle = ANGLE * t;
|
||||||
|
} else if (child instanceof HemisphereLight) {
|
||||||
|
child.intensity = HEMISPHERE_INTENSITY * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const avg =
|
if (x < 2) invalidate();
|
||||||
samples.current.reduce((a, b) => a + b, 0) / samples.current.length;
|
|
||||||
|
|
||||||
if (avg < TARGET_FRAME_RATE) setCast(false);
|
if (
|
||||||
|
x > 1 &&
|
||||||
|
Tankopedia.state.display !== Tankopedia.state.requestedDisplay
|
||||||
|
) {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.display = requestedDisplay;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestedDisplay === display) return;
|
||||||
|
|
||||||
|
transition.current = true;
|
||||||
|
animationTime.current = TRANSITION_ANIMATION_TIME;
|
||||||
|
}, [requestedDisplay]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<group ref={wrapper}>
|
||||||
|
<hemisphereLight intensity={0} color="#ffffff" groundColor="#afafaf" />
|
||||||
|
|
||||||
{times(LIGHTS_COUNT, (index) => {
|
{times(LIGHTS_COUNT, (index) => {
|
||||||
const x = index / (LIGHTS_COUNT - 1);
|
const x = index / (LIGHTS_COUNT - 1);
|
||||||
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) - degToRad(152);
|
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET;
|
||||||
const position = [
|
const position = [
|
||||||
LIGHT_DISTANCE * Math.sin(theta),
|
LIGHT_DISTANCE * Math.sin(theta),
|
||||||
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x,
|
lerp(LIGHT_HEIGHT_0, LIGHT_HEIGHT_1, x),
|
||||||
LIGHT_DISTANCE * Math.cos(theta),
|
LIGHT_DISTANCE * Math.cos(theta),
|
||||||
] as const;
|
] as const;
|
||||||
const color = new Color();
|
const intensity = lerp(INTENSITY_0, INTENSITY_1, x);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HelpingSpotLight
|
<HelpingSpotLight
|
||||||
|
userData={{ index }}
|
||||||
key={index}
|
key={index}
|
||||||
position={position}
|
position={position}
|
||||||
intensity={INTENSITY}
|
intensity={intensity}
|
||||||
penumbra={1}
|
penumbra={1}
|
||||||
castShadow={cast}
|
castShadow={highGraphics}
|
||||||
decay={1}
|
decay={0}
|
||||||
color={color}
|
color="#ffffff"
|
||||||
|
angle={0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+3
-2
@@ -4,7 +4,7 @@ import { Quicklime, type QuicklimeEvent } from "quicklime";
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { Mesh, MeshStandardMaterial } from "three";
|
import { Mesh, MeshStandardMaterial } from "three";
|
||||||
|
|
||||||
const SIZE = 2 ** 5;
|
const SIZE = 2 ** 6;
|
||||||
|
|
||||||
export const screenshotReadyEvent = new Quicklime(false);
|
export const screenshotReadyEvent = new Quicklime(false);
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ export function SceneProps() {
|
|||||||
receiveShadow
|
receiveShadow
|
||||||
visible={!screenshotReadyEvent.last!}
|
visible={!screenshotReadyEvent.last!}
|
||||||
ref={mesh}
|
ref={mesh}
|
||||||
|
renderOrder={-1}
|
||||||
>
|
>
|
||||||
<planeGeometry args={[SIZE, SIZE]} />
|
<planeGeometry args={[SIZE, SIZE]} />
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial
|
||||||
@@ -43,7 +44,7 @@ export function SceneProps() {
|
|||||||
ref={material}
|
ref={material}
|
||||||
color="black"
|
color="black"
|
||||||
roughness={1}
|
roughness={1}
|
||||||
metalness={1}
|
metalness={0}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-1
@@ -106,7 +106,9 @@ export function TankModel() {
|
|||||||
const isTurret = node.name.startsWith("turret_");
|
const isTurret = node.name.startsWith("turret_");
|
||||||
const isCurrentTurret =
|
const isCurrentTurret =
|
||||||
node.name ===
|
node.name ===
|
||||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`;
|
`turret_${turretModelDefinition.model_id
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
const isVisible = isCurrentTurret;
|
const isVisible = isCurrentTurret;
|
||||||
const position = new Vector2();
|
const position = new Vector2();
|
||||||
const delta = new Vector2();
|
const delta = new Vector2();
|
||||||
|
|||||||
+12
-53
@@ -1,12 +1,9 @@
|
|||||||
import { BLITZKIT_TANK_ICON_SIZE } from "@blitzkit/core";
|
|
||||||
import { invalidate } from "@react-three/fiber";
|
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
Suspense,
|
Suspense,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Fog } from "three";
|
import { Fog } from "three";
|
||||||
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
|
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
|
||||||
@@ -29,26 +26,16 @@ import { SmartCanvas } from "../../../../SmartCanvas";
|
|||||||
import { AutoClear } from "./components/AutoClear";
|
import { AutoClear } from "./components/AutoClear";
|
||||||
import { Controls } from "./components/Control";
|
import { Controls } from "./components/Control";
|
||||||
import { InitialAligner } from "./components/InitialAligner";
|
import { InitialAligner } from "./components/InitialAligner";
|
||||||
import { InitialFogReveal } from "./components/InitialFogReveal";
|
|
||||||
import { Lighting } from "./components/Lighting";
|
import { Lighting } from "./components/Lighting";
|
||||||
import { SceneProps } from "./components/SceneProps";
|
import { SceneProps } from "./components/SceneProps";
|
||||||
import { TankModel } from "./components/TankModel";
|
import { TankModel } from "./components/TankModel";
|
||||||
import { TransitionSkeleton } from "./components/TransitionSkeleton";
|
|
||||||
|
|
||||||
interface TankSandboxProps {
|
interface TankSandboxProps {
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
naked?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const forNear0 = 30;
|
|
||||||
export const fogFar0 = 50;
|
|
||||||
export const forNear1 = 0;
|
|
||||||
export const fogFar1 = 0;
|
|
||||||
export const fogAnimationTime = 2.5;
|
|
||||||
|
|
||||||
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||||
({ thicknessRange, naked }, ref) => {
|
({ thicknessRange }, ref) => {
|
||||||
const fog = useRef(new Fog("black", forNear1, fogFar1));
|
|
||||||
const canvas = useRef<HTMLCanvasElement>(null);
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||||
@@ -58,40 +45,11 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
const turretModelDefinition =
|
const turretModelDefinition =
|
||||||
tankModelDefinition.turrets[protagonistTurret.id];
|
tankModelDefinition.turrets[protagonistTurret.id];
|
||||||
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
|
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
|
||||||
const rawDisplay = Tankopedia.use((state) => state.display);
|
const display = Tankopedia.use((state) => state.display);
|
||||||
const [display, setDisplay] = useState(rawDisplay);
|
|
||||||
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
||||||
(state) => state.hideTankModelUnderArmor
|
(state) => state.hideTankModelUnderArmor
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (rawDisplay === display) return;
|
|
||||||
|
|
||||||
const t0 = Date.now();
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
const t = (Date.now() - t0) / 1000;
|
|
||||||
const x = t / fogAnimationTime;
|
|
||||||
const y = Math.sin(Math.PI * x);
|
|
||||||
|
|
||||||
const near = forNear0 + (forNear1 - forNear0) * y;
|
|
||||||
const far = fogFar0 + (fogFar1 - fogFar0) * y;
|
|
||||||
|
|
||||||
fog.current.near = near;
|
|
||||||
fog.current.far = far;
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
if (rawDisplay !== display && x >= 0.5) setDisplay(rawDisplay);
|
|
||||||
if (x >= 1) {
|
|
||||||
fog.current.near = forNear0;
|
|
||||||
fog.current.far = fogFar0;
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, 1000 / 60);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [rawDisplay]);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => canvas.current!, []);
|
useImperativeHandle(ref, () => canvas.current!, []);
|
||||||
|
|
||||||
function handlePointerDown() {
|
function handlePointerDown() {
|
||||||
@@ -188,10 +146,12 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
}
|
}
|
||||||
}, [display]);
|
}, [display]);
|
||||||
|
|
||||||
|
console.log(TankopediaDisplay[display]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SmartCanvas
|
<SmartCanvas
|
||||||
ref={canvas}
|
ref={canvas}
|
||||||
scene={{ fog: naked ? undefined : fog.current }}
|
scene={{ fog: new Fog("black", 30, 50) }}
|
||||||
gl={{
|
gl={{
|
||||||
clippingPlanes: [],
|
clippingPlanes: [],
|
||||||
localClippingEnabled: true,
|
localClippingEnabled: true,
|
||||||
@@ -207,30 +167,29 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
width: naked ? BLITZKIT_TANK_ICON_SIZE.width : "100%",
|
width: "100%",
|
||||||
height: naked ? BLITZKIT_TANK_ICON_SIZE.height : "100%",
|
height: "100%",
|
||||||
outline: naked ? "1rem red solid" : undefined,
|
outline: undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!naked && <SceneProps />}
|
<Lighting />
|
||||||
|
|
||||||
|
<SceneProps />
|
||||||
{(display === TankopediaDisplay.Model ||
|
{(display === TankopediaDisplay.Model ||
|
||||||
(display === TankopediaDisplay.DynamicArmor &&
|
(display === TankopediaDisplay.DynamicArmor &&
|
||||||
!hideTankModelUnderArmor)) && <TankModel />}
|
!hideTankModelUnderArmor)) && <TankModel />}
|
||||||
<TransitionSkeleton />
|
|
||||||
<ShotDisplay />
|
<ShotDisplay />
|
||||||
<ArmorPlateDisplay />
|
<ArmorPlateDisplay />
|
||||||
<AutoClear />
|
<AutoClear />
|
||||||
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
|
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
|
||||||
<Controls naked={naked} />
|
<Controls />
|
||||||
<InitialFogReveal />
|
|
||||||
|
|
||||||
{display === TankopediaDisplay.DynamicArmor && <Armor />}
|
{display === TankopediaDisplay.DynamicArmor && <Armor />}
|
||||||
{display === TankopediaDisplay.StaticArmor && (
|
{display === TankopediaDisplay.StaticArmor && (
|
||||||
<StaticArmor thicknessRange={thicknessRange} />
|
<StaticArmor thicknessRange={thicknessRange} />
|
||||||
)}
|
)}
|
||||||
<Lighting />
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<InitialAligner />
|
<InitialAligner />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, Flex } from "@radix-ui/themes";
|
import { Box, Flex } from "@radix-ui/themes";
|
||||||
import { times } from "lodash-es";
|
import { times } from "lodash-es";
|
||||||
import { Suspense, useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||||
import { useFullScreen } from "../../../hooks/useFullScreen";
|
import { useFullScreen } from "../../../hooks/useFullScreen";
|
||||||
import { Duel } from "../../../stores/duel";
|
import { Duel } from "../../../stores/duel";
|
||||||
@@ -99,11 +99,11 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
isFullScreen
|
isFullScreen
|
||||||
? "100vh"
|
? "100vh"
|
||||||
: disturbed
|
: disturbed
|
||||||
? "calc(100svh - 8rem)"
|
? "calc(100svh - 8rem)"
|
||||||
: {
|
: {
|
||||||
initial: "28rem",
|
initial: "28rem",
|
||||||
md: "calc(100svh - 14rem)",
|
md: "calc(100svh - 14rem)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maxWidth={isFullScreen ? undefined : "120rem"}
|
maxWidth={isFullScreen ? undefined : "120rem"}
|
||||||
flexGrow="1"
|
flexGrow="1"
|
||||||
@@ -120,30 +120,17 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
position="relative"
|
position="relative"
|
||||||
>
|
>
|
||||||
<Box position="absolute" width="100%" height="100%" overflow="hidden">
|
<Box position="absolute" width="100%" height="100%" overflow="hidden">
|
||||||
<Box
|
<Box width="100%" height="100%" position="relative">
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
position="relative"
|
|
||||||
style={{
|
|
||||||
transitionDuration: "1s",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
position="absolute"
|
||||||
width: "100%",
|
width="100%"
|
||||||
height: "100%",
|
height="100%"
|
||||||
transitionDuration: "2s",
|
top="0"
|
||||||
opacity: revealed ? 1 : 0,
|
left="0"
|
||||||
filter: disturbed
|
|
||||||
? undefined
|
|
||||||
: `drop-shadow(0 1rem 1rem black)`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Suspense>
|
{!skeleton && (
|
||||||
{!skeleton && (
|
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
)}
|
||||||
)}
|
|
||||||
</Suspense>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,14 @@ export async function filterTank(
|
|||||||
owned: number[] = []
|
owned: number[] = []
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
(filters.ownership === "all" ||
|
((filters.showOwned && owned.includes(tank.id)) ||
|
||||||
(filters.ownership === "owned" && owned.includes(tank.id)) ||
|
(filters.showUnowned && !owned.includes(tank.id))) &&
|
||||||
(filters.ownership === "unowned" && !owned.includes(tank.id))) &&
|
|
||||||
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
|
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
|
||||||
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
|
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
|
||||||
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
|
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
|
||||||
(filters.types.length === 0 || filters.types.includes(tank.type)) &&
|
(filters.types.length === 0 || filters.types.includes(tank.type)) &&
|
||||||
(filters.testing === "include" ||
|
((filters.showTesting && tank.testing) ||
|
||||||
(filters.testing === "only" && tank.testing) ||
|
(filters.showNonTesting && !tank.testing)) &&
|
||||||
(filters.testing === "exclude" && !tank.testing)) &&
|
|
||||||
(filters.gunType.length === 0 ||
|
(filters.gunType.length === 0 ||
|
||||||
(filters.gunType.includes("regular") &&
|
(filters.gunType.includes("regular") &&
|
||||||
tank.turrets.some((turret) =>
|
tank.turrets.some((turret) =>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
---
|
---
|
||||||
|
import ToolOpenGraph from "../../../components/ToolOpenGraph.astro";
|
||||||
|
import { tools } from "../../../constants/tools";
|
||||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||||
import { Page } from "./_index";
|
import { Page } from "./_index";
|
||||||
|
|
||||||
@@ -7,6 +9,8 @@ export { getStaticPaths } from "../index.astro";
|
|||||||
const { locale } = Astro.props;
|
const { locale } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<ToolOpenGraph {locale} tool={tools.guess} />
|
||||||
|
|
||||||
<BaseLayout {locale} noFooter>
|
<BaseLayout {locale} noFooter>
|
||||||
<Page {locale} client:only="react">
|
<Page {locale} client:only="react">
|
||||||
<Page {locale} skeleton slot="fallback" />
|
<Page {locale} skeleton slot="fallback" />
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
TankSearch,
|
TankSearch,
|
||||||
type TankSearchProps,
|
type TankSearchProps,
|
||||||
} from "../../../components/TankSearch";
|
} from "../../../components/TankSearch";
|
||||||
|
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||||
import { curateMixer } from "../../../core/blitzkit/curateMixer";
|
import { curateMixer } from "../../../core/blitzkit/curateMixer";
|
||||||
import {
|
import {
|
||||||
LocaleProvider,
|
LocaleProvider,
|
||||||
@@ -24,8 +25,13 @@ import {
|
|||||||
type LocaleAcceptorProps,
|
type LocaleAcceptorProps,
|
||||||
} from "../../../hooks/useLocale";
|
} from "../../../hooks/useLocale";
|
||||||
import { Mixer } from "../../../stores/mixer";
|
import { Mixer } from "../../../stores/mixer";
|
||||||
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||||
|
|
||||||
|
const modelDefinition = await awaitableModelDefinitions.then(
|
||||||
|
({ models }) => models[1]
|
||||||
|
);
|
||||||
|
|
||||||
export function Page({
|
export function Page({
|
||||||
locale,
|
locale,
|
||||||
skeleton,
|
skeleton,
|
||||||
@@ -33,6 +39,7 @@ export function Page({
|
|||||||
const initial = useRef(curateMixer());
|
const initial = useRef(curateMixer());
|
||||||
|
|
||||||
Mixer.useInitialization(initial.current);
|
Mixer.useInitialization(initial.current);
|
||||||
|
Tankopedia.useInitialization(modelDefinition);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="gray" p="0" maxWidth="unset">
|
<PageWrapper color="gray" p="0" maxWidth="unset">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PageWrapper } from "../../../components/PageWrapper";
|
import { PageWrapper } from "../../../components/PageWrapper";
|
||||||
import { PerformanceInfo } from "../../../components/Performance/PerformanceInfo";
|
|
||||||
import { PlayerCountControl } from "../../../components/Performance/PlayerCountControl";
|
import { PlayerCountControl } from "../../../components/Performance/PlayerCountControl";
|
||||||
import { TankTable } from "../../../components/Performance/Table";
|
import { TankTable } from "../../../components/Performance/Table";
|
||||||
import { FilterControl } from "../../../components/TankSearch/components/FilterControl";
|
import { FilterControl } from "../../../components/TankSearch/components/FilterControl";
|
||||||
@@ -21,8 +20,7 @@ export function Page({ skeleton, locale }: PageProps) {
|
|||||||
|
|
||||||
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="jade" maxWidth="100%">
|
<PageWrapper pb="0" px="0" color="jade" maxWidth="100%">
|
||||||
<PerformanceInfo skeleton={skeleton} />
|
|
||||||
<FilterControl />
|
<FilterControl />
|
||||||
<PlayerCountControl />
|
<PlayerCountControl />
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ export function Page({
|
|||||||
}: Props & MaybeSkeletonComponentProps) {
|
}: Props & MaybeSkeletonComponentProps) {
|
||||||
const generated = Playlist.use((state) => state.list !== undefined);
|
const generated = Playlist.use((state) => state.list !== undefined);
|
||||||
|
|
||||||
if (App.state.logins.wargaming && TankFilters.state.ownership !== "owned") {
|
if (App.state.logins.wargaming) TankFilters.state.showUnowned = false;
|
||||||
TankFilters.state.ownership = "owned";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Samples } from '@blitzkit/core';
|
import type { Samples } from "@blitzkit/core";
|
||||||
import { Varuna } from 'varuna';
|
import { Varuna } from "varuna";
|
||||||
|
|
||||||
export interface Performance {
|
export interface Performance {
|
||||||
playerCountPeriod: PlayerCountPeriod;
|
playerCountPeriod: PlayerCountPeriod;
|
||||||
@@ -8,5 +8,5 @@ export interface Performance {
|
|||||||
export type PlayerCountPeriod = keyof Samples;
|
export type PlayerCountPeriod = keyof Samples;
|
||||||
|
|
||||||
export const Performance = new Varuna<Performance>({
|
export const Performance = new Varuna<Performance>({
|
||||||
playerCountPeriod: 'd_30',
|
playerCountPeriod: "total",
|
||||||
});
|
});
|
||||||
@@ -17,12 +17,15 @@ export interface TankFilters {
|
|||||||
nations: string[];
|
nations: string[];
|
||||||
classes: TankClass[];
|
classes: TankClass[];
|
||||||
types: TankType[];
|
types: TankType[];
|
||||||
testing: "include" | "exclude" | "only";
|
|
||||||
search: string | null;
|
search: string | null;
|
||||||
searching: boolean;
|
searching: boolean;
|
||||||
gunType: CaseType<GunDefinition>[];
|
gunType: CaseType<GunDefinition>[];
|
||||||
shells: [ShellType | null, ShellType | null, ShellType | null];
|
shells: [ShellType | null, ShellType | null, ShellType | null];
|
||||||
ownership: "all" | "owned" | "unowned";
|
|
||||||
|
showTesting: boolean;
|
||||||
|
showNonTesting: boolean;
|
||||||
|
showOwned: boolean;
|
||||||
|
showUnowned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TankFilters = new Varuna<TankFilters>({
|
export const TankFilters = new Varuna<TankFilters>({
|
||||||
@@ -30,10 +33,13 @@ export const TankFilters = new Varuna<TankFilters>({
|
|||||||
nations: [],
|
nations: [],
|
||||||
classes: [],
|
classes: [],
|
||||||
types: [],
|
types: [],
|
||||||
testing: "include",
|
|
||||||
search: null,
|
search: null,
|
||||||
searching: false,
|
searching: false,
|
||||||
gunType: [],
|
gunType: [],
|
||||||
shells: [null, null, null],
|
shells: [null, null, null],
|
||||||
ownership: "all",
|
|
||||||
|
showTesting: true,
|
||||||
|
showNonTesting: true,
|
||||||
|
showOwned: true,
|
||||||
|
showUnowned: true,
|
||||||
});
|
});
|
||||||
@@ -100,6 +100,7 @@ interface Tankopedia {
|
|||||||
);
|
);
|
||||||
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
||||||
customShell?: ShellDefinition;
|
customShell?: ShellDefinition;
|
||||||
|
requestedDisplay: TankopediaDisplay;
|
||||||
display: TankopediaDisplay;
|
display: TankopediaDisplay;
|
||||||
statSearch?: string;
|
statSearch?: string;
|
||||||
}
|
}
|
||||||
@@ -114,5 +115,6 @@ export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
|||||||
skills: createDefaultSkills(skillDefinitions),
|
skills: createDefaultSkills(skillDefinitions),
|
||||||
model,
|
model,
|
||||||
xpMultiplier: 1,
|
xpMultiplier: 1,
|
||||||
|
requestedDisplay: TankopediaDisplay.Model,
|
||||||
display: TankopediaDisplay.Model,
|
display: TankopediaDisplay.Model,
|
||||||
}));
|
}));
|
||||||
@@ -15,6 +15,7 @@ export interface TankopediaPersistent {
|
|||||||
showPrimaryArmor: boolean;
|
showPrimaryArmor: boolean;
|
||||||
recentlyViewed: number[];
|
recentlyViewed: number[];
|
||||||
hideTankModelUnderArmor: boolean;
|
hideTankModelUnderArmor: boolean;
|
||||||
|
highGraphics: boolean;
|
||||||
|
|
||||||
sort: {
|
sort: {
|
||||||
by: TankopediaSortBy;
|
by: TankopediaSortBy;
|
||||||
@@ -33,6 +34,7 @@ export const TankopediaPersistent = new Varuna<TankopediaPersistent>(
|
|||||||
showExternalModules: true,
|
showExternalModules: true,
|
||||||
showPrimaryArmor: true,
|
showPrimaryArmor: true,
|
||||||
recentlyViewed: [],
|
recentlyViewed: [],
|
||||||
|
highGraphics: true,
|
||||||
sort: {
|
sort: {
|
||||||
by: "meta.none",
|
by: "meta.none",
|
||||||
direction: "ascending",
|
direction: "ascending",
|
||||||
|
|||||||
Reference in new issue
Block a user