mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
958f0f2a52 | ||
|
|
b34fef2147 | ||
|
|
fe661124fa | ||
|
|
457dcbcad5 | ||
|
|
c18ee70eba | ||
|
|
095ffc3d6a | ||
|
|
e8b91648b1 | ||
|
|
1b43f6ba99 | ||
|
|
fc5360363c | ||
|
|
47b3c3029e | ||
|
|
dea7f34ca3 | ||
|
|
1e5026ea4a | ||
|
|
98f568ff6a | ||
|
|
10a6f8371d | ||
|
|
0933aa6615 | ||
|
|
746592418d | ||
|
|
e2404b063c | ||
|
|
bae1301556 | ||
|
|
c2de4952ad | ||
|
|
cce61d8075 | ||
|
|
34a0388f45 | ||
|
|
9d62965fc3 | ||
|
|
892434e28f | ||
|
|
2597fe4bcd | ||
|
|
f85b4b36bd | ||
|
|
f777dafefc | ||
|
|
6ef65ccea3 | ||
|
|
974b27a313 | ||
|
|
5124d8abcd | ||
|
|
849bd356c0 | ||
|
|
6406486441 | ||
|
|
2993ada2e5 | ||
|
|
f083c18dd0 | ||
|
|
c7021c5689 | ||
|
|
815369805a | ||
|
|
d8feb5c3a8 | ||
|
|
cb101da8a8 | ||
|
|
71481f5971 | ||
|
|
817796ed3c | ||
|
|
ebb7618f2d | ||
|
|
9717f153cd | ||
|
|
afa0cfaeef | ||
|
|
298b89b227 | ||
|
|
3b94034767 | ||
|
|
444b4c21af | ||
|
|
3e9c828781 | ||
|
|
ca3a1fbe3f | ||
|
|
5f28b541e3 | ||
|
|
18edd349f2 | ||
|
|
f7c6a065c1 | ||
|
|
91d48eeadd | ||
|
|
21bafa1b7b | ||
|
|
38af2f2817 | ||
|
|
8faa1143e9 | ||
|
|
8f34ebbca4 | ||
|
|
1c03701e17 | ||
|
|
c8edd16eb3 | ||
|
|
b96ff2e239 | ||
|
|
a575da3005 | ||
|
|
3ca2dece3e | ||
|
|
191d6133b2 | ||
|
|
b15e73e3a8 | ||
|
|
9a6b8ab979 | ||
|
|
87786d41a7 | ||
|
|
ee7ffe7bc8 | ||
|
|
379a33375d | ||
|
|
bd3f5dea05 | ||
|
|
146d36234e | ||
|
|
f3edcb8077 | ||
|
|
9e492015ff | ||
|
|
d2977182d9 | ||
|
|
7d2d65bab8 | ||
|
|
415c3bd066 | ||
|
|
990ab1520c | ||
|
|
81cb1afeff | ||
|
|
dba5a0a2cd | ||
|
|
04cd205fee | ||
|
|
ec93d8e9ab | ||
|
|
56321d9e33 | ||
|
|
d39ab43e74 | ||
|
|
787d3ae6ea | ||
|
|
035f751bf1 | ||
|
|
43442ac829 | ||
|
|
da02a089b9 | ||
|
|
476d8e804a | ||
|
|
43e1d53d39 | ||
|
|
5372068830 | ||
|
|
025cec3687 | ||
|
|
c47c629a83 | ||
|
|
2b7907f8f7 | ||
|
|
903d4e4d95 | ||
|
|
8857b2baae | ||
|
|
7169e0d91a | ||
|
|
1c9b7463fc | ||
|
|
80741d66fc |
No files matched your search
@@ -2,9 +2,10 @@ name: CD Build Assets
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
- cron: '0 2 * * *'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
name: CD Ratings Asia
|
name: CD Rating Asia
|
||||||
|
|
||||||
# latest: '0 0 * * *'
|
|
||||||
# asia midnight: '0 17 * * *'
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
|
||||||
- cron: '0 17 * * *'
|
- cron: '0 17 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -35,4 +31,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=asia --target=midnight --production
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
name: CD Ratings Europe
|
name: CD Rating Europe
|
||||||
|
|
||||||
# latest: '0 0 * * *'
|
|
||||||
# eu midnight: '0 23 * * *'
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
|
||||||
- cron: '0 23 * * *'
|
- cron: '0 23 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -35,4 +31,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=eu --target=midnight --production
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: CD Rating Latest
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
host:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install Yarn
|
||||||
|
run: npm install --location=global yarn
|
||||||
|
|
||||||
|
- name: Use node_modules cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: '**/node_modules'
|
||||||
|
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
|
||||||
|
- name: Generate all leaderboards
|
||||||
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=na,asia,eu --target=latest --production
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
name: CD Ratings North America
|
name: CD Rating North America
|
||||||
|
|
||||||
# latest: '0 0 * * *'
|
|
||||||
# na midnight: '0 5 * * *'
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
|
||||||
- cron: '0 5 * * *'
|
- cron: '0 5 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -35,4 +31,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=na --target=midnight --production
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build:bot
|
run: yarn build:bot --production
|
||||||
|
|
||||||
- name: Archive build
|
- name: Archive build
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
+3
-2
@@ -8,7 +8,6 @@ dist/
|
|||||||
dist.*
|
dist.*
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
/test.*
|
/test.*
|
||||||
test.*
|
|
||||||
/docs/.vitepress/dist
|
/docs/.vitepress/dist
|
||||||
/docs/.vitepress/cache
|
/docs/.vitepress/cache
|
||||||
servers.csv
|
servers.csv
|
||||||
@@ -18,4 +17,6 @@ temp/
|
|||||||
/.yarn
|
/.yarn
|
||||||
/.VSCodeCounter
|
/.VSCodeCounter
|
||||||
/.yarn
|
/.yarn
|
||||||
depots
|
depots
|
||||||
|
public/sitemap*.xml
|
||||||
|
public/robots.txt
|
||||||
Vendored
+2
-2
@@ -16,6 +16,7 @@
|
|||||||
"betakrieg",
|
"betakrieg",
|
||||||
"BINORMAL",
|
"BINORMAL",
|
||||||
"Binormals",
|
"Binormals",
|
||||||
|
"BKRL",
|
||||||
"bkwd",
|
"bkwd",
|
||||||
"blitzstars",
|
"blitzstars",
|
||||||
"Camo",
|
"Camo",
|
||||||
@@ -99,8 +100,7 @@
|
|||||||
"POSITIVEZ",
|
"POSITIVEZ",
|
||||||
"Promisable",
|
"Promisable",
|
||||||
"Pyogenics",
|
"Pyogenics",
|
||||||
"ratingsleaderboard",
|
"ratingleaderboard",
|
||||||
"ratingsplot",
|
|
||||||
"raycaster",
|
"raycaster",
|
||||||
"researchable",
|
"researchable",
|
||||||
"Resvg",
|
"Resvg",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Blitzkrieg 1.17.0
|
||||||
|
|
||||||
|
A preview command and a touchup.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added `/research` in preview only accessible in the official Discord server.
|
||||||
|
- More details will be posted when the command is made public.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- `/owned-tanks` now returns a simple text message listing all the tanks instead of an image.
|
||||||
|
- This is meant to improve the readability and ease of copying.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Blitzkrieg 1.17.1
|
||||||
|
|
||||||
|
Some stability fixes.
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- `sharp` is now included in the build instead of having being installed before runtime.
|
||||||
|
- Png converter cache is now more memory efficient.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Blitzkrieg 1.17.2
|
||||||
|
|
||||||
|
Introducing the new `/research` command!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added the `/research` command
|
||||||
|
- Type in the name of a tech tree tank you desire to research and this command will list all the expenses in researching all tanks in that line
|
||||||
|
- The starting tank is inferred automatically by the tanks you own
|
||||||
|
- This may be a bit inaccurate because of the lack of accurate information Wargaming provides about owned tanks
|
||||||
|
- You are also manually provide a starting tank and a user to see the progress on a line for
|
||||||
|
- Replaced Vercel Analytics (which collapsed under the insane amount of traffic on the website) with Google Analytics
|
||||||
|
- Spaced armor can now be toggled in the Tankopedia armor sandbox
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Blitzkrieg 1.18.0
|
||||||
|
|
||||||
|
Introducing the new `/rating` command!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- `/rating` command
|
||||||
|
- Features statistics on today's performance in ratings
|
||||||
|
- The included stats are battles, winrate, damage, survival, and kills
|
||||||
|
- This command also features the score, rank, and league you are at plus the reward you'll get if you are high enough on the leaderboard
|
||||||
|
- `/ratings`, the old leaderboard command, has been renamed `/rating-leaderboard`
|
||||||
|
- Reward icons have been removed from the leaderboard
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Rating leaderboards are now built in a more action-efficient manner.
|
||||||
|
- This version also introduces the BKRL (Blitzkrieg rating leaderboard) file format which is an efficient way to store entire leaderboards of regions to disk (1 MB per 40,000 users).
|
||||||
|
- Latest archival actions now build all regions at once in a single action.
|
||||||
|
- The sharp module is now included in the build.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Blitzkrieg 1.18.1
|
||||||
|
|
||||||
|
HE gets more accurate and say goodbye to NaNaNaNaNaN.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Armor highlighting now works on Apple MacBooks
|
||||||
|
- Applied equipments now persist after switching shells
|
||||||
|
- The bottom of the tank sandbox panel can now be interacted with as it was previously blocked by an invisible layer
|
||||||
|
- `/rating` now uses the midnight leaderboard instead of the absolute latest one
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Blitzkrieg Rating Leaderboard
|
||||||
|
|
||||||
|
A straightforward way of storing the entire World of Tanks Blitz rating leaderboard to disk.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
primary BlitzkriegRatingLeaderboard {
|
||||||
|
utf8"BKRL" magic;
|
||||||
|
Header header;
|
||||||
|
primary Body<header> body;
|
||||||
|
}
|
||||||
|
|
||||||
|
Header {
|
||||||
|
uint16 version; // always 1
|
||||||
|
Format format;
|
||||||
|
uint32 count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Format enum<uint8> {
|
||||||
|
Base,
|
||||||
|
Superset1,
|
||||||
|
}
|
||||||
|
|
||||||
|
Body<Header header> match<header.format> {
|
||||||
|
Base {
|
||||||
|
primary {
|
||||||
|
uint32 id;
|
||||||
|
uint16 score;
|
||||||
|
}[header.count] entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
Superset1 {
|
||||||
|
primary {
|
||||||
|
uint32 id;
|
||||||
|
uint16 score;
|
||||||
|
|
||||||
|
uint32 battles;
|
||||||
|
uint32 wins;
|
||||||
|
uint32 survived;
|
||||||
|
|
||||||
|
uint32 damage;
|
||||||
|
uint32 kills;
|
||||||
|
}[header.count] entries;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
+1
-3
@@ -1,7 +1,5 @@
|
|||||||
# Cedilla Object Notation
|
# Cedilla Object Notation
|
||||||
|
|
||||||
This documentation uses an experimental binary notation syntax called "Buffer Up".
|
|
||||||
|
|
||||||
1. JSON compressed as CDON, written directly to disk (a direct 50% reduction).
|
1. JSON compressed as CDON, written directly to disk (a direct 50% reduction).
|
||||||
2. CDON further compressed with LZ4 (a clean 90% reduction).
|
2. CDON further compressed with LZ4 (a clean 90% reduction).
|
||||||
3. The raw JSON file as fetched from a random data source.
|
3. The raw JSON file as fetched from a random data source.
|
||||||
@@ -19,7 +17,7 @@ primary CedillaObjectNotation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Header {
|
Header {
|
||||||
uint16 version;
|
uint16 version; // always 1
|
||||||
FSFormat fsFormat;
|
FSFormat fsFormat;
|
||||||
StringTable<fsFormat> stringTable;
|
StringTable<fsFormat> stringTable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ Blitzkrieg is available as a Discord bot and can be [installed very easily](/gui
|
|||||||
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
|
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
|
||||||
|
|
||||||
- Session tracker
|
- Session tracker
|
||||||
- Ratings leaderboard
|
- Rating leaderboard
|
||||||
- Way more to come
|
- Way more to come
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next-sitemap').IConfig} */
|
||||||
|
module.exports = {
|
||||||
|
siteUrl: process.env.SITE_URL || 'https://blitz-krieg.vercel.app',
|
||||||
|
generateRobotsTxt: true,
|
||||||
|
sourceDir: 'dist/website',
|
||||||
|
};
|
||||||
+8
-9
@@ -1,24 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.16.4",
|
"version": "1.18.1",
|
||||||
"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:bot": "vite-node --script scripts/build",
|
"build:bot": "vite-node --script scripts/build",
|
||||||
"build:website": "next build",
|
"build:website": "next build && next-sitemap --config next-sitemap.config.cjs",
|
||||||
"build:docs": "vitepress build docs",
|
"build:docs": "vitepress build docs",
|
||||||
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
"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",
|
"build:test": "vite-node --script scripts/buildTest",
|
||||||
"build:ratingsplot": "vite-node --script scripts/buildRatingsPlot",
|
"dev:bot": "vite-node --script scripts/build --production && yarn start:bot",
|
||||||
"dev:bot": "yarn build:bot && yarn start:bot",
|
|
||||||
"dev:docs": "vitepress dev docs",
|
"dev:docs": "vitepress dev docs",
|
||||||
"dev:website": "next dev",
|
"dev:website": "next dev",
|
||||||
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
|
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
|
||||||
"start:website": "next start",
|
"start:website": "next start",
|
||||||
"host:docs": "vitepress preview docs",
|
"host:docs": "vitepress preview docs",
|
||||||
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
"publish:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard --publish",
|
||||||
"setup:patches": "vite-node --script scripts/applyPatches",
|
"setup:patches": "vite-node --script scripts/applyPatches",
|
||||||
"lint": "npx tsc",
|
"lint": "npx tsc",
|
||||||
"lint:website": "next lint",
|
"lint:website": "next lint",
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
"@octokit/plugin-throttling": "^8.1.3",
|
"@octokit/plugin-throttling": "^8.1.3",
|
||||||
"@octokit/rest": "^20.0.1",
|
"@octokit/rest": "^20.0.1",
|
||||||
"@prisma/client": "^5.4.1",
|
"@prisma/client": "^5.4.1",
|
||||||
"@radix-ui/colors": "^3.0.0-rc.4",
|
"@radix-ui/colors": "^3.0.0",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/themes": "^2.0.1",
|
"@radix-ui/themes": "^2.0.1",
|
||||||
"@react-three/drei": "^9.93.0",
|
"@react-three/drei": "^9.93.0",
|
||||||
@@ -43,7 +42,6 @@
|
|||||||
"@resvg/resvg-js": "^2.4.1",
|
"@resvg/resvg-js": "^2.4.1",
|
||||||
"@types/three": "^0.160.0",
|
"@types/three": "^0.160.0",
|
||||||
"@vanilla-extract/css": "^1.13.0",
|
"@vanilla-extract/css": "^1.13.0",
|
||||||
"@vercel/analytics": "^1.2.2",
|
|
||||||
"bepaint": "^0.1.0-beta.3",
|
"bepaint": "^0.1.0-beta.3",
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"discord.js": "^14.13.0",
|
"discord.js": "^14.13.0",
|
||||||
@@ -58,6 +56,7 @@
|
|||||||
"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",
|
||||||
@@ -66,7 +65,7 @@
|
|||||||
"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",
|
"react-spinners": "^0.13.8",
|
||||||
"satori": "^0.10.8",
|
"satori": "^0.10.13",
|
||||||
"sharp": "0.33.1",
|
"sharp": "0.33.1",
|
||||||
"three": "0.159.0",
|
"three": "0.159.0",
|
||||||
"three-stdlib": "^2.29.4",
|
"three-stdlib": "^2.29.4",
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
+20
-3
@@ -1,7 +1,13 @@
|
|||||||
|
import { exec } from 'child_process';
|
||||||
import { build } from 'esbuild';
|
import { build } from 'esbuild';
|
||||||
import { cp, readdir } from 'fs/promises';
|
import { cp, readdir } from 'fs/promises';
|
||||||
|
import { argv } from 'process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
|
||||||
readdir('src/workers').then((workers) => {
|
const execPromise = promisify(exec);
|
||||||
|
const isProduction = argv.includes('--production');
|
||||||
|
|
||||||
|
await readdir('src/workers').then((workers) => {
|
||||||
const files = [
|
const files = [
|
||||||
'bot',
|
'bot',
|
||||||
'index',
|
'index',
|
||||||
@@ -25,6 +31,7 @@ readdir('src/workers').then((workers) => {
|
|||||||
'.js': '.cjs',
|
'.js': '.cjs',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
external: ['sharp'],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
minifyIdentifiers: false, // causes goofy errors
|
minifyIdentifiers: false, // causes goofy errors
|
||||||
@@ -36,9 +43,19 @@ readdir('src/workers').then((workers) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// copy schema.prisma
|
// copy schema.prisma
|
||||||
cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
|
await cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
|
||||||
readdir('node_modules/prisma').then((files) =>
|
await readdir('node_modules/prisma').then((files) =>
|
||||||
files
|
files
|
||||||
.filter((file) => file.endsWith('.node'))
|
.filter((file) => file.endsWith('.node'))
|
||||||
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
|
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isProduction) {
|
||||||
|
// install fixed sharp
|
||||||
|
console.log('Installing fixed sharp...');
|
||||||
|
await execPromise('yarn init -y && yarn add [email protected] --ignore-engines', {
|
||||||
|
cwd: 'dist/bot',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Generated:', await readdir('dist/bot'));
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||||
|
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||||
|
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||||
|
import { DATA } from './constants';
|
||||||
|
|
||||||
|
const ICONS = [
|
||||||
|
['currency_silver_m.packed.webp', 'silver'],
|
||||||
|
['currency_premium_xl.packed.webp', 'premium'],
|
||||||
|
['currency_gold_m.packed.webp', 'gold'],
|
||||||
|
['currency_free-xp_xl.packed.webp', 'free-xp'],
|
||||||
|
['currency_elite-xp_xl.packed.webp', 'elite-xp'],
|
||||||
|
['currency_crew-xp_xl.packed.webp', 'crew-xp'],
|
||||||
|
['currency_battle-xp_xl.packed.webp', 'xp'],
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function currencies(production: boolean) {
|
||||||
|
console.log('Building currency icons...');
|
||||||
|
|
||||||
|
const changes = await Promise.all(
|
||||||
|
ICONS.map(async ([file, name]) => {
|
||||||
|
const content = (
|
||||||
|
await sharp(
|
||||||
|
await readDVPLFile(`${DATA}/Gfx/Lobby/currency/${file}.dvpl`),
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.toBuffer()
|
||||||
|
).toString('base64');
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
encoding: 'base64',
|
||||||
|
path: `icons/currencies/${name}.webp`,
|
||||||
|
} satisfies FileChange;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await commitAssets('currency icons', changes, production);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readdir } from 'fs/promises';
|
import { readdir, writeFile } from 'fs/promises';
|
||||||
import { parse as parsePath } from 'path';
|
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';
|
||||||
@@ -79,6 +79,16 @@ type VehicleDefinitionArmor = Record<
|
|||||||
string,
|
string,
|
||||||
number | { vehicleDamageFactor: 0; '#text': number }
|
number | { vehicleDamageFactor: 0; '#text': number }
|
||||||
>;
|
>;
|
||||||
|
interface UnlocksInner {
|
||||||
|
cost: number;
|
||||||
|
'#text': number;
|
||||||
|
}
|
||||||
|
type Unlocks = {
|
||||||
|
[key in 'vehicle' | 'engine' | 'chassis' | 'turret' | 'gun']:
|
||||||
|
| UnlocksInner
|
||||||
|
| UnlocksInner[];
|
||||||
|
};
|
||||||
|
type UnlocksListing = Unlocks | undefined;
|
||||||
interface VehicleDefinitions {
|
interface VehicleDefinitions {
|
||||||
invisibility: {
|
invisibility: {
|
||||||
moving: number;
|
moving: number;
|
||||||
@@ -92,7 +102,7 @@ interface VehicleDefinitions {
|
|||||||
};
|
};
|
||||||
consumableSlots: number;
|
consumableSlots: number;
|
||||||
provisionSlots: number;
|
provisionSlots: number;
|
||||||
optDevicePreset: string;
|
optDevicePreset: string | string[];
|
||||||
hull: {
|
hull: {
|
||||||
armor: VehicleDefinitionArmor;
|
armor: VehicleDefinitionArmor;
|
||||||
turretPositions: { turret: string };
|
turretPositions: { turret: string };
|
||||||
@@ -102,6 +112,7 @@ interface VehicleDefinitions {
|
|||||||
};
|
};
|
||||||
chassis: {
|
chassis: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
|
unlocks: UnlocksListing;
|
||||||
weight: number;
|
weight: number;
|
||||||
terrainResistance: string;
|
terrainResistance: string;
|
||||||
rotationSpeed: number;
|
rotationSpeed: number;
|
||||||
@@ -130,10 +141,13 @@ interface VehicleDefinitions {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
engines: {
|
engines: {
|
||||||
[key: string]: unknown;
|
[key: string]: {
|
||||||
|
unlocks: UnlocksListing;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
turrets0: {
|
turrets0: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
|
unlocks: UnlocksListing;
|
||||||
rotationSpeed: number;
|
rotationSpeed: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
circularVisionRadius: number;
|
circularVisionRadius: number;
|
||||||
@@ -149,6 +163,7 @@ interface VehicleDefinitions {
|
|||||||
};
|
};
|
||||||
guns: {
|
guns: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
|
unlocks: UnlocksListing;
|
||||||
armor: VehicleDefinitionArmor & {
|
armor: VehicleDefinitionArmor & {
|
||||||
gun?:
|
gun?:
|
||||||
| number
|
| number
|
||||||
@@ -353,6 +368,8 @@ export async function definitions(production: boolean) {
|
|||||||
`${DATA}/${POI.provisionsCommon}.dvpl`,
|
`${DATA}/${POI.provisionsCommon}.dvpl`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tankXps = new Map<number, number>();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
nations.map(async (nation) => {
|
nations.map(async (nation) => {
|
||||||
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||||
@@ -385,6 +402,10 @@ export async function definitions(production: boolean) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const tankKey in tankList.root) {
|
for (const tankKey in tankList.root) {
|
||||||
|
const gunXps = new Map<number, number>();
|
||||||
|
const turretXps = new Map<number, number>();
|
||||||
|
const engineXps = new Map<number, number>();
|
||||||
|
const trackXps = new Map<number, number>();
|
||||||
const tank = tankList.root[tankKey];
|
const tank = tankList.root[tankKey];
|
||||||
const tankPrice: TankDefinitionPrice =
|
const tankPrice: TankDefinitionPrice =
|
||||||
typeof tank.price === 'number'
|
typeof tank.price === 'number'
|
||||||
@@ -427,6 +448,7 @@ export async function definitions(production: boolean) {
|
|||||||
});
|
});
|
||||||
const crew: Crew[] = [];
|
const crew: Crew[] = [];
|
||||||
const fixedCamouflage = tankTags.includes('eventCamouflage_user');
|
const fixedCamouflage = tankTags.includes('eventCamouflage_user');
|
||||||
|
const totalUnlocks: UnlocksListing[] = [];
|
||||||
|
|
||||||
Object.entries(tankDefinition.root.crew).forEach(([key, value]) => {
|
Object.entries(tankDefinition.root.crew).forEach(([key, value]) => {
|
||||||
let entry: Crew;
|
let entry: Crew;
|
||||||
@@ -473,7 +495,8 @@ export async function definitions(production: boolean) {
|
|||||||
forwards: tankDefinition.root.speedLimits.forward,
|
forwards: tankDefinition.root.speedLimits.forward,
|
||||||
backwards: tankDefinition.root.speedLimits.backward,
|
backwards: tankDefinition.root.speedLimits.backward,
|
||||||
},
|
},
|
||||||
equipment,
|
equipment:
|
||||||
|
typeof equipment === 'string' ? equipment : equipment.at(-1)!,
|
||||||
consumables: tankDefinition.root.consumableSlots,
|
consumables: tankDefinition.root.consumableSlots,
|
||||||
provisions: tankDefinition.root.provisionSlots,
|
provisions: tankDefinition.root.provisionSlots,
|
||||||
name:
|
name:
|
||||||
@@ -531,6 +554,7 @@ export async function definitions(production: boolean) {
|
|||||||
.split(' ')
|
.split(' ')
|
||||||
.map(Number) as Vector3Tuple;
|
.map(Number) as Vector3Tuple;
|
||||||
|
|
||||||
|
totalUnlocks.push(track.unlocks);
|
||||||
tankDefinitions[tankId].tracks.push({
|
tankDefinitions[tankId].tracks.push({
|
||||||
id: trackId,
|
id: trackId,
|
||||||
weight: track.weight,
|
weight: track.weight,
|
||||||
@@ -557,16 +581,18 @@ export async function definitions(production: boolean) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(tankDefinition.root.engines).forEach((engineKey) => {
|
Object.keys(tankDefinition.root.engines).forEach((engineKey) => {
|
||||||
const engine = enginesList.root.shared[engineKey];
|
const engine = tankDefinition.root.engines[engineKey];
|
||||||
|
const engineListEntry = enginesList.root.shared[engineKey];
|
||||||
const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]);
|
const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]);
|
||||||
|
|
||||||
|
totalUnlocks.push(engine.unlocks);
|
||||||
tankDefinitions[tankId].engines.push({
|
tankDefinitions[tankId].engines.push({
|
||||||
id: engineId,
|
id: engineId,
|
||||||
name: strings[engine.userString],
|
name: strings[engineListEntry.userString],
|
||||||
fireChance: engine.fireStartingChance,
|
fireChance: engineListEntry.fireStartingChance,
|
||||||
tier: engine.level as Tier,
|
tier: engineListEntry.level as Tier,
|
||||||
weight: engine.weight,
|
weight: engineListEntry.weight,
|
||||||
power: engine.power,
|
power: engineListEntry.power,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -593,6 +619,7 @@ export async function definitions(production: boolean) {
|
|||||||
.map(Number) as Vector3Tuple;
|
.map(Number) as Vector3Tuple;
|
||||||
const turretArmor: ModelArmor = { thickness: {} };
|
const turretArmor: ModelArmor = { thickness: {} };
|
||||||
|
|
||||||
|
totalUnlocks.push(turret.unlocks);
|
||||||
Object.keys(turret.armor)
|
Object.keys(turret.armor)
|
||||||
.filter((name) => name.startsWith('armor_'))
|
.filter((name) => name.startsWith('armor_'))
|
||||||
.forEach((name) => {
|
.forEach((name) => {
|
||||||
@@ -645,11 +672,11 @@ export async function definitions(production: boolean) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(turret.guns).forEach((gunKey, gunIndex) => {
|
Object.keys(turret.guns).forEach((gunKey, gunIndex) => {
|
||||||
const turretGunEntry = turret.guns[gunKey];
|
const gun = turret.guns[gunKey];
|
||||||
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
|
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
|
||||||
const gunListEntry = gunList.root.shared[gunKey];
|
const gunListEntry = gunList.root.shared[gunKey];
|
||||||
const pitchLimitsRaw =
|
const pitchLimitsRaw =
|
||||||
turretGunEntry.pitchLimits ?? gunListEntry.pitchLimits;
|
gun.pitchLimits ?? gunListEntry.pitchLimits;
|
||||||
const gunPitch = (
|
const gunPitch = (
|
||||||
typeof pitchLimitsRaw === 'string'
|
typeof pitchLimitsRaw === 'string'
|
||||||
? pitchLimitsRaw
|
? pitchLimitsRaw
|
||||||
@@ -658,43 +685,41 @@ export async function definitions(production: boolean) {
|
|||||||
.split(' ')
|
.split(' ')
|
||||||
.map(Number) as [number, number];
|
.map(Number) as [number, number];
|
||||||
const gunModel = Number(
|
const gunModel = Number(
|
||||||
parsePath(turretGunEntry.models.undamaged).name.split('_')[1],
|
parsePath(gun.models.undamaged).name.split('_')[1],
|
||||||
);
|
);
|
||||||
const gunName =
|
const gunName =
|
||||||
strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' ');
|
strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' ');
|
||||||
const gunType =
|
const gunType =
|
||||||
'clip' in turretGunEntry
|
'clip' in gun
|
||||||
? turretGunEntry.pumpGunMode
|
? gun.pumpGunMode
|
||||||
? 'autoReloader'
|
? 'autoReloader'
|
||||||
: 'autoLoader'
|
: 'autoLoader'
|
||||||
: 'regular';
|
: 'regular';
|
||||||
const gunReload =
|
const gunReload =
|
||||||
gunType === 'autoReloader'
|
gunType === 'autoReloader'
|
||||||
? turretGunEntry.pumpGunReloadTimes!.split(' ').map(Number)
|
? gun.pumpGunReloadTimes!.split(' ').map(Number)
|
||||||
: turretGunEntry.reloadTime;
|
: gun.reloadTime;
|
||||||
const gunClipCount =
|
const gunClipCount =
|
||||||
gunType === 'regular' ? undefined : turretGunEntry.clip!.count;
|
gunType === 'regular' ? undefined : gun.clip!.count;
|
||||||
const gunIntraClip =
|
const gunIntraClip =
|
||||||
gunType === 'regular'
|
gunType === 'regular' ? undefined : 60 / gun.clip!.rate;
|
||||||
? undefined
|
const front = gun.extraPitchLimits?.front
|
||||||
: 60 / turretGunEntry.clip!.rate;
|
? gun.extraPitchLimits.front.split(' ').map(Number)
|
||||||
const front = turretGunEntry.extraPitchLimits?.front
|
|
||||||
? turretGunEntry.extraPitchLimits.front.split(' ').map(Number)
|
|
||||||
: undefined;
|
: undefined;
|
||||||
const back = turretGunEntry.extraPitchLimits?.back
|
const back = gun.extraPitchLimits?.back
|
||||||
? turretGunEntry.extraPitchLimits.back.split(' ').map(Number)
|
? gun.extraPitchLimits.back.split(' ').map(Number)
|
||||||
: undefined;
|
: undefined;
|
||||||
const transition = turretGunEntry.extraPitchLimits?.transition
|
const transition = gun.extraPitchLimits?.transition
|
||||||
? typeof turretGunEntry.extraPitchLimits.transition === 'number'
|
? typeof gun.extraPitchLimits.transition === 'number'
|
||||||
? turretGunEntry.extraPitchLimits.transition
|
? gun.extraPitchLimits.transition
|
||||||
: turretGunEntry.extraPitchLimits.transition.at(-1)!
|
: gun.extraPitchLimits.transition.at(-1)!
|
||||||
: undefined;
|
: undefined;
|
||||||
const gunArmor: ModelArmor = { thickness: {} };
|
const gunArmor: ModelArmor = { thickness: {} };
|
||||||
const shotDispersionFactors =
|
const shotDispersionFactors =
|
||||||
turretGunEntry.shotDispersionFactors ??
|
gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors;
|
||||||
gunListEntry.shotDispersionFactors;
|
|
||||||
|
|
||||||
Object.keys(turretGunEntry.armor)
|
totalUnlocks.push(gun.unlocks);
|
||||||
|
Object.keys(gun.armor)
|
||||||
.filter((name) => name.startsWith('armor_'))
|
.filter((name) => name.startsWith('armor_'))
|
||||||
.forEach((name) => {
|
.forEach((name) => {
|
||||||
const armorIdString = name.match(/armor_(\d+)/)?.[1];
|
const armorIdString = name.match(/armor_(\d+)/)?.[1];
|
||||||
@@ -702,7 +727,7 @@ export async function definitions(production: boolean) {
|
|||||||
throw new SyntaxError(`Invalid armor id: ${name}`);
|
throw new SyntaxError(`Invalid armor id: ${name}`);
|
||||||
}
|
}
|
||||||
const armorId = parseInt(armorIdString);
|
const armorId = parseInt(armorIdString);
|
||||||
const armorRaw = turretGunEntry.armor[name];
|
const armorRaw = gun.armor[name];
|
||||||
if (typeof armorRaw === 'number') {
|
if (typeof armorRaw === 'number') {
|
||||||
gunArmor.thickness[armorId] = armorRaw;
|
gunArmor.thickness[armorId] = armorRaw;
|
||||||
} else {
|
} else {
|
||||||
@@ -724,13 +749,13 @@ export async function definitions(production: boolean) {
|
|||||||
count: gunClipCount,
|
count: gunClipCount,
|
||||||
intraClip: gunIntraClip,
|
intraClip: gunIntraClip,
|
||||||
camouflageLoss:
|
camouflageLoss:
|
||||||
typeof turretGunEntry.invisibilityFactorAtShot === 'number'
|
typeof gun.invisibilityFactorAtShot === 'number'
|
||||||
? turretGunEntry.invisibilityFactorAtShot
|
? gun.invisibilityFactorAtShot
|
||||||
: turretGunEntry.invisibilityFactorAtShot.at(-1)!,
|
: gun.invisibilityFactorAtShot.at(-1)!,
|
||||||
aimTime: turretGunEntry.aimingTime ?? gunListEntry.aimingTime,
|
aimTime: gun.aimingTime ?? gunListEntry.aimingTime,
|
||||||
dispersion: {
|
dispersion: {
|
||||||
base:
|
base:
|
||||||
turretGunEntry.shotDispersionRadius ??
|
gun.shotDispersionRadius ??
|
||||||
gunListEntry.shotDispersionRadius,
|
gunListEntry.shotDispersionRadius,
|
||||||
damaged: shotDispersionFactors.whileGunDamaged,
|
damaged: shotDispersionFactors.whileGunDamaged,
|
||||||
shot: shotDispersionFactors.afterShot,
|
shot: shotDispersionFactors.afterShot,
|
||||||
@@ -742,11 +767,11 @@ export async function definitions(production: boolean) {
|
|||||||
armor: gunArmor,
|
armor: gunArmor,
|
||||||
model: gunModel,
|
model: gunModel,
|
||||||
thickness:
|
thickness:
|
||||||
turretGunEntry.armor.gun === undefined
|
gun.armor.gun === undefined
|
||||||
? 0
|
? 0
|
||||||
: typeof turretGunEntry.armor.gun === 'number'
|
: typeof gun.armor.gun === 'number'
|
||||||
? turretGunEntry.armor.gun
|
? gun.armor.gun
|
||||||
: turretGunEntry.armor.gun['#text'],
|
: gun.armor.gun['#text'],
|
||||||
pitch: {
|
pitch: {
|
||||||
min: gunPitch[0],
|
min: gunPitch[0],
|
||||||
max: gunPitch[1],
|
max: gunPitch[1],
|
||||||
@@ -808,10 +833,100 @@ export async function definitions(production: boolean) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
totalUnlocks.forEach((unlocks) => {
|
||||||
|
if (unlocks === undefined) return;
|
||||||
|
|
||||||
|
Object.entries(unlocks).forEach(([key, value]) => {
|
||||||
|
(Array.isArray(value) ? value : [value]).forEach((vehicle) => {
|
||||||
|
switch (key as keyof Unlocks) {
|
||||||
|
case 'vehicle': {
|
||||||
|
const tankListEntry = tankList.root[vehicle['#text']];
|
||||||
|
const currentTank = tankDefinitions[tankId];
|
||||||
|
const successorId = toUniqueId(nation, tankListEntry.id);
|
||||||
|
|
||||||
|
tankXps.set(successorId, vehicle.cost);
|
||||||
|
|
||||||
|
if (currentTank.successors === undefined) {
|
||||||
|
currentTank.successors = [];
|
||||||
|
}
|
||||||
|
if (!currentTank.successors!.includes(successorId)) {
|
||||||
|
currentTank.successors!.push(successorId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'gun': {
|
||||||
|
gunXps.set(
|
||||||
|
toUniqueId(nation, gunList.root.ids[vehicle['#text']]),
|
||||||
|
vehicle.cost,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'turret': {
|
||||||
|
turretXps.set(
|
||||||
|
toUniqueId(nation, turretList.root.ids[vehicle['#text']]),
|
||||||
|
vehicle.cost,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'engine': {
|
||||||
|
engineXps.set(
|
||||||
|
toUniqueId(nation, enginesList.root.ids[vehicle['#text']]),
|
||||||
|
vehicle.cost,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'chassis': {
|
||||||
|
trackXps.set(
|
||||||
|
toUniqueId(nation, chassisList.root.ids[vehicle['#text']]),
|
||||||
|
vehicle.cost,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(tankDefinitions[tankId].turrets).forEach((turret) => {
|
||||||
|
turret.xp = turretXps.get(turret.id);
|
||||||
|
|
||||||
|
Object.values(turret.guns).forEach((gun) => {
|
||||||
|
gun.xp = gunXps.get(gun.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(tankDefinitions[tankId].engines).forEach((engine) => {
|
||||||
|
engine.xp = engineXps.get(engine.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(tankDefinitions[tankId].tracks).forEach((track) => {
|
||||||
|
track.xp = trackXps.get(track.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Object.values(tankDefinitions).forEach((tank) => {
|
||||||
|
tank.xp = tankXps.get(tank.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(tankDefinitions).forEach((tank) => {
|
||||||
|
tank.successors?.forEach((predecessorId) => {
|
||||||
|
if (tankDefinitions[predecessorId].ancestors === undefined) {
|
||||||
|
tankDefinitions[predecessorId].ancestors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tankDefinitions[predecessorId].ancestors?.includes(tank.id)) {
|
||||||
|
tankDefinitions[predecessorId].ancestors?.push(tank.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Object.entries(optionalDevices.root).forEach(
|
Object.entries(optionalDevices.root).forEach(
|
||||||
([optionalDeviceKey, optionalDeviceEntry]) => {
|
([optionalDeviceKey, optionalDeviceEntry]) => {
|
||||||
if (optionalDeviceKey === 'nextAvailableId') return;
|
if (optionalDeviceKey === 'nextAvailableId') return;
|
||||||
@@ -969,6 +1084,10 @@ export async function definitions(production: boolean) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
writeFile(
|
||||||
|
'test.tanks.cdon.lz4',
|
||||||
|
Buffer.from(superCompress(tankDefinitions), 'base64'),
|
||||||
|
);
|
||||||
await commitAssets(
|
await commitAssets(
|
||||||
'definitions',
|
'definitions',
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { boosterIcons } from './boosterIcons';
|
|||||||
import { camouflageIcons } from './camouflageIcons';
|
import { camouflageIcons } from './camouflageIcons';
|
||||||
import { circleFlags } from './circleFlags';
|
import { circleFlags } from './circleFlags';
|
||||||
import { consumableProvisionIcons } from './consumableProvisionIcons';
|
import { consumableProvisionIcons } from './consumableProvisionIcons';
|
||||||
|
import { currencies } from './currencies';
|
||||||
import { definitions } from './definitions';
|
import { definitions } from './definitions';
|
||||||
import { equipmentIcons } from './equipmentIcons';
|
import { equipmentIcons } from './equipmentIcons';
|
||||||
import { moduleIcons } from './moduleIcons';
|
import { moduleIcons } from './moduleIcons';
|
||||||
@@ -38,6 +39,7 @@ const methods = [
|
|||||||
boosterIcons,
|
boosterIcons,
|
||||||
tankArmors,
|
tankArmors,
|
||||||
tankModels,
|
tankModels,
|
||||||
|
currencies,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const method of methods) {
|
for (const method of methods) {
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { chunk } from 'lodash';
|
||||||
|
import { argv } from 'process';
|
||||||
|
import {
|
||||||
|
RatingInfo,
|
||||||
|
RatingNeighbors,
|
||||||
|
RatingPlayer,
|
||||||
|
} from '../src/commands/ratingLeaderboard';
|
||||||
|
import { RegionSubdomain } from '../src/constants/regions';
|
||||||
|
import { getAccountInfo } from '../src/core/blitz/getAccountInfo';
|
||||||
|
import regionSubdomainToRegion from '../src/core/blitz/regionSubdomainToRegion';
|
||||||
|
import { commitAssets } from '../src/core/blitzkrieg/commitAssets';
|
||||||
|
import { FileChange } from '../src/core/blitzkrieg/commitMultipleFiles.js';
|
||||||
|
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
|
||||||
|
import { superCompress } from '../src/core/blitzkrieg/superCompress';
|
||||||
|
import {
|
||||||
|
BkrlBaseEntry,
|
||||||
|
BkrlFormat,
|
||||||
|
BkrlSuperset1Entry,
|
||||||
|
BkrlWriteStream,
|
||||||
|
} from '../src/core/streams/bkrl';
|
||||||
|
import { CdonValue, CdonWriteStream } from '../src/core/streams/cdon';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Central North American Time (UTC-5): use 0 5 * * *.
|
||||||
|
* Central European Time (UTC+1): use 0 23 * * *.
|
||||||
|
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const production = argv.includes('--production');
|
||||||
|
const target = argv
|
||||||
|
.find((arg) => arg.startsWith('--target='))
|
||||||
|
?.split('=')[1] as 'midnight' | 'latest' | undefined;
|
||||||
|
const regionSubdomains = argv
|
||||||
|
.find((arg) => arg.startsWith('--region='))
|
||||||
|
?.split('=')[1];
|
||||||
|
|
||||||
|
if (!target) throw new Error('Target parameter not specified');
|
||||||
|
if (!regionSubdomains) throw new Error('Region parameter not specified');
|
||||||
|
|
||||||
|
regionSubdomains.split(',').forEach(async (regionSubdomain) => {
|
||||||
|
const region = regionSubdomainToRegion(regionSubdomain as RegionSubdomain);
|
||||||
|
|
||||||
|
console.log(`Building ${target} rating leaderboard for ${region}`);
|
||||||
|
|
||||||
|
const NEIGHBORS = 2 ** 8;
|
||||||
|
const PLAYERS = await patientFetchJSON<RatingInfo>(
|
||||||
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
|
).then((data) => (data.detail === undefined ? data.count : undefined));
|
||||||
|
|
||||||
|
if (PLAYERS === undefined) {
|
||||||
|
throw new Error('No current rating season running');
|
||||||
|
}
|
||||||
|
|
||||||
|
const players: Record<number, RatingPlayer> = {};
|
||||||
|
const leaderboard: BkrlBaseEntry[] = [];
|
||||||
|
let registered = 0;
|
||||||
|
|
||||||
|
async function branchFromPlayer(id: number) {
|
||||||
|
const { neighbors } = await patientFetchJSON<RatingNeighbors>(
|
||||||
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstPlayer = neighbors[0];
|
||||||
|
const lastPlayer = neighbors[neighbors.length - 1];
|
||||||
|
|
||||||
|
const branches: Promise<void>[] = [];
|
||||||
|
if (!players[firstPlayer.number])
|
||||||
|
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
||||||
|
if (!players[lastPlayer.number])
|
||||||
|
branches.push(branchFromPlayer(lastPlayer.spa_id));
|
||||||
|
|
||||||
|
neighbors.forEach((neighbor) => {
|
||||||
|
if (players[neighbor.number]) return;
|
||||||
|
|
||||||
|
players[neighbor.number] = neighbor;
|
||||||
|
registered++;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Registered ${registered} / ${PLAYERS} (${(
|
||||||
|
(registered / PLAYERS!) *
|
||||||
|
100
|
||||||
|
).toFixed(2)}%)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(branches);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function branchFromLeague(id: number) {
|
||||||
|
const { result } = await patientFetchJSON<{ result: RatingPlayer[] }>(
|
||||||
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstPlayer = result[0];
|
||||||
|
const lastPlayer = result.at(-1)!;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
branchFromPlayer(firstPlayer.spa_id),
|
||||||
|
branchFromPlayer(lastPlayer.spa_id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Branching from leagues...');
|
||||||
|
await Promise.all([
|
||||||
|
branchFromLeague(0),
|
||||||
|
branchFromLeague(1),
|
||||||
|
branchFromLeague(2),
|
||||||
|
branchFromLeague(3),
|
||||||
|
branchFromLeague(4),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('Converting to an array...');
|
||||||
|
for (let index = 0; index < PLAYERS; index++) {
|
||||||
|
const listing = players[index + 1];
|
||||||
|
if (listing) {
|
||||||
|
leaderboard[index] = { id: listing.spa_id, score: listing.score };
|
||||||
|
} else {
|
||||||
|
console.warn(`Position ${index + 1} had no player`);
|
||||||
|
leaderboard[index] = { id: 0, score: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Requesting further stats...');
|
||||||
|
const comprehensiveLeaderboard: BkrlSuperset1Entry[] = [];
|
||||||
|
let chunkIndex = 0;
|
||||||
|
for (const leaderboardChunk of chunk(leaderboard, 100)) {
|
||||||
|
console.log(
|
||||||
|
`Chunk ${chunkIndex + 1} of ${Math.ceil(leaderboard.length / 100)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const stats = await getAccountInfo(
|
||||||
|
region,
|
||||||
|
leaderboardChunk.map(({ id }) => id),
|
||||||
|
['statistics.rating'],
|
||||||
|
);
|
||||||
|
|
||||||
|
stats.forEach((stat, index) => {
|
||||||
|
const trueIndex = chunkIndex * 100 + index;
|
||||||
|
comprehensiveLeaderboard[trueIndex] =
|
||||||
|
leaderboardChunk[index].id === 0 ||
|
||||||
|
stat?.statistics.rating === undefined
|
||||||
|
? {
|
||||||
|
battles: 0,
|
||||||
|
damage: 0,
|
||||||
|
id: 0,
|
||||||
|
kills: 0,
|
||||||
|
score: 0,
|
||||||
|
survived: 0,
|
||||||
|
wins: 0,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: leaderboardChunk[index].id,
|
||||||
|
score: leaderboardChunk[index].score,
|
||||||
|
battles: stat.statistics.rating.battles,
|
||||||
|
damage: stat.statistics.rating.damage_dealt,
|
||||||
|
kills: stat.statistics.rating.frags,
|
||||||
|
survived: stat.statistics.rating.survived_battles,
|
||||||
|
wins: stat.statistics.rating.wins,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
chunkIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = await patientFetchJSON<RatingInfo & { detail: undefined }>(
|
||||||
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
|
);
|
||||||
|
const normalizedServer = regionSubdomain === 'na' ? 'com' : regionSubdomain;
|
||||||
|
const infoContent = new CdonWriteStream().cdon(
|
||||||
|
info as unknown as CdonValue,
|
||||||
|
).uint8Array;
|
||||||
|
const leaderboardWriteStream = new BkrlWriteStream();
|
||||||
|
|
||||||
|
leaderboardWriteStream.bkrl({
|
||||||
|
format: BkrlFormat.Superset1,
|
||||||
|
entries: comprehensiveLeaderboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
const changes: FileChange[] = [
|
||||||
|
{
|
||||||
|
path: `regions/${normalizedServer}/rating/${info.current_season}/info.cdon.lz4`,
|
||||||
|
content: superCompress(infoContent),
|
||||||
|
encoding: 'base64',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `regions/${normalizedServer}/rating/${info.current_season}/${target}.bkrl`,
|
||||||
|
content: Buffer.from(leaderboardWriteStream.uint8Array).toString(
|
||||||
|
'base64',
|
||||||
|
),
|
||||||
|
encoding: 'base64',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
commitAssets(
|
||||||
|
`rating leaderboard ${regionSubdomain} ${target}`,
|
||||||
|
changes,
|
||||||
|
production,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import { rm, writeFile } from 'fs/promises';
|
|
||||||
import { argv } from 'process';
|
|
||||||
import {
|
|
||||||
RatingsInfo,
|
|
||||||
RatingsNeighbors,
|
|
||||||
RatingsPlayer,
|
|
||||||
} from '../src/commands/ratings';
|
|
||||||
import commitMultipleFiles from '../src/core/blitzkrieg/commitMultipleFiles.js';
|
|
||||||
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Central North American Time (UTC-5): use 0 5 * * *.
|
|
||||||
* Central European Time (UTC+1): use 0 23 * * *.
|
|
||||||
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
|
||||||
*
|
|
||||||
* [region]/ratings/[season]/info.json: season info like rewards, start & end time, etc.
|
|
||||||
* [region]/ratings/[season]/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
|
||||||
* [region]/ratings/[season]/midnight.json: last midnight's leaderboard relative to the region
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type BlitzkriegRatingsLeaderboardEntry = { id: number; score: number };
|
|
||||||
export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[];
|
|
||||||
|
|
||||||
const publish = argv.includes('--publish');
|
|
||||||
const target = argv
|
|
||||||
.find((arg) => arg.startsWith('--target='))
|
|
||||||
?.split('=')[1] as 'midnight' | 'latest' | undefined;
|
|
||||||
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
|
|
||||||
|
|
||||||
if (!target) throw new Error('Target parameter not specified');
|
|
||||||
if (!server) throw new Error('Region parameter not specified');
|
|
||||||
|
|
||||||
const NEIGHBORS = 2 ** 8;
|
|
||||||
const PLAYERS = await patientFetchJSON<RatingsInfo>(
|
|
||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
|
||||||
).then((data) => (data.detail === undefined ? data.count : undefined));
|
|
||||||
|
|
||||||
if (PLAYERS === undefined) {
|
|
||||||
throw new Error('No current ratings season running');
|
|
||||||
}
|
|
||||||
|
|
||||||
const players: Record<number, RatingsPlayer> = {};
|
|
||||||
const leaderboard: BlitzkriegRatingsLeaderboard = [];
|
|
||||||
let registered = 0;
|
|
||||||
|
|
||||||
async function branchFromPlayer(id: number) {
|
|
||||||
const { neighbors } = await patientFetchJSON<RatingsNeighbors>(
|
|
||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstPlayer = neighbors[0];
|
|
||||||
const lastPlayer = neighbors[neighbors.length - 1];
|
|
||||||
|
|
||||||
const branches: Promise<void>[] = [];
|
|
||||||
if (!players[firstPlayer.number])
|
|
||||||
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
|
||||||
if (!players[lastPlayer.number])
|
|
||||||
branches.push(branchFromPlayer(lastPlayer.spa_id));
|
|
||||||
|
|
||||||
neighbors.forEach((neighbor) => {
|
|
||||||
if (players[neighbor.number]) return;
|
|
||||||
|
|
||||||
players[neighbor.number] = neighbor;
|
|
||||||
registered++;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Registered ${registered} / ${PLAYERS} (${(
|
|
||||||
(registered / PLAYERS!) *
|
|
||||||
100
|
|
||||||
).toFixed(2)}%)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(branches);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function branchFromLeague(id: number) {
|
|
||||||
const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
|
|
||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstPlayer = result[0];
|
|
||||||
const lastPlayer = result[result.length - 1];
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
branchFromPlayer(firstPlayer.spa_id),
|
|
||||||
branchFromPlayer(lastPlayer.spa_id),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Branching from leagues...');
|
|
||||||
await Promise.all([
|
|
||||||
branchFromLeague(0),
|
|
||||||
branchFromLeague(1),
|
|
||||||
branchFromLeague(2),
|
|
||||||
branchFromLeague(3),
|
|
||||||
branchFromLeague(4),
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log('Converting to an array...');
|
|
||||||
for (let index = 0; index < PLAYERS; index++) {
|
|
||||||
const listing = players[index + 1];
|
|
||||||
if (!listing) {
|
|
||||||
console.warn(`Position ${index + 1} had no player`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
leaderboard[index] = {
|
|
||||||
id: listing.spa_id,
|
|
||||||
score: listing.score,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const leaderboardJSON = JSON.stringify(leaderboard);
|
|
||||||
|
|
||||||
if (publish) {
|
|
||||||
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
|
|
||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
|
||||||
);
|
|
||||||
const normalizedServer = server === 'na' ? 'com' : server;
|
|
||||||
const infoPath = `regions/${normalizedServer}/ratings/${info.current_season}/info.json`;
|
|
||||||
const leaderboardPath = `regions/${normalizedServer}/ratings/${info.current_season}/${target}.json`;
|
|
||||||
const infoJSON = JSON.stringify(info);
|
|
||||||
|
|
||||||
console.log(`Publishing to season ${info.current_season}...`);
|
|
||||||
commitMultipleFiles(
|
|
||||||
'tresabhi',
|
|
||||||
'blitzkrieg-assets',
|
|
||||||
'main',
|
|
||||||
new Date().toString(),
|
|
||||||
[
|
|
||||||
{ path: infoPath, content: infoJSON, encoding: 'utf-8' },
|
|
||||||
{ path: leaderboardPath, content: leaderboardJSON, encoding: 'utf-8' },
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log('Writing to disk...');
|
|
||||||
await rm('dist.json', { force: true });
|
|
||||||
await writeFile('dist.json', leaderboardJSON);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { writeFile } from 'fs/promises';
|
|
||||||
import { Region } from '../src/constants/regions';
|
|
||||||
import { getRatingsNeighbors } from '../src/core/blitz/getRatingsNeighbors';
|
|
||||||
|
|
||||||
const REGION: Region = 'com';
|
|
||||||
const PLAYER = 1041988373;
|
|
||||||
const ITERATIONS = 2 ** 4;
|
|
||||||
const MAX_NEIGHBORS = 2 ** 7;
|
|
||||||
const data: [number, number][] = [];
|
|
||||||
let finished = 0;
|
|
||||||
|
|
||||||
for (let index = 0; index < ITERATIONS; index++) {
|
|
||||||
const neighbors = Math.round((Math.random() * MAX_NEIGHBORS) / 2) * 2;
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
console.log(`🟡 Iteration ${index} with ${neighbors} neighbors`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await getRatingsNeighbors(REGION, PLAYER, neighbors);
|
|
||||||
|
|
||||||
finished++;
|
|
||||||
console.log(
|
|
||||||
`🟢 (${Math.round(
|
|
||||||
100 * (finished / ITERATIONS),
|
|
||||||
)}%) Iteration ${index} with ${neighbors} neighbors`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const end = Date.now();
|
|
||||||
const time = end - start;
|
|
||||||
|
|
||||||
data.push([neighbors, time]);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
`🔴 Iteration ${index} with ${neighbors} neighbors\n\n${error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🟢 Done, sorting');
|
|
||||||
data.sort(([neighborsA], [neighborsB]) => neighborsA - neighborsB);
|
|
||||||
|
|
||||||
console.log('🟢 Writing');
|
|
||||||
writeFile(
|
|
||||||
'dist/ratingsPlot.csv',
|
|
||||||
data.map(([neighbors, time]) => `${neighbors},${time}`).join('\n'),
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"incremental": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { Region } from '../../../../../../constants/regions';
|
import { Region } from '../../../../../../constants/regions';
|
||||||
import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo';
|
import getRatingInfo from '../../../../../../core/blitz/getRatingInfo';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: { region: Region } },
|
{ params }: { params: { region: Region } },
|
||||||
) {
|
) {
|
||||||
return NextResponse.json(await getRatingsInfo(params.region));
|
return NextResponse.json(await getRatingInfo(params.region));
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { Region } from '../../../../../../../constants/regions';
|
import { Region } from '../../../../../../../constants/regions';
|
||||||
import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague';
|
import { getRatingLeague } from '../../../../../../../core/blitz/getRatingLeague';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: { region: Region; league: number } },
|
{ params }: { params: { region: Region; league: number } },
|
||||||
) {
|
) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
await getRatingsLeague(params.region, params.league, true),
|
await getRatingLeague(params.region, params.league, true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { Region } from '../../../../../../../constants/regions';
|
import { Region } from '../../../../../../../constants/regions';
|
||||||
import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers';
|
import { searchCurrentRatingPlayers } from '../../../../../../../core/blitz/searchCurrentRatingPlayers';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: { region: Region; username: string } },
|
{ params }: { params: { region: Region; username: string } },
|
||||||
) {
|
) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
await searchCurrentRatingsPlayers(params.region, params.username),
|
await searchCurrentRatingPlayers(params.region, params.username),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+2
-3
@@ -3,12 +3,12 @@
|
|||||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||||
import { AlertDialog, Button, Flex, Text, 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 { Analytics } from '@vercel/analytics/react';
|
|
||||||
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, useEffect, useState } from 'react';
|
||||||
import { SkyLine } from '../assets/art/SkyLine';
|
import { SkyLine } from '../assets/art/SkyLine';
|
||||||
|
import { Analytics } from '../components/Analytics';
|
||||||
import Navbar from '../components/Navbar';
|
import Navbar from '../components/Navbar';
|
||||||
import PageWrapper from '../components/PageWrapper';
|
import PageWrapper from '../components/PageWrapper';
|
||||||
import isDev from '../core/blitzkrieg/isDev';
|
import isDev from '../core/blitzkrieg/isDev';
|
||||||
@@ -51,6 +51,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
name="description"
|
name="description"
|
||||||
content="🎉 Tools for everything World of Tanks Blitz"
|
content="🎉 Tools for everything World of Tanks Blitz"
|
||||||
/>
|
/>
|
||||||
|
<Analytics />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
@@ -60,8 +61,6 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
backgroundColor: isEmbed ? 'transparent' : undefined,
|
backgroundColor: isEmbed ? 'transparent' : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Analytics />
|
|
||||||
|
|
||||||
<Theme
|
<Theme
|
||||||
appearance="dark"
|
appearance="dark"
|
||||||
panelBackground="translucent"
|
panelBackground="translucent"
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
@@ -15,11 +15,11 @@ import { produce } from 'immer';
|
|||||||
import { debounce, range } from 'lodash';
|
import { debounce, range } from 'lodash';
|
||||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { RatingsInfo, RatingsPlayer } from '../../../commands/ratings';
|
import { RatingInfo, RatingPlayer } from '../../../commands/ratingLeaderboard';
|
||||||
import PageWrapper from '../../../components/PageWrapper';
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
import { Skeleton } from '../../../components/Skeleton';
|
import { Skeleton } from '../../../components/Skeleton';
|
||||||
import { LEAGUES } from '../../../constants/leagues';
|
import { LEAGUES } from '../../../constants/leagues';
|
||||||
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings';
|
import { FIRST_MINIMAL_ARCHIVED_RATING_SEASON } from '../../../constants/rating';
|
||||||
import {
|
import {
|
||||||
REGIONS,
|
REGIONS,
|
||||||
Region,
|
Region,
|
||||||
@@ -28,17 +28,17 @@ import {
|
|||||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
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 getRatingsInfo from '../../../core/blitz/getRatingsInfo';
|
import getRatingInfo from '../../../core/blitz/getRatingInfo';
|
||||||
import { getRatingsLeague } from '../../../core/blitz/getRatingsLeague';
|
import { getRatingLeague } from '../../../core/blitz/getRatingLeague';
|
||||||
import { getRatingsNeighbors } from '../../../core/blitz/getRatingsNeighbors';
|
import { getRatingNeighbors } from '../../../core/blitz/getRatingNeighbors';
|
||||||
import { searchCurrentRatingsPlayers } from '../../../core/blitz/searchCurrentRatingsPlayers';
|
import { searchCurrentRatingPlayers } from '../../../core/blitz/searchCurrentRatingPlayers';
|
||||||
import {
|
import {
|
||||||
AccountList,
|
AccountList,
|
||||||
AccountListItem,
|
AccountListItem,
|
||||||
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||||
import { getArchivedLatestSeasonNumber } from '../../../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
import { getArchivedLatestSeasonNumber } from '../../../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
||||||
import getArchivedRatingsInfo from '../../../core/blitzkrieg/getArchivedRatingsInfo';
|
import getArchivedRatingInfo from '../../../core/blitzkrieg/getArchivedRatingInfo';
|
||||||
import { getArchivedRatingsLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingsLeaderboard';
|
import { getArchivedRatingLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingLeaderboard';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
import { PageTurner } from './components/PageTurner';
|
import { PageTurner } from './components/PageTurner';
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ interface NameCacheEntry {
|
|||||||
}
|
}
|
||||||
type NameCache = Record<Region, Record<number, NameCacheEntry | null>>;
|
type NameCache = Record<Region, Record<number, NameCacheEntry | null>>;
|
||||||
type LeaderboardCache = Record<Region, Record<number, Record<number, number>>>;
|
type LeaderboardCache = Record<Region, Record<number, Record<number, number>>>;
|
||||||
type InfoCache = Record<Region, Record<number, RatingsInfo | undefined>>;
|
type InfoCache = Record<Region, Record<number, RatingInfo | undefined>>;
|
||||||
type ScoreCache = Record<Region, Record<number, Record<number, number>>>;
|
type ScoreCache = Record<Region, Record<number, Record<number, number>>>;
|
||||||
|
|
||||||
const useNameCache = create<NameCache>(() => ({
|
const useNameCache = create<NameCache>(() => ({
|
||||||
@@ -93,7 +93,7 @@ export default function Page() {
|
|||||||
{ type: 'id'; id: number } | { type: 'position'; position: number } | null
|
{ type: 'id'; id: number } | { type: 'position'; position: number } | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const ratingsInfo = useInfoCache()[region][season];
|
const ratingInfo = useInfoCache()[region][season];
|
||||||
const [latestArchivedSeasonNumber, setLatestArchivedSeasonNumber] = useState<
|
const [latestArchivedSeasonNumber, setLatestArchivedSeasonNumber] = useState<
|
||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
@@ -111,7 +111,7 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pages = Math.ceil(
|
const pages = Math.ceil(
|
||||||
(ratingsInfo && ratingsInfo.detail === undefined ? ratingsInfo?.count : 1) /
|
(ratingInfo && ratingInfo.detail === undefined ? ratingInfo?.count : 1) /
|
||||||
ROWS_PER_PAGE,
|
ROWS_PER_PAGE,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -122,8 +122,8 @@ export default function Page() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
const newInfo =
|
const newInfo =
|
||||||
season === 0
|
season === 0
|
||||||
? await getRatingsInfo(region)
|
? await getRatingInfo(region)
|
||||||
: await getArchivedRatingsInfo(region, season);
|
: await getArchivedRatingInfo(region, season);
|
||||||
useInfoCache.setState(
|
useInfoCache.setState(
|
||||||
produce((draft: InfoCache) => {
|
produce((draft: InfoCache) => {
|
||||||
draft[region][season] = newInfo;
|
draft[region][season] = newInfo;
|
||||||
@@ -140,7 +140,7 @@ export default function Page() {
|
|||||||
const results = (
|
const results = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
range(0, 5).map(async (league: number) => {
|
range(0, 5).map(async (league: number) => {
|
||||||
const data = await getRatingsLeague(region, league);
|
const data = await getRatingLeague(region, league);
|
||||||
leaguePositionCache.current[region][league] = data[0].number - 1;
|
leaguePositionCache.current[region][league] = data[0].number - 1;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -178,12 +178,12 @@ export default function Page() {
|
|||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
} else {
|
} else {
|
||||||
getArchivedRatingsLeaderboard(region, season).then((results) => {
|
getArchivedRatingLeaderboard(region, season).then((results) => {
|
||||||
useLeaderboardCache.setState(
|
useLeaderboardCache.setState(
|
||||||
produce((draft: LeaderboardCache) => {
|
produce((draft: LeaderboardCache) => {
|
||||||
if (!(season in draft[region])) draft[region][season] = {};
|
if (!(season in draft[region])) draft[region][season] = {};
|
||||||
|
|
||||||
results.forEach((result, index) => {
|
results.entries.forEach((result, index) => {
|
||||||
draft[region][season][index] = result.id;
|
draft[region][season][index] = result.id;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
@@ -192,7 +192,7 @@ export default function Page() {
|
|||||||
produce((draft: ScoreCache) => {
|
produce((draft: ScoreCache) => {
|
||||||
if (!(season in draft[region])) draft[region][season] = {};
|
if (!(season in draft[region])) draft[region][season] = {};
|
||||||
|
|
||||||
results.forEach((result) => {
|
results.entries.forEach((result) => {
|
||||||
draft[region][season][result.id] = result.score;
|
draft[region][season][result.id] = result.score;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
@@ -213,7 +213,7 @@ export default function Page() {
|
|||||||
}, [leaderboard[region]?.[season] && 0 in leaderboard[region][season]]);
|
}, [leaderboard[region]?.[season] && 0 in leaderboard[region][season]]);
|
||||||
// neighboring page caching for all seasons
|
// neighboring page caching for all seasons
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ratingsInfo || ratingsInfo?.detail) return;
|
if (!ratingInfo || ratingInfo?.detail) return;
|
||||||
|
|
||||||
if (season === 0) {
|
if (season === 0) {
|
||||||
const middleIndex = page * ROWS_PER_PAGE + ROWS_PER_PAGE / 2;
|
const middleIndex = page * ROWS_PER_PAGE + ROWS_PER_PAGE / 2;
|
||||||
@@ -236,7 +236,7 @@ export default function Page() {
|
|||||||
const ids = range(
|
const ids = range(
|
||||||
Math.max(0, (page - 1) * ROWS_PER_PAGE),
|
Math.max(0, (page - 1) * ROWS_PER_PAGE),
|
||||||
Math.min(
|
Math.min(
|
||||||
ratingsInfo.count - 1,
|
ratingInfo.count - 1,
|
||||||
(page + 1) * ROWS_PER_PAGE + ROWS_PER_PAGE,
|
(page + 1) * ROWS_PER_PAGE + ROWS_PER_PAGE,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -265,7 +265,7 @@ export default function Page() {
|
|||||||
page,
|
page,
|
||||||
region,
|
region,
|
||||||
season,
|
season,
|
||||||
ratingsInfo?.detail,
|
ratingInfo?.detail,
|
||||||
leaderboard[region][season] !== undefined &&
|
leaderboard[region][season] !== undefined &&
|
||||||
0 in leaderboard[region][season],
|
0 in leaderboard[region][season],
|
||||||
]);
|
]);
|
||||||
@@ -279,7 +279,7 @@ export default function Page() {
|
|||||||
async function cacheNeighbors(
|
async function cacheNeighbors(
|
||||||
id: number,
|
id: number,
|
||||||
size: number,
|
size: number,
|
||||||
): Promise<RatingsPlayer[] | null> {
|
): Promise<RatingPlayer[] | null> {
|
||||||
const radius = Math.round(size / 2);
|
const radius = Math.round(size / 2);
|
||||||
const targetPosition = Object.entries(leaderboard[region][season]).find(
|
const targetPosition = Object.entries(leaderboard[region][season]).find(
|
||||||
([, player]) => id === player,
|
([, player]) => id === player,
|
||||||
@@ -303,7 +303,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { neighbors } = await getRatingsNeighbors(region, id, size, true);
|
const { neighbors } = await getRatingNeighbors(region, id, size, true);
|
||||||
|
|
||||||
useLeaderboardCache.setState(
|
useLeaderboardCache.setState(
|
||||||
produce((draft: LeaderboardCache) => {
|
produce((draft: LeaderboardCache) => {
|
||||||
@@ -341,11 +341,11 @@ export default function Page() {
|
|||||||
return neighbors;
|
return neighbors;
|
||||||
}
|
}
|
||||||
async function jumpToPosition(position: number) {
|
async function jumpToPosition(position: number) {
|
||||||
if (!ratingsInfo || ratingsInfo?.detail) return;
|
if (!ratingInfo || ratingInfo?.detail) return;
|
||||||
|
|
||||||
const targetPosition = Math.min(
|
const targetPosition = Math.min(
|
||||||
ratingsInfo.count - 1,
|
ratingInfo.count - 1,
|
||||||
Math.max(0, Math.min(position, ratingsInfo.count - 1)),
|
Math.max(0, Math.min(position, ratingInfo.count - 1)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (season === 0) {
|
if (season === 0) {
|
||||||
@@ -378,7 +378,7 @@ export default function Page() {
|
|||||||
const loadingDirection = Math.sign(targetPosition - closestPosition);
|
const loadingDirection = Math.sign(targetPosition - closestPosition);
|
||||||
let seedingPlayer = closestPlayerId as number;
|
let seedingPlayer = closestPlayerId as number;
|
||||||
let targetPositionAcquired = false;
|
let targetPositionAcquired = false;
|
||||||
let neighbors: RatingsPlayer[] | null;
|
let neighbors: RatingPlayer[] | null;
|
||||||
|
|
||||||
setLoadingProgress([0, closestPositionDistance]);
|
setLoadingProgress([0, closestPositionDistance]);
|
||||||
|
|
||||||
@@ -423,7 +423,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function jumpToScore() {
|
async function jumpToScore() {
|
||||||
if (!ratingsInfo || ratingsInfo?.detail) return;
|
if (!ratingInfo || ratingInfo?.detail) return;
|
||||||
|
|
||||||
const targetScore = scoreInput.current!.valueAsNumber;
|
const targetScore = scoreInput.current!.valueAsNumber;
|
||||||
|
|
||||||
@@ -522,7 +522,7 @@ export default function Page() {
|
|||||||
} else {
|
} else {
|
||||||
let playerIndex = -1;
|
let playerIndex = -1;
|
||||||
|
|
||||||
for (let index = ratingsInfo.count; index >= 0; index--) {
|
for (let index = ratingInfo.count; index >= 0; index--) {
|
||||||
if (!(index in leaderboard[region][season])) continue;
|
if (!(index in leaderboard[region][season])) continue;
|
||||||
|
|
||||||
if (scores[leaderboard[region][season][index]] >= targetScore) {
|
if (scores[leaderboard[region][season][index]] >= targetScore) {
|
||||||
@@ -545,7 +545,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalPlayers =
|
const totalPlayers =
|
||||||
!ratingsInfo?.detail && ratingsInfo?.count ? ratingsInfo.count : undefined;
|
!ratingInfo?.detail && ratingInfo?.count ? ratingInfo.count : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="orange">
|
<PageWrapper color="orange">
|
||||||
@@ -573,7 +573,7 @@ export default function Page() {
|
|||||||
<Select.Trigger style={{ flex: 1 }} />
|
<Select.Trigger style={{ flex: 1 }} />
|
||||||
|
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{ratingsInfo?.detail === undefined && (
|
{ratingInfo?.detail === undefined && (
|
||||||
<Select.Item value="0">Current season</Select.Item>
|
<Select.Item value="0">Current season</Select.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -582,7 +582,7 @@ export default function Page() {
|
|||||||
|
|
||||||
{latestArchivedSeasonNumber &&
|
{latestArchivedSeasonNumber &&
|
||||||
range(
|
range(
|
||||||
FIRST_ARCHIVED_RATINGS_SEASON,
|
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
|
||||||
latestArchivedSeasonNumber + 1,
|
latestArchivedSeasonNumber + 1,
|
||||||
)
|
)
|
||||||
.map((season) => (
|
.map((season) => (
|
||||||
@@ -681,8 +681,8 @@ export default function Page() {
|
|||||||
<Select.Trigger />
|
<Select.Trigger />
|
||||||
|
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{ratingsInfo?.detail === undefined &&
|
{ratingInfo?.detail === undefined &&
|
||||||
ratingsInfo?.leagues.map(({ index, title }) => (
|
ratingInfo?.leagues.map(({ index, title }) => (
|
||||||
<Select.Item key={index} value={`${index}`}>
|
<Select.Item key={index} value={`${index}`}>
|
||||||
{title}
|
{title}
|
||||||
</Select.Item>
|
</Select.Item>
|
||||||
@@ -714,11 +714,7 @@ export default function Page() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (!ratingInfo || ratingInfo.detail || !leaderboard)
|
||||||
!ratingsInfo ||
|
|
||||||
ratingsInfo.detail ||
|
|
||||||
!leaderboard
|
|
||||||
)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const minScore =
|
const minScore =
|
||||||
@@ -727,7 +723,7 @@ export default function Page() {
|
|||||||
|
|
||||||
for (
|
for (
|
||||||
let index = 0;
|
let index = 0;
|
||||||
index < ratingsInfo.count;
|
index < ratingInfo.count;
|
||||||
index++
|
index++
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
@@ -779,7 +775,7 @@ export default function Page() {
|
|||||||
setSearchLoading(true);
|
setSearchLoading(true);
|
||||||
|
|
||||||
const accountList =
|
const accountList =
|
||||||
await searchCurrentRatingsPlayers(
|
await searchCurrentRatingPlayers(
|
||||||
region,
|
region,
|
||||||
trimmedSearch,
|
trimmedSearch,
|
||||||
);
|
);
|
||||||
@@ -885,12 +881,12 @@ export default function Page() {
|
|||||||
id: player.account_id,
|
id: player.account_id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!ratingsInfo || ratingsInfo.detail) return;
|
if (!ratingInfo || ratingInfo.detail) return;
|
||||||
let playerIndex = -1;
|
let playerIndex = -1;
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let index = 0;
|
let index = 0;
|
||||||
index < ratingsInfo.count;
|
index < ratingInfo.count;
|
||||||
index++
|
index++
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
@@ -951,9 +947,7 @@ export default function Page() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<PageTurner
|
<PageTurner
|
||||||
totalPlayers={
|
totalPlayers={(ratingInfo?.detail ? undefined : ratingInfo?.count) ?? 0}
|
||||||
(ratingsInfo?.detail ? undefined : ratingsInfo?.count) ?? 0
|
|
||||||
}
|
|
||||||
leaderboard={leaderboard[region][season] ?? {}}
|
leaderboard={leaderboard[region][season] ?? {}}
|
||||||
rowsPerPage={ROWS_PER_PAGE}
|
rowsPerPage={ROWS_PER_PAGE}
|
||||||
page={page}
|
page={page}
|
||||||
@@ -982,7 +976,7 @@ export default function Page() {
|
|||||||
page * ROWS_PER_PAGE,
|
page * ROWS_PER_PAGE,
|
||||||
Math.min(
|
Math.min(
|
||||||
page * ROWS_PER_PAGE + ROWS_PER_PAGE,
|
page * ROWS_PER_PAGE + ROWS_PER_PAGE,
|
||||||
ratingsInfo?.detail ? 0 : (ratingsInfo?.count ?? 1) - 1,
|
ratingInfo?.detail ? 0 : (ratingInfo?.count ?? 1) - 1,
|
||||||
),
|
),
|
||||||
).map((index) => {
|
).map((index) => {
|
||||||
const id = leaderboard[region][season]?.[index];
|
const id = leaderboard[region][season]?.[index];
|
||||||
@@ -994,8 +988,8 @@ export default function Page() {
|
|||||||
: highlightedPlayer.position === index
|
: highlightedPlayer.position === index
|
||||||
: false;
|
: false;
|
||||||
const reward =
|
const reward =
|
||||||
!ratingsInfo?.detail && ratingsInfo?.rewards
|
!ratingInfo?.detail && ratingInfo?.rewards
|
||||||
? ratingsInfo.rewards.find(
|
? ratingInfo.rewards.find(
|
||||||
(reward) =>
|
(reward) =>
|
||||||
reward.from_position <= position &&
|
reward.from_position <= position &&
|
||||||
reward.to_position >= position,
|
reward.to_position >= position,
|
||||||
@@ -1090,9 +1084,7 @@ export default function Page() {
|
|||||||
</Table.Root>
|
</Table.Root>
|
||||||
|
|
||||||
<PageTurner
|
<PageTurner
|
||||||
totalPlayers={
|
totalPlayers={(ratingInfo?.detail ? undefined : ratingInfo?.count) ?? 0}
|
||||||
(ratingsInfo?.detail ? undefined : ratingsInfo?.count) ?? 0
|
|
||||||
}
|
|
||||||
rowsPerPage={ROWS_PER_PAGE}
|
rowsPerPage={ROWS_PER_PAGE}
|
||||||
leaderboard={leaderboard[region][season] ?? {}}
|
leaderboard={leaderboard[region][season] ?? {}}
|
||||||
page={page}
|
page={page}
|
||||||
@@ -419,6 +419,11 @@ export function Characteristics() {
|
|||||||
<InfoWithDelta name="Module damage" unit="hp" decimals={0}>
|
<InfoWithDelta name="Module damage" unit="hp" decimals={0}>
|
||||||
{shell.damage.module * damageCoefficient}
|
{shell.damage.module * damageCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
|
{isExplosive(shell.type) && (
|
||||||
|
<InfoWithDelta name="Splash radius" unit="m" decimals={0}>
|
||||||
|
{shell.explosionRadius!}
|
||||||
|
</InfoWithDelta>
|
||||||
|
)}
|
||||||
<InfoWithDelta name="Shell velocity" unit="m/s" decimals={0}>
|
<InfoWithDelta name="Shell velocity" unit="m/s" decimals={0}>
|
||||||
{shell.speed * shellVelocityCoefficient}
|
{shell.speed * shellVelocityCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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/blitzkrieg/pose';
|
||||||
import mutateTankopediaPersistent, {
|
import mutateTankopediaPersistent, {
|
||||||
|
mutateTankopediaTemporary,
|
||||||
useTankopediaPersistent,
|
useTankopediaPersistent,
|
||||||
} from '../../../../../../../stores/tankopedia';
|
} from '../../../../../../../stores/tankopedia';
|
||||||
import { ENVIRONMENTS } from '../../Lighting';
|
import { ENVIRONMENTS } from '../../Lighting';
|
||||||
@@ -30,6 +31,9 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
|||||||
const wireframe = useTankopediaPersistent(
|
const wireframe = useTankopediaPersistent(
|
||||||
(state) => state.model.visual.wireframe,
|
(state) => state.model.visual.wireframe,
|
||||||
);
|
);
|
||||||
|
const showSpacedArmor = useTankopediaPersistent(
|
||||||
|
(state) => state.model.visual.showSpacedArmor,
|
||||||
|
);
|
||||||
const opaque = useTankopediaPersistent((state) => state.model.visual.opaque);
|
const opaque = useTankopediaPersistent((state) => state.model.visual.opaque);
|
||||||
const fullScreenAvailable =
|
const fullScreenAvailable =
|
||||||
typeof document !== 'undefined' && document.fullscreenEnabled;
|
typeof document !== 'undefined' && document.fullscreenEnabled;
|
||||||
@@ -81,6 +85,20 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
|||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
<DropdownMenu.Label>Armor</DropdownMenu.Label>
|
<DropdownMenu.Label>Armor</DropdownMenu.Label>
|
||||||
|
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={showSpacedArmor}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.model.visual.showSpacedArmor = checked;
|
||||||
|
});
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Spaced armor
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={greenPenetration}
|
checked={greenPenetration}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
|
|||||||
@@ -46,15 +46,11 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 8,
|
left: 8,
|
||||||
bottom: isFullScreen ? 80 : 8,
|
bottom: isFullScreen ? 80 : 8,
|
||||||
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
gap="4"
|
gap="4"
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex gap="2" style={{ width: 256, pointerEvents: 'none' }}>
|
||||||
gap="2"
|
|
||||||
style={{
|
|
||||||
width: 256,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -94,13 +90,7 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
ref={turretYawInput}
|
ref={turretYawInput}
|
||||||
style={{ textAlign: 'right' }}
|
style={{ textAlign: 'right' }}
|
||||||
/>
|
/>
|
||||||
<TextField.Slot
|
<TextField.Slot style={{ userSelect: 'none' }}>°</TextField.Slot>
|
||||||
style={{
|
|
||||||
userSelect: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
°
|
|
||||||
</TextField.Slot>
|
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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 { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
import { emblemURL } from '../core/blitzkrieg/emblemURL';
|
||||||
import getTankHistories from '../core/blitzkrieg/getTankHistories';
|
import getTankHistories from '../core/blitzkrieg/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';
|
||||||
@@ -41,7 +41,7 @@ async function render(
|
|||||||
) {
|
) {
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
const logo = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
const logo = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
const histories = await (type === 'player'
|
const histories = await (type === 'player'
|
||||||
? getPlayerHistories(region, id, {
|
? getPlayerHistories(region, id, {
|
||||||
start,
|
start,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import TierWeights from '../components/TierWeights';
|
|||||||
import TitleBar from '../components/TitleBar';
|
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 { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
import { emblemURL } from '../core/blitzkrieg/emblemURL';
|
||||||
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
@@ -40,7 +40,7 @@ async function render(
|
|||||||
) {
|
) {
|
||||||
const { nickname } = await getAccountInfo(region, id);
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
const diffedTankStats = await getStatsInPeriod(
|
const diffedTankStats = await getStatsInPeriod(
|
||||||
region,
|
region,
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { teal } from '@radix-ui/colors';
|
|||||||
import { GuildMemberRoleManager } from 'discord.js';
|
import { GuildMemberRoleManager } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import discord from '../../discord.json' assert { type: 'json' };
|
import discord from '../../discord.json' assert { type: 'json' };
|
||||||
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
|
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
|
||||||
import CommandWrapper from '../components/CommandWrapper';
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
|||||||
+30
-74
@@ -1,14 +1,13 @@
|
|||||||
import CommandWrapper from '../components/CommandWrapper';
|
import { chunk } from 'lodash';
|
||||||
import NoData from '../components/NoData';
|
import markdownEscape from 'markdown-escape';
|
||||||
import * as Tanks from '../components/Tanks';
|
import { TANK_CLASSES } from '../components/Tanks/components/Item/constants';
|
||||||
import TitleBar from '../components/TitleBar';
|
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
|
||||||
import getTankStats from '../core/blitz/getTankStats';
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
|
||||||
import {
|
import {
|
||||||
|
NATIONS,
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
Tier,
|
Tier,
|
||||||
|
flags,
|
||||||
tankDefinitions,
|
tankDefinitions,
|
||||||
} from '../core/blitzkrieg/tankDefinitions';
|
} from '../core/blitzkrieg/tankDefinitions';
|
||||||
import { TIER_ROMAN_NUMERALS } from '../core/blitzkrieg/tankDefinitions/constants';
|
import { TIER_ROMAN_NUMERALS } from '../core/blitzkrieg/tankDefinitions/constants';
|
||||||
@@ -20,6 +19,8 @@ import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
|||||||
import { translator } from '../core/localization/translator';
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
|
const TANKS_PER_MESSAGE = 64;
|
||||||
|
|
||||||
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
@@ -32,10 +33,9 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
|||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const { translate } = translator(interaction.locale);
|
const { translate } = translator(interaction.locale);
|
||||||
const tier = Number(interaction.options.getString('tier'));
|
const tier = Number(interaction.options.getString('tier'));
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, region: server } = account;
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
const accountInfo = await getAccountInfo(server, id);
|
const tankStats = await getTankStats(region, id, interaction.locale);
|
||||||
const tankStats = await getTankStats(server, id, interaction.locale);
|
|
||||||
const filteredTanks = (
|
const filteredTanks = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
tankStats.map(async (tankData) => ({
|
tankStats.map(async (tankData) => ({
|
||||||
@@ -44,7 +44,6 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
).filter((tank) => tank.tankDefinitions?.tier === tier);
|
).filter((tank) => tank.tankDefinitions?.tier === tier);
|
||||||
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
|
|
||||||
const groupedTanks: Record<string, TankDefinition[]> = {};
|
const groupedTanks: Record<string, TankDefinition[]> = {};
|
||||||
const nations: string[] = [];
|
const nations: string[] = [];
|
||||||
|
|
||||||
@@ -57,71 +56,28 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
nations.sort();
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const awaitedNations = await NATIONS;
|
||||||
|
const lines = tankStats
|
||||||
|
.map(({ tank_id }) => awaitedTankDefinitions[tank_id])
|
||||||
|
.filter((tank) => tank.tier === tier)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
|
||||||
|
)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
awaitedNations.indexOf(a.nation) - awaitedNations.indexOf(b.nation),
|
||||||
|
)
|
||||||
|
.map((tank) => `${flags[tank.nation]} ${markdownEscape(tank.name)}`);
|
||||||
|
|
||||||
return (
|
return chunk(lines, TANKS_PER_MESSAGE).map((lines, index) =>
|
||||||
<CommandWrapper>
|
index === 0
|
||||||
<TitleBar
|
? `${translate('bot.commands.owned_tanks.body.title', [
|
||||||
title={accountInfo.nickname}
|
markdownEscape(accountInfo.nickname),
|
||||||
image={
|
|
||||||
clanAccountInfo?.clan
|
|
||||||
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
description={translate('bot.commands.owned_tanks.body.subtitle', [
|
|
||||||
TIER_ROMAN_NUMERALS[tier as Tier],
|
TIER_ROMAN_NUMERALS[tier as Tier],
|
||||||
])}
|
])}${lines.join('\n')}`
|
||||||
/>
|
: lines.join('\n'),
|
||||||
|
|
||||||
{filteredTanks.length === 0 && (
|
|
||||||
<NoData type="tanks_found" locale={interaction.locale} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{filteredTanks.length > 0 &&
|
|
||||||
(await Promise.all(
|
|
||||||
nations.map(async (nation) => {
|
|
||||||
const tanks = groupedTanks[nation];
|
|
||||||
const leftColumnSize = Math.ceil(tanks.length / 2);
|
|
||||||
const leftColumn = tanks.slice(0, leftColumnSize);
|
|
||||||
const rightColumn = tanks.slice(leftColumnSize);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tanks.Root key={nation}>
|
|
||||||
<Tanks.Title>
|
|
||||||
{translate(`common.nations.${nation}`)}
|
|
||||||
</Tanks.Title>
|
|
||||||
|
|
||||||
<Tanks.Row>
|
|
||||||
<Tanks.Column>
|
|
||||||
{await Promise.all(
|
|
||||||
leftColumn.map(async (tank) => (
|
|
||||||
<Tanks.Item
|
|
||||||
key={tank.id}
|
|
||||||
name={tank.name}
|
|
||||||
tankType={tank.class}
|
|
||||||
treeType={tank.treeType}
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
)}
|
|
||||||
</Tanks.Column>
|
|
||||||
<Tanks.Column>
|
|
||||||
{await Promise.all(
|
|
||||||
rightColumn.map(async (tank) => (
|
|
||||||
<Tanks.Item
|
|
||||||
key={tank.id}
|
|
||||||
name={tank.name}
|
|
||||||
tankType={tank.class}
|
|
||||||
treeType={tank.treeType}
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
)}
|
|
||||||
</Tanks.Column>
|
|
||||||
</Tanks.Row>
|
|
||||||
</Tanks.Root>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
))}
|
|
||||||
</CommandWrapper>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
|
|||||||
interaction.appPermissions?.has('AttachFiles'),
|
interaction.appPermissions?.has('AttachFiles'),
|
||||||
t`bot.commands.permissions.body.attach_files`,
|
t`bot.commands.permissions.body.attach_files`,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
interaction.appPermissions?.has('UseExternalEmojis'),
|
||||||
|
t`bot.commands.permissions.body.external_emojis`,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
const body = permissions
|
const body = permissions
|
||||||
.map(([hasPermission, description]) => {
|
.map(([hasPermission, description]) => {
|
||||||
|
|||||||
@@ -0,0 +1,494 @@
|
|||||||
|
import markdownEscape from 'markdown-escape';
|
||||||
|
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
|
||||||
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
|
import { DeltaCaret } from '../components/DeltaCaret';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import { getLeagueFromScore } from '../core/blitz/getLeagueFromScore';
|
||||||
|
import getRatingInfo from '../core/blitz/getRatingInfo';
|
||||||
|
import { getRatingNeighbors } from '../core/blitz/getRatingNeighbors';
|
||||||
|
import { normalizeLeagueIcon } from '../core/blitz/normalizeLeagueIcon';
|
||||||
|
import { emblemURL } from '../core/blitzkrieg/emblemURL';
|
||||||
|
import getArchivedRatingMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingMidnightLeaderboard';
|
||||||
|
import { webpToPng } from '../core/blitzkrieg/iconPng';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
|
import { deltaBkrlBlitzStats } from '../core/statistics/deltaBkrlBlitzStats';
|
||||||
|
import { BkrlFormat } from '../core/streams/bkrl';
|
||||||
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
|
const LEAGUE_ACCENT = [
|
||||||
|
'_purple',
|
||||||
|
'_sand',
|
||||||
|
'_amber',
|
||||||
|
'_mauve',
|
||||||
|
'_brown',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
command:
|
||||||
|
createLocalizedCommand('rating').addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { t, translate } = translator(interaction.locale);
|
||||||
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
|
const leaderboard = await getArchivedRatingMidnightLeaderboard(
|
||||||
|
region,
|
||||||
|
53,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (leaderboard?.format === BkrlFormat.Base) {
|
||||||
|
throw new Error(
|
||||||
|
'Encountered bkrl minimal format in latest season. Wait till next cron?',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratingInfo = await getRatingInfo(region);
|
||||||
|
const accountInfo = await getAccountInfo(region, id, [
|
||||||
|
'statistics.rating',
|
||||||
|
]);
|
||||||
|
const statsIndex = leaderboard?.entries.findIndex(
|
||||||
|
(entry) => entry.id === id,
|
||||||
|
);
|
||||||
|
const statsA = leaderboard ? leaderboard.entries[statsIndex!] : null;
|
||||||
|
const positionA = leaderboard ? statsIndex! + 1 : undefined;
|
||||||
|
const statsB1 = accountInfo.statistics.rating!;
|
||||||
|
const [statsB2] = (await getRatingNeighbors(region, id, 0)).neighbors;
|
||||||
|
const league = getLeagueFromScore(statsB2.score);
|
||||||
|
|
||||||
|
if (ratingInfo.detail) {
|
||||||
|
throw new Error(`Rating info not available for ${region}`);
|
||||||
|
}
|
||||||
|
if (!statsA) {
|
||||||
|
return t`bot.commands.rating.errors.unrecorded_stats`;
|
||||||
|
}
|
||||||
|
if (!statsB1) {
|
||||||
|
return translate('bot.commands.rating.errors.no_participation', [
|
||||||
|
markdownEscape(accountInfo.nickname),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reward = ratingInfo.rewards.find(
|
||||||
|
(reward) =>
|
||||||
|
statsB2.number >= reward.from_position &&
|
||||||
|
statsB2.number <= reward.to_position,
|
||||||
|
);
|
||||||
|
const positionDelta = leaderboard
|
||||||
|
? statsB2.number - positionA!
|
||||||
|
: undefined;
|
||||||
|
const delta = deltaBkrlBlitzStats(
|
||||||
|
statsA,
|
||||||
|
accountInfo.statistics.rating!,
|
||||||
|
statsB2,
|
||||||
|
);
|
||||||
|
const leagueTheme = LEAGUE_ACCENT[league.index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={accountInfo.nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${t`bot.commands.rating.body.subtitle`} • ${new Date().toLocaleDateString(
|
||||||
|
interaction.locale,
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
|
||||||
|
borderRadius: 4,
|
||||||
|
border: `1px solid ${theme.colors[`componentInteractive${leagueTheme}`]}`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{reward === undefined && (
|
||||||
|
<Glow
|
||||||
|
endOpacity={3 / 4}
|
||||||
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
||||||
|
style={{
|
||||||
|
transform: 'translateX(-16px) rotate(90deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
|
src={normalizeLeagueIcon(
|
||||||
|
ratingInfo.leagues[league.index].big_icon,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textHighContrast${leagueTheme}`],
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 900,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{statsB2.score.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
{delta.score !== 0 && (
|
||||||
|
<>
|
||||||
|
<DeltaCaret delta={delta.score} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
delta.score > 0
|
||||||
|
? theme.colors.textLowContrast_green
|
||||||
|
: theme.colors.textLowContrast_red,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.abs(delta.score).toLocaleString(
|
||||||
|
interaction.locale,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textLowContrast${leagueTheme}`],
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#{statsB2.number.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
{positionDelta !== undefined && positionDelta !== 0 && (
|
||||||
|
<>
|
||||||
|
<DeltaCaret delta={positionDelta} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
positionDelta > 0
|
||||||
|
? theme.colors.textLowContrast_green
|
||||||
|
: theme.colors.textLowContrast_red,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.abs(positionDelta).toLocaleString(
|
||||||
|
interaction.locale,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{reward && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textHighContrast${leagueTheme}`],
|
||||||
|
fontSize: 16,
|
||||||
|
maxWidth: 136,
|
||||||
|
textAlign: 'right',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{reward.type === 'vehicle'
|
||||||
|
? reward.vehicle.user_string
|
||||||
|
: reward.stuff.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textLowContrast${leagueTheme}`],
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reward
|
||||||
|
{reward.type === 'stuff' ? ` x${reward.count}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
src={await webpToPng(
|
||||||
|
reward.type === 'vehicle'
|
||||||
|
? reward.vehicle.preview_image_url
|
||||||
|
: reward.stuff.image_url,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{reward === undefined && (
|
||||||
|
<Glow
|
||||||
|
endOpacity={3 / 4}
|
||||||
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
||||||
|
style={{
|
||||||
|
transform: 'translateX(16px) rotate(-90deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* separated because fragment middle man invalidates relative parent: https://github.com/vercel/satori/issues/605 */}
|
||||||
|
{reward && (
|
||||||
|
<Glow
|
||||||
|
direction="reverse"
|
||||||
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: -40,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{reward && (
|
||||||
|
<Glow
|
||||||
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -40,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{delta.battles > 0 && (
|
||||||
|
<div style={{ padding: '8px 0', display: 'flex' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{delta.battles.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Battles
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(100 * (delta.wins / delta.battles)).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Winrate
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.round(delta.damage / delta.battles).toLocaleString(
|
||||||
|
interaction.locale,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Damage
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(100 * (delta.survived / delta.battles)).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Survival
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(delta.kills / delta.battles).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kills
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
import {
|
||||||
|
APIApplicationCommandOptionChoice,
|
||||||
|
Locale,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import { range } from 'lodash';
|
||||||
|
import markdownEscape from 'markdown-escape';
|
||||||
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
|
import * as Leaderboard from '../components/Leaderboard';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import { LEAGUES } from '../constants/leagues';
|
||||||
|
import { FIRST_MINIMAL_ARCHIVED_RATING_SEASON } from '../constants/rating';
|
||||||
|
import { Region } from '../constants/regions';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import getRatingInfo from '../core/blitz/getRatingInfo';
|
||||||
|
import { getRatingLeague } from '../core/blitz/getRatingLeague';
|
||||||
|
import { getRatingNeighbors } from '../core/blitz/getRatingNeighbors';
|
||||||
|
import { isOnGoingRatingSeason } from '../core/blitz/isOnGoingRatingSeason';
|
||||||
|
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
||||||
|
import getArchivedRatingInfo from '../core/blitzkrieg/getArchivedRatingInfo';
|
||||||
|
import { getArchivedRatingLeaderboard } from '../core/blitzkrieg/getArchivedRatingLeaderboard';
|
||||||
|
import getArchivedRatingMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingMidnightLeaderboard';
|
||||||
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
import { localizationObject } from '../core/discord/localizationObject';
|
||||||
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
|
import { BkrlDiscriminatedEntries } from '../core/streams/bkrl';
|
||||||
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
import { UserError } from '../hooks/userError';
|
||||||
|
|
||||||
|
export interface RatingPlayer {
|
||||||
|
spa_id: number;
|
||||||
|
mmr: number;
|
||||||
|
season_number: number;
|
||||||
|
calibrationBattlesLeft: number;
|
||||||
|
number: number;
|
||||||
|
percentile: number;
|
||||||
|
skip: boolean;
|
||||||
|
updated_at: string;
|
||||||
|
score: number;
|
||||||
|
nickname: string;
|
||||||
|
clan_tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueTop {
|
||||||
|
result: RatingPlayer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RatingReward = {
|
||||||
|
from_position: number;
|
||||||
|
to_position: number;
|
||||||
|
count: number;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: 'vehicle';
|
||||||
|
|
||||||
|
vehicle: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
nation: string;
|
||||||
|
subnation: string;
|
||||||
|
use_subnation_flag: boolean;
|
||||||
|
type_slug: string;
|
||||||
|
level: number;
|
||||||
|
roman_level: string;
|
||||||
|
user_string: string;
|
||||||
|
image_url: string;
|
||||||
|
preview_image_url: string;
|
||||||
|
is_premium: boolean;
|
||||||
|
is_collectible: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'stuff';
|
||||||
|
|
||||||
|
stuff: {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
title: string;
|
||||||
|
image_url: string;
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
sizes: {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface RatingLeague {
|
||||||
|
title: string;
|
||||||
|
small_icon: string;
|
||||||
|
big_icon: string;
|
||||||
|
background: string;
|
||||||
|
index: number;
|
||||||
|
percentile: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RatingInfo =
|
||||||
|
| {
|
||||||
|
detail: undefined;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
icon: string | null;
|
||||||
|
start_at: string;
|
||||||
|
finish_at: string;
|
||||||
|
current_season: number;
|
||||||
|
updated_at: string;
|
||||||
|
count: number;
|
||||||
|
|
||||||
|
rewards: RatingReward[];
|
||||||
|
leagues: RatingLeague[];
|
||||||
|
}
|
||||||
|
| { detail: { error: string } };
|
||||||
|
|
||||||
|
export interface RatingNeighbors {
|
||||||
|
neighbors: RatingPlayer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SimplifiedPlayer {
|
||||||
|
id: number;
|
||||||
|
score: number;
|
||||||
|
position: number;
|
||||||
|
clan: string | undefined;
|
||||||
|
nickname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_LIMIT = 10;
|
||||||
|
|
||||||
|
export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
|
||||||
|
async (resolve) => {
|
||||||
|
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
|
||||||
|
const onGoingSeason = await isOnGoingRatingSeason();
|
||||||
|
const seasonNumbers = range(
|
||||||
|
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
|
||||||
|
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
function addOptions(option: SlashCommandSubcommandBuilder) {
|
||||||
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
return option
|
||||||
|
.addIntegerOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(t`bot.commands.rating_leaderboard.options.limit`)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.rating_leaderboard.options.limit',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate(
|
||||||
|
'bot.commands.rating_leaderboard.options.limit.description',
|
||||||
|
[`${DEFAULT_LIMIT}`],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.rating_leaderboard.options.limit.description',
|
||||||
|
[`${DEFAULT_LIMIT}`],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(false)
|
||||||
|
.setMinValue(5)
|
||||||
|
.setMaxValue(30),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(t`bot.commands.rating_leaderboard.options.season`)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.rating_leaderboard.options.season',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate(
|
||||||
|
'bot.commands.rating_leaderboard.options.season.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.rating_leaderboard.options.season.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
|
...seasonNumbers
|
||||||
|
.map((number, index) => {
|
||||||
|
const current =
|
||||||
|
index === seasonNumbers.length - 1 && onGoingSeason;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: current
|
||||||
|
? translate(
|
||||||
|
'bot.commands.rating_leaderboard.options.season.choices.current',
|
||||||
|
[`${number}`],
|
||||||
|
)
|
||||||
|
: `${number}`,
|
||||||
|
value: `${number}`,
|
||||||
|
name_localizations: current
|
||||||
|
? localizationObject(
|
||||||
|
'bot.commands.rating_leaderboard.options.season.choices.current',
|
||||||
|
[`${number}`],
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
} satisfies APIApplicationCommandOptionChoice<string>;
|
||||||
|
})
|
||||||
|
.reverse(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: createLocalizedCommand('rating-leaderboard', [
|
||||||
|
{
|
||||||
|
subcommand: 'neighbors',
|
||||||
|
modify(option: SlashCommandSubcommandBuilder) {
|
||||||
|
option.addStringOption(addUsernameChoices);
|
||||||
|
addOptions(option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subcommand: 'league',
|
||||||
|
modify(option: SlashCommandSubcommandBuilder) {
|
||||||
|
option
|
||||||
|
.addStringOption((option) => {
|
||||||
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
return option
|
||||||
|
.setName(t`bot.commands.rating_leaderboard.options.league`)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.rating_leaderboard.options.league',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate(
|
||||||
|
'bot.commands.rating_leaderboard.options.league.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.rating_leaderboard.options.league.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.addChoices(
|
||||||
|
...LEAGUES.map(
|
||||||
|
({ name }, value) =>
|
||||||
|
({
|
||||||
|
name: translate(`common.leagues.${name}`),
|
||||||
|
value: `${value}`,
|
||||||
|
name_localizations: localizationObject(
|
||||||
|
`common.leagues.${name}`,
|
||||||
|
),
|
||||||
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(true);
|
||||||
|
})
|
||||||
|
.addStringOption(addRegionChoices);
|
||||||
|
addOptions(option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
// BIG TODO: title seems to be in english
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { t, translate } = translator(interaction.locale);
|
||||||
|
const subcommand = interaction.options.getSubcommand(true) as
|
||||||
|
| 'league'
|
||||||
|
| 'neighbors';
|
||||||
|
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
|
||||||
|
const seasonOption = interaction.options.getString('season');
|
||||||
|
const season = seasonOption
|
||||||
|
? parseInt(seasonOption)
|
||||||
|
: onGoingSeason
|
||||||
|
? latestArchivedSeasonNumber + 1
|
||||||
|
: latestArchivedSeasonNumber;
|
||||||
|
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
|
||||||
|
let titleName: string;
|
||||||
|
let titleImage: string | undefined;
|
||||||
|
let titleDescription: string;
|
||||||
|
let playersAfter: number;
|
||||||
|
let players: SimplifiedPlayer[] | undefined;
|
||||||
|
let midnightLeaderboard: BkrlDiscriminatedEntries | null;
|
||||||
|
let playerId: number | undefined;
|
||||||
|
let regionRatingInfo: RatingInfo;
|
||||||
|
|
||||||
|
if (subcommand === 'league') {
|
||||||
|
const leagueIndex = parseInt(
|
||||||
|
interaction.options.getString('league')!,
|
||||||
|
);
|
||||||
|
const region = interaction.options.getString('region') as Region;
|
||||||
|
regionRatingInfo = await (isCurrentSeason
|
||||||
|
? getRatingInfo(region)
|
||||||
|
: getArchivedRatingInfo(region, season!));
|
||||||
|
|
||||||
|
if (regionRatingInfo.detail !== undefined)
|
||||||
|
return t`bot.commands.rating_leaderboard.body.no_ongoing_season`;
|
||||||
|
|
||||||
|
const result = isCurrentSeason
|
||||||
|
? await getRatingLeague(region, leagueIndex).then(async (result) =>
|
||||||
|
result.slice(0, limit).map(
|
||||||
|
(player) =>
|
||||||
|
({
|
||||||
|
id: player.spa_id,
|
||||||
|
score: player.score,
|
||||||
|
position: player.number,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: await getArchivedRatingLeaderboard(region, season!).then(
|
||||||
|
async (players) => {
|
||||||
|
const firstPlayerIndex =
|
||||||
|
leagueIndex === 0
|
||||||
|
? 0
|
||||||
|
: players.entries.findIndex(
|
||||||
|
(player) =>
|
||||||
|
player.score < LEAGUES[leagueIndex - 1].minScore,
|
||||||
|
);
|
||||||
|
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
||||||
|
const trimmed = players.entries.slice(
|
||||||
|
firstPlayerIndex,
|
||||||
|
lastPlayerIndex + 1,
|
||||||
|
);
|
||||||
|
const clanData = await getClanAccountInfo(
|
||||||
|
region,
|
||||||
|
trimmed.map(({ id }) => id),
|
||||||
|
['clan'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return trimmed.map(
|
||||||
|
(player, index) =>
|
||||||
|
({
|
||||||
|
id: player.id,
|
||||||
|
score: player.score,
|
||||||
|
position: firstPlayerIndex + index + 1,
|
||||||
|
clan: clanData[player.id]?.clan?.tag,
|
||||||
|
nickname:
|
||||||
|
clanData[index]?.account_name ??
|
||||||
|
translate(
|
||||||
|
'bot.commands.rating_leaderboard.body.deleted_player',
|
||||||
|
[`${player.id}`],
|
||||||
|
),
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const leagueInfo = regionRatingInfo.leagues[leagueIndex];
|
||||||
|
midnightLeaderboard = isCurrentSeason
|
||||||
|
? await getArchivedRatingMidnightLeaderboard(
|
||||||
|
region,
|
||||||
|
regionRatingInfo.current_season,
|
||||||
|
)
|
||||||
|
: await getArchivedRatingMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
|
players = result;
|
||||||
|
playersAfter =
|
||||||
|
regionRatingInfo.count - result[result.length - 1].position;
|
||||||
|
titleName = `${
|
||||||
|
leagueInfo
|
||||||
|
? translate(`common.leagues.${LEAGUES[leagueInfo.index].name}`)
|
||||||
|
: ''
|
||||||
|
} - ${translate(`common.regions.short.${region}`)}`;
|
||||||
|
titleImage = leagueInfo.big_icon.startsWith('http')
|
||||||
|
? leagueInfo.big_icon
|
||||||
|
: `https:${leagueInfo.big_icon}`;
|
||||||
|
titleDescription = translate(
|
||||||
|
'bot.commands.rating_leaderboard.body.top_players',
|
||||||
|
[`${limit}`],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
|
regionRatingInfo = isCurrentSeason
|
||||||
|
? await getRatingInfo(region)
|
||||||
|
: await getArchivedRatingInfo(region, season!);
|
||||||
|
|
||||||
|
if (regionRatingInfo.detail !== undefined)
|
||||||
|
return t`bot.commands.rating_leaderboard.body.no_ongoing_season`;
|
||||||
|
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const neighbors = isCurrentSeason
|
||||||
|
? await getRatingNeighbors(region, id, Math.round(limit / 2)).then(
|
||||||
|
(players) =>
|
||||||
|
players.neighbors.map(
|
||||||
|
(player) =>
|
||||||
|
({
|
||||||
|
id: player.spa_id,
|
||||||
|
position: player.number,
|
||||||
|
score: player.score,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: await getArchivedRatingLeaderboard(region, season!).then(
|
||||||
|
async (players) => {
|
||||||
|
const playerIndex = players.entries.findIndex(
|
||||||
|
(player) => player.id === id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playerIndex === -1) {
|
||||||
|
throw new UserError(
|
||||||
|
translate(
|
||||||
|
'bot.commands.rating_leaderboard.body.no_participation',
|
||||||
|
[markdownEscape(accountInfo.nickname), `${season}`],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const halfRange = Math.round(limit / 2);
|
||||||
|
const trimmed = players.entries.slice(
|
||||||
|
playerIndex - halfRange,
|
||||||
|
playerIndex + halfRange + 1,
|
||||||
|
);
|
||||||
|
const clanData = await getClanAccountInfo(
|
||||||
|
region,
|
||||||
|
trimmed.map((player) => player.id),
|
||||||
|
['clan'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return trimmed.map((player, index) => ({
|
||||||
|
id: player.id,
|
||||||
|
score: player.score,
|
||||||
|
position: playerIndex + index + 1,
|
||||||
|
clan: clanData[index]?.clan?.tag,
|
||||||
|
nickname: clanData[index]
|
||||||
|
? clanData[index]!.account_name
|
||||||
|
: `Deleted ${player.id}`,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
midnightLeaderboard = isCurrentSeason
|
||||||
|
? await getArchivedRatingMidnightLeaderboard(
|
||||||
|
region,
|
||||||
|
regionRatingInfo.current_season,
|
||||||
|
)
|
||||||
|
: await getArchivedRatingMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
|
players = neighbors;
|
||||||
|
titleImage = clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
|
: undefined;
|
||||||
|
titleName = neighbors ? accountInfo.nickname : '';
|
||||||
|
titleDescription = t`bot.commands.rating_leaderboard.body.neighbors`;
|
||||||
|
playersAfter =
|
||||||
|
regionRatingInfo.count - neighbors[neighbors.length - 1].position;
|
||||||
|
playerId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = players.map((player) => {
|
||||||
|
const midnightIndex = midnightLeaderboard?.entries.findIndex(
|
||||||
|
(item) => item.id === player.id,
|
||||||
|
);
|
||||||
|
const midnightScore =
|
||||||
|
midnightIndex === undefined
|
||||||
|
? player.score
|
||||||
|
: midnightLeaderboard!.entries[midnightIndex].score;
|
||||||
|
const midnightPosition =
|
||||||
|
midnightIndex === undefined ? player.position : midnightIndex + 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Leaderboard.Item
|
||||||
|
nickname={player.nickname}
|
||||||
|
score={player.score}
|
||||||
|
position={player.position}
|
||||||
|
clan={player.clan}
|
||||||
|
deltaScore={player.score - midnightScore}
|
||||||
|
deltaPosition={midnightPosition - player.position}
|
||||||
|
key={player.id}
|
||||||
|
highlight={player.id === playerId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={titleName}
|
||||||
|
image={titleImage}
|
||||||
|
description={titleDescription}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{items && (
|
||||||
|
<Leaderboard.Root>
|
||||||
|
{items}
|
||||||
|
{playersAfter > 0 && (
|
||||||
|
<Leaderboard.Gap
|
||||||
|
message={translate(
|
||||||
|
'bot.commands.rating_leaderboard.body.more',
|
||||||
|
[`${playersAfter.toLocaleString(interaction.locale)}`],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Leaderboard.Root>
|
||||||
|
)}
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
import {
|
|
||||||
APIApplicationCommandOptionChoice,
|
|
||||||
Locale,
|
|
||||||
SlashCommandSubcommandBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import { range } from 'lodash';
|
|
||||||
import markdownEscape from 'markdown-escape';
|
|
||||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
|
||||||
import CommandWrapper from '../components/CommandWrapper';
|
|
||||||
import * as Leaderboard from '../components/Leaderboard';
|
|
||||||
import TitleBar from '../components/TitleBar';
|
|
||||||
import { LEAGUES } from '../constants/leagues';
|
|
||||||
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings';
|
|
||||||
import { Region } from '../constants/regions';
|
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
|
||||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
|
||||||
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
|
|
||||||
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
|
|
||||||
import { isOnGoingRatingsSeason } from '../core/blitz/isOnGoingRatingsSeason';
|
|
||||||
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
|
||||||
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
|
|
||||||
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
|
|
||||||
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
|
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
|
||||||
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
|
||||||
import { localizationObject } from '../core/discord/localizationObject';
|
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
|
||||||
import { translator } from '../core/localization/translator';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
|
||||||
import { UserError } from '../hooks/userError';
|
|
||||||
|
|
||||||
export interface RatingsPlayer {
|
|
||||||
spa_id: number;
|
|
||||||
mmr: number;
|
|
||||||
season_number: number;
|
|
||||||
calibrationBattlesLeft: number;
|
|
||||||
number: number;
|
|
||||||
percentile: number;
|
|
||||||
skip: boolean;
|
|
||||||
updated_at: string;
|
|
||||||
score: number;
|
|
||||||
nickname: string;
|
|
||||||
clan_tag: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LeagueTop {
|
|
||||||
result: RatingsPlayer[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RatingsReward = {
|
|
||||||
from_position: number;
|
|
||||||
to_position: number;
|
|
||||||
count: number;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: 'vehicle';
|
|
||||||
|
|
||||||
vehicle: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
nation: string;
|
|
||||||
subnation: string;
|
|
||||||
use_subnation_flag: boolean;
|
|
||||||
type_slug: string;
|
|
||||||
level: number;
|
|
||||||
roman_level: string;
|
|
||||||
user_string: string;
|
|
||||||
image_url: string;
|
|
||||||
preview_url: string;
|
|
||||||
is_premium: boolean;
|
|
||||||
is_collectible: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'stuff';
|
|
||||||
|
|
||||||
stuff: {
|
|
||||||
name: string;
|
|
||||||
count: number;
|
|
||||||
title: string;
|
|
||||||
image_url: string;
|
|
||||||
type: string;
|
|
||||||
|
|
||||||
sizes: {};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface RatingsLeague {
|
|
||||||
title: string;
|
|
||||||
small_icon: string;
|
|
||||||
big_icon: string;
|
|
||||||
background: string;
|
|
||||||
index: number;
|
|
||||||
percentile: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RatingsInfo =
|
|
||||||
| {
|
|
||||||
detail: undefined;
|
|
||||||
|
|
||||||
title: string;
|
|
||||||
icon: string | null;
|
|
||||||
start_at: string;
|
|
||||||
finish_at: string;
|
|
||||||
current_season: number;
|
|
||||||
updated_at: string;
|
|
||||||
count: number;
|
|
||||||
|
|
||||||
rewards: RatingsReward[];
|
|
||||||
leagues: RatingsLeague[];
|
|
||||||
}
|
|
||||||
| { detail: { error: string } };
|
|
||||||
|
|
||||||
export interface RatingsNeighbors {
|
|
||||||
neighbors: RatingsPlayer[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SimplifiedPlayer {
|
|
||||||
id: number;
|
|
||||||
score: number;
|
|
||||||
position: number;
|
|
||||||
clan: string | undefined;
|
|
||||||
nickname: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_LIMIT = 10;
|
|
||||||
|
|
||||||
export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
|
|
||||||
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
|
|
||||||
const onGoingSeason = await isOnGoingRatingsSeason();
|
|
||||||
const seasonNumbers = range(
|
|
||||||
FIRST_ARCHIVED_RATINGS_SEASON,
|
|
||||||
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
function addOptions(option: SlashCommandSubcommandBuilder) {
|
|
||||||
const { t, translate } = translator(Locale.EnglishUS);
|
|
||||||
|
|
||||||
return option
|
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.setName(t`bot.commands.ratings.options.limit`)
|
|
||||||
.setNameLocalizations(
|
|
||||||
localizationObject('bot.commands.ratings.options.limit'),
|
|
||||||
)
|
|
||||||
.setDescription(
|
|
||||||
translate('bot.commands.ratings.options.limit.description', [
|
|
||||||
`${DEFAULT_LIMIT}`,
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.setDescriptionLocalizations(
|
|
||||||
localizationObject(
|
|
||||||
'bot.commands.ratings.options.limit.description',
|
|
||||||
[`${DEFAULT_LIMIT}`],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setRequired(false)
|
|
||||||
.setMinValue(5)
|
|
||||||
.setMaxValue(30),
|
|
||||||
)
|
|
||||||
.addStringOption((option) =>
|
|
||||||
option
|
|
||||||
.setName(t`bot.commands.ratings.options.season`)
|
|
||||||
.setNameLocalizations(
|
|
||||||
localizationObject('bot.commands.ratings.options.season'),
|
|
||||||
)
|
|
||||||
.setDescription(
|
|
||||||
translate('bot.commands.ratings.options.season.description'),
|
|
||||||
)
|
|
||||||
.setDescriptionLocalizations(
|
|
||||||
localizationObject(
|
|
||||||
'bot.commands.ratings.options.season.description',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setRequired(false)
|
|
||||||
.addChoices(
|
|
||||||
...seasonNumbers
|
|
||||||
.map((number, index) => {
|
|
||||||
const current =
|
|
||||||
index === seasonNumbers.length - 1 && onGoingSeason;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: current
|
|
||||||
? translate(
|
|
||||||
'bot.commands.ratings.options.season.choices.current',
|
|
||||||
[`${number}`],
|
|
||||||
)
|
|
||||||
: `${number}`,
|
|
||||||
value: `${number}`,
|
|
||||||
name_localizations: current
|
|
||||||
? localizationObject(
|
|
||||||
'bot.commands.ratings.options.season.choices.current',
|
|
||||||
[`${number}`],
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
} satisfies APIApplicationCommandOptionChoice<string>;
|
|
||||||
})
|
|
||||||
.reverse(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
inProduction: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command: createLocalizedCommand('ratings', [
|
|
||||||
{
|
|
||||||
subcommand: 'neighbors',
|
|
||||||
modify(option: SlashCommandSubcommandBuilder) {
|
|
||||||
option.addStringOption(addUsernameChoices);
|
|
||||||
addOptions(option);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
subcommand: 'league',
|
|
||||||
modify(option: SlashCommandSubcommandBuilder) {
|
|
||||||
option
|
|
||||||
.addStringOption((option) => {
|
|
||||||
const { t, translate } = translator(Locale.EnglishUS);
|
|
||||||
|
|
||||||
return option
|
|
||||||
.setName(t`bot.commands.ratings.options.league`)
|
|
||||||
.setNameLocalizations(
|
|
||||||
localizationObject('bot.commands.ratings.options.league'),
|
|
||||||
)
|
|
||||||
.setDescription(
|
|
||||||
translate('bot.commands.ratings.options.league.description'),
|
|
||||||
)
|
|
||||||
.setDescriptionLocalizations(
|
|
||||||
localizationObject(
|
|
||||||
'bot.commands.ratings.options.league.description',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addChoices(
|
|
||||||
...LEAGUES.map(
|
|
||||||
({ name }, value) =>
|
|
||||||
({
|
|
||||||
name: translate(`common.leagues.${name}`),
|
|
||||||
value: `${value}`,
|
|
||||||
name_localizations: localizationObject(
|
|
||||||
`common.leagues.${name}`,
|
|
||||||
),
|
|
||||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setRequired(true);
|
|
||||||
})
|
|
||||||
.addStringOption(addRegionChoices);
|
|
||||||
addOptions(option);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
|
|
||||||
// BIG TODO: title seems to be in english
|
|
||||||
|
|
||||||
async handler(interaction) {
|
|
||||||
const { t, translate } = translator(interaction.locale);
|
|
||||||
const subcommand = interaction.options.getSubcommand(true) as
|
|
||||||
| 'league'
|
|
||||||
| 'neighbors';
|
|
||||||
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
|
|
||||||
const seasonOption = interaction.options.getString('season');
|
|
||||||
const season = seasonOption
|
|
||||||
? parseInt(seasonOption)
|
|
||||||
: onGoingSeason
|
|
||||||
? latestArchivedSeasonNumber + 1
|
|
||||||
: latestArchivedSeasonNumber;
|
|
||||||
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
|
|
||||||
let titleName: string;
|
|
||||||
let titleImage: string | undefined;
|
|
||||||
let titleDescription: string;
|
|
||||||
let playersAfter: number;
|
|
||||||
let players: SimplifiedPlayer[] | undefined;
|
|
||||||
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
|
||||||
let playerId: number | undefined;
|
|
||||||
let regionRatingsInfo: RatingsInfo;
|
|
||||||
|
|
||||||
if (subcommand === 'league') {
|
|
||||||
const leagueIndex = parseInt(interaction.options.getString('league')!);
|
|
||||||
const region = interaction.options.getString('region') as Region;
|
|
||||||
regionRatingsInfo = await (isCurrentSeason
|
|
||||||
? getRatingsInfo(region)
|
|
||||||
: getArchivedRatingsInfo(region, season!));
|
|
||||||
|
|
||||||
if (regionRatingsInfo.detail !== undefined)
|
|
||||||
return t`bot.commands.ratings.body.no_ongoing_season`;
|
|
||||||
|
|
||||||
const result = isCurrentSeason
|
|
||||||
? await getRatingsLeague(region, leagueIndex).then(async (result) =>
|
|
||||||
result.slice(0, limit).map(
|
|
||||||
(player) =>
|
|
||||||
({
|
|
||||||
id: player.spa_id,
|
|
||||||
score: player.score,
|
|
||||||
position: player.number,
|
|
||||||
clan: player.clan_tag,
|
|
||||||
nickname: player.nickname,
|
|
||||||
}) satisfies SimplifiedPlayer,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: await getArchivedRatingsLeaderboard(region, season!).then(
|
|
||||||
async (players) => {
|
|
||||||
const firstPlayerIndex =
|
|
||||||
leagueIndex === 0
|
|
||||||
? 0
|
|
||||||
: players.findIndex(
|
|
||||||
(player) =>
|
|
||||||
player.score < LEAGUES[leagueIndex - 1].minScore,
|
|
||||||
);
|
|
||||||
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
|
||||||
const trimmed = players.slice(
|
|
||||||
firstPlayerIndex,
|
|
||||||
lastPlayerIndex + 1,
|
|
||||||
);
|
|
||||||
const clanData = await getClanAccountInfo(
|
|
||||||
region,
|
|
||||||
trimmed.map(({ id }) => id),
|
|
||||||
['clan'],
|
|
||||||
);
|
|
||||||
|
|
||||||
return trimmed.map(
|
|
||||||
(player, index) =>
|
|
||||||
({
|
|
||||||
id: player.id,
|
|
||||||
score: player.score,
|
|
||||||
position: firstPlayerIndex + index + 1,
|
|
||||||
clan: clanData[player.id]?.clan?.tag,
|
|
||||||
nickname:
|
|
||||||
clanData[index]?.account_name ??
|
|
||||||
translate('bot.commands.ratings.body.deleted_player', [
|
|
||||||
`${player.id}`,
|
|
||||||
]),
|
|
||||||
}) satisfies SimplifiedPlayer,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
|
||||||
midnightLeaderboard = isCurrentSeason
|
|
||||||
? await getArchivedRatingsMidnightLeaderboard(
|
|
||||||
region,
|
|
||||||
regionRatingsInfo.current_season,
|
|
||||||
)
|
|
||||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
|
||||||
|
|
||||||
players = result;
|
|
||||||
playersAfter =
|
|
||||||
regionRatingsInfo.count - result[result.length - 1].position;
|
|
||||||
titleName = `${
|
|
||||||
leagueInfo
|
|
||||||
? translate(`common.leagues.${LEAGUES[leagueInfo.index].name}`)
|
|
||||||
: ''
|
|
||||||
} - ${translate(`common.regions.short.${region}`)}`;
|
|
||||||
titleImage = leagueInfo.big_icon.startsWith('http')
|
|
||||||
? leagueInfo.big_icon
|
|
||||||
: `https:${leagueInfo.big_icon}`;
|
|
||||||
titleDescription = translate('bot.commands.ratings.body.top_players', [
|
|
||||||
`${limit}`,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
|
||||||
regionRatingsInfo = isCurrentSeason
|
|
||||||
? await getRatingsInfo(region)
|
|
||||||
: await getArchivedRatingsInfo(region, season!);
|
|
||||||
|
|
||||||
if (regionRatingsInfo.detail !== undefined)
|
|
||||||
return t`bot.commands.ratings.body.no_ongoing_season`;
|
|
||||||
|
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
|
||||||
const neighbors = isCurrentSeason
|
|
||||||
? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then(
|
|
||||||
(players) =>
|
|
||||||
players.neighbors.map(
|
|
||||||
(player) =>
|
|
||||||
({
|
|
||||||
id: player.spa_id,
|
|
||||||
position: player.number,
|
|
||||||
score: player.score,
|
|
||||||
clan: player.clan_tag,
|
|
||||||
nickname: player.nickname,
|
|
||||||
}) satisfies SimplifiedPlayer,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: await getArchivedRatingsLeaderboard(region, season!).then(
|
|
||||||
async (players) => {
|
|
||||||
const playerIndex = players.findIndex(
|
|
||||||
(player) => player.id === id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (playerIndex === -1) {
|
|
||||||
throw new UserError(
|
|
||||||
translate('bot.commands.ratings.body.no_participation', [
|
|
||||||
markdownEscape(accountInfo.nickname),
|
|
||||||
`${season}`,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const halfRange = Math.round(limit / 2);
|
|
||||||
const trimmed = players.slice(
|
|
||||||
playerIndex - halfRange,
|
|
||||||
playerIndex + halfRange + 1,
|
|
||||||
);
|
|
||||||
const clanData = await getClanAccountInfo(
|
|
||||||
region,
|
|
||||||
trimmed.map((player) => player.id),
|
|
||||||
['clan'],
|
|
||||||
);
|
|
||||||
|
|
||||||
return trimmed.map((player, index) => ({
|
|
||||||
id: player.id,
|
|
||||||
score: player.score,
|
|
||||||
position: playerIndex + index + 1,
|
|
||||||
clan: clanData[index]?.clan?.tag,
|
|
||||||
nickname: clanData[index]
|
|
||||||
? clanData[index]!.account_name
|
|
||||||
: `Deleted ${player.id}`,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
midnightLeaderboard = isCurrentSeason
|
|
||||||
? await getArchivedRatingsMidnightLeaderboard(
|
|
||||||
region,
|
|
||||||
regionRatingsInfo.current_season,
|
|
||||||
)
|
|
||||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
|
||||||
|
|
||||||
players = neighbors;
|
|
||||||
titleImage = clan
|
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
|
||||||
: undefined;
|
|
||||||
titleName = neighbors ? accountInfo.nickname : '';
|
|
||||||
titleDescription = t`bot.commands.ratings.body.neighbors`;
|
|
||||||
playersAfter =
|
|
||||||
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
|
|
||||||
playerId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
|
|
||||||
detail: undefined;
|
|
||||||
} = regionRatingsInfo;
|
|
||||||
|
|
||||||
const items = players.map((player) => {
|
|
||||||
const reward = resolvedRegionRatingsInfo.rewards.find(
|
|
||||||
(reward) =>
|
|
||||||
player.position >= reward.from_position &&
|
|
||||||
player.position <= reward.to_position,
|
|
||||||
);
|
|
||||||
const midnightIndex = midnightLeaderboard?.findIndex(
|
|
||||||
(item) => item.id === player.id,
|
|
||||||
);
|
|
||||||
const midnightScore =
|
|
||||||
midnightIndex === undefined
|
|
||||||
? player.score
|
|
||||||
: midnightLeaderboard![midnightIndex].score;
|
|
||||||
const midnightPosition =
|
|
||||||
midnightIndex === undefined ? player.position : midnightIndex + 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Leaderboard.Item
|
|
||||||
nickname={player.nickname}
|
|
||||||
score={player.score}
|
|
||||||
position={player.position}
|
|
||||||
clan={player.clan}
|
|
||||||
reward={reward}
|
|
||||||
deltaScore={player.score - midnightScore}
|
|
||||||
deltaPosition={midnightPosition - player.position}
|
|
||||||
key={player.id}
|
|
||||||
highlight={player.id === playerId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommandWrapper>
|
|
||||||
<TitleBar
|
|
||||||
title={titleName}
|
|
||||||
image={titleImage}
|
|
||||||
description={titleDescription}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{items && (
|
|
||||||
<Leaderboard.Root>
|
|
||||||
{items}
|
|
||||||
{playersAfter > 0 && (
|
|
||||||
<Leaderboard.Gap
|
|
||||||
message={translate('bot.commands.ratings.body.more', [
|
|
||||||
`${playersAfter.toLocaleString(interaction.locale)}`,
|
|
||||||
])}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Leaderboard.Root>
|
|
||||||
)}
|
|
||||||
</CommandWrapper>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
import { Locale, escapeMarkdown } from 'discord.js';
|
||||||
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import { equipmentPriceMatrix } from '../constants/equipmentPrice';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
|
import resolveTankId from '../core/blitz/resolveTankId';
|
||||||
|
import { asset } from '../core/blitzkrieg/asset';
|
||||||
|
import { buildTechTreeLine } from '../core/blitzkrieg/buildTechTreeLine';
|
||||||
|
import { emblemURL } from '../core/blitzkrieg/emblemURL';
|
||||||
|
import { webpToPng } from '../core/blitzkrieg/iconPng';
|
||||||
|
import { resolveAncestry } from '../core/blitzkrieg/resolveAncestry';
|
||||||
|
import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions';
|
||||||
|
import {
|
||||||
|
TANK_ICONS,
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
} from '../core/blitzkrieg/tankDefinitions/constants';
|
||||||
|
import { tankIconPng } from '../core/blitzkrieg/tankIconPng';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
import { localizationObject } from '../core/discord/localizationObject';
|
||||||
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
|
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
const { t } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: createLocalizedCommand('research')
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(t`bot.commands.research.options.target_tank`)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject('bot.commands.research.options.target_tank'),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
t`bot.commands.research.options.target_tank.description`,
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.research.options.target_tank.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setAutocomplete(true)
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(t`bot.commands.research.options.starting_tank`)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject('bot.commands.research.options.starting_tank'),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
t`bot.commands.research.options.starting_tank.description`,
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.research.options.starting_tank.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setAutocomplete(true)
|
||||||
|
.setRequired(false),
|
||||||
|
)
|
||||||
|
.addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { t, translate } = translator(interaction.locale);
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const targetTankId = await resolveTankId(
|
||||||
|
interaction.options.getString('target-tank', true),
|
||||||
|
interaction.locale,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const startingTankRaw = interaction.options.getString('starting-tank');
|
||||||
|
const targetTank = awaitedTankDefinitions[targetTankId];
|
||||||
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
|
let startingTankId: number;
|
||||||
|
const targetTankAncestry = await resolveAncestry(targetTankId);
|
||||||
|
|
||||||
|
if (startingTankRaw) {
|
||||||
|
startingTankId = await resolveTankId(
|
||||||
|
startingTankRaw,
|
||||||
|
interaction.locale,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetTankId === startingTankId) {
|
||||||
|
return translate('bot.commands.research.errors.start_end_equal', [
|
||||||
|
escapeMarkdown(targetTank.name),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startingTank = awaitedTankDefinitions[startingTankId];
|
||||||
|
|
||||||
|
if (startingTank.tier > targetTank.tier) {
|
||||||
|
return translate('bot.commands.research.errors.unordered', [
|
||||||
|
escapeMarkdown(startingTank.name),
|
||||||
|
escapeMarkdown(targetTank.name),
|
||||||
|
escapeMarkdown(targetTank.name),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetTankAncestry.includes(startingTankId)) {
|
||||||
|
return translate('bot.commands.research.errors.tanks_not_on_line', [
|
||||||
|
escapeMarkdown(targetTank.name),
|
||||||
|
escapeMarkdown(startingTank.name),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (targetTank.treeType !== 'researchable') {
|
||||||
|
return translate('bot.commands.research.errors.non_tech_tree', [
|
||||||
|
escapeMarkdown(targetTank.name),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetTank.tier === 1) {
|
||||||
|
return translate('bot.commands.research.errors.tier_1', [
|
||||||
|
escapeMarkdown(targetTank.name),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tankStats = await getTankStats(region, id, interaction.locale);
|
||||||
|
|
||||||
|
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
|
||||||
|
return translate('bot.commands.research.errors.already_researched', [
|
||||||
|
escapeMarkdown(targetTank.name),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundAncestor = targetTankAncestry.find(
|
||||||
|
(id) =>
|
||||||
|
awaitedTankDefinitions[id].tier === 1 ||
|
||||||
|
tankStats.some(({ tank_id }) => tank_id === id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (foundAncestor === undefined) {
|
||||||
|
throw new Error('No first owned tech tree found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
startingTankId = foundAncestor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const techTreeLine = await buildTechTreeLine(
|
||||||
|
startingTankId,
|
||||||
|
targetTankId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const line = [...techTreeLine, startingTankId].reverse();
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
|
const costs = await Promise.all(
|
||||||
|
line.map(async (id) => {
|
||||||
|
const tank = awaitedTankDefinitions[id];
|
||||||
|
const turretXps = new Map<number, number>();
|
||||||
|
const gunXps = new Map<number, number>();
|
||||||
|
const engineXps = new Map<number, number>();
|
||||||
|
const trackXps = new Map<number, number>();
|
||||||
|
|
||||||
|
tank.turrets.forEach((turret) => {
|
||||||
|
if (turret.xp !== undefined) {
|
||||||
|
turretXps.set(turret.id, turret.xp);
|
||||||
|
}
|
||||||
|
|
||||||
|
turret.guns.forEach((gun) => {
|
||||||
|
if (gun.xp !== undefined) {
|
||||||
|
gunXps.set(gun.id, gun.xp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tank.engines.forEach((engine) => {
|
||||||
|
if (engine.xp !== undefined) {
|
||||||
|
engineXps.set(engine.id, engine.xp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tank.tracks.forEach((track) => {
|
||||||
|
if (track.xp !== undefined) {
|
||||||
|
trackXps.set(track.id, track.xp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let moduleXp = 0;
|
||||||
|
turretXps.forEach((value) => {
|
||||||
|
moduleXp += value;
|
||||||
|
});
|
||||||
|
gunXps.forEach((value) => {
|
||||||
|
moduleXp += value;
|
||||||
|
});
|
||||||
|
engineXps.forEach((value) => {
|
||||||
|
moduleXp += value;
|
||||||
|
});
|
||||||
|
trackXps.forEach((value) => {
|
||||||
|
moduleXp += value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
research: tank.xp,
|
||||||
|
upgrades: moduleXp,
|
||||||
|
purchase: tank.price.value,
|
||||||
|
equipment: equipmentPriceMatrix[tank.tier].reduce(
|
||||||
|
(a, b) => a + 3 * b,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const image = (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${t`bot.commands.research.body.subtitle`} • ${targetTank.name}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: theme.colors.appBackground2,
|
||||||
|
border: theme.borderStyles.subtle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: 48 }}></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
right: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t`bot.commands.research.body.total`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 8,
|
||||||
|
padding: '16px 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={await webpToPng(asset('icons/currencies/xp.webp'))}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Experience:{' '}
|
||||||
|
{costs
|
||||||
|
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
|
||||||
|
.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={await webpToPng(asset('icons/currencies/silver.webp'))}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Purchase:{' '}
|
||||||
|
{costs
|
||||||
|
.reduce((a, b) => a + b.purchase, 0)
|
||||||
|
.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={await webpToPng(asset('icons/currencies/silver.webp'))}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Equipment:{' '}
|
||||||
|
{costs
|
||||||
|
.reduce((a, b) => a + b.equipment, 0)
|
||||||
|
.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{await Promise.all(
|
||||||
|
line.map(async (id, index) => {
|
||||||
|
const tank = awaitedTankDefinitions[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: theme.colors.appBackground2,
|
||||||
|
border: theme.borderStyles.subtle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 48,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{TIER_ROMAN_NUMERALS[tank.tier]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
position: 'relative',
|
||||||
|
right: '5%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 120,
|
||||||
|
width: 240,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
left: '25%',
|
||||||
|
top: '5%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={await tankIconPng(id)}
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 8,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={TANK_ICONS[tank.class]}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tank.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 8,
|
||||||
|
padding: '16px 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={await webpToPng(
|
||||||
|
asset('icons/currencies/xp.webp'),
|
||||||
|
)}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate('bot.commands.research.body.research', [
|
||||||
|
`${costs[index].research?.toLocaleString(
|
||||||
|
interaction.locale,
|
||||||
|
)}`,
|
||||||
|
])}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{costs[index].upgrades > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={await webpToPng(
|
||||||
|
asset('icons/currencies/free-xp.webp'),
|
||||||
|
)}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate('bot.commands.research.body.upgrades', [
|
||||||
|
costs[index].upgrades.toLocaleString(),
|
||||||
|
])}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={await webpToPng(
|
||||||
|
asset('icons/currencies/silver.webp'),
|
||||||
|
)}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate('bot.commands.research.body.purchase', [
|
||||||
|
costs[index].purchase.toLocaleString(
|
||||||
|
interaction.locale,
|
||||||
|
),
|
||||||
|
])}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={await webpToPng(
|
||||||
|
asset('icons/currencies/silver.webp'),
|
||||||
|
)}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate('bot.commands.research.body.equipment', [
|
||||||
|
costs[index].equipment.toLocaleString(
|
||||||
|
interaction.locale,
|
||||||
|
),
|
||||||
|
])}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
return startingTankRaw
|
||||||
|
? image
|
||||||
|
: [t`bot.commands.research.body.estimation`, image];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete(interaction) {
|
||||||
|
autocompleteTanks(interaction, true, ['target-tank', 'starting-tank']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ import NoData from '../components/NoData';
|
|||||||
import TitleBar from '../components/TitleBar';
|
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 { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
import { emblemURL } from '../core/blitzkrieg/emblemURL';
|
||||||
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
@@ -38,7 +38,7 @@ async function render(
|
|||||||
) {
|
) {
|
||||||
const { nickname } = await getAccountInfo(region, id);
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
const diffedTankStats = await getStatsInPeriod(
|
const diffedTankStats = await getStatsInPeriod(
|
||||||
region,
|
region,
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export interface GlowProps extends ComponentProps<'div'> {
|
||||||
|
color: string;
|
||||||
|
direction?: 'default' | 'reverse';
|
||||||
|
rotation?: number;
|
||||||
|
startOpacity?: number;
|
||||||
|
endOpacity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = 16;
|
||||||
|
|
||||||
|
export function Glow({
|
||||||
|
color,
|
||||||
|
direction = 'default',
|
||||||
|
rotation = 0,
|
||||||
|
style,
|
||||||
|
startOpacity = 0,
|
||||||
|
endOpacity = 1,
|
||||||
|
...props
|
||||||
|
}: GlowProps) {
|
||||||
|
const color1 = `${color}${Math.round(endOpacity * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, '0')}`;
|
||||||
|
const color2 = `${color}${Math.round(startOpacity * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, '0')}`;
|
||||||
|
const background = `linear-gradient(${
|
||||||
|
direction === 'default' ? 0 : 180
|
||||||
|
}deg, ${color1} 0%, ${color2} 100%)`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: direction === 'default' ? 'flex-end' : 'flex-start',
|
||||||
|
justifyContent: 'center',
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div style={{ height: 54, width, background }} />
|
||||||
|
<div style={{ height: 72, width, background }} />
|
||||||
|
<div style={{ height: 80, width, background }} />
|
||||||
|
<div style={{ height: 72, width, background }} />
|
||||||
|
<div style={{ height: 54, width, background }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+32
-19
@@ -1,5 +1,6 @@
|
|||||||
import { Locale } from 'discord.js';
|
import { Locale } from 'discord.js';
|
||||||
import { Percentile } from '../../../../constants/percentiles';
|
import { Percentile } from '../../../../constants/percentiles';
|
||||||
|
import { SupplementaryStats } from '../../../../core/blitz/getAccountInfo';
|
||||||
import { translator } from '../../../../core/localization/translator';
|
import { translator } from '../../../../core/localization/translator';
|
||||||
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
|
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
|
||||||
import { theme } from '../../../../stitches.config';
|
import { theme } from '../../../../stitches.config';
|
||||||
@@ -7,16 +8,19 @@ import { PERCENTILE_COLORS } from '../../../PercentileIndicator/constants';
|
|||||||
import { Glow } from './components/Glow';
|
import { Glow } from './components/Glow';
|
||||||
|
|
||||||
export interface WN8DisplayProps {
|
export interface WN8DisplayProps {
|
||||||
wn8?: number;
|
stats: SupplementaryStats;
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WN8Display({ wn8, locale }: WN8DisplayProps) {
|
export function HeroStat({ stats, locale }: WN8DisplayProps) {
|
||||||
const percentile =
|
|
||||||
wn8 === undefined ? Percentile.VeryBad : getWN8Percentile(wn8);
|
|
||||||
const color = PERCENTILE_COLORS[percentile];
|
|
||||||
const { translate } = translator(locale);
|
const { translate } = translator(locale);
|
||||||
|
|
||||||
|
const percentile =
|
||||||
|
stats.WN8 === undefined ? Percentile.VeryBad : getWN8Percentile(stats.WN8);
|
||||||
|
const color = PERCENTILE_COLORS[percentile];
|
||||||
|
const heroStat = stats.WN8 === undefined ? '--' : stats.WN8.toFixed(0);
|
||||||
|
const subtitle = translate(`common.wn8_percentile.${percentile}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -43,23 +47,32 @@ export function WN8Display({ wn8, locale }: WN8DisplayProps) {
|
|||||||
gap: -4,
|
gap: -4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 900,
|
display: 'flex',
|
||||||
fontSize: 32,
|
flexDirection: 'column',
|
||||||
color: theme.colors.textHighContrast,
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{wn8 === undefined ? '--' : wn8.toFixed(0)}
|
<span
|
||||||
</span>
|
style={{
|
||||||
<span
|
fontWeight: 900,
|
||||||
style={{
|
fontSize: 32,
|
||||||
fontSize: 16,
|
color: theme.colors.textHighContrast,
|
||||||
color: theme.colors.textHighContrast,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{heroStat}
|
||||||
{translate(`common.wn8_percentile.${percentile}`)}
|
</span>
|
||||||
</span>
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Glow color={color} direction="reverse" />
|
<Glow color={color} direction="reverse" />
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
export interface GlowProps {
|
|
||||||
color: string;
|
|
||||||
direction?: 'default' | 'reverse';
|
|
||||||
rotation?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Glow({
|
|
||||||
color,
|
|
||||||
direction = 'default',
|
|
||||||
rotation = 0,
|
|
||||||
}: GlowProps) {
|
|
||||||
const color1 = color;
|
|
||||||
const color2 = `${color}00`;
|
|
||||||
|
|
||||||
const background = `linear-gradient(${
|
|
||||||
direction === 'default' ? 0 : 180
|
|
||||||
}deg, ${color1} 0%, ${color2} 100%)`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: direction === 'default' ? 'flex-end' : 'flex-start',
|
|
||||||
justifyContent: 'center',
|
|
||||||
transform: `rotate(${rotation}deg)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ height: 54, width: 16, background }} />
|
|
||||||
<div style={{ height: 72, width: 16, background }} />
|
|
||||||
<div style={{ height: 80, width: 16, background }} />
|
|
||||||
<div style={{ height: 72, width: 16, background }} />
|
|
||||||
<div style={{ height: 54, width: 16, background }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
import { Locale } from 'discord.js';
|
import { Locale } from 'discord.js';
|
||||||
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
import { SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
||||||
import { translator } from '../../core/localization/translator';
|
import { translator } from '../../core/localization/translator';
|
||||||
import { WN8Display } from './components/WN8Display';
|
import { HeroStat } from './components/HeroStat';
|
||||||
import {
|
import {
|
||||||
WN8SurroundingStat,
|
WN8SurroundingStat,
|
||||||
WN8SurroundingStatAlign,
|
WN8SurroundingStatAlign,
|
||||||
} from './components/WN8SurroundingStat';
|
} from './components/WN8SurroundingStat';
|
||||||
|
|
||||||
export interface AllStatsOverviewProps {
|
export interface AllStatsOverviewProps {
|
||||||
stats: AllStats;
|
stats: {
|
||||||
|
battles: number;
|
||||||
|
wins: number;
|
||||||
|
damage_dealt: number;
|
||||||
|
damage_received: number;
|
||||||
|
survived_battles: number;
|
||||||
|
hits: number;
|
||||||
|
shots: number;
|
||||||
|
frags: number;
|
||||||
|
};
|
||||||
supplementaryStats: SupplementaryStats;
|
supplementaryStats: SupplementaryStats;
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
}
|
}
|
||||||
@@ -24,6 +33,7 @@ export default function AllStatsOverview({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
gap: -64,
|
gap: -64,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
@@ -38,18 +48,18 @@ export default function AllStatsOverview({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label={t`bot.commands.stats.body.battles`}
|
label={t`bot.common.hero_stats.battles`}
|
||||||
value={stats.battles.toLocaleString()}
|
value={stats.battles.toLocaleString()}
|
||||||
align={WN8SurroundingStatAlign.Right}
|
align={WN8SurroundingStatAlign.Right}
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label={t`bot.commands.stats.body.winrate`}
|
label={t`bot.common.hero_stats.winrate`}
|
||||||
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
|
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
|
||||||
align={WN8SurroundingStatAlign.Right}
|
align={WN8SurroundingStatAlign.Right}
|
||||||
padded
|
padded
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label={t`bot.commands.stats.body.damage`}
|
label={t`bot.common.hero_stats.damage`}
|
||||||
value={Math.round(
|
value={Math.round(
|
||||||
stats.damage_dealt / stats.battles,
|
stats.damage_dealt / stats.battles,
|
||||||
).toLocaleString()}
|
).toLocaleString()}
|
||||||
@@ -57,13 +67,13 @@ export default function AllStatsOverview({
|
|||||||
padded
|
padded
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label={t`bot.commands.stats.body.damage_ratio`}
|
label={t`bot.common.hero_stats.damage_ratio`}
|
||||||
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
|
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
|
||||||
align={WN8SurroundingStatAlign.Right}
|
align={WN8SurroundingStatAlign.Right}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WN8Display locale={locale} wn8={supplementaryStats.WN8} />
|
<HeroStat locale={locale} stats={supplementaryStats} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -74,29 +84,26 @@ export default function AllStatsOverview({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label={t`bot.commands.stats.body.tier`}
|
label={t`bot.common.hero_stats.tier`}
|
||||||
value={
|
value={
|
||||||
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
|
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label={t`bot.commands.stats.body.survival`}
|
label={t`bot.common.hero_stats.survival`}
|
||||||
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
|
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
|
||||||
2,
|
2,
|
||||||
)}%`}
|
)}%`}
|
||||||
padded
|
padded
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label={t`bot.commands.stats.body.accuracy`}
|
label={t`bot.common.hero_stats.accuracy`}
|
||||||
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
|
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
|
||||||
padded
|
padded
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label={t`bot.commands.stats.body.kill_ratio`}
|
label={t`bot.common.hero_stats.kills`}
|
||||||
value={(
|
value={(stats.frags / stats.battles).toFixed(2)}
|
||||||
stats.frags /
|
|
||||||
(stats.battles - stats.survived_battles)
|
|
||||||
).toFixed(2)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export function Analytics() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
async
|
||||||
|
src="https://www.googletagmanager.com/gtag/js?id=G-98NTMRHRP9"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-98NTMRHRP9');
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -48,7 +48,9 @@ export function CoreArmorSceneComponent({
|
|||||||
explosionRadius: { value: null },
|
explosionRadius: { value: null },
|
||||||
greenPenetration: { value: null },
|
greenPenetration: { value: null },
|
||||||
opaque: { value: null },
|
opaque: { value: null },
|
||||||
|
useSpacedArmor: { value: null },
|
||||||
|
|
||||||
|
inverseProjectionMatrix: { value: null },
|
||||||
resolution: { value: new Vector2() },
|
resolution: { value: new Vector2() },
|
||||||
spacedArmorBuffer: { value: null },
|
spacedArmorBuffer: { value: null },
|
||||||
spacedArmorDepth: { value: null },
|
spacedArmorDepth: { value: null },
|
||||||
@@ -56,10 +58,7 @@ export function CoreArmorSceneComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange(shell: ShellDefinition) {
|
async function handleShellChange(shell: ShellDefinition) {
|
||||||
material.uniforms.penetration.value = resolveNearPenetration(
|
|
||||||
shell.penetration,
|
|
||||||
);
|
|
||||||
material.uniforms.caliber.value = shell.caliber;
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
material.uniforms.ricochet.value = degToRad(shell.ricochet ?? 90);
|
material.uniforms.ricochet.value = degToRad(shell.ricochet ?? 90);
|
||||||
material.uniforms.normalization.value = degToRad(
|
material.uniforms.normalization.value = degToRad(
|
||||||
@@ -69,12 +68,17 @@ export function CoreArmorSceneComponent({
|
|||||||
material.uniforms.canSplash.value = canSplash(shell.type);
|
material.uniforms.canSplash.value = canSplash(shell.type);
|
||||||
material.uniforms.damage.value = shell.damage.armor;
|
material.uniforms.damage.value = shell.damage.armor;
|
||||||
material.uniforms.explosionRadius.value = shell.explosionRadius;
|
material.uniforms.explosionRadius.value = shell.explosionRadius;
|
||||||
|
|
||||||
|
const duel = useDuel.getState();
|
||||||
|
await handleProtagonistEquipmentChange(duel.protagonist!.equipment);
|
||||||
|
await handleAntagonistEquipmentChange(duel.antagonist!.equipment);
|
||||||
}
|
}
|
||||||
function handleVisualChange(
|
function handleVisualChange(
|
||||||
visual: TankopediaPersistent['model']['visual'],
|
visual: TankopediaPersistent['model']['visual'],
|
||||||
) {
|
) {
|
||||||
material.uniforms.greenPenetration.value = visual.greenPenetration;
|
material.uniforms.greenPenetration.value = visual.greenPenetration;
|
||||||
material.uniforms.opaque.value = visual.opaque || visual.wireframe;
|
material.uniforms.opaque.value = visual.opaque || visual.wireframe;
|
||||||
|
material.uniforms.useSpacedArmor.value = visual.showSpacedArmor;
|
||||||
material.wireframe = visual.wireframe;
|
material.wireframe = visual.wireframe;
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
async function handleProtagonistEquipmentChange(
|
||||||
@@ -89,6 +93,7 @@ export function CoreArmorSceneComponent({
|
|||||||
const shell = useDuel.getState().antagonist!.shell;
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
const penetration = resolveNearPenetration(shell.penetration);
|
const penetration = resolveNearPenetration(shell.penetration);
|
||||||
const hasCalibratedShells = await hasEquipment(103, true, equipment);
|
const hasCalibratedShells = await hasEquipment(103, true, equipment);
|
||||||
|
|
||||||
material.uniforms.penetration.value = hasCalibratedShells
|
material.uniforms.penetration.value = hasCalibratedShells
|
||||||
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05)
|
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05)
|
||||||
: penetration;
|
: penetration;
|
||||||
@@ -120,13 +125,15 @@ export function CoreArmorSceneComponent({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useFrame(({ gl }) => {
|
useFrame(({ gl, camera }) => {
|
||||||
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||||
gl.getPixelRatio(),
|
gl.getPixelRatio(),
|
||||||
);
|
);
|
||||||
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
||||||
material.uniforms.spacedArmorDepth.value =
|
material.uniforms.spacedArmorDepth.value =
|
||||||
spacedArmorRenderTarget.depthTexture;
|
spacedArmorRenderTarget.depthTexture;
|
||||||
|
material.uniforms.inverseProjectionMatrix.value =
|
||||||
|
camera.projectionMatrixInverse;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
varying vec3 vViewPosition;
|
varying vec3 vViewPosition;
|
||||||
varying mat4 vProjectionMatrix;
|
|
||||||
|
|
||||||
uniform float thickness;
|
uniform float thickness;
|
||||||
uniform float penetration;
|
uniform float penetration;
|
||||||
@@ -14,13 +13,15 @@ uniform float damage;
|
|||||||
uniform float explosionRadius;
|
uniform float explosionRadius;
|
||||||
uniform bool greenPenetration;
|
uniform bool greenPenetration;
|
||||||
uniform bool opaque;
|
uniform bool opaque;
|
||||||
|
uniform bool useSpacedArmor;
|
||||||
|
uniform mat4 inverseProjectionMatrix;
|
||||||
|
|
||||||
uniform highp sampler2D spacedArmorBuffer;
|
uniform highp sampler2D spacedArmorBuffer;
|
||||||
uniform highp sampler2D spacedArmorDepth;
|
uniform highp sampler2D spacedArmorDepth;
|
||||||
|
|
||||||
float depthToDistance(float depth) {
|
float depthToDistance(float depth) {
|
||||||
vec4 clipPosition = vec4(gl_FragCoord.xy / resolution * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
|
vec4 clipPosition = vec4(gl_FragCoord.xy / resolution * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
|
||||||
vec4 eyePosition = inverse(vProjectionMatrix) * clipPosition;
|
vec4 eyePosition = inverseProjectionMatrix * clipPosition;
|
||||||
return length(eyePosition.xyz / eyePosition.w);
|
return length(eyePosition.xyz / eyePosition.w);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,36 +42,42 @@ void main() {
|
|||||||
bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0;
|
bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0;
|
||||||
float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization;
|
float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization;
|
||||||
float finalThickness = thickness / cos(max(0.0, angle - finalNormalization));
|
float finalThickness = thickness / cos(max(0.0, angle - finalNormalization));
|
||||||
float remainingPenetration = penetration;
|
|
||||||
|
|
||||||
// world space is in meters
|
// world space in meters
|
||||||
vec4 spacedArmorDepthFragment = texture2D(spacedArmorDepth, screenCoordinates);
|
vec4 spacedArmorDepthFragment = texture2D(spacedArmorDepth, screenCoordinates);
|
||||||
float spacedArmorDistance = depthToDistance(spacedArmorDepthFragment.r);
|
float spacedArmorDistance = depthToDistance(spacedArmorDepthFragment.r);
|
||||||
float coreArmorDistance = depthToDistance(gl_FragCoord.z);
|
float coreArmorDistance = depthToDistance(gl_FragCoord.z);
|
||||||
float distanceFromSpacedArmor = coreArmorDistance - spacedArmorDistance;
|
float distanceFromSpacedArmor = coreArmorDistance - spacedArmorDistance;
|
||||||
|
|
||||||
if (isUnderSpacedArmor) {
|
if (canSplash && useSpacedArmor && isUnderSpacedArmor) {
|
||||||
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
|
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
|
||||||
remainingPenetration -= spacedArmorThickness;
|
float finalDamage = 0.5 * damage * (1.0 - distanceFromSpacedArmor / explosionRadius) - 1.1 * (finalThickness + spacedArmorThickness);
|
||||||
|
|
||||||
if (isExplosive && remainingPenetration > 0.0) {
|
penetrationChance = 0.0;
|
||||||
// there is a 50% penetration loss per meter for HEAT shells
|
splashChance = finalDamage > 0.0 ? 1.0 : 0.0;
|
||||||
remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingPenetration = max(0.0, remainingPenetration);
|
|
||||||
float delta = finalThickness - remainingPenetration;
|
|
||||||
float randomization = remainingPenetration * 0.05;
|
|
||||||
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
|
|
||||||
|
|
||||||
if (canSplash) {
|
|
||||||
// only allow splashing if the damage equation deals more than 0 damage
|
|
||||||
float nominalArmorThickness = finalThickness - remainingPenetration;
|
|
||||||
float finalDamage = 0.5 * damage * (1.0 - distanceFromSpacedArmor / explosionRadius) - 1.1 * nominalArmorThickness;
|
|
||||||
splashChance = finalDamage > 0.0 ? 1.0 : 0.0; // TODO: add randomization here?
|
|
||||||
} else {
|
} else {
|
||||||
splashChance = 0.0;
|
float remainingPenetration = penetration;
|
||||||
|
|
||||||
|
if (useSpacedArmor && isUnderSpacedArmor) {
|
||||||
|
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
|
||||||
|
remainingPenetration -= spacedArmorThickness;
|
||||||
|
|
||||||
|
if (isExplosive && remainingPenetration > 0.0) {
|
||||||
|
remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingPenetration = max(0.0, remainingPenetration);
|
||||||
|
float delta = finalThickness - remainingPenetration;
|
||||||
|
float randomization = remainingPenetration * 0.05;
|
||||||
|
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
|
||||||
|
|
||||||
|
if (canSplash) {
|
||||||
|
float splashDamage = 0.5 * damage - 1.1 * finalThickness;
|
||||||
|
splashChance = splashDamage > 0.0 ? 1.0 : 0.0;
|
||||||
|
} else {
|
||||||
|
splashChance = 0.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
varying vec3 vViewPosition;
|
varying vec3 vViewPosition;
|
||||||
varying mat4 vProjectionMatrix;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0);
|
vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0);
|
||||||
|
|
||||||
vViewPosition = vViewPosition4.xyz;
|
vViewPosition = vViewPosition4.xyz;
|
||||||
vProjectionMatrix = projectionMatrix;
|
|
||||||
vNormal = normalMatrix * normal;
|
vNormal = normalMatrix * normal;
|
||||||
|
|
||||||
gl_Position = projectionMatrix * vViewPosition4;
|
gl_Position = projectionMatrix * vViewPosition4;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useFrame } from '@react-three/fiber';
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { Mesh } from 'three';
|
import { Mesh } from 'three';
|
||||||
import { radToDeg } from 'three/src/math/MathUtils';
|
import { radToDeg } from 'three/src/math/MathUtils';
|
||||||
|
import { canSplash } from '../../../core/blitz/canSplash';
|
||||||
import { isExplosive } from '../../../core/blitz/isExplosive';
|
import { isExplosive } from '../../../core/blitz/isExplosive';
|
||||||
import { NaNFallback } from '../../../core/math/NaNFallback';
|
import { NaNFallback } from '../../../core/math/NaNFallback';
|
||||||
import { normalToEuler } from '../../../core/math/normalToEuler';
|
import { normalToEuler } from '../../../core/math/normalToEuler';
|
||||||
@@ -42,6 +43,7 @@ export function ShotDisplay() {
|
|||||||
);
|
);
|
||||||
const { shell } = useDuel.getState().antagonist!;
|
const { shell } = useDuel.getState().antagonist!;
|
||||||
const explosive = isExplosive(shell.type);
|
const explosive = isExplosive(shell.type);
|
||||||
|
const splashing = canSplash(shell.type);
|
||||||
const midPointLayer = (shot?.layers.findLast(
|
const midPointLayer = (shot?.layers.findLast(
|
||||||
(layer) => layer.type !== null && layer.status === 'ricochet',
|
(layer) => layer.type !== null && layer.status === 'ricochet',
|
||||||
) ?? shot?.layers.findLast((layer) => layer.type !== null)) as ShotLayerBase;
|
) ?? shot?.layers.findLast((layer) => layer.type !== null)) as ShotLayerBase;
|
||||||
@@ -64,6 +66,18 @@ export function ShotDisplay() {
|
|||||||
return accumulator;
|
return accumulator;
|
||||||
}, 0) + LENGTH_INFINITY
|
}, 0) + LENGTH_INFINITY
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const preMidPointThickness = shot
|
||||||
|
? preMidPointLayers!.reduce(
|
||||||
|
(accumulator, layer) =>
|
||||||
|
accumulator +
|
||||||
|
(layer.type === null
|
||||||
|
? 0
|
||||||
|
: layer.type === ArmorType.External
|
||||||
|
? layer.thickness
|
||||||
|
: layer.thicknessAngled),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
const postMidPointGap = shot
|
const postMidPointGap = shot
|
||||||
? postMidPointLayers!.reduce((accumulator, layer) => {
|
? postMidPointLayers!.reduce((accumulator, layer) => {
|
||||||
if (layer.type === null) return accumulator + layer.distance;
|
if (layer.type === null) return accumulator + layer.distance;
|
||||||
@@ -246,8 +260,9 @@ export function ShotDisplay() {
|
|||||||
(1 -
|
(1 -
|
||||||
distanceFromSpacedArmor! / shell.explosionRadius!) -
|
distanceFromSpacedArmor! / shell.explosionRadius!) -
|
||||||
1.1 *
|
1.1 *
|
||||||
(midPointLayer as ShotLayerNonExternal)
|
((midPointLayer as ShotLayerNonExternal)
|
||||||
.thicknessAngled,
|
.thicknessAngled +
|
||||||
|
preMidPointThickness!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
0,
|
0,
|
||||||
@@ -255,7 +270,7 @@ export function ShotDisplay() {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{shot.layers.map((layer, index) => {
|
{shot.layers.map((layer, index) => {
|
||||||
if (layer.type === null && !explosive) return null;
|
if (layer.type === null && (!explosive || splashing)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex key={index} align="center" gap="2">
|
<Flex key={index} align="center" gap="2">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useDuel } from '../../../../stores/duel';
|
|||||||
import {
|
import {
|
||||||
Shot,
|
Shot,
|
||||||
ShotLayerBase,
|
ShotLayerBase,
|
||||||
|
useTankopediaPersistent,
|
||||||
useTankopediaTemporary,
|
useTankopediaTemporary,
|
||||||
} from '../../../../stores/tankopedia';
|
} from '../../../../stores/tankopedia';
|
||||||
import { ArmorType } from '../SpacedArmorScene';
|
import { ArmorType } from '../SpacedArmorScene';
|
||||||
@@ -115,12 +116,20 @@ export function SpacedArmorSceneComponent({
|
|||||||
pushLogs(shellNormal, filterIntersections(event.intersections), 0);
|
pushLogs(shellNormal, filterIntersections(event.intersections), 0);
|
||||||
|
|
||||||
function filterIntersections(intersectionsRaw: Intersection[]) {
|
function filterIntersections(intersectionsRaw: Intersection[]) {
|
||||||
const intersectionsAll = intersectionsRaw.filter(
|
const showSpacedArmor =
|
||||||
|
useTankopediaPersistent.getState().model.visual.showSpacedArmor;
|
||||||
|
const intersectionsTyped = intersectionsRaw.filter(
|
||||||
({ object }) => object.userData.type !== undefined,
|
({ object }) => object.userData.type !== undefined,
|
||||||
) as ArmorMeshIntersection[];
|
) as ArmorMeshIntersection[];
|
||||||
const intersectionsWithDuplicates = intersectionsAll.slice(
|
const intersectionsFiltered = intersectionsTyped.filter(
|
||||||
|
({ object }) =>
|
||||||
|
showSpacedArmor
|
||||||
|
? true
|
||||||
|
: object.userData.type === ArmorType.Core,
|
||||||
|
);
|
||||||
|
const intersectionsWithDuplicates = intersectionsFiltered.slice(
|
||||||
0,
|
0,
|
||||||
intersectionsAll.findIndex(
|
intersectionsFiltered.findIndex(
|
||||||
({ object }) => object.userData.type === ArmorType.Core,
|
({ object }) => object.userData.type === ArmorType.Core,
|
||||||
) + 1,
|
) + 1,
|
||||||
);
|
);
|
||||||
@@ -170,7 +179,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
const distance = (
|
const distance = (
|
||||||
shot.layers.at(-1) as ShotLayerBase
|
shot.layers.at(-1) as ShotLayerBase
|
||||||
).point.distanceTo(intersections[0].point);
|
).point.distanceTo(intersections[0].point);
|
||||||
if (explosive)
|
if (explosive && !splashing)
|
||||||
remainingPenetration -= 0.5 * remainingPenetration * distance;
|
remainingPenetration -= 0.5 * remainingPenetration * distance;
|
||||||
const blocked = remainingPenetration < 0;
|
const blocked = remainingPenetration < 0;
|
||||||
|
|
||||||
@@ -196,7 +205,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
const previousIntersection = intersections[layerIndex - 1];
|
const previousIntersection = intersections[layerIndex - 1];
|
||||||
const distance =
|
const distance =
|
||||||
intersection.distance - previousIntersection.distance;
|
intersection.distance - previousIntersection.distance;
|
||||||
if (explosive)
|
if (explosive && !splashing)
|
||||||
remainingPenetration -=
|
remainingPenetration -=
|
||||||
0.5 * remainingPenetration * distance;
|
0.5 * remainingPenetration * distance;
|
||||||
const wasted = remainingPenetration < 0;
|
const wasted = remainingPenetration < 0;
|
||||||
@@ -207,7 +216,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
distance,
|
distance,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (wasted) return;
|
if (wasted && !splashing) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intersection.object.userData.type === ArmorType.External) {
|
if (intersection.object.userData.type === ArmorType.External) {
|
||||||
@@ -224,7 +233,11 @@ export function SpacedArmorSceneComponent({
|
|||||||
status: blocked ? 'blocked' : 'penetration',
|
status: blocked ? 'blocked' : 'penetration',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blocked) break;
|
if (splashing) {
|
||||||
|
remainingPenetration = Math.max(0, remainingPenetration);
|
||||||
|
} else if (blocked) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const type = intersection.object.userData.type;
|
const type = intersection.object.userData.type;
|
||||||
const angle = surfaceNormal.angleTo(shellNormal);
|
const angle = surfaceNormal.angleTo(shellNormal);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export default function CommandWrapper({ children, fat }: CommandWrapperProps) {
|
|||||||
gap: 16,
|
gap: 16,
|
||||||
width: 480,
|
width: 480,
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
position: 'relative',
|
||||||
|
background: theme.colors.appBackground1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
interface DeltaCaretProps {
|
||||||
|
delta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeltaCaret({ delta }: DeltaCaretProps) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
|
src={
|
||||||
|
delta > 0
|
||||||
|
? 'https://i.imgur.com/gjI4a5f.png'
|
||||||
|
: 'https://i.imgur.com/lLGZygN.png'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ComponentProps } from 'react';
|
import { ComponentProps } from 'react';
|
||||||
import { RatingsReward } from '../../../commands/ratings';
|
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
interface ItemProps extends ComponentProps<'div'> {
|
interface ItemProps extends ComponentProps<'div'> {
|
||||||
@@ -7,22 +6,16 @@ interface ItemProps extends ComponentProps<'div'> {
|
|||||||
deltaPosition?: number;
|
deltaPosition?: number;
|
||||||
score?: number;
|
score?: number;
|
||||||
deltaScore?: number;
|
deltaScore?: number;
|
||||||
reward?: RatingsReward;
|
|
||||||
nickname: string;
|
nickname: string;
|
||||||
clan?: string;
|
clan?: string;
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeImage(url: string) {
|
|
||||||
return url.endsWith('.webp') ? 'https://i.imgur.com/ZniCzbO.png' : url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Item({
|
export function Item({
|
||||||
position,
|
position,
|
||||||
deltaPosition,
|
deltaPosition,
|
||||||
score,
|
score,
|
||||||
deltaScore,
|
deltaScore,
|
||||||
reward,
|
|
||||||
nickname,
|
nickname,
|
||||||
clan,
|
clan,
|
||||||
highlight,
|
highlight,
|
||||||
@@ -120,40 +113,8 @@ export function Item({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
{/* TODO: lol remove this spacer div for a better solution */}
|
||||||
style={{
|
<div style={{ flex: 1 }} />
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
gap: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{reward && (
|
|
||||||
<img
|
|
||||||
style={{ width: 16, height: 16, objectFit: 'cover' }}
|
|
||||||
src={normalizeImage(
|
|
||||||
reward.type === 'vehicle'
|
|
||||||
? reward.vehicle.image_url
|
|
||||||
: reward.stuff.image_url,
|
|
||||||
)}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{reward && reward.count > 1 && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: highlight
|
|
||||||
? theme.colors.textLowContrast_blue
|
|
||||||
: theme.colors.textLowContrast,
|
|
||||||
fontSize: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
x {reward.count.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -189,6 +150,7 @@ export function Item({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{score !== undefined && (
|
{score !== undefined && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { translator } from '../../core/localization/translator';
|
|||||||
import { theme } from '../../stitches.config';
|
import { theme } from '../../stitches.config';
|
||||||
|
|
||||||
interface NoDataProps {
|
interface NoDataProps {
|
||||||
type: 'battles_in_period' | 'players_in_period' | 'tanks_found';
|
type:
|
||||||
|
| 'battles_in_period'
|
||||||
|
| 'players_in_period'
|
||||||
|
| 'tanks_found'
|
||||||
|
| 'battles_in_season';
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function TitleBar({ title, image, description }: TitleBarProps) {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
padding: '8px 0px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Vector3Tuple } from 'three';
|
||||||
|
import { Tier } from '../core/blitzkrieg/tankDefinitions';
|
||||||
|
|
||||||
|
export const equipmentPriceMatrix: Record<Tier, Vector3Tuple> = {
|
||||||
|
1: [100, 200, 300],
|
||||||
|
2: [200, 300, 400],
|
||||||
|
3: [400, 500, 600],
|
||||||
|
4: [4000, 5000, 6000],
|
||||||
|
5: [40_000, 50_000, 60_000],
|
||||||
|
6: [100_000, 120_000, 140_000],
|
||||||
|
7: [150_000, 170_000, 190_000],
|
||||||
|
8: [200_000, 225_000, 250_000],
|
||||||
|
9: [250_000, 275_000, 300_000],
|
||||||
|
10: [300_000, 350_000, 400_000],
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export const LEAGUES = [
|
export const LEAGUES = [
|
||||||
{ name: 'diamond', minScore: 5000 },
|
{ name: 'diamond', minScore: 5000, index: 0 },
|
||||||
{ name: 'platinum', minScore: 4000 },
|
{ name: 'platinum', minScore: 4000, index: 1 },
|
||||||
{ name: 'gold', minScore: 3000 },
|
{ name: 'gold', minScore: 3000, index: 2 },
|
||||||
{ name: 'silver', minScore: 2000 },
|
{ name: 'silver', minScore: 2000, index: 3 },
|
||||||
{ name: 'bronze', minScore: 0 },
|
{ name: 'bronze', minScore: -Infinity, index: 4 },
|
||||||
];
|
];
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const FIRST_MINIMAL_ARCHIVED_RATING_SEASON = 49;
|
||||||
|
export const FIRST_COMPREHENSIVE1_ARCHIVED_RATING_SEASON = 53;
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const FIRST_ARCHIVED_RATINGS_SEASON = 49;
|
|
||||||
@@ -18,9 +18,9 @@ export const TOOLS: Tool[] = [
|
|||||||
description: 'Blitz tank encyclopedia',
|
description: 'Blitz tank encyclopedia',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ratings',
|
id: 'rating',
|
||||||
title: 'Ratings',
|
title: 'Rating leaderboard',
|
||||||
description: 'Ratings gamemode leaderboards',
|
description: 'Full rating gamemode leaderboard',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'discord',
|
id: 'discord',
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
material.setBaseColorTexture(
|
material.setBaseColorTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/png')
|
.setMimeType('image/jpeg')
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readTexture(
|
||||||
`${data}/3d/${dirname(path)}/${textures.albedo}`,
|
`${data}/3d/${dirname(path)}/${textures.albedo}`,
|
||||||
@@ -70,7 +70,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
material.setMetallicRoughnessTexture(
|
material.setMetallicRoughnessTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/png')
|
.setMimeType('image/jpeg')
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readTexture(
|
||||||
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`,
|
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`,
|
||||||
@@ -86,7 +86,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
material.setNormalTexture(
|
material.setNormalTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/png')
|
.setMimeType('image/jpeg')
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readTexture(
|
||||||
`${data}/3d/${dirname(path)}/${
|
`${data}/3d/${dirname(path)}/${
|
||||||
@@ -102,7 +102,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
material.setOcclusionTexture(
|
material.setOcclusionTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/png')
|
.setMimeType('image/jpeg')
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readTexture(
|
||||||
`${data}/3d/${dirname(path)}/${textures.miscMap}`,
|
`${data}/3d/${dirname(path)}/${textures.miscMap}`,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { patientFetch } from '../blitzkrieg/patientFetch';
|
||||||
|
|
||||||
type BlitzResponse<Data extends object> =
|
type BlitzResponse<Data extends object> =
|
||||||
| {
|
| {
|
||||||
status: 'error';
|
status: 'error';
|
||||||
@@ -23,7 +25,7 @@ async function manageQueue() {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const { url, resolve } = queue.shift()!;
|
const { url, resolve } = queue.shift()!;
|
||||||
const data = (await fetch(url)
|
const data = (await patientFetch(url)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.catch((error) => ({ status: 'error', error }))) as BlitzResponse<
|
.catch((error) => ({ status: 'error', error }))) as BlitzResponse<
|
||||||
typeof resolve
|
typeof resolve
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export interface AllStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SupplementaryStats {
|
export interface SupplementaryStats {
|
||||||
// if undefined, WN8 is not available for tanks without averages
|
|
||||||
WN8?: number;
|
WN8?: number;
|
||||||
tier?: number;
|
tier?: number;
|
||||||
}
|
}
|
||||||
@@ -51,6 +50,28 @@ export interface ClanStats {
|
|||||||
dropped_capture_points: number;
|
dropped_capture_points: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RatingStats {
|
||||||
|
spotted: number;
|
||||||
|
calibration_battles_left: number;
|
||||||
|
hits: number;
|
||||||
|
frags: number;
|
||||||
|
recalibration_start_time: number;
|
||||||
|
mm_rating: number;
|
||||||
|
wins: number;
|
||||||
|
losses: number;
|
||||||
|
is_recalibration: boolean;
|
||||||
|
capture_points: number;
|
||||||
|
battles: number;
|
||||||
|
damage_dealt: number;
|
||||||
|
damage_received: number;
|
||||||
|
shots: number;
|
||||||
|
frags8p: number;
|
||||||
|
xp: number;
|
||||||
|
win_and_survived: number;
|
||||||
|
survived_battles: number;
|
||||||
|
dropped_capture_points: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface IndividualAccountInfo {
|
interface IndividualAccountInfo {
|
||||||
account_id: number;
|
account_id: number;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
@@ -61,6 +82,7 @@ interface IndividualAccountInfo {
|
|||||||
statistics: {
|
statistics: {
|
||||||
all: AllStats;
|
all: AllStats;
|
||||||
clan: ClanStats;
|
clan: ClanStats;
|
||||||
|
rating?: RatingStats;
|
||||||
frags: null | number;
|
frags: null | number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -74,11 +96,11 @@ export async function getAccountInfo<
|
|||||||
ReturnType = Ids extends number
|
ReturnType = Ids extends number
|
||||||
? IndividualAccountInfo
|
? IndividualAccountInfo
|
||||||
: IndividualAccountInfo[],
|
: IndividualAccountInfo[],
|
||||||
>(region: Region, ids: Ids) {
|
>(region: Region, ids: Ids, extra: string[] = []) {
|
||||||
const object = await fetchBlitz<AccountInfo>(
|
const object = await fetchBlitz<AccountInfo>(
|
||||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
|
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
|
||||||
ids,
|
ids,
|
||||||
)}`,
|
)}${extra.length === 0 ? '' : `&extra=${extra.join(',')}`}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typeof ids === 'number') {
|
if (typeof ids === 'number') {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { LEAGUES } from '../../constants/leagues';
|
||||||
|
|
||||||
|
export function getLeagueFromScore(score: number) {
|
||||||
|
return LEAGUES.find(({ minScore }) => score >= minScore)!;
|
||||||
|
}
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
import { RatingsInfo } from '../../commands/ratings';
|
import { RatingInfo } from '../../commands/ratingLeaderboard';
|
||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { context } from '../blitzkrieg/context';
|
import { context } from '../blitzkrieg/context';
|
||||||
import { patientFetch } from '../blitzkrieg/patientFetch';
|
import { patientFetch } from '../blitzkrieg/patientFetch';
|
||||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||||
|
|
||||||
const ratingsInfoCache: Partial<Record<Region, RatingsInfo>> = {};
|
const cache: Partial<Record<Region, RatingInfo>> = {};
|
||||||
|
|
||||||
export default async function getRatingsInfo(region: Region) {
|
export default async function getRatingInfo(region: Region) {
|
||||||
if (ratingsInfoCache[region]) return ratingsInfoCache[region]!;
|
if (cache[region]) return cache[region]!;
|
||||||
|
|
||||||
const response = await patientFetch(
|
const response = await patientFetch(
|
||||||
context === 'website'
|
context === 'website'
|
||||||
? `/api/${region}/ratings/current/info`
|
? `/api/${region}/rating/current/info`
|
||||||
: `https://${regionToRegionSubdomain(
|
: `https://${regionToRegionSubdomain(
|
||||||
region,
|
region,
|
||||||
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
undefined,
|
undefined,
|
||||||
{ cache: 'no-store' },
|
{ cache: 'no-store' },
|
||||||
);
|
);
|
||||||
const data = (await response.json()) as RatingsInfo;
|
const data = (await response.json()) as RatingInfo;
|
||||||
|
|
||||||
ratingsInfoCache[region] = data;
|
cache[region] = data;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { LeagueTop, RatingsPlayer } from '../../commands/ratings';
|
import { LeagueTop, RatingPlayer } from '../../commands/ratingLeaderboard';
|
||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { context } from '../blitzkrieg/context';
|
import { context } from '../blitzkrieg/context';
|
||||||
import { patientFetch } from '../blitzkrieg/patientFetch';
|
import { patientFetch } from '../blitzkrieg/patientFetch';
|
||||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||||
|
|
||||||
export async function getRatingsLeague(
|
export async function getRatingLeague(
|
||||||
region: Region,
|
region: Region,
|
||||||
league: number,
|
league: number,
|
||||||
usePatientFetcher = false,
|
usePatientFetcher = false,
|
||||||
@@ -15,7 +15,7 @@ export async function getRatingsLeague(
|
|||||||
: fetch(url, { cache: 'no-store' });
|
: fetch(url, { cache: 'no-store' });
|
||||||
const response = await fetcher(
|
const response = await fetcher(
|
||||||
context === 'website'
|
context === 'website'
|
||||||
? `/api/${region}/ratings/current/league/${league}`
|
? `/api/${region}/rating/current/league/${league}`
|
||||||
: `https://${regionToRegionSubdomain(
|
: `https://${regionToRegionSubdomain(
|
||||||
region,
|
region,
|
||||||
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`,
|
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`,
|
||||||
@@ -23,6 +23,6 @@ export async function getRatingsLeague(
|
|||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
||||||
return context === 'website'
|
return context === 'website'
|
||||||
? (json as RatingsPlayer[])
|
? (json as RatingPlayer[])
|
||||||
: (json as LeagueTop).result;
|
: (json as LeagueTop).result;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { RatingsNeighbors } from '../../commands/ratings';
|
import { RatingNeighbors } from '../../commands/ratingLeaderboard';
|
||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { patientFetch } from '../blitzkrieg/patientFetch';
|
import { patientFetch } from '../blitzkrieg/patientFetch';
|
||||||
import { withCORSProxy } from '../blitzkrieg/withCORSProxy';
|
import { withCORSProxy } from '../blitzkrieg/withCORSProxy';
|
||||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||||
|
|
||||||
export async function getRatingsNeighbors(
|
export async function getRatingNeighbors(
|
||||||
region: Region,
|
region: Region,
|
||||||
id: number,
|
id: number,
|
||||||
count: number,
|
count: number,
|
||||||
@@ -21,7 +21,7 @@ export async function getRatingsNeighbors(
|
|||||||
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`,
|
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const json = (await response.json()) as RatingsNeighbors;
|
const json = (await response.json()) as RatingNeighbors;
|
||||||
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import getRatingInfo from './getRatingInfo';
|
||||||
|
|
||||||
|
export async function isOnGoingRatingSeason() {
|
||||||
|
const ratingInfo = await getRatingInfo('com');
|
||||||
|
return ratingInfo.detail === undefined;
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import getRatingsInfo from './getRatingsInfo';
|
|
||||||
|
|
||||||
export async function isOnGoingRatingsSeason() {
|
|
||||||
const ratingsInfo = await getRatingsInfo('com');
|
|
||||||
return ratingsInfo.detail === undefined;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function normalizeLeagueIcon(url: string) {
|
||||||
|
return url.startsWith('http') ? url : `https:${url}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Region, RegionSubdomain } from '../../constants/regions';
|
||||||
|
|
||||||
|
export default function regionSubdomainToRegion(
|
||||||
|
regionDomain: RegionSubdomain,
|
||||||
|
): Region {
|
||||||
|
return regionDomain === 'na' ? 'com' : regionDomain;
|
||||||
|
}
|
||||||
@@ -2,24 +2,27 @@ import { Locale } from 'discord.js';
|
|||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { UserError } from '../../hooks/userError';
|
import { UserError } from '../../hooks/userError';
|
||||||
import {
|
import { tankDefinitions, tankNames } from '../blitzkrieg/tankDefinitions';
|
||||||
tankDefinitions,
|
import { tankNamesTechTreeOnly } from '../discord/autocompleteTanks';
|
||||||
tanksDefinitionsArray,
|
|
||||||
} from '../blitzkrieg/tankDefinitions';
|
|
||||||
import { translator } from '../localization/translator';
|
import { translator } from '../localization/translator';
|
||||||
|
|
||||||
export default async function resolveTankId(
|
export default async function resolveTankId(
|
||||||
tank: string | number,
|
tank: string | number,
|
||||||
locale: Locale,
|
locale: Locale,
|
||||||
|
techTreeOnly = false,
|
||||||
) {
|
) {
|
||||||
const { translate } = translator(locale);
|
const { translate } = translator(locale);
|
||||||
const number = typeof tank === 'string' ? Number(tank) : tank;
|
const number = typeof tank === 'string' ? Number(tank) : tank;
|
||||||
|
|
||||||
if (Number.isNaN(number)) {
|
if (Number.isNaN(number)) {
|
||||||
const searchResult = go(`${tank}`, await tanksDefinitionsArray, {
|
const searchResult = go(
|
||||||
keys: ['name'],
|
`${tank}`,
|
||||||
limit: 1,
|
await (techTreeOnly ? tankNamesTechTreeOnly : tankNames),
|
||||||
});
|
{
|
||||||
|
keys: ['combined'],
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (searchResult.length === 0) {
|
if (searchResult.length === 0) {
|
||||||
throw new UserError(
|
throw new UserError(
|
||||||
|
|||||||
+4
-7
@@ -2,7 +2,7 @@ import { Region } from '../../constants/regions';
|
|||||||
import { context } from '../blitzkrieg/context';
|
import { context } from '../blitzkrieg/context';
|
||||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||||
|
|
||||||
type CurrentRatingsPlayerSearch = Record<
|
type CurrentRatingPlayerSearch = Record<
|
||||||
number,
|
number,
|
||||||
{
|
{
|
||||||
spa_id: number;
|
spa_id: number;
|
||||||
@@ -24,20 +24,17 @@ type CurrentRatingsPlayerSearch = Record<
|
|||||||
)
|
)
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export async function searchCurrentRatingsPlayers(
|
export async function searchCurrentRatingPlayers(region: Region, name: string) {
|
||||||
region: Region,
|
|
||||||
name: string,
|
|
||||||
) {
|
|
||||||
const encodedSearch = encodeURIComponent(name);
|
const encodedSearch = encodeURIComponent(name);
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
context === 'website'
|
context === 'website'
|
||||||
? `/api/${region}/ratings/current/search/${encodedSearch}`
|
? `/api/${region}/rating/current/search/${encodedSearch}`
|
||||||
: `https://${regionToRegionSubdomain(
|
: `https://${regionToRegionSubdomain(
|
||||||
region,
|
region,
|
||||||
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
|
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
|
||||||
{ cache: 'no-store' },
|
{ cache: 'no-store' },
|
||||||
);
|
);
|
||||||
const accountList = (await response.json()) as CurrentRatingsPlayerSearch;
|
const accountList = (await response.json()) as CurrentRatingPlayerSearch;
|
||||||
|
|
||||||
return accountList;
|
return accountList;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { resolveAncestry } from './resolveAncestry';
|
||||||
|
import { tankDefinitions } from './tankDefinitions';
|
||||||
|
|
||||||
|
export async function buildTechTreeLine(start: number, end: number) {
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const endTank = awaitedTankDefinitions[end];
|
||||||
|
if (!endTank.ancestors) throw new Error('End tank has no ancestors');
|
||||||
|
const line = [end];
|
||||||
|
|
||||||
|
if (start === end) return line;
|
||||||
|
for (const ancestorId of endTank.ancestors) {
|
||||||
|
if ((await resolveAncestry(ancestorId)).includes(start)) {
|
||||||
|
const ancestorTree = await buildTechTreeLine(start, ancestorId);
|
||||||
|
line.push(...ancestorTree);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return line;
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export function emblemIdToURL(emblemId: number) {
|
export function emblemURL(emblemId: number) {
|
||||||
return `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${emblemId}.png`;
|
return `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${emblemId}.png`;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import getRatingsInfo from '../blitz/getRatingsInfo';
|
import getRatingInfo from '../blitz/getRatingInfo';
|
||||||
|
|
||||||
interface GitHubTrees {
|
interface GitHubTrees {
|
||||||
tree: { path: string; url: string }[];
|
tree: { path: string; url: string }[];
|
||||||
@@ -11,9 +11,9 @@ export async function getArchivedLatestSeasonNumber() {
|
|||||||
return cachedLatestArchivedSeasonNumber;
|
return cachedLatestArchivedSeasonNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ratingsInfo = await getRatingsInfo('com');
|
const ratingInfo = await getRatingInfo('com');
|
||||||
|
|
||||||
if (ratingsInfo.detail) {
|
if (ratingInfo.detail) {
|
||||||
const regionsURL = (
|
const regionsURL = (
|
||||||
(await fetch(
|
(await fetch(
|
||||||
'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main',
|
'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main',
|
||||||
@@ -24,14 +24,14 @@ export async function getArchivedLatestSeasonNumber() {
|
|||||||
response.json(),
|
response.json(),
|
||||||
)) as GitHubTrees
|
)) as GitHubTrees
|
||||||
).tree.find(({ path }) => path === 'com')!.url;
|
).tree.find(({ path }) => path === 'com')!.url;
|
||||||
const ratingsURL = (
|
const ratingURL = (
|
||||||
(await fetch(comURL).then((response) => response.json())) as {
|
(await fetch(comURL).then((response) => response.json())) as {
|
||||||
tree: [{ url: string }];
|
tree: [{ url: string }];
|
||||||
}
|
}
|
||||||
).tree[0].url;
|
).tree[0].url;
|
||||||
const latestSeason = Math.max(
|
const latestSeason = Math.max(
|
||||||
...(
|
...(
|
||||||
(await fetch(ratingsURL).then((response) => response.json())) as {
|
(await fetch(ratingURL).then((response) => response.json())) as {
|
||||||
tree: { path: string }[];
|
tree: { path: string }[];
|
||||||
}
|
}
|
||||||
).tree.map(({ path }) => Number(path)),
|
).tree.map(({ path }) => Number(path)),
|
||||||
@@ -39,7 +39,7 @@ export async function getArchivedLatestSeasonNumber() {
|
|||||||
|
|
||||||
cachedLatestArchivedSeasonNumber = latestSeason;
|
cachedLatestArchivedSeasonNumber = latestSeason;
|
||||||
} else {
|
} else {
|
||||||
cachedLatestArchivedSeasonNumber = ratingsInfo.current_season - 1;
|
cachedLatestArchivedSeasonNumber = ratingInfo.current_season - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cachedLatestArchivedSeasonNumber;
|
return cachedLatestArchivedSeasonNumber;
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { RatingInfo } from '../../commands/ratingLeaderboard';
|
||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { asset } from './asset';
|
||||||
|
import { superDecompress } from './superDecompress';
|
||||||
|
|
||||||
|
const cache: Record<
|
||||||
|
Region,
|
||||||
|
Record<
|
||||||
|
number,
|
||||||
|
RatingInfo & {
|
||||||
|
detail: undefined;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
> = {
|
||||||
|
com: {},
|
||||||
|
eu: {},
|
||||||
|
asia: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function getArchivedRatingInfo(
|
||||||
|
region: Region,
|
||||||
|
season: number,
|
||||||
|
) {
|
||||||
|
if (!cache[region][season]) {
|
||||||
|
const info = await fetch(
|
||||||
|
asset(`regions/${region}/rating/${season}/info.cdon.lz4`),
|
||||||
|
)
|
||||||
|
.then((response) => response.arrayBuffer())
|
||||||
|
.then((buffer) =>
|
||||||
|
superDecompress<RatingInfo & { detail: undefined }>(
|
||||||
|
new Uint8Array(buffer),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
cache[region][season] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache[region][season];
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { BkrlDiscriminatedEntries, BkrlReadStream } from '../streams/bkrl';
|
||||||
|
import { asset } from './asset';
|
||||||
|
|
||||||
|
const cache: Record<Region, Record<number, BkrlDiscriminatedEntries>> = {
|
||||||
|
asia: {},
|
||||||
|
eu: {},
|
||||||
|
com: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getArchivedRatingLeaderboard(
|
||||||
|
region: Region,
|
||||||
|
season: number,
|
||||||
|
) {
|
||||||
|
if (!cache[region][season]) {
|
||||||
|
const info = await fetch(
|
||||||
|
asset(`regions/${region}/rating/${season}/latest.bkrl`),
|
||||||
|
)
|
||||||
|
.then((response) => response.arrayBuffer())
|
||||||
|
.then((buffer) => new BkrlReadStream(buffer).bkrl());
|
||||||
|
cache[region][season] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache[region][season];
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { BkrlDiscriminatedEntries, BkrlReadStream } from '../streams/bkrl';
|
||||||
|
import { asset } from './asset';
|
||||||
|
|
||||||
|
const cache: Record<Region, Record<number, BkrlDiscriminatedEntries | null>> = {
|
||||||
|
com: {},
|
||||||
|
eu: {},
|
||||||
|
asia: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function getArchivedRatingMidnightLeaderboard(
|
||||||
|
region: Region,
|
||||||
|
season: number,
|
||||||
|
) {
|
||||||
|
if (!cache[region][season]) {
|
||||||
|
const info = await fetch(
|
||||||
|
asset(`regions/${region}/rating/${season}/midnight.bkrl`),
|
||||||
|
)
|
||||||
|
.then((response) =>
|
||||||
|
response.status === 404 ? null : response.arrayBuffer(),
|
||||||
|
)
|
||||||
|
.then((buffer) =>
|
||||||
|
buffer === null ? null : new BkrlReadStream(buffer).bkrl(),
|
||||||
|
);
|
||||||
|
cache[region][season] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache[region][season];
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { RatingsInfo } from '../../commands/ratings';
|
|
||||||
import { Region } from '../../constants/regions';
|
|
||||||
|
|
||||||
const INFO_CACHE: Record<
|
|
||||||
Region,
|
|
||||||
Record<
|
|
||||||
number,
|
|
||||||
RatingsInfo & {
|
|
||||||
detail: undefined;
|
|
||||||
}
|
|
||||||
>
|
|
||||||
> = {
|
|
||||||
com: {},
|
|
||||||
eu: {},
|
|
||||||
asia: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function getArchivedRatingsInfo(
|
|
||||||
region: Region,
|
|
||||||
season: number,
|
|
||||||
) {
|
|
||||||
if (INFO_CACHE[region][season]) {
|
|
||||||
return INFO_CACHE[region][season];
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`https://raw.githubusercontent.com/tresabhi/blitzkrieg-assets/main/regions/${region}/ratings/${season}/info.json`,
|
|
||||||
);
|
|
||||||
const jsonContent = (await response.json()) as RatingsInfo & {
|
|
||||||
detail: undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
INFO_CACHE[region][season] = jsonContent;
|
|
||||||
|
|
||||||
return jsonContent;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
|
|
||||||
import { Region } from '../../constants/regions';
|
|
||||||
|
|
||||||
const LEADERBOARD_CACHE: Record<
|
|
||||||
Region,
|
|
||||||
Record<number, BlitzkriegRatingsLeaderboard>
|
|
||||||
> = {
|
|
||||||
asia: {},
|
|
||||||
eu: {},
|
|
||||||
com: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getArchivedRatingsLeaderboard(
|
|
||||||
region: Region,
|
|
||||||
season: number,
|
|
||||||
) {
|
|
||||||
if (LEADERBOARD_CACHE[region][season]) {
|
|
||||||
return LEADERBOARD_CACHE[region][season];
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`https://raw.githubusercontent.com/tresabhi/blitzkrieg-assets/main/regions/${region}/ratings/${season}/latest.json`,
|
|
||||||
);
|
|
||||||
const json = (await response.json()) as BlitzkriegRatingsLeaderboard;
|
|
||||||
LEADERBOARD_CACHE[region][season] = json;
|
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
|
|
||||||
import { Region } from '../../constants/regions';
|
|
||||||
|
|
||||||
const MIDNIGHT_LEADERBOARD_CACHE: Record<
|
|
||||||
Region,
|
|
||||||
Record<number, BlitzkriegRatingsLeaderboard>
|
|
||||||
> = {
|
|
||||||
com: {},
|
|
||||||
eu: {},
|
|
||||||
asia: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function getArchivedRatingsMidnightLeaderboard(
|
|
||||||
region: Region,
|
|
||||||
season: number,
|
|
||||||
) {
|
|
||||||
if (MIDNIGHT_LEADERBOARD_CACHE[region][season]) {
|
|
||||||
return MIDNIGHT_LEADERBOARD_CACHE[region][season];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://raw.githubusercontent.com/tresabhi/blitzkrieg-assets/main/regions/${region}/ratings/${season}/midnight.json`,
|
|
||||||
);
|
|
||||||
const json = (await response.json()) as BlitzkriegRatingsLeaderboard;
|
|
||||||
MIDNIGHT_LEADERBOARD_CACHE[region][season] = json;
|
|
||||||
|
|
||||||
return json;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`No midnight leaderboard found for season ${season}, region ${region}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
const cache: Record<string, Buffer> = {};
|
||||||
|
|
||||||
|
export async function webpToPng(url: string) {
|
||||||
|
if (!url.endsWith('.webp')) return url;
|
||||||
|
|
||||||
|
if (!cache[url]) {
|
||||||
|
const imageResponse = await fetch(url);
|
||||||
|
const imageBufferWEBP = await imageResponse.arrayBuffer();
|
||||||
|
const imageBufferPNG = await sharp(imageBufferWEBP).png().toBuffer();
|
||||||
|
cache[url] = imageBufferPNG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `data:image/png;base64,${cache[url].toString('base64')}`;
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ export async function readTexture(path: string, mutation?: TextureMutation) {
|
|||||||
raw.data[index + 3] = 255;
|
raw.data[index + 3] = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await sharp(raw.data, { raw }).png().toBuffer();
|
return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
case TextureMutation.RoughnessMetallicness: {
|
case TextureMutation.RoughnessMetallicness: {
|
||||||
@@ -45,16 +45,16 @@ export async function readTexture(path: string, mutation?: TextureMutation) {
|
|||||||
});
|
});
|
||||||
const metallicness = sharp(raw.data, { raw })
|
const metallicness = sharp(raw.data, { raw })
|
||||||
.extractChannel('green')
|
.extractChannel('green')
|
||||||
.png()
|
.jpeg()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
const roughness = sharp(raw.data, { raw })
|
const roughness = sharp(raw.data, { raw })
|
||||||
.extractChannel('alpha')
|
.extractChannel('alpha')
|
||||||
.png()
|
.jpeg()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
return await newImage
|
return await newImage
|
||||||
.joinChannel(await Promise.all([roughness, metallicness]))
|
.joinChannel(await Promise.all([roughness, metallicness]))
|
||||||
.png()
|
.jpeg()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,15 +65,15 @@ export async function readTexture(path: string, mutation?: TextureMutation) {
|
|||||||
*/
|
*/
|
||||||
return await sharp(raw.data, { raw })
|
return await sharp(raw.data, { raw })
|
||||||
.extractChannel('alpha')
|
.extractChannel('alpha')
|
||||||
.png()
|
.jpeg()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
case TextureMutation.Albedo: {
|
case TextureMutation.Albedo: {
|
||||||
return await sharp(raw.data, { raw }).removeAlpha().png().toBuffer();
|
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return await sharp(raw.data, { raw }).png().toBuffer();
|
return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { tankDefinitions } from './tankDefinitions';
|
||||||
|
|
||||||
|
export async function resolveAncestry(id: number) {
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const tank = awaitedTankDefinitions[id];
|
||||||
|
const ancestors: number[] = [];
|
||||||
|
|
||||||
|
if (tank.ancestors) {
|
||||||
|
for (const ancestor of tank.ancestors) {
|
||||||
|
if (!ancestors.includes(ancestor)) {
|
||||||
|
ancestors.push(ancestor);
|
||||||
|
|
||||||
|
for (const ancestorLayer2 of await resolveAncestry(ancestor)) {
|
||||||
|
if (!ancestors.includes(ancestorLayer2)) {
|
||||||
|
ancestors.push(ancestorLayer2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ancestors.sort(
|
||||||
|
(a, b) => awaitedTankDefinitions[b].tier - awaitedTankDefinitions[a].tier,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ export interface Crew {
|
|||||||
export interface TankDefinition {
|
export interface TankDefinition {
|
||||||
id: number;
|
id: number;
|
||||||
fixedCamouflage?: boolean;
|
fixedCamouflage?: boolean;
|
||||||
|
ancestors?: number[];
|
||||||
|
successors?: number[];
|
||||||
crew: Crew[];
|
crew: Crew[];
|
||||||
health: number;
|
health: number;
|
||||||
nation: string;
|
nation: string;
|
||||||
@@ -37,6 +39,7 @@ export interface TankDefinition {
|
|||||||
engines: EngineDefinition[];
|
engines: EngineDefinition[];
|
||||||
tracks: TrackDefinition[];
|
tracks: TrackDefinition[];
|
||||||
price: TankDefinitionPrice;
|
price: TankDefinitionPrice;
|
||||||
|
xp?: number;
|
||||||
speed: {
|
speed: {
|
||||||
forwards: number;
|
forwards: number;
|
||||||
backwards: number;
|
backwards: number;
|
||||||
@@ -54,6 +57,7 @@ export interface TrackDefinition {
|
|||||||
tier: Tier;
|
tier: Tier;
|
||||||
weight: number;
|
weight: number;
|
||||||
traverseSpeed: number;
|
traverseSpeed: number;
|
||||||
|
xp?: number;
|
||||||
dispersion: {
|
dispersion: {
|
||||||
move: number;
|
move: number;
|
||||||
traverse: number;
|
traverse: number;
|
||||||
@@ -67,6 +71,7 @@ export interface TrackDefinition {
|
|||||||
export interface EngineDefinition {
|
export interface EngineDefinition {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
xp?: number;
|
||||||
tier: Tier;
|
tier: Tier;
|
||||||
fireChance: number;
|
fireChance: number;
|
||||||
power: number;
|
power: number;
|
||||||
@@ -79,6 +84,7 @@ export interface TurretDefinition {
|
|||||||
health: number;
|
health: number;
|
||||||
viewRange: number;
|
viewRange: number;
|
||||||
traverseSpeed: number;
|
traverseSpeed: number;
|
||||||
|
xp?: number;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
tier: Tier;
|
tier: Tier;
|
||||||
@@ -91,6 +97,7 @@ export type GunDefinition =
|
|||||||
| GunDefinitionAutoReloader;
|
| GunDefinitionAutoReloader;
|
||||||
interface GunDefinitionBase {
|
interface GunDefinitionBase {
|
||||||
rotationSpeed: number;
|
rotationSpeed: number;
|
||||||
|
xp?: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -139,6 +146,10 @@ export const tankDefinitions = fetchCdonLz4<TankDefinitions>(
|
|||||||
asset('definitions/tanks.cdon.lz4'),
|
asset('definitions/tanks.cdon.lz4'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// export const tankDefinitions = readFile('test.tanks.cdon.lz4').then((data) =>
|
||||||
|
// superDecompress<TankDefinitions>(data.buffer),
|
||||||
|
// );
|
||||||
|
|
||||||
const entries = new Promise<TankDefinition[]>(async (resolve) => {
|
const entries = new Promise<TankDefinition[]>(async (resolve) => {
|
||||||
resolve(Object.entries(await tankDefinitions).map(([, entry]) => entry));
|
resolve(Object.entries(await tankDefinitions).map(([, entry]) => entry));
|
||||||
});
|
});
|
||||||
@@ -156,6 +167,7 @@ export const tankNames = tanksDefinitionsArray.then((tanks) =>
|
|||||||
id,
|
id,
|
||||||
original: tank.name,
|
original: tank.name,
|
||||||
combined: `${tank.name}${deburr(tank.name)}${tank.nameFull ? `${tank.nameFull}${deburr(tank.nameFull)}` : ''}`,
|
combined: `${tank.name}${deburr(tank.name)}${tank.nameFull ? `${tank.nameFull}${deburr(tank.nameFull)}` : ''}`,
|
||||||
|
treeType: tank.treeType,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -173,3 +185,15 @@ export const NATIONS = tanksDefinitionsArray.then((tanks) => {
|
|||||||
|
|
||||||
return nations;
|
return nations;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const flags: Record<string, string> = {
|
||||||
|
ussr: '<:ussr:1218421042033197197>',
|
||||||
|
germany: '🇩🇪',
|
||||||
|
usa: '🇺🇸',
|
||||||
|
china: '🇨🇳',
|
||||||
|
uk: '🇬🇧',
|
||||||
|
france: '🇫🇷',
|
||||||
|
japan: '🇯🇵',
|
||||||
|
european: '🇪🇺',
|
||||||
|
other: '<:other:1218421572243558482>',
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { webpToPng } from './iconPng';
|
||||||
|
import { tankIcon } from './tankIcon';
|
||||||
|
|
||||||
|
export function tankIconPng(id: number) {
|
||||||
|
return webpToPng(tankIcon(id));
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ export default async function getStatsInPeriod(
|
|||||||
return get(latest.all) - get(previous.all);
|
return get(latest.all) - get(previous.all);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if there was a change in battles as games in ratings do update last_battle_time
|
// check if there was a change in battles as games in rating do update last_battle_time
|
||||||
if (d((a) => a.battles) > 0) {
|
if (d((a) => a.battles) > 0) {
|
||||||
diff[tankId] = {
|
diff[tankId] = {
|
||||||
battles: d((a) => a.battles),
|
battles: d((a) => a.battles),
|
||||||
|
|||||||
@@ -10,19 +10,29 @@ import {
|
|||||||
OVERFLOW_SUFFIX,
|
OVERFLOW_SUFFIX,
|
||||||
} from './autocompleteClan/constants';
|
} from './autocompleteClan/constants';
|
||||||
|
|
||||||
|
export const tankNamesTechTreeOnly = tankNames.then((names) =>
|
||||||
|
names.filter((tank) => tank.treeType === 'researchable'),
|
||||||
|
);
|
||||||
|
|
||||||
export default async function autocompleteTanks(
|
export default async function autocompleteTanks(
|
||||||
interaction: AutocompleteInteraction<CacheType>,
|
interaction: AutocompleteInteraction<CacheType>,
|
||||||
|
techTreeOnly = false,
|
||||||
|
fields = ['tank'],
|
||||||
) {
|
) {
|
||||||
const focusedOption = interaction.options.getFocused(true);
|
const focusedOption = interaction.options.getFocused(true);
|
||||||
if (focusedOption.name !== 'tank') return;
|
if (!fields.includes(focusedOption.name)) return;
|
||||||
|
|
||||||
await interaction.respond(
|
await interaction.respond(
|
||||||
focusedOption.value
|
focusedOption.value
|
||||||
? await Promise.all(
|
? await Promise.all(
|
||||||
go(focusedOption.value, await tankNames, {
|
go(
|
||||||
keys: ['combined'],
|
focusedOption.value,
|
||||||
limit: 10,
|
await (techTreeOnly ? tankNamesTechTreeOnly : tankNames),
|
||||||
}).map(async (item) => {
|
{
|
||||||
|
keys: ['combined'],
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
).map(async (item) => {
|
||||||
let name = item.obj.original;
|
let name = item.obj.original;
|
||||||
|
|
||||||
if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) {
|
if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
InteractionEditReplyOptions,
|
InteractionEditReplyOptions,
|
||||||
|
InteractionReplyOptions,
|
||||||
|
MessageEditOptions,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { InteractionReturnable } from '../../events/interactionCreate';
|
import { InteractionReturnable } from '../../events/interactionCreate';
|
||||||
import jsxToPngThreaded from '../blitzkrieg/jsxToPngThreaded';
|
import jsxToPngThreaded from '../blitzkrieg/jsxToPngThreaded';
|
||||||
@@ -12,7 +14,9 @@ export default async function normalizeInteractionReturnable(
|
|||||||
returnable: InteractionReturnable,
|
returnable: InteractionReturnable,
|
||||||
) {
|
) {
|
||||||
const images: [number, Buffer][] = [];
|
const images: [number, Buffer][] = [];
|
||||||
const reply: InteractionEditReplyOptions = {};
|
const reply: InteractionEditReplyOptions &
|
||||||
|
InteractionReplyOptions &
|
||||||
|
MessageEditOptions = {};
|
||||||
|
|
||||||
const awaitedReturnable = await returnable;
|
const awaitedReturnable = await returnable;
|
||||||
const normalizedReturnable = Array.isArray(awaitedReturnable)
|
const normalizedReturnable = Array.isArray(awaitedReturnable)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { RatingPlayer } from '../../commands/ratingLeaderboard';
|
||||||
|
import { RatingStats } from '../blitz/getAccountInfo';
|
||||||
|
import { BkrlSuperset1Entry } from '../streams/bkrl';
|
||||||
|
|
||||||
|
export function deltaBkrlBlitzStats(
|
||||||
|
a: BkrlSuperset1Entry,
|
||||||
|
b1: RatingStats,
|
||||||
|
b2: RatingPlayer,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
battles: b1.battles - a.battles,
|
||||||
|
damage: b1.damage_dealt - a.damage,
|
||||||
|
kills: b1.frags - a.kills,
|
||||||
|
survived: b1.survived_battles - a.survived,
|
||||||
|
wins: b1.wins - a.wins,
|
||||||
|
score: b2.score - a.score,
|
||||||
|
} satisfies Omit<BkrlSuperset1Entry, 'id'>;
|
||||||
|
}
|
||||||
@@ -5,60 +5,55 @@ import {
|
|||||||
} from '../../types/tanksStats';
|
} from '../../types/tanksStats';
|
||||||
|
|
||||||
export function deltaTankStats(
|
export function deltaTankStats(
|
||||||
a: NormalizedTankStats,
|
aRaw: NormalizedTankStats,
|
||||||
b: NormalizedTankStats,
|
bRaw: NormalizedTankStats,
|
||||||
time?: number,
|
time?: number,
|
||||||
) {
|
) {
|
||||||
const bTanks = Object.keys(b);
|
const bTanks = Object.keys(bRaw);
|
||||||
const changedTanks = bTanks.filter((id) => {
|
const changedTanks = bTanks.filter((id) => {
|
||||||
return (
|
return (
|
||||||
a[id as unknown as number]?.all.battles !==
|
aRaw[id as unknown as number]?.all.battles !==
|
||||||
b[id as unknown as number].all.battles &&
|
bRaw[id as unknown as number].all.battles &&
|
||||||
(time === undefined
|
(time === undefined
|
||||||
? true
|
? true
|
||||||
: b[id as unknown as number].last_battle_time > time)
|
: bRaw[id as unknown as number].last_battle_time > time)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const diff = changedTanks.map((id) => {
|
const diff = changedTanks.map((id) => {
|
||||||
const aTank = a[id as unknown as number] ?? emptyIndividualTankStats;
|
const a = aRaw[id as unknown as number] ?? emptyIndividualTankStats;
|
||||||
const bTank = b[id as unknown as number];
|
const b = bRaw[id as unknown as number];
|
||||||
|
|
||||||
const tankDiff = {
|
const tankDiff = {
|
||||||
account_id: bTank.account_id,
|
account_id: b.account_id,
|
||||||
battle_life_time: bTank.battle_life_time,
|
battle_life_time: b.battle_life_time,
|
||||||
frags: (bTank.frags ?? aTank.frags ?? 0) - (aTank.frags ?? 0),
|
frags: (b.frags ?? a.frags ?? 0) - (a.frags ?? 0),
|
||||||
max_frags: Math.max(aTank.max_frags, bTank.max_frags),
|
max_frags: Math.max(a.max_frags, b.max_frags),
|
||||||
in_garage: bTank.in_garage,
|
in_garage: b.in_garage,
|
||||||
max_xp: Math.max(aTank.max_xp, bTank.max_xp),
|
max_xp: Math.max(a.max_xp, b.max_xp),
|
||||||
in_garage_updated: bTank.in_garage_updated,
|
in_garage_updated: b.in_garage_updated,
|
||||||
last_battle_time: Math.max(
|
last_battle_time: Math.max(b.last_battle_time, a.last_battle_time),
|
||||||
bTank.last_battle_time,
|
mark_of_mastery: b.mark_of_mastery,
|
||||||
aTank.last_battle_time,
|
tank_id: b.tank_id,
|
||||||
),
|
|
||||||
mark_of_mastery: bTank.mark_of_mastery,
|
|
||||||
tank_id: bTank.tank_id,
|
|
||||||
|
|
||||||
all: {
|
all: {
|
||||||
battles: bTank.all.battles - aTank.all.battles,
|
battles: b.all.battles - a.all.battles,
|
||||||
wins: bTank.all.wins - aTank.all.wins,
|
wins: b.all.wins - a.all.wins,
|
||||||
capture_points: bTank.all.capture_points - aTank.all.capture_points,
|
capture_points: b.all.capture_points - a.all.capture_points,
|
||||||
damage_dealt: bTank.all.damage_dealt - aTank.all.damage_dealt,
|
damage_dealt: b.all.damage_dealt - a.all.damage_dealt,
|
||||||
damage_received: bTank.all.damage_received - aTank.all.damage_received,
|
damage_received: b.all.damage_received - a.all.damage_received,
|
||||||
dropped_capture_points:
|
dropped_capture_points:
|
||||||
bTank.all.dropped_capture_points - aTank.all.dropped_capture_points,
|
b.all.dropped_capture_points - a.all.dropped_capture_points,
|
||||||
frags: bTank.all.frags - aTank.all.frags,
|
frags: b.all.frags - a.all.frags,
|
||||||
frags8p: bTank.all.frags8p - aTank.all.frags8p,
|
frags8p: b.all.frags8p - a.all.frags8p,
|
||||||
hits: bTank.all.hits - aTank.all.hits,
|
hits: b.all.hits - a.all.hits,
|
||||||
losses: bTank.all.losses - aTank.all.losses,
|
losses: b.all.losses - a.all.losses,
|
||||||
max_frags: Math.max(aTank.max_frags, bTank.max_frags),
|
max_frags: Math.max(a.max_frags, b.max_frags),
|
||||||
max_xp: Math.max(aTank.max_xp, bTank.max_xp),
|
max_xp: Math.max(a.max_xp, b.max_xp),
|
||||||
shots: bTank.all.shots - aTank.all.shots,
|
shots: b.all.shots - a.all.shots,
|
||||||
spotted: bTank.all.spotted - aTank.all.spotted,
|
spotted: b.all.spotted - a.all.spotted,
|
||||||
survived_battles:
|
survived_battles: b.all.survived_battles - a.all.survived_battles,
|
||||||
bTank.all.survived_battles - aTank.all.survived_battles,
|
win_and_survived: b.all.win_and_survived - a.all.win_and_survived,
|
||||||
win_and_survived:
|
xp: b.all.xp - a.all.xp,
|
||||||
bTank.all.win_and_survived - aTank.all.win_and_survived,
|
|
||||||
xp: bTank.all.xp - aTank.all.xp,
|
|
||||||
},
|
},
|
||||||
} satisfies IndividualTankStats;
|
} satisfies IndividualTankStats;
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user