Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d041b353c9 | ||
|
|
77575168a0 | ||
|
|
051997d2d3 | ||
|
|
e53bdd8e19 | ||
|
|
4013192faa | ||
|
|
3da009f859 | ||
|
|
2a5888c033 | ||
|
|
ded98d78d4 | ||
|
|
f4b231de10 | ||
|
|
9fda3c6861 | ||
|
|
d3f58d9cda | ||
|
|
3ef793a776 | ||
|
|
13a901c839 | ||
|
|
5ede2324b3 | ||
|
|
4e8f1a4a71 | ||
|
|
eae9cbf971 | ||
|
|
0eac25f1c8 | ||
|
|
c281e9b66f | ||
|
|
eb7fc62e00 | ||
|
|
d5f83c444e | ||
|
|
c890bfa8bd | ||
|
|
9ead3eb627 | ||
|
|
b8db78c328 | ||
|
|
8b65b94851 | ||
|
|
9da723ed31 | ||
|
|
2f48dbc25c | ||
|
|
730d6f2bb8 | ||
|
|
335ae4493f | ||
|
|
eb83039967 | ||
|
|
6e91810565 | ||
|
|
a55c259205 | ||
|
|
b5188331bd | ||
|
|
2b354770c7 | ||
|
|
11e4009303 | ||
|
|
9bce5e5d75 | ||
|
|
8e8d68a5b5 | ||
|
|
2c6fcdf21f | ||
|
|
abb72fb674 | ||
|
|
b805c2e3b4 | ||
|
|
7e6aa41ebb | ||
|
|
a2cfbda932 | ||
|
|
0b222be6ef | ||
|
|
1548813a21 | ||
|
|
c615284d87 | ||
|
|
6a48638171 | ||
|
|
0b4829abd3 | ||
|
|
57cd3aae50 | ||
|
|
6298d6230f | ||
|
|
df7a5bb8a0 | ||
|
|
dd41cac3ac | ||
|
|
f17b275aa7 | ||
|
|
4a04f8a8f3 | ||
|
|
0f7d682782 | ||
|
|
14cecdebe3 | ||
|
|
c2b936c925 | ||
|
|
0fd17d9c96 | ||
|
|
b0645c39a0 | ||
|
|
1d947399ab | ||
|
|
659dddbe8e | ||
|
|
a5c1223f7d | ||
|
|
dc084f8d34 | ||
|
|
44b5907435 | ||
|
|
052fbec628 | ||
|
|
23d8c54366 | ||
|
|
dd88d42884 | ||
|
|
ed676f73b2 | ||
|
|
adb0a9af64 | ||
|
|
ccded1b2f8 | ||
|
|
89d43b4791 | ||
|
|
2226ac9852 | ||
|
|
29bace875e | ||
|
|
04107c10b7 | ||
|
|
0a1e823ce4 | ||
|
|
c656d31618 | ||
|
|
7eda4825ea | ||
|
|
60f0eb9a3c | ||
|
|
ce5c9f4afa | ||
|
|
e834d31d92 | ||
|
|
b679393b8a | ||
|
|
eb5f79228b | ||
|
|
6082576b60 | ||
|
|
92e9073939 | ||
|
|
d6669a84d6 | ||
|
|
0c4debe327 | ||
|
|
9f41bab28a | ||
|
|
b906b42998 | ||
|
|
728a11efa4 | ||
|
|
c35916c291 | ||
|
|
2fd2e93f55 | ||
|
|
b566d5982c | ||
|
|
bef43e9ae6 | ||
|
|
e6404d5c77 | ||
|
|
f4d58015f5 | ||
|
|
cd77d24dc6 | ||
|
|
e25a3cd868 | ||
|
|
68923fe800 | ||
|
|
ccc6ddd5c9 | ||
|
|
1c28b33da3 | ||
|
|
900337f3ce | ||
|
|
fae9f8bded | ||
|
|
a0d78f89ff | ||
|
|
6853d63bbb | ||
|
|
2b27954a3b | ||
|
|
7aa0b65d1d | ||
|
|
3e7a73c528 | ||
|
|
076785a9ad | ||
|
|
74828e3c45 | ||
|
|
310640c1ec | ||
|
|
c49c56fba3 | ||
|
|
cc744f6154 | ||
|
|
93bf4a4742 | ||
|
|
badeb588dd | ||
|
|
5876bf35b4 | ||
|
|
73903114c9 | ||
|
|
b29f7ca7f5 | ||
|
|
9d715c23e5 | ||
|
|
4ff5f7cdeb | ||
|
|
dccfe3d652 | ||
|
|
d780d32291 | ||
|
|
ebbdbc53a4 | ||
|
|
9bc1635680 | ||
|
|
d7d19a0b50 | ||
|
|
1b6845abf8 | ||
|
|
545ee70cd0 | ||
|
|
c92729d0e0 | ||
|
|
71c3b5d2ef | ||
|
|
7372075ba3 | ||
|
|
f119b06dd8 | ||
|
|
e23a440508 | ||
|
|
6a9fa08500 | ||
|
|
8e86e81e91 | ||
|
|
8b359e220a | ||
|
|
690bc5dbb5 | ||
|
|
d6e96e17ad | ||
|
|
ab26b346e6 | ||
|
|
f02db6ef0b | ||
|
|
55ba2525d1 | ||
|
|
ea07d2646d | ||
|
|
ca111510f6 | ||
|
|
519558dd28 | ||
|
|
c4d1e305f6 | ||
|
|
59772afb04 | ||
|
|
8c9dcfa551 | ||
|
|
8bbf03a128 | ||
|
|
519fba84dc | ||
|
|
e7e74f706e | ||
|
|
12e2793c92 | ||
|
|
69040dc453 | ||
|
|
511151e101 | ||
|
|
7fc993c2eb | ||
|
|
030ca744e9 | ||
|
|
74756ab7bf | ||
|
|
e9a61d579d | ||
|
|
7c3cad2c7d | ||
|
|
5f39ba55f6 | ||
|
|
2bfd8286a4 | ||
|
|
f3421b6f70 | ||
|
|
abbbf11b3d | ||
|
|
797f5a90d2 | ||
|
|
818d13fade | ||
|
|
129d7c9688 | ||
|
|
28407b0f05 | ||
|
|
8a9d72d0e6 | ||
|
|
f98a4c7caa | ||
|
|
92b329a0fb | ||
|
|
f3d32a4e63 | ||
|
|
0e73d79c9c | ||
|
|
526b5785b7 | ||
|
|
02129feb25 | ||
|
|
0a38c164a4 | ||
|
|
2e0d52e30e | ||
|
|
6d48f9fe7f | ||
|
|
aba52c9968 | ||
|
|
b96b9fd574 | ||
|
|
fe4ee8b611 | ||
|
|
94558393d9 | ||
|
|
db1e4af20c | ||
|
|
be20173acd | ||
|
|
d90a70ed2a | ||
|
|
85d699dbfc | ||
|
|
5d0fe8726b | ||
|
|
850308b2ac | ||
|
|
88a85ef152 | ||
|
|
8160935081 | ||
|
|
c057e295d2 | ||
|
|
28d0c26268 | ||
|
|
52cb73a575 | ||
|
|
9f244452da | ||
|
|
d46d948c66 | ||
|
|
11b99ef211 | ||
|
|
85d350a912 | ||
|
|
145bc84fb1 | ||
|
|
b7486022eb | ||
|
|
eadb4bb7b1 | ||
|
|
79723157d2 | ||
|
|
4214ff0de5 | ||
|
|
f516c9275e | ||
|
|
f0b905593c | ||
|
|
11440bc654 | ||
|
|
2dab957a05 | ||
|
|
60f3951e7a | ||
|
|
4f03788f15 | ||
|
|
17f4435a30 | ||
|
|
04e42c929c | ||
|
|
4ba45b8fd8 | ||
|
|
0ece1f4b18 | ||
|
|
49d3e1d1c2 | ||
|
|
184ca522dd | ||
|
|
9f07d910b8 | ||
|
|
ac3f2ea93a | ||
|
|
a77ff1a638 | ||
|
|
96d802a09f | ||
|
|
1babd66649 | ||
|
|
2f2996a40c | ||
|
|
9d1e059ea5 | ||
|
|
48603b918f | ||
|
|
c1253e8121 | ||
|
|
ecc029402e | ||
|
|
1b88773e28 | ||
|
|
cf419a9ebb | ||
|
|
8565d75747 | ||
|
|
186ea8b458 | ||
|
|
4292081df6 | ||
|
|
4b54b61c35 | ||
|
|
5d098c46fa | ||
|
|
7575f4581c | ||
|
|
4669fa6ec5 | ||
|
|
49294d1906 | ||
|
|
3eee946978 | ||
|
|
7fff1c9737 | ||
|
|
5b4c3119dd | ||
|
|
0a41ee15ed | ||
|
|
ad57d284bb | ||
|
|
b929e9449d | ||
|
|
948ea0bb8d | ||
|
|
c1b1b8d304 | ||
|
|
3bc5a6d740 | ||
|
|
d25da724e1 | ||
|
|
753804b8cf | ||
|
|
096122d43c | ||
|
|
3448427de0 | ||
|
|
d8bbd02715 | ||
|
|
f696c5f3c9 | ||
|
|
bd412cdaa2 | ||
|
|
913411275b | ||
|
|
17ff8e0819 | ||
|
|
b5fdc4d5cf | ||
|
|
59c5d95aff | ||
|
|
16babbad90 | ||
|
|
a9dc9e7ebd | ||
|
|
52e8df560e | ||
|
|
87259ad515 | ||
|
|
c96655a6c0 | ||
|
|
43894567b8 | ||
|
|
76a952a8e6 | ||
|
|
e2efe428a2 | ||
|
|
887cec2dd4 | ||
|
|
04ad48bd33 | ||
|
|
3613f5d13b | ||
|
|
3362e3b091 | ||
|
|
3322fa97d4 | ||
|
|
1dc7748a04 | ||
|
|
6176b3b0d8 | ||
|
|
81c2af08c6 | ||
|
|
2d83519f93 | ||
|
|
869c1f0def | ||
|
|
7a64fb8176 | ||
|
|
9256244286 | ||
|
|
85e54e4a07 | ||
|
|
8075dc8d9f | ||
|
|
57dced109a | ||
|
|
dd19d5b979 | ||
|
|
68d3fe6805 | ||
|
|
70f7698466 | ||
|
|
26a5fdea0e | ||
|
|
367bd1386b | ||
|
|
fac773a8f8 | ||
|
|
6bd8b7d42f | ||
|
|
7093662130 |
No files matched your search
@@ -62,4 +62,6 @@ jobs:
|
|||||||
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
||||||
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
||||||
$env:DATABASE_URL = "${{ secrets.DATABASE_URL }}"
|
$env:DATABASE_URL = "${{ secrets.DATABASE_URL }}"
|
||||||
|
$env:VIDEOS_SHEET = "${{ secrets.VIDEOS_SHEET }}"
|
||||||
|
$env:GOOGLE_APPLICATION_CREDENTIALS = "${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}"
|
||||||
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
|
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download release assets
|
- name: Download release assets
|
||||||
run: |
|
run: |
|
||||||
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
|
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkit/releases/latest \
|
||||||
| grep "browser_download_url.*bot.zip" \
|
| grep "browser_download_url.*bot.zip" \
|
||||||
| cut -d : -f 2,3 \
|
| cut -d : -f 2,3 \
|
||||||
| tr -d \")
|
| tr -d \")
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
|
- name: Build Prisma
|
||||||
|
run: yarn prisma generate
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build:bot --production
|
run: yarn build:bot --production
|
||||||
|
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ temp/
|
|||||||
/.VSCodeCounter
|
/.VSCodeCounter
|
||||||
/.yarn
|
/.yarn
|
||||||
depots
|
depots
|
||||||
public/sitemap*.xml
|
public/sitemap*
|
||||||
public/robots.txt
|
/firebase.json
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[submodule "submodules/closedkrieg"]
|
[submodule "submodules/blitzkit-closed"]
|
||||||
path = submodules/closedkrieg
|
path = submodules/blitzkit-closed
|
||||||
url = https://github.com/tresabhi/closedkrieg
|
url = https://github.com/tresabhi/blitzkit-closed
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"AABBOX",
|
"AABBOX",
|
||||||
"Abhi",
|
"Abhi",
|
||||||
"accountinfo",
|
"accountinfo",
|
||||||
|
"adsense",
|
||||||
"aeson",
|
"aeson",
|
||||||
"alphatest",
|
"alphatest",
|
||||||
"APAC",
|
"APAC",
|
||||||
@@ -17,7 +18,11 @@
|
|||||||
"BINORMAL",
|
"BINORMAL",
|
||||||
"Binormals",
|
"Binormals",
|
||||||
"BKRL",
|
"BKRL",
|
||||||
|
"BKRTS",
|
||||||
|
"BKRTSMANIFEST",
|
||||||
"bkwd",
|
"bkwd",
|
||||||
|
"blitzkit",
|
||||||
|
"blitzrinth",
|
||||||
"blitzstars",
|
"blitzstars",
|
||||||
"Camo",
|
"Camo",
|
||||||
"CDON",
|
"CDON",
|
||||||
@@ -72,12 +77,15 @@
|
|||||||
"leaderboards",
|
"leaderboards",
|
||||||
"lerp",
|
"lerp",
|
||||||
"LINEARSIZE",
|
"LINEARSIZE",
|
||||||
|
"loadouts",
|
||||||
"loddist",
|
"loddist",
|
||||||
"lods",
|
"lods",
|
||||||
|
"mantlets",
|
||||||
"metallicness",
|
"metallicness",
|
||||||
"minitelrose",
|
"minitelrose",
|
||||||
"Minotauro",
|
"Minotauro",
|
||||||
"MIPMAPCOUNT",
|
"MIPMAPCOUNT",
|
||||||
|
"mišek",
|
||||||
"modeltransform",
|
"modeltransform",
|
||||||
"MURICA",
|
"MURICA",
|
||||||
"NEGATIVEX",
|
"NEGATIVEX",
|
||||||
@@ -104,8 +112,12 @@
|
|||||||
"raycaster",
|
"raycaster",
|
||||||
"researchable",
|
"researchable",
|
||||||
"Resvg",
|
"Resvg",
|
||||||
|
"rizz",
|
||||||
"roboto",
|
"roboto",
|
||||||
|
"rtsc",
|
||||||
|
"RTSM",
|
||||||
"SCPG",
|
"SCPG",
|
||||||
|
"scroller",
|
||||||
"searchclans",
|
"searchclans",
|
||||||
"searchplayers",
|
"searchplayers",
|
||||||
"searchtanks",
|
"searchtanks",
|
||||||
@@ -147,12 +159,14 @@
|
|||||||
"whitespaces",
|
"whitespaces",
|
||||||
"winrate",
|
"winrate",
|
||||||
"wireframe",
|
"wireframe",
|
||||||
|
"Woah",
|
||||||
"WOTB",
|
"WOTB",
|
||||||
"wotblitz",
|
"wotblitz",
|
||||||
"xffn",
|
"xffn",
|
||||||
"xkdkw",
|
"xkdkw",
|
||||||
"XMLDVPL",
|
"XMLDVPL",
|
||||||
"YAMLDVPL",
|
"YAMLDVPL",
|
||||||
|
"youtubers",
|
||||||
"zustand"
|
"zustand"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Blitzkrieg
|
# BlitzKit
|
||||||
|
|
||||||
[](https://codeium.com)
|
[](https://codeium.com)
|
||||||
|
|
||||||
@@ -6,18 +6,18 @@ Tools for everything World of Tanks Blitz
|
|||||||
|
|
||||||
## Invite the bot
|
## Invite the bot
|
||||||
|
|
||||||
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Visit the website
|
## Visit the website
|
||||||
|
|
||||||
### https://blitz-krieg.vercel.app/
|
### https://blitzkit.vercel.app/
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Visit the documentation website
|
## Visit the documentation website
|
||||||
|
|
||||||
### https://tresabhi.github.io/blitzkrieg/
|
### https://tresabhi.github.io/blitzkit/
|
||||||
|
|
||||||

|

|
||||||
@@ -5,9 +5,9 @@ import { defineConfig } from 'vitepress';
|
|||||||
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
|
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
title: 'blitzkrieg',
|
title: 'BlitzKit',
|
||||||
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
|
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
|
||||||
base: '/blitzkrieg/',
|
base: '/blitzkit/',
|
||||||
outDir: '../dist/docs',
|
outDir: '../dist/docs',
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
@@ -33,7 +33,7 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
|
|
||||||
socialLinks: [
|
socialLinks: [
|
||||||
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' },
|
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkit' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Blitzkrieg 1.19.0
|
||||||
|
|
||||||
|
Introducing the Blitzkrieg Compare tool!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added Compare
|
||||||
|
- Allows you to compare multiple tanks
|
||||||
|
- Shows deltas in the form of differences between values or percentages
|
||||||
|
- All stats from the Tankopedia are available here
|
||||||
|
- [CLS](https://web.dev/articles/cls) was reduced due to the new loading screens
|
||||||
|
- Clicking enter when searching for tanks now selects the first result
|
||||||
|
- The first tank name is also displayed in the search input
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Removed the donation banner instead for a little note in the footer
|
||||||
|
- The footer now shows the version of Blitzkrieg and the version of WoTB its running on
|
||||||
|
- Removed frosted glass effect from most components apart from the navigation bar for performance reasons (I am looking at you, Apple!)
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Tank search is now not ignored when changing filters
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Blitzkrieg 1.19.1
|
||||||
|
|
||||||
|
A minor-major fix to the armor sandbox. This update only affects the website.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Guns are now clipped properly allowing some tanks like the 252U and T34's mantlets to be penetrable (usually with premium shells) as they are in-game
|
||||||
|
|
||||||
|
| Before | After |
|
||||||
|
| ------------------------------------------ | ----------------------------------------- |
|
||||||
|
|  |  |
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Blitzkrieg 1.19.2
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Maximum DPM for double shot auto-reloaders is now calculate correctly with intra-clip in mind
|
||||||
|
- Blitzkrieg logo is now removed on lower resolutions to allow for other buttons to fit in the title bar of the Tankopedia
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Blitzkrieg 1.19.3
|
||||||
|
|
||||||
|
Google rizz. This version only affects the website.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- The titles and descriptions of the Tankopedia tank pages are now more informative
|
||||||
|
- Added a settings page
|
||||||
|
- Added a hamburger menu to the navigation bar
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Tank icons in URL previews are now presented properly
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Blitzkrieg 1.19.4
|
||||||
|
|
||||||
|
I genuinely have no idea how these bugs sneaked in here. This update only affects the website.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added a button in Compare which links to the corresponding Tankopedia page
|
||||||
|
- Added a button in Tankopedia that links to the corresponding Compare page
|
||||||
|
- If the two tanks (the shooter and the one of which armor is displayed) in the Tankopedia are different, they will be both added by default in the Compare page
|
||||||
|
- Simplified the footer and added a link to my website and the credits page
|
||||||
|
- Added an aggressive donate button right at the top of the home page
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
Thanks to mišek for these bug reports!
|
||||||
|
|
||||||
|
- The intra-clip time is now displayed for auto-loaders
|
||||||
|
- High End Consumables now increase the duration of consumables correctly instead of decreasing it
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Blitzkrieg 1.19.5
|
||||||
|
|
||||||
|
The tankopedia, now more helpful than ever! This update only affects the website.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- The Tankopedia pages for tanks now include videos from YouTubers
|
||||||
|
- The selection of tanks that have videos is very small currently but you can help expand it by dropping suggestions on [the official Discord server](https://discord.gg/nDt7AjGJQH)
|
||||||
|
- Added shell normalization and ricochet angles to the Tankopedia and Compare
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Removed undefined clipping planes
|
||||||
|
- Added proper keying to all JSX tree nodes, navbar menu items, antagonist bar shells, skill rows and fragments of tank stats in the Tankopedia
|
||||||
|
- The second half of barrels for tanks like the T-100 LT are now properly included
|
||||||
|
|
||||||
|
## Technical Features
|
||||||
|
|
||||||
|
- The list of videos is fetched transpiled from a Google Sheets spreadsheet into a CSV file in the asset repository
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# BlitzKit 1.19.6
|
||||||
|
|
||||||
|
Renamed Blitzkrieg to BlitzKit.
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# Blitzkrieg Rating Leaderboard
|
# Blitzkit Rating Leaderboard
|
||||||
|
|
||||||
A straightforward way of storing the entire World of Tanks Blitz rating leaderboard to disk.
|
A straightforward way of storing the entire World of Tanks Blitz rating leaderboard to disk.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
primary BlitzkriegRatingLeaderboard {
|
primary BlitzkitRatingLeaderboard {
|
||||||
utf8"BKRL" magic;
|
utf8"BKRL" magic;
|
||||||
Header header;
|
Header header;
|
||||||
primary Body<header> body;
|
primary Body<header> body;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
# Team
|
# Team
|
||||||
|
|
||||||
- TrèsAbhi: creator & programmer
|
- TrèsAbhi: creator & programmer
|
||||||
- Prince_NA: game advisor & tester
|
|
||||||
|
|
||||||
# Contributors
|
# Contributors
|
||||||
|
|
||||||
- S0AP: bot verification process & initial advertizement platform
|
- Prince_NA: general game advisor and tester
|
||||||
|
- S0AP: bot verification and initial advertizement platform
|
||||||
- Vovko: discord bot rendering pipeline
|
- Vovko: discord bot rendering pipeline
|
||||||
- Maddox: game file decompression
|
- Maddox: game file decompression
|
||||||
- Pyogenics: tank model decompression
|
- Pyogenics: tank model parsing
|
||||||
- Aeson: game files layout
|
- Aeson: game files layout
|
||||||
- Minitelrose, ChickenMan7777: 3-caliber rule edge cases
|
- Minitelrose, ChickenMan7777: 3-caliber rule edge cases
|
||||||
- \_34, HisRoyalFatness, Synx, xkdkw: embed UX
|
- \_34, HisRoyalFatness, Synx, xkdkw: embed UX
|
||||||
- [OPTML], Aeson, \_34, Droodles: beta testing tankopedia
|
- [OPTML], Aeson, \_34, Droodles: beta testing Tankopedia
|
||||||
|
- [OPTML], Droodles: beta testing Compare
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Bot
|
## Bot
|
||||||
|
|
||||||
Blitzkrieg is available as a Discord bot and can be [installed very easily](/guide/invite).
|
BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite).
|
||||||
|
|
||||||
- Track statistics
|
- Track statistics
|
||||||
- Find inactive players in a clan
|
- Find inactive players in a clan
|
||||||
@@ -15,7 +15,7 @@ Blitzkrieg is available as a Discord bot and can be [installed very easily](/gui
|
|||||||
|
|
||||||
## Website
|
## Website
|
||||||
|
|
||||||
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
|
Provides a bunch of very helpful tools; https://blitzkit.vercel.app/
|
||||||
|
|
||||||
- Session tracker
|
- Session tracker
|
||||||
- Rating leaderboard
|
- Rating leaderboard
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Inviting the Bot
|
# Inviting the Bot
|
||||||
|
|
||||||
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# BlitzKit Regular Tank Stats
|
||||||
|
|
||||||
|
A straightforward way of storing the entire World of Tanks Blitz tank statistics for players to disk.
|
||||||
|
|
||||||
|
## Manifest `manifest.rtsm`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
primary RegularTankStatisticsManifest {
|
||||||
|
utf8"RTSM" magic;
|
||||||
|
Header header;
|
||||||
|
primary Body body;
|
||||||
|
}
|
||||||
|
|
||||||
|
Header {
|
||||||
|
uint16 version; // always 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Body {
|
||||||
|
uint32 time;
|
||||||
|
uint8 base;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk `*.rtsc`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
primary RegularTankStatisticsChunk {
|
||||||
|
utf8"RTSC" magic;
|
||||||
|
Header header;
|
||||||
|
primary Body body<header.playerCount>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Header {
|
||||||
|
uint16 version; // always 1
|
||||||
|
uint32 playerCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
Body<uint32 playerCount> {
|
||||||
|
Player[playerCount] players;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player {
|
||||||
|
uint32 id;
|
||||||
|
uint16 tankCount;
|
||||||
|
Tank[tankCount] tanks;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tank {
|
||||||
|
uint32 id;
|
||||||
|
uint32 battleLifeTime;
|
||||||
|
uint32 lastBattleTime;
|
||||||
|
uint8 markOfMastery;
|
||||||
|
uint32 battles;
|
||||||
|
uint32 capturePoints;
|
||||||
|
uint32 damageDealt;
|
||||||
|
uint32 damageReceived;
|
||||||
|
uint32 droppedCapturePoints;
|
||||||
|
uint32 frags;
|
||||||
|
uint32 frags8p;
|
||||||
|
uint32 hits;
|
||||||
|
uint32 losses;
|
||||||
|
uint8 maxFrags;
|
||||||
|
uint16 maxXp;
|
||||||
|
uint32 shots;
|
||||||
|
uint32 spotted;
|
||||||
|
uint32 survivedBattles;
|
||||||
|
uint32 winAndSurvived;
|
||||||
|
uint32 wins;
|
||||||
|
uint32 xp;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
layout: home
|
layout: home
|
||||||
|
|
||||||
hero:
|
hero:
|
||||||
name: 'blitzkrieg'
|
name: 'BlitzKit'
|
||||||
text: Everything Blitz, right in Discord
|
text: Everything Blitz, right in Discord
|
||||||
tagline: All-in-one Discord bot for everything World of Tanks Blitz
|
tagline: All-in-one Discord bot for everything World of Tanks Blitz
|
||||||
image:
|
image:
|
||||||
src: '/assets/images/hero.png'
|
src: '/assets/images/hero.png'
|
||||||
alt: 'blitzkrieg'
|
alt: 'blitzkit'
|
||||||
actions:
|
actions:
|
||||||
- theme: brand
|
- theme: brand
|
||||||
text: Add to Discord Server
|
text: Add to Discord Server
|
||||||
@@ -20,11 +20,11 @@ hero:
|
|||||||
features:
|
features:
|
||||||
- title: Clean and Efficient UI
|
- title: Clean and Efficient UI
|
||||||
icon: 🚀
|
icon: 🚀
|
||||||
details: Blitzkrieg is where concise meets efficient. Get the information you need in the way you want.
|
details: BlitzKit is where concise meets efficient. Get the information you need in the way you want.
|
||||||
- title: Endless Possibilities
|
- title: Endless Possibilities
|
||||||
icon: 🛠
|
icon: 🛠
|
||||||
details: With endless ways to customize the output, it's guaranteed that you'll always get the information you need.
|
details: With endless ways to customize the output, it's guaranteed that you'll always get the information you need.
|
||||||
- title: Easy to Use
|
- title: Easy to Use
|
||||||
icon: 💻
|
icon: 💻
|
||||||
details: Leveraging Discord's, Blitz Stars', and Wargaming's powerful APIS, Blitzkrieg does all the hard work for you.
|
details: Leveraging Discord's, Blitz Stars', and Wargaming's powerful APIS, Blitzkit does all the hard work for you.
|
||||||
---
|
---
|
||||||
@@ -1,22 +1,18 @@
|
|||||||
# Blitzkrieg Privacy Policy
|
# BlitzKit Privacy Policy
|
||||||
|
|
||||||
Blitzkrieg collects little to no data. Regardless, here is an exhaustive list of Blitzkrieg's data collection practices.
|
BlitzKit collects little to no data. Regardless, here is an exhaustive list of BlitzKit's data collection practices.
|
||||||
|
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
All Blitzkrieg logs are available to the public as an effort for open source transparency. A history of all logs [can be found on GitHub](https://github.com/tresabhi/blitzkrieg/actions/workflows/host.yml). All logs expire automatically and are permanently deleted after 30 days. However, anyone may chose to save a local copy of the logs which is not tracked by GitHub.
|
All BlitzKit logs are available to the public as an effort for open source transparency. A history of all logs [can be found on GitHub](https://github.com/tresabhi/blitzkit/actions/workflows/host.yml). All logs expire automatically and are permanently deleted after 30 days. However, anyone may chose to save a local copy of the logs which is not tracked by GitHub.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
Everytime a command a command is used, the command name and parameters provided is logged. Any identifying information like manually provided in-game usernames are also recorded.
|
Everytime a command a command is used, the command name and parameters provided is logged. Any identifying information like manually provided in-game usernames are also recorded.
|
||||||
|
|
||||||
## Errors
|
|
||||||
|
|
||||||
Everytime an error occurs, a detailed stack trace, scope of the error, and the error message is logged both in the GitHub logs and in the public channel [#🔲︱blitzkrieg-logs](https://discord.com/channels/734786591205359697/880650462540476416) in [the official Discord server](https://discord.gg/nDt7AjGJQH).
|
|
||||||
|
|
||||||
## Databases
|
## Databases
|
||||||
|
|
||||||
No databases are used by Blitzkrieg meaning that no data is collected outside of what has already been mentioned above. This is no permanent state.
|
No databases are used by BlitzKit meaning that no data is collected outside of what has already been mentioned above. This is no permanent state.
|
||||||
|
|
||||||
## 3rd Party
|
## 3rd Party
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Blitzkrieg Terms of Service
|
# BlitzKit Terms of Service
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
Blitzkrieg is a multi-function Discord bot that serves users with needs specific to the game World of Tanks Blitz.
|
BlitzKit is a multi-function Discord bot that serves users with needs specific to the game World of Tanks Blitz.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Blitzkrieg is licensed under the MIT License.
|
BlitzKit is licensed under the MIT License.
|
||||||
|
|
||||||
## 3rd Party
|
## 3rd Party
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 35 KiB |
@@ -1,6 +0,0 @@
|
|||||||
/** @type {import('next-sitemap').IConfig} */
|
|
||||||
module.exports = {
|
|
||||||
siteUrl: process.env.SITE_URL || 'https://blitz-krieg.vercel.app',
|
|
||||||
generateRobotsTxt: true,
|
|
||||||
sourceDir: 'dist/website',
|
|
||||||
};
|
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkit",
|
||||||
"version": "1.18.3",
|
"version": "1.19.6",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build:db": "prisma generate",
|
||||||
"build:bot": "vite-node --script scripts/build --production",
|
"build:bot": "vite-node --script scripts/build --production",
|
||||||
"build:website": "next build && next-sitemap --config next-sitemap.config.cjs",
|
"build:website": "yarn build:db && yarn build:sitemap && next build",
|
||||||
|
"build:sitemap": "vite-node --script scripts/sitemap",
|
||||||
"build:docs": "vitepress build docs",
|
"build:docs": "vitepress build docs",
|
||||||
"build:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard",
|
"build:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard",
|
||||||
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
|
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
|
||||||
"build:test": "vite-node --script scripts/buildTest",
|
"test": "vite-node --script scripts/test",
|
||||||
"dev:bot": "vite-node --script scripts/build && yarn start:bot",
|
"dev:bot": "vite-node --script scripts/build && yarn start:bot",
|
||||||
"dev:docs": "vitepress dev docs",
|
"dev:docs": "vitepress dev docs",
|
||||||
"dev:website": "next dev",
|
"dev:website": "next dev",
|
||||||
@@ -50,13 +52,13 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"false": "^0.0.4",
|
"false": "^0.0.4",
|
||||||
"fuzzysort": "^2.0.4",
|
"fuzzysort": "^2.0.4",
|
||||||
|
"googleapis": "^134.0.0",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lz4js": "^0.2.0",
|
"lz4js": "^0.2.0",
|
||||||
"markdown-escape": "^2.0.0",
|
"markdown-escape": "^2.0.0",
|
||||||
"next": "^14.1.0",
|
"next": "^14.1.0",
|
||||||
"next-sitemap": "^4.2.3",
|
|
||||||
"parse-dds": "^1.2.1",
|
"parse-dds": "^1.2.1",
|
||||||
"postprocessing": "^6.34.1",
|
"postprocessing": "^6.34.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -64,7 +66,6 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-fuzzysort": "^1.0.1",
|
"react-fuzzysort": "^1.0.1",
|
||||||
"react-promise-suspense": "^0.3.4",
|
"react-promise-suspense": "^0.3.4",
|
||||||
"react-spinners": "^0.13.8",
|
|
||||||
"satori": "^0.10.13",
|
"satori": "^0.10.13",
|
||||||
"sharp": "0.33.1",
|
"sharp": "0.33.1",
|
||||||
"three": "0.159.0",
|
"three": "0.159.0",
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
"@types/js-yaml": "^4.0.8",
|
"@types/js-yaml": "^4.0.8",
|
||||||
"@types/lodash": "^4.14.199",
|
"@types/lodash": "^4.14.199",
|
||||||
"@types/lz4": "^0.6.4",
|
"@types/lz4": "^0.6.4",
|
||||||
|
"@types/lz4js": "^0.2.1",
|
||||||
"@types/markdown-escape": "^1.1.0",
|
"@types/markdown-escape": "^1.1.0",
|
||||||
"@types/node": "^20.6.5",
|
"@types/node": "^20.6.5",
|
||||||
"@types/parse-dds": "^1.0.3",
|
"@types/parse-dds": "^1.0.3",
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
google.com, pub-1289406790225052, DIRECT, f08c47fec0942fa0
|
google.com, pub-1289406790225052, DIRECT, f08c47fec0942fa0
|
||||||
|
appnexus.com, 14163, RESELLER
|
||||||
|
pubcenter.microsoft.com, 253617120, DIRECT
|
||||||
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 921 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Host: https://blitzkit.vercel.app
|
||||||
|
|
||||||
|
Sitemap: https://blitzkit.vercel.app/sitemap.txt
|
||||||
|
Sitemap: https://blitzkit.vercel.app/sitemap.xml
|
||||||
@@ -3,7 +3,7 @@ import { parse as parsePath } from 'path';
|
|||||||
import { parse as parseYaml } from 'yaml';
|
import { parse as parseYaml } from 'yaml';
|
||||||
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
|
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
|
||||||
import { writeDVPL } from '../src/core/blitz/writeDVPL';
|
import { writeDVPL } from '../src/core/blitz/writeDVPL';
|
||||||
import { secrets } from '../src/core/blitzkrieg/secrets';
|
import { secrets } from '../src/core/blitzkit/secrets';
|
||||||
import { dvp } from '../submodules/closedkrieg/src/dvp';
|
import { dvp } from '../submodules/closedkrieg/src/dvp';
|
||||||
import { DATA } from './buildAssets/constants';
|
import { DATA } from './buildAssets/constants';
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { readdir } from 'fs/promises';
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
|
|
||||||
export async function boosterIcons(production: boolean) {
|
export async function boosterIcons(production: boolean) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
|
|
||||||
export async function camouflageIcons(production: boolean) {
|
export async function camouflageIcons(production: boolean) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readdir } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
|
|
||||||
export async function circleFlags(production: boolean) {
|
export async function circleFlags(production: boolean) {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
|||||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
import { ConsumablesCommon, ProvisionsCommon } from './definitions';
|
import { ConsumablesCommon, ProvisionsCommon } from './definitions';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA } from './constants';
|
import { DATA } from './constants';
|
||||||
|
|
||||||
const ICONS = [
|
const ICONS = [
|
||||||
|
|||||||
@@ -3,26 +3,28 @@ import { parse as parsePath } from 'path';
|
|||||||
import { Vector3Tuple } from 'three';
|
import { Vector3Tuple } from 'three';
|
||||||
import { parse as parseYaml } from 'yaml';
|
import { parse as parseYaml } from 'yaml';
|
||||||
import { TankClass } from '../../src/components/Tanks';
|
import { TankClass } from '../../src/components/Tanks';
|
||||||
|
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||||
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import {
|
import {
|
||||||
ConsumableDefinitions,
|
ConsumableDefinitions,
|
||||||
TankFilterDefinitionCategory,
|
TankFilterDefinitionCategory,
|
||||||
} from '../../src/core/blitzkrieg/consumableDefinitions';
|
} from '../../src/core/blitzkit/consumableDefinitions';
|
||||||
import {
|
import {
|
||||||
EquipmentDefinitions,
|
EquipmentDefinitions,
|
||||||
EquipmentPreset,
|
EquipmentPreset,
|
||||||
EquipmentRow,
|
EquipmentRow,
|
||||||
} from '../../src/core/blitzkrieg/equipmentDefinitions';
|
} from '../../src/core/blitzkit/equipmentDefinitions';
|
||||||
|
import { GameDefinitions } from '../../src/core/blitzkit/gameDefinitions';
|
||||||
import {
|
import {
|
||||||
ModelArmor,
|
ModelArmor,
|
||||||
ModelDefinitions,
|
ModelDefinitions,
|
||||||
} from '../../src/core/blitzkrieg/modelDefinitions';
|
} from '../../src/core/blitzkit/modelDefinitions';
|
||||||
import { ProvisionDefinitions } from '../../src/core/blitzkrieg/provisionDefinitions';
|
import { ProvisionDefinitions } from '../../src/core/blitzkit/provisionDefinitions';
|
||||||
import { SkillDefinitions } from '../../src/core/blitzkrieg/skillDefinitions';
|
import { SkillDefinitions } from '../../src/core/blitzkit/skillDefinitions';
|
||||||
import { superCompress } from '../../src/core/blitzkrieg/superCompress';
|
import { superCompress } from '../../src/core/blitzkit/superCompress';
|
||||||
import {
|
import {
|
||||||
Crew,
|
Crew,
|
||||||
CrewMember,
|
CrewMember,
|
||||||
@@ -31,12 +33,12 @@ import {
|
|||||||
TankDefinitionPrice,
|
TankDefinitionPrice,
|
||||||
TankDefinitions,
|
TankDefinitions,
|
||||||
Tier,
|
Tier,
|
||||||
} from '../../src/core/blitzkrieg/tankDefinitions';
|
} from '../../src/core/blitzkit/tankDefinitions';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
import { Avatar } from './skillIcons';
|
import { Avatar } from './skillIcons';
|
||||||
import { TankParameters } from './tankIcons';
|
import { TankParameters } from './tankIcons';
|
||||||
|
|
||||||
interface Strings {
|
export interface BlitzStrings {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
type ShellKind =
|
type ShellKind =
|
||||||
@@ -323,7 +325,7 @@ type ConsumablesVehicleFilter =
|
|||||||
| { name: string }
|
| { name: string }
|
||||||
| { extendedTags: string };
|
| { extendedTags: string };
|
||||||
|
|
||||||
const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
|
const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
|
||||||
ARMOR_PIERCING: 'ap',
|
ARMOR_PIERCING: 'ap',
|
||||||
ARMOR_PIERCING_CR: 'ap_cr',
|
ARMOR_PIERCING_CR: 'ap_cr',
|
||||||
HIGH_EXPLOSIVE: 'he',
|
HIGH_EXPLOSIVE: 'he',
|
||||||
@@ -332,10 +334,18 @@ const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
|
|||||||
const missingStrings: Record<string, string> = {
|
const missingStrings: Record<string, string> = {
|
||||||
'#artefacts:tungstentip/name': 'Tungsten Shells',
|
'#artefacts:tungstentip/name': 'Tungsten Shells',
|
||||||
};
|
};
|
||||||
|
export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/;
|
||||||
|
|
||||||
export async function definitions(production: boolean) {
|
export async function definitions(production: boolean) {
|
||||||
console.log('Building definitions...');
|
console.log('Building definitions...');
|
||||||
|
|
||||||
|
const gameDefinitions: GameDefinitions = {
|
||||||
|
version: (await readStringDVPL(`${DATA}/version.txt.dvpl`))
|
||||||
|
.split(' ')[0]
|
||||||
|
.split('.')
|
||||||
|
.slice(0, 3)
|
||||||
|
.join('.'),
|
||||||
|
};
|
||||||
const tankDefinitions: TankDefinitions = {};
|
const tankDefinitions: TankDefinitions = {};
|
||||||
const modelDefinitions: ModelDefinitions = {};
|
const modelDefinitions: ModelDefinitions = {};
|
||||||
const equipmentDefinitions: EquipmentDefinitions = {
|
const equipmentDefinitions: EquipmentDefinitions = {
|
||||||
@@ -351,12 +361,12 @@ export async function definitions(production: boolean) {
|
|||||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||||
);
|
);
|
||||||
const tankStringIdMap: Record<string, number> = {};
|
const tankStringIdMap: Record<string, number> = {};
|
||||||
const stringsPreInstalled = await readYAMLDVPL<Strings>(
|
const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
|
||||||
`${DATA}/${POI.strings}/en.yaml.dvpl`,
|
`${DATA}/${POI.strings}/en.yaml.dvpl`,
|
||||||
);
|
);
|
||||||
const stringsCache = await fetch(POI.cachedStrings)
|
const stringsCache = await fetch(POI.cachedStrings)
|
||||||
.then((response) => response.text())
|
.then((response) => response.text())
|
||||||
.then((string) => parseYaml(string) as Strings);
|
.then((string) => parseYaml(string) as BlitzStrings);
|
||||||
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>(
|
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>(
|
||||||
`${DATA}/${POI.optionalDevices}.dvpl`,
|
`${DATA}/${POI.optionalDevices}.dvpl`,
|
||||||
);
|
);
|
||||||
@@ -411,6 +421,8 @@ export async function definitions(production: boolean) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const tankKey in tankList.root) {
|
for (const tankKey in tankList.root) {
|
||||||
|
if (botPattern.test(tankKey)) continue;
|
||||||
|
|
||||||
const gunXps = new Map<number, number>();
|
const gunXps = new Map<number, number>();
|
||||||
const turretXps = new Map<number, number>();
|
const turretXps = new Map<number, number>();
|
||||||
const engineXps = new Map<number, number>();
|
const engineXps = new Map<number, number>();
|
||||||
@@ -727,6 +739,17 @@ export async function definitions(production: boolean) {
|
|||||||
const gunArmor: ModelArmor = { thickness: {} };
|
const gunArmor: ModelArmor = { thickness: {} };
|
||||||
const shotDispersionFactors =
|
const shotDispersionFactors =
|
||||||
gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors;
|
gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors;
|
||||||
|
const maskName = `gun_${gunModel.toString().padStart(2, '0')}`;
|
||||||
|
const maskEnabled =
|
||||||
|
tankParameters.maskSlice?.[maskName]?.enabled ?? false;
|
||||||
|
let mask: number | undefined;
|
||||||
|
|
||||||
|
if (maskEnabled) {
|
||||||
|
const maskRaw = tankParameters.maskSlice![maskName]!;
|
||||||
|
mask = maskRaw.planePosition[1];
|
||||||
|
} else {
|
||||||
|
mask = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
totalUnlocks.push(gun.unlocks);
|
totalUnlocks.push(gun.unlocks);
|
||||||
Object.keys(gun.armor)
|
Object.keys(gun.armor)
|
||||||
@@ -776,6 +799,7 @@ export async function definitions(production: boolean) {
|
|||||||
modelDefinitions[tankId].turrets[turretId].guns[gunId] = {
|
modelDefinitions[tankId].turrets[turretId].guns[gunId] = {
|
||||||
armor: gunArmor,
|
armor: gunArmor,
|
||||||
model: gunModel,
|
model: gunModel,
|
||||||
|
mask,
|
||||||
thickness:
|
thickness:
|
||||||
gun.armor.gun === undefined
|
gun.armor.gun === undefined
|
||||||
? 0
|
? 0
|
||||||
@@ -828,7 +852,7 @@ export async function definitions(production: boolean) {
|
|||||||
caliber: shell.caliber,
|
caliber: shell.caliber,
|
||||||
normalization: shell.normalizationAngle,
|
normalization: shell.normalizationAngle,
|
||||||
ricochet: shell.ricochetAngle,
|
ricochet: shell.ricochetAngle,
|
||||||
type: blitzShellKindToBLitzkrieg[shell.kind],
|
type: blitzShellKindToBlitzkit[shell.kind],
|
||||||
explosionRadius:
|
explosionRadius:
|
||||||
shell.kind === 'HIGH_EXPLOSIVE'
|
shell.kind === 'HIGH_EXPLOSIVE'
|
||||||
? shell.explosionRadius ?? 0
|
? shell.explosionRadius ?? 0
|
||||||
@@ -1101,6 +1125,11 @@ export async function definitions(production: boolean) {
|
|||||||
await commitAssets(
|
await commitAssets(
|
||||||
'definitions',
|
'definitions',
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
content: superCompress(gameDefinitions),
|
||||||
|
encoding: 'base64',
|
||||||
|
path: 'definitions/game.cdon.lz4',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
content: superCompress(tankDefinitions),
|
content: superCompress(tankDefinitions),
|
||||||
encoding: 'base64',
|
encoding: 'base64',
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
|||||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
import { OptionalDevices } from './definitions';
|
import { OptionalDevices } from './definitions';
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,16 @@ import { skillIcons } from './skillIcons';
|
|||||||
import { tankArmors } from './tankArmors';
|
import { tankArmors } from './tankArmors';
|
||||||
import { tankIcons } from './tankIcons';
|
import { tankIcons } from './tankIcons';
|
||||||
import { tankModels } from './tankModels';
|
import { tankModels } from './tankModels';
|
||||||
|
import { videos } from './videos';
|
||||||
|
|
||||||
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
||||||
|
|
||||||
const methods = [
|
const methods = [
|
||||||
definitions,
|
definitions,
|
||||||
tankIcons,
|
videos,
|
||||||
scratchedFlags,
|
scratchedFlags,
|
||||||
|
currencies,
|
||||||
|
skillIcons,
|
||||||
circleFlags,
|
circleFlags,
|
||||||
shellIcons,
|
shellIcons,
|
||||||
moduleIcons,
|
moduleIcons,
|
||||||
@@ -38,10 +41,9 @@ const methods = [
|
|||||||
consumableProvisionIcons,
|
consumableProvisionIcons,
|
||||||
camouflageIcons,
|
camouflageIcons,
|
||||||
boosterIcons,
|
boosterIcons,
|
||||||
|
tankIcons,
|
||||||
tankArmors,
|
tankArmors,
|
||||||
tankModels,
|
tankModels,
|
||||||
currencies,
|
|
||||||
skillIcons,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const method of methods) {
|
for (const method of methods) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { readdir } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
import sharp from 'sharp';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
|
|
||||||
export async function moduleIcons(production: boolean) {
|
export async function moduleIcons(production: boolean) {
|
||||||
@@ -16,9 +17,11 @@ export async function moduleIcons(production: boolean) {
|
|||||||
.map(async (file) => {
|
.map(async (file) => {
|
||||||
const nameRaw = file.match(/vehicle(.+)\.packed\.webp\.dvpl/)![1];
|
const nameRaw = file.match(/vehicle(.+)\.packed\.webp\.dvpl/)![1];
|
||||||
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
|
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
|
||||||
const content = await readBase64DVPL(
|
const content = (
|
||||||
`${DATA}/${POI.moduleIcons}/${file}`,
|
await sharp(await readDVPLFile(`${DATA}/${POI.moduleIcons}/${file}`))
|
||||||
);
|
.trim()
|
||||||
|
.toBuffer()
|
||||||
|
).toString('base64');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readdir } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
|
|
||||||
export async function scratchedFlags(production: boolean) {
|
export async function scratchedFlags(production: boolean) {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { readdir } from 'fs/promises';
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
|
|
||||||
export async function shellIcons(production: boolean) {
|
export async function shellIcons(production: boolean) {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import sharp from 'sharp';
|
|||||||
import { TankClass } from '../../src/components/Tanks';
|
import { TankClass } from '../../src/components/Tanks';
|
||||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA } from './constants';
|
import { DATA } from './constants';
|
||||||
|
|
||||||
interface SkillIcon {
|
interface SkillIcon {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { readdir } from 'fs/promises';
|
|||||||
import { extractArmor } from '../../src/core/blitz/extractArmor';
|
import { extractArmor } from '../../src/core/blitz/extractArmor';
|
||||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
import { VehicleDefinitionList } from './definitions';
|
import { VehicleDefinitionList } from './definitions';
|
||||||
|
|
||||||
@@ -28,9 +28,6 @@ export async function tankArmors(production: boolean) {
|
|||||||
if (tankKey.includes('tutorial_bot')) return;
|
if (tankKey.includes('tutorial_bot')) return;
|
||||||
|
|
||||||
const id = toUniqueId(nation, tank.id);
|
const id = toUniqueId(nation, tank.id);
|
||||||
|
|
||||||
console.log(`Building armor ${id} @ ${nation}/${tankKey}`);
|
|
||||||
|
|
||||||
const model = await extractArmor(DATA, `${nation}-${tankKey}`);
|
const model = await extractArmor(DATA, `${nation}-${tankKey}`);
|
||||||
|
|
||||||
changes.push({
|
changes.push({
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { readdir } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
|
import sharp from 'sharp';
|
||||||
import { Vector3Tuple } from 'three';
|
import { Vector3Tuple } from 'three';
|
||||||
import { NATION_IDS } from '../../src/constants/nations';
|
import { NATION_IDS } from '../../src/constants/nations';
|
||||||
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
import { VehicleDefinitionList } from './definitions';
|
import { VehicleDefinitionList } from './definitions';
|
||||||
|
|
||||||
@@ -25,6 +26,20 @@ export interface TankParameters {
|
|||||||
averageThickness?: Record<string, number>;
|
averageThickness?: Record<string, number>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
maskSlice?: {
|
||||||
|
[key: string]:
|
||||||
|
| {
|
||||||
|
enabled: boolean;
|
||||||
|
planePosition: Vector3Tuple;
|
||||||
|
planeNormal: Vector3Tuple;
|
||||||
|
planeAxis: Vector3Tuple;
|
||||||
|
frustumConeBaseRadius: number;
|
||||||
|
frustumConeAngleDegrees: number;
|
||||||
|
planeSliceMode: number;
|
||||||
|
frustumConeSliceMode: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function tankIcons(production: boolean) {
|
export async function tankIcons(production: boolean) {
|
||||||
@@ -50,23 +65,29 @@ export async function tankIcons(production: boolean) {
|
|||||||
const parameters = await readYAMLDVPL<TankParameters>(
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
|
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
|
||||||
);
|
);
|
||||||
const small = `${DATA}/${parameters.resourcesPath.smallIconPath
|
const smallPath = `${DATA}/${parameters.resourcesPath.smallIconPath
|
||||||
.replace(/~res:\//, '')
|
.replace(/~res:\//, '')
|
||||||
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||||
const big = `${DATA}/${parameters.resourcesPath.bigIconPath
|
const bigPath = `${DATA}/${parameters.resourcesPath.bigIconPath
|
||||||
.replace(/~res:\//, '')
|
.replace(/~res:\//, '')
|
||||||
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||||
|
const big = await sharp(await readDVPLFile(bigPath))
|
||||||
|
.trim()
|
||||||
|
.toBuffer();
|
||||||
|
const small = await sharp(await readDVPLFile(smallPath))
|
||||||
|
.trim()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
if (big) {
|
if (big) {
|
||||||
changes.push({
|
changes.push({
|
||||||
content: await readBase64DVPL(big),
|
content: big.toString('base64'),
|
||||||
encoding: 'base64',
|
encoding: 'base64',
|
||||||
path: `icons/tanks/big/${id}.webp`,
|
path: `icons/tanks/big/${id}.webp`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (small) {
|
if (small) {
|
||||||
changes.push({
|
changes.push({
|
||||||
content: await readBase64DVPL(small),
|
content: small.toString('base64'),
|
||||||
encoding: 'base64',
|
encoding: 'base64',
|
||||||
path: `icons/tanks/small/${id}.webp`,
|
path: `icons/tanks/small/${id}.webp`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { extractModel } from '../../src/core/blitz/extractModel';
|
|||||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||||
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
import { VehicleDefinitionList } from './definitions';
|
import { VehicleDefinitionList } from './definitions';
|
||||||
import { TankParameters } from './tankIcons';
|
import { TankParameters } from './tankIcons';
|
||||||
@@ -32,9 +32,6 @@ export async function tankModels(production: boolean) {
|
|||||||
if (tankKey.includes('tutorial_bot')) return;
|
if (tankKey.includes('tutorial_bot')) return;
|
||||||
|
|
||||||
const id = toUniqueId(nation, tank.id);
|
const id = toUniqueId(nation, tank.id);
|
||||||
|
|
||||||
console.log(`Building model ${id} @ ${nation}/${tankKey}`);
|
|
||||||
|
|
||||||
const parameters = await readYAMLDVPL<TankParameters>(
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
|
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { readdir } from 'fs/promises';
|
||||||
|
import { google } from 'googleapis';
|
||||||
|
import { cloneDeep, uniqBy } from 'lodash';
|
||||||
|
import { parse as parseYaml } from 'yaml';
|
||||||
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
|
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||||
|
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||||
|
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||||
|
import {
|
||||||
|
VideoDefinitions,
|
||||||
|
videoDefinitions,
|
||||||
|
} from '../../src/core/blitzkit/videos';
|
||||||
|
import { DATA, POI } from './constants';
|
||||||
|
import { BlitzStrings, botPattern, VehicleDefinitionList } from './definitions';
|
||||||
|
|
||||||
|
const youtubers = [
|
||||||
|
'UCKBYXp4Xn2I2tL1UL4fpbhw', // droodles
|
||||||
|
'UCrQ-Dy8lVsm11u8pCJ8W7Tw', // fatness
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function videos(production: boolean) {
|
||||||
|
console.log('Building videos...');
|
||||||
|
|
||||||
|
const currentVideos = await videoDefinitions;
|
||||||
|
const auth = await google.auth.getClient({
|
||||||
|
scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
|
||||||
|
});
|
||||||
|
const youtube = google.youtube({ version: 'v3', auth });
|
||||||
|
|
||||||
|
const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
|
||||||
|
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||||
|
);
|
||||||
|
const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
|
||||||
|
`${DATA}/${POI.strings}/en.yaml.dvpl`,
|
||||||
|
);
|
||||||
|
const stringsCache = await fetch(POI.cachedStrings)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((string) => parseYaml(string) as BlitzStrings);
|
||||||
|
const strings = {
|
||||||
|
...stringsPreInstalled,
|
||||||
|
...stringsCache,
|
||||||
|
};
|
||||||
|
const tanks: { id: number; name: string; tier: number }[] = [];
|
||||||
|
await Promise.all(
|
||||||
|
nations.map(async (nation) => {
|
||||||
|
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||||
|
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(tankList.root).map(async ([tankKey, tank]) => {
|
||||||
|
if (botPattern.test(tankKey)) return null;
|
||||||
|
|
||||||
|
const id = toUniqueId(nation, tank.id);
|
||||||
|
const name =
|
||||||
|
(tank.shortUserString
|
||||||
|
? strings[tank.shortUserString]
|
||||||
|
: undefined) ?? strings[tank.userString];
|
||||||
|
const tier = tank.level;
|
||||||
|
|
||||||
|
tanks.push({ id, name, tier });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const tanksSanitized = tanks
|
||||||
|
.sort((a, b) => b.tier - a.tier)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const lastUpdatedA = currentVideos[a.id]?.lastUpdated ?? 0;
|
||||||
|
const lastUpdatedB = currentVideos[b.id]?.lastUpdated ?? 0;
|
||||||
|
return lastUpdatedA - lastUpdatedB;
|
||||||
|
});
|
||||||
|
const videos: VideoDefinitions = cloneDeep(currentVideos);
|
||||||
|
|
||||||
|
let done = 0;
|
||||||
|
for (const tank of tanksSanitized) {
|
||||||
|
try {
|
||||||
|
const results = await youtube.search.list({
|
||||||
|
part: ['snippet'],
|
||||||
|
q: `World of Tanks Blitz ${tank.name}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!results.data.items || results.data.items.length === 0) continue;
|
||||||
|
|
||||||
|
const items = uniqBy(
|
||||||
|
results.data.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.snippet?.channelId &&
|
||||||
|
item.id?.videoId &&
|
||||||
|
youtubers.includes(item.snippet.channelId),
|
||||||
|
),
|
||||||
|
(item) => item.snippet!.channelId,
|
||||||
|
);
|
||||||
|
|
||||||
|
videos[tank.id] = {
|
||||||
|
lastUpdated: Math.round(Date.now() / 1000),
|
||||||
|
videos: items.map((item) => item.id!.videoId!),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${++done} / ${tanks.length} (${Math.round(100 * (done / tanks.length))}%) done; found ${items.length} for ${tank.name}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000 / 5));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('An error occurred, safely exiting prematurely...', error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = Object.entries(videos)
|
||||||
|
.map(
|
||||||
|
([id, { videos, lastUpdated }]) =>
|
||||||
|
`${id},${lastUpdated},${videos.join(',')}`,
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
await commitAssets(
|
||||||
|
'videos',
|
||||||
|
[{ content, encoding: 'utf-8', path: 'definitions/videos.csv' }],
|
||||||
|
production,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
import { RegionSubdomain } from '../src/constants/regions';
|
import { RegionSubdomain } from '../src/constants/regions';
|
||||||
import { getAccountInfo } from '../src/core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../src/core/blitz/getAccountInfo';
|
||||||
import regionSubdomainToRegion from '../src/core/blitz/regionSubdomainToRegion';
|
import regionSubdomainToRegion from '../src/core/blitz/regionSubdomainToRegion';
|
||||||
import { commitAssets } from '../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../src/core/blitzkit/commitAssets';
|
||||||
import { FileChange } from '../src/core/blitzkrieg/commitMultipleFiles.js';
|
import { FileChange } from '../src/core/blitzkit/commitMultipleFiles.js';
|
||||||
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
|
import { patientFetchJSON } from '../src/core/blitzkit/patientFetchJSON';
|
||||||
import { superCompress } from '../src/core/blitzkrieg/superCompress';
|
import { superCompress } from '../src/core/blitzkit/superCompress';
|
||||||
import {
|
import {
|
||||||
BkrlBaseEntry,
|
BkrlBaseEntry,
|
||||||
BkrlFormat,
|
BkrlFormat,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { tankDefinitions } from '../src/core/blitzkit/tankDefinitions';
|
||||||
|
|
||||||
|
// random vercel bug forces require import
|
||||||
|
const { writeFileSync } = require('fs');
|
||||||
|
|
||||||
|
console.log('Building sitemap...');
|
||||||
|
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const values = Object.values(awaitedTankDefinitions);
|
||||||
|
const txt = values
|
||||||
|
.map((tank) => `https://blitzkit.vercel.app/tools/tankopedia/${tank.id}/`)
|
||||||
|
.join('\n');
|
||||||
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||||
|
${values
|
||||||
|
.map(
|
||||||
|
(tank) =>
|
||||||
|
`<url><loc>https://blitzkit.vercel.app/tools/tankopedia/${tank.id}/</loc></url>`,
|
||||||
|
)
|
||||||
|
.join('\n')}
|
||||||
|
</urlset>`;
|
||||||
|
|
||||||
|
writeFileSync('public/sitemap.txt', txt);
|
||||||
|
writeFileSync('public/sitemap.xml', xml);
|
||||||
|
|
||||||
|
console.log(`Built sitemap for ${values.length} tanks`);
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { chunk, times } from 'lodash';
|
||||||
|
import { compress } from 'lz4js';
|
||||||
|
import { argv } from 'process';
|
||||||
|
import { Region, REGIONS } from '../src/constants/regions';
|
||||||
|
import getTankStats from '../src/core/blitz/getTankStats';
|
||||||
|
import { idToRegion } from '../src/core/blitz/idToRegion';
|
||||||
|
import { commitAssets } from '../src/core/blitzkit/commitAssets';
|
||||||
|
import { FileChange } from '../src/core/blitzkit/commitMultipleFiles';
|
||||||
|
import {
|
||||||
|
RtscWritePlayer,
|
||||||
|
RtscWriteStream,
|
||||||
|
RtsmWriteStream,
|
||||||
|
} from '../src/core/streams/rts';
|
||||||
|
import usersRaw from '../test.users.json';
|
||||||
|
|
||||||
|
const PLAYERS_PER_CHUNK = 256;
|
||||||
|
const CHUNKS_PER_BATCH = 128;
|
||||||
|
|
||||||
|
const production = argv.includes('--production');
|
||||||
|
const region = argv
|
||||||
|
.find((argument) => argument.startsWith('--region'))
|
||||||
|
?.split('=')[1] as Region | undefined;
|
||||||
|
|
||||||
|
if (!region || !REGIONS.includes(region)) {
|
||||||
|
throw new Error('No valid region specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const todayPath = `${today.getUTCFullYear()}/${today.getUTCMonth() + 1}/${today.getUTCDate()}`;
|
||||||
|
const users = usersRaw.filter(({ blitz }) => idToRegion(blitz) === region);
|
||||||
|
const base = Math.ceil(users.length / PLAYERS_PER_CHUNK);
|
||||||
|
const chunks = times(base, (offset) => ({
|
||||||
|
offset,
|
||||||
|
users: users.filter((user) => user.blitz % base === offset),
|
||||||
|
}));
|
||||||
|
const batches = chunk(chunks, CHUNKS_PER_BATCH);
|
||||||
|
const manifestWriteStream = new RtsmWriteStream();
|
||||||
|
|
||||||
|
console.log(`Found ${users.length} players in ${region}`);
|
||||||
|
|
||||||
|
await commitAssets(
|
||||||
|
`regular tank stats manifests ${todayPath}`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
encoding: 'base64',
|
||||||
|
path: `regions/${region}/regular/tanks/${todayPath}/manifest.rtsm`,
|
||||||
|
content: Buffer.from(
|
||||||
|
compress(manifestWriteStream.rtsm(base).uint8Array),
|
||||||
|
).toString('base64'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
production,
|
||||||
|
);
|
||||||
|
|
||||||
|
let batchesDone = 0;
|
||||||
|
for (const batch of batches) {
|
||||||
|
console.log(`Batch ${++batchesDone} of ${batches.length}`);
|
||||||
|
|
||||||
|
const changes: FileChange[] = [];
|
||||||
|
|
||||||
|
let chunksDone = 0;
|
||||||
|
for (const chunk of batch) {
|
||||||
|
console.log(`Chunk ${++chunksDone} of ${chunks.length}`);
|
||||||
|
|
||||||
|
let playersDone = 0;
|
||||||
|
const players = await Promise.all(
|
||||||
|
chunk.users.map(async (user) => {
|
||||||
|
const tanks = await getTankStats(region, user.blitz);
|
||||||
|
|
||||||
|
console.log(`Player ${++playersDone} of ${chunk.users.length}`);
|
||||||
|
|
||||||
|
if (tanks === null) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.blitz,
|
||||||
|
tanks,
|
||||||
|
} satisfies RtscWritePlayer;
|
||||||
|
}),
|
||||||
|
).then((players) => players.filter((player) => player !== null));
|
||||||
|
const writeStream = new RtscWriteStream();
|
||||||
|
|
||||||
|
writeStream.rtsc(players);
|
||||||
|
changes.push({
|
||||||
|
encoding: 'base64',
|
||||||
|
content: Buffer.from(compress(writeStream.uint8Array)).toString('base64'),
|
||||||
|
path: `regions/${region}/regular/tanks/${todayPath}/${chunk.offset}.rtsc.lz4`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not await; commit in background
|
||||||
|
commitAssets(
|
||||||
|
`regular tank stats ${todayPath} batch ${batches.indexOf(batch) + 1}`,
|
||||||
|
changes,
|
||||||
|
production,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,13 +3,12 @@
|
|||||||
import { ContextMenu } from '@radix-ui/themes';
|
import { ContextMenu } from '@radix-ui/themes';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import * as Breakdown from '../../../components/Breakdown';
|
import * as Breakdown from '../../../components/Breakdown';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
|
||||||
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||||
import { resetSession } from '../../../core/blitzkrieg/resetSession';
|
import { resetSession } from '../../../core/blitzkit/resetSession';
|
||||||
import {
|
import {
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
tankDefinitions,
|
tankDefinitions,
|
||||||
} from '../../../core/blitzkrieg/tankDefinitions';
|
} from '../../../core/blitzkit/tankDefinitions';
|
||||||
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||||
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
||||||
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
||||||
@@ -88,10 +87,10 @@ export default function SessionPage({ naked = true }: SessionPageProps) {
|
|||||||
async function recalculateDiff() {
|
async function recalculateDiff() {
|
||||||
if (session.isTracking) {
|
if (session.isTracking) {
|
||||||
const { id, region } = session;
|
const { id, region } = session;
|
||||||
const careerRaw = await fetchBlitz<TanksStats>(
|
const careerRaw = await fetchBlitz<TanksStats>(region, 'tanks/stats', {
|
||||||
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
account_id: id,
|
||||||
);
|
});
|
||||||
const career = careerRaw[id].reduce<
|
const career = careerRaw[id]!.reduce<
|
||||||
Record<number, IndividualTankStats>
|
Record<number, IndividualTankStats>
|
||||||
>(
|
>(
|
||||||
(accumulator, curr) => ({
|
(accumulator, curr) => ({
|
||||||
@@ -103,7 +102,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) {
|
|||||||
const awaitedTankDefinitions = await tankDefinitions;
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
const awaitedTankAverages = await tankAverages;
|
const awaitedTankAverages = await tankAverages;
|
||||||
const careerWN8 =
|
const careerWN8 =
|
||||||
careerRaw[id].reduce(
|
careerRaw[id]!.reduce(
|
||||||
(accumulator, { tank_id, all }) =>
|
(accumulator, { tank_id, all }) =>
|
||||||
accumulator +
|
accumulator +
|
||||||
(awaitedTankAverages[tank_id] && all.battles > 0
|
(awaitedTankAverages[tank_id] && all.battles > 0
|
||||||
@@ -112,7 +111,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) {
|
|||||||
: 0),
|
: 0),
|
||||||
0,
|
0,
|
||||||
) /
|
) /
|
||||||
careerRaw[id].reduce(
|
careerRaw[id]!.reduce(
|
||||||
(accumulator, { tank_id, all }) =>
|
(accumulator, { tank_id, all }) =>
|
||||||
accumulator +
|
accumulator +
|
||||||
(awaitedTankAverages[tank_id] && all.battles > 0
|
(awaitedTankAverages[tank_id] && all.battles > 0
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
import { AlertDialog, Button, Flex, Theme } from '@radix-ui/themes';
|
||||||
import { AlertDialog, Button, Flex, Text, Theme } from '@radix-ui/themes';
|
|
||||||
import '@radix-ui/themes/styles.css';
|
import '@radix-ui/themes/styles.css';
|
||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
import { Roboto_Flex } from 'next/font/google';
|
import { Roboto_Flex } from 'next/font/google';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { ReactNode, useEffect, useState } from 'react';
|
import { ReactNode, Suspense, useEffect, useState } from 'react';
|
||||||
import { SkyLine } from '../assets/art/SkyLine';
|
import { Footer } from '../components/Footer';
|
||||||
import { Analytics } from '../components/Analytics';
|
import { Loader } from '../components/Loader';
|
||||||
import Navbar from '../components/Navbar';
|
import Navbar from '../components/Navbar';
|
||||||
import PageWrapper from '../components/PageWrapper';
|
import { Party3 } from '../components/Party3';
|
||||||
import isDev from '../core/blitzkrieg/isDev';
|
import isDev from '../core/blitzkit/isDev';
|
||||||
import { isLocalhost } from '../core/blitzkrieg/isLocalhost';
|
import { isLocalhost } from '../core/blitzkit/isLocalhost';
|
||||||
import { useFullScreen } from '../hooks/useFullScreen';
|
import { useFullScreen } from '../hooks/useFullScreen';
|
||||||
import { useApp } from '../stores/app';
|
import { useApp } from '../stores/app';
|
||||||
import { mintTheme } from '../themes/mint';
|
|
||||||
|
|
||||||
config();
|
config();
|
||||||
|
|
||||||
@@ -33,6 +31,8 @@ const robotoFlex = Roboto_Flex({
|
|||||||
export default function RootLayout({ children }: RootLayoutProps) {
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isEmbed = pathname.split('/')[1] === 'embeds';
|
const isEmbed = pathname.split('/')[1] === 'embeds';
|
||||||
|
const isRoot = pathname === '/';
|
||||||
|
const darkMode = useApp((state) => state.darkMode);
|
||||||
const [showDevBuildAlert, setShowDevBuildAlert] = useState(false);
|
const [showDevBuildAlert, setShowDevBuildAlert] = useState(false);
|
||||||
const isFullScreen = useFullScreen();
|
const isFullScreen = useFullScreen();
|
||||||
|
|
||||||
@@ -48,115 +48,80 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
return (
|
return (
|
||||||
<html lang="en" className={robotoFlex.className}>
|
<html lang="en" className={robotoFlex.className}>
|
||||||
<head>
|
<head>
|
||||||
<title>Blitzkrieg</title>
|
{isRoot && (
|
||||||
<meta
|
<>
|
||||||
name="description"
|
<title>BlitzKit</title>
|
||||||
content="🎉 Tools for everything World of Tanks Blitz"
|
<meta
|
||||||
/>
|
name="description"
|
||||||
<Analytics />
|
content="🎉 Tools for everything World of Tanks Blitz"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<meta property="og:site_name" content="BlitzKit" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
|
||||||
|
<Party3 />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
paddingTop: isEmbed ? 0 : '3.25rem',
|
|
||||||
backgroundColor: isEmbed ? 'transparent' : undefined,
|
backgroundColor: isEmbed ? 'transparent' : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Theme
|
<Theme
|
||||||
appearance="dark"
|
appearance={darkMode ? 'dark' : 'light'}
|
||||||
panelBackground="translucent"
|
panelBackground="solid"
|
||||||
radius="full"
|
radius="full"
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
>
|
>
|
||||||
{!isEmbed && <Navbar />}
|
<Flex
|
||||||
<AlertDialog.Root open={showDevBuildAlert}>
|
direction="column"
|
||||||
<AlertDialog.Content>
|
style={{ minHeight: '100vh', paddingTop: isEmbed ? undefined : 64 }}
|
||||||
<AlertDialog.Title>Experimental version!</AlertDialog.Title>
|
>
|
||||||
<AlertDialog.Description>
|
{!isEmbed && <Navbar />}
|
||||||
This version may have a lot of issues. Report issues to{' '}
|
<AlertDialog.Root open={showDevBuildAlert}>
|
||||||
<a href="https://discord.gg/nDt7AjGJQH" target="_blank">
|
<AlertDialog.Content>
|
||||||
the official Discord server
|
<AlertDialog.Title>Experimental version!</AlertDialog.Title>
|
||||||
</a>
|
<AlertDialog.Description>
|
||||||
. Also consider using{' '}
|
This version may have a lot of issues. Report issues to{' '}
|
||||||
<a href="https://blitz-krieg.vercel.app/">
|
<a href="https://discord.gg/nDt7AjGJQH" target="_blank">
|
||||||
the more stable release version
|
the official Discord server
|
||||||
</a>
|
</a>
|
||||||
. You will be asked again in 8 days.
|
. Also consider using{' '}
|
||||||
</AlertDialog.Description>
|
<a href="https://blitzkit.vercel.app/">
|
||||||
|
the more stable release version
|
||||||
|
</a>
|
||||||
|
. You will be asked again in 8 days.
|
||||||
|
</AlertDialog.Description>
|
||||||
|
|
||||||
<Flex justify="end">
|
<Flex justify="end">
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowDevBuildAlert(false);
|
setShowDevBuildAlert(false);
|
||||||
useApp.setState({ devBuildAgreementTime: Date.now() });
|
useApp.setState({ devBuildAgreementTime: Date.now() });
|
||||||
}}
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</AlertDialog.Content>
|
|
||||||
</AlertDialog.Root>
|
|
||||||
|
|
||||||
<Flex direction="column">{children}</Flex>
|
|
||||||
|
|
||||||
{!isEmbed && !isFullScreen && (
|
|
||||||
<PageWrapper>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://www.patreon.com/tresabhi"
|
|
||||||
style={{ textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
style={{
|
|
||||||
padding: 16,
|
|
||||||
borderRadius: 8,
|
|
||||||
backgroundColor: mintTheme.colors.componentInteractive,
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
direction="column"
|
|
||||||
gap="1"
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="4" color="mint">
|
Continue
|
||||||
Blitzkrieg. Free forever. For everyone.
|
</Button>
|
||||||
</Text>
|
|
||||||
<Text color="mint">
|
|
||||||
<b>Consider supporting today 🍀</b>
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
color="mint"
|
|
||||||
style={{
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex>
|
|
||||||
<CaretRightIcon width={32} height={32} />
|
|
||||||
</Flex>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<SkyLine
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
right: 'min(8vw, 15%)',
|
|
||||||
top: '5%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</a>
|
</AlertDialog.Content>
|
||||||
</PageWrapper>
|
</AlertDialog.Root>
|
||||||
)}
|
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
{!isEmbed && !isFullScreen && <Footer />}
|
||||||
|
</Flex>
|
||||||
</Theme>
|
</Theme>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { theme } from '../stitches.config';
|
|||||||
|
|
||||||
export const tool = styleVariants({
|
export const tool = styleVariants({
|
||||||
enabled: {
|
enabled: {
|
||||||
boxShadow: `inset 0 -256px 512px -128px ${theme.colors.appBackground1}`,
|
boxShadow: `inset 0 -200px 128px -128px ${theme.colors.appBackground1}`,
|
||||||
|
|
||||||
':hover': {
|
':hover': {
|
||||||
boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`,
|
boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`,
|
||||||
|
|||||||
@@ -1,94 +1,91 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||||
import { Flex, Text, TextField } from '@radix-ui/themes';
|
import { Button, Flex, Link as LinkRadix } from '@radix-ui/themes';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRef } from 'react';
|
|
||||||
import { Search, SearchItem } from 'react-fuzzysort';
|
|
||||||
import PageWrapper from '../components/PageWrapper';
|
import PageWrapper from '../components/PageWrapper';
|
||||||
import { TOOLS } from '../constants/tools';
|
import { TOOLS } from '../constants/tools';
|
||||||
|
import { PatreonIcon } from '../icons/Patreon';
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
import * as styles from './page.css';
|
import * as styles from './page.css';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const input = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<TextField.Root placeholder="Search tools...">
|
<Flex
|
||||||
<TextField.Slot>
|
direction="column"
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
gap="5"
|
||||||
</TextField.Slot>
|
justify="center"
|
||||||
</TextField.Root>
|
align="center"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
<LinkRadix href="https://www.patreon.com/tresabhi" target="_blank">
|
||||||
|
<Button
|
||||||
|
color="ruby"
|
||||||
|
style={{
|
||||||
|
boxShadow: `0 0 8px ${theme.colors.solidBackground_ruby}f0`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PatreonIcon style={{ width: '1em', height: '1em' }} /> Consider
|
||||||
|
donating on Patreon
|
||||||
|
</Button>
|
||||||
|
</LinkRadix>
|
||||||
|
|
||||||
<Flex gap="3" wrap="wrap" align="center" justify="center">
|
<Flex gap="3" wrap="wrap" align="center" justify="center">
|
||||||
<Search
|
{TOOLS.map((tool) => (
|
||||||
input={input}
|
<Link
|
||||||
list={TOOLS.map(
|
href={tool.href ?? `/tools/${tool.id}`}
|
||||||
(tool) =>
|
target={tool.href ? '_blank' : undefined}
|
||||||
({
|
className={
|
||||||
node: (
|
tool.disabled ? styles.tool.disabled : styles.tool.enabled
|
||||||
<Link
|
}
|
||||||
href={tool.href ?? `/tools/${tool.id}`}
|
style={{
|
||||||
target={tool.href ? '_blank' : undefined}
|
backgroundImage: `url(/assets/banners/${tool.id}.webp)`,
|
||||||
className={
|
cursor: tool.disabled ? 'default' : 'pointer',
|
||||||
tool.disabled ? styles.tool.disabled : styles.tool.enabled
|
opacity: tool.disabled ? 0.25 : 1,
|
||||||
}
|
height: 128,
|
||||||
style={{
|
minWidth: 256,
|
||||||
backgroundImage: `url(/assets/banners/${tool.id}.webp)`,
|
flex: 1,
|
||||||
cursor: tool.disabled ? 'default' : 'pointer',
|
borderRadius: 8,
|
||||||
opacity: tool.disabled ? 0.25 : 1,
|
textDecoration: 'none',
|
||||||
height: 128,
|
backgroundSize: 'cover',
|
||||||
minWidth: 256,
|
backgroundPosition: 'center',
|
||||||
flex: 1,
|
overflow: 'hidden',
|
||||||
borderRadius: 8,
|
display: 'flex',
|
||||||
textDecoration: 'none',
|
flexDirection: 'column',
|
||||||
backgroundSize: 'cover',
|
alignItems: 'flex-start',
|
||||||
backgroundPosition: 'center',
|
justifyContent: 'flex-end',
|
||||||
overflow: 'hidden',
|
padding: 16,
|
||||||
display: 'flex',
|
transition: `box-shadow ${theme.durations.regular}`,
|
||||||
flexDirection: 'column',
|
}}
|
||||||
alignItems: 'flex-start',
|
onClick={(event) => {
|
||||||
justifyContent: 'flex-end',
|
if (tool.disabled) event.preventDefault();
|
||||||
padding: 16,
|
}}
|
||||||
gap: 4,
|
>
|
||||||
transition: `box-shadow ${theme.durations.regular}`,
|
<span
|
||||||
}}
|
style={{
|
||||||
onClick={(event) => {
|
fontSize: 32,
|
||||||
if (tool.disabled) event.preventDefault();
|
color: theme.colors.textHighContrast,
|
||||||
}}
|
display: 'flex',
|
||||||
>
|
alignItems: 'center',
|
||||||
<span
|
justifyContent: 'center',
|
||||||
style={{
|
}}
|
||||||
fontSize: 32,
|
>
|
||||||
color: theme.colors.textHighContrast,
|
{tool.title}
|
||||||
display: 'flex',
|
<CaretRightIcon style={{ width: '1em', height: '1em' }} />
|
||||||
alignItems: 'center',
|
</span>
|
||||||
justifyContent: 'center',
|
<span
|
||||||
}}
|
style={{
|
||||||
>
|
color: theme.colors.textLowContrast,
|
||||||
{tool.title}
|
fontSize: 16,
|
||||||
<CaretRightIcon style={{ width: '1em', height: '1em' }} />
|
}}
|
||||||
</span>
|
>
|
||||||
<span
|
{tool.disabled ? 'Coming soon!' : tool.description}
|
||||||
style={{
|
</span>
|
||||||
color: theme.colors.textLowContrast,
|
</Link>
|
||||||
fontSize: 16,
|
))}
|
||||||
}}
|
</Flex>
|
||||||
>
|
|
||||||
{tool.disabled ? 'Coming soon!' : tool.description}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
query: `${tool.title} ${tool.description}`,
|
|
||||||
}) satisfies SearchItem,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Text size="1" color="gray" align="center">
|
|
||||||
Images courtesy of Wargaming Art Department
|
|
||||||
</Text>
|
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Flex, Heading, Switch, Text } from '@radix-ui/themes';
|
||||||
|
import PageWrapper from '../../components/PageWrapper';
|
||||||
|
import { useApp } from '../../stores/app';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const developerMode = useApp((state) => state.developerMode);
|
||||||
|
const darkMode = useApp((state) => state.darkMode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper justify="center" align="center">
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<Flex gap="3" direction="column">
|
||||||
|
<Heading size="5">Appearance</Heading>
|
||||||
|
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Text>Dark mode</Text>
|
||||||
|
<Switch
|
||||||
|
checked={darkMode}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useApp.setState({ darkMode: checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="3" direction="column">
|
||||||
|
<Heading size="5">Advanced</Heading>
|
||||||
|
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Text>Developer mode</Text>
|
||||||
|
<Switch
|
||||||
|
checked={developerMode}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useApp.setState({ developerMode: checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Text color="gray">
|
||||||
|
<i>Not a lot here, eh?</i>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { use, useState } from 'react';
|
||||||
|
import { provisionDefinitions } from '../../../../core/blitzkit/provisionDefinitions';
|
||||||
|
import { tankToCompareMember } from '../../../../core/blitzkit/tankToCompareMember';
|
||||||
|
import { mutateCompareTemporary } from '../../../../stores/compare';
|
||||||
|
import { TankSearch } from '../../tankopedia/components/TankSearch';
|
||||||
|
|
||||||
|
interface TankControlProps {
|
||||||
|
index: number;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TankControl({ index, id }: TankControlProps) {
|
||||||
|
const awaitedProvisionDefinitions = use(provisionDefinitions);
|
||||||
|
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap="3" justify="center" style={{ width: '100%' }}>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
mutateCompareTemporary((draft) => {
|
||||||
|
draft.members.splice(index, 1);
|
||||||
|
draft.sorting = undefined;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Dialog.Root
|
||||||
|
open={switchTankDialogOpen}
|
||||||
|
onOpenChange={setSwitchTankDialogOpen}
|
||||||
|
>
|
||||||
|
<Dialog.Trigger>
|
||||||
|
<IconButton variant="ghost">
|
||||||
|
<LoopIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
<Dialog.Content>
|
||||||
|
<Flex gap="4" direction="column">
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
gap="4"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<TankSearch
|
||||||
|
compact
|
||||||
|
onSelect={(tank) => {
|
||||||
|
mutateCompareTemporary((draft) => {
|
||||||
|
draft.members[index] = tankToCompareMember(
|
||||||
|
tank,
|
||||||
|
awaitedProvisionDefinitions,
|
||||||
|
);
|
||||||
|
draft.sorting = undefined;
|
||||||
|
});
|
||||||
|
setSwitchTankDialogOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<Link href={`/tools/tankopedia/${id}`}>
|
||||||
|
<IconButton variant="ghost">
|
||||||
|
<ExternalLinkIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { blackA } from '@radix-ui/colors';
|
|
||||||
import { Flex, Text } from '@radix-ui/themes';
|
|
||||||
import { PuffLoader } from 'react-spinners';
|
|
||||||
import { theme } from '../../../stitches.config';
|
|
||||||
|
|
||||||
const NAV_HEIGHT = 52.65;
|
|
||||||
|
|
||||||
interface LoaderProps {
|
|
||||||
naked?: boolean;
|
|
||||||
color?: string;
|
|
||||||
progress?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Loader({ naked, color, progress }: LoaderProps) {
|
|
||||||
const animation = (
|
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
{progress !== undefined && (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: '50%',
|
|
||||||
top: '50%',
|
|
||||||
transform: 'translate(calc(-50% + 5px), calc(-50% + 5px))',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Math.round(progress * 100)}%
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<PuffLoader color={color ?? theme.colors.solidBackground} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (naked) return animation;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
style={{
|
|
||||||
backgroundColor: blackA.blackA10,
|
|
||||||
position: 'fixed',
|
|
||||||
top: NAV_HEIGHT,
|
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
|
||||||
height: `calc(100% - ${NAV_HEIGHT}px)`,
|
|
||||||
padding: theme.spaces.paddingMajor,
|
|
||||||
}}
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
>
|
|
||||||
{animation}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { ReactNode, Suspense } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { TOOLS } from '../../constants/tools';
|
import { TOOLS } from '../../constants/tools';
|
||||||
import { theme } from '../../stitches.config';
|
import { theme } from '../../stitches.config';
|
||||||
import { Loader } from './components/Loader';
|
|
||||||
|
|
||||||
interface ToolsLayoutProps {
|
interface ToolsLayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -15,11 +14,34 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const pathSplit = pathname.split('/');
|
const pathSplit = pathname.split('/');
|
||||||
const toolId = pathSplit.at(2);
|
const toolId = pathSplit.at(2);
|
||||||
|
const isToolRoot = pathname === `/tools/${toolId}`;
|
||||||
const tool = TOOLS.find((tool) => tool.id === toolId);
|
const tool = TOOLS.find((tool) => tool.id === toolId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{tool && <title>{tool.title}</title>}
|
{tool && (
|
||||||
|
<>
|
||||||
|
{isToolRoot && (
|
||||||
|
<>
|
||||||
|
<title>{tool.title}</title>
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content={`/assets/banners/${toolId}.webp`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{tool.pageDescription !== undefined && (
|
||||||
|
<>
|
||||||
|
<meta name="description" content={tool.pageDescription} />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content={tool.pageDescription}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{pathSplit.length === 3 && (
|
{pathSplit.length === 3 && (
|
||||||
<div
|
<div
|
||||||
@@ -73,7 +95,7 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Suspense fallback={<Loader />}>{children}</Suspense>
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
Region,
|
Region,
|
||||||
UNLOCALIZED_REGION_NAMES,
|
UNLOCALIZED_REGION_NAMES,
|
||||||
} from '../../../constants/regions';
|
} from '../../../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
|
||||||
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||||
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
|
||||||
import getRatingInfo from '../../../core/blitz/getRatingInfo';
|
import getRatingInfo from '../../../core/blitz/getRatingInfo';
|
||||||
@@ -36,9 +35,9 @@ import {
|
|||||||
AccountList,
|
AccountList,
|
||||||
AccountListItem,
|
AccountListItem,
|
||||||
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||||
import { getArchivedLatestSeasonNumber } from '../../../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
import { getArchivedLatestSeasonNumber } from '../../../core/blitzkit/getArchivedLatestSeasonNumber';
|
||||||
import getArchivedRatingInfo from '../../../core/blitzkrieg/getArchivedRatingInfo';
|
import getArchivedRatingInfo from '../../../core/blitzkit/getArchivedRatingInfo';
|
||||||
import { getArchivedRatingLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingLeaderboard';
|
import { getArchivedRatingLeaderboard } from '../../../core/blitzkit/getArchivedRatingLeaderboard';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
import { PageTurner } from './components/PageTurner';
|
import { PageTurner } from './components/PageTurner';
|
||||||
|
|
||||||
@@ -818,7 +817,9 @@ export default function Page() {
|
|||||||
} else {
|
} else {
|
||||||
setSearchLoading(true);
|
setSearchLoading(true);
|
||||||
const accountList = await fetchBlitz<AccountList>(
|
const accountList = await fetchBlitz<AccountList>(
|
||||||
`https://api.wotblitz.${region}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}`,
|
region,
|
||||||
|
'account/list',
|
||||||
|
{ search: encodedSearch },
|
||||||
);
|
);
|
||||||
const playerKeys = Object.keys(
|
const playerKeys = Object.keys(
|
||||||
leaderboard[region][season],
|
leaderboard[region][season],
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
Flex,
|
Flex,
|
||||||
|
Heading,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
@@ -22,8 +23,8 @@ import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions';
|
|||||||
import searchPlayersAcrossRegions, {
|
import searchPlayersAcrossRegions, {
|
||||||
AccountListWithServer,
|
AccountListWithServer,
|
||||||
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||||
import { resetSession } from '../../../core/blitzkrieg/resetSession';
|
import { resetSession } from '../../../core/blitzkit/resetSession';
|
||||||
import { setSession } from '../../../core/blitzkrieg/setSession';
|
import { setSession } from '../../../core/blitzkit/setSession';
|
||||||
import { useSession } from '../../../stores/session';
|
import { useSession } from '../../../stores/session';
|
||||||
import SessionPage from '../../embeds/session/page';
|
import SessionPage from '../../embeds/session/page';
|
||||||
import { Menu } from './components/Menu';
|
import { Menu } from './components/Menu';
|
||||||
@@ -249,6 +250,20 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SessionPage naked={false} />
|
<SessionPage naked={false} />
|
||||||
|
|
||||||
|
{!session.isTracking && (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
direction="column"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
<Heading color="gray">No player selected</Heading>
|
||||||
|
<Text color="gray">
|
||||||
|
Search for a <PersonIcon /> Player to get started
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { CaretDownIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Button, Flex, Table, Text } from '@radix-ui/themes';
|
||||||
|
import { random } from 'lodash';
|
||||||
|
import { use, useMemo, useState } from 'react';
|
||||||
|
import { tankDefinitions } from '../../../../core/blitzkit/tankDefinitions';
|
||||||
|
import { tankIcon } from '../../../../core/blitzkit/tankIcon';
|
||||||
|
|
||||||
|
interface DayProps {
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Day({ offset }: DayProps) {
|
||||||
|
const timeTo = new Date(Date.now() - offset * 24 * 60 * 60 * 1000);
|
||||||
|
const timeFrom = new Date(Date.now() - (offset + 1) * 24 * 60 * 60 * 1000);
|
||||||
|
const awaitedTankDefinitions = use(tankDefinitions);
|
||||||
|
const tanksList = useMemo(
|
||||||
|
() => Object.values(awaitedTankDefinitions),
|
||||||
|
[awaitedTankDefinitions],
|
||||||
|
);
|
||||||
|
const [expanded, setExpanded] = useState(offset === 0);
|
||||||
|
const tanks = tanksList
|
||||||
|
.filter((tank) => tank.tier === 10)
|
||||||
|
.sort(() => Math.random() - 0.5)
|
||||||
|
.slice(0, random(2, 5));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.RowHeaderCell>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setExpanded((state) => !state)}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
gap="1"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expanded ? <CaretDownIcon /> : <CaretRightIcon />}
|
||||||
|
{timeTo.toLocaleString('default', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
</Table.RowHeaderCell>
|
||||||
|
<Table.Cell align="right">{random(1, 10)}</Table.Cell>
|
||||||
|
<Table.Cell align="right">
|
||||||
|
{random(60, 80, true).toFixed(0)}%
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
|
||||||
|
{expanded &&
|
||||||
|
tanks.map((tank) => (
|
||||||
|
<Table.Row
|
||||||
|
key={tank.id}
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table.RowHeaderCell
|
||||||
|
style={{
|
||||||
|
paddingLeft: 32,
|
||||||
|
position: 'relative',
|
||||||
|
overflowY: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
draggable={false}
|
||||||
|
src={tankIcon(tank.id)}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: 128 + 32,
|
||||||
|
height: '200%',
|
||||||
|
top: '-50%',
|
||||||
|
left: 0,
|
||||||
|
boxShadow: 'inset 128px 0 64px -64px #00000040',
|
||||||
|
objectFit: 'contain',
|
||||||
|
objectPosition: '50% 50%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
textShadow: 'black 0 0 4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tank.name}
|
||||||
|
</Text>
|
||||||
|
</Table.RowHeaderCell>
|
||||||
|
<Table.Cell align="right">{random(1, 10)}</Table.Cell>
|
||||||
|
<Table.Cell align="right">
|
||||||
|
{random(60, 80, true).toFixed(0)}%
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Flex, Heading, Table } from '@radix-ui/themes';
|
||||||
|
import { random, times } from 'lodash';
|
||||||
|
import { HeroStat } from '../../../components/AllStatsOverview/components/HeroStat';
|
||||||
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
|
import { Day } from './components/Day';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<PageWrapper color="iris">
|
||||||
|
<Flex gap="2">
|
||||||
|
<Heading>TresAbhi</Heading>
|
||||||
|
<Heading color="gray">[OPTML]</Heading>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<HeroStat color="#8E4EC6" subtitle="Super Unicum" value={3142} />
|
||||||
|
|
||||||
|
<Table.Root variant="surface">
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>Day</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell width="0" align="right">
|
||||||
|
Battles
|
||||||
|
</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell width="0" align="right">
|
||||||
|
Winrate
|
||||||
|
</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{times(random(5, 10), (index) => (
|
||||||
|
<Day offset={index} key={index} />
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,11 +5,11 @@ import { ModuleButton } from '../../../../../components/ModuleButtons/ModuleButt
|
|||||||
import { ShellButton } from '../../../../../components/ModuleButtons/ShellButton';
|
import { ShellButton } from '../../../../../components/ModuleButtons/ShellButton';
|
||||||
import { SmallTankIcon } from '../../../../../components/SmallTankIcon';
|
import { SmallTankIcon } from '../../../../../components/SmallTankIcon';
|
||||||
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
|
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
|
||||||
import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopediaPath';
|
import { pushTankopediaPath } from '../../../../../core/blitzkit/pushTankopediaPath';
|
||||||
import {
|
import {
|
||||||
SHELL_NAMES,
|
SHELL_NAMES,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '../../../../../core/blitzkrieg/tankDefinitions/constants';
|
} from '../../../../../core/blitzkit/tankDefinitions/constants';
|
||||||
import { mutateDuel, useDuel } from '../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../stores/duel';
|
||||||
import {
|
import {
|
||||||
mutateTankopediaTemporary,
|
mutateTankopediaTemporary,
|
||||||
@@ -191,6 +191,7 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
|
|||||||
<Flex>
|
<Flex>
|
||||||
{antagonist.turret.guns.map((gun, index) => (
|
{antagonist.turret.guns.map((gun, index) => (
|
||||||
<ModuleButton
|
<ModuleButton
|
||||||
|
key={gun.id}
|
||||||
rowChild
|
rowChild
|
||||||
first={index === 0}
|
first={index === 0}
|
||||||
last={
|
last={
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import { ConsumableButton } from '../../../../../../../components/ModuleButtons/ConsumableButton';
|
import { ConsumablesManager } from '../../../../../../../components/ConsumablesManager';
|
||||||
import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableProvisionInclusivity';
|
import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkit/checkConsumableProvisionInclusivity';
|
||||||
import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumableDefinitions';
|
import { consumableDefinitions } from '../../../../../../../core/blitzkit/consumableDefinitions';
|
||||||
import { useEquipment } from '../../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../../hooks/useEquipment';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||||
@@ -68,48 +68,20 @@ export function Consumables() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex wrap="wrap">
|
<ConsumablesManager
|
||||||
{consumablesList.map((consumable, index) => {
|
consumables={consumablesList}
|
||||||
const selected = consumables.includes(consumable.id);
|
selected={consumables}
|
||||||
|
cooldownBooster={cooldownBooster}
|
||||||
return (
|
disabled={protagonist.tank.consumables === consumables.length}
|
||||||
<ConsumableButton
|
hasConsumableDeliverySystem={hasConsumableDeliverySystem}
|
||||||
special={consumable.duration === undefined}
|
hasHighEndConsumables={hasHighEndConsumables}
|
||||||
duration={
|
timers
|
||||||
consumable.duration === undefined
|
onChange={(consumables) => {
|
||||||
? undefined
|
mutateDuel((draft) => {
|
||||||
: consumable.duration * (hasHighEndConsumables ? 0.7 : 1)
|
draft.protagonist!.consumables = consumables;
|
||||||
}
|
});
|
||||||
cooldown={
|
}}
|
||||||
consumable.cooldown *
|
/>
|
||||||
(hasConsumableDeliverySystem ? 0.85 : 1) *
|
|
||||||
(1 - cooldownBooster * 0.1)
|
|
||||||
}
|
|
||||||
key={consumable.id}
|
|
||||||
first={index === 0}
|
|
||||||
last={index === consumablesList.length - 1}
|
|
||||||
rowChild
|
|
||||||
disabled={
|
|
||||||
protagonist.tank.consumables === consumables.length && !selected
|
|
||||||
}
|
|
||||||
consumable={consumable.id}
|
|
||||||
selected={selected}
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
if (selected) {
|
|
||||||
draft.protagonist!.consumables =
|
|
||||||
draft.protagonist!.consumables.filter(
|
|
||||||
(id) => id !== consumable.id,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
draft.protagonist!.consumables.push(consumable.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</ConfigurationChildWrapper>
|
</ConfigurationChildWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Button, Flex, Heading } from '@radix-ui/themes';
|
import { Button, Flex, Heading } from '@radix-ui/themes';
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import { EquipmentButton } from '../../../../../../../components/ModuleButtons/EquipmentButton';
|
import { EquipmentManager } from '../../../../../../../components/EquipmentManager';
|
||||||
import { equipmentDefinitions } from '../../../../../../../core/blitzkrieg/equipmentDefinitions';
|
import { equipmentDefinitions } from '../../../../../../../core/blitzkit/equipmentDefinitions';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||||
|
|
||||||
export function Equipment() {
|
export function Equipment() {
|
||||||
const protagonist = useDuel((state) => state.protagonist!);
|
const protagonist = useDuel((state) => state.protagonist!);
|
||||||
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
|
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
|
||||||
const equipmentRows =
|
const equipmentPreset =
|
||||||
awaitedEquipmentDefinitions.presets[protagonist.tank.equipment];
|
awaitedEquipmentDefinitions.presets[protagonist.tank.equipment];
|
||||||
const equipmentMatrix = useDuel((state) => state.protagonist!.equipment);
|
const equipmentMatrix = useDuel(
|
||||||
|
(state) => state.protagonist!.equipmentMatrix,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigurationChildWrapper>
|
<ConfigurationChildWrapper>
|
||||||
@@ -21,7 +23,7 @@ export function Equipment() {
|
|||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.protagonist!.equipment.forEach((row) => {
|
draft.protagonist!.equipmentMatrix.forEach((row) => {
|
||||||
row.forEach((_, index) => {
|
row.forEach((_, index) => {
|
||||||
row[index] = 0;
|
row[index] = 0;
|
||||||
});
|
});
|
||||||
@@ -33,47 +35,15 @@ export function Equipment() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex direction="column" gap="2">
|
<EquipmentManager
|
||||||
{equipmentRows.map((equipmentRow, rowIndex) => (
|
matrix={equipmentMatrix}
|
||||||
<Flex gap="2" key={rowIndex}>
|
preset={equipmentPreset}
|
||||||
{equipmentRow.map((equipment, columnIndex) => (
|
onChange={(matrix) => {
|
||||||
<Flex key={columnIndex}>
|
mutateDuel((draft) => {
|
||||||
<EquipmentButton
|
draft.protagonist!.equipmentMatrix = matrix;
|
||||||
equipment={equipment[0]}
|
});
|
||||||
first
|
}}
|
||||||
rowChild
|
/>
|
||||||
selected={equipmentMatrix[rowIndex][columnIndex] === -1}
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
if (equipmentMatrix[rowIndex][columnIndex] === -1) {
|
|
||||||
draft.protagonist!.equipment[rowIndex][columnIndex] = 0;
|
|
||||||
} else {
|
|
||||||
draft.protagonist!.equipment[rowIndex][columnIndex] =
|
|
||||||
-1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<EquipmentButton
|
|
||||||
equipment={equipment[1]}
|
|
||||||
last
|
|
||||||
rowChild
|
|
||||||
selected={equipmentMatrix[rowIndex][columnIndex] === 1}
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
if (equipmentMatrix[rowIndex][columnIndex] === 1) {
|
|
||||||
draft.protagonist!.equipment[rowIndex][columnIndex] = 0;
|
|
||||||
} else {
|
|
||||||
draft.protagonist!.equipment[rowIndex][columnIndex] = 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</ConfigurationChildWrapper>
|
</ConfigurationChildWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import { Flex, Heading, Slider, TextField } from '@radix-ui/themes';
|
|||||||
import { clamp, debounce } from 'lodash';
|
import { clamp, debounce } from 'lodash';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { GenericTankComponentButton } from '../../../../../../../components/ModuleButtons/GenericTankComponentButton';
|
import { GenericTankComponentButton } from '../../../../../../../components/ModuleButtons/GenericTankComponentButton';
|
||||||
import { asset } from '../../../../../../../core/blitzkrieg/asset';
|
import { asset } from '../../../../../../../core/blitzkit/asset';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Button, DropdownMenu, Flex, Heading } from '@radix-ui/themes';
|
import { Button, Flex, Heading } from '@radix-ui/themes';
|
||||||
import { useState } from 'react';
|
import { ModuleManager } from '../../../../../../../components/ModuleManager';
|
||||||
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
|
|
||||||
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
|
||||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||||
import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia';
|
import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia';
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||||
@@ -13,10 +11,6 @@ export function Modules() {
|
|||||||
protagonist.tank.turrets[0].guns.length > 1 ||
|
protagonist.tank.turrets[0].guns.length > 1 ||
|
||||||
protagonist.tank.engines.length > 1 ||
|
protagonist.tank.engines.length > 1 ||
|
||||||
protagonist.tank.tracks.length > 1;
|
protagonist.tank.tracks.length > 1;
|
||||||
const [turretMenuOpen, setTurretMenuOpen] = useState(false);
|
|
||||||
const [gunMenuOpen, setGunMenuOpen] = useState(false);
|
|
||||||
const [engineMenuOpen, setEngineMenuOpen] = useState(false);
|
|
||||||
const [trackMenuOpen, setTrackMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigurationChildWrapper>
|
<ConfigurationChildWrapper>
|
||||||
@@ -64,181 +58,22 @@ export function Modules() {
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex gap="1" wrap="wrap">
|
<ModuleManager
|
||||||
<ModuleButton
|
tank={protagonist.tank}
|
||||||
module="turret"
|
turret={protagonist.turret}
|
||||||
discriminator={TIER_ROMAN_NUMERALS[protagonist.turret.tier]}
|
gun={protagonist.gun}
|
||||||
first
|
shell={protagonist.shell}
|
||||||
last
|
engine={protagonist.engine}
|
||||||
rowChild
|
track={protagonist.track}
|
||||||
onClick={() => setTurretMenuOpen(true)}
|
onChange={(modules) => {
|
||||||
/>
|
mutateDuel((draft) => {
|
||||||
|
draft.protagonist = { ...draft.protagonist!, ...modules };
|
||||||
<DropdownMenu.Root
|
});
|
||||||
open={turretMenuOpen}
|
mutateTankopediaTemporary((draft) => {
|
||||||
onOpenChange={setTurretMenuOpen}
|
draft.shot = undefined;
|
||||||
>
|
});
|
||||||
<DropdownMenu.Trigger>
|
}}
|
||||||
<div />
|
/>
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
<DropdownMenu.RadioGroup value={`${protagonist.turret.id}`}>
|
|
||||||
{[...protagonist.tank.turrets].reverse().map((turret) => {
|
|
||||||
return (
|
|
||||||
<DropdownMenu.RadioItem
|
|
||||||
key={turret.id}
|
|
||||||
value={`${turret.id}`}
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist!.turret = turret;
|
|
||||||
draft.protagonist!.gun = turret.guns.at(-1)!;
|
|
||||||
draft.protagonist!.shell =
|
|
||||||
turret.guns.at(-1)!.shells[0];
|
|
||||||
});
|
|
||||||
mutateTankopediaTemporary((draft) => {
|
|
||||||
draft.shot = undefined;
|
|
||||||
});
|
|
||||||
setTurretMenuOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModuleButton
|
|
||||||
variant="ghost"
|
|
||||||
module="turret"
|
|
||||||
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
|
|
||||||
/>{' '}
|
|
||||||
{turret.name}
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
|
|
||||||
<ModuleButton
|
|
||||||
module="gun"
|
|
||||||
discriminator={TIER_ROMAN_NUMERALS[protagonist.gun.tier]}
|
|
||||||
first
|
|
||||||
last
|
|
||||||
rowChild
|
|
||||||
onClick={() => setGunMenuOpen(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownMenu.Root open={gunMenuOpen} onOpenChange={setGunMenuOpen}>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<div />
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
<DropdownMenu.RadioGroup value={`${protagonist.gun.id}`}>
|
|
||||||
{[...protagonist.turret.guns].reverse().map((gun) => {
|
|
||||||
return (
|
|
||||||
<DropdownMenu.RadioItem
|
|
||||||
key={gun.id}
|
|
||||||
value={`${gun.id}`}
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist!.gun = gun;
|
|
||||||
draft.protagonist!.shell = gun.shells[0];
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModuleButton
|
|
||||||
variant="ghost"
|
|
||||||
module="gun"
|
|
||||||
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
|
||||||
/>{' '}
|
|
||||||
{gun.name}
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
|
|
||||||
<ModuleButton
|
|
||||||
module="engine"
|
|
||||||
discriminator={TIER_ROMAN_NUMERALS[protagonist.engine.tier]}
|
|
||||||
first
|
|
||||||
last
|
|
||||||
rowChild
|
|
||||||
onClick={() => setEngineMenuOpen(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownMenu.Root
|
|
||||||
open={engineMenuOpen}
|
|
||||||
onOpenChange={setEngineMenuOpen}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<div />
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
<DropdownMenu.RadioGroup value={`${protagonist.engine.id}`}>
|
|
||||||
{[...protagonist.tank.engines].reverse().map((engine) => {
|
|
||||||
return (
|
|
||||||
<DropdownMenu.RadioItem
|
|
||||||
key={engine.id}
|
|
||||||
value={`${engine.id}`}
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist!.engine = engine;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModuleButton
|
|
||||||
variant="ghost"
|
|
||||||
module="engine"
|
|
||||||
discriminator={TIER_ROMAN_NUMERALS[engine.tier]}
|
|
||||||
/>{' '}
|
|
||||||
{engine.name}
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
|
|
||||||
<ModuleButton
|
|
||||||
module="chassis"
|
|
||||||
discriminator={TIER_ROMAN_NUMERALS[protagonist.track.tier]}
|
|
||||||
first
|
|
||||||
last
|
|
||||||
rowChild
|
|
||||||
onClick={() => setTrackMenuOpen(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownMenu.Root open={trackMenuOpen} onOpenChange={setTrackMenuOpen}>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<div />
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
<DropdownMenu.RadioGroup value={`${protagonist.track.id}`}>
|
|
||||||
{[...protagonist.tank.tracks].reverse().map((track) => {
|
|
||||||
return (
|
|
||||||
<DropdownMenu.RadioItem
|
|
||||||
key={track.id}
|
|
||||||
value={`${track.id}`}
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist!.track = track;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModuleButton
|
|
||||||
variant="ghost"
|
|
||||||
module="chassis"
|
|
||||||
discriminator={TIER_ROMAN_NUMERALS[track.tier]}
|
|
||||||
/>{' '}
|
|
||||||
{track.name}
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
</Flex>
|
|
||||||
</ConfigurationChildWrapper>
|
</ConfigurationChildWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Button, Flex, Heading } from '@radix-ui/themes';
|
import { Button, Flex, Heading } from '@radix-ui/themes';
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import { ProvisionButton } from '../../../../../../../components/ModuleButtons/ProvisionButton';
|
import { ProvisionsManager } from '../../../../../../../components/ProvisionsManager';
|
||||||
import { availableProvisions } from '../../../../../../../core/blitzkrieg/availableProvisions';
|
import { availableProvisions } from '../../../../../../../core/blitzkit/availableProvisions';
|
||||||
import { provisionDefinitions } from '../../../../../../../core/blitzkrieg/provisionDefinitions';
|
import { provisionDefinitions } from '../../../../../../../core/blitzkit/provisionDefinitions';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||||
|
|
||||||
@@ -33,35 +33,16 @@ export function Provisions() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex wrap="wrap">
|
<ProvisionsManager
|
||||||
{provisionsList.map((provision, index) => {
|
provisions={provisionsList.map((provision) => provision.id)}
|
||||||
const selected = provisions.includes(provision.id);
|
selected={provisions}
|
||||||
|
disabled={tank.provisions === provisions.length}
|
||||||
return (
|
onChange={(provisions) => {
|
||||||
<ProvisionButton
|
mutateDuel((draft) => {
|
||||||
key={provision.id}
|
draft.protagonist!.provisions = provisions;
|
||||||
first={index === 0}
|
});
|
||||||
last={index === provisionsList.length - 1}
|
}}
|
||||||
rowChild
|
/>
|
||||||
disabled={tank.provisions === provisions.length && !selected}
|
|
||||||
provision={provision.id}
|
|
||||||
selected={selected}
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
if (selected) {
|
|
||||||
draft.protagonist!.provisions =
|
|
||||||
draft.protagonist!.provisions.filter(
|
|
||||||
(id) => id !== provision.id,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
draft.protagonist!.provisions.push(provision.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</ConfigurationChildWrapper>
|
</ConfigurationChildWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,17 +9,14 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import { GenericTankComponentButton } from '../../../../../../../../components/ModuleButtons/GenericTankComponentButton';
|
import { CrewSkillManager } from '../../../../../../../../components/CrewSkillManager';
|
||||||
import { asset } from '../../../../../../../../core/blitzkrieg/asset';
|
import { createDefaultSkills } from '../../../../../../../../core/blitzkit/createDefaultSkills';
|
||||||
import { skillDefinitions } from '../../../../../../../../core/blitzkrieg/skillDefinitions';
|
import { skillDefinitions } from '../../../../../../../../core/blitzkit/skillDefinitions';
|
||||||
import { Tier } from '../../../../../../../../core/blitzkrieg/tankDefinitions';
|
|
||||||
import { TIER_ROMAN_NUMERALS } from '../../../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
|
||||||
import {
|
import {
|
||||||
mutateTankopediaTemporary,
|
mutateTankopediaTemporary,
|
||||||
useTankopediaTemporary,
|
useTankopediaTemporary,
|
||||||
} from '../../../../../../../../stores/tankopedia';
|
} from '../../../../../../../../stores/tankopedia';
|
||||||
import { ConfigurationChildWrapper } from '../ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from '../ConfigurationChildWrapper';
|
||||||
import { permanentSkills } from './constants';
|
|
||||||
|
|
||||||
export function Skills() {
|
export function Skills() {
|
||||||
const awaitedSkillDefinitions = use(skillDefinitions);
|
const awaitedSkillDefinitions = use(skillDefinitions);
|
||||||
@@ -27,11 +24,7 @@ export function Skills() {
|
|||||||
|
|
||||||
if (Object.keys(skillLevels).length === 0) {
|
if (Object.keys(skillLevels).length === 0) {
|
||||||
mutateTankopediaTemporary((draft) => {
|
mutateTankopediaTemporary((draft) => {
|
||||||
Object.values(awaitedSkillDefinitions.classes).forEach((skills) => {
|
draft.skills = createDefaultSkills(awaitedSkillDefinitions);
|
||||||
skills.forEach((skill) => {
|
|
||||||
draft.skills[skill] = permanentSkills.includes(skill) ? 7 : 0;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -92,43 +85,14 @@ export function Skills() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex direction="column" gap="2">
|
<CrewSkillManager
|
||||||
{Object.entries(awaitedSkillDefinitions.classes).map(
|
skillLevels={skillLevels}
|
||||||
([tankClass, skills]) => (
|
onChange={(skills) => {
|
||||||
<Flex gap="2" key={tankClass}>
|
mutateTankopediaTemporary((draft) => {
|
||||||
{skills.map((skill) => {
|
draft.skills = skills;
|
||||||
const level = skillLevels[skill];
|
});
|
||||||
|
}}
|
||||||
return (
|
/>
|
||||||
<GenericTankComponentButton
|
|
||||||
special={!permanentSkills.includes(skill)}
|
|
||||||
first
|
|
||||||
last
|
|
||||||
selected={level > 0}
|
|
||||||
discriminator={
|
|
||||||
level === 0
|
|
||||||
? undefined
|
|
||||||
: TIER_ROMAN_NUMERALS[level as Tier]
|
|
||||||
}
|
|
||||||
icon={asset(`icons/skills/${skill}.webp`)}
|
|
||||||
onClick={(event) => {
|
|
||||||
mutateTankopediaTemporary((draft) => {
|
|
||||||
if (event.shiftKey) {
|
|
||||||
draft.skills[skill] =
|
|
||||||
draft.skills[skill] === 0 ? 7 : 0;
|
|
||||||
} else {
|
|
||||||
draft.skills[skill] = level - 1;
|
|
||||||
if (draft.skills[skill] < 0) draft.skills[skill] = 7;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</ConfigurationChildWrapper>
|
</ConfigurationChildWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,53 +9,58 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { use, useEffect, useRef, useState } from 'react';
|
import { Fragment, use, useEffect, useRef, useState } from 'react';
|
||||||
import { lerp } from 'three/src/math/MathUtils';
|
import { lerp } from 'three/src/math/MathUtils';
|
||||||
import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton';
|
import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
||||||
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
|
import { coefficient } from '../../../../../../core/blitzkit/coefficient';
|
||||||
import { coefficient } from '../../../../../../core/blitzkrieg/coefficient';
|
import { equipmentDefinitions } from '../../../../../../core/blitzkit/equipmentDefinitions';
|
||||||
import { degressiveStat } from '../../../../../../core/blitzkrieg/degressiveStat';
|
import { modelDefinitions } from '../../../../../../core/blitzkit/modelDefinitions';
|
||||||
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
import { provisionDefinitions } from '../../../../../../core/blitzkit/provisionDefinitions';
|
||||||
import { normalizeBoundingBox } from '../../../../../../core/blitzkrieg/normalizeBoundingBox';
|
import { tankCharacteristics } from '../../../../../../core/blitzkit/tankCharacteristics';
|
||||||
import { progressiveStat } from '../../../../../../core/blitzkrieg/progressiveStat';
|
|
||||||
import { resolveDpm } from '../../../../../../core/blitzkrieg/resolveDpm';
|
|
||||||
import { sum } from '../../../../../../core/blitzkrieg/sum';
|
|
||||||
import {
|
import {
|
||||||
CREW_MEMBER_NAMES,
|
CREW_MEMBER_NAMES,
|
||||||
GUN_TYPE_NAMES,
|
GUN_TYPE_NAMES,
|
||||||
SHELL_NAMES,
|
SHELL_NAMES,
|
||||||
} from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
} from '../../../../../../core/blitzkit/tankDefinitions/constants';
|
||||||
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
|
|
||||||
import { useConsumable } from '../../../../../../hooks/useConsumable';
|
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
||||||
import { useProvision } from '../../../../../../hooks/useProvision';
|
|
||||||
import { useProvisions } from '../../../../../../hooks/useProvisions';
|
|
||||||
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
||||||
import { useTankopediaTemporary } from '../../../../../../stores/tankopedia';
|
import { useTankopediaTemporary } from '../../../../../../stores/tankopedia';
|
||||||
import { Info } from './components/Info';
|
import { Info } from './components/Info';
|
||||||
import { InfoWithDelta } from './components/InfoWithDelta';
|
import { InfoWithDelta } from './components/InfoWithDelta';
|
||||||
|
|
||||||
export function Characteristics() {
|
export function Characteristics() {
|
||||||
|
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
|
||||||
const awaitedModelDefinitions = use(modelDefinitions);
|
const awaitedModelDefinitions = use(modelDefinitions);
|
||||||
|
const awaitedProvisionDefinitions = use(provisionDefinitions);
|
||||||
const crewSkills = useTankopediaTemporary((state) => state.skills);
|
const crewSkills = useTankopediaTemporary((state) => state.skills);
|
||||||
const { tank, turret, gun, engine, track, shell } = useDuel(
|
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
||||||
(state) => state.protagonist!,
|
|
||||||
);
|
|
||||||
const isFullScreen = useFullScreen();
|
const isFullScreen = useFullScreen();
|
||||||
const hasImprovedVentilation = useEquipment(102);
|
const hasImprovedVentilation = useEquipment(102);
|
||||||
const provisions = useProvisions();
|
|
||||||
const crewMastery = useDuel((state) => state.protagonist!.crewMastery);
|
const crewMastery = useDuel((state) => state.protagonist!.crewMastery);
|
||||||
|
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
||||||
|
|
||||||
|
const provisions = useDuel((state) => state.protagonist!.provisions);
|
||||||
const provisionCrewBonus =
|
const provisionCrewBonus =
|
||||||
provisions.reduce(
|
provisions.reduce(
|
||||||
(total, provision) =>
|
(total, provision) =>
|
||||||
provision.crew ? total + provision.crew / 100 : total,
|
awaitedProvisionDefinitions[provision].crew
|
||||||
|
? total + awaitedProvisionDefinitions[provision].crew! / 100
|
||||||
|
: total,
|
||||||
0,
|
0,
|
||||||
) + (hasImprovedVentilation ? 0.05 : 0);
|
) + (hasImprovedVentilation ? 0.05 : 0);
|
||||||
const commanderMastery = crewMastery + provisionCrewBonus;
|
const commanderMastery = crewMastery + provisionCrewBonus;
|
||||||
const crewMemberMastery = commanderMastery * 1.1;
|
const crewMemberMastery = commanderMastery * 1.1;
|
||||||
|
const consumables = useDuel((state) => state.protagonist!.consumables);
|
||||||
|
const camouflage = useDuel((state) => state.protagonist!.camouflage);
|
||||||
|
const equipmentMatrix = useDuel(
|
||||||
|
(state) => state.protagonist!.equipmentMatrix,
|
||||||
|
);
|
||||||
|
const { tank, turret, gun, engine, track, shell } = useDuel(
|
||||||
|
(state) => state.protagonist!,
|
||||||
|
);
|
||||||
const stockEngine = tank.engines[0];
|
const stockEngine = tank.engines[0];
|
||||||
const stockTrack = tank.tracks[0];
|
const stockTrack = tank.tracks[0];
|
||||||
const stockTurret = tank.turrets[0];
|
const stockTurret = tank.turrets[0];
|
||||||
@@ -63,187 +68,46 @@ export function Characteristics() {
|
|||||||
const tankModelDefinition = awaitedModelDefinitions[tank.id];
|
const tankModelDefinition = awaitedModelDefinitions[tank.id];
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
const size = normalizeBoundingBox(
|
const stats = tankCharacteristics(
|
||||||
unionBoundingBox(
|
{
|
||||||
tankModelDefinition.boundingBox,
|
tank,
|
||||||
turretModelDefinition.boundingBox,
|
camouflage,
|
||||||
),
|
consumables,
|
||||||
|
crewMastery,
|
||||||
|
crewSkills,
|
||||||
|
engine,
|
||||||
|
equipmentMatrix,
|
||||||
|
gun,
|
||||||
|
provisions,
|
||||||
|
shell,
|
||||||
|
turret,
|
||||||
|
track,
|
||||||
|
stockEngine,
|
||||||
|
stockGun,
|
||||||
|
stockTrack,
|
||||||
|
stockTurret,
|
||||||
|
applyReactive: false,
|
||||||
|
applyDynamicArmor: false,
|
||||||
|
applySpallLiner: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
equipmentDefinitions: awaitedEquipmentDefinitions,
|
||||||
|
modelDefinitions: awaitedModelDefinitions,
|
||||||
|
provisionDefinitions: awaitedProvisionDefinitions,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
|
|
||||||
const weight =
|
|
||||||
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
|
|
||||||
const weightTons = weight / 1000;
|
|
||||||
const stockWeight =
|
|
||||||
tank.weight +
|
|
||||||
stockEngine.weight +
|
|
||||||
stockTrack.weight +
|
|
||||||
stockTurret.weight +
|
|
||||||
stockGun.weight;
|
|
||||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
|
||||||
const camouflage = useDuel((state) => state.protagonist!.camouflage);
|
|
||||||
const hasRammer = useEquipment(100);
|
|
||||||
const hasCalibratedShells = useEquipment(103);
|
|
||||||
const hasEnhancedGunLayingDrive = useEquipment(104);
|
|
||||||
const hasSupercharger = useEquipment(107);
|
|
||||||
const hasVerticalStabilizer = useEquipment(105);
|
|
||||||
const hasRefinedGun = useEquipment(106);
|
|
||||||
const hasImprovedAssembly = useEquipment(111);
|
|
||||||
const hasImprovedOptics = useEquipment(114);
|
|
||||||
const hasImprovedControl = useEquipment(116);
|
|
||||||
const hasEngineAccelerator = useEquipment(117);
|
|
||||||
const hasCamouflageNet = useEquipment(115);
|
|
||||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
|
||||||
const hasImprovedSuspension = useEquipment(123);
|
|
||||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
|
||||||
const hasEnginePowerBoost = useConsumable(14);
|
|
||||||
const hasImprovedEnginePowerBoost = useConsumable(27);
|
|
||||||
const hasAdrenaline = useConsumable(18);
|
|
||||||
const hasTungsten = useConsumable(45);
|
|
||||||
const hasReticleCalibration = useConsumable(28);
|
|
||||||
const hasShellReloadBoost = useConsumable(29);
|
|
||||||
const hasStandardFuel = useProvision(18);
|
|
||||||
const hasImprovedFuel = useProvision(19);
|
|
||||||
const hasProtectiveKit = useProvision(22);
|
|
||||||
const hasSandbagArmor = useProvision(28);
|
|
||||||
const hasEnhancedSandbagArmor = useProvision(29);
|
|
||||||
const hasImprovedGunPowder = useProvision(46);
|
|
||||||
const hasGearOil = useProvision(44);
|
|
||||||
const hasImprovedGearOil = useProvision(45);
|
|
||||||
|
|
||||||
const healthCoefficient = coefficient(
|
const hasSupercharger = useEquipment(107);
|
||||||
[hasSandbagArmor, 0.03],
|
const hasCalibratedShells = useEquipment(103);
|
||||||
[hasEnhancedSandbagArmor, 0.06],
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
[hasImprovedAssembly, 0.06],
|
|
||||||
);
|
|
||||||
const reloadCoefficient =
|
|
||||||
(coefficient([hasRammer, -0.07]) *
|
|
||||||
coefficient([true, degressiveStat(crewMemberMastery)])) /
|
|
||||||
coefficient([hasAdrenaline && gun.type === 'regular', 0.2]);
|
|
||||||
const penetrationCoefficient = coefficient([
|
const penetrationCoefficient = coefficient([
|
||||||
hasCalibratedShells,
|
hasCalibratedShells,
|
||||||
isExplosive(shell.type) ? 0.1 : 0.05,
|
isExplosive(shell.type) ? 0.1 : 0.05,
|
||||||
]);
|
]);
|
||||||
const damageCoefficient = coefficient(
|
|
||||||
// TODO: add max rolls
|
|
||||||
[hasTungsten, 0.15],
|
|
||||||
);
|
|
||||||
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
|
|
||||||
const shellVelocityCoefficient = coefficient(
|
|
||||||
[hasSupercharger, 0.3],
|
|
||||||
[hasImprovedGunPowder, 0.3],
|
|
||||||
);
|
|
||||||
const penetrationLossOverDistanceCoefficient = coefficient([
|
const penetrationLossOverDistanceCoefficient = coefficient([
|
||||||
hasSupercharger,
|
hasSupercharger,
|
||||||
-0.5,
|
-0.5,
|
||||||
]);
|
]);
|
||||||
const aimTimeCoefficient =
|
|
||||||
coefficient(
|
|
||||||
[hasEnhancedGunLayingDrive, -0.1],
|
|
||||||
[hasReticleCalibration, -0.4],
|
|
||||||
) * coefficient([true, degressiveStat(crewMemberMastery)]);
|
|
||||||
const dispersionStillCoefficient =
|
|
||||||
coefficient([hasRefinedGun, -0.1], [hasReticleCalibration, -0.4]) *
|
|
||||||
coefficient([true, degressiveStat(crewMemberMastery)]);
|
|
||||||
const dispersionDamagedCoefficient = coefficient([
|
|
||||||
hasVerticalStabilizer,
|
|
||||||
-0.15,
|
|
||||||
]);
|
|
||||||
const dispersionHullTraverseCoefficient = coefficient(
|
|
||||||
[hasVerticalStabilizer, -0.15],
|
|
||||||
[true, -crewSkills.smooth_turn / 100],
|
|
||||||
);
|
|
||||||
const dispersionAfterShotCoefficient = coefficient(
|
|
||||||
[hasVerticalStabilizer, -0.15],
|
|
||||||
[true, -crewSkills.soft_recoil / 100],
|
|
||||||
);
|
|
||||||
const dispersionMovingCoefficient = coefficient(
|
|
||||||
[hasVerticalStabilizer, -0.15],
|
|
||||||
[true, -crewSkills.smooth_driving / 100],
|
|
||||||
);
|
|
||||||
const dispersionTurretTraverseCoefficient = coefficient(
|
|
||||||
[hasVerticalStabilizer, -0.15],
|
|
||||||
[true, -crewSkills.smooth_turret * (2 / 100)],
|
|
||||||
);
|
|
||||||
const enginePowerCoefficient = coefficient(
|
|
||||||
[
|
|
||||||
hasEngineAccelerator &&
|
|
||||||
(tank.class === 'lightTank' || tank.class === 'mediumTank'),
|
|
||||||
0.05,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
hasEngineAccelerator &&
|
|
||||||
(tank.class === 'heavyTank' || tank.class === 'AT-SPG'),
|
|
||||||
0.07,
|
|
||||||
],
|
|
||||||
[hasEnginePowerBoost, 0.2],
|
|
||||||
[hasImprovedEnginePowerBoost, 0.4],
|
|
||||||
[hasStandardFuel, 0.03],
|
|
||||||
[hasImprovedFuel, 0.1],
|
|
||||||
[hasGearOil, 0.03],
|
|
||||||
[hasImprovedGearOil, 0.06],
|
|
||||||
);
|
|
||||||
const turretTraverseCoefficient =
|
|
||||||
coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) *
|
|
||||||
coefficient([true, progressiveStat(crewMemberMastery)]);
|
|
||||||
const hullTraverseCoefficient =
|
|
||||||
coefficient(
|
|
||||||
[hasImprovedControl, 0.1],
|
|
||||||
[hasImprovedEnginePowerBoost, 0.05],
|
|
||||||
[true, crewSkills.virtuoso / 100],
|
|
||||||
) * coefficient([true, progressiveStat(crewMemberMastery)]);
|
|
||||||
const resistanceCoefficient = coefficient([hasImprovedSuspension, -0.25]);
|
|
||||||
const viewRangeCoefficient =
|
|
||||||
coefficient([
|
|
||||||
hasImprovedOptics,
|
|
||||||
tank.class === 'AT-SPG' ? 0.05 : tank.class === 'heavyTank' ? 0.07 : 0.1,
|
|
||||||
]) * coefficient([true, progressiveStat(commanderMastery)]);
|
|
||||||
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
|
|
||||||
|
|
||||||
const speedForwardsSum = sum(
|
|
||||||
[hasImprovedEnginePowerBoost, 5],
|
|
||||||
[hasGearOil, 2],
|
|
||||||
[hasImprovedGearOil, 4],
|
|
||||||
);
|
|
||||||
const speedBackwardsSum = sum(
|
|
||||||
[hasImprovedEnginePowerBoost, 10],
|
|
||||||
[hasGearOil, 2],
|
|
||||||
[hasImprovedGearOil, 4],
|
|
||||||
);
|
|
||||||
const camouflageSumMoving = sum(
|
|
||||||
[
|
|
||||||
hasCamouflageNet,
|
|
||||||
tank.class === 'heavyTank' ? 0.03 : tank.class === 'AT-SPG' ? 0.07 : 0.05,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
camouflage,
|
|
||||||
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
const camouflageSumStill = sum(
|
|
||||||
[
|
|
||||||
hasCamouflageNet,
|
|
||||||
2 *
|
|
||||||
(tank.class === 'heavyTank'
|
|
||||||
? 0.03
|
|
||||||
: tank.class === 'AT-SPG'
|
|
||||||
? 0.07
|
|
||||||
: 0.05),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
camouflage,
|
|
||||||
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
|
|
||||||
],
|
|
||||||
[true, crewSkills.camouflage * (3 / 100)],
|
|
||||||
);
|
|
||||||
|
|
||||||
const resolvedEnginePower = engine.power * enginePowerCoefficient;
|
|
||||||
const dpm = resolveDpm(
|
|
||||||
gun,
|
|
||||||
shell,
|
|
||||||
damageCoefficient,
|
|
||||||
reloadCoefficient,
|
|
||||||
intraClipCoefficient,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (penetrationDistanceInput.current) {
|
if (penetrationDistanceInput.current) {
|
||||||
@@ -291,22 +155,14 @@ export function Characteristics() {
|
|||||||
})}
|
})}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Info name="Gun type">{GUN_TYPE_NAMES[gun.type]}</Info>
|
<Info name="Gun type">{GUN_TYPE_NAMES[gun.type]}</Info>
|
||||||
<InfoWithDelta name="DPM" decimals={0} unit="hp / min">
|
<InfoWithDelta name="DPM" decimals={0} unit="hp / min">
|
||||||
{dpm}
|
{stats.dpm}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
{gun.type === 'autoReloader' && (
|
{gun.type === 'autoReloader' && (
|
||||||
<>
|
<>
|
||||||
<InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min">
|
<InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min">
|
||||||
{gun.reload.at(-1)! < gun.reload.at(-2)!
|
{stats.dpmMaximum!}
|
||||||
? ((damageCoefficient * shell.damage.armor) /
|
|
||||||
(gun.reload.at(-1)! * reloadCoefficient +
|
|
||||||
gun.intraClip * intraClipCoefficient)) *
|
|
||||||
60
|
|
||||||
: ((damageCoefficient * shell.damage.armor) /
|
|
||||||
(gun.reload[0] * reloadCoefficient)) *
|
|
||||||
60}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={0}
|
decimals={0}
|
||||||
@@ -314,34 +170,14 @@ export function Characteristics() {
|
|||||||
name="Effective at 60s"
|
name="Effective at 60s"
|
||||||
unit="hp / min"
|
unit="hp / min"
|
||||||
>
|
>
|
||||||
{gun.reload.at(-1)! < gun.reload.at(-2)!
|
{stats.dpmEffective!}
|
||||||
? ((damageCoefficient * shell.damage.armor) /
|
|
||||||
(gun.reload.at(-1)! * reloadCoefficient +
|
|
||||||
gun.intraClip * intraClipCoefficient)) *
|
|
||||||
(60 -
|
|
||||||
(gun.reload.slice(0, -1).length - 1) *
|
|
||||||
gun.intraClip *
|
|
||||||
intraClipCoefficient) +
|
|
||||||
damageCoefficient *
|
|
||||||
shell.damage.armor *
|
|
||||||
gun.reload.slice(0, -1).length
|
|
||||||
: ((damageCoefficient * shell.damage.armor) /
|
|
||||||
(gun.reload[0] * reloadCoefficient +
|
|
||||||
gun.intraClip * intraClipCoefficient)) *
|
|
||||||
(60 -
|
|
||||||
(gun.reload.slice(1).length - 1) *
|
|
||||||
gun.intraClip *
|
|
||||||
intraClipCoefficient) +
|
|
||||||
damageCoefficient *
|
|
||||||
shell.damage.armor *
|
|
||||||
gun.reload.slice(1).length}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<Info
|
<Info
|
||||||
indent
|
indent
|
||||||
name={
|
name={
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://tresabhi.github.io/blitzkrieg/guide/dpm.html"
|
href="https://tresabhi.github.io/blitzkit/guide/dpm.html"
|
||||||
>
|
>
|
||||||
What's the difference?
|
What's the difference?
|
||||||
</a>
|
</a>
|
||||||
@@ -349,14 +185,16 @@ export function Characteristics() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{gun.type !== 'regular' && (
|
||||||
|
<InfoWithDelta name="Shells">{stats.shells}</InfoWithDelta>
|
||||||
|
)}
|
||||||
{gun.type === 'autoReloader' ? (
|
{gun.type === 'autoReloader' ? (
|
||||||
<>
|
<>
|
||||||
<InfoWithDelta name="Shells">{gun.reload.length}</InfoWithDelta>
|
|
||||||
<Info indent name="Most optimal">
|
<Info indent name="Most optimal">
|
||||||
{gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1}
|
{stats.mostOptimalShellIndex}
|
||||||
</Info>
|
</Info>
|
||||||
<Info name="Shell reloads" unit="s" />
|
<Info name="Shell reloads" unit="s" />
|
||||||
{gun.reload.map((reload, index) => (
|
{stats.shellReloads!.map((reload, index) => (
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
key={index}
|
key={index}
|
||||||
indent
|
indent
|
||||||
@@ -364,7 +202,7 @@ export function Characteristics() {
|
|||||||
decimals={2}
|
decimals={2}
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{reload * reloadCoefficient}
|
{reload}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -375,26 +213,22 @@ export function Characteristics() {
|
|||||||
unit="s"
|
unit="s"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.reload * reloadCoefficient}
|
{stats.shellReload!}
|
||||||
|
</InfoWithDelta>
|
||||||
|
)}
|
||||||
|
{(gun.type === 'autoLoader' || gun.type === 'autoReloader') && (
|
||||||
|
<InfoWithDelta
|
||||||
|
indent
|
||||||
|
decimals={2}
|
||||||
|
name="Intra-clip"
|
||||||
|
unit="s"
|
||||||
|
deltaType="lowerIsBetter"
|
||||||
|
>
|
||||||
|
{stats.intraClip!}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
)}
|
)}
|
||||||
{gun.type === 'autoLoader' ||
|
|
||||||
(gun.type === 'autoReloader' && (
|
|
||||||
<InfoWithDelta
|
|
||||||
indent
|
|
||||||
decimals={2}
|
|
||||||
name="Intra-clip"
|
|
||||||
unit="s"
|
|
||||||
deltaType="lowerIsBetter"
|
|
||||||
>
|
|
||||||
{gun.intraClip * intraClipCoefficient}
|
|
||||||
</InfoWithDelta>
|
|
||||||
))}
|
|
||||||
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
|
|
||||||
{shell.caliber}
|
|
||||||
</InfoWithDelta>
|
|
||||||
<InfoWithDelta decimals={0} name="Penetration" unit="mm">
|
<InfoWithDelta decimals={0} name="Penetration" unit="mm">
|
||||||
{resolveNearPenetration(shell.penetration) * penetrationCoefficient}
|
{stats.penetration}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
{typeof shell.penetration !== 'number' && (
|
{typeof shell.penetration !== 'number' && (
|
||||||
<>
|
<>
|
||||||
@@ -446,18 +280,34 @@ export function Characteristics() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<InfoWithDelta name="Damage" unit="hp" decimals={0}>
|
<InfoWithDelta name="Damage" unit="hp" decimals={0}>
|
||||||
{shell.damage.armor * damageCoefficient}
|
{stats.damage}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta name="Module damage" unit="hp" decimals={0}>
|
<InfoWithDelta name="Module damage" unit="hp" decimals={0}>
|
||||||
{shell.damage.module * damageCoefficient}
|
{stats.moduleDamage}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
{isExplosive(shell.type) && (
|
{isExplosive(shell.type) && (
|
||||||
<InfoWithDelta name="Splash radius" unit="m" decimals={0}>
|
<InfoWithDelta name="Splash radius" unit="m" decimals={0}>
|
||||||
{shell.explosionRadius!}
|
{stats.explosionRadius!}
|
||||||
|
</InfoWithDelta>
|
||||||
|
)}
|
||||||
|
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
|
||||||
|
{stats.caliber}
|
||||||
|
</InfoWithDelta>
|
||||||
|
<InfoWithDelta name="Normalization" decimals={0} unit="°">
|
||||||
|
{stats.shellNormalization}
|
||||||
|
</InfoWithDelta>
|
||||||
|
{shell.type !== 'he' && (
|
||||||
|
<InfoWithDelta
|
||||||
|
name="Ricochet"
|
||||||
|
decimals={0}
|
||||||
|
deltaType="lowerIsBetter"
|
||||||
|
unit="°"
|
||||||
|
>
|
||||||
|
{stats.shellRicochet!}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
)}
|
)}
|
||||||
<InfoWithDelta name="Shell velocity" unit="m/s" decimals={0}>
|
<InfoWithDelta name="Shell velocity" unit="m/s" decimals={0}>
|
||||||
{shell.speed * shellVelocityCoefficient}
|
{stats.shellVelocity}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={2}
|
decimals={2}
|
||||||
@@ -465,7 +315,7 @@ export function Characteristics() {
|
|||||||
unit="s"
|
unit="s"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.aimTime * aimTimeCoefficient}
|
{stats.aimTime}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<Info name="Dispersion at 100m" />
|
<Info name="Dispersion at 100m" />
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
@@ -475,7 +325,7 @@ export function Characteristics() {
|
|||||||
unit="m"
|
unit="m"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.dispersion.base * dispersionStillCoefficient}
|
{stats.dispersion}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
prefix="+ "
|
prefix="+ "
|
||||||
@@ -484,7 +334,7 @@ export function Characteristics() {
|
|||||||
name="Moving"
|
name="Moving"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{track.dispersion.move * dispersionMovingCoefficient}
|
{stats.dispersionMoving}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={3}
|
decimals={3}
|
||||||
@@ -493,7 +343,7 @@ export function Characteristics() {
|
|||||||
name="Hull traversing"
|
name="Hull traversing"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{track.dispersion.traverse * dispersionHullTraverseCoefficient}
|
{stats.dispersionHullTraversing}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={3}
|
decimals={3}
|
||||||
@@ -502,7 +352,7 @@ export function Characteristics() {
|
|||||||
name="Turret traversing"
|
name="Turret traversing"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.dispersion.traverse * dispersionTurretTraverseCoefficient}
|
{stats.dispersionTurretTraversing}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={3}
|
decimals={3}
|
||||||
@@ -511,7 +361,7 @@ export function Characteristics() {
|
|||||||
name="After shooting"
|
name="After shooting"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.dispersion.shot * dispersionAfterShotCoefficient}
|
{stats.dispersionShooting}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={1}
|
decimals={1}
|
||||||
@@ -521,46 +371,42 @@ export function Characteristics() {
|
|||||||
unit="scalar"
|
unit="scalar"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.dispersion.damaged * dispersionDamagedCoefficient}
|
{stats.dispersionGunDamaged}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<Info name="Gun flexibility" unit="°" />
|
<Info name="Gun flexibility" unit="°" />
|
||||||
<InfoWithDelta decimals={1} indent name="Depression">
|
<InfoWithDelta decimals={1} indent name="Depression">
|
||||||
{gunModelDefinition.pitch.max +
|
{stats.gunDepression}
|
||||||
gunDefaultPitch +
|
|
||||||
(hasImprovedVerticalStabilizer ? 3 : 0)}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta decimals={1} indent name="Elevation">
|
<InfoWithDelta decimals={1} indent name="Elevation">
|
||||||
{-gunModelDefinition.pitch.min -
|
{stats.gunElevation}
|
||||||
gunDefaultPitch +
|
|
||||||
(hasImprovedVerticalStabilizer ? 3 : 0)}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
{gunModelDefinition.pitch.front && (
|
{gunModelDefinition.pitch.front && (
|
||||||
<>
|
<>
|
||||||
<InfoWithDelta decimals={1} indent name="Frontal depression">
|
<InfoWithDelta decimals={1} indent name="Frontal depression">
|
||||||
{gunModelDefinition.pitch.front.max}
|
{stats.gunFrontalDepression!}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta decimals={1} indent name="Frontal elevation">
|
<InfoWithDelta decimals={1} indent name="Frontal elevation">
|
||||||
{-gunModelDefinition.pitch.front.min}
|
{stats.gunFrontalElevation!}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{gunModelDefinition.pitch.back && (
|
{gunModelDefinition.pitch.back && (
|
||||||
<>
|
<>
|
||||||
<InfoWithDelta decimals={1} indent name="Rear depression">
|
<InfoWithDelta decimals={1} indent name="Rear depression">
|
||||||
{gunModelDefinition.pitch.back.max}
|
{stats.gunRearDepression!}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta decimals={1} indent name="Rear elevation">
|
<InfoWithDelta decimals={1} indent name="Rear elevation">
|
||||||
{-gunModelDefinition.pitch.back.min}
|
{stats.gunRearElevation!}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{turretModelDefinition.yaw && (
|
{turretModelDefinition.yaw && (
|
||||||
<>
|
<>
|
||||||
<InfoWithDelta decimals={1} indent name="Azimuth left">
|
<InfoWithDelta decimals={1} indent name="Azimuth left">
|
||||||
{-turretModelDefinition.yaw.min}
|
{stats.azimuthLeft!}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta decimals={1} indent name="Azimuth right">
|
<InfoWithDelta decimals={1} indent name="Azimuth right">
|
||||||
{turretModelDefinition.yaw.max}
|
{stats.azimuthRight!}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -570,29 +416,13 @@ export function Characteristics() {
|
|||||||
<Heading size="5">Maneuverability</Heading>
|
<Heading size="5">Maneuverability</Heading>
|
||||||
<Info name="Speed" unit="km/hr" />
|
<Info name="Speed" unit="km/hr" />
|
||||||
<InfoWithDelta decimals={0} indent name="Forwards">
|
<InfoWithDelta decimals={0} indent name="Forwards">
|
||||||
{tank.speed.forwards + speedForwardsSum}
|
{stats.speedForwards}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta decimals={0} indent name="Backwards">
|
<InfoWithDelta decimals={0} indent name="Backwards">
|
||||||
{tank.speed.backwards + speedBackwardsSum}
|
{stats.speedBackwards}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta decimals={0} name="Power" unit="hp">
|
<InfoWithDelta decimals={0} name="Engine power" unit="hp">
|
||||||
{resolvedEnginePower}
|
{stats.enginePower}
|
||||||
</InfoWithDelta>
|
|
||||||
<Info name="Power to weight ratio" unit="hp/tn" />
|
|
||||||
<InfoWithDelta decimals={1} indent name="On hard terrain">
|
|
||||||
{resolvedEnginePower /
|
|
||||||
weightTons /
|
|
||||||
(track.resistance.hard * resistanceCoefficient)}
|
|
||||||
</InfoWithDelta>
|
|
||||||
<InfoWithDelta decimals={1} indent name="On medium terrain">
|
|
||||||
{resolvedEnginePower /
|
|
||||||
weightTons /
|
|
||||||
(track.resistance.medium * resistanceCoefficient)}
|
|
||||||
</InfoWithDelta>
|
|
||||||
<InfoWithDelta decimals={1} indent name="On soft terrain">
|
|
||||||
{resolvedEnginePower /
|
|
||||||
weightTons /
|
|
||||||
(track.resistance.soft * resistanceCoefficient)}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={1}
|
decimals={1}
|
||||||
@@ -600,71 +430,76 @@ export function Characteristics() {
|
|||||||
unit="tn"
|
unit="tn"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{weightTons}
|
{stats.weight}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta name="Turret traverse speed" unit="°/s" decimals={0}>
|
<Info name="Power to weight ratio" unit="hp/tn" />
|
||||||
{turret.traverseSpeed * turretTraverseCoefficient}
|
|
||||||
</InfoWithDelta>
|
|
||||||
<Info name="Effective traverse speed" unit="°/s" />
|
|
||||||
<InfoWithDelta decimals={1} indent name="On hard terrain">
|
<InfoWithDelta decimals={1} indent name="On hard terrain">
|
||||||
{(resolvedEnginePower / stockEngine.power) *
|
{stats.powerToWeightRatioHardTerrain}
|
||||||
track.traverseSpeed *
|
|
||||||
hullTraverseCoefficient *
|
|
||||||
(track.resistance.hard /
|
|
||||||
(track.resistance.hard * resistanceCoefficient)) *
|
|
||||||
(stockWeight / weight)}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta decimals={1} indent name="On medium terrain">
|
<InfoWithDelta decimals={1} indent name="On medium terrain">
|
||||||
{(resolvedEnginePower / stockEngine.power) *
|
{stats.powerToWeightRatioMediumTerrain}
|
||||||
track.traverseSpeed *
|
|
||||||
hullTraverseCoefficient *
|
|
||||||
(track.resistance.hard /
|
|
||||||
(track.resistance.medium * resistanceCoefficient)) *
|
|
||||||
(stockWeight / weight)}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta decimals={1} indent name="On soft terrain">
|
<InfoWithDelta decimals={1} indent name="On soft terrain">
|
||||||
{(resolvedEnginePower / stockEngine.power) *
|
{stats.powerToWeightRatioSoftTerrain}
|
||||||
track.traverseSpeed *
|
</InfoWithDelta>
|
||||||
hullTraverseCoefficient *
|
<InfoWithDelta name="Turret traverse speed" unit="°/s" decimals={1}>
|
||||||
(track.resistance.hard /
|
{stats.turretTraverseSpeed}
|
||||||
(track.resistance.soft * resistanceCoefficient)) *
|
</InfoWithDelta>
|
||||||
(stockWeight / weight)}
|
<Info name="Hull traverse speed" unit="°/s" />
|
||||||
|
<InfoWithDelta decimals={1} indent name="On hard terrain">
|
||||||
|
{stats.hullTraverseHardTerrain}
|
||||||
|
</InfoWithDelta>
|
||||||
|
<InfoWithDelta decimals={1} indent name="On medium terrain">
|
||||||
|
{stats.hullTraverseMediumTerrain}
|
||||||
|
</InfoWithDelta>
|
||||||
|
<InfoWithDelta decimals={1} indent name="On soft terrain">
|
||||||
|
{stats.hullTraverseSoftTerrain}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Heading size="5">Survivability</Heading>
|
<Heading size="5">Survivability</Heading>
|
||||||
<InfoWithDelta name="Health" unit="hp" decimals={0}>
|
<InfoWithDelta name="Health" unit="hp" decimals={0}>
|
||||||
{(tank.health + turret.health) * healthCoefficient}
|
{stats.health}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta name="Fire chance" unit="%" deltaType="lowerIsBetter">
|
<InfoWithDelta
|
||||||
{Math.round(engine.fireChance * fireChanceCoefficient * 100)}
|
name="Fire chance"
|
||||||
|
unit="%"
|
||||||
|
deltaType="lowerIsBetter"
|
||||||
|
decimals={0}
|
||||||
|
>
|
||||||
|
{stats.fireChance * 100}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta name="View range" unit="m" decimals={0}>
|
<InfoWithDelta name="View range" unit="m" decimals={0}>
|
||||||
{turret.viewRange * viewRangeCoefficient}
|
{stats.viewRange}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<Info name="Camouflage" unit="%" />
|
<Info name="Camouflage" unit="%" />
|
||||||
<InfoWithDelta indent name="Still" decimals={2}>
|
<InfoWithDelta indent name="Still" decimals={0}>
|
||||||
{(tank.camouflage.still + camouflageSumStill) * 100}
|
{stats.camouflageStill * 100}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta indent name="Moving" decimals={2}>
|
<InfoWithDelta indent name="Moving" decimals={0}>
|
||||||
{(tank.camouflage.moving + camouflageSumMoving) * 100}
|
{stats.camouflageMoving * 100}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta indent name="Shooting still" decimals={2}>
|
<InfoWithDelta indent name="Shooting still" decimals={0}>
|
||||||
{(tank.camouflage.still * gun.camouflageLoss + camouflageSumStill) *
|
{stats.camouflageShootingStill * 100}
|
||||||
100}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta indent name="Shooting on move" decimals={2}>
|
<InfoWithDelta indent name="Shooting moving" decimals={0}>
|
||||||
{(tank.camouflage.moving * gun.camouflageLoss + camouflageSumMoving) *
|
{stats.camouflageShootingMoving * 100}
|
||||||
100}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta indent name="Caught on fire" decimals={2}>
|
<InfoWithDelta indent name="Caught on fire" decimals={0}>
|
||||||
{(tank.camouflage.onFire * tank.camouflage.still +
|
{stats.camouflageCaughtOnFire * 100}
|
||||||
camouflageSumStill) *
|
|
||||||
100}
|
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<Info name="Size" unit="m">
|
<Info name="Width" unit="m" decimals={0} deltaType="lowerIsBetter">
|
||||||
{size[0].toFixed(2)} x {size[2].toFixed(2)} x {size[1].toFixed(2)}
|
{stats.width}
|
||||||
|
</Info>
|
||||||
|
<Info name="Height" unit="m" decimals={0} deltaType="lowerIsBetter">
|
||||||
|
{stats.height}
|
||||||
|
</Info>
|
||||||
|
<Info name="Length" unit="m" decimals={0} deltaType="lowerIsBetter">
|
||||||
|
{stats.length}
|
||||||
|
</Info>
|
||||||
|
<Info name="Volume" unit="m" decimals={0} deltaType="lowerIsBetter">
|
||||||
|
{stats.volume}
|
||||||
</Info>
|
</Info>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@@ -692,7 +527,7 @@ export function Characteristics() {
|
|||||||
const count = member.count ?? 1;
|
const count = member.count ?? 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={member.type}>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
key={`${member.type}-root`}
|
key={`${member.type}-root`}
|
||||||
name={`${CREW_MEMBER_NAMES[member.type]}${count > 1 ? ` x ${count}` : ''}`}
|
name={`${CREW_MEMBER_NAMES[member.type]}${count > 1 ? ` x ${count}` : ''}`}
|
||||||
@@ -721,7 +556,7 @@ export function Characteristics() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { useEffect, useRef } from 'react';
|
|||||||
import { Vector3 } from 'three';
|
import { Vector3 } from 'three';
|
||||||
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
||||||
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { hasEquipment } from '../../../../../core/blitzkrieg/hasEquipment';
|
import { hasEquipment } from '../../../../../core/blitzkit/hasEquipment';
|
||||||
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../core/blitzkit/modelDefinitions';
|
||||||
import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose';
|
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
|
||||||
import { useAwait } from '../../../../../hooks/useAwait';
|
import { useAwait } from '../../../../../hooks/useAwait';
|
||||||
import { useDuel } from '../../../../../stores/duel';
|
import { useDuel } from '../../../../../stores/duel';
|
||||||
import { useTankopediaPersistent } from '../../../../../stores/tankopedia';
|
import { useTankopediaPersistent } from '../../../../../stores/tankopedia';
|
||||||
@@ -69,7 +69,12 @@ export function Controls() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function handlePoseEvent(event: Pose) {
|
async function handlePoseEvent(event: Pose) {
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(122);
|
const duel = useDuel.getState();
|
||||||
|
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||||
|
122,
|
||||||
|
duel.protagonist!.tank.equipment,
|
||||||
|
duel.protagonist!.equipmentMatrix,
|
||||||
|
);
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case Pose.HullDown: {
|
case Pose.HullDown: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LightningBoltIcon } from '@radix-ui/react-icons';
|
import { LightningBoltIcon } from '@radix-ui/react-icons';
|
||||||
import { Button, Card, Flex, Tabs, Theme } from '@radix-ui/themes';
|
import { Button, Card, Flex, Tabs, Text, Theme } from '@radix-ui/themes';
|
||||||
import { PerspectiveCamera } from '@react-three/drei';
|
import { PerspectiveCamera } from '@react-three/drei';
|
||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
||||||
@@ -7,13 +7,14 @@ import { isMobile } from 'react-device-detect';
|
|||||||
import { Armor } from '../../../../../../components/Armor';
|
import { Armor } from '../../../../../../components/Armor';
|
||||||
import { ShotDisplay } from '../../../../../../components/Armor/components/ShotDisplay';
|
import { ShotDisplay } from '../../../../../../components/Armor/components/ShotDisplay';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../../core/blitzkit/modelDefinitions';
|
||||||
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
|
||||||
import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose';
|
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
|
||||||
import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon';
|
import { tankIcon } from '../../../../../../core/blitzkit/tankIcon';
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
||||||
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
||||||
|
import { BlitzkitWide } from '../../../../../../icons/BlitzkitWide';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
||||||
import mutateTankopediaPersistent, {
|
import mutateTankopediaPersistent, {
|
||||||
TankopediaMode,
|
TankopediaMode,
|
||||||
@@ -150,112 +151,123 @@ export function TankSandbox() {
|
|||||||
}}
|
}}
|
||||||
ref={canvasWrapper}
|
ref={canvasWrapper}
|
||||||
>
|
>
|
||||||
<Theme radius="full" style={{ height: '100%' }}>
|
<Flex
|
||||||
<Flex
|
style={{
|
||||||
style={{
|
height: isFullScreen ? '100%' : '75vh',
|
||||||
height: isFullScreen ? '100%' : '75vh',
|
maxHeight: isFullScreen ? 'unset' : 576,
|
||||||
maxHeight: isFullScreen ? 'unset' : 576,
|
}}
|
||||||
|
direction="column"
|
||||||
|
gap="2"
|
||||||
|
>
|
||||||
|
<Tabs.Root
|
||||||
|
value={mode}
|
||||||
|
onValueChange={(mode) => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.mode = mode as TankopediaMode;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
direction="column"
|
|
||||||
gap="2"
|
|
||||||
>
|
>
|
||||||
<Tabs.Root
|
<Tabs.List>
|
||||||
value={mode}
|
<Tabs.Trigger value="model">
|
||||||
onValueChange={(mode) => {
|
<Flex gap="2" align="center">
|
||||||
mutateTankopediaPersistent((draft) => {
|
Model
|
||||||
draft.mode = mode as TankopediaMode;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs.List>
|
|
||||||
<Tabs.Trigger value="model">
|
|
||||||
<Flex gap="2" align="center">
|
|
||||||
Model
|
|
||||||
</Flex>
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger value="armor">Armor</Tabs.Trigger>
|
|
||||||
</Tabs.List>
|
|
||||||
</Tabs.Root>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: 'calc(100% - 64px)',
|
|
||||||
position: 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loadModel ? (
|
|
||||||
<div style={{ width: '100%', height: '100%' }}>
|
|
||||||
<Canvas
|
|
||||||
shadows
|
|
||||||
ref={canvas}
|
|
||||||
onPointerDown={handlePointerDown}
|
|
||||||
onPointerMissed={() => {
|
|
||||||
mutateTankopediaTemporary((draft) => {
|
|
||||||
draft.shot = undefined;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PerspectiveCamera makeDefault fov={25} far={32} />
|
|
||||||
<Controls />
|
|
||||||
<SceneProps />
|
|
||||||
|
|
||||||
<Suspense fallback={<ModelLoader />}>
|
|
||||||
{/* TODO: add rotation cube to the top right */}
|
|
||||||
<Lighting />
|
|
||||||
<TankModel />
|
|
||||||
<Armor />
|
|
||||||
<ShotDisplay />
|
|
||||||
</Suspense>
|
|
||||||
</Canvas>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
style={{ height: '100%', position: 'relative' }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
style={{
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
setLoadModel(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LightningBoltIcon /> Load model
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
style={{
|
|
||||||
filter: 'blur(16px)',
|
|
||||||
width: 256,
|
|
||||||
height: 128,
|
|
||||||
display: 'flex',
|
|
||||||
position: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
transformOrigin: 'center center',
|
|
||||||
transform: 'translate(25%, -25%) scale(300%)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={tankIcon(duel.antagonist!.tank.id, 'big')} />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
</Tabs.Trigger>
|
||||||
</div>
|
<Tabs.Trigger value="armor">Armor</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
</Tabs.Root>
|
||||||
|
|
||||||
<Options
|
{wideFormat && (
|
||||||
canvasWrapper={canvasWrapper}
|
<Text
|
||||||
isFullScreen={isFullScreen}
|
color="gray"
|
||||||
/>
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
top: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BlitzkitWide height={32} />
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{isFullScreen && <AntagonistBar floating />}
|
<div
|
||||||
|
style={{
|
||||||
|
height: 'calc(100% - 64px)',
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loadModel ? (
|
||||||
|
<div style={{ width: '100%', height: '100%' }}>
|
||||||
|
<Canvas
|
||||||
|
gl={{
|
||||||
|
clippingPlanes: Object.freeze([]),
|
||||||
|
localClippingEnabled: true,
|
||||||
|
}}
|
||||||
|
shadows
|
||||||
|
ref={canvas}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerMissed={() => {
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PerspectiveCamera makeDefault fov={25} far={32} />
|
||||||
|
<Controls />
|
||||||
|
<SceneProps />
|
||||||
|
|
||||||
{loadModel && <RotationInputs isFullScreen={isFullScreen} />}
|
<Suspense fallback={<ModelLoader />}>
|
||||||
</Flex>
|
<Lighting />
|
||||||
</Theme>
|
<TankModel />
|
||||||
|
<Armor />
|
||||||
|
<ShotDisplay />
|
||||||
|
</Suspense>
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
style={{ height: '100%', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setLoadModel(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LightningBoltIcon /> Load model
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
filter: 'blur(16px)',
|
||||||
|
width: 256,
|
||||||
|
height: 128,
|
||||||
|
display: 'flex',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transformOrigin: 'center center',
|
||||||
|
transform: 'translate(25%, -25%) scale(300%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={tankIcon(duel.antagonist!.tank.id, 'big')} />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Options canvasWrapper={canvasWrapper} isFullScreen={isFullScreen} />
|
||||||
|
|
||||||
|
{isFullScreen && <AntagonistBar floating />}
|
||||||
|
|
||||||
|
{loadModel && <RotationInputs isFullScreen={isFullScreen} />}
|
||||||
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
</Theme>
|
</Theme>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { Html, useProgress } from '@react-three/drei';
|
import { Html, useProgress } from '@react-three/drei';
|
||||||
import { theme } from '../../../../../../../stitches.config';
|
import { Loader } from '../../../../../../../components/Loader';
|
||||||
import { Loader } from '../../../../../components/Loader';
|
|
||||||
|
|
||||||
export function ModelLoader() {
|
export function ModelLoader() {
|
||||||
const { progress } = useProgress();
|
const { progress } = useProgress();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html center position={[0, 1.5, 0]}>
|
<Html center position={[0, 1.5, 0]}>
|
||||||
<Loader
|
<Loader naked progress={progress / 100} />
|
||||||
naked
|
|
||||||
color={theme.colors.textHighContrast_purple}
|
|
||||||
progress={progress / 100}
|
|
||||||
/>
|
|
||||||
</Html>
|
</Html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
} from '@radix-ui/react-icons';
|
} from '@radix-ui/react-icons';
|
||||||
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
|
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
|
||||||
import { RefObject } from 'react';
|
import { RefObject } from 'react';
|
||||||
import { Pose, poseEvent } from '../../../../../../../core/blitzkrieg/pose';
|
import { Pose, poseEvent } from '../../../../../../../core/blitzkit/pose';
|
||||||
|
import { useApp } from '../../../../../../../stores/app';
|
||||||
import mutateTankopediaPersistent, {
|
import mutateTankopediaPersistent, {
|
||||||
mutateTankopediaTemporary,
|
mutateTankopediaTemporary,
|
||||||
useTankopediaPersistent,
|
useTankopediaPersistent,
|
||||||
@@ -40,6 +41,7 @@ export function Options({ isFullScreen }: OptionsProps) {
|
|||||||
const environment = useTankopediaPersistent(
|
const environment = useTankopediaPersistent(
|
||||||
(state) => state.model.visual.environment,
|
(state) => state.model.visual.environment,
|
||||||
);
|
);
|
||||||
|
const developerMode = useApp((state) => state.developerMode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -107,16 +109,18 @@ export function Options({ isFullScreen }: OptionsProps) {
|
|||||||
Green penetration
|
Green penetration
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
{developerMode && (
|
||||||
checked={wireframe}
|
<DropdownMenu.CheckboxItem
|
||||||
onCheckedChange={(checked) => {
|
checked={wireframe}
|
||||||
mutateTankopediaPersistent((draft) => {
|
onCheckedChange={(checked) => {
|
||||||
draft.model.visual.wireframe = checked;
|
mutateTankopediaPersistent((draft) => {
|
||||||
});
|
draft.model.visual.wireframe = checked;
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
Wireframe
|
>
|
||||||
</DropdownMenu.CheckboxItem>
|
Wireframe
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={opaque}
|
checked={opaque}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { Euler, Group, Vector3 } from 'three';
|
|||||||
import { degToRad } from 'three/src/math/MathUtils';
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis';
|
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis';
|
||||||
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { hasEquipment } from '../../../../../../../core/blitzkrieg/hasEquipment';
|
import { hasEquipment } from '../../../../../../../core/blitzkit/hasEquipment';
|
||||||
import { jsxTree } from '../../../../../../../core/blitzkrieg/jsxTree';
|
import { jsxTree } from '../../../../../../../core/blitzkit/jsxTree';
|
||||||
import { modelDefinitions } from '../../../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../../../core/blitzkit/modelDefinitions';
|
||||||
import {
|
import {
|
||||||
ModelTransformEventData,
|
ModelTransformEventData,
|
||||||
modelTransformEvent,
|
modelTransformEvent,
|
||||||
} from '../../../../../../../core/blitzkrieg/modelTransform';
|
} from '../../../../../../../core/blitzkit/modelTransform';
|
||||||
import { normalizeAngleRad } from '../../../../../../../core/math/normalizeAngleRad';
|
import { normalizeAngleRad } from '../../../../../../../core/math/normalizeAngleRad';
|
||||||
import { useAwait } from '../../../../../../../hooks/useAwait';
|
import { useAwait } from '../../../../../../../hooks/useAwait';
|
||||||
import { useModel } from '../../../../../../../hooks/useModel';
|
import { useModel } from '../../../../../../../hooks/useModel';
|
||||||
@@ -124,11 +124,15 @@ export const TankModel = memo(() => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsxTree(node, {
|
return jsxTree(
|
||||||
castShadow: true,
|
node,
|
||||||
receiveShadow: true,
|
{
|
||||||
onPointerDown,
|
castShadow: true,
|
||||||
});
|
receiveShadow: true,
|
||||||
|
onPointerDown,
|
||||||
|
},
|
||||||
|
node.uuid,
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<group ref={turretContainer}>
|
<group ref={turretContainer}>
|
||||||
@@ -162,7 +166,12 @@ export const TankModel = memo(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
async function handlePointerMove(event: PointerEvent) {
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(122);
|
const duel = useDuel.getState();
|
||||||
|
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||||
|
122,
|
||||||
|
duel.protagonist!.tank.equipment,
|
||||||
|
duel.protagonist!.equipmentMatrix,
|
||||||
|
);
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
[pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch,
|
pitch,
|
||||||
yaw + event.movementX * (Math.PI / canvas.width),
|
yaw + event.movementX * (Math.PI / canvas.width),
|
||||||
@@ -184,11 +193,15 @@ export const TankModel = memo(() => {
|
|||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsxTree(node, {
|
return jsxTree(
|
||||||
castShadow: true,
|
node,
|
||||||
receiveShadow: true,
|
{
|
||||||
onPointerDown,
|
castShadow: true,
|
||||||
});
|
receiveShadow: true,
|
||||||
|
onPointerDown,
|
||||||
|
},
|
||||||
|
node.uuid,
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<group ref={gunContainer}>
|
<group ref={gunContainer}>
|
||||||
@@ -223,7 +236,12 @@ export const TankModel = memo(() => {
|
|||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
async function handlePointerMove(event: PointerEvent) {
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(122);
|
const duel = useDuel.getState();
|
||||||
|
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||||
|
122,
|
||||||
|
duel.protagonist!.tank.equipment,
|
||||||
|
duel.protagonist!.equipmentMatrix,
|
||||||
|
);
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
[pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch - event.movementY * (Math.PI / canvas.height),
|
pitch - event.movementY * (Math.PI / canvas.height),
|
||||||
yaw + event.movementX * (Math.PI / canvas.width),
|
yaw + event.movementX * (Math.PI / canvas.width),
|
||||||
@@ -245,11 +263,15 @@ export const TankModel = memo(() => {
|
|||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsxTree(node, {
|
return jsxTree(
|
||||||
castShadow: true,
|
node,
|
||||||
receiveShadow: true,
|
{
|
||||||
onPointerDown,
|
castShadow: true,
|
||||||
});
|
receiveShadow: true,
|
||||||
|
onPointerDown,
|
||||||
|
},
|
||||||
|
node.uuid,
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@radix-ui/themes';
|
import { Button } from '@radix-ui/themes';
|
||||||
import { ComponentProps } from 'react';
|
import { ComponentProps } from 'react';
|
||||||
import { asset } from '../../../../../../../core/blitzkrieg/asset';
|
import { asset } from '../../../../../../../core/blitzkit/asset';
|
||||||
|
|
||||||
interface QuickEquipmentButtonProps extends ComponentProps<typeof Button> {
|
interface QuickEquipmentButtonProps extends ComponentProps<typeof Button> {
|
||||||
equipment: number;
|
equipment: number;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Flex, TextField } from '@radix-ui/themes';
|
|||||||
import { use, useEffect, useRef } from 'react';
|
import { use, useEffect, useRef } from 'react';
|
||||||
import { degToRad, radToDeg } from 'three/src/math/MathUtils';
|
import { degToRad, radToDeg } from 'three/src/math/MathUtils';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../../core/blitzkit/modelDefinitions';
|
||||||
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
||||||
import { useTankopediaPersistent } from '../../../../../../stores/tankopedia';
|
import { useTankopediaPersistent } from '../../../../../../stores/tankopedia';
|
||||||
@@ -52,6 +52,7 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
>
|
>
|
||||||
<Flex gap="2" style={{ width: 256, pointerEvents: 'auto' }}>
|
<Flex gap="2" style={{ width: 256, pointerEvents: 'auto' }}>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
|
radius="full"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
@@ -99,6 +100,7 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
|
radius="full"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
@@ -159,7 +161,9 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
active={hasCalibratedShells}
|
active={hasCalibratedShells}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.antagonist!.equipment[0][0] = hasCalibratedShells ? 0 : 1;
|
draft.antagonist!.equipmentMatrix[0][0] = hasCalibratedShells
|
||||||
|
? 0
|
||||||
|
: 1;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -168,7 +172,9 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
active={hasEnhancedArmor}
|
active={hasEnhancedArmor}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.protagonist!.equipment[1][1] = hasEnhancedArmor ? 0 : -1;
|
draft.protagonist!.equipmentMatrix[1][1] = hasEnhancedArmor
|
||||||
|
? 0
|
||||||
|
: -1;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,21 +1,31 @@
|
|||||||
import { UpdateIcon } from '@radix-ui/react-icons';
|
import { CaretRightIcon, UpdateIcon } from '@radix-ui/react-icons';
|
||||||
import { Button, Dialog, Flex, Heading } from '@radix-ui/themes';
|
import { Button, Dialog, Flex, Heading } from '@radix-ui/themes';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TREE_TYPE_ICONS } from '../../../../../components/Tanks/components/Item/constants';
|
import { TREE_TYPE_ICONS } from '../../../../../components/Tanks/components/Item/constants';
|
||||||
import { assignDuelMember } from '../../../../../core/blitzkrieg/assignDuelMember';
|
import { assignDuelMember } from '../../../../../core/blitzkit/assignDuelMember';
|
||||||
import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopediaPath';
|
import { pushTankopediaPath } from '../../../../../core/blitzkit/pushTankopediaPath';
|
||||||
import { useDuel } from '../../../../../stores/duel';
|
import { useDuel } from '../../../../../stores/duel';
|
||||||
import { TankSearch } from '../../components/TankSearch';
|
import { TankSearch } from '../../components/TankSearch';
|
||||||
|
|
||||||
export function Title() {
|
export function Title() {
|
||||||
const protagonist = useDuel((state) => state.protagonist!);
|
const protagonist = useDuel((state) => state.protagonist!);
|
||||||
|
const antagonist = useDuel((state) => state.antagonist!);
|
||||||
|
const compareTanks =
|
||||||
|
protagonist.tank.id === antagonist.tank.id
|
||||||
|
? [protagonist.tank.id]
|
||||||
|
: [protagonist.tank.id, antagonist.tank.id];
|
||||||
const [changeTankDialogOpen, setChangeTankDialogOpen] = useState(false);
|
const [changeTankDialogOpen, setChangeTankDialogOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>{protagonist.tank.name}</title>
|
<Flex
|
||||||
|
gap="6"
|
||||||
<Flex gap="6" justify="center" align="center" style={{ padding: 16 }}>
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
style={{ padding: 16 }}
|
||||||
|
wrap="wrap"
|
||||||
|
>
|
||||||
<Flex gap="2" align="center">
|
<Flex gap="2" align="center">
|
||||||
<Heading
|
<Heading
|
||||||
color={
|
color={
|
||||||
@@ -43,36 +53,44 @@ export function Title() {
|
|||||||
</Heading>
|
</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Dialog.Root
|
<Flex gap="6">
|
||||||
open={changeTankDialogOpen}
|
<Dialog.Root
|
||||||
onOpenChange={setChangeTankDialogOpen}
|
open={changeTankDialogOpen}
|
||||||
>
|
onOpenChange={setChangeTankDialogOpen}
|
||||||
<Dialog.Trigger>
|
>
|
||||||
<Button variant="ghost">
|
<Dialog.Trigger>
|
||||||
Change <UpdateIcon />
|
<Button variant="ghost">
|
||||||
</Button>
|
Change <UpdateIcon />
|
||||||
</Dialog.Trigger>
|
</Button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Flex gap="4" direction="column">
|
<Flex gap="4" direction="column">
|
||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
gap="4"
|
gap="4"
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
assignDuelMember('protagonist', tank.id);
|
assignDuelMember('protagonist', tank.id);
|
||||||
setChangeTankDialogOpen(false);
|
setChangeTankDialogOpen(false);
|
||||||
pushTankopediaPath(tank.id);
|
pushTankopediaPath(tank.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Dialog.Content>
|
||||||
</Dialog.Content>
|
</Dialog.Root>
|
||||||
</Dialog.Root>
|
|
||||||
|
<Link href={`/tools/compare?tanks=${compareTanks.join('%2C')}`}>
|
||||||
|
<Button variant="ghost">
|
||||||
|
Compare <CaretRightIcon />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { InfoCircledIcon, PlusIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Callout, Card, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||||
|
import { use } from 'react';
|
||||||
|
import { videoDefinitions } from '../../../../../core/blitzkit/videos';
|
||||||
|
import { useFullScreen } from '../../../../../hooks/useFullScreen';
|
||||||
|
import { useDuel } from '../../../../../stores/duel';
|
||||||
|
|
||||||
|
export function Videos() {
|
||||||
|
const isFullScreen = useFullScreen();
|
||||||
|
const tank = useDuel((state) => state.protagonist!.tank);
|
||||||
|
const awaitedVideoDefinitions = use(videoDefinitions);
|
||||||
|
const videos = awaitedVideoDefinitions[tank.id]?.videos ?? [];
|
||||||
|
|
||||||
|
if (isFullScreen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" gap="4" mt="4" align="start">
|
||||||
|
<Heading size="6">Review videos</Heading>
|
||||||
|
|
||||||
|
<Callout.Root>
|
||||||
|
<Callout.Icon>
|
||||||
|
<InfoCircledIcon />
|
||||||
|
</Callout.Icon>
|
||||||
|
<Callout.Text>
|
||||||
|
Some YouTubers may not have videos on the {tank.name} leading to
|
||||||
|
off-topic videos.
|
||||||
|
</Callout.Text>
|
||||||
|
</Callout.Root>
|
||||||
|
|
||||||
|
<Flex gap="3" wrap="wrap">
|
||||||
|
{videos.map((video) => (
|
||||||
|
<Link
|
||||||
|
key={video}
|
||||||
|
href={`https://www.youtube.com/watch?v=${video}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`https://i.ytimg.com/vi/${video}/hqdefault.jpg`}
|
||||||
|
style={{
|
||||||
|
aspectRatio: '16 / 9',
|
||||||
|
objectFit: 'cover',
|
||||||
|
height: 128,
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Link href="https://discord.gg/nDt7AjGJQH" target="_blank">
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
aspectRatio: '16 / 9',
|
||||||
|
height: 128,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
gap="1"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text color="purple">
|
||||||
|
{videos.length === 0 ? 'Suggest' : 'Suggest more'} <PlusIcon />
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { tankDefinitions } from '../../../../core/blitzkit/tankDefinitions';
|
||||||
|
import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants';
|
||||||
|
import { tankIcon } from '../../../../core/blitzkit/tankIcon';
|
||||||
|
import strings from '../../../../lang/en-US.json';
|
||||||
|
|
||||||
|
interface TankopediaLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
params: { id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function TankopediaLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: TankopediaLayoutProps) {
|
||||||
|
const id = Number(params.id);
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const tank = awaitedTankDefinitions[id];
|
||||||
|
const title = `${tank.name} - Tier ${TIER_ROMAN_NUMERALS[tank.tier]} ${
|
||||||
|
(strings.common.nations as Record<string, string>)[tank.nation]
|
||||||
|
} ${strings.common.tank_class_short[tank.class]}`;
|
||||||
|
const description = `Statistics, armor profiles, and equipment for ${tank.name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:image" content={tankIcon(id)} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { Flex } from '@radix-ui/themes';
|
import { Flex } from '@radix-ui/themes';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import PageWrapper from '../../../../components/PageWrapper';
|
import PageWrapper from '../../../../components/PageWrapper';
|
||||||
import { assignDuelMember } from '../../../../core/blitzkrieg/assignDuelMember';
|
import { assignDuelMember } from '../../../../core/blitzkit/assignDuelMember';
|
||||||
import { useWideFormat } from '../../../../hooks/useWideFormat';
|
import { useWideFormat } from '../../../../hooks/useWideFormat';
|
||||||
import { mutateDuel, useDuel } from '../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../stores/duel';
|
||||||
import { mutateTankopediaTemporary } from '../../../../stores/tankopedia';
|
import { mutateTankopediaTemporary } from '../../../../stores/tankopedia';
|
||||||
@@ -17,6 +17,7 @@ import { Provisions } from './components/Characteristics/components/Provisions';
|
|||||||
import { Skills } from './components/Characteristics/components/Skills';
|
import { Skills } from './components/Characteristics/components/Skills';
|
||||||
import { TankSandbox } from './components/Model/TankSandbox';
|
import { TankSandbox } from './components/Model/TankSandbox';
|
||||||
import { Title } from './components/Title';
|
import { Title } from './components/Title';
|
||||||
|
import { Videos } from './components/Videos';
|
||||||
|
|
||||||
export default function Page({ params }: { params: { id: string } }) {
|
export default function Page({ params }: { params: { id: string } }) {
|
||||||
const initialId = parseInt(params.id);
|
const initialId = parseInt(params.id);
|
||||||
@@ -83,7 +84,7 @@ export default function Page({ params }: { params: { id: string } }) {
|
|||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
top: 64,
|
top: 64 + 16,
|
||||||
position: wideFormat ? 'sticky' : undefined,
|
position: wideFormat ? 'sticky' : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -91,7 +92,6 @@ export default function Page({ params }: { params: { id: string } }) {
|
|||||||
<TankSandbox />
|
<TankSandbox />
|
||||||
<AntagonistBar />
|
<AntagonistBar />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex gap="5" wrap="wrap">
|
<Flex gap="5" wrap="wrap">
|
||||||
<Modules />
|
<Modules />
|
||||||
<Provisions />
|
<Provisions />
|
||||||
@@ -110,6 +110,8 @@ export default function Page({ params }: { params: { id: string } }) {
|
|||||||
<Characteristics />
|
<Characteristics />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
<Videos />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CaretLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
import { CaretLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
||||||
import { Button, Flex, TextField } from '@radix-ui/themes';
|
import { Button, Flex, TextField } from '@radix-ui/themes';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { TankDefinition } from '../../../../core/blitzkrieg/tankDefinitions';
|
import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions';
|
||||||
import mutateTankopediaPersistent, {
|
import mutateTankopediaPersistent, {
|
||||||
useTankopediaPersistent,
|
useTankopediaPersistent,
|
||||||
} from '../../../../stores/tankopedia';
|
} from '../../../../stores/tankopedia';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, Flex, Text } from '@radix-ui/themes';
|
import { Button, Flex, Text } from '@radix-ui/themes';
|
||||||
import { SmallTankIcon } from '../../../../../../components/SmallTankIcon';
|
import { SmallTankIcon } from '../../../../../../components/SmallTankIcon';
|
||||||
import { TankDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
|
import { TankDefinition } from '../../../../../../core/blitzkit/tankDefinitions';
|
||||||
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkit/tankDefinitions/constants';
|
||||||
|
|
||||||
interface CompactSearchResultRowProps {
|
interface CompactSearchResultRowProps {
|
||||||
tanks: TankDefinition[];
|
tanks: TankDefinition[];
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import {
|
|||||||
TREE_TYPE_ICONS,
|
TREE_TYPE_ICONS,
|
||||||
TREE_TYPE_IMAGES,
|
TREE_TYPE_IMAGES,
|
||||||
} from '../../../../../../../components/Tanks/components/Item/constants';
|
} from '../../../../../../../components/Tanks/components/Item/constants';
|
||||||
import { NATIONS } from '../../../../../../../core/blitzkrieg/tankDefinitions';
|
import { NATIONS } from '../../../../../../../core/blitzkit/tankDefinitions';
|
||||||
import {
|
import {
|
||||||
TIERS,
|
TIERS,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
} from '../../../../../../../core/blitzkit/tankDefinitions/constants';
|
||||||
import mutateTankopediaPersistent, {
|
import mutateTankopediaPersistent, {
|
||||||
TankopediaTestTankDisplay,
|
TankopediaTestTankDisplay,
|
||||||
useTankopediaPersistent,
|
useTankopediaPersistent,
|
||||||
@@ -28,32 +28,6 @@ export function Options() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap="2" wrap="wrap" justify="center">
|
<Flex gap="2" wrap="wrap" justify="center">
|
||||||
<DropdownMenu.Root>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<Button variant="soft" color="gray">
|
|
||||||
Test tanks
|
|
||||||
<CaretDownIcon />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
<DropdownMenu.RadioGroup
|
|
||||||
value={filters.test}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.filters.test = value as TankopediaTestTankDisplay;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DropdownMenu.RadioItem value="include">
|
|
||||||
Include
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
<DropdownMenu.RadioItem value="exclude">
|
|
||||||
Exclude
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
<DropdownMenu.RadioItem value="only">Only</DropdownMenu.RadioItem>
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
<Flex>
|
<Flex>
|
||||||
{TREE_TYPES.map((type, index) => (
|
{TREE_TYPES.map((type, index) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -334,6 +308,32 @@ export function Options() {
|
|||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button variant="soft" color="gray">
|
||||||
|
Test tanks
|
||||||
|
<CaretDownIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.RadioGroup
|
||||||
|
value={filters.test}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.filters.test = value as TankopediaTestTankDisplay;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DropdownMenu.RadioItem value="include">
|
||||||
|
Include
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="exclude">
|
||||||
|
Exclude
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="only">Only</DropdownMenu.RadioItem>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { slateDark } from '@radix-ui/colors';
|
import { slateDark } from '@radix-ui/colors';
|
||||||
import {
|
import {
|
||||||
CaretDownIcon,
|
CaretDownIcon,
|
||||||
|
CaretRightIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from '@radix-ui/react-icons';
|
||||||
import {
|
import {
|
||||||
|
AlertDialog,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -19,24 +21,24 @@ import { use, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { TreeType } from '../../../../../components/Tanks';
|
import { TreeType } from '../../../../../components/Tanks';
|
||||||
import { TANK_CLASSES } from '../../../../../components/Tanks/components/Item/constants';
|
import { TANK_CLASSES } from '../../../../../components/Tanks/components/Item/constants';
|
||||||
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
|
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
|
||||||
import { asset } from '../../../../../core/blitzkrieg/asset';
|
import { asset } from '../../../../../core/blitzkit/asset';
|
||||||
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../core/blitzkit/modelDefinitions';
|
||||||
import { normalizeBoundingBox } from '../../../../../core/blitzkrieg/normalizeBoundingBox';
|
import { normalizeBoundingBox } from '../../../../../core/blitzkit/normalizeBoundingBox';
|
||||||
import { resolveDpm } from '../../../../../core/blitzkrieg/resolveDpm';
|
import { resolveDpm } from '../../../../../core/blitzkit/resolveDpm';
|
||||||
import { resolveReload } from '../../../../../core/blitzkrieg/resolveReload';
|
import { resolveReload } from '../../../../../core/blitzkit/resolveReload';
|
||||||
import {
|
import {
|
||||||
NATIONS,
|
NATIONS,
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
tanksDefinitionsArray,
|
tanksDefinitionsArray,
|
||||||
} from '../../../../../core/blitzkrieg/tankDefinitions';
|
} from '../../../../../core/blitzkit/tankDefinitions';
|
||||||
import {
|
import {
|
||||||
TANK_ICONS,
|
TANK_ICONS,
|
||||||
TANK_ICONS_COLLECTOR,
|
TANK_ICONS_COLLECTOR,
|
||||||
TANK_ICONS_PREMIUM,
|
TANK_ICONS_PREMIUM,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '../../../../../core/blitzkrieg/tankDefinitions/constants';
|
} from '../../../../../core/blitzkit/tankDefinitions/constants';
|
||||||
import { tankIcon } from '../../../../../core/blitzkrieg/tankIcon';
|
import { tankIcon } from '../../../../../core/blitzkit/tankIcon';
|
||||||
import { unionBoundingBox } from '../../../../../core/blitzkrieg/unionBoundingBox';
|
import { unionBoundingBox } from '../../../../../core/blitzkit/unionBoundingBox';
|
||||||
import { theme } from '../../../../../stitches.config';
|
import { theme } from '../../../../../stitches.config';
|
||||||
import mutateTankopediaPersistent, {
|
import mutateTankopediaPersistent, {
|
||||||
SORT_NAMES,
|
SORT_NAMES,
|
||||||
@@ -51,11 +53,16 @@ import { Options } from './components/Options';
|
|||||||
interface TankSearchProps {
|
interface TankSearchProps {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
onSelect?: (tank: TankDefinition) => void;
|
onSelect?: (tank: TankDefinition) => void;
|
||||||
|
onSelectAll?: (tanks: TankDefinition[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const treeTypeOrder: TreeType[] = ['researchable', 'premium', 'collector'];
|
const treeTypeOrder: TreeType[] = ['researchable', 'premium', 'collector'];
|
||||||
|
|
||||||
export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
export function TankSearch({
|
||||||
|
compact,
|
||||||
|
onSelect = () => {},
|
||||||
|
onSelectAll,
|
||||||
|
}: TankSearchProps) {
|
||||||
const tanksPerPage = compact ? 16 : 96;
|
const tanksPerPage = compact ? 16 : 96;
|
||||||
const nations = use(NATIONS);
|
const nations = use(NATIONS);
|
||||||
const filters = useTankopediaPersistent((state) => state.filters);
|
const filters = useTankopediaPersistent((state) => state.filters);
|
||||||
@@ -393,20 +400,28 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
.filter(([name]) => name !== 'page')
|
.filter(([name]) => name !== 'page')
|
||||||
.map(([, value]) => value),
|
.map(([, value]) => value),
|
||||||
]);
|
]);
|
||||||
const [searchResults, setSearchedList] = useState(searchableTanks);
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const searchResults = useMemo(
|
||||||
|
() =>
|
||||||
|
go(searchQuery, searchableTanks, {
|
||||||
|
keys: ['name', 'nameFull', 'id'] satisfies (keyof TankDefinition)[],
|
||||||
|
all: true,
|
||||||
|
}).map(({ obj }) => obj),
|
||||||
|
[searchQuery, searchableTanks],
|
||||||
|
);
|
||||||
const page = useTankopediaPersistent((state) => state.filters.page);
|
const page = useTankopediaPersistent((state) => state.filters.page);
|
||||||
const wideFormat = typeof window !== 'undefined' && window.innerWidth > 512;
|
|
||||||
const chunkSize = wideFormat ? tanksPerPage / 2 : Infinity;
|
|
||||||
const searchResultsPageSlice = searchResults.slice(
|
const searchResultsPageSlice = searchResults.slice(
|
||||||
page * tanksPerPage,
|
page * tanksPerPage,
|
||||||
(page + 1) * tanksPerPage,
|
(page + 1) * tanksPerPage,
|
||||||
);
|
);
|
||||||
const firstChunk = searchResultsPageSlice.slice(0, chunkSize);
|
const firstChunk = searchResultsPageSlice.slice(
|
||||||
const secondChunk = searchResultsPageSlice.slice(chunkSize);
|
0,
|
||||||
|
Math.ceil(searchResultsPageSlice.length / 2),
|
||||||
|
);
|
||||||
|
const secondChunk = searchResultsPageSlice.slice(
|
||||||
|
Math.ceil(searchResultsPageSlice.length / 2),
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchedList(searchableTanks);
|
|
||||||
}, [searchableTanks]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.filters.page = Math.min(
|
draft.filters.page = Math.min(
|
||||||
@@ -425,21 +440,36 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
ref={input}
|
ref={input}
|
||||||
placeholder="Search tanks..."
|
placeholder="Search tanks..."
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const results = go(event.target.value, searchableTanks, {
|
setSearchQuery(event.target.value);
|
||||||
keys: [
|
}}
|
||||||
'name',
|
onKeyDown={(event) => {
|
||||||
'nameFull',
|
if (event.key === 'Enter') {
|
||||||
'id',
|
if (
|
||||||
] satisfies (keyof TankDefinition)[],
|
searchResults.length > 0 &&
|
||||||
all: true,
|
input.current &&
|
||||||
}).map(({ obj }) => obj);
|
input.current.value.length > 0
|
||||||
|
) {
|
||||||
setSearchedList(results);
|
onSelect(searchResults[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
|
|
||||||
|
{searchResults.length > 0 &&
|
||||||
|
input.current &&
|
||||||
|
input.current.value.length > 0 && (
|
||||||
|
<TextField.Slot>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => onSelect(searchResults[0])}
|
||||||
|
>
|
||||||
|
{searchResults[0].name} <CaretRightIcon />
|
||||||
|
</Button>
|
||||||
|
</TextField.Slot>
|
||||||
|
)}
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
@@ -796,16 +826,63 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
<Options />
|
<Options />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex justify="center">
|
<Flex justify="center" gap="4" align="center">
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
Sorting: {SORT_NAMES[sort.by]}, {sort.direction}
|
Sorting: {SORT_NAMES[sort.by]}, {sort.direction}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{onSelectAll && searchResults.length <= 8 && (
|
||||||
|
<Button variant="ghost" onClick={() => onSelectAll(searchResults)}>
|
||||||
|
Select all
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onSelectAll && searchResults.length > 8 && (
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<AlertDialog.Trigger>
|
||||||
|
<Button variant="ghost">Select all</Button>
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Title>
|
||||||
|
Woah! That's a lot of tanks.
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
Would you like to select{' '}
|
||||||
|
<Text color="red">all {searchResults.length} tanks</Text>?
|
||||||
|
</AlertDialog.Description>
|
||||||
|
|
||||||
|
<Flex gap="2" justify="end">
|
||||||
|
<AlertDialog.Cancel>
|
||||||
|
<Button variant="soft" color="gray">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
|
||||||
|
<AlertDialog.Action onClick={() => onSelectAll(searchResults)}>
|
||||||
|
<Button variant="solid" color="red">
|
||||||
|
Select {searchResults.length} tanks
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</Flex>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<PageTurner tanksPerPage={tanksPerPage} searchedList={searchResults} />
|
<PageTurner tanksPerPage={tanksPerPage} searchedList={searchResults} />
|
||||||
)}
|
)}
|
||||||
<Flex wrap="wrap" gap="2" justify={compact ? 'between' : 'center'}>
|
|
||||||
|
<Flex
|
||||||
|
wrap="wrap"
|
||||||
|
gap="2"
|
||||||
|
style={{
|
||||||
|
justifyContent:
|
||||||
|
compact && firstChunk.length > 0 && secondChunk.length > 0
|
||||||
|
? 'space-around'
|
||||||
|
: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{!compact &&
|
{!compact &&
|
||||||
searchResultsPageSlice.map((tank) => (
|
searchResultsPageSlice.map((tank) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -927,7 +1004,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{compact && (
|
{compact && firstChunk.length > 0 && (
|
||||||
<CompactSearchResultRow tanks={firstChunk} onSelect={onSelect} />
|
<CompactSearchResultRow tanks={firstChunk} onSelect={onSelect} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -935,6 +1012,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
<CompactSearchResultRow tanks={secondChunk} onSelect={onSelect} />
|
<CompactSearchResultRow tanks={secondChunk} onSelect={onSelect} />
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<PageTurner tanksPerPage={tanksPerPage} searchedList={searchResults} />
|
<PageTurner tanksPerPage={tanksPerPage} searchedList={searchResults} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Flex, Link } from '@radix-ui/themes';
|
||||||
|
import { use } from 'react';
|
||||||
|
import { AdHorizontal } from '../../../../components/Ads/Horizontal';
|
||||||
|
import PageWrapper from '../../../../components/PageWrapper';
|
||||||
|
import { tankDefinitions } from '../../../../core/blitzkit/tankDefinitions';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const awaitedTankDefinitions = use(tankDefinitions);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper>
|
||||||
|
<title>Super duper secret map for SEO</title>
|
||||||
|
|
||||||
|
<AdHorizontal />
|
||||||
|
|
||||||
|
<Flex direction="column" align="center">
|
||||||
|
{Object.values(awaitedTankDefinitions)
|
||||||
|
.sort((a, b) => b.tier - a.tier)
|
||||||
|
.map((tank) => (
|
||||||
|
<Link href={`./${tank.id}`} key={tank.id}>
|
||||||
|
{tank.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { Flex } from '@radix-ui/themes';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import PageWrapper from '../../../components/PageWrapper';
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
import { TankSearch } from './components/TankSearch';
|
import { TankSearch } from './components/TankSearch';
|
||||||
@@ -14,6 +16,10 @@ export default function Page() {
|
|||||||
router.push(`./tankopedia/${tank.id}`);
|
router.push(`./tankopedia/${tank.id}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Flex justify="center" mt="4">
|
||||||
|
<Link href="/tools/tankopedia/map">Super duper secret map</Link>
|
||||||
|
</Flex>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { registerProcesses } from './core/blitzkrieg/processes';
|
import { registerProcesses } from './core/blitzkit/processes';
|
||||||
import { secrets } from './core/blitzkrieg/secrets';
|
import { secrets } from './core/blitzkit/secrets';
|
||||||
import { client } from './core/discord/client';
|
import { client } from './core/discord/client';
|
||||||
|
|
||||||
// console.log(`🟡 Launching bot ${client.shard?.ids[0]}`);
|
// console.log(`🟡 Launching bot ${client.shard?.ids[0]}`);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
|
|||||||
.join('\n')}`;
|
.join('\n')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/${DOCS[subcommand as Subcommand]}.md`;
|
const url = `https://raw.githubusercontent.com/tresabhi/blitzkit/main/docs/${DOCS[subcommand as Subcommand]}.md`;
|
||||||
const content = fetch(url).then((response) => response.text());
|
const content = fetch(url).then((response) => response.text());
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import TitleBar from '../../components/TitleBar';
|
|||||||
import { AllStats, getAccountInfo } from '../../core/blitz/getAccountInfo';
|
import { AllStats, getAccountInfo } from '../../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../../core/blitz/getClanAccountInfo';
|
||||||
import getTankStats from '../../core/blitz/getTankStats';
|
import getTankStats from '../../core/blitz/getTankStats';
|
||||||
import { filtersToDescription } from '../../core/blitzkrieg/filtersToDescription';
|
import { filtersToDescription } from '../../core/blitzkit/filtersToDescription';
|
||||||
import {
|
import {
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
tankDefinitions,
|
tankDefinitions,
|
||||||
} from '../../core/blitzkrieg/tankDefinitions';
|
} from '../../core/blitzkit/tankDefinitions';
|
||||||
import getStatsInPeriod from '../../core/blitzstars/getStatsInPeriod';
|
import getStatsInPeriod from '../../core/blitzstars/getStatsInPeriod';
|
||||||
import { tankAverages } from '../../core/blitzstars/tankAverages';
|
import { tankAverages } from '../../core/blitzstars/tankAverages';
|
||||||
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand';
|
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand';
|
||||||
@@ -20,6 +20,7 @@ import { translator } from '../../core/localization/translator';
|
|||||||
import calculateWN8 from '../../core/statistics/calculateWN8';
|
import calculateWN8 from '../../core/statistics/calculateWN8';
|
||||||
import { StatFilters, filterStats } from '../../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../../core/statistics/filterStats';
|
||||||
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
|
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
|
||||||
|
import { UserError } from '../../hooks/userError';
|
||||||
|
|
||||||
const ROWS_PER_PAGE = 8;
|
const ROWS_PER_PAGE = 8;
|
||||||
const MAX_PAGES = 9;
|
const MAX_PAGES = 9;
|
||||||
@@ -37,7 +38,7 @@ export async function renderBreakdown(
|
|||||||
const { filteredOrder } = await filterStats(statsInPeriod, filters);
|
const { filteredOrder } = await filterStats(statsInPeriod, filters);
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
const clanData = await getClanAccountInfo(region, id, ['clan']);
|
const clanData = await getClanAccountInfo(region, id, ['clan']);
|
||||||
const tankStats = await getTankStats(region, id, locale);
|
const tankStats = await getTankStats(region, id);
|
||||||
const filterDescriptions = await filtersToDescription(filters, locale);
|
const filterDescriptions = await filtersToDescription(filters, locale);
|
||||||
const orderedCurrentStats: AllStats[] = [];
|
const orderedCurrentStats: AllStats[] = [];
|
||||||
const orderedCareerStats: AllStats[] = [];
|
const orderedCareerStats: AllStats[] = [];
|
||||||
@@ -45,6 +46,10 @@ export async function renderBreakdown(
|
|||||||
const orderedCareerWN8: (number | undefined)[] = [];
|
const orderedCareerWN8: (number | undefined)[] = [];
|
||||||
const orderedCareerWN8Full: (number | undefined)[] = [];
|
const orderedCareerWN8Full: (number | undefined)[] = [];
|
||||||
|
|
||||||
|
if (tankStats === null) {
|
||||||
|
throw new UserError(t`bot.common.errors.no_tank_stats`);
|
||||||
|
}
|
||||||
|
|
||||||
filteredOrder.forEach((id) => {
|
filteredOrder.forEach((id) => {
|
||||||
const tankAveragesEntry = awaitedTankAverages[id];
|
const tankAveragesEntry = awaitedTankAverages[id];
|
||||||
const current = statsInPeriod.diff[id];
|
const current = statsInPeriod.diff[id];
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
|
|
||||||
// embedWarning(
|
// embedWarning(
|
||||||
// 'This is an approximation!',
|
// 'This is an approximation!',
|
||||||
// "Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkrieg's reset times (use `/about timezones` for more info)",
|
// "Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkit's reset times (use `/about timezones` for more info)",
|
||||||
// ),
|
// ),
|
||||||
// ];
|
// ];
|
||||||
// },
|
// },
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
// import { SlashCommandBuilder } from 'discord.js';
|
// import { SlashCommandBuilder } from 'discord.js';
|
||||||
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
// import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
|
// import getPeriodNow from '../core/blitzkit/getPeriodNow';
|
||||||
// import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
|
// import getTimeDaysAgo from '../core/blitzkit/getTimeDaysAgo';
|
||||||
// import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions';
|
// import { tankDefinitions } from '../core/blitzkit/tankDefinitions';
|
||||||
// import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
// import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
// import { tankAverages } from '../core/blitzstars/tankAverages';
|
// import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
// import addUsernameChoices from '../core/discord/addUsernameChoices';
|
// import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import TitleBar from '../components/TitleBar';
|
|||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import resolveTankId from '../core/blitz/resolveTankId';
|
||||||
import { emblemURL } from '../core/blitzkrieg/emblemURL';
|
import { emblemURL } from '../core/blitzkit/emblemURL';
|
||||||
import getTankHistories from '../core/blitzkrieg/getTankHistories';
|
import getTankHistories from '../core/blitzkit/getTankHistories';
|
||||||
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
|
import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
|
||||||
import { addPeriodSubCommands } from '../core/discord/addPeriodSubCommands';
|
import { addPeriodSubCommands } from '../core/discord/addPeriodSubCommands';
|
||||||
|
|||||||