mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-08 02:10:37 +01:00
Compare commits
37
Commits
fe7b2aea02
...
v1.29.6
| 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 | ||
|
|
fced1e96ef | ||
|
|
778efb2372 | ||
|
|
b67fd5b58c | ||
|
|
2559782a28 | ||
|
|
efb479fece | ||
|
|
fb829d4b81 | ||
|
|
a692d1ee0b | ||
|
|
6c8fbf7245 | ||
|
|
2ea22512d4 | ||
|
|
697d1fa1b4 | ||
|
|
2c03b530d2 | ||
|
|
182471c4d6 | ||
|
|
aecc2adb07 | ||
|
|
f8ee2b64a6 | ||
|
|
7c4e28bd09 |
No files matched your search
@@ -69,7 +69,6 @@
|
||||
"@types/bun": "^1.3.1",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/lz4js": "^0.2.1",
|
||||
"@types/steam-user": "^5.1.0",
|
||||
"@types/three": "^0.180.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"crc": "^4.3.2",
|
||||
@@ -79,7 +78,6 @@
|
||||
"lz4js": "^0.2.0",
|
||||
"pickleparser": "^0.2.1",
|
||||
"protobufjs": "^7.5.4",
|
||||
"steam-user": "^5.2.3",
|
||||
"three": "^0.180.0",
|
||||
"ts-proto": "^2.7.7",
|
||||
"yaml": "^2.8.1",
|
||||
@@ -107,6 +105,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/lz4js": "^0.2.1",
|
||||
"@types/progress": "^2.0.7",
|
||||
"@types/steam-user": "^5.1.0",
|
||||
"@types/three": "^0.180.0",
|
||||
"crc": "^4.3.2",
|
||||
"csv-parse": "^6.1.0",
|
||||
@@ -120,6 +119,7 @@
|
||||
"progress": "^2.0.3",
|
||||
"puppeteer": "^24.22.3",
|
||||
"sharp": "^0.34.4",
|
||||
"steam-user": "^5.2.3",
|
||||
"three": "^0.180.0",
|
||||
"ts-proto": "^2.7.7",
|
||||
"yaml": "^2.8.1",
|
||||
@@ -2613,10 +2613,6 @@
|
||||
|
||||
"@blitzkit/bot/prisma": ["[email protected]", "", { "dependencies": { "@prisma/config": "6.17.1", "@prisma/engines": "6.17.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g=="],
|
||||
|
||||
"@blitzkit/scripts/@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
|
||||
|
||||
"@blitzkit/scripts/fast-xml-parser": ["[email protected]", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
||||
|
||||
"@blitzkit/website/vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="],
|
||||
|
||||
"@discordjs/rest/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
||||
@@ -2785,8 +2781,6 @@
|
||||
|
||||
"@blitzkit/bot/prisma/@prisma/engines": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "6.17.1", "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", "@prisma/fetch-engine": "6.17.1", "@prisma/get-platform": "6.17.1" } }, "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw=="],
|
||||
|
||||
"@blitzkit/scripts/@types/bun/bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
|
||||
|
||||
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
"@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
@@ -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",
|
||||
"version": "1.29.4",
|
||||
"version": "1.29.6",
|
||||
"license": "BlitzKit License",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -10,6 +10,7 @@
|
||||
"build:website": "cd packages/website && bun run build",
|
||||
"build:prisma": "prisma generate",
|
||||
"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:app": "cd packages/app && bun run build",
|
||||
"dev:bot": "bun run build:bot --dev && bun start:bot",
|
||||
|
||||
+1
-1
Submodule packages/closed updated: 33b7d22419...5b4a85d503.
@@ -13,7 +13,6 @@
|
||||
"@types/bun": "^1.3.1",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/lz4js": "^0.2.1",
|
||||
"@types/steam-user": "^5.1.0",
|
||||
"@types/three": "^0.180.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"crc": "^4.3.2",
|
||||
@@ -23,7 +22,6 @@
|
||||
"lz4js": "^0.2.0",
|
||||
"pickleparser": "^0.2.1",
|
||||
"protobufjs": "^7.5.4",
|
||||
"steam-user": "^5.2.3",
|
||||
"three": "^0.180.0",
|
||||
"ts-proto": "^2.7.7",
|
||||
"yaml": "^2.8.1"
|
||||
|
||||
@@ -6,6 +6,5 @@ export * from "./patreon";
|
||||
export * from "./protobuf";
|
||||
export * from "./protos";
|
||||
export * from "./statistics";
|
||||
export * from "./steam";
|
||||
export * from "./streams";
|
||||
export * from "./types";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./vfs";
|
||||
@@ -906,8 +906,8 @@
|
||||
},
|
||||
|
||||
"guess": {
|
||||
"name": "Guess the tank",
|
||||
"button": "Guess away",
|
||||
"name": "Guess",
|
||||
"button": "What could it be?",
|
||||
"description": "Silhouette mini-game",
|
||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||
"search": {
|
||||
|
||||
@@ -803,8 +803,8 @@
|
||||
"randomize": "Randomize"
|
||||
},
|
||||
"guess": {
|
||||
"name": "Guess the tank",
|
||||
"button": "Guess away",
|
||||
"name": "Guess",
|
||||
"button": "What could it be?",
|
||||
"description": "Silhouette mini-game",
|
||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||
"search": {
|
||||
|
||||
@@ -803,8 +803,8 @@
|
||||
"randomize": "Randomize"
|
||||
},
|
||||
"guess": {
|
||||
"name": "Guess the tank",
|
||||
"button": "Guess away",
|
||||
"name": "Guess",
|
||||
"button": "What could it be?",
|
||||
"description": "Silhouette mini-game",
|
||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||
"search": {
|
||||
|
||||
@@ -803,8 +803,8 @@
|
||||
"randomize": "Randomize"
|
||||
},
|
||||
"guess": {
|
||||
"name": "Guess the tank",
|
||||
"button": "Guess away",
|
||||
"name": "Guess",
|
||||
"button": "What could it be?",
|
||||
"description": "Silhouette mini-game",
|
||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||
"search": {
|
||||
|
||||
@@ -803,8 +803,8 @@
|
||||
"randomize": "Randomize"
|
||||
},
|
||||
"guess": {
|
||||
"name": "Guess the tank",
|
||||
"button": "Guess away",
|
||||
"name": "Guess",
|
||||
"button": "What could it be?",
|
||||
"description": "Silhouette mini-game",
|
||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||
"search": {
|
||||
|
||||
@@ -803,8 +803,8 @@
|
||||
"randomize": "Randomize"
|
||||
},
|
||||
"guess": {
|
||||
"name": "Guess the tank",
|
||||
"button": "Guess away",
|
||||
"name": "Guess",
|
||||
"button": "What could it be?",
|
||||
"description": "Silhouette mini-game",
|
||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||
"search": {
|
||||
|
||||
@@ -803,8 +803,8 @@
|
||||
"randomize": "Случайно"
|
||||
},
|
||||
"guess": {
|
||||
"name": "Угадай танк",
|
||||
"button": "Угадать",
|
||||
"name": "Угадать",
|
||||
"button": "Что это за танк?",
|
||||
"description": "Мини-игра «Силуэт»",
|
||||
"seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz",
|
||||
"search": {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"test": {
|
||||
"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": "Швидка бура лисиця перестрибує через лінивого собаку."
|
||||
},
|
||||
"common": {
|
||||
"name": "BlitzKit",
|
||||
"units": {
|
||||
"m": "{value}m",
|
||||
"mm": "{value}mm",
|
||||
"m": "{value}м",
|
||||
"mm": "{value}мм",
|
||||
"deg": "{value}°",
|
||||
"hp": "{value}hp",
|
||||
"hp": "{value}од",
|
||||
"x": "x{value}",
|
||||
"percentage": "{value}%",
|
||||
"deg_s": "{value}°/s"
|
||||
"deg_s": "{value}град/c"
|
||||
},
|
||||
"gun_types": {
|
||||
"auto_loader": "Магазинна система заряджання",
|
||||
@@ -80,7 +80,7 @@
|
||||
"nations": {
|
||||
"ussr": "СРСР",
|
||||
"germany": "Німеччина",
|
||||
"usa": "США",
|
||||
"usa": "ЗША",
|
||||
"china": "Китай",
|
||||
"uk": "Велика Британія",
|
||||
"france": "Франція",
|
||||
@@ -186,11 +186,11 @@
|
||||
"bot": {
|
||||
"common": {
|
||||
"errors": {
|
||||
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
|
||||
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
|
||||
"clan_not_found": "# Не вдалося знайти клан\nНе вдалося знайти \"{search}\". Спробуйте обрати клан з результату пошуку.",
|
||||
"player_not_found": "# Не вдалося знайти користувача\nНе вдалося знайти користувача \"{search}\". Спробуйте вибрати користувача із результатів пошуку, ввівши правильне ім'я користувача, або скористайтеся командою `/link`.",
|
||||
"player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!",
|
||||
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
|
||||
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
|
||||
"tank_name_not_found": "# Танк не знайдено\nНе вдалося знайти танк із назвою «{search}».",
|
||||
"tank_id_not_found": "# Танк не знайдено\nНе вдалося знайти танк з ID «{search}». Спробуйте ввести назву танка замість номера.",
|
||||
"no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.",
|
||||
"get_help": "Допомогти Discord серверу",
|
||||
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
|
||||
@@ -284,19 +284,19 @@
|
||||
"name": "рівень",
|
||||
"description": "Рівень",
|
||||
"choices": {
|
||||
"tier": "Tier {tier_1} - {tier_2}"
|
||||
"tier": "Рівень {tier_1} - {tier_2}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"periods": {
|
||||
"career": "Кар'єра",
|
||||
"today": "Сьогодні",
|
||||
"days": "{days} days",
|
||||
"custom": "{day_1} - {day_2} days"
|
||||
"days": "{days} днів",
|
||||
"custom": "{day_1} - {day_2} днів"
|
||||
},
|
||||
"filters": {
|
||||
"none": "Немає фільтрів",
|
||||
"tier": "Tier {tier}"
|
||||
"tier": "Рівень {tier}"
|
||||
},
|
||||
"no_data": {
|
||||
"battles_in_period": "За цей період боїв не було",
|
||||
@@ -339,10 +339,10 @@
|
||||
"body": {
|
||||
"subtitle": "Дослідження",
|
||||
"total": "Загалом",
|
||||
"research": "Research: {cost}",
|
||||
"research": "Дослідження: {cost}",
|
||||
"upgrades": "Upgrades: {cost}",
|
||||
"purchase": "Purchase: {cost}",
|
||||
"equipment": "Equipment: {cost}",
|
||||
"purchase": "Вартість: {cost}",
|
||||
"equipment": "Спорядження: {cost}",
|
||||
"estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди."
|
||||
},
|
||||
"errors": {
|
||||
@@ -435,7 +435,7 @@
|
||||
},
|
||||
"body": {
|
||||
"subtitle": "Inactive for {days}+ days",
|
||||
"listing": "{days} days"
|
||||
"listing": "{days} днів"
|
||||
}
|
||||
},
|
||||
"full_stats": {
|
||||
@@ -549,7 +549,7 @@
|
||||
"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_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
|
||||
"deleted_player": "Deleted {id}",
|
||||
"deleted_player": "",
|
||||
"neighbors": "Rating neighbors",
|
||||
"more": "+ {count} more"
|
||||
}
|
||||
@@ -643,9 +643,9 @@
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "Development mode",
|
||||
"title": "Налаштування",
|
||||
"description": "Змінити налаштування BlitzKit ",
|
||||
"dev_mode": "Режим розробника",
|
||||
"language": "Мова"
|
||||
},
|
||||
"common": {
|
||||
@@ -658,16 +658,16 @@
|
||||
"copied": "Скопійовано!"
|
||||
},
|
||||
"tank_search": {
|
||||
"select_dialog_title": "Search for tanks",
|
||||
"select_dialog_title": "Знайти танк",
|
||||
"swap_dialog_title": "Swap tank",
|
||||
"clear_filters": "Clear filters",
|
||||
"login": "Login to use this",
|
||||
"clear_filters": "Очистити",
|
||||
"login": "Увійдіть, щоб скористатися",
|
||||
"search_bar_hint": "Шукайте техніку...",
|
||||
"recent": "Недавно переглянуті",
|
||||
"select_all": "Вибрати все",
|
||||
"count_singular": "1 танк",
|
||||
"count_plural": "{count} tanks",
|
||||
"sorting_by": "Sorting by {name}",
|
||||
"count_plural": "Танків: {count} ",
|
||||
"sorting_by": "Відсортувати за {name}",
|
||||
"no_tanks_found": {
|
||||
"body": "Нічого не знайдено.",
|
||||
"search_again": "Спробуйте шукати заново.",
|
||||
@@ -731,7 +731,7 @@
|
||||
"home": {
|
||||
"pwa": {
|
||||
"title": "Install the BlitzKit App",
|
||||
"new": "NEW",
|
||||
"new": "",
|
||||
"checking": "Checking compatibility...",
|
||||
"instructions": "Instructions for Apple devices.",
|
||||
"install": "Install",
|
||||
@@ -763,20 +763,20 @@
|
||||
},
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "Reset",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"reset": "Скинути",
|
||||
"shuffle": "Перемішати",
|
||||
"remove": "Видалити зіграні",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "Сортувати за",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
"last_played": "Остання гра",
|
||||
"least_played": "Найменше зіграно"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"never": "Ніколи",
|
||||
"tank": "Танк",
|
||||
"last_played": "Last played",
|
||||
"last_played": "Остання гра",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "Перемоги"
|
||||
@@ -784,14 +784,14 @@
|
||||
"large_warning": {
|
||||
"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!",
|
||||
"cancel": "Cancel",
|
||||
"proceed": "Proceed"
|
||||
"cancel": "Скасувати",
|
||||
"proceed": "Продовжити"
|
||||
},
|
||||
"generate": "Generate playlist",
|
||||
"name": "Playlist",
|
||||
"generate": "Згенерувати плейлист",
|
||||
"name": "Плейлист",
|
||||
"description": "Tank playlist generator",
|
||||
"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"
|
||||
},
|
||||
"mixer": {
|
||||
@@ -803,8 +803,8 @@
|
||||
"randomize": "Randomize"
|
||||
},
|
||||
"guess": {
|
||||
"name": "Guess the tank",
|
||||
"button": "Guess away",
|
||||
"name": "Guess",
|
||||
"button": "What could it be?",
|
||||
"description": "Silhouette mini-game",
|
||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||
"search": {
|
||||
|
||||
@@ -803,8 +803,8 @@
|
||||
"randomize": "随机搭配"
|
||||
},
|
||||
"guess": {
|
||||
"name": "猜坦克",
|
||||
"button": "猜猜看",
|
||||
"name": "猜测",
|
||||
"button": "What could it be?",
|
||||
"description": "坦克剪影小游戏",
|
||||
"seo_description": "看剪影猜坦克",
|
||||
"search": {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/lz4js": "^0.2.1",
|
||||
"@types/progress": "^2.0.7",
|
||||
"@types/steam-user": "^5.1.0",
|
||||
"@types/three": "^0.180.0",
|
||||
"crc": "^4.3.2",
|
||||
"csv-parse": "^6.1.0",
|
||||
@@ -27,6 +28,7 @@
|
||||
"progress": "^2.0.3",
|
||||
"puppeteer": "^24.22.3",
|
||||
"sharp": "^0.34.4",
|
||||
"steam-user": "^5.2.3",
|
||||
"three": "^0.180.0",
|
||||
"ts-proto": "^2.7.7",
|
||||
"yaml": "^2.8.1"
|
||||
@@ -42,6 +44,7 @@
|
||||
"build:bot": "bun src/buildBot.ts",
|
||||
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts",
|
||||
"build:proxyclient": "bun src/buildProxyClient.ts",
|
||||
"build:catalogitemaccesssor": "bun src/buildCatalogItemAccessor.ts",
|
||||
"prunestrings": "bun src/pruneStrings.ts",
|
||||
"build:protos": "bun src/protos.ts",
|
||||
"patchnotes": "bun src/patchNotes.ts"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { assertSecret, SteamVFS } from "@blitzkit/core";
|
||||
import { assertSecret } from "@blitzkit/core";
|
||||
import { SteamVFS } from "../core/steam/vfs";
|
||||
|
||||
const APP = 444200;
|
||||
const DEPOT = 444202;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { argv } from "process";
|
||||
const projectDir = "../..";
|
||||
const srcRoot = `${projectDir}/packages/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 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 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 requests = source.matchAll(requestPattern);
|
||||
|
||||
const imports: string[] = [];
|
||||
const methods: { name: string; method: string }[] = [];
|
||||
const names: string[] = [];
|
||||
|
||||
for (const match of requests) {
|
||||
const name = match[1];
|
||||
// const method =
|
||||
// name.charAt(0).toLowerCase() + name.slice(1).replace(/Async$/, "");
|
||||
const method = name;
|
||||
|
||||
imports.push(`${name}Request`, `${name}Response`);
|
||||
methods.push({ name, method });
|
||||
names.push(name);
|
||||
}
|
||||
|
||||
let content = "";
|
||||
@@ -31,9 +28,9 @@ content += 'import { BaseProxyClient } from "./baseProxyClient";\n\n';
|
||||
|
||||
content +=
|
||||
"export abstract class MethodPopulatedProxyClient extends BaseProxyClient {\n";
|
||||
content += methods
|
||||
.map(({ name, method }) => {
|
||||
return ` ${method}(data: ${name}Request) {
|
||||
content += names
|
||||
.map((name) => {
|
||||
return ` ${name}(data: ${name}Request) {
|
||||
return this.request("${name}Request", ${name}Request, data, ${name}Response);
|
||||
}\n`;
|
||||
})
|
||||
|
||||
@@ -2,11 +2,11 @@ import {
|
||||
Hierarchy,
|
||||
Sc2ReadStream,
|
||||
ScgReadStream,
|
||||
SteamVFS,
|
||||
VertexAttribute,
|
||||
} from "@blitzkit/core";
|
||||
import { Accessor, Document, Node, Scene } from "@gltf-transform/core";
|
||||
import { times } from "lodash-es";
|
||||
import { SteamVFS } from "../steam/vfs";
|
||||
import {
|
||||
vertexAttributeGLTFName,
|
||||
vertexAttributeGltfVectorSizes,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
Hierarchy,
|
||||
Sc2ReadStream,
|
||||
ScgReadStream,
|
||||
SteamVFS,
|
||||
Textures,
|
||||
VertexAttribute,
|
||||
} from "@blitzkit/core";
|
||||
@@ -10,6 +9,7 @@ import { Document, Material, Node, Scene } from "@gltf-transform/core";
|
||||
import { dedup, prune } from "@gltf-transform/functions";
|
||||
import { times } from "lodash-es";
|
||||
import { dirname } from "path";
|
||||
import { SteamVFS } from "../../steam/vfs";
|
||||
import { readBaseColor } from "../readBaseColor";
|
||||
import { readNormal } from "../readNormal";
|
||||
import { readRoughnessMetallic } from "../readRoughnessMetallic";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { readDVPL } from "@blitzkit/core/src/blitz";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
import { normalize } from "path/posix";
|
||||
import SteamUser, { EConnectionProtocol } from "steam-user";
|
||||
import { parse } from "yaml";
|
||||
import { readDVPL } from "../blitz";
|
||||
|
||||
interface SteamManifestFile {
|
||||
chunks: {
|
||||
@@ -4,6 +4,10 @@ import { readdir, rm } from "fs/promises";
|
||||
import { promisify } from "util";
|
||||
|
||||
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 = [
|
||||
// "../../packages/core/src/protos",
|
||||
@@ -12,12 +16,24 @@ const roots = [
|
||||
|
||||
const exec = promisify(execSync);
|
||||
|
||||
let executableFileExtension: string;
|
||||
let tsProtoExecutableLocation: string | undefined = undefined;
|
||||
|
||||
if (existsSync("../../node_modules/.bin/protoc-gen-ts_proto.exe")) {
|
||||
executableFileExtension = ".exe";
|
||||
} else {
|
||||
executableFileExtension = "";
|
||||
for (const location of TS_PROTO_EXECUTABLE_LOCATIONS) {
|
||||
if (existsSync(location)) {
|
||||
tsProtoExecutableLocation = location;
|
||||
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) {
|
||||
@@ -32,7 +48,7 @@ for (const root of roots) {
|
||||
while (files.length > 0) {
|
||||
let command = [
|
||||
"protoc",
|
||||
`--plugin=../../node_modules/.bin/protoc-gen-ts_proto${executableFileExtension}`,
|
||||
`--plugin=${tsProtoExecutableLocation}`,
|
||||
"--ts_proto_opt=esModuleInterop=true",
|
||||
"--ts_proto_opt=oneof=unions-value",
|
||||
// "--ts_proto_opt=removeEnumPrefix=true",
|
||||
|
||||
@@ -67,7 +67,9 @@ export const PrimaryArmorScene = memo(() => {
|
||||
<group ref={turretContainer}>
|
||||
{armorNodes.map((node) => {
|
||||
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 armorId = nameToArmorId(node.name);
|
||||
|
||||
+12
-1
@@ -4,6 +4,7 @@ import {
|
||||
resolvePenetrationCoefficient,
|
||||
} from "@blitzkit/core";
|
||||
import { invalidate, useFrame } from "@react-three/fiber";
|
||||
import type { QuicklimeEvent } from "quicklime";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
MeshBasicMaterial,
|
||||
@@ -21,6 +22,7 @@ import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
|
||||
import { transitionEvent } from "../../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||
import { spacedArmorRenderTarget } from "./target";
|
||||
@@ -72,6 +74,8 @@ export function PrimaryArmorSceneComponent({
|
||||
resolution: { value: new Vector2() },
|
||||
spacedArmorBuffer: { value: null },
|
||||
spacedArmorDepth: { value: null },
|
||||
|
||||
opacity: { value: 0 },
|
||||
},
|
||||
]),
|
||||
});
|
||||
@@ -154,6 +158,10 @@ export function PrimaryArmorSceneComponent({
|
||||
if (!noInvalidate) invalidate();
|
||||
}
|
||||
|
||||
function handleTransitionEvent(event: QuicklimeEvent<number>) {
|
||||
material.uniforms.opacity.value = event.data;
|
||||
}
|
||||
|
||||
handleShellChange();
|
||||
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
|
||||
handleAdvancedHighlightingChange(
|
||||
@@ -164,6 +172,8 @@ export function PrimaryArmorSceneComponent({
|
||||
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
|
||||
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
|
||||
|
||||
transitionEvent.on(handleTransitionEvent);
|
||||
|
||||
const unsubscribes = [
|
||||
Duel.on((state) => state.antagonist.shell, handleShellChange),
|
||||
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||
@@ -184,12 +194,13 @@ export function PrimaryArmorSceneComponent({
|
||||
(state) => state.antagonist.equipmentMatrix,
|
||||
(equipment) => handleAntagonistEquipmentChange(equipment)
|
||||
),
|
||||
() => transitionEvent.off(handleTransitionEvent),
|
||||
];
|
||||
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useFrame(({ gl, camera }) => {
|
||||
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||
|
||||
+2
-5
@@ -1,12 +1,9 @@
|
||||
precision mediump float;
|
||||
|
||||
#define USE_FOG
|
||||
|
||||
#include <fog_pars_fragment>
|
||||
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vViewPosition;
|
||||
|
||||
uniform float opacity;
|
||||
uniform float thickness;
|
||||
uniform float penetration;
|
||||
uniform float caliber;
|
||||
@@ -119,5 +116,5 @@ void main() {
|
||||
gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
|
||||
}
|
||||
|
||||
#include <fog_fragment>
|
||||
gl_FragColor.a *= opacity;
|
||||
}
|
||||
-6
@@ -1,9 +1,5 @@
|
||||
precision mediump float;
|
||||
|
||||
#define USE_FOG
|
||||
|
||||
#include <fog_pars_vertex>
|
||||
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vViewPosition;
|
||||
|
||||
@@ -14,6 +10,4 @@ void main() {
|
||||
vNormal = normalMatrix * normal;
|
||||
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
|
||||
#include <fog_vertex>
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "../../../core/blitzkit/modelTransform";
|
||||
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
import { transitionEvent } from "../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||
import { ArmorType } from "./SpacedArmorScene";
|
||||
import type {
|
||||
ArmorUserData,
|
||||
@@ -108,10 +109,11 @@ export function StaticArmorSceneComponent({
|
||||
() =>
|
||||
new MeshBasicMaterial({
|
||||
color,
|
||||
opacity,
|
||||
transparent: opacity < 1,
|
||||
transparent: true,
|
||||
depthWrite,
|
||||
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
||||
opacity: 0,
|
||||
userData: { opacity0: opacity },
|
||||
}),
|
||||
[thickness]
|
||||
);
|
||||
@@ -121,6 +123,8 @@ export function StaticArmorSceneComponent({
|
||||
color: color
|
||||
.clone()
|
||||
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
||||
opacity: 0,
|
||||
transparent: true,
|
||||
}),
|
||||
[thickness]
|
||||
);
|
||||
@@ -163,11 +167,22 @@ export function StaticArmorSceneComponent({
|
||||
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 = [
|
||||
Tankopedia.on(
|
||||
(state) => state.highlightArmor?.name,
|
||||
handleHighlightArmor
|
||||
),
|
||||
() => transitionEvent.off(handleTransitionEvent),
|
||||
];
|
||||
|
||||
if (props.clip) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
|
||||
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||
import { CompareCellDirection } from "../CompareCell";
|
||||
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||
import { CompareRow } from "./CompareRow";
|
||||
import { CompareSectionTitle } from "./CompareSectionTitle";
|
||||
@@ -79,7 +80,7 @@ export function Body({ stats }: BodyProps) {
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name={strings.website.tools.tankopedia.characteristics.values.reload}
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
value={(stats) =>
|
||||
stats.shellReload ??
|
||||
stats.shellReloads!.reduce((a, b) => a + b, 0) /
|
||||
@@ -95,7 +96,7 @@ export function Body({ stats }: BodyProps) {
|
||||
<>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
value="intraClip"
|
||||
decimals={2}
|
||||
/>
|
||||
@@ -107,7 +108,7 @@ export function Body({ stats }: BodyProps) {
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
value="shellRicochet"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow stats={stats} value="shellVelocity" decimals={0} />
|
||||
@@ -116,48 +117,48 @@ export function Body({ stats }: BodyProps) {
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
value="aimTime"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={2}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
value="dispersion"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={3}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="dispersionMoving"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={3}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="dispersionHullTraversing"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={3}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="dispersionTurretTraversing"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={3}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="dispersionShooting"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={3}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="dispersionGunDamaged"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={3}
|
||||
/>
|
||||
<CompareRow stats={stats} value="gunDepression" decimals={1} />
|
||||
@@ -186,21 +187,21 @@ export function Body({ stats }: BodyProps) {
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
value="hardTerrainCoefficientRaw"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={2}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="mediumTerrainCoefficientRaw"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={2}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="softTerrainCoefficientRaw"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={2}
|
||||
/>
|
||||
<CompareRow
|
||||
@@ -246,7 +247,7 @@ export function Body({ stats }: BodyProps) {
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
value="fireChance"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
display={(stats) => (stats.fireChance * 100).toFixed(0)}
|
||||
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
|
||||
/>
|
||||
@@ -288,28 +289,28 @@ export function Body({ stats }: BodyProps) {
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
value="volume"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="width"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="height"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
indent
|
||||
value="length"
|
||||
deltaType="lowerIsBetter"
|
||||
deltaType={CompareCellDirection.LOWER_IS_BETTER}
|
||||
decimals={1}
|
||||
/>
|
||||
</Table.Body>
|
||||
|
||||
@@ -6,17 +6,17 @@ import {
|
||||
import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
|
||||
import { useRef } from "react";
|
||||
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||
import { Var } from "../../core/radix/var";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||
import { ComparePersistent } from "../../stores/comparePersistent";
|
||||
import { CompareCell, CompareCellDirection } from "../CompareCell";
|
||||
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
||||
|
||||
type CompareRowProps = {
|
||||
display?: (
|
||||
member: Awaited<TankCharacteristics>
|
||||
) => number | string | undefined;
|
||||
deltaType?: "higherIsBetter" | "lowerIsBetter";
|
||||
deltaType?: CompareCellDirection;
|
||||
decimals?: number;
|
||||
deltaNominalDisplay?: (delta: number) => string | number;
|
||||
indent?: boolean;
|
||||
@@ -33,7 +33,7 @@ type CompareRowProps = {
|
||||
|
||||
export function CompareRow({
|
||||
display,
|
||||
deltaType = "higherIsBetter",
|
||||
deltaType = CompareCellDirection.HIGHER_IS_BETTER,
|
||||
decimals,
|
||||
deltaNominalDisplay,
|
||||
indent,
|
||||
@@ -108,33 +108,18 @@ export function CompareRow({
|
||||
{values.map((value, index) => {
|
||||
const delta = value - values[0];
|
||||
const deltaPercentage = value / values[0] - 1;
|
||||
const normalizedDeltaPercentage = Math.round(
|
||||
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25)
|
||||
);
|
||||
const resolvedDisplayValue = display
|
||||
? display(stats[index])
|
||||
: decimals === undefined
|
||||
? value
|
||||
: value?.toFixed(decimals);
|
||||
? value
|
||||
: value?.toFixed(decimals);
|
||||
|
||||
return (
|
||||
<Table.Cell
|
||||
<CompareCell
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor:
|
||||
index === 0 ||
|
||||
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")})`,
|
||||
}}
|
||||
truth={values[0]}
|
||||
value={value}
|
||||
direction={deltaType}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
@@ -160,8 +145,8 @@ export function CompareRow({
|
||||
deltaNominalDisplay
|
||||
? deltaNominalDisplay(delta)
|
||||
: decimals === undefined
|
||||
? delta
|
||||
: delta.toFixed(decimals)
|
||||
? delta
|
||||
: delta.toFixed(decimals)
|
||||
}`}
|
||||
)
|
||||
</Text>
|
||||
@@ -179,7 +164,7 @@ export function CompareRow({
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Table.Cell>
|
||||
</CompareCell>
|
||||
);
|
||||
})}
|
||||
</Table.Row>
|
||||
|
||||
@@ -51,19 +51,17 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
variant="surface"
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "100%",
|
||||
borderRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
|
||||
<Flex
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
@@ -107,8 +105,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
crewSkills[skill] === 0
|
||||
? Var("gray-11")
|
||||
: permanentSkills.includes(skill)
|
||||
? Var("crimson-11")
|
||||
: Var("amber-11"),
|
||||
? Var("crimson-11")
|
||||
: Var("amber-11"),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -102,7 +102,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StickyColumnHeaderCell>
|
||||
<StickyColumnHeaderCell width="0">
|
||||
<Flex direction="column" align="center" justify="between" gap="2">
|
||||
<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";
|
||||
|
||||
interface Props extends ComponentProps<"spotLight"> {
|
||||
@@ -8,10 +10,20 @@ export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
|
||||
return (
|
||||
<>
|
||||
{debug && (
|
||||
<mesh position={props.position}>
|
||||
<icosahedronGeometry args={[0.1, 2]} />
|
||||
<meshBasicMaterial color={props.color} />
|
||||
</mesh>
|
||||
<group position={props.position}>
|
||||
<mesh>
|
||||
<icosahedronGeometry args={[0.1, 2]} />
|
||||
<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} />
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||
import { StickyTableRoot } from '../StickyTableRoot';
|
||||
import { Header } from './Header';
|
||||
import { Tanks } from './Tanks';
|
||||
import { Flex } from "@radix-ui/themes";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
import { StickyTableRoot } from "../StickyTableRoot";
|
||||
import { Header } from "./Header";
|
||||
import { Tanks } from "./Tanks";
|
||||
|
||||
export function TankTable({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<Flex justify="center" maxWidth="100%" flexGrow="1" position="relative">
|
||||
<StickyTableRoot
|
||||
size={{ initial: '1', sm: '2' }}
|
||||
size={{ initial: "1", sm: "2" }}
|
||||
variant="surface"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
height: '100%',
|
||||
position: "absolute",
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
height: "100%",
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { TankDefinition, formatCompact } from '@blitzkit/core';
|
||||
import { Table } from '@radix-ui/themes';
|
||||
import { memo } from 'react';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { Performance } from '../../stores/performance';
|
||||
import { TankRowHeaderCell } from '../TankRowHeaderCell';
|
||||
import { TankDefinition, formatCompact } from "@blitzkit/core";
|
||||
import { Table } from "@radix-ui/themes";
|
||||
import { memo } from "react";
|
||||
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { Performance } from "../../stores/performance";
|
||||
import { CompareCell, CompareCellDirection } from "../CompareCell";
|
||||
import { TankRowHeaderCell } from "../TankRowHeaderCell";
|
||||
|
||||
interface TankRowProps {
|
||||
tank: TankDefinition;
|
||||
average: Record<string, number>;
|
||||
}
|
||||
|
||||
const averageDefinitions = await awaitableAverageDefinitions;
|
||||
|
||||
export const TankRow = memo<TankRowProps>(
|
||||
({ tank }) => {
|
||||
({ tank, average }) => {
|
||||
const averages = averageDefinitions.averages[tank.id];
|
||||
const ratio = useAveragesExclusionRatio();
|
||||
const playerCountPeriod = Performance.use(
|
||||
(state) => state.playerCountPeriod,
|
||||
(state) => state.playerCountPeriod
|
||||
);
|
||||
const { locale } = useLocale();
|
||||
|
||||
@@ -26,59 +28,108 @@ export const TankRow = memo<TankRowProps>(
|
||||
<Table.Row>
|
||||
<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)}%
|
||||
</Table.Cell>
|
||||
</CompareCell>
|
||||
<Table.Cell align="center">
|
||||
{formatCompact(
|
||||
locale,
|
||||
Math.round(ratio * averages.samples[playerCountPeriod]),
|
||||
Math.round(ratio * averages.samples[playerCountPeriod])
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
<CompareCell
|
||||
value={averages.mu.damage_dealt / averages.mu.battles}
|
||||
truth={average.damage}
|
||||
align="center"
|
||||
>
|
||||
{Math.round(
|
||||
averages.mu.damage_dealt / averages.mu.battles,
|
||||
averages.mu.damage_dealt / averages.mu.battles
|
||||
).toLocaleString(locale)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<CompareCell
|
||||
value={averages.mu.survived_battles / averages.mu.battles}
|
||||
truth={average.survival}
|
||||
align="center"
|
||||
>
|
||||
{Math.round(
|
||||
(averages.mu.survived_battles / averages.mu.battles) * 100,
|
||||
(averages.mu.survived_battles / averages.mu.battles) * 100
|
||||
)}
|
||||
%
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<CompareCell
|
||||
value={averages.mu.xp / averages.mu.battles}
|
||||
truth={average.xp}
|
||||
align="center"
|
||||
>
|
||||
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString(
|
||||
locale,
|
||||
locale
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<CompareCell
|
||||
value={averages.mu.frags / averages.mu.battles}
|
||||
truth={average.kills}
|
||||
align="center"
|
||||
>
|
||||
{(averages.mu.frags / averages.mu.battles).toFixed(2)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<CompareCell
|
||||
value={averages.mu.spotted / averages.mu.battles}
|
||||
truth={average.spots}
|
||||
align="center"
|
||||
>
|
||||
{(averages.mu.spotted / averages.mu.battles).toFixed(2)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<CompareCell
|
||||
value={averages.mu.hits / averages.mu.shots}
|
||||
truth={average.accuracy}
|
||||
align="center"
|
||||
>
|
||||
{Math.round((averages.mu.hits / averages.mu.shots) * 100)}%
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<CompareCell
|
||||
value={averages.mu.shots / averages.mu.battles}
|
||||
truth={average.shots}
|
||||
align="center"
|
||||
>
|
||||
{(averages.mu.shots / averages.mu.battles).toFixed(1)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<CompareCell
|
||||
value={averages.mu.hits / averages.mu.battles}
|
||||
truth={average.hits}
|
||||
align="center"
|
||||
>
|
||||
{(averages.mu.hits / averages.mu.battles).toFixed(1)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<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)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<CompareCell
|
||||
value={averages.mu.damage_received / averages.mu.battles}
|
||||
truth={average.damageReceived}
|
||||
direction={CompareCellDirection.LOWER_IS_BETTER}
|
||||
align="center"
|
||||
>
|
||||
{Math.round(
|
||||
averages.mu.damage_received / averages.mu.battles,
|
||||
averages.mu.damage_received / averages.mu.battles
|
||||
).toLocaleString(locale)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
</CompareCell>
|
||||
<CompareCell
|
||||
value={averages.mu.capture_points / averages.mu.battles}
|
||||
truth={average.capturePoints}
|
||||
align="center"
|
||||
>
|
||||
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)}
|
||||
</Table.Cell>
|
||||
</CompareCell>
|
||||
</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 { 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 { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||
import { filterTanks } from "../../core/blitzkit/filterTanks";
|
||||
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { Performance } from "../../stores/performance";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
||||
import { RowLoader } from "./RowLoader";
|
||||
import { TankRow } from "./TankRow";
|
||||
import { Total } from "./Total";
|
||||
|
||||
const PREVIEW_COUNT = 10;
|
||||
const DEFAULT_LOADED_ROWS = 25;
|
||||
@@ -145,9 +155,83 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
setLoadedRows(DEFAULT_LOADED_ROWS);
|
||||
}, [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 (
|
||||
<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) => {
|
||||
const tank = tankDefinitions.tanks[averages.id];
|
||||
@@ -157,7 +241,7 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<Fragment key={tank.id}>
|
||||
<Suspense fallback={<RowLoader />}>
|
||||
<TankRow tank={tank} />
|
||||
<TankRow average={average} tank={tank} />
|
||||
</Suspense>
|
||||
</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}
|
||||
</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 && (
|
||||
<>
|
||||
<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.Trigger>
|
||||
<Button size={{ initial: "1", xs: "2" }}>
|
||||
|
||||
@@ -5,11 +5,7 @@ import {
|
||||
TankType,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from "@blitzkit/core";
|
||||
import {
|
||||
ComponentBooleanIcon,
|
||||
LockClosedIcon,
|
||||
LockOpen2Icon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { LockClosedIcon, LockOpen2Icon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -71,7 +67,9 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||
<img
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
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
|
||||
src={asset(
|
||||
`icons/shells/${shellTypeIcons[shellType]}${premium ? "_premium" : ""}.webp`
|
||||
`icons/shells/${shellTypeIcons[shellType]}${
|
||||
premium ? "_premium" : ""
|
||||
}.webp`
|
||||
)}
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
/>
|
||||
@@ -135,29 +135,13 @@ function OwnershipFilter(props: FlexProps) {
|
||||
<IconButton
|
||||
size={size}
|
||||
disabled={!wargaming}
|
||||
variant={tankFilters.ownership === "all" ? "solid" : "soft"}
|
||||
variant={tankFilters.showOwned ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.ownership === "all" ? undefined : "gray"}
|
||||
color={tankFilters.showOwned ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.ownership = "all";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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";
|
||||
draft.showOwned = !draft.showOwned;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -167,13 +151,13 @@ function OwnershipFilter(props: FlexProps) {
|
||||
<IconButton
|
||||
size={size}
|
||||
disabled={!wargaming}
|
||||
variant={tankFilters.ownership === "unowned" ? "solid" : "soft"}
|
||||
variant={tankFilters.showUnowned ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.ownership === "unowned" ? undefined : "gray"}
|
||||
color={tankFilters.showUnowned ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.ownership = "unowned";
|
||||
draft.showUnowned = !draft.showUnowned;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -579,27 +563,13 @@ export function FilterControl() {
|
||||
>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
||||
variant={tankFilters.showNonTesting ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "include" ? undefined : "gray"}
|
||||
color={tankFilters.showNonTesting ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "include";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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";
|
||||
draft.showNonTesting = !draft.showNonTesting;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -607,15 +577,16 @@ export function FilterControl() {
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
||||
variant={tankFilters.showTesting ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "only" ? undefined : "gray"}
|
||||
color={tankFilters.showTesting ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "only";
|
||||
draft.showTesting = !draft.showTesting;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -487,7 +487,7 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{tankFilters.testing === "only" && (
|
||||
{tankFilters.showTesting && !tankFilters.showNonTesting && (
|
||||
<Flex justify="center" mt="4">
|
||||
<Callout.Root color="amber">
|
||||
<Callout.Icon>
|
||||
|
||||
+24
-6
@@ -12,6 +12,7 @@ import {
|
||||
EyeOpenIcon,
|
||||
ShadowInnerIcon,
|
||||
ShadowNoneIcon,
|
||||
SunIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import {
|
||||
Button,
|
||||
@@ -55,7 +56,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
const hasCustomShell = Tankopedia.use(
|
||||
(state) => state.customShell !== undefined
|
||||
);
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const isFullScreen = useFullScreen();
|
||||
const advancedHighlighting = TankopediaPersistent.use(
|
||||
(state) => state.advancedHighlighting
|
||||
@@ -73,6 +74,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
const { strings, unwrap } = useLocale();
|
||||
const revealed = Tankopedia.use((state) => state.revealed);
|
||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -84,7 +86,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
gap="2"
|
||||
direction="column"
|
||||
top="50%"
|
||||
right={display === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"}
|
||||
right={
|
||||
requestedDisplay === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"
|
||||
}
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: "translateY(-50%)",
|
||||
@@ -296,7 +300,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
>
|
||||
{disturbed && (
|
||||
<>
|
||||
{display === TankopediaDisplay.DynamicArmor && (
|
||||
{requestedDisplay === TankopediaDisplay.DynamicArmor && (
|
||||
<IconButton
|
||||
size="2"
|
||||
highContrast
|
||||
@@ -349,6 +353,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
</DropdownMenu.Content>
|
||||
</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
|
||||
color="gray"
|
||||
size="2"
|
||||
@@ -384,11 +402,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Flex gap="3" align="center" mt="2">
|
||||
<SegmentedControl.Root
|
||||
variant="classic"
|
||||
value={`${disturbed ? display : -1}`}
|
||||
value={`${disturbed ? requestedDisplay : -1}`}
|
||||
onValueChange={(value) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
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.Root>
|
||||
|
||||
{display === TankopediaDisplay.DynamicArmor && (
|
||||
{requestedDisplay === TankopediaDisplay.DynamicArmor && (
|
||||
<Dialog.Root
|
||||
open={antagonistSelectorOpen}
|
||||
onOpenChange={setAntagonistSelectorOpen}
|
||||
|
||||
+3
-13
@@ -22,14 +22,12 @@ const poseDistances: Record<Pose, number> = {
|
||||
const modelDefinitions = await awaitableModelDefinitions;
|
||||
|
||||
interface ControlsProps {
|
||||
naked?: boolean;
|
||||
autoRotate?: boolean;
|
||||
zoomable?: boolean;
|
||||
enableRotate?: boolean;
|
||||
}
|
||||
|
||||
export function Controls({
|
||||
naked,
|
||||
autoRotate = true,
|
||||
zoomable = true,
|
||||
enableRotate = true,
|
||||
@@ -79,11 +77,7 @@ export function Controls({
|
||||
protagonistHullOrigin.y +
|
||||
protagonistTurretOrigin.y +
|
||||
protagonistGunOrigin.y;
|
||||
const inspectModeInitialPosition = new Vector3(
|
||||
-8,
|
||||
naked ? gunHeight + 10 : 2,
|
||||
-13
|
||||
);
|
||||
const inspectModeInitialPosition = new Vector3(-8, 2, -13);
|
||||
|
||||
const t0 = useRef(Date.now() / 1000);
|
||||
useFrame(() => {
|
||||
@@ -236,14 +230,10 @@ export function Controls({
|
||||
orbitControls.current.rotateSpeed = 0.25;
|
||||
|
||||
camera.position.copy(inspectModeInitialPosition);
|
||||
orbitControls.current.target.set(
|
||||
0,
|
||||
gunHeight * (naked ? 1 / 4 : 3 / 4),
|
||||
0
|
||||
);
|
||||
orbitControls.current.target.set(0, gunHeight * 0.6, 0);
|
||||
orbitControls.current.enablePan = true;
|
||||
orbitControls.current.enableZoom = true;
|
||||
camera.fov = naked ? 20 : 25;
|
||||
camera.fov = 25;
|
||||
|
||||
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 } from "@react-three/fiber";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { times } from "lodash-es";
|
||||
import { useRef, useState } from "react";
|
||||
import { Color, SpotLight, SpotLightHelper } from "three";
|
||||
import { degToRad } from "three/src/math/MathUtils.js";
|
||||
import { isHalloween } from "../../../../../../core/blitzkit/isHalloween";
|
||||
import { Quicklime } from "quicklime";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { HemisphereLight, SpotLight, type Group } from "three";
|
||||
import { clamp, degToRad, lerp } from "three/src/math/MathUtils.js";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
|
||||
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 THETA_OFFSET = degToRad(-152);
|
||||
const LIGHT_DISTANCE = 13;
|
||||
const LIGHT_HEIGHT_0 = 4;
|
||||
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;
|
||||
const TARGET_FRAME_RATE = 30;
|
||||
export const transitionEvent = new Quicklime<number>(0);
|
||||
|
||||
export function Lighting() {
|
||||
const [cast, setCast] = useState(true);
|
||||
|
||||
const wrapper = useRef<Group>(null!);
|
||||
const t0 = useRef(0);
|
||||
const topLight = useRef<SpotLight>(null!);
|
||||
const frontLight = useRef<SpotLight>(null!);
|
||||
const transition = useRef(false);
|
||||
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");
|
||||
useHelper(frontLight, SpotLightHelper, "green");
|
||||
useEffect(() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
|
||||
const last = useRef<number>(0);
|
||||
const samples = useRef<number[]>([]);
|
||||
t0.current = clock.elapsedTime - REVEAL_ANIMATION_TIME;
|
||||
}, []);
|
||||
|
||||
useFrame(({ clock, frameloop }) => {
|
||||
if (frameloop === "never") {
|
||||
useFrame(({ invalidate }) => {
|
||||
if (transition.current) {
|
||||
transition.current = false;
|
||||
t0.current = clock.elapsedTime;
|
||||
samples.current = [];
|
||||
return;
|
||||
}
|
||||
if (!cast || clock.elapsedTime - t0.current < TARGET_FRAME_RATE * N) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dt = clock.elapsedTime - last.current;
|
||||
last.current = clock.elapsedTime;
|
||||
const x = clamp(
|
||||
(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;
|
||||
samples.current.push(fps);
|
||||
if (t !== transitionEvent.last) {
|
||||
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 =
|
||||
samples.current.reduce((a, b) => a + b, 0) / samples.current.length;
|
||||
if (x < 2) invalidate();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<group ref={wrapper}>
|
||||
<hemisphereLight intensity={0} color="#ffffff" groundColor="#afafaf" />
|
||||
|
||||
{times(LIGHTS_COUNT, (index) => {
|
||||
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 = [
|
||||
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),
|
||||
] as const;
|
||||
const color = new Color();
|
||||
const intensity = lerp(INTENSITY_0, INTENSITY_1, x);
|
||||
|
||||
return (
|
||||
<HelpingSpotLight
|
||||
userData={{ index }}
|
||||
key={index}
|
||||
position={position}
|
||||
intensity={INTENSITY}
|
||||
intensity={intensity}
|
||||
penumbra={1}
|
||||
castShadow={cast}
|
||||
decay={1}
|
||||
color={color}
|
||||
castShadow={highGraphics}
|
||||
decay={0}
|
||||
color="#ffffff"
|
||||
angle={0}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
+3
-2
@@ -4,7 +4,7 @@ import { Quicklime, type QuicklimeEvent } from "quicklime";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Mesh, MeshStandardMaterial } from "three";
|
||||
|
||||
const SIZE = 2 ** 5;
|
||||
const SIZE = 2 ** 6;
|
||||
|
||||
export const screenshotReadyEvent = new Quicklime(false);
|
||||
|
||||
@@ -36,6 +36,7 @@ export function SceneProps() {
|
||||
receiveShadow
|
||||
visible={!screenshotReadyEvent.last!}
|
||||
ref={mesh}
|
||||
renderOrder={-1}
|
||||
>
|
||||
<planeGeometry args={[SIZE, SIZE]} />
|
||||
<meshStandardMaterial
|
||||
@@ -43,7 +44,7 @@ export function SceneProps() {
|
||||
ref={material}
|
||||
color="black"
|
||||
roughness={1}
|
||||
metalness={1}
|
||||
metalness={0}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
|
||||
+3
-1
@@ -106,7 +106,9 @@ export function TankModel() {
|
||||
const isTurret = node.name.startsWith("turret_");
|
||||
const isCurrentTurret =
|
||||
node.name ===
|
||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`;
|
||||
`turret_${turretModelDefinition.model_id
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
const isVisible = isCurrentTurret;
|
||||
const position = 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 {
|
||||
forwardRef,
|
||||
Suspense,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Fog } from "three";
|
||||
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
|
||||
@@ -29,26 +26,16 @@ import { SmartCanvas } from "../../../../SmartCanvas";
|
||||
import { AutoClear } from "./components/AutoClear";
|
||||
import { Controls } from "./components/Control";
|
||||
import { InitialAligner } from "./components/InitialAligner";
|
||||
import { InitialFogReveal } from "./components/InitialFogReveal";
|
||||
import { Lighting } from "./components/Lighting";
|
||||
import { SceneProps } from "./components/SceneProps";
|
||||
import { TankModel } from "./components/TankModel";
|
||||
import { TransitionSkeleton } from "./components/TransitionSkeleton";
|
||||
|
||||
interface TankSandboxProps {
|
||||
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>(
|
||||
({ thicknessRange, naked }, ref) => {
|
||||
const fog = useRef(new Fog("black", forNear1, fogFar1));
|
||||
({ thicknessRange }, ref) => {
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||
@@ -58,40 +45,11 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonistTurret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
|
||||
const rawDisplay = Tankopedia.use((state) => state.display);
|
||||
const [display, setDisplay] = useState(rawDisplay);
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
||||
(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!, []);
|
||||
|
||||
function handlePointerDown() {
|
||||
@@ -188,10 +146,12 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
}
|
||||
}, [display]);
|
||||
|
||||
console.log(TankopediaDisplay[display]);
|
||||
|
||||
return (
|
||||
<SmartCanvas
|
||||
ref={canvas}
|
||||
scene={{ fog: naked ? undefined : fog.current }}
|
||||
scene={{ fog: new Fog("black", 30, 50) }}
|
||||
gl={{
|
||||
clippingPlanes: [],
|
||||
localClippingEnabled: true,
|
||||
@@ -207,30 +167,29 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
}}
|
||||
style={{
|
||||
userSelect: "none",
|
||||
width: naked ? BLITZKIT_TANK_ICON_SIZE.width : "100%",
|
||||
height: naked ? BLITZKIT_TANK_ICON_SIZE.height : "100%",
|
||||
outline: naked ? "1rem red solid" : undefined,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
outline: undefined,
|
||||
}}
|
||||
>
|
||||
{!naked && <SceneProps />}
|
||||
<Lighting />
|
||||
|
||||
<SceneProps />
|
||||
{(display === TankopediaDisplay.Model ||
|
||||
(display === TankopediaDisplay.DynamicArmor &&
|
||||
!hideTankModelUnderArmor)) && <TankModel />}
|
||||
<TransitionSkeleton />
|
||||
<ShotDisplay />
|
||||
<ArmorPlateDisplay />
|
||||
<AutoClear />
|
||||
|
||||
<Suspense>
|
||||
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
|
||||
<Controls naked={naked} />
|
||||
<InitialFogReveal />
|
||||
<Controls />
|
||||
|
||||
{display === TankopediaDisplay.DynamicArmor && <Armor />}
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<StaticArmor thicknessRange={thicknessRange} />
|
||||
)}
|
||||
<Lighting />
|
||||
</Suspense>
|
||||
|
||||
<InitialAligner />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Flex } from "@radix-ui/themes";
|
||||
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 { useFullScreen } from "../../../hooks/useFullScreen";
|
||||
import { Duel } from "../../../stores/duel";
|
||||
@@ -99,11 +99,11 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
isFullScreen
|
||||
? "100vh"
|
||||
: disturbed
|
||||
? "calc(100svh - 8rem)"
|
||||
: {
|
||||
initial: "28rem",
|
||||
md: "calc(100svh - 14rem)",
|
||||
}
|
||||
? "calc(100svh - 8rem)"
|
||||
: {
|
||||
initial: "28rem",
|
||||
md: "calc(100svh - 14rem)",
|
||||
}
|
||||
}
|
||||
maxWidth={isFullScreen ? undefined : "120rem"}
|
||||
flexGrow="1"
|
||||
@@ -120,30 +120,17 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
position="relative"
|
||||
>
|
||||
<Box position="absolute" width="100%" height="100%" overflow="hidden">
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="relative"
|
||||
style={{
|
||||
transitionDuration: "1s",
|
||||
}}
|
||||
>
|
||||
<Box width="100%" height="100%" position="relative">
|
||||
<Box
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transitionDuration: "2s",
|
||||
opacity: revealed ? 1 : 0,
|
||||
filter: disturbed
|
||||
? undefined
|
||||
: `drop-shadow(0 1rem 1rem black)`,
|
||||
}}
|
||||
position="absolute"
|
||||
width="100%"
|
||||
height="100%"
|
||||
top="0"
|
||||
left="0"
|
||||
>
|
||||
<Suspense>
|
||||
{!skeleton && (
|
||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||
)}
|
||||
</Suspense>
|
||||
{!skeleton && (
|
||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -10,16 +10,14 @@ export async function filterTank(
|
||||
owned: number[] = []
|
||||
) {
|
||||
return (
|
||||
(filters.ownership === "all" ||
|
||||
(filters.ownership === "owned" && owned.includes(tank.id)) ||
|
||||
(filters.ownership === "unowned" && !owned.includes(tank.id))) &&
|
||||
((filters.showOwned && owned.includes(tank.id)) ||
|
||||
(filters.showUnowned && !owned.includes(tank.id))) &&
|
||||
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
|
||||
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
|
||||
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
|
||||
(filters.types.length === 0 || filters.types.includes(tank.type)) &&
|
||||
(filters.testing === "include" ||
|
||||
(filters.testing === "only" && tank.testing) ||
|
||||
(filters.testing === "exclude" && !tank.testing)) &&
|
||||
((filters.showTesting && tank.testing) ||
|
||||
(filters.showNonTesting && !tank.testing)) &&
|
||||
(filters.gunType.length === 0 ||
|
||||
(filters.gunType.includes("regular") &&
|
||||
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 { Page } from "./_index";
|
||||
|
||||
@@ -7,6 +9,8 @@ export { getStaticPaths } from "../index.astro";
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolOpenGraph {locale} tool={tools.guess} />
|
||||
|
||||
<BaseLayout {locale} noFooter>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
TankSearch,
|
||||
type TankSearchProps,
|
||||
} from "../../../components/TankSearch";
|
||||
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||
import { curateMixer } from "../../../core/blitzkit/curateMixer";
|
||||
import {
|
||||
LocaleProvider,
|
||||
@@ -24,8 +25,13 @@ import {
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../hooks/useLocale";
|
||||
import { Mixer } from "../../../stores/mixer";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
|
||||
const modelDefinition = await awaitableModelDefinitions.then(
|
||||
({ models }) => models[1]
|
||||
);
|
||||
|
||||
export function Page({
|
||||
locale,
|
||||
skeleton,
|
||||
@@ -33,6 +39,7 @@ export function Page({
|
||||
const initial = useRef(curateMixer());
|
||||
|
||||
Mixer.useInitialization(initial.current);
|
||||
Tankopedia.useInitialization(modelDefinition);
|
||||
|
||||
return (
|
||||
<PageWrapper color="gray" p="0" maxWidth="unset">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import { PerformanceInfo } from "../../../components/Performance/PerformanceInfo";
|
||||
import { PlayerCountControl } from "../../../components/Performance/PlayerCountControl";
|
||||
import { TankTable } from "../../../components/Performance/Table";
|
||||
import { FilterControl } from "../../../components/TankSearch/components/FilterControl";
|
||||
@@ -21,8 +20,7 @@ export function Page({ skeleton, locale }: PageProps) {
|
||||
|
||||
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<PageWrapper color="jade" maxWidth="100%">
|
||||
<PerformanceInfo skeleton={skeleton} />
|
||||
<PageWrapper pb="0" px="0" color="jade" maxWidth="100%">
|
||||
<FilterControl />
|
||||
<PlayerCountControl />
|
||||
|
||||
|
||||
@@ -17,9 +17,7 @@ export function Page({
|
||||
}: Props & MaybeSkeletonComponentProps) {
|
||||
const generated = Playlist.use((state) => state.list !== undefined);
|
||||
|
||||
if (App.state.logins.wargaming && TankFilters.state.ownership !== "owned") {
|
||||
TankFilters.state.ownership = "owned";
|
||||
}
|
||||
if (App.state.logins.wargaming) TankFilters.state.showUnowned = false;
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Samples } from '@blitzkit/core';
|
||||
import { Varuna } from 'varuna';
|
||||
import type { Samples } from "@blitzkit/core";
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
export interface Performance {
|
||||
playerCountPeriod: PlayerCountPeriod;
|
||||
@@ -8,5 +8,5 @@ export interface Performance {
|
||||
export type PlayerCountPeriod = keyof Samples;
|
||||
|
||||
export const Performance = new Varuna<Performance>({
|
||||
playerCountPeriod: 'd_30',
|
||||
playerCountPeriod: "total",
|
||||
});
|
||||
@@ -17,12 +17,15 @@ export interface TankFilters {
|
||||
nations: string[];
|
||||
classes: TankClass[];
|
||||
types: TankType[];
|
||||
testing: "include" | "exclude" | "only";
|
||||
search: string | null;
|
||||
searching: boolean;
|
||||
gunType: CaseType<GunDefinition>[];
|
||||
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>({
|
||||
@@ -30,10 +33,13 @@ export const TankFilters = new Varuna<TankFilters>({
|
||||
nations: [],
|
||||
classes: [],
|
||||
types: [],
|
||||
testing: "include",
|
||||
search: null,
|
||||
searching: false,
|
||||
gunType: [],
|
||||
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];
|
||||
customShell?: ShellDefinition;
|
||||
requestedDisplay: TankopediaDisplay;
|
||||
display: TankopediaDisplay;
|
||||
statSearch?: string;
|
||||
}
|
||||
@@ -114,5 +115,6 @@ export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
||||
skills: createDefaultSkills(skillDefinitions),
|
||||
model,
|
||||
xpMultiplier: 1,
|
||||
requestedDisplay: TankopediaDisplay.Model,
|
||||
display: TankopediaDisplay.Model,
|
||||
}));
|
||||
@@ -15,6 +15,7 @@ export interface TankopediaPersistent {
|
||||
showPrimaryArmor: boolean;
|
||||
recentlyViewed: number[];
|
||||
hideTankModelUnderArmor: boolean;
|
||||
highGraphics: boolean;
|
||||
|
||||
sort: {
|
||||
by: TankopediaSortBy;
|
||||
@@ -33,6 +34,7 @@ export const TankopediaPersistent = new Varuna<TankopediaPersistent>(
|
||||
showExternalModules: true,
|
||||
showPrimaryArmor: true,
|
||||
recentlyViewed: [],
|
||||
highGraphics: true,
|
||||
sort: {
|
||||
by: "meta.none",
|
||||
direction: "ascending",
|
||||
|
||||
Reference in new issue
Block a user