mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8a5f57787 | ||
|
|
13651650ab | ||
|
|
9644692084 | ||
|
|
b52c3c6c68 | ||
|
|
c1bd91a7f9 | ||
|
|
1d65ff3e0c | ||
|
|
f7fac5ad26 | ||
|
|
e45f055493 | ||
|
|
8946f115c0 | ||
|
|
6094875ffb | ||
|
|
0644c9a67f | ||
|
|
c91ab29a23 | ||
|
|
8f4e33fd5a | ||
|
|
8cf5012602 | ||
|
|
23a23e36bb | ||
|
|
0480445bae | ||
|
|
ae07b8de4c | ||
|
|
4c746cbb86 | ||
|
|
890a4d142b | ||
|
|
ff9bda29cf | ||
|
|
5ca9e29f3d | ||
|
|
c804a3d564 | ||
|
|
daa549e650 | ||
|
|
fe562915cb | ||
|
|
fff42b61e1 | ||
|
|
90ee71c9ca | ||
|
|
3f264d6a49 | ||
|
|
61eab0bca8 | ||
|
|
0a19157dbb | ||
|
|
634bd2ea07 | ||
|
|
f8f88ff8f2 | ||
|
|
8a2dc24f7e | ||
|
|
dcc3ab6e4e | ||
|
|
dabeab8612 | ||
|
|
66dbc5b2ed | ||
|
|
2e2ab9b2d9 | ||
|
|
662e0bb9ce | ||
|
|
3f40e94235 | ||
|
|
483b625f2d | ||
|
|
d18cf625d0 | ||
|
|
dc01eff812 | ||
|
|
5471c7fa32 | ||
|
|
83e28b4dac | ||
|
|
af95897bde | ||
|
|
4c2f6bea25 | ||
|
|
fdf77a43cb | ||
|
|
f1855a569a | ||
|
|
a0434b6ed2 | ||
|
|
365c714213 | ||
|
|
875abf4d32 | ||
|
|
6b9bf76da2 | ||
|
|
610c75d274 | ||
|
|
81826d0549 | ||
|
|
8caf4069dc | ||
|
|
6c496e616d | ||
|
|
3d5dd54ec0 | ||
|
|
e7bfe81940 | ||
|
|
fb9c04c779 | ||
|
|
84a1af8100 | ||
|
|
b804e60bac | ||
|
|
604e751bf4 | ||
|
|
2ce083222e | ||
|
|
9a44b1fcfb | ||
|
|
9df47777d0 | ||
|
|
aa78bc4045 | ||
|
|
4bfe34ba53 | ||
|
|
ad0fc69596 | ||
|
|
cfe71e15a4 | ||
|
|
bf7a319770 | ||
|
|
cadc84b32f | ||
|
|
7bde92c7f1 | ||
|
|
965d36cdc5 | ||
|
|
f8ef0063e4 | ||
|
|
fb4e00afc4 | ||
|
|
6ed9a191b9 | ||
|
|
b503f4fa8f | ||
|
|
7020e5641f | ||
|
|
5b4601b671 | ||
|
|
3e952faad3 | ||
|
|
c51abc1c64 | ||
|
|
c7e2780d17 | ||
|
|
28e2139683 | ||
|
|
20c839394a | ||
|
|
c5e1a7d6ab | ||
|
|
579d7570af | ||
|
|
1d1d751d43 | ||
|
|
86664b5491 | ||
|
|
b7e9898163 | ||
|
|
d1fb5da002 | ||
|
|
17935fbe89 | ||
|
|
48578407db | ||
|
|
bda0c4ef1b | ||
|
|
7ea5e444b3 | ||
|
|
bef362935a | ||
|
|
f3144be382 | ||
|
|
818ee1d447 | ||
|
|
67e45ada85 | ||
|
|
bdc7132337 | ||
|
|
c5214f8d89 | ||
|
|
6910f3c95e | ||
|
|
e23f898c5f | ||
|
|
3cf81afc4a | ||
|
|
ca463a0f0e | ||
|
|
a76572ebe3 | ||
|
|
5bf83da726 | ||
|
|
393da234e4 |
No files matched your search
@@ -2,7 +2,7 @@ name: CD Host
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */6 * * *'
|
- cron: '0 */5 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||||
Vendored
+24
@@ -1,40 +1,61 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"AABBOX",
|
||||||
"Abhi",
|
"Abhi",
|
||||||
"accountinfo",
|
"accountinfo",
|
||||||
|
"alphatest",
|
||||||
|
"apcr",
|
||||||
|
"atgm",
|
||||||
"bepaint",
|
"bepaint",
|
||||||
|
"BINORMAL",
|
||||||
"blitzstars",
|
"blitzstars",
|
||||||
"cleandist",
|
"cleandist",
|
||||||
"cockroackdb",
|
"cockroackdb",
|
||||||
"corsproxy",
|
"corsproxy",
|
||||||
|
"CUBETEXCOORD",
|
||||||
"customperiod",
|
"customperiod",
|
||||||
"deburr",
|
"deburr",
|
||||||
|
"decalmask",
|
||||||
"diacriticless",
|
"diacriticless",
|
||||||
|
"dvpl",
|
||||||
|
"DVPLCRC",
|
||||||
"Everytime",
|
"Everytime",
|
||||||
|
"FASTNAME",
|
||||||
"fullstats",
|
"fullstats",
|
||||||
"fuzzysort",
|
"fuzzysort",
|
||||||
|
"gamemode",
|
||||||
|
"haha",
|
||||||
"immer",
|
"immer",
|
||||||
|
"JOINTINDEX",
|
||||||
|
"JOINTWEIGHT",
|
||||||
"kranvagn",
|
"kranvagn",
|
||||||
"krieg",
|
"krieg",
|
||||||
"Leaderboard",
|
"Leaderboard",
|
||||||
"leaderboards",
|
"leaderboards",
|
||||||
"MURICA",
|
"MURICA",
|
||||||
|
"normalmap",
|
||||||
"Oopsy",
|
"Oopsy",
|
||||||
"outdir",
|
"outdir",
|
||||||
"outfile",
|
"outfile",
|
||||||
"ownedtanks",
|
"ownedtanks",
|
||||||
"playerachievements",
|
"playerachievements",
|
||||||
"playerinfo",
|
"playerinfo",
|
||||||
|
"Promisable",
|
||||||
"ratingsleaderboard",
|
"ratingsleaderboard",
|
||||||
|
"ratingsplot",
|
||||||
|
"researchable",
|
||||||
"Resvg",
|
"Resvg",
|
||||||
"roboto",
|
"roboto",
|
||||||
|
"SCPG",
|
||||||
"searchclans",
|
"searchclans",
|
||||||
"searchplayers",
|
"searchplayers",
|
||||||
"searchtanks",
|
"searchtanks",
|
||||||
"SKLLD",
|
"SKLLD",
|
||||||
"Smri",
|
"Smri",
|
||||||
|
"Specularity",
|
||||||
"startcase",
|
"startcase",
|
||||||
"statsfull",
|
"statsfull",
|
||||||
|
"steamapps",
|
||||||
"Stringifying",
|
"Stringifying",
|
||||||
"strvk",
|
"strvk",
|
||||||
"subnation",
|
"subnation",
|
||||||
@@ -42,6 +63,7 @@
|
|||||||
"tankopedia",
|
"tankopedia",
|
||||||
"tankopedias",
|
"tankopedias",
|
||||||
"tankstats",
|
"tankstats",
|
||||||
|
"TEXCOORD",
|
||||||
"tresabhi",
|
"tresabhi",
|
||||||
"Unicum",
|
"Unicum",
|
||||||
"Unregistering",
|
"Unregistering",
|
||||||
@@ -52,6 +74,8 @@
|
|||||||
"webp",
|
"webp",
|
||||||
"winrate",
|
"winrate",
|
||||||
"wotblitz",
|
"wotblitz",
|
||||||
|
"XMLDVPL",
|
||||||
|
"YAMLDVPL",
|
||||||
"zustand"
|
"zustand"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ Go to [application directory for Blitzkrieg](https://discord.com/application-dir
|
|||||||
|
|
||||||
### https://tresabhi.github.io/blitzkrieg/
|
### https://tresabhi.github.io/blitzkrieg/
|
||||||
|
|
||||||

|

|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Blitzkrieg 1.15.10
|
||||||
|
|
||||||
|
The 10th patch for the `1.15` update is here, breaking the record of the most patches for a single update!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added `/today`
|
||||||
|
- This is a direct alias for `/breakdown today`
|
||||||
|
- Drastically simplified ratings command
|
||||||
|
- `/ratings current` and `/ratings archive` have been now merged
|
||||||
|
- Where the data is fetched from is now decided directly from the chosen season number
|
||||||
|
- Simplified many error messages
|
||||||
|
- Title bar description text is now centered when there is no image
|
||||||
|
- It always felt awkward when there was no image on the left but the left still aligned to the right
|
||||||
|
- Removed `/link`'s requirement to select a username explicitly from the search results
|
||||||
|
- Full names for regions are now used in the `/link` command
|
||||||
|
- So "NA" turns into "North America"
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed contrast issues with WN8 percentile colors behind white text
|
||||||
|
- Archived ratings neighbors now functions once again
|
||||||
|
- Disabled tankopedia; this feature isn't finished yet and the changes were accidentally leaked
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Blitzkrieg 1.15.11
|
||||||
|
|
||||||
|
There's an easter egg somewhere in this update 🤔
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added `/permissions` which lists all permissions required to run the bot and checks if the bot has them or not
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Error messages now do no use embeds, instead utilize simple headings and paragraphs
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Blitzkrieg 1.15.12
|
||||||
|
|
||||||
|
Sometimes embeds need love too.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Session embeds are now translucent by default
|
||||||
|
- Session embed titles are now the username of the player by default
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.15.13
|
||||||
|
|
||||||
|
I have run out of things to say here. This patch fixes like 1 thing haha.
|
||||||
|
|
||||||
|
# Technical Fixes
|
||||||
|
|
||||||
|
- Tank deltas are now filtered by last battle time
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Blitzkrieg 1.15.9
|
||||||
|
|
||||||
|
~~Curiosity killed the cat~~ Ratings killed the Blitzkrieg
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Deleted ratings players are now handled properly
|
||||||
|
|
||||||
|
## Technical Fixes
|
||||||
|
|
||||||
|
- Latest archived season number
|
||||||
|
- Is now cached properly
|
||||||
|
- Is now fetched in a chaining fashion correctly
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Permissions
|
|
||||||
|
|
||||||
The following Discord permissions are required by the bot to ensure full functionality:
|
|
||||||
|
|
||||||
- Manage nicknames
|
|
||||||
- Read messages / view channels
|
|
||||||
- Attach files
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
|
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
|
||||||
|
|
||||||
const withVanillaExtract = createVanillaExtractPlugin();
|
const withVanillaExtract = createVanillaExtractPlugin({ tests: /\.css$/ });
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"main": "server.js",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node --enable-source-maps server.cjs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+19
-6
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.15.8",
|
"version": "1.15.13",
|
||||||
"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",
|
||||||
@@ -9,7 +9,9 @@
|
|||||||
"build:website": "next build",
|
"build:website": "next build",
|
||||||
"build:docs": "vitepress build docs",
|
"build:docs": "vitepress build docs",
|
||||||
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||||
"build:tankopedia": "vite-node --script scripts/buildTankopedia",
|
"build:assets": "vite-node --script scripts/buildAssets",
|
||||||
|
"build:test": "vite-node --script scripts/buildTest",
|
||||||
|
"build:ratingsplot": "vite-node --script scripts/buildRatingsPlot",
|
||||||
"dev:bot": "yarn build:bot && 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",
|
||||||
@@ -27,11 +29,14 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@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-rc.4",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/themes": "^1.1.2",
|
"@radix-ui/themes": "^2.0.1",
|
||||||
|
"@react-three/drei": "^9.90.1",
|
||||||
|
"@react-three/fiber": "^8.15.12",
|
||||||
"@resvg/resvg-js": "^2.4.1",
|
"@resvg/resvg-js": "^2.4.1",
|
||||||
"@vanilla-extract/css": "^1.13.0",
|
"@vanilla-extract/css": "^1.13.0",
|
||||||
"bepaint": "^0.1.0-beta.3",
|
"bepaint": "^0.1.0-beta.3",
|
||||||
@@ -40,20 +45,23 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"esbuild": "^0.19.3",
|
"esbuild": "^0.19.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"false": "^0.0.4",
|
||||||
"fuzzysort": "^2.0.4",
|
"fuzzysort": "^2.0.4",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-escape": "^2.0.0",
|
"markdown-escape": "^2.0.0",
|
||||||
"next": "^13.5.2",
|
"next": "^14.0.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-fuzzysort": "^1.0.1",
|
"react-fuzzysort": "^1.0.1",
|
||||||
"satori": "^0.10.8",
|
"satori": "^0.10.8",
|
||||||
|
"sharp": "^0.33.0",
|
||||||
|
"three": "^0.159.0",
|
||||||
"vite-node": "^0.34.5",
|
"vite-node": "^0.34.5",
|
||||||
"vitepress": "^1.0.0-rc.20",
|
"vitepress": "^1.0.0-rc.20",
|
||||||
"vitepress-plugin-auto-sidebar": "^1.1.0",
|
"vitepress-plugin-auto-sidebar": "^1.1.0",
|
||||||
"xml-js": "^1.6.11",
|
"yaml": "^2.3.4",
|
||||||
"zustand": "^4.4.1"
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -63,12 +71,17 @@
|
|||||||
"@types/express": "^4.17.18",
|
"@types/express": "^4.17.18",
|
||||||
"@types/js-yaml": "^4.0.8",
|
"@types/js-yaml": "^4.0.8",
|
||||||
"@types/lodash": "^4.14.199",
|
"@types/lodash": "^4.14.199",
|
||||||
|
"@types/lz4": "^0.6.4",
|
||||||
"@types/markdown-escape": "^1.1.0",
|
"@types/markdown-escape": "^1.1.0",
|
||||||
"@types/node": "^20.6.5",
|
"@types/node": "^20.6.5",
|
||||||
"@types/react": "^18.2.22",
|
"@types/react": "^18.2.22",
|
||||||
|
"@types/three": "^0.159.0",
|
||||||
"@vanilla-extract/next-plugin": "^2.3.1",
|
"@vanilla-extract/next-plugin": "^2.3.1",
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"knip": "^2.27.1",
|
"crc": "^4.3.2",
|
||||||
|
"fast-xml-parser": "^4.3.2",
|
||||||
|
"knip": "^3.4.0",
|
||||||
|
"lz4": "^0.6.5",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"prisma": "^5.4.1",
|
"prisma": "^5.4.1",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
|
|||||||
@@ -0,0 +1,544 @@
|
|||||||
|
import { config } from 'dotenv';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { mkdir, readdir, rm } from 'fs/promises';
|
||||||
|
import { argv } from 'process';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { TankType } from '../src/components/Tanks';
|
||||||
|
import { NATION_IDS } from '../src/constants/nations';
|
||||||
|
import { SCPGStream } from '../src/core/blitz/SCPGStream';
|
||||||
|
import { readBase64DVPL } from '../src/core/blitz/readBase64DVPL';
|
||||||
|
import { readDVPLFile } from '../src/core/blitz/readDVPLFile';
|
||||||
|
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
|
||||||
|
import { readXMLDVPL } from '../src/core/blitz/readXMLDVPL';
|
||||||
|
import { readYAMLDVPL } from '../src/core/blitz/readYAMLDVPL';
|
||||||
|
import { toUniqueId } from '../src/core/blitz/toUniqueId';
|
||||||
|
import commitMultipleFiles, {
|
||||||
|
FileChange,
|
||||||
|
} from '../src/core/blitzkrieg/commitMultipleFiles';
|
||||||
|
import { BlitzkriegGunDefinitions } from '../src/core/blitzkrieg/definitions/guns';
|
||||||
|
import {
|
||||||
|
BlitzkriegShellDefinitions,
|
||||||
|
ShellType,
|
||||||
|
} from '../src/core/blitzkrieg/definitions/shells';
|
||||||
|
import {
|
||||||
|
BlitzkriegTankDefinitions,
|
||||||
|
Tier,
|
||||||
|
} from '../src/core/blitzkrieg/definitions/tanks';
|
||||||
|
import { BlitzkriegTurretDefinitions } from '../src/core/blitzkrieg/definitions/turrets';
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
|
// WARNING! MOST OF THESE TYPES ARE NOT EXHAUSTIVE!
|
||||||
|
|
||||||
|
interface VehicleDefinitionList {
|
||||||
|
[key: string]: {
|
||||||
|
id: number;
|
||||||
|
userString: string;
|
||||||
|
shortUserString?: string;
|
||||||
|
description: string;
|
||||||
|
price: number | { gold: ''; '#text': number };
|
||||||
|
sellPrice?: { gold: ''; '#text': number };
|
||||||
|
enrichmentPermanentCost: number;
|
||||||
|
notInShop?: boolean;
|
||||||
|
tags: string;
|
||||||
|
level: number;
|
||||||
|
combatRole?: Record<string, string>;
|
||||||
|
configurationModes: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface TurretDefinitionsList {
|
||||||
|
nextAvailableId: number;
|
||||||
|
ids: Record<string, number>;
|
||||||
|
}
|
||||||
|
interface GunDefinitionsList {
|
||||||
|
nextAvailableId: number;
|
||||||
|
ids: Record<string, number>;
|
||||||
|
shared: {
|
||||||
|
[key: string]: {
|
||||||
|
userString: string;
|
||||||
|
tags: string;
|
||||||
|
level: number;
|
||||||
|
pitchLimits: string;
|
||||||
|
burst?: { count: number; rate: number };
|
||||||
|
clip?: { count: number; rate: number };
|
||||||
|
shots: {
|
||||||
|
[key: string]: {
|
||||||
|
speed: number;
|
||||||
|
piercingPower: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
type ShellKind =
|
||||||
|
| 'ARMOR_PIERCING'
|
||||||
|
| 'ARMOR_PIERCING_CR'
|
||||||
|
| 'HIGH_EXPLOSIVE'
|
||||||
|
| 'HOLLOW_CHARGE';
|
||||||
|
type ShellDefinitionsList = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: number;
|
||||||
|
userString: string;
|
||||||
|
icon: string;
|
||||||
|
kind: ShellKind;
|
||||||
|
caliber: number;
|
||||||
|
damage: { armor: number; devices: number };
|
||||||
|
normalizationAngle: number;
|
||||||
|
ricochetAngle: number;
|
||||||
|
}
|
||||||
|
> & {
|
||||||
|
icons: Record<string, string>;
|
||||||
|
};
|
||||||
|
interface VehicleDefinitions {
|
||||||
|
turrets0: {
|
||||||
|
[key: string]: {
|
||||||
|
userString: number;
|
||||||
|
level: number;
|
||||||
|
yawLimits: string | string[];
|
||||||
|
guns: {
|
||||||
|
[key: string]: {
|
||||||
|
maxAmmo: number;
|
||||||
|
pitchLimits?: string | string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface Strings {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
|
||||||
|
interface TankParameters {
|
||||||
|
resourcesPath: {
|
||||||
|
smallIconPath: string;
|
||||||
|
bigIconPath: string;
|
||||||
|
blitzModelPath: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const blitzTankTypeToBlitzkrieg: Record<BlitzTankType, TankType> = {
|
||||||
|
'AT-SPG': 'tank_destroyer',
|
||||||
|
lightTank: 'light',
|
||||||
|
mediumTank: 'medium',
|
||||||
|
heavyTank: 'heavy',
|
||||||
|
};
|
||||||
|
const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
|
||||||
|
ARMOR_PIERCING: 'ap',
|
||||||
|
ARMOR_PIERCING_CR: 'ap_cr',
|
||||||
|
HIGH_EXPLOSIVE: 'he',
|
||||||
|
HOLLOW_CHARGE: 'hc',
|
||||||
|
};
|
||||||
|
const DATA =
|
||||||
|
'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data';
|
||||||
|
const LANGUAGE = 'en';
|
||||||
|
const DOI = {
|
||||||
|
vehicleDefinitions: 'XML/item_defs/vehicles',
|
||||||
|
strings: 'Strings',
|
||||||
|
tankParameters: '3d/Tanks/Parameters',
|
||||||
|
smallIcons: 'Gfx/UI/BattleScreenHUD/SmallTankIcons',
|
||||||
|
bigIcons: 'Gfx/UI/BigTankIcons',
|
||||||
|
flags: 'Gfx/Lobby/flags',
|
||||||
|
'3d': '3d',
|
||||||
|
bigShellIcons: 'Gfx/Shared/tank-supply/ammunition/big',
|
||||||
|
};
|
||||||
|
|
||||||
|
const allTargets = argv.includes('--all-targets');
|
||||||
|
const targets = argv
|
||||||
|
.find((argument) => argument.startsWith('--target'))
|
||||||
|
?.split('=')[1]
|
||||||
|
.split(',');
|
||||||
|
|
||||||
|
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
||||||
|
|
||||||
|
if (allTargets || targets?.includes('definitions')) {
|
||||||
|
console.log('Building tank definitions...');
|
||||||
|
|
||||||
|
const tankDefinitions: BlitzkriegTankDefinitions = {};
|
||||||
|
const turretDefinitions: BlitzkriegTurretDefinitions = {};
|
||||||
|
const gunDefinitions: BlitzkriegGunDefinitions = {};
|
||||||
|
const shellDefinitions: BlitzkriegShellDefinitions = {};
|
||||||
|
const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then(
|
||||||
|
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||||
|
);
|
||||||
|
const strings = await readYAMLDVPL<Strings>(
|
||||||
|
`${DATA}/${DOI.strings}/${LANGUAGE}.yaml.dvpl`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
nations.map(async (nation) => {
|
||||||
|
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||||
|
`${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||||
|
);
|
||||||
|
const turretList = await readXMLDVPL<{
|
||||||
|
root: TurretDefinitionsList;
|
||||||
|
}>(
|
||||||
|
`${DATA}/${DOI.vehicleDefinitions}/${nation}/components/turrets.xml.dvpl`,
|
||||||
|
);
|
||||||
|
const gunList = await readXMLDVPL<{
|
||||||
|
root: GunDefinitionsList;
|
||||||
|
}>(
|
||||||
|
`${DATA}/${DOI.vehicleDefinitions}/${nation}/components/guns.xml.dvpl`,
|
||||||
|
);
|
||||||
|
const shellList = await readXMLDVPL<{
|
||||||
|
root: ShellDefinitionsList;
|
||||||
|
}>(
|
||||||
|
`${DATA}/${DOI.vehicleDefinitions}/${nation}/components/shells.xml.dvpl`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const tankKey in tankList.root) {
|
||||||
|
const tank = tankList.root[tankKey];
|
||||||
|
const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>(
|
||||||
|
`${DATA}/${DOI.vehicleDefinitions}/${nation}/${tankKey}.xml.dvpl`,
|
||||||
|
);
|
||||||
|
const tankId = toUniqueId(nation, tank.id);
|
||||||
|
const tankTags = tank.tags.split(' ');
|
||||||
|
const tankTurrets = Object.keys(tankDefinition.root.turrets0).map(
|
||||||
|
(turretKey) => {
|
||||||
|
const turret = tankDefinition.root.turrets0[turretKey];
|
||||||
|
const turretId = toUniqueId(nation, turretList.root.ids[turretKey]);
|
||||||
|
const turretGuns = Object.keys(turret.guns).map((gunKey) => {
|
||||||
|
const turretGunEntry = turret.guns[gunKey];
|
||||||
|
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
|
||||||
|
const gun = gunList.root.shared[gunKey];
|
||||||
|
const pitchLimitsRaw =
|
||||||
|
turretGunEntry.pitchLimits ?? gun.pitchLimits;
|
||||||
|
const gunShells = Object.keys(gun.shots).map((shellKey) => {
|
||||||
|
const turretShellEntry = gun.shots[shellKey];
|
||||||
|
const shell = shellList.root[shellKey];
|
||||||
|
const shellId = toUniqueId(nation, shell.id);
|
||||||
|
|
||||||
|
shellDefinitions[shellId] = {
|
||||||
|
id: shellId,
|
||||||
|
name:
|
||||||
|
strings[shell.userString] ?? shellKey.replaceAll('_', ' '),
|
||||||
|
speed: turretShellEntry.speed,
|
||||||
|
damage: {
|
||||||
|
armor: shell.damage.armor,
|
||||||
|
devices: shell.damage.devices,
|
||||||
|
},
|
||||||
|
caliber: shell.caliber,
|
||||||
|
normalization: shell.normalizationAngle,
|
||||||
|
ricochet: shell.ricochetAngle,
|
||||||
|
type: blitzShellKindToBLitzkrieg[shell.kind],
|
||||||
|
icon: shell.icon,
|
||||||
|
};
|
||||||
|
|
||||||
|
return shellId;
|
||||||
|
});
|
||||||
|
|
||||||
|
gunDefinitions[gunId] = {
|
||||||
|
id: gunId,
|
||||||
|
pitch: (typeof pitchLimitsRaw === 'string'
|
||||||
|
? pitchLimitsRaw
|
||||||
|
: pitchLimitsRaw.at(-1)!
|
||||||
|
)
|
||||||
|
.split(' ')
|
||||||
|
.map(Number) as [number, number],
|
||||||
|
name: strings[gun.userString] ?? gunKey.replaceAll('_', ' '),
|
||||||
|
tier: gun.level as Tier,
|
||||||
|
shells: gunShells,
|
||||||
|
};
|
||||||
|
|
||||||
|
return gunId;
|
||||||
|
});
|
||||||
|
|
||||||
|
turretDefinitions[turretId] = {
|
||||||
|
id: turretId,
|
||||||
|
name:
|
||||||
|
strings[turret.userString] ??
|
||||||
|
turretKey
|
||||||
|
.replaceAll('_', ' ')
|
||||||
|
.replace(/^(Turret ([0-9] )?)+/, ''),
|
||||||
|
tier: turret.level as Tier,
|
||||||
|
yaw: (typeof turret.yawLimits === 'string'
|
||||||
|
? turret.yawLimits
|
||||||
|
: turret.yawLimits.at(-1)!
|
||||||
|
)
|
||||||
|
.split(' ')
|
||||||
|
.map(Number) as [number, number],
|
||||||
|
guns: turretGuns,
|
||||||
|
};
|
||||||
|
|
||||||
|
return turretId;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tankDefinitions[tankId] = {
|
||||||
|
id: tankId,
|
||||||
|
name:
|
||||||
|
(tank.shortUserString
|
||||||
|
? strings[tank.shortUserString]
|
||||||
|
: undefined) ??
|
||||||
|
strings[tank.userString] ??
|
||||||
|
tankKey.replaceAll('_', ' '),
|
||||||
|
nation,
|
||||||
|
tree_type: (tank.sellPrice ? 'gold' in tank.sellPrice : false)
|
||||||
|
? 'collector'
|
||||||
|
: (typeof tank.price === 'number' ? false : 'gold' in tank.price)
|
||||||
|
? 'premium'
|
||||||
|
: 'researchable',
|
||||||
|
tier: tank.level as Tier,
|
||||||
|
type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType],
|
||||||
|
testing: tankTags.includes('testTank') ? true : undefined,
|
||||||
|
turrets: tankTurrets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Committing tank definitions...');
|
||||||
|
await commitMultipleFiles(
|
||||||
|
'tresabhi',
|
||||||
|
'blitzkrieg-assets',
|
||||||
|
'main',
|
||||||
|
'definitions',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
content: JSON.stringify(tankDefinitions),
|
||||||
|
encoding: 'utf-8',
|
||||||
|
path: 'definitions/tanks.json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: JSON.stringify(turretDefinitions),
|
||||||
|
encoding: 'utf-8',
|
||||||
|
path: 'definitions/turrets.json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: JSON.stringify(gunDefinitions),
|
||||||
|
encoding: 'utf-8',
|
||||||
|
path: 'definitions/guns.json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: JSON.stringify(shellDefinitions),
|
||||||
|
encoding: 'utf-8',
|
||||||
|
path: 'definitions/shells.json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
allTargets ||
|
||||||
|
targets?.includes('bigTankIcons') ||
|
||||||
|
targets?.includes('smallTankIcons')
|
||||||
|
) {
|
||||||
|
console.log('Building tank icons...');
|
||||||
|
|
||||||
|
const changes: FileChange[] = [];
|
||||||
|
const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then(
|
||||||
|
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
nations.map(async (nation) => {
|
||||||
|
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||||
|
`${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const tankIndex in tanks.root) {
|
||||||
|
const tank = tanks.root[tankIndex];
|
||||||
|
const nationVehicleId = tank.id;
|
||||||
|
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||||
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
|
`${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`,
|
||||||
|
);
|
||||||
|
const small = `${DATA}/${parameters.resourcesPath.smallIconPath
|
||||||
|
.replace(/~res:\//, '')
|
||||||
|
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||||
|
const big = `${DATA}/${parameters.resourcesPath.bigIconPath
|
||||||
|
.replace(/~res:\//, '')
|
||||||
|
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||||
|
|
||||||
|
if (allTargets || targets?.includes('bigTankIcons')) {
|
||||||
|
changes.push({
|
||||||
|
content: await readBase64DVPL(big),
|
||||||
|
encoding: 'base64',
|
||||||
|
path: `icons/big/${id}.webp`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (allTargets || targets?.includes('smallTankIcons')) {
|
||||||
|
changes.push({
|
||||||
|
content: await readBase64DVPL(small),
|
||||||
|
encoding: 'base64',
|
||||||
|
path: `icons/small/${id}.webp`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Committing tank icons...');
|
||||||
|
await commitMultipleFiles(
|
||||||
|
'tresabhi',
|
||||||
|
'blitzkrieg-assets',
|
||||||
|
'main',
|
||||||
|
'tank icons',
|
||||||
|
changes,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTargets || targets?.includes('scratchedFlags')) {
|
||||||
|
console.log('Building scratched flags...');
|
||||||
|
|
||||||
|
const flags = await readdir(`${DATA}/${DOI.flags}`);
|
||||||
|
|
||||||
|
console.log('Committing scratched flags...');
|
||||||
|
commitMultipleFiles(
|
||||||
|
'tresabhi',
|
||||||
|
'blitzkrieg-assets',
|
||||||
|
'main',
|
||||||
|
'scratched flags',
|
||||||
|
await Promise.all(
|
||||||
|
flags
|
||||||
|
.filter(
|
||||||
|
(flag) =>
|
||||||
|
flag.startsWith('flag_tutor-tank_') &&
|
||||||
|
!flag.endsWith('@2x.packed.webp.dvpl'),
|
||||||
|
)
|
||||||
|
.map(async (flag) => {
|
||||||
|
const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`);
|
||||||
|
const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
encoding: 'base64',
|
||||||
|
path: `flags/scratched/${name}.webp`,
|
||||||
|
} satisfies FileChange;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTargets || targets?.includes('circleFlags')) {
|
||||||
|
const flags = await readdir(`${DATA}/${DOI.flags}`);
|
||||||
|
|
||||||
|
commitMultipleFiles(
|
||||||
|
'tresabhi',
|
||||||
|
'blitzkrieg-assets',
|
||||||
|
'main',
|
||||||
|
'circle flags',
|
||||||
|
await Promise.all(
|
||||||
|
flags
|
||||||
|
.filter(
|
||||||
|
(flag) =>
|
||||||
|
flag.startsWith('flag_profile-stat_') &&
|
||||||
|
!flag.endsWith('@2x.packed.webp.dvpl'),
|
||||||
|
)
|
||||||
|
.map(async (flag) => {
|
||||||
|
const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`);
|
||||||
|
const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
encoding: 'base64',
|
||||||
|
path: `flags/circle/${name}.webp`,
|
||||||
|
} satisfies FileChange;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTargets || targets?.includes('tankModels')) {
|
||||||
|
console.log('Building tank models...');
|
||||||
|
|
||||||
|
if (existsSync('dist/assets/models')) {
|
||||||
|
await rm('dist/assets/models', { recursive: true });
|
||||||
|
}
|
||||||
|
await mkdir('dist/assets/models', { recursive: true });
|
||||||
|
|
||||||
|
const changes: FileChange[] = [];
|
||||||
|
const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then(
|
||||||
|
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
nations.map(async (nation) => {
|
||||||
|
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||||
|
`${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const tankIndex in tanks.root) {
|
||||||
|
const tank = tanks.root[tankIndex];
|
||||||
|
const nationVehicleId = tank.id;
|
||||||
|
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||||
|
|
||||||
|
// if (id !== 897) continue;
|
||||||
|
console.log(`Building model ${id} @ ${nation}/${tankIndex}`);
|
||||||
|
|
||||||
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
|
`${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`,
|
||||||
|
);
|
||||||
|
const sc2Path = parameters.resourcesPath.blitzModelPath;
|
||||||
|
const scgPath = parameters.resourcesPath.blitzModelPath.replace(
|
||||||
|
/\.sc2$/,
|
||||||
|
'.scg',
|
||||||
|
);
|
||||||
|
const sc2Stream = await SCPGStream.fromDVPLFile(
|
||||||
|
`${DATA}/${DOI['3d']}/${sc2Path}.dvpl`,
|
||||||
|
);
|
||||||
|
const scgStream = await SCPGStream.fromDVPLFile(
|
||||||
|
`${DATA}/${DOI['3d']}/${scgPath}.dvpl`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sc2 = sc2Stream.consumeSC2();
|
||||||
|
const scg = scgStream.consumeSCG();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTargets || targets?.includes('shellIcons')) {
|
||||||
|
console.log('Building shell icons...');
|
||||||
|
|
||||||
|
const image = sharp(
|
||||||
|
await readDVPLFile(
|
||||||
|
`${DATA}/${DOI.bigShellIcons}/texture0.packed.webp.dvpl`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const changes = await Promise.all(
|
||||||
|
(await readdir(`${DATA}/${DOI.bigShellIcons}`))
|
||||||
|
.filter((file) => file.endsWith('_l.txt.dvpl'))
|
||||||
|
.map(async (file) => {
|
||||||
|
const name = file.match(/(.+)_l\.txt\.dvpl/)![1];
|
||||||
|
const sizes = (
|
||||||
|
await readStringDVPL(`${DATA}/${DOI.bigShellIcons}/${file}`)
|
||||||
|
)
|
||||||
|
.split('\n')[4]
|
||||||
|
.split(' ')
|
||||||
|
.map(Number);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: (
|
||||||
|
await image
|
||||||
|
.clone()
|
||||||
|
.extract({
|
||||||
|
left: sizes[0],
|
||||||
|
top: sizes[1],
|
||||||
|
width: sizes[2],
|
||||||
|
height: sizes[3],
|
||||||
|
})
|
||||||
|
.toBuffer()
|
||||||
|
).toString('base64'),
|
||||||
|
encoding: 'base64',
|
||||||
|
path: `shells/${name}.webp`,
|
||||||
|
} satisfies FileChange;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Committing shell icons...');
|
||||||
|
commitMultipleFiles(
|
||||||
|
'tresabhi',
|
||||||
|
'blitzkrieg-assets',
|
||||||
|
'main',
|
||||||
|
'shell icons',
|
||||||
|
changes,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
RatingsNeighbors,
|
RatingsNeighbors,
|
||||||
RatingsPlayer,
|
RatingsPlayer,
|
||||||
} from '../src/commands/ratings';
|
} from '../src/commands/ratings';
|
||||||
|
import commitMultipleFiles from '../src/core/blitzkrieg/commitMultipleFiles.js';
|
||||||
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
|
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
|
||||||
import commitMultipleFiles from './commitMultipleFiles.js';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Central North American Time (UTC-5): use 0 5 * * *.
|
* Central North American Time (UTC-5): use 0 5 * * *.
|
||||||
@@ -126,14 +126,13 @@ if (publish) {
|
|||||||
|
|
||||||
console.log(`Publishing to season ${info.current_season}...`);
|
console.log(`Publishing to season ${info.current_season}...`);
|
||||||
commitMultipleFiles(
|
commitMultipleFiles(
|
||||||
octokit,
|
|
||||||
'tresabhi',
|
'tresabhi',
|
||||||
'blitzkrieg-assets',
|
'blitzkrieg-assets',
|
||||||
'main',
|
'main',
|
||||||
new Date().toString(),
|
new Date().toString(),
|
||||||
[
|
[
|
||||||
{ path: infoPath, content: infoJSON },
|
{ path: infoPath, content: infoJSON, encoding: 'utf-8' },
|
||||||
{ path: leaderboardPath, content: leaderboardJSON },
|
{ path: leaderboardPath, content: leaderboardJSON, encoding: 'utf-8' },
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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'),
|
||||||
|
);
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import decompress from 'decompress';
|
|
||||||
import { readFile, readdir, rm, writeFile } from 'fs/promises';
|
|
||||||
import { load } from 'js-yaml';
|
|
||||||
import { ElementCompact, xml2js } from 'xml-js';
|
|
||||||
import { NATION_IDS } from '../src/constants/nations';
|
|
||||||
|
|
||||||
const REWRITE = false;
|
|
||||||
const VEHICLE_DEFS = 'assets/Data/XML/item_defs/vehicles';
|
|
||||||
const APK_FILE = 'temp/blitz.apk';
|
|
||||||
const STRINGS = 'assets/Data/Strings';
|
|
||||||
const LANGUAGE = 'en';
|
|
||||||
const DIRECTORIES_OF_INTEREST = [VEHICLE_DEFS, STRINGS];
|
|
||||||
|
|
||||||
if (REWRITE) {
|
|
||||||
await rm('temp/blitz', { recursive: true, force: true });
|
|
||||||
decompress(APK_FILE, 'temp/blitz', {
|
|
||||||
filter: (file) =>
|
|
||||||
DIRECTORIES_OF_INTEREST.some((dir) => file.path.startsWith(dir)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BlitzkriegTankopedia {
|
|
||||||
id: number;
|
|
||||||
nation: string;
|
|
||||||
name: string;
|
|
||||||
name_short?: string;
|
|
||||||
is_premium: boolean;
|
|
||||||
is_collector: boolean;
|
|
||||||
tier: number;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strings = await readFile(
|
|
||||||
`temp/blitz/${STRINGS}/${LANGUAGE}.yaml`,
|
|
||||||
'utf-8',
|
|
||||||
).then((data) => load(data) as Record<string, string>);
|
|
||||||
const tankopedia: Record<number, BlitzkriegTankopedia> = {};
|
|
||||||
const nations = await readdir(`temp/blitz/${VEHICLE_DEFS}`);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
nations
|
|
||||||
.filter((nation) => nation !== 'common')
|
|
||||||
.map(async (nation) => {
|
|
||||||
const listXML = await readFile(
|
|
||||||
`temp/blitz/${VEHICLE_DEFS}/${nation}/list.xml`,
|
|
||||||
'utf-8',
|
|
||||||
);
|
|
||||||
const list = xml2js(listXML, { compact: true }) as ElementCompact;
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Object.entries<ElementCompact>(list.root).map(
|
|
||||||
async ([vehicle, vehicleData]) => {
|
|
||||||
// const vehicleXML = await readFile(
|
|
||||||
// `temp/blitz/${VEHICLE_DEFS}/${nation}/${vehicle}.xml`,
|
|
||||||
// 'utf-8',
|
|
||||||
// ).then((xml) => xml2js(xml, { compact: true }) as ElementCompact);
|
|
||||||
const nationVehicleId = parseInt(list.root[vehicle].id._text);
|
|
||||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
|
||||||
// TODO: use userString property instead
|
|
||||||
const name = strings[`#${nation}_vehicles:${vehicle}`];
|
|
||||||
const name_short = strings[`#${nation}_vehicles:${vehicle}_short`];
|
|
||||||
const is_premium = 'gold' in vehicleData.price;
|
|
||||||
const is_collector = vehicleData.sellPrice
|
|
||||||
? 'gold' in vehicleData.sellPrice
|
|
||||||
: false;
|
|
||||||
const tier = parseInt(vehicleData.level._text);
|
|
||||||
const tags = vehicleData.tags._text.split(' ');
|
|
||||||
const type = tags[0];
|
|
||||||
|
|
||||||
tankopedia[id] = {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
name_short,
|
|
||||||
nation,
|
|
||||||
is_premium,
|
|
||||||
is_collector,
|
|
||||||
tier,
|
|
||||||
type,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
writeFile('dist/tankopedia.json', JSON.stringify(tankopedia, null, 2));
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { Octokit } from '@octokit/rest';
|
|
||||||
|
|
||||||
export default async function commitMultipleFiles(
|
|
||||||
octokit: Octokit,
|
|
||||||
owner: string,
|
|
||||||
repo: string,
|
|
||||||
branch: string,
|
|
||||||
message: string,
|
|
||||||
changes: { path: string; content: string }[],
|
|
||||||
) {
|
|
||||||
// Get the latest commit SHA on the specified branch
|
|
||||||
const { data: refData } = await octokit.git.getRef({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
ref: `heads/${branch}`,
|
|
||||||
});
|
|
||||||
const latestCommitSha = refData.object.sha;
|
|
||||||
|
|
||||||
// Get the tree SHA for the latest commit
|
|
||||||
const { data: commitData } = await octokit.git.getCommit({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
commit_sha: latestCommitSha,
|
|
||||||
});
|
|
||||||
const treeSha = commitData.tree.sha;
|
|
||||||
|
|
||||||
// Create blobs for each file
|
|
||||||
const blobs = await Promise.all(
|
|
||||||
changes.map(async (change) => {
|
|
||||||
const { data: blobData } = await octokit.git.createBlob({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
content: change.content,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sha: blobData.sha,
|
|
||||||
path: change.path,
|
|
||||||
mode: '100644',
|
|
||||||
type: 'blob',
|
|
||||||
} as const;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a new tree with the new blobs
|
|
||||||
const { data: treeData } = await octokit.git.createTree({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
base_tree: treeSha,
|
|
||||||
tree: blobs,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a new commit with the new tree
|
|
||||||
const { data: newCommitData } = await octokit.git.createCommit({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
message,
|
|
||||||
tree: treeData.sha,
|
|
||||||
parents: [latestCommitSha],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the branch reference to point to the new commit
|
|
||||||
await octokit.git.updateRef({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
ref: `heads/${branch}`,
|
|
||||||
sha: newCommitData.sha,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { tankopedia } from '../../../core/blitzstars/tankopedia';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
return NextResponse.json(await tankopedia);
|
|
||||||
}
|
|
||||||
+113
-91
@@ -6,22 +6,29 @@ import * as Breakdown from '../../../components/Breakdown';
|
|||||||
import { TreeTypeEnum } from '../../../components/Tanks';
|
import { TreeTypeEnum } from '../../../components/Tanks';
|
||||||
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 { tankAverages } from '../../../core/blitzstars/tankAverages';
|
|
||||||
import {
|
import {
|
||||||
TankopediaEntry,
|
BlitzkriegTankDefinition,
|
||||||
tankopedia,
|
tankDefinitions,
|
||||||
} from '../../../core/blitzstars/tankopedia';
|
} from '../../../core/blitzkrieg/definitions/tanks';
|
||||||
|
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||||
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
||||||
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
||||||
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
|
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
|
||||||
import { resetSession, useSession } from '../../../stores/session';
|
import mutateSession, {
|
||||||
|
resetSession,
|
||||||
|
useSession,
|
||||||
|
} from '../../../stores/session';
|
||||||
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
||||||
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
||||||
import { Menu } from '../../tools/session/components/Menu';
|
import { Menu } from '../../tools/session/components/Menu';
|
||||||
|
|
||||||
const REFRESH_RATE = 1 / 5;
|
const REFRESH_RATE = 1 / 5;
|
||||||
|
|
||||||
export default function SessionPage() {
|
interface SessionPageProps {
|
||||||
|
naked?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SessionPage({ naked = true }: SessionPageProps) {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const since = session.isTracking ? new Date(session.time) : undefined;
|
const since = session.isTracking ? new Date(session.time) : undefined;
|
||||||
const [diff, setDiff] = useState<
|
const [diff, setDiff] = useState<
|
||||||
@@ -30,7 +37,7 @@ export default function SessionPage() {
|
|||||||
|
|
||||||
list: {
|
list: {
|
||||||
stats: IndividualTankStats;
|
stats: IndividualTankStats;
|
||||||
tankopedia?: TankopediaEntry;
|
tankDefinitions?: BlitzkriegTankDefinition;
|
||||||
career: IndividualTankStats;
|
career: IndividualTankStats;
|
||||||
currentWN8?: number;
|
currentWN8?: number;
|
||||||
careerWN8?: number;
|
careerWN8?: number;
|
||||||
@@ -72,6 +79,14 @@ export default function SessionPage() {
|
|||||||
0,
|
0,
|
||||||
) / careerBattles;
|
) / careerBattles;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session.title === 'Untitled session') {
|
||||||
|
mutateSession((draft) => {
|
||||||
|
if (draft.isTracking) draft.title = draft.nickname;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [session.title]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function recalculateDiff() {
|
async function recalculateDiff() {
|
||||||
if (session.isTracking) {
|
if (session.isTracking) {
|
||||||
@@ -88,7 +103,7 @@ export default function SessionPage() {
|
|||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
const awaitedTankopedia = await tankopedia;
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
const awaitedTankAverages = await tankAverages;
|
const awaitedTankAverages = await tankAverages;
|
||||||
const careerWN8 =
|
const careerWN8 =
|
||||||
careerRaw[id].reduce(
|
careerRaw[id].reduce(
|
||||||
@@ -114,12 +129,12 @@ export default function SessionPage() {
|
|||||||
|
|
||||||
list: (
|
list: (
|
||||||
Object.values(
|
Object.values(
|
||||||
deltaTankStats(session.tankStats, career),
|
deltaTankStats(session.tankStats, career, session.time / 1000),
|
||||||
) as IndividualTankStats[]
|
) as IndividualTankStats[]
|
||||||
).map((stats) => {
|
).map((stats) => {
|
||||||
return {
|
return {
|
||||||
stats,
|
stats,
|
||||||
tankopedia: awaitedTankopedia[stats.tank_id],
|
tankDefinitions: awaitedTankDefinitions[stats.tank_id],
|
||||||
career: career[stats.tank_id],
|
career: career[stats.tank_id],
|
||||||
currentWN8: awaitedTankAverages[stats.tank_id]
|
currentWN8: awaitedTankAverages[stats.tank_id]
|
||||||
? calculateWN8(
|
? calculateWN8(
|
||||||
@@ -207,6 +222,7 @@ export default function SessionPage() {
|
|||||||
since !== undefined &&
|
since !== undefined &&
|
||||||
diff !== undefined && (
|
diff !== undefined && (
|
||||||
<Breakdown.Row
|
<Breakdown.Row
|
||||||
|
naked={naked}
|
||||||
color={session.color}
|
color={session.color}
|
||||||
minimized={!session.showCareer}
|
minimized={!session.showCareer}
|
||||||
title={session.title}
|
title={session.title}
|
||||||
@@ -231,90 +247,96 @@ export default function SessionPage() {
|
|||||||
.sort(
|
.sort(
|
||||||
(a, b) => b.stats.last_battle_time - a.stats.last_battle_time,
|
(a, b) => b.stats.last_battle_time - a.stats.last_battle_time,
|
||||||
)
|
)
|
||||||
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => {
|
.map(
|
||||||
const rowStats: Record<
|
({ stats, tankDefinitions, career, careerWN8, currentWN8 }) => {
|
||||||
CustomColumnDisplay,
|
const rowStats: Record<
|
||||||
Breakdown.RowStatItem | undefined
|
CustomColumnDisplay,
|
||||||
> = {
|
Breakdown.RowStatItem | undefined
|
||||||
battles: {
|
> = {
|
||||||
title: 'Battles',
|
battles: {
|
||||||
current: stats.all.battles.toLocaleString(),
|
title: 'Battles',
|
||||||
career: career.all.battles.toLocaleString(),
|
current: stats.all.battles.toLocaleString(),
|
||||||
},
|
career: career.all.battles.toLocaleString(),
|
||||||
winrate: {
|
},
|
||||||
title: 'Winrate',
|
winrate: {
|
||||||
current: `${(
|
title: 'Winrate',
|
||||||
100 *
|
current: `${(
|
||||||
(stats.all.wins / stats.all.battles)
|
100 *
|
||||||
).toFixed(2)}%`,
|
(stats.all.wins / stats.all.battles)
|
||||||
career: `${(
|
).toFixed(2)}%`,
|
||||||
100 *
|
career: `${(
|
||||||
(career.all.wins / career.all.battles)
|
100 *
|
||||||
).toFixed(2)}%`,
|
(career.all.wins / career.all.battles)
|
||||||
delta:
|
).toFixed(2)}%`,
|
||||||
stats.all.wins / stats.all.battles -
|
delta:
|
||||||
career.all.wins / career.all.battles,
|
stats.all.wins / stats.all.battles -
|
||||||
},
|
career.all.wins / career.all.battles,
|
||||||
wn8: {
|
},
|
||||||
title: 'WN8',
|
wn8: {
|
||||||
current:
|
title: 'WN8',
|
||||||
currentWN8 === undefined
|
current:
|
||||||
? undefined
|
currentWN8 === undefined
|
||||||
: Math.round(currentWN8).toLocaleString(),
|
? undefined
|
||||||
career:
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
careerWN8 === undefined
|
career:
|
||||||
? undefined
|
careerWN8 === undefined
|
||||||
: Math.round(careerWN8).toLocaleString(),
|
? undefined
|
||||||
percentile:
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
currentWN8 === undefined
|
percentile:
|
||||||
? undefined
|
currentWN8 === undefined
|
||||||
: getWN8Percentile(currentWN8),
|
? undefined
|
||||||
},
|
: getWN8Percentile(currentWN8),
|
||||||
damage: {
|
},
|
||||||
title: 'Damage',
|
damage: {
|
||||||
current: Math.round(
|
title: 'Damage',
|
||||||
stats.all.damage_dealt / stats.all.battles,
|
current: Math.round(
|
||||||
).toLocaleString(),
|
stats.all.damage_dealt / stats.all.battles,
|
||||||
career: Math.round(
|
).toLocaleString(),
|
||||||
career.all.damage_dealt / career.all.battles,
|
career: Math.round(
|
||||||
).toLocaleString(),
|
career.all.damage_dealt / career.all.battles,
|
||||||
delta:
|
).toLocaleString(),
|
||||||
stats.all.damage_dealt / stats.all.battles -
|
delta:
|
||||||
career.all.damage_dealt / career.all.battles,
|
stats.all.damage_dealt / stats.all.battles -
|
||||||
},
|
career.all.damage_dealt / career.all.battles,
|
||||||
none: undefined,
|
},
|
||||||
};
|
none: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breakdown.Row
|
<Breakdown.Row
|
||||||
color={session.color}
|
naked={naked}
|
||||||
key={stats.tank_id}
|
color={session.color}
|
||||||
minimized={!session.showCareer}
|
key={stats.tank_id}
|
||||||
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`}
|
minimized={!session.showCareer}
|
||||||
type="tank"
|
title={
|
||||||
tankType={tankopedia?.type}
|
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
|
||||||
treeType={(() => {
|
}
|
||||||
if (tankopedia?.is_collectible)
|
type="tank"
|
||||||
return TreeTypeEnum.Collector;
|
tankType={tankDefinitions?.type}
|
||||||
if (tankopedia?.is_premium) return TreeTypeEnum.Premium;
|
treeType={(() => {
|
||||||
})()}
|
if (tankDefinitions?.tree_type === 'collector')
|
||||||
stats={session.customColumns.map((customColumn) => {
|
return TreeTypeEnum.Collector;
|
||||||
const rowStat = rowStats[customColumn.display];
|
if (tankDefinitions?.tree_type === 'premium')
|
||||||
|
return TreeTypeEnum.Premium;
|
||||||
|
})()}
|
||||||
|
stats={session.customColumns.map((customColumn) => {
|
||||||
|
const rowStat = rowStats[customColumn.display];
|
||||||
|
|
||||||
if (!rowStat) return undefined;
|
if (!rowStat) return undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rowStat,
|
...rowStat,
|
||||||
delta:
|
delta:
|
||||||
customColumn.showDelta &&
|
customColumn.showDelta &&
|
||||||
rowStat.percentile === undefined
|
rowStat.percentile === undefined
|
||||||
? rowStat.delta
|
? rowStat.delta
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
</Breakdown.Root>
|
</Breakdown.Root>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenu.Trigger>
|
</ContextMenu.Trigger>
|
||||||
|
|||||||
+8
-1
@@ -36,12 +36,19 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
<body
|
<body
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
|
paddingTop: isEmbed ? 0 : '3.25rem',
|
||||||
background: isEmbed
|
background: isEmbed
|
||||||
? 'transparent'
|
? 'transparent'
|
||||||
: 'url(https://i.imgur.com/PhS06NJ.png)',
|
: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Theme appearance="dark" panelBackground="translucent" radius="full">
|
<Theme
|
||||||
|
appearance="dark"
|
||||||
|
panelBackground="translucent"
|
||||||
|
radius="full"
|
||||||
|
suppressHydrationWarning
|
||||||
|
suppressContentEditableWarning
|
||||||
|
>
|
||||||
{!isEmbed && <Navbar />}
|
{!isEmbed && <Navbar />}
|
||||||
|
|
||||||
<Flex direction="column">{children}</Flex>
|
<Flex direction="column">{children}</Flex>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface ToolsLayoutProps {
|
|||||||
|
|
||||||
export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const toolId = pathname.split('/').at(-1);
|
const toolId = pathname.split('/').at(2);
|
||||||
const tool = TOOLS.find((tool) => tool.id === toolId);
|
const tool = TOOLS.find((tool) => tool.id === toolId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export function PageTurner({
|
|||||||
type="number"
|
type="number"
|
||||||
ref={pageInput}
|
ref={pageInput}
|
||||||
style={{ width: 64, textAlign: 'center' }}
|
style={{ width: 64, textAlign: 'center' }}
|
||||||
|
min={1}
|
||||||
|
max={pages}
|
||||||
onBlur={(event) => {
|
onBlur={(event) => {
|
||||||
onPageChange(
|
onPageChange(
|
||||||
Math.max(
|
Math.max(
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SessionPage />
|
<SessionPage naked={false} />
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ArrowLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Slider,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
Theme,
|
||||||
|
Tooltip,
|
||||||
|
} from '@radix-ui/themes';
|
||||||
|
import { OrbitControls } from '@react-three/drei';
|
||||||
|
import { Canvas } from '@react-three/fiber';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { use, useEffect, useState } from 'react';
|
||||||
|
import { Flag } from '../../../../components/Flag';
|
||||||
|
import PageWrapper from '../../../../components/PageWrapper';
|
||||||
|
import { asset } from '../../../../core/blitzkrieg/asset';
|
||||||
|
import { gunDefinitions } from '../../../../core/blitzkrieg/definitions/guns';
|
||||||
|
import { shellDefinitions } from '../../../../core/blitzkrieg/definitions/shells';
|
||||||
|
import {
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
tankDefinitions,
|
||||||
|
tanksDefinitionsArray,
|
||||||
|
} from '../../../../core/blitzkrieg/definitions/tanks';
|
||||||
|
import { turretDefinitions } from '../../../../core/blitzkrieg/definitions/turrets';
|
||||||
|
import * as styles from '../page.css';
|
||||||
|
|
||||||
|
type Mode = 'model' | 'armor';
|
||||||
|
|
||||||
|
export default function Page({ params }: { params: { id: string } }) {
|
||||||
|
const id = parseInt(params.id);
|
||||||
|
const awaitedTankDefinitions = use(tankDefinitions);
|
||||||
|
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
|
||||||
|
const awaitedTurretDefinitions = use(turretDefinitions);
|
||||||
|
const awaitedGunDefinitions = use(gunDefinitions);
|
||||||
|
const awaitedShellDefinitions = use(shellDefinitions);
|
||||||
|
const tank = awaitedTankDefinitions[id];
|
||||||
|
const [turret, setTurret] = useState(
|
||||||
|
awaitedTurretDefinitions[tank.turrets[0]],
|
||||||
|
);
|
||||||
|
const [gun, setGun] = useState(awaitedGunDefinitions[turret.guns[0]]);
|
||||||
|
const [crew, setCrew] = useState(100);
|
||||||
|
const [versusTank, setVersusTank] = useState(
|
||||||
|
awaitedTanksDefinitionsArray[0].id,
|
||||||
|
);
|
||||||
|
const [mode, setMode] = useState<Mode>('model');
|
||||||
|
|
||||||
|
console.log(versusTank);
|
||||||
|
|
||||||
|
useEffect(() => setGun(awaitedGunDefinitions[turret.guns[0]]), [turret]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper>
|
||||||
|
<Flex gap="8" direction="column">
|
||||||
|
<Flex gap="4" direction="column">
|
||||||
|
<Flex justify="between" align="center">
|
||||||
|
<Link
|
||||||
|
href="/tools/tankopedia"
|
||||||
|
style={{ color: 'unset', textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
<Flex gap="1" align="center">
|
||||||
|
<ArrowLeftIcon />
|
||||||
|
<Text>Back to tankopedia</Text>
|
||||||
|
</Flex>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Flex gap="2" align="center">
|
||||||
|
<Flag nation={tank.nation} />
|
||||||
|
<Heading>{tank.name}</Heading>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Card style={{ position: 'relative' }}>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<Tabs.Root
|
||||||
|
value={mode}
|
||||||
|
onValueChange={(value) => setMode(value as Mode)}
|
||||||
|
>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Trigger value="model">Model</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="armor">Armor</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
</Tabs.Root>
|
||||||
|
|
||||||
|
<div style={{ height: '50vh', maxHeight: 576 }}>
|
||||||
|
<Canvas onPointerDown={(event) => event.preventDefault()}>
|
||||||
|
<OrbitControls />
|
||||||
|
|
||||||
|
<ambientLight />
|
||||||
|
<directionalLight />
|
||||||
|
|
||||||
|
<mesh>
|
||||||
|
<torusKnotGeometry args={[undefined, undefined, 128, 16]} />
|
||||||
|
<meshStandardMaterial color="cyan" />
|
||||||
|
</mesh>
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mode === 'armor' && (
|
||||||
|
<Flex gap="2" align="center" className={styles.enhancedArmor}>
|
||||||
|
<Checkbox defaultChecked />
|
||||||
|
<Text>Enhanced armor</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{mode === 'armor' && (
|
||||||
|
<Card>
|
||||||
|
<Flex align="center" justify="between" gap="2">
|
||||||
|
<Flex align="center" gap="4">
|
||||||
|
<Text>Versus</Text>
|
||||||
|
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger>
|
||||||
|
<Button variant="ghost">
|
||||||
|
{awaitedTankDefinitions[versusTank].name}
|
||||||
|
<CaretRightIcon />
|
||||||
|
</Button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
<Dialog.Content>Do something lol</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="2" align="center">
|
||||||
|
<Checkbox defaultChecked />
|
||||||
|
<Text>Calibrated shells</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="1">
|
||||||
|
<Button variant="solid" radius="small">
|
||||||
|
<img src={asset('shells/ap.webp')} width={24} height={24} />
|
||||||
|
</Button>
|
||||||
|
<Button variant="soft" radius="small" color="gray">
|
||||||
|
<img
|
||||||
|
src={asset('shells/hc_premium.webp')}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button variant="soft" radius="small" color="gray">
|
||||||
|
<img src={asset('shells/he.webp')} width={24} height={24} />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="6" direction="column">
|
||||||
|
<Heading>Build</Heading>
|
||||||
|
|
||||||
|
<Flex gap="2" direction="column">
|
||||||
|
<Heading size="5" weight="regular">
|
||||||
|
Modules
|
||||||
|
</Heading>
|
||||||
|
<Theme radius="small">
|
||||||
|
<Flex gap="4">
|
||||||
|
<Flex gap="1">
|
||||||
|
{tank.turrets.map((turretId) => {
|
||||||
|
const thisTurret = awaitedTurretDefinitions[turretId];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content={thisTurret.name}>
|
||||||
|
<Button
|
||||||
|
onClick={() => setTurret(thisTurret)}
|
||||||
|
variant={
|
||||||
|
turret.id === thisTurret.id ? 'solid' : 'soft'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/modules/turret.png"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
{TIER_ROMAN_NUMERALS[thisTurret.tier]}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="1">
|
||||||
|
{turret.guns.map((gunId, index) => {
|
||||||
|
const thisGun = awaitedGunDefinitions[gunId];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content={thisGun.name}>
|
||||||
|
<Button
|
||||||
|
onClick={() => setGun(thisGun)}
|
||||||
|
variant={gun.id === thisGun.id ? 'solid' : 'soft'}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/modules/gun.png"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
{TIER_ROMAN_NUMERALS[thisGun.tier]}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Theme>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="2" direction="column">
|
||||||
|
<Heading size="5" weight="regular">
|
||||||
|
Crew
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Flex gap="4" align="center">
|
||||||
|
<Text>{crew}%</Text>
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
value={[crew]}
|
||||||
|
min={50}
|
||||||
|
max={100}
|
||||||
|
onValueChange={([crew]) => setCrew(crew)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="2" direction="column">
|
||||||
|
<Heading size="5">Shells</Heading>
|
||||||
|
|
||||||
|
{gun.shells.map((shellId) => {
|
||||||
|
const shell = awaitedShellDefinitions[shellId];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{shell.name} ({shell.type}): {shell.damage.armor}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const splitFilters = style({
|
||||||
|
'@media': {
|
||||||
|
'screen and (min-width: 513px)': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const oneLineFilters = style({
|
||||||
|
'@media': {
|
||||||
|
'screen and (max-width: 512px)': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { CaretDownIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
|
||||||
|
import { use } from 'react';
|
||||||
|
import { Flag } from '../../../../../components/Flag';
|
||||||
|
import {
|
||||||
|
TANK_TYPES,
|
||||||
|
TREE_TYPES,
|
||||||
|
TREE_TYPE_ICONS,
|
||||||
|
TREE_TYPE_IMAGES,
|
||||||
|
TreeTypeEnum,
|
||||||
|
} from '../../../../../components/Tanks';
|
||||||
|
import {
|
||||||
|
NATIONS,
|
||||||
|
TIERS,
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
} from '../../../../../core/blitzkrieg/definitions/tanks';
|
||||||
|
import mutateTankopedia, {
|
||||||
|
TankopediaTestTankDisplay,
|
||||||
|
useTankopedia,
|
||||||
|
} from '../../../../../stores/tankopedia';
|
||||||
|
import * as styles from './index.css';
|
||||||
|
|
||||||
|
export function Options() {
|
||||||
|
const nations = use(NATIONS);
|
||||||
|
const tankopediaState = useTankopedia();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex justify="center" direction="column" gap="2">
|
||||||
|
<Flex gap="2" style={{ flex: 1 }} wrap="wrap">
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
onClick={() =>
|
||||||
|
useTankopedia.setState({
|
||||||
|
filters: {
|
||||||
|
nations: [],
|
||||||
|
tiers: [],
|
||||||
|
treeTypes: [],
|
||||||
|
types: [],
|
||||||
|
test: 'include',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button variant="soft">
|
||||||
|
Test tanks
|
||||||
|
<CaretDownIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.RadioGroup
|
||||||
|
value={tankopediaState.filters.test}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
draft.filters.test = value as TankopediaTestTankDisplay;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DropdownMenu.RadioItem value="include">
|
||||||
|
Include
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="exclude">
|
||||||
|
Exclude
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="only">Only</DropdownMenu.RadioItem>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
<Flex style={{ flex: 1 }}>
|
||||||
|
{TREE_TYPES.map((type, index) => (
|
||||||
|
<Button
|
||||||
|
key={type}
|
||||||
|
variant={
|
||||||
|
tankopediaState.filters.treeTypes.includes(type)
|
||||||
|
? 'solid'
|
||||||
|
: 'soft'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
margin: -0.5,
|
||||||
|
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderTopRightRadius:
|
||||||
|
index === TREE_TYPES.length - 1 ? undefined : 0,
|
||||||
|
borderBottomRightRadius:
|
||||||
|
index === TREE_TYPES.length - 1 ? undefined : 0,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
if (draft.filters.treeTypes.includes(type)) {
|
||||||
|
draft.filters.treeTypes = draft.filters.treeTypes.filter(
|
||||||
|
(preexistingType) => preexistingType !== type,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draft.filters.treeTypes.push(type);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={TREE_TYPE_IMAGES[type]}
|
||||||
|
style={{ width: '1em', height: '1em' }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
<Flex style={{ flex: 1 }}>
|
||||||
|
{[...TANK_TYPES].reverse().map((type, index) => (
|
||||||
|
<Button
|
||||||
|
key={type}
|
||||||
|
variant={
|
||||||
|
tankopediaState.filters.types.includes(type) ? 'solid' : 'soft'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
margin: -0.5,
|
||||||
|
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderTopRightRadius:
|
||||||
|
index === TANK_TYPES.length - 1 ? undefined : 0,
|
||||||
|
borderBottomRightRadius:
|
||||||
|
index === TANK_TYPES.length - 1 ? undefined : 0,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
if (draft.filters.types.includes(type)) {
|
||||||
|
draft.filters.types = draft.filters.types.filter(
|
||||||
|
(preexistingType) => preexistingType !== type,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draft.filters.types.push(type);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={TREE_TYPE_ICONS[TreeTypeEnum.TechTree][type]}
|
||||||
|
style={{ width: '1em', height: '1em' }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
className={styles.splitFilters}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Flex>
|
||||||
|
{[...TIERS]
|
||||||
|
.reverse()
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((tier, index) => (
|
||||||
|
<Button
|
||||||
|
key={tier}
|
||||||
|
variant={
|
||||||
|
tankopediaState.filters.tiers.includes(tier)
|
||||||
|
? 'solid'
|
||||||
|
: 'soft'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
margin: -0.5,
|
||||||
|
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
borderTopRightRadius:
|
||||||
|
index === TIERS.length - Math.round(TIERS.length / 2) - 1
|
||||||
|
? undefined
|
||||||
|
: 0,
|
||||||
|
borderBottomRightRadius: 0,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
if (draft.filters.tiers.includes(tier)) {
|
||||||
|
draft.filters.tiers = draft.filters.tiers.filter(
|
||||||
|
(preexistingTier) => preexistingTier !== tier,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draft.filters.tiers.push(tier);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{TIER_ROMAN_NUMERALS[tier]}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
{[...TIERS]
|
||||||
|
.reverse()
|
||||||
|
.slice(5, 10)
|
||||||
|
.map((tier, index) => (
|
||||||
|
<Button
|
||||||
|
key={tier}
|
||||||
|
variant={
|
||||||
|
tankopediaState.filters.tiers.includes(tier)
|
||||||
|
? 'solid'
|
||||||
|
: 'soft'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
margin: -0.5,
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
borderBottomRightRadius:
|
||||||
|
index === TIERS.length - Math.round(TIERS.length / 2) - 1
|
||||||
|
? undefined
|
||||||
|
: 0,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
if (draft.filters.tiers.includes(tier)) {
|
||||||
|
draft.filters.tiers = draft.filters.tiers.filter(
|
||||||
|
(preexistingTier) => preexistingTier !== tier,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draft.filters.tiers.push(tier);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{TIER_ROMAN_NUMERALS[tier]}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
className={styles.splitFilters}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Flex>
|
||||||
|
{nations.slice(0, 5).map((nation, index) => (
|
||||||
|
<Button
|
||||||
|
key={nation}
|
||||||
|
variant={
|
||||||
|
tankopediaState.filters.nations.includes(nation)
|
||||||
|
? 'solid'
|
||||||
|
: 'soft'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
margin: -0.5,
|
||||||
|
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
borderTopRightRadius:
|
||||||
|
index === nations.length - Math.round(nations.length / 2)
|
||||||
|
? undefined
|
||||||
|
: 0,
|
||||||
|
borderBottomRightRadius:
|
||||||
|
index === nations.length - 1 ? undefined : 0,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
if (draft.filters.nations.includes(nation)) {
|
||||||
|
draft.filters.nations = draft.filters.nations.filter(
|
||||||
|
(preexistingType) => preexistingType !== nation,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draft.filters.nations.push(nation);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flag nation={nation} />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex>
|
||||||
|
{nations.slice(5, 10).map((nation, index) => (
|
||||||
|
<Button
|
||||||
|
key={nation}
|
||||||
|
variant={
|
||||||
|
tankopediaState.filters.nations.includes(nation)
|
||||||
|
? 'solid'
|
||||||
|
: 'soft'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
margin: -0.5,
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderTopRightRadius:
|
||||||
|
index === nations.length - 1 ? undefined : 0,
|
||||||
|
borderBottomRightRadius:
|
||||||
|
index ===
|
||||||
|
nations.length - Math.round(nations.length / 2) - 1
|
||||||
|
? undefined
|
||||||
|
: 0,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
if (draft.filters.nations.includes(nation)) {
|
||||||
|
draft.filters.nations = draft.filters.nations.filter(
|
||||||
|
(preexistingType) => preexistingType !== nation,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draft.filters.nations.push(nation);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flag nation={nation} />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="2" style={{ flex: 1 }} wrap="wrap">
|
||||||
|
<Flex className={styles.oneLineFilters} style={{ flex: 1 }}>
|
||||||
|
{[...TIERS].reverse().map((tier, index) => (
|
||||||
|
<Button
|
||||||
|
key={tier}
|
||||||
|
variant={
|
||||||
|
tankopediaState.filters.tiers.includes(tier) ? 'solid' : 'soft'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
margin: -0.5,
|
||||||
|
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderTopRightRadius:
|
||||||
|
index === TIERS.length - 1 ? undefined : 0,
|
||||||
|
borderBottomRightRadius:
|
||||||
|
index === TIERS.length - 1 ? undefined : 0,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
if (draft.filters.tiers.includes(tier)) {
|
||||||
|
draft.filters.tiers = draft.filters.tiers.filter(
|
||||||
|
(preexistingTier) => preexistingTier !== tier,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draft.filters.tiers.push(tier);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{TIER_ROMAN_NUMERALS[tier]}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
<Flex className={styles.oneLineFilters} style={{ flex: 1 }}>
|
||||||
|
{nations.map((nation, index) => (
|
||||||
|
<Button
|
||||||
|
key={nation}
|
||||||
|
variant={
|
||||||
|
tankopediaState.filters.nations.includes(nation)
|
||||||
|
? 'solid'
|
||||||
|
: 'soft'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
margin: -0.5,
|
||||||
|
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||||
|
borderTopRightRadius:
|
||||||
|
index === nations.length - 1 ? undefined : 0,
|
||||||
|
borderBottomRightRadius:
|
||||||
|
index === nations.length - 1 ? undefined : 0,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
if (draft.filters.nations.includes(nation)) {
|
||||||
|
draft.filters.nations = draft.filters.nations.filter(
|
||||||
|
(preexistingType) => preexistingType !== nation,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draft.filters.nations.push(nation);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flag nation={nation} />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { CaretLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Button, Flex, TextField } from '@radix-ui/themes';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { BlitzkriegTankDefinition } from '../../../../core/blitzkrieg/definitions/tanks';
|
||||||
|
|
||||||
|
interface PageTurnerProps {
|
||||||
|
page: number;
|
||||||
|
setPage: (page: number) => void;
|
||||||
|
searchedList: BlitzkriegTankDefinition[];
|
||||||
|
tanksPerPage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageTurner({
|
||||||
|
page,
|
||||||
|
setPage,
|
||||||
|
searchedList,
|
||||||
|
tanksPerPage,
|
||||||
|
}: PageTurnerProps) {
|
||||||
|
const pageInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pageInput.current) pageInput.current.value = `${page + 1}`;
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex align="center" justify="center" gap="2">
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
disabled={page === 0}
|
||||||
|
onClick={() => setPage(Math.max(0, page - 1))}
|
||||||
|
>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
</Button>
|
||||||
|
<TextField.Root>
|
||||||
|
<TextField.Slot>Page</TextField.Slot>
|
||||||
|
<TextField.Input
|
||||||
|
defaultValue={1}
|
||||||
|
type="number"
|
||||||
|
ref={pageInput}
|
||||||
|
min={1}
|
||||||
|
max={Math.floor(searchedList.length / tanksPerPage) + 1}
|
||||||
|
style={{ width: 64, textAlign: 'center' }}
|
||||||
|
onBlur={(event) => {
|
||||||
|
setPage(
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
Math.floor(searchedList.length / tanksPerPage),
|
||||||
|
event.target.valueAsNumber - 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
(event.target as HTMLInputElement).blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField.Slot>
|
||||||
|
out of {Math.floor(searchedList.length / tanksPerPage) + 1}
|
||||||
|
</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
disabled={Math.floor(searchedList.length / tanksPerPage) === page}
|
||||||
|
onClick={() =>
|
||||||
|
setPage(
|
||||||
|
Math.min(Math.floor(searchedList.length / tanksPerPage), page + 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CaretRightIcon />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
|
export const listing = style({});
|
||||||
|
|
||||||
|
export const flag = style({
|
||||||
|
transitionDuration: theme.durations.regular,
|
||||||
|
left: '0',
|
||||||
|
top: '0',
|
||||||
|
|
||||||
|
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||||
|
transform: 'translateX(-25%)',
|
||||||
|
filter: 'blur(3rem)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listingShadow = style({
|
||||||
|
boxShadow: `inset 128px -128px 128px -128px ${theme.colors.appBackground1}`,
|
||||||
|
|
||||||
|
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||||
|
boxShadow: `inset 0 0 0 -128px ${theme.colors.appBackground1}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listingLabel = style({
|
||||||
|
transition: `transform ${theme.durations.regular}`,
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
|
||||||
|
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||||
|
transform: 'translateY(200%)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listingImage = style({
|
||||||
|
transition: `transform ${theme.durations.regular}`,
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
|
||||||
|
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||||
|
transform: 'translate(-25%, 10%)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const enhancedArmor = style({
|
||||||
|
'@media': {
|
||||||
|
'screen and (min-width: 400px)': {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
'screen and (max-width: 399px)': {
|
||||||
|
// left: 16,
|
||||||
|
// bottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { slateDark } from '@radix-ui/colors';
|
||||||
|
import {
|
||||||
|
CaretDownIcon,
|
||||||
|
CaretUpIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
DropdownMenu,
|
||||||
|
Flex,
|
||||||
|
Inset,
|
||||||
|
Text,
|
||||||
|
TextField,
|
||||||
|
} from '@radix-ui/themes';
|
||||||
|
import { go } from 'fuzzysort';
|
||||||
|
import { Suspense, use, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
|
import { asset } from '../../../core/blitzkrieg/asset';
|
||||||
|
import {
|
||||||
|
BlitzkriegTankDefinition,
|
||||||
|
TANK_ICONS,
|
||||||
|
TANK_ICONS_COLLECTOR,
|
||||||
|
TANK_ICONS_PREMIUM,
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
tanksDefinitionsArray,
|
||||||
|
} from '../../../core/blitzkrieg/definitions/tanks';
|
||||||
|
import { tankIcon } from '../../../core/blitzkrieg/tankIcon';
|
||||||
|
import { theme } from '../../../stitches.config';
|
||||||
|
import mutateTankopedia, {
|
||||||
|
TankopediaSortBy,
|
||||||
|
TankopediaSortDirection,
|
||||||
|
useTankopedia,
|
||||||
|
} from '../../../stores/tankopedia';
|
||||||
|
import { Options } from './components/Options';
|
||||||
|
import { PageTurner } from './components/PageTurner';
|
||||||
|
import * as styles from './page.css';
|
||||||
|
|
||||||
|
const TANKS_PER_PAGE = 3 * 2 ** 3;
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const tankopediaState = useTankopedia();
|
||||||
|
const awaitedTanks = use(tanksDefinitionsArray);
|
||||||
|
const input = useRef<HTMLInputElement>(null);
|
||||||
|
const searchableTanks = useMemo(
|
||||||
|
() =>
|
||||||
|
awaitedTanks
|
||||||
|
.filter(
|
||||||
|
(tank) =>
|
||||||
|
(tankopediaState.filters.tiers.length === 0
|
||||||
|
? true
|
||||||
|
: tankopediaState.filters.tiers.includes(tank.tier)) &&
|
||||||
|
(tankopediaState.filters.types.length === 0
|
||||||
|
? true
|
||||||
|
: tankopediaState.filters.types.includes(tank.type)) &&
|
||||||
|
(tankopediaState.filters.treeTypes.length === 0
|
||||||
|
? true
|
||||||
|
: tankopediaState.filters.treeTypes.includes(tank.tree_type)) &&
|
||||||
|
(tankopediaState.filters.nations.length === 0
|
||||||
|
? true
|
||||||
|
: tankopediaState.filters.nations.includes(tank.nation)) &&
|
||||||
|
(tankopediaState.filters.test === 'include'
|
||||||
|
? true
|
||||||
|
: tankopediaState.filters.test === 'exclude'
|
||||||
|
? !tank.testing
|
||||||
|
: tank.testing),
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
let diff = 0;
|
||||||
|
|
||||||
|
if (tankopediaState.sort.by === 'tier') {
|
||||||
|
diff = a.tier - b.tier;
|
||||||
|
}
|
||||||
|
if (tankopediaState.sort.by === 'name') {
|
||||||
|
diff = a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tankopediaState.sort.direction === 'ascending' ? diff : -diff;
|
||||||
|
}),
|
||||||
|
[tankopediaState.filters, tankopediaState.sort],
|
||||||
|
);
|
||||||
|
const [searchedList, setSearchedList] = useState(searchableTanks);
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchedList(searchableTanks);
|
||||||
|
if (input.current) input.current.value = '';
|
||||||
|
}, [searchableTanks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(
|
||||||
|
Math.min(
|
||||||
|
Math.max(0, page),
|
||||||
|
Math.ceil(searchedList.length / TANKS_PER_PAGE) - 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [searchedList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper size="wide" color="purple">
|
||||||
|
<Suspense fallback={<Text>Loading...</Text>}>
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<Flex gap="2">
|
||||||
|
<TextField.Root style={{ flex: 1 }}>
|
||||||
|
<TextField.Slot>
|
||||||
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
<TextField.Input
|
||||||
|
ref={input}
|
||||||
|
placeholder="Search tanks..."
|
||||||
|
onChange={(event) => {
|
||||||
|
if (event.target.value.length === 0) {
|
||||||
|
setSearchedList(searchableTanks);
|
||||||
|
} else {
|
||||||
|
setSearchedList(
|
||||||
|
go(event.target.value, searchableTanks, {
|
||||||
|
keys: [
|
||||||
|
'name',
|
||||||
|
'id',
|
||||||
|
] satisfies (keyof BlitzkriegTankDefinition)[],
|
||||||
|
}).map(({ obj }) => obj),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TextField.Root>
|
||||||
|
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button variant="soft">
|
||||||
|
Sort
|
||||||
|
<CaretDownIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Label>By</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.RadioGroup
|
||||||
|
value={tankopediaState.sort.by}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
draft.sort.by = value as TankopediaSortBy;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DropdownMenu.RadioItem value="tier">
|
||||||
|
Tier
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="name">
|
||||||
|
Name
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
|
<DropdownMenu.Label>Direction</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.RadioGroup
|
||||||
|
value={tankopediaState.sort.direction}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
draft.sort.direction = value as TankopediaSortDirection;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DropdownMenu.RadioItem value="ascending">
|
||||||
|
Ascending
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="descending">
|
||||||
|
Descending
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => setShowFilters((state) => !state)}
|
||||||
|
>
|
||||||
|
Filters {showFilters ? <CaretUpIcon /> : <CaretDownIcon />}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{showFilters && (
|
||||||
|
<Card>
|
||||||
|
<Suspense fallback={<Text>Loading...</Text>}>
|
||||||
|
<Options />
|
||||||
|
</Suspense>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<PageTurner
|
||||||
|
page={page}
|
||||||
|
setPage={setPage}
|
||||||
|
tanksPerPage={TANKS_PER_PAGE}
|
||||||
|
searchedList={searchedList}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex wrap="wrap" gap="3" justify="center">
|
||||||
|
{searchedList
|
||||||
|
.map((tank) => (
|
||||||
|
<a
|
||||||
|
key={tank.id}
|
||||||
|
href={`/tools/tankopedia/${tank.id}`}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'unset',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card className={styles.listing}>
|
||||||
|
<Inset
|
||||||
|
key={tank.id}
|
||||||
|
style={{
|
||||||
|
minWidth: 256,
|
||||||
|
minHeight: 128,
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={styles.flag}
|
||||||
|
src={asset(`flags/scratched/${tank.nation}.webp`)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
position: 'absolute',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 256,
|
||||||
|
height: 128,
|
||||||
|
}}
|
||||||
|
className={styles.listingImage}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={tankIcon(tank.id)}
|
||||||
|
style={{
|
||||||
|
objectFit: 'contain',
|
||||||
|
objectPosition: 'left center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.listingShadow}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
transition: `box-shadow ${theme.durations.regular}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="between"
|
||||||
|
className={styles.listingLabel}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 8,
|
||||||
|
padding: '0 12px',
|
||||||
|
width: '100%',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex align="center" justify="center" gap="1">
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
(tank.tree_type === 'collector'
|
||||||
|
? TANK_ICONS_COLLECTOR
|
||||||
|
: tank.tree_type === 'premium'
|
||||||
|
? TANK_ICONS_PREMIUM
|
||||||
|
: TANK_ICONS)[tank.type]
|
||||||
|
}
|
||||||
|
style={{ width: '1em', height: '1em' }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
size="4"
|
||||||
|
color={
|
||||||
|
tank.tree_type === 'collector'
|
||||||
|
? 'blue'
|
||||||
|
: tank.tree_type === 'premium'
|
||||||
|
? 'amber'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
weight="medium"
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
tank.tree_type === 'researchable'
|
||||||
|
? slateDark.slate12
|
||||||
|
: undefined,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
maxWidth: 160,
|
||||||
|
display: 'block',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tank.name}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Text size="4" color="gray" highContrast={false}>
|
||||||
|
{TIER_ROMAN_NUMERALS[tank.tier]}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Inset>
|
||||||
|
</Card>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
.slice(page * TANKS_PER_PAGE, (page + 1) * TANKS_PER_PAGE)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<PageTurner
|
||||||
|
page={page}
|
||||||
|
setPage={setPage}
|
||||||
|
tanksPerPage={TANKS_PER_PAGE}
|
||||||
|
searchedList={searchedList}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate';
|
|||||||
|
|
||||||
const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`;
|
const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`;
|
||||||
const DOCS = {
|
const DOCS = {
|
||||||
permissions: 'guide/permissions',
|
|
||||||
embeds: 'guide/embeds',
|
embeds: 'guide/embeds',
|
||||||
introduction: 'guide/introduction',
|
introduction: 'guide/introduction',
|
||||||
invite: 'guide/invite',
|
invite: 'guide/invite',
|
||||||
@@ -14,7 +13,6 @@ const DOCS = {
|
|||||||
creators: 'guide/creators',
|
creators: 'guide/creators',
|
||||||
};
|
};
|
||||||
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
||||||
permissions: 'The permissions needed for the bot to function',
|
|
||||||
embeds: 'How embeds work',
|
embeds: 'How embeds work',
|
||||||
introduction: 'About the bot',
|
introduction: 'About the bot',
|
||||||
invite: 'How to invite the bot',
|
invite: 'How to invite the bot',
|
||||||
|
|||||||
+13
-14
@@ -8,13 +8,12 @@ import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
|
|||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import getTankStats from '../core/blitz/getTankStats';
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
import getTreeType from '../core/blitz/getTreeType';
|
import getTreeType from '../core/blitz/getTreeType';
|
||||||
import resolveTankName from '../core/blitz/resolveTankName';
|
import { tankDefinitions } from '../core/blitzkrieg/definitions/tanks';
|
||||||
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';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
import { tankopedia } from '../core/blitzstars/tankopedia';
|
import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
|
||||||
import addFilterOptions from '../core/discord/addFilterOptions';
|
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
@@ -34,17 +33,17 @@ import resolvePlayerFromCommand, {
|
|||||||
import calculateWN8 from '../core/statistics/calculateWN8';
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
const ROWS_PER_PAGE = 8;
|
const ROWS_PER_PAGE = 8;
|
||||||
const MAX_PAGES = 10;
|
const MAX_PAGES = 10;
|
||||||
|
|
||||||
async function render(
|
export async function renderBreakdown(
|
||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
{ start, end, name }: ResolvedPeriod,
|
{ start, end, name }: ResolvedPeriod,
|
||||||
filters: StatFilters,
|
filters: StatFilters,
|
||||||
) {
|
) {
|
||||||
const awaitedTankopedia = await tankopedia;
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
const awaitedTankAverages = await tankAverages;
|
const awaitedTankAverages = await tankAverages;
|
||||||
const statsInPeriod = await getStatsInPeriod(region, id, start, end);
|
const statsInPeriod = await getStatsInPeriod(region, id, start, end);
|
||||||
const { filteredOrder } = await filterStats(statsInPeriod, filters);
|
const { filteredOrder } = await filterStats(statsInPeriod, filters);
|
||||||
@@ -183,7 +182,7 @@ async function render(
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
filteredOrder.map(async (id, index) => {
|
filteredOrder.map(async (id, index) => {
|
||||||
const tankopediaEntry = awaitedTankopedia[id];
|
const tankDefinition = awaitedTankDefinitions[id];
|
||||||
const current = orderedCurrentStats[index];
|
const current = orderedCurrentStats[index];
|
||||||
const career = orderedCareerStats[index];
|
const career = orderedCareerStats[index];
|
||||||
const currentWN8 = orderedCurrentWN8[index];
|
const currentWN8 = orderedCurrentWN8[index];
|
||||||
@@ -193,9 +192,9 @@ async function render(
|
|||||||
<Breakdown.Row
|
<Breakdown.Row
|
||||||
key={id}
|
key={id}
|
||||||
type="tank"
|
type="tank"
|
||||||
tankType={tankopediaEntry?.type}
|
tankType={tankDefinition?.type}
|
||||||
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
|
treeType={tankDefinition ? await getTreeType(id) : undefined}
|
||||||
title={await resolveTankName(id)}
|
title={awaitedTankDefinitions[id].name}
|
||||||
stats={[
|
stats={[
|
||||||
{
|
{
|
||||||
title: 'Battles',
|
title: 'Battles',
|
||||||
@@ -291,9 +290,9 @@ async function render(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
export const breakdownCommand = new Promise<CommandRegistryPromisable>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addFilterOptions(
|
const command = await addPeriodicFilterOptions(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('breakdown')
|
.setName('breakdown')
|
||||||
.setDescription("A period's breakdown by tanks played"),
|
.setDescription("A period's breakdown by tanks played"),
|
||||||
@@ -317,7 +316,7 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(await render(player, period, filters)),
|
...(await renderBreakdown(player, period, filters)),
|
||||||
buttonPrimary(path, 'Refresh'),
|
buttonPrimary(path, 'Refresh'),
|
||||||
await getBlitzStarsLinkButton(player.region, player.id),
|
await getBlitzStarsLinkButton(player.region, player.id),
|
||||||
];
|
];
|
||||||
@@ -333,7 +332,7 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
|||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
const filters = getFiltersFromButton(interaction);
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await render(player, period, filters);
|
return await renderBreakdown(player, period, filters);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const debugCommand: CommandRegistry = {
|
|||||||
.setName('debug')
|
.setName('debug')
|
||||||
.setDescription('Debug information about the bot'),
|
.setDescription('Debug information about the bot'),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler() {
|
||||||
const currentTime = new Date().getTime();
|
const currentTime = new Date().getTime();
|
||||||
const uptime = currentTime - executionStart;
|
const uptime = currentTime - executionStart;
|
||||||
const list = [
|
const list = [
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { tankDefinitions } from '../core/blitzkrieg/definitions/tanks';
|
||||||
import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
|
import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
|
||||||
import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
|
import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
|
||||||
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
import { tankopedia } from '../core/blitzstars/tankopedia';
|
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import embedPositive from '../core/discord/embedPositive';
|
import embedPositive from '../core/discord/embedPositive';
|
||||||
@@ -66,11 +66,11 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
(await entries.reduce<PossiblyPromise<number>>(
|
(await entries.reduce<PossiblyPromise<number>>(
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
async (accumulator, [tankIdString, stats]) => {
|
||||||
const tankId = parseInt(tankIdString);
|
const tankId = parseInt(tankIdString);
|
||||||
const tankopediaEntry = (await tankopedia)[tankId];
|
const tankDefinition = (await tankDefinitions)[tankId];
|
||||||
|
|
||||||
if (!tankopediaEntry) return accumulator;
|
if (!tankDefinition) return accumulator;
|
||||||
|
|
||||||
const tankTier = tankopediaEntry.tier;
|
const tankTier = tankDefinition.tier;
|
||||||
|
|
||||||
return (await accumulator) + tankTier * stats.battles;
|
return (await accumulator) + tankTier * stats.battles;
|
||||||
},
|
},
|
||||||
@@ -79,9 +79,9 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
(await entries.reduce<PossiblyPromise<number>>(
|
(await entries.reduce<PossiblyPromise<number>>(
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
async (accumulator, [tankIdString, stats]) => {
|
||||||
const tankId = parseInt(tankIdString);
|
const tankId = parseInt(tankIdString);
|
||||||
const tankopediaEntry = (await tankopedia)[tankId];
|
const tankDefinition = (await tankDefinitions)[tankId];
|
||||||
|
|
||||||
if (!tankopediaEntry) return accumulator;
|
if (!tankDefinition) return accumulator;
|
||||||
|
|
||||||
return stats.battles;
|
return stats.battles;
|
||||||
},
|
},
|
||||||
@@ -110,7 +110,7 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
async (accumulator, [tankIdString, stats]) => {
|
async (accumulator, [tankIdString, stats]) => {
|
||||||
const tankId = parseInt(tankIdString);
|
const tankId = parseInt(tankIdString);
|
||||||
|
|
||||||
if ((await tankopedia)[tankId]?.tier === 10) {
|
if ((await tankDefinitions)[tankId]?.tier === 10) {
|
||||||
const tankStats = diffed[tankId];
|
const tankStats = diffed[tankId];
|
||||||
return (await accumulator) + tankStats.damage_dealt;
|
return (await accumulator) + tankStats.damage_dealt;
|
||||||
} else return accumulator;
|
} else return accumulator;
|
||||||
@@ -121,7 +121,7 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
async (accumulator, [tankIdString, stats]) => {
|
async (accumulator, [tankIdString, stats]) => {
|
||||||
const tankId = parseInt(tankIdString);
|
const tankId = parseInt(tankIdString);
|
||||||
|
|
||||||
if ((await tankopedia)[tankId]?.tier === 10) {
|
if ((await tankDefinitions)[tankId]?.tier === 10) {
|
||||||
const tankStats = diffed[tankId];
|
const tankStats = diffed[tankId];
|
||||||
return (await accumulator) + tankStats.battles;
|
return (await accumulator) + tankStats.battles;
|
||||||
} else return accumulator;
|
} else return accumulator;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
|||||||
import resolvePlayerFromCommand, {
|
import resolvePlayerFromCommand, {
|
||||||
ResolvedPlayer,
|
ResolvedPlayer,
|
||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
type EvolutionStatType = 'player' | 'tank';
|
type EvolutionStatType = 'player' | 'tank';
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ async function render(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
export const evolutionCommand = new Promise<CommandRegistryPromisable>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
let playerGroup: SlashCommandSubcommandGroupBuilder;
|
let playerGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
let tankGroup: SlashCommandSubcommandGroupBuilder;
|
let tankGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
|||||||
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';
|
||||||
import addFilterOptions from '../core/discord/addFilterOptions';
|
import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
@@ -29,7 +29,7 @@ import resolvePlayerFromCommand, {
|
|||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import { getTierWeights } from '../core/statistics/getTierWeights';
|
import { getTierWeights } from '../core/statistics/getTierWeights';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
async function render(
|
async function render(
|
||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
@@ -64,9 +64,9 @@ async function render(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addFilterOptions(
|
const command = await addPeriodicFilterOptions(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('full-stats')
|
.setName('full-stats')
|
||||||
.setDescription('Full in-game statistics'),
|
.setDescription('Full in-game statistics'),
|
||||||
|
|||||||
+140
-154
@@ -1,25 +1,21 @@
|
|||||||
import { teal } from '@radix-ui/colors';
|
import { teal } from '@radix-ui/colors';
|
||||||
import {
|
import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js';
|
||||||
AttachmentBuilder,
|
|
||||||
GuildMemberRoleManager,
|
|
||||||
SlashCommandBuilder,
|
|
||||||
} 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/WN8Display/components/Glow';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
import { REGION_NAMES } from '../constants/regions';
|
||||||
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 { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
|
|
||||||
|
|
||||||
export const verifyCommand: CommandRegistry = {
|
export const verifyCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
@@ -30,193 +26,183 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const username = interaction.options.getString('username', true);
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
|
const discordId = parseInt(interaction.user.id);
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
|
||||||
|
|
||||||
if (serverAndIdPattern.test(username)) {
|
await linkBlitzAndDiscord(discordId, region, id);
|
||||||
const [region, idString] = username.split('/');
|
|
||||||
const id = parseInt(idString);
|
|
||||||
const discordId = parseInt(interaction.user.id);
|
|
||||||
const accountInfo = await getAccountInfo(region as Region, id);
|
|
||||||
const clanAccountInfo = await getClanAccountInfo(region as Region, id, [
|
|
||||||
'clan',
|
|
||||||
]);
|
|
||||||
|
|
||||||
await linkBlitzAndDiscord(discordId, region as Region, id);
|
if (interaction.guildId === discord.sklld_guild_id) {
|
||||||
|
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
||||||
if (interaction.guildId === discord.sklld_guild_id) {
|
return embedNegative(
|
||||||
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
`${markdownEscape(interaction.user.username)} failed to verify`,
|
||||||
return embedNegative(
|
"I don't have the permission to change your manage roles.",
|
||||||
`${markdownEscape(interaction.user.username)} failed to verify`,
|
|
||||||
"I don't have the permission to change your manage roles.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await (interaction.member!.roles as GuildMemberRoleManager).remove(
|
|
||||||
discord.sklld_verify_role,
|
|
||||||
);
|
|
||||||
await (interaction.member!.roles as GuildMemberRoleManager).add(
|
|
||||||
discord.sklld_peasant_role,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
await (interaction.member!.roles as GuildMemberRoleManager).remove(
|
||||||
<Wrapper fat>
|
discord.sklld_verify_role,
|
||||||
|
);
|
||||||
|
await (interaction.member!.roles as GuildMemberRoleManager).add(
|
||||||
|
discord.sklld_peasant_role,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
<Wrapper fat>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Glow color={teal.teal9} rotation={90} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'center',
|
||||||
|
gap: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Glow color={teal.teal9} rotation={90} />
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
interaction.user.avatarURL({ extension: 'png' }) ??
|
||||||
|
'https://i.stack.imgur.com/l60Hf.png'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
maxWidth: 240,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{interaction.user.displayName}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
@{interaction.user.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 16,
|
gap: 4,
|
||||||
|
color: theme.colors.textLowContrast_teal,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<img
|
||||||
|
src="https://i.imgur.com/jIcRgog.png"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
width: 16,
|
||||||
gap: 8,
|
height: 16,
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
|
<span style={{ fontSize: 16 }}>Accounts linked</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan ? (
|
||||||
<img
|
<img
|
||||||
src={
|
|
||||||
interaction.user.avatarURL({ extension: 'png' }) ??
|
|
||||||
'https://i.stack.imgur.com/l60Hf.png'
|
|
||||||
}
|
|
||||||
style={{
|
style={{
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
borderRadius: 32,
|
borderRadius: 32,
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
}}
|
}}
|
||||||
|
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 32,
|
|
||||||
color: theme.colors.textHighContrast,
|
|
||||||
fontWeight: 900,
|
|
||||||
maxWidth: 240,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{interaction.user.displayName}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 16,
|
|
||||||
color: theme.colors.textLowContrast,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
@{interaction.user.username}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 4,
|
gap: 4,
|
||||||
color: theme.colors.textLowContrast_teal,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<span
|
||||||
src="https://i.imgur.com/jIcRgog.png"
|
|
||||||
style={{
|
style={{
|
||||||
width: 16,
|
fontSize: 32,
|
||||||
height: 16,
|
color: theme.colors.textHighContrast,
|
||||||
}}
|
fontWeight: 900,
|
||||||
/>
|
maxWidth: 240,
|
||||||
<span style={{ fontSize: 16 }}>Accounts linked</span>
|
overflow: 'hidden',
|
||||||
</div>
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: 8,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{clanAccountInfo?.clan ? (
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
borderRadius: 32,
|
|
||||||
objectFit: 'cover',
|
|
||||||
}}
|
|
||||||
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 4,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
{accountInfo.nickname}
|
||||||
style={{
|
</span>
|
||||||
fontSize: 32,
|
<span
|
||||||
color: theme.colors.textHighContrast,
|
style={{
|
||||||
fontWeight: 900,
|
fontSize: 16,
|
||||||
maxWidth: 240,
|
color: theme.colors.textLowContrast,
|
||||||
overflow: 'hidden',
|
}}
|
||||||
textOverflow: 'ellipsis',
|
>
|
||||||
whiteSpace: 'nowrap',
|
{clanAccountInfo?.clan
|
||||||
}}
|
? `[${clanAccountInfo?.clan?.tag}] - `
|
||||||
>
|
: ''}
|
||||||
{accountInfo.nickname}
|
{REGION_NAMES[region]}
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 16,
|
|
||||||
color: theme.colors.textLowContrast,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{clanAccountInfo?.clan
|
|
||||||
? `[${clanAccountInfo?.clan?.tag}] - `
|
|
||||||
: ''}
|
|
||||||
{REGION_NAMES_SHORT[region as Region]}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Glow color={teal.teal9} rotation={-90} />
|
|
||||||
</div>
|
</div>
|
||||||
</Wrapper>
|
|
||||||
);
|
<Glow color={teal.teal9} rotation={-90} />
|
||||||
} else {
|
</div>
|
||||||
return [
|
</Wrapper>,
|
||||||
embedNegative(
|
|
||||||
'Please select from the options',
|
embedInfo(
|
||||||
'Follow the procedure like the video above. The search results may take a while to show up.',
|
'Incorrect account?',
|
||||||
),
|
'Use the command again and select a username from the search results.',
|
||||||
new AttachmentBuilder('https://i.imgur.com/2p6GFgC.gif'),
|
),
|
||||||
];
|
];
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
autocomplete: autocompleteUsername,
|
||||||
|
|||||||
+23
-23
@@ -3,19 +3,19 @@ import NoData, { NoDataType } from '../components/NoData';
|
|||||||
import * as Tanks from '../components/Tanks';
|
import * as Tanks from '../components/Tanks';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
|
||||||
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 getTankStats from '../core/blitz/getTankStats';
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
import getTreeType from '../core/blitz/getTreeType';
|
import getTreeType from '../core/blitz/getTreeType';
|
||||||
import resolveTankName from '../core/blitz/resolveTankName';
|
|
||||||
import { tankopediaInfo } from '../core/blitz/tankopediaInfo';
|
|
||||||
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
|
||||||
import {
|
import {
|
||||||
|
BlitzkriegTankDefinition,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
TankopediaEntry,
|
|
||||||
Tier,
|
Tier,
|
||||||
tankopedia,
|
tankDefinitions,
|
||||||
} from '../core/blitzstars/tankopedia';
|
} from '../core/blitzkrieg/definitions/tanks';
|
||||||
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
|
import { tankIcon } from '../core/blitzkrieg/tankIcon';
|
||||||
import addTierChoices from '../core/discord/addTierChoices';
|
import addTierChoices from '../core/discord/addTierChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
@@ -41,21 +41,21 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
const filteredTanks = (
|
const filteredTanks = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
tankStats.map(async (tankData) => ({
|
tankStats.map(async (tankData) => ({
|
||||||
tankopedia: (await tankopedia)[tankData.tank_id]!,
|
tankDefinitions: (await tankDefinitions)[tankData.tank_id]!,
|
||||||
tank_id: tankData.tank_id,
|
id: tankData.tank_id,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
).filter((tank) => tank.tankopedia?.tier === tier);
|
).filter((tank) => tank.tankDefinitions?.tier === tier);
|
||||||
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
|
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
|
||||||
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
const groupedTanks: Record<string, BlitzkriegTankDefinition[]> = {};
|
||||||
const nations: string[] = [];
|
const nations: string[] = [];
|
||||||
|
|
||||||
filteredTanks.forEach((tank) => {
|
filteredTanks.forEach((tank) => {
|
||||||
if (groupedTanks[tank.tankopedia.nation] === undefined) {
|
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
|
||||||
groupedTanks[tank.tankopedia.nation] = [tank.tankopedia];
|
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
|
||||||
nations.push(tank.tankopedia.nation);
|
nations.push(tank.tankDefinitions.nation);
|
||||||
} else {
|
} else {
|
||||||
groupedTanks[tank.tankopedia.nation].push(tank.tankopedia);
|
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
return (
|
return (
|
||||||
<Tanks.Root>
|
<Tanks.Root>
|
||||||
<Tanks.Title>
|
<Tanks.Title>
|
||||||
{(await tankopediaInfo).vehicle_nations[nation]}
|
{(await encyclopediaInfo).vehicle_nations[nation]}
|
||||||
</Tanks.Title>
|
</Tanks.Title>
|
||||||
|
|
||||||
<Tanks.Row>
|
<Tanks.Row>
|
||||||
@@ -94,11 +94,11 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
{await Promise.all(
|
{await Promise.all(
|
||||||
leftColumn.map(async (tank) => (
|
leftColumn.map(async (tank) => (
|
||||||
<Tanks.Item
|
<Tanks.Item
|
||||||
key={tank.tank_id}
|
key={tank.id}
|
||||||
name={await resolveTankName(tank.tank_id)}
|
name={tank.name}
|
||||||
tankType={tank.type}
|
tankType={tank.type}
|
||||||
image={tank.images?.normal}
|
image={tankIcon(tank.id)}
|
||||||
treeType={await getTreeType(tank.tank_id)}
|
treeType={await getTreeType(tank.id)}
|
||||||
/>
|
/>
|
||||||
)),
|
)),
|
||||||
)}
|
)}
|
||||||
@@ -107,11 +107,11 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
{await Promise.all(
|
{await Promise.all(
|
||||||
rightColumn.map(async (tank) => (
|
rightColumn.map(async (tank) => (
|
||||||
<Tanks.Item
|
<Tanks.Item
|
||||||
key={tank.tank_id}
|
key={tank.id}
|
||||||
name={await resolveTankName(tank.tank_id)}
|
name={tank.name}
|
||||||
tankType={tank.type}
|
tankType={tank.type}
|
||||||
image={tank.images?.normal}
|
image={tankIcon(tank.id)}
|
||||||
treeType={await getTreeType(tank.tank_id)}
|
treeType={await getTreeType(tank.id)}
|
||||||
/>
|
/>
|
||||||
)),
|
)),
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
|
export const permissionsCommand: CommandRegistry = {
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: new SlashCommandBuilder()
|
||||||
|
.setName('permissions')
|
||||||
|
.setDescription('Checks wether the bot has the required permissions'),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
// friday the 13th permissions easter egg
|
||||||
|
const isFridayThe13th =
|
||||||
|
new Date().getDay() === 5 && new Date().getDate() === 13;
|
||||||
|
const permissions = [
|
||||||
|
[
|
||||||
|
interaction.appPermissions?.has('ViewChannel'),
|
||||||
|
'View channels: needed for the refresh button to work',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
interaction.appPermissions?.has('ReadMessageHistory'),
|
||||||
|
'Read message history: also needed for the refresh button to work',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
interaction.appPermissions?.has('AttachFiles'),
|
||||||
|
'Attach files: needed for image-based commands to work',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
const body = permissions
|
||||||
|
.map(([hasPermission, description]) => {
|
||||||
|
if (hasPermission) {
|
||||||
|
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
|
||||||
|
} else {
|
||||||
|
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return `# Permissions\n${body}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
+164
-192
@@ -2,9 +2,9 @@ import {
|
|||||||
APIApplicationCommandOptionChoice,
|
APIApplicationCommandOptionChoice,
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
SlashCommandSubcommandBuilder,
|
SlashCommandSubcommandBuilder,
|
||||||
SlashCommandSubcommandGroupBuilder,
|
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { range } from 'lodash';
|
import { range } from 'lodash';
|
||||||
|
import markdownEscape from 'markdown-escape';
|
||||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||||
import * as Leaderboard from '../components/Leaderboard';
|
import * as Leaderboard from '../components/Leaderboard';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
@@ -17,17 +17,17 @@ import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
|||||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||||
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
|
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
|
||||||
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
|
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
|
||||||
|
import { isOnGoingRatingsSeason } from '../core/blitz/isOnGoingRatingsSeason';
|
||||||
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
||||||
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
|
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
|
||||||
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
|
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
|
||||||
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
|
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
|
||||||
import { UserError } from '../core/blitzkrieg/userError';
|
import { UserError } from '../core/blitzkrieg/userError';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
export interface RatingsPlayer {
|
export interface RatingsPlayer {
|
||||||
spa_id: number;
|
spa_id: number;
|
||||||
@@ -116,18 +116,22 @@ export interface RatingsNeighbors {
|
|||||||
neighbors: RatingsPlayer[];
|
neighbors: RatingsPlayer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSubcommands(
|
const noOngoingSeason = embedNegative(
|
||||||
option: SlashCommandSubcommandGroupBuilder,
|
'No ongoing season',
|
||||||
addOptions = (option: SlashCommandSubcommandBuilder) => option,
|
"Wargaming didn't provide any data for this season.",
|
||||||
) {
|
);
|
||||||
return option
|
|
||||||
.addSubcommand((option) =>
|
export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
||||||
addOptions(
|
async (resolve) => {
|
||||||
option
|
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
|
||||||
.setName('neighbors')
|
const onGoingSeason = await isOnGoingRatingsSeason();
|
||||||
.setDescription('Your position and neighbors'),
|
const seasonNumbers = range(
|
||||||
)
|
FIRST_ARCHIVED_RATINGS_SEASON,
|
||||||
.addStringOption(addUsernameChoices)
|
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
function addSubcommands(option: SlashCommandSubcommandBuilder) {
|
||||||
|
return option
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('limit')
|
.setName('limit')
|
||||||
@@ -135,77 +139,57 @@ function addSubcommands(
|
|||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
.setMinValue(5)
|
.setMinValue(5)
|
||||||
.setMaxValue(30),
|
.setMaxValue(30),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
addOptions(option.setName('league').setDescription('Top league players'))
|
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('league')
|
.setName('season')
|
||||||
.setDescription('The league to display')
|
.setDescription('Season number')
|
||||||
.addChoices(
|
|
||||||
...LEAGUES.map(
|
|
||||||
({ name }, value) =>
|
|
||||||
({
|
|
||||||
name,
|
|
||||||
value: `${value}`,
|
|
||||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setRequired(true),
|
|
||||||
)
|
|
||||||
.addStringOption(addRegionChoices)
|
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('limit')
|
|
||||||
.setDescription('How many rows to display (default: 10)')
|
|
||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
.setMinValue(5)
|
.addChoices(
|
||||||
.setMaxValue(30),
|
...seasonNumbers
|
||||||
),
|
.map(
|
||||||
);
|
(number, index) =>
|
||||||
}
|
({
|
||||||
|
name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`,
|
||||||
const noOngoingSeason = embedNegative(
|
value: `${number}`,
|
||||||
'No ongoing season',
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
"Wargaming didn't provide any data for this season.",
|
)
|
||||||
);
|
.reverse(),
|
||||||
|
),
|
||||||
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
);
|
||||||
async (resolve) => {
|
}
|
||||||
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
|
|
||||||
const seasonChoices = range(
|
|
||||||
FIRST_ARCHIVED_RATINGS_SEASON,
|
|
||||||
latestArchivedSeasonNumber + 1,
|
|
||||||
).map(
|
|
||||||
(number) =>
|
|
||||||
({
|
|
||||||
name: `Season ${number}`,
|
|
||||||
value: `${number}`,
|
|
||||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const command = new SlashCommandBuilder()
|
const command = new SlashCommandBuilder()
|
||||||
.setName('ratings')
|
.setName('ratings')
|
||||||
.setDescription('Ratings battles statistics')
|
.setDescription('Ratings battles statistics')
|
||||||
.addSubcommandGroup((option) =>
|
.addSubcommand((option) =>
|
||||||
addSubcommands(
|
|
||||||
option.setName('archive').setDescription('Any previous season'),
|
|
||||||
(option) =>
|
|
||||||
option.addStringOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('season')
|
|
||||||
.setDescription('The season number')
|
|
||||||
.setChoices(...seasonChoices)
|
|
||||||
.setRequired(true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addSubcommandGroup((option) =>
|
|
||||||
addSubcommands(
|
addSubcommands(
|
||||||
option
|
option
|
||||||
.setName('current')
|
.setName('neighbors')
|
||||||
.setDescription('The current running Ratings season'),
|
.setDescription('Shows neighboring players on the leaderboard'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.addSubcommand((option) =>
|
||||||
|
addSubcommands(
|
||||||
|
option
|
||||||
|
.setName('league')
|
||||||
|
.setDescription('Shows top players from a league')
|
||||||
|
.addStringOption(addRegionChoices)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('league')
|
||||||
|
.setDescription('The league to display')
|
||||||
|
.addChoices(
|
||||||
|
...LEAGUES.map(
|
||||||
|
({ name }, value) =>
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
value: `${value}`,
|
||||||
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -224,18 +208,19 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
nickname: string;
|
nickname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
|
|
||||||
| 'archive'
|
|
||||||
| 'current';
|
|
||||||
const subcommand = interaction.options.getSubcommand(true) as
|
const subcommand = interaction.options.getSubcommand(true) as
|
||||||
| 'league'
|
| 'league'
|
||||||
| 'neighbors';
|
| 'neighbors';
|
||||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
const limit = interaction.options.getInteger('limit') ?? 10;
|
||||||
const seasonOption = interaction.options.getString('season');
|
const seasonOption = interaction.options.getString('season');
|
||||||
const season = seasonOption ? parseInt(seasonOption) : undefined;
|
const season = seasonOption
|
||||||
|
? parseInt(seasonOption)
|
||||||
|
: onGoingSeason
|
||||||
|
? latestArchivedSeasonNumber + 1
|
||||||
|
: latestArchivedSeasonNumber;
|
||||||
|
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
|
||||||
let titleName: string;
|
let titleName: string;
|
||||||
let titleImage: string | undefined;
|
let titleImage: string | undefined;
|
||||||
let titleNameDiscriminator: string | undefined;
|
|
||||||
let titleDescription: string;
|
let titleDescription: string;
|
||||||
let playersBefore: number;
|
let playersBefore: number;
|
||||||
let playersAfter: number;
|
let playersAfter: number;
|
||||||
@@ -249,69 +234,66 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
interaction.options.getString('league')!,
|
interaction.options.getString('league')!,
|
||||||
);
|
);
|
||||||
const region = interaction.options.getString('region') as Region;
|
const region = interaction.options.getString('region') as Region;
|
||||||
regionRatingsInfo = await (subcommandGroup === 'current'
|
regionRatingsInfo = await (isCurrentSeason
|
||||||
? getRatingsInfo(region)
|
? getRatingsInfo(region)
|
||||||
: getArchivedRatingsInfo(region, season!));
|
: getArchivedRatingsInfo(region, season!));
|
||||||
|
|
||||||
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||||
|
|
||||||
const result =
|
const result = isCurrentSeason
|
||||||
subcommandGroup === 'current'
|
? await getRatingsLeague(region, leagueIndex).then(async (result) =>
|
||||||
? await getRatingsLeague(region, leagueIndex).then(
|
result.slice(0, limit).map(
|
||||||
async (result) =>
|
(player) =>
|
||||||
result.slice(0, limit).map(
|
({
|
||||||
(player) =>
|
id: player.spa_id,
|
||||||
({
|
score: player.score,
|
||||||
id: player.spa_id,
|
position: player.number,
|
||||||
score: player.score,
|
clan: player.clan_tag,
|
||||||
position: player.number,
|
nickname: player.nickname,
|
||||||
clan: player.clan_tag,
|
}) satisfies SimplifiedPlayer,
|
||||||
nickname: player.nickname,
|
),
|
||||||
}) satisfies SimplifiedPlayer,
|
)
|
||||||
),
|
: await getArchivedRatingsLeaderboard(region, season!).then(
|
||||||
)
|
async (players) => {
|
||||||
: await getArchivedRatingsLeaderboard(region, season!).then(
|
const firstPlayerIndex =
|
||||||
async (players) => {
|
leagueIndex === 0
|
||||||
const firstPlayerIndex =
|
? 0
|
||||||
leagueIndex === 0
|
: players.findIndex(
|
||||||
? 0
|
(player) =>
|
||||||
: players.findIndex(
|
player.score < LEAGUES[leagueIndex - 1].minScore,
|
||||||
(player) =>
|
);
|
||||||
player.score < LEAGUES[leagueIndex - 1].minScore,
|
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
||||||
);
|
const trimmed = players.slice(
|
||||||
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
firstPlayerIndex,
|
||||||
const trimmed = players.slice(
|
lastPlayerIndex + 1,
|
||||||
firstPlayerIndex,
|
);
|
||||||
lastPlayerIndex + 1,
|
const clanData = await getClanAccountInfo(
|
||||||
);
|
region,
|
||||||
const clanData = await getClanAccountInfo(
|
trimmed.map(({ id }) => id),
|
||||||
region,
|
['clan'],
|
||||||
trimmed.map(({ id }) => id),
|
);
|
||||||
['clan'],
|
|
||||||
);
|
|
||||||
|
|
||||||
return trimmed.map(
|
return trimmed.map(
|
||||||
(player, index) =>
|
(player, index) =>
|
||||||
({
|
({
|
||||||
id: player.id,
|
id: player.id,
|
||||||
score: player.score,
|
score: player.score,
|
||||||
position: firstPlayerIndex + index + 1,
|
position: firstPlayerIndex + index + 1,
|
||||||
clan: clanData[player.id]?.clan?.tag,
|
clan: clanData[player.id]?.clan?.tag,
|
||||||
nickname:
|
nickname:
|
||||||
clanData[player.id]?.account_name ??
|
clanData[index]?.account_name ??
|
||||||
`Unknown Player ${player.id}`,
|
`Deleted ${player.id}`,
|
||||||
}) satisfies SimplifiedPlayer,
|
}) satisfies SimplifiedPlayer,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
||||||
midnightLeaderboard =
|
midnightLeaderboard = isCurrentSeason
|
||||||
subcommandGroup === 'current'
|
? await getArchivedRatingsMidnightLeaderboard(
|
||||||
? await getArchivedRatingsMidnightLeaderboard(
|
region,
|
||||||
region,
|
regionRatingsInfo.current_season,
|
||||||
regionRatingsInfo.current_season,
|
)
|
||||||
)
|
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
||||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
|
||||||
|
|
||||||
players = result;
|
players = result;
|
||||||
playersBefore = result ? result[0].position - 1 : 0;
|
playersBefore = result ? result[0].position - 1 : 0;
|
||||||
@@ -326,22 +308,17 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
titleDescription = `Top ${limit} players`;
|
titleDescription = `Top ${limit} players`;
|
||||||
} else {
|
} else {
|
||||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
regionRatingsInfo =
|
regionRatingsInfo = isCurrentSeason
|
||||||
subcommandGroup === 'current'
|
? await getRatingsInfo(region)
|
||||||
? await getRatingsInfo(region)
|
: await getArchivedRatingsInfo(region, season!);
|
||||||
: await getArchivedRatingsInfo(region, season!);
|
|
||||||
|
|
||||||
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||||
|
|
||||||
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 neighbors =
|
const neighbors = isCurrentSeason
|
||||||
subcommandGroup === 'current'
|
? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then(
|
||||||
? await getRatingsNeighbors(
|
(players) =>
|
||||||
region,
|
|
||||||
id,
|
|
||||||
Math.round(limit / 2),
|
|
||||||
).then((players) =>
|
|
||||||
players.neighbors.map(
|
players.neighbors.map(
|
||||||
(player) =>
|
(player) =>
|
||||||
({
|
({
|
||||||
@@ -352,52 +329,47 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
nickname: player.nickname,
|
nickname: player.nickname,
|
||||||
}) satisfies SimplifiedPlayer,
|
}) satisfies SimplifiedPlayer,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: await getArchivedRatingsLeaderboard(region, season!).then(
|
: await getArchivedRatingsLeaderboard(region, season!).then(
|
||||||
async (players) => {
|
async (players) => {
|
||||||
const playerIndex = players.findIndex(
|
const playerIndex = players.findIndex(
|
||||||
(player) => player.id === id,
|
(player) => player.id === id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playerIndex === -1) {
|
||||||
|
throw new UserError(
|
||||||
|
`# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (playerIndex === -1) {
|
const halfRange = Math.round(limit / 2);
|
||||||
throw new UserError(
|
const trimmed = players.slice(
|
||||||
`${accountInfo.nickname} didn't player ratings in season ${season}`,
|
playerIndex - halfRange,
|
||||||
{
|
playerIndex + halfRange + 1,
|
||||||
cause:
|
);
|
||||||
'This player did not participate in this season or did not get past calibration.',
|
const clanData = await getClanAccountInfo(
|
||||||
},
|
region,
|
||||||
);
|
trimmed.map((player) => player.id),
|
||||||
}
|
['clan'],
|
||||||
|
);
|
||||||
|
|
||||||
const halfRange = Math.round(limit / 2);
|
return trimmed.map((player, index) => ({
|
||||||
const trimmed = players.slice(
|
id: player.id,
|
||||||
playerIndex - halfRange,
|
score: player.score,
|
||||||
playerIndex + halfRange + 1,
|
position: playerIndex + index + 1,
|
||||||
);
|
clan: clanData[index]?.clan?.tag,
|
||||||
const clanData = await getClanAccountInfo(
|
nickname: clanData[index]
|
||||||
region,
|
? clanData[index]!.account_name
|
||||||
trimmed.map((player) => player.id),
|
: `Deleted ${player.id}`,
|
||||||
['clan'],
|
}));
|
||||||
);
|
},
|
||||||
|
);
|
||||||
return trimmed.map((player, index) => ({
|
midnightLeaderboard = isCurrentSeason
|
||||||
id: player.id,
|
? await getArchivedRatingsMidnightLeaderboard(
|
||||||
score: player.score,
|
region,
|
||||||
position: playerIndex + index + 1,
|
regionRatingsInfo.current_season,
|
||||||
clan: clanData[player.id]!.clan?.tag,
|
)
|
||||||
nickname: clanData[player.id]!.account_name,
|
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
||||||
}));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
midnightLeaderboard =
|
|
||||||
subcommandGroup === 'current'
|
|
||||||
? await getArchivedRatingsMidnightLeaderboard(
|
|
||||||
region,
|
|
||||||
regionRatingsInfo.current_season,
|
|
||||||
)
|
|
||||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
|
||||||
|
|
||||||
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
|
||||||
|
|
||||||
players = neighbors;
|
players = neighbors;
|
||||||
titleImage = clan
|
titleImage = clan
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { TANK_NAMES_DIACRITICS } from '../core/blitzstars/tankopedia';
|
import { tankNamesDiacritics } from '../core/blitzkrieg/definitions/tanks';
|
||||||
import addTankChoices from '../core/discord/addTankChoices';
|
import addTankChoices from '../core/discord/addTankChoices';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
@@ -25,7 +25,7 @@ export const searchTanksCommand: CommandRegistry = {
|
|||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const tank = interaction.options.getString('tank')!;
|
const tank = interaction.options.getString('tank')!;
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
const results = go(tank, await TANK_NAMES_DIACRITICS, {
|
const results = go(tank, await tankNamesDiacritics, {
|
||||||
limit,
|
limit,
|
||||||
keys: ['combined'],
|
keys: ['combined'],
|
||||||
}).map((result) => result.obj.original);
|
}).map((result) => result.obj.original);
|
||||||
|
|||||||
+42
-40
@@ -9,7 +9,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
|||||||
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';
|
||||||
import addFilterOptions from '../core/discord/addFilterOptions';
|
import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
@@ -27,7 +27,7 @@ import resolvePlayerFromCommand, {
|
|||||||
ResolvedPlayer,
|
ResolvedPlayer,
|
||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
async function render(
|
async function render(
|
||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
@@ -57,48 +57,50 @@ async function render(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
export const statsCommand = new Promise<CommandRegistryPromisable>(
|
||||||
const command = await addFilterOptions(
|
async (resolve) => {
|
||||||
new SlashCommandBuilder()
|
const command = await addPeriodicFilterOptions(
|
||||||
.setName('stats')
|
new SlashCommandBuilder()
|
||||||
.setDescription('Regular battles statistics'),
|
.setName('stats')
|
||||||
(option) => option.addStringOption(addUsernameChoices),
|
.setDescription('Regular battles statistics'),
|
||||||
);
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command,
|
command,
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
const filters = await getFiltersFromCommand(interaction);
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
const path = commandToURL(interaction, {
|
const path = commandToURL(interaction, {
|
||||||
...player,
|
...player,
|
||||||
...getCustomPeriodParams(interaction),
|
...getCustomPeriodParams(interaction),
|
||||||
...filters,
|
...filters,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
render(player, period, filters),
|
render(player, period, filters),
|
||||||
buttonRefresh(interaction, path),
|
buttonRefresh(interaction, path),
|
||||||
getBlitzStarsLinkButton(player.region, player.id),
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
autocomplete: (interaction) => {
|
||||||
autocompleteUsername(interaction);
|
autocompleteUsername(interaction);
|
||||||
autocompleteTanks(interaction);
|
autocompleteTanks(interaction);
|
||||||
},
|
},
|
||||||
|
|
||||||
async button(interaction) {
|
async button(interaction) {
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
const filters = getFiltersFromButton(interaction);
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await render(player, period, filters);
|
return await render(player, period, filters);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import { addFilterOptions } from '../core/discord/addFilterOptions';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import buttonPrimary from '../core/discord/buttonPrimary';
|
||||||
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
|
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
||||||
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
import { renderBreakdown } from './breakdown';
|
||||||
|
|
||||||
|
export const todayCommand = new Promise<CommandRegistryPromisable>(
|
||||||
|
async (resolve) => {
|
||||||
|
const awaitedEncyclopediaInfo = await encyclopediaInfo;
|
||||||
|
const command = addFilterOptions(
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('today')
|
||||||
|
.setDescription("Today's played tanks and statistics"),
|
||||||
|
awaitedEncyclopediaInfo,
|
||||||
|
).addStringOption(addUsernameChoices);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command,
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const period = resolvePeriodFromCommand(
|
||||||
|
player.region,
|
||||||
|
interaction,
|
||||||
|
'today',
|
||||||
|
);
|
||||||
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
|
const path = commandToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction, true),
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(await renderBreakdown(player, period, filters)),
|
||||||
|
buttonPrimary(path, 'Refresh'),
|
||||||
|
await getBlitzStarsLinkButton(player.region, player.id),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
|
return await renderBreakdown(player, period, filters);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -37,7 +37,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|||||||
gap: -16,
|
gap: -16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Glow color={PERCENTILE_COLORS[percentile]} />
|
<Glow color={color} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -71,7 +71,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Glow color={PERCENTILE_COLORS[percentile]} direction="reverse" />
|
<Glow color={color} direction="reverse" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
GrayColor,
|
GrayColor,
|
||||||
PALETTES,
|
PALETTES,
|
||||||
} from '../../../constants/radixColors';
|
} from '../../../constants/radixColors';
|
||||||
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
import { TREE_TYPE_ICONS, TankType, TreeTypeEnum } from '../../Tanks';
|
||||||
import { RowStat } from './RowStat';
|
import { RowStat } from './RowStat';
|
||||||
|
|
||||||
export interface RowStatItem {
|
export interface RowStatItem {
|
||||||
@@ -18,10 +18,11 @@ export interface RowStatItem {
|
|||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
type?: 'tank' | 'summary';
|
type?: 'tank' | 'summary';
|
||||||
|
naked?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
minimized?: boolean;
|
minimized?: boolean;
|
||||||
treeType?: TreeTypeEnum;
|
treeType?: TreeTypeEnum;
|
||||||
tankType?: string;
|
tankType?: TankType;
|
||||||
color?: AccentColor | GrayColor;
|
color?: AccentColor | GrayColor;
|
||||||
stats: (RowStatItem | undefined)[];
|
stats: (RowStatItem | undefined)[];
|
||||||
}
|
}
|
||||||
@@ -33,18 +34,23 @@ export function Row({
|
|||||||
minimized,
|
minimized,
|
||||||
tankType,
|
tankType,
|
||||||
treeType,
|
treeType,
|
||||||
|
naked = false,
|
||||||
type,
|
type,
|
||||||
}: RowProps) {
|
}: RowProps) {
|
||||||
const theme = createColors(PALETTES[`${color}Dark`]);
|
const palette = PALETTES[`${color}Dark`];
|
||||||
|
const theme = createColors(palette);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="session-tracker-row"
|
className="session-tracker-row"
|
||||||
style={{
|
style={{
|
||||||
|
maxWidth: naked ? 600 : undefined,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
backgroundColor: theme.appBackground2,
|
backgroundColor: naked
|
||||||
|
? `${theme.appBackground2}c0`
|
||||||
|
: theme.appBackground2,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -55,7 +61,9 @@ export function Row({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
gap: 4,
|
gap: 4,
|
||||||
backgroundColor: theme.componentInteractive,
|
backgroundColor: naked
|
||||||
|
? `${theme.componentInteractive}c0`
|
||||||
|
: theme.componentInteractive,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type === 'tank' && tankType !== undefined && (
|
{type === 'tank' && tankType !== undefined && (
|
||||||
@@ -72,8 +80,8 @@ export function Row({
|
|||||||
? treeType === TreeTypeEnum.Collector
|
? treeType === TreeTypeEnum.Collector
|
||||||
? theme.textLowContrast_blue
|
? theme.textLowContrast_blue
|
||||||
: treeType === TreeTypeEnum.Premium
|
: treeType === TreeTypeEnum.Premium
|
||||||
? theme.textLowContrast_amber
|
? theme.textLowContrast_amber
|
||||||
: theme.textHighContrast
|
: theme.textHighContrast
|
||||||
: theme.textLowContrast,
|
: theme.textLowContrast,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { asset } from '../core/blitzkrieg/asset';
|
||||||
|
|
||||||
|
interface FlagProps {
|
||||||
|
nation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Flag({ nation }: FlagProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={asset(`flags/circle/${nation}.webp`)} alt={nation} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
interface ButtonProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Button({ children }: ButtonProps) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
padding: 16,
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
background: 'none',
|
|
||||||
color: 'inherit',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,72 @@
|
|||||||
|
import { DiscordLogoIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Flex, Text, Theme } from '@radix-ui/themes';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { theme } from '../../stitches.config';
|
import { theme } from '../../stitches.config';
|
||||||
import { BlitzkriegWormWide } from '../BlitzkriegWormWide';
|
import { BlitzkriegWormWide } from '../BlitzkriegWormWide';
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
return (
|
return (
|
||||||
<div
|
<Theme radius="none">
|
||||||
style={{
|
<div
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: theme.colors.textHighContrast,
|
|
||||||
backgroundColor: theme.colors.appBackground2,
|
|
||||||
borderBottom: theme.borderStyles.nonInteractive,
|
|
||||||
height: 48, // TODO: remove this when the icons are implemented
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* <Button>
|
|
||||||
<HamburgerMenuIcon />
|
|
||||||
</Button> */}
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
style={{
|
style={{
|
||||||
color: 'inherit',
|
position: 'fixed',
|
||||||
display: 'flex',
|
top: 0,
|
||||||
alignItems: 'center',
|
left: 0,
|
||||||
justifyContent: 'center',
|
width: '100%',
|
||||||
|
zIndex: 1,
|
||||||
|
padding: '1rem',
|
||||||
|
background: theme.colors.appBackground1_alpha,
|
||||||
|
borderBottom: theme.borderStyles.nonInteractive,
|
||||||
|
backdropFilter: 'blur(4rem)',
|
||||||
|
boxSizing: 'border-box',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BlitzkriegWormWide />
|
<Flex justify="center">
|
||||||
</Link>
|
<Flex
|
||||||
{/* <Button>
|
justify="between"
|
||||||
<PersonIcon />
|
align="center"
|
||||||
</Button> */}
|
style={{ maxWidth: 800, width: '100%', padding: '0 16px' }}
|
||||||
</div>
|
>
|
||||||
|
<Flex justify="center" align="center" gap="2">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
style={{
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BlitzkriegWormWide />
|
||||||
|
</Link>
|
||||||
|
<Text size="2">❤️</Text>
|
||||||
|
<Text size="2">
|
||||||
|
<Link
|
||||||
|
href="https://tresabhi.github.io/"
|
||||||
|
style={{ color: 'inherit', textDecoration: 'none' }}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
by TrèsAbhi
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex justify="center" align="center" gap="2">
|
||||||
|
<Link
|
||||||
|
href="https://discord.gg/nDt7AjGJQH"
|
||||||
|
style={{
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DiscordLogoIcon />
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</Theme>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,28 +3,30 @@ import { ComponentProps } from 'react';
|
|||||||
|
|
||||||
interface PageWrapperProps extends ComponentProps<typeof Flex> {
|
interface PageWrapperProps extends ComponentProps<typeof Flex> {
|
||||||
color?: ComponentProps<typeof Theme>['accentColor'];
|
color?: ComponentProps<typeof Theme>['accentColor'];
|
||||||
|
size?: 'regular' | 'wide';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PageWrapper({
|
export default function PageWrapper({
|
||||||
style,
|
style,
|
||||||
color,
|
color,
|
||||||
|
size = 'regular',
|
||||||
...props
|
...props
|
||||||
}: PageWrapperProps) {
|
}: PageWrapperProps) {
|
||||||
return (
|
return (
|
||||||
<Theme accentColor={color}>
|
<Theme accentColor={color}>
|
||||||
<Flex
|
<Flex
|
||||||
{...props}
|
|
||||||
direction="column"
|
direction="column"
|
||||||
gap="4"
|
gap="4"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: 800,
|
maxWidth: size === 'regular' ? 800 : 960,
|
||||||
margin: 'auto',
|
margin: 'auto',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
|
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</Theme>
|
</Theme>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
amber,
|
||||||
blue,
|
blue,
|
||||||
green,
|
green,
|
||||||
lime,
|
lime,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
red,
|
red,
|
||||||
slate,
|
slate,
|
||||||
teal,
|
teal,
|
||||||
yellow,
|
|
||||||
} from '@radix-ui/colors';
|
} from '@radix-ui/colors';
|
||||||
import { Percentile } from '../constants/percentiles';
|
import { Percentile } from '../constants/percentiles';
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ export const PERCENTILE_COLORS = {
|
|||||||
[Percentile.VeryBad]: slate.slate9,
|
[Percentile.VeryBad]: slate.slate9,
|
||||||
[Percentile.Bad]: red.red9,
|
[Percentile.Bad]: red.red9,
|
||||||
[Percentile.BelowAverage]: orange.orange9,
|
[Percentile.BelowAverage]: orange.orange9,
|
||||||
[Percentile.Average]: yellow.yellow9,
|
[Percentile.Average]: amber.amber10,
|
||||||
[Percentile.AboveAverage]: lime.lime9,
|
[Percentile.AboveAverage]: lime.lime8,
|
||||||
[Percentile.Good]: green.green9,
|
[Percentile.Good]: green.green9,
|
||||||
[Percentile.VeryGood]: teal.teal9,
|
[Percentile.VeryGood]: teal.teal9,
|
||||||
[Percentile.Great]: blue.blue9,
|
[Percentile.Great]: blue.blue9,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
TANK_ICONS,
|
TANK_ICONS,
|
||||||
TANK_ICONS_COLLECTOR,
|
TANK_ICONS_COLLECTOR,
|
||||||
TANK_ICONS_PREMIUM,
|
TANK_ICONS_PREMIUM,
|
||||||
} from '../../../core/blitzstars/tankopedia';
|
} from '../../../core/blitzkrieg/definitions/tanks';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
export enum TreeTypeEnum {
|
export enum TreeTypeEnum {
|
||||||
@@ -11,17 +11,33 @@ export enum TreeTypeEnum {
|
|||||||
Collector,
|
Collector,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TreeTypeString = 'tech-tree' | 'premium' | 'collector';
|
export const TANK_TYPES = [
|
||||||
|
'tank_destroyer',
|
||||||
|
'light',
|
||||||
|
'medium',
|
||||||
|
'heavy',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
|
||||||
|
|
||||||
|
export type TankType = (typeof TANK_TYPES)[number];
|
||||||
|
export type TreeTypeString = (typeof TREE_TYPES)[number];
|
||||||
|
|
||||||
export const TREE_TYPE_NAMES: Record<TreeTypeString, string> = {
|
export const TREE_TYPE_NAMES: Record<TreeTypeString, string> = {
|
||||||
'tech-tree': 'Tech tree',
|
researchable: 'Tech tree',
|
||||||
premium: 'Premium',
|
premium: 'Premium',
|
||||||
collector: 'Collector',
|
collector: 'Collector',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TREE_TYPE_IMAGES: Record<TreeTypeString, string> = {
|
||||||
|
researchable: 'https://i.imgur.com/pJxO2XY.png',
|
||||||
|
premium: 'https://i.imgur.com/mZzSwOU.png',
|
||||||
|
collector: 'https://i.imgur.com/7A0RsG5.png',
|
||||||
|
};
|
||||||
|
|
||||||
export interface ItemProps {
|
export interface ItemProps {
|
||||||
image?: string;
|
image?: string;
|
||||||
tankType?: string;
|
tankType?: TankType;
|
||||||
name: string;
|
name: string;
|
||||||
treeType: TreeTypeEnum;
|
treeType: TreeTypeEnum;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Tier } from '../core/blitzstars/tankopedia';
|
import { Tier } from '../core/blitzkrieg/definitions/tanks';
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export default function TitleBar({ name, image, description }: TitleBarProps) {
|
|||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -51,9 +53,21 @@ export default function TitleBar({ name, image, description }: TitleBarProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
<div
|
||||||
{description}
|
style={{
|
||||||
</span>
|
display: 'flex',
|
||||||
|
justifyContent: image ? 'flex-start' : 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ export const TOOLS: Tool[] = [
|
|||||||
{
|
{
|
||||||
id: 'ratings',
|
id: 'ratings',
|
||||||
title: 'Ratings',
|
title: 'Ratings',
|
||||||
description: 'Current and archived ratings leaderboards',
|
description: 'Ratings gamemode leaderboards',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tankopedia',
|
id: 'tankopedia',
|
||||||
title: 'Tankopedia',
|
title: 'Tankopedia',
|
||||||
description: 'Full Blitz tank encyclopedia',
|
description: 'Blitz tank encyclopedia',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
import { times } from 'lodash';
|
||||||
|
import { readDVPL } from './readDVPL';
|
||||||
|
import { readDVPLFile } from './readDVPLFile';
|
||||||
|
|
||||||
|
enum KAType {
|
||||||
|
NONE = 0,
|
||||||
|
BOOLEAN = 1,
|
||||||
|
INT32 = 2,
|
||||||
|
FLOAT = 3,
|
||||||
|
STRING = 4,
|
||||||
|
WIDE_STRING = 5,
|
||||||
|
BYTE_ARRAY = 6,
|
||||||
|
UINT32 = 7,
|
||||||
|
KEYED_ARCHIVE = 8,
|
||||||
|
INT64 = 9,
|
||||||
|
UINT64 = 10,
|
||||||
|
VECTOR2 = 11,
|
||||||
|
VECTOR3 = 12,
|
||||||
|
VECTOR4 = 13,
|
||||||
|
MATRIX2 = 14,
|
||||||
|
MATRIX3 = 15,
|
||||||
|
MATRIX4 = 16,
|
||||||
|
COLOR = 17,
|
||||||
|
FASTNAME = 18,
|
||||||
|
AABBOX3 = 19,
|
||||||
|
FILEPATH = 20,
|
||||||
|
FLOAT64 = 21,
|
||||||
|
INT8 = 22,
|
||||||
|
UINT8 = 23,
|
||||||
|
INT16 = 24,
|
||||||
|
UINT16 = 25,
|
||||||
|
ARRAY = 27,
|
||||||
|
UNCONFIRMED_TRANSFORM = 29,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VertexType {
|
||||||
|
VERTEX,
|
||||||
|
NORMAL,
|
||||||
|
COLOR,
|
||||||
|
TEXCOORD0,
|
||||||
|
TEXCOORD1,
|
||||||
|
TEXCOORD2,
|
||||||
|
TEXCOORD3,
|
||||||
|
TANGENT,
|
||||||
|
BINORMAL,
|
||||||
|
HARD_JOINTINDEX,
|
||||||
|
PIVOT4,
|
||||||
|
FLEXIBILITY,
|
||||||
|
ANGLE_SIN_COS,
|
||||||
|
JOINTINDEX,
|
||||||
|
JOINTWEIGHT,
|
||||||
|
CUBETEXCOORD0,
|
||||||
|
CUBETEXCOORD1,
|
||||||
|
CUBETEXCOORD2,
|
||||||
|
CUBETEXCOORD3,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PolygonGroupRaw {
|
||||||
|
'##name': 'PolygonGroup';
|
||||||
|
'#id': Buffer;
|
||||||
|
cubeTextureCoordCount: number;
|
||||||
|
indexCount: number;
|
||||||
|
indexFormat: 0 | 1;
|
||||||
|
indices: Buffer;
|
||||||
|
packing: 0 | 1;
|
||||||
|
primitiveCount: number;
|
||||||
|
rhi_primitiveType: 1 | 2 | 10;
|
||||||
|
textureCoordCount: number;
|
||||||
|
vertexCount: number;
|
||||||
|
vertexFormat: number;
|
||||||
|
vertices: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VECTOR_SIZES = [
|
||||||
|
[VertexType.COLOR, VertexType.FLEXIBILITY, VertexType.HARD_JOINTINDEX],
|
||||||
|
[
|
||||||
|
VertexType.TEXCOORD0,
|
||||||
|
VertexType.TEXCOORD1,
|
||||||
|
VertexType.TEXCOORD2,
|
||||||
|
VertexType.TEXCOORD3,
|
||||||
|
VertexType.ANGLE_SIN_COS,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
VertexType.VERTEX,
|
||||||
|
VertexType.NORMAL,
|
||||||
|
VertexType.TANGENT,
|
||||||
|
VertexType.BINORMAL,
|
||||||
|
VertexType.CUBETEXCOORD0,
|
||||||
|
VertexType.CUBETEXCOORD1,
|
||||||
|
VertexType.CUBETEXCOORD2,
|
||||||
|
VertexType.CUBETEXCOORD3,
|
||||||
|
],
|
||||||
|
[VertexType.PIVOT4, VertexType.JOINTINDEX, VertexType.JOINTWEIGHT],
|
||||||
|
];
|
||||||
|
|
||||||
|
export class SCPGStream {
|
||||||
|
public index = 0;
|
||||||
|
constructor(public buffer: Buffer) {}
|
||||||
|
|
||||||
|
skip(size: number) {
|
||||||
|
this.index += size;
|
||||||
|
}
|
||||||
|
back(size: number) {
|
||||||
|
this.skip(-size);
|
||||||
|
}
|
||||||
|
read(size: number) {
|
||||||
|
return this.buffer.subarray(this.index, this.index + size);
|
||||||
|
}
|
||||||
|
consume(size: number) {
|
||||||
|
const subarray = this.read(size);
|
||||||
|
this.skip(size);
|
||||||
|
|
||||||
|
return subarray;
|
||||||
|
}
|
||||||
|
|
||||||
|
readRemaining() {
|
||||||
|
return this.read(Number.POSITIVE_INFINITY);
|
||||||
|
}
|
||||||
|
consumeRemaining() {
|
||||||
|
return this.consume(Number.POSITIVE_INFINITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeByteArray(size: number) {
|
||||||
|
return this.consume(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeAscii(size: number) {
|
||||||
|
return this.consume(size).toString('ascii');
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeInt8() {
|
||||||
|
return this.consume(1).readInt8();
|
||||||
|
}
|
||||||
|
consumeInt16() {
|
||||||
|
return this.consume(2).readInt16LE();
|
||||||
|
}
|
||||||
|
consumeInt32() {
|
||||||
|
return this.consume(4).readInt32LE();
|
||||||
|
}
|
||||||
|
consumeInt64() {
|
||||||
|
return this.consume(8).readBigInt64LE();
|
||||||
|
}
|
||||||
|
consumeUInt8() {
|
||||||
|
return this.consume(1).readUInt8();
|
||||||
|
}
|
||||||
|
consumeUInt16() {
|
||||||
|
return this.consume(2).readUInt16LE();
|
||||||
|
}
|
||||||
|
consumeUInt32() {
|
||||||
|
return this.consume(4).readUInt32LE();
|
||||||
|
}
|
||||||
|
consumeUInt64() {
|
||||||
|
return this.consume(8).readBigUInt64LE();
|
||||||
|
}
|
||||||
|
consumeFloat() {
|
||||||
|
return this.consume(4).readFloatLE();
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeBoolean() {
|
||||||
|
return this.consume(1).readUInt8() === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeVectorN(size: number) {
|
||||||
|
return times(size, () => this.consumeFloat());
|
||||||
|
}
|
||||||
|
consumeVector2() {
|
||||||
|
return this.consumeVectorN(2);
|
||||||
|
}
|
||||||
|
consumeVector3() {
|
||||||
|
return this.consumeVectorN(3);
|
||||||
|
}
|
||||||
|
consumeVector4() {
|
||||||
|
return this.consumeVectorN(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeMatrixN(size: number) {
|
||||||
|
return times(size, () => this.consumeVectorN(size));
|
||||||
|
}
|
||||||
|
consumeMatrix2() {
|
||||||
|
return this.consumeMatrixN(2);
|
||||||
|
}
|
||||||
|
consumeMatrix3() {
|
||||||
|
return this.consumeMatrixN(3);
|
||||||
|
}
|
||||||
|
consumeMatrix4() {
|
||||||
|
return this.consumeMatrixN(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeSCGHeader() {
|
||||||
|
return {
|
||||||
|
name: this.consumeAscii(4),
|
||||||
|
version: this.consumeUInt32(),
|
||||||
|
nodeCount: this.consumeUInt32(),
|
||||||
|
nodeCount2: this.consumeUInt32(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
consumeSCG() {
|
||||||
|
const header = this.consumeSCGHeader();
|
||||||
|
const polygonGroupsRaw: PolygonGroupRaw[] = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < header.nodeCount; index++) {
|
||||||
|
const polygonGroupRaw = this.consumeKA() as PolygonGroupRaw;
|
||||||
|
polygonGroupsRaw.push(polygonGroupRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
const polygonGroups = polygonGroupsRaw.map((polygonGroupRaw) => {
|
||||||
|
const indicesStream = new SCPGStream(polygonGroupRaw.indices);
|
||||||
|
const indices: number[] = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < polygonGroupRaw.indexCount; index++) {
|
||||||
|
indices.push(
|
||||||
|
polygonGroupRaw.indexFormat === 0
|
||||||
|
? indicesStream.consumeUInt16()
|
||||||
|
: indicesStream.consumeUInt32(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const verticesStream = new SCPGStream(polygonGroupRaw.vertices);
|
||||||
|
const vertices: Partial<Record<VertexType, number[]>>[] = [];
|
||||||
|
const vertexFormatBuffer = Buffer.alloc(4);
|
||||||
|
vertexFormatBuffer.writeUInt32LE(polygonGroupRaw.vertexFormat, 0);
|
||||||
|
const vertexFormat = [...vertexFormatBuffer]
|
||||||
|
.map((number) => number.toString(2))
|
||||||
|
.join('')
|
||||||
|
.split('')
|
||||||
|
.map((bitString, index) => (bitString === '1' ? index : null))
|
||||||
|
.filter((type) => type !== null) as VertexType[];
|
||||||
|
|
||||||
|
for (let index = 0; index < polygonGroupRaw.vertexCount; index++) {
|
||||||
|
vertexFormat.forEach((type) => {
|
||||||
|
const resolved = VECTOR_SIZES.some((types, size) => {
|
||||||
|
if (types.includes(type)) {
|
||||||
|
if (!(index in vertices)) vertices[index] = {};
|
||||||
|
vertices[index][type] = verticesStream.consumeVectorN(size + 1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
throw new TypeError(`Unknown vertex type: ${type}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { vertices, indices };
|
||||||
|
});
|
||||||
|
|
||||||
|
return polygonGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeSC2Header() {
|
||||||
|
const header = {
|
||||||
|
name: this.consumeAscii(4),
|
||||||
|
version: this.consumeUInt32(),
|
||||||
|
nodeCount: this.consumeUInt32(),
|
||||||
|
};
|
||||||
|
const versionTags = this.consumeKA();
|
||||||
|
const descriptor = this.consumeSC2Descriptor();
|
||||||
|
|
||||||
|
return { header, versionTags, descriptor };
|
||||||
|
}
|
||||||
|
consumeSC2Descriptor() {
|
||||||
|
const size = this.consumeUInt32();
|
||||||
|
const fileType = this.consumeUInt32();
|
||||||
|
|
||||||
|
// other felids that we don't care about for some reason lol
|
||||||
|
this.skip(size - 4);
|
||||||
|
|
||||||
|
return { size, fileType };
|
||||||
|
}
|
||||||
|
consumeSC2() {
|
||||||
|
this.consumeSC2Header();
|
||||||
|
return this.consumeKA();
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeKAValue(
|
||||||
|
type: KAType = this.consumeUInt8(),
|
||||||
|
stringTable?: Record<number, string>,
|
||||||
|
): unknown {
|
||||||
|
switch (type) {
|
||||||
|
case KAType.NONE:
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
case KAType.FILEPATH:
|
||||||
|
case KAType.STRING: {
|
||||||
|
if (stringTable) {
|
||||||
|
const id = this.consumeUInt32();
|
||||||
|
return stringTable[id];
|
||||||
|
} else {
|
||||||
|
const length = this.consumeUInt32();
|
||||||
|
return this.consumeAscii(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case KAType.BYTE_ARRAY: {
|
||||||
|
const length = this.consumeUInt32();
|
||||||
|
return this.consumeByteArray(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
case KAType.FLOAT:
|
||||||
|
return this.consumeFloat();
|
||||||
|
|
||||||
|
case KAType.ARRAY: {
|
||||||
|
const length = this.consumeUInt32();
|
||||||
|
const value = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < length; index++) {
|
||||||
|
value.push(this.consumeKAValue(undefined, stringTable));
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
case KAType.KEYED_ARCHIVE: {
|
||||||
|
this.skip(4); // UInt32 length of the nested archive in bytes
|
||||||
|
|
||||||
|
return this.consumeKA(stringTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
case KAType.INT8:
|
||||||
|
return this.consumeInt8();
|
||||||
|
case KAType.INT16:
|
||||||
|
return this.consumeInt16();
|
||||||
|
case KAType.INT32:
|
||||||
|
return this.consumeInt32();
|
||||||
|
case KAType.INT64:
|
||||||
|
return this.consumeInt64();
|
||||||
|
case KAType.UINT8:
|
||||||
|
return this.consumeUInt8();
|
||||||
|
case KAType.UINT16:
|
||||||
|
return this.consumeUInt16();
|
||||||
|
case KAType.UINT32:
|
||||||
|
return this.consumeUInt32();
|
||||||
|
case KAType.UINT64:
|
||||||
|
return this.consumeUInt64();
|
||||||
|
|
||||||
|
case KAType.BOOLEAN:
|
||||||
|
return this.consumeBoolean();
|
||||||
|
|
||||||
|
case KAType.VECTOR2:
|
||||||
|
return this.consumeVector2();
|
||||||
|
case KAType.VECTOR3:
|
||||||
|
return this.consumeVector3();
|
||||||
|
case KAType.VECTOR4:
|
||||||
|
return this.consumeVector4();
|
||||||
|
|
||||||
|
case KAType.COLOR:
|
||||||
|
return this.consumeVector4(); // as RGBA
|
||||||
|
|
||||||
|
case KAType.MATRIX2:
|
||||||
|
return this.consumeMatrix2();
|
||||||
|
case KAType.MATRIX3:
|
||||||
|
return this.consumeMatrix3();
|
||||||
|
case KAType.MATRIX4:
|
||||||
|
return this.consumeMatrix4();
|
||||||
|
|
||||||
|
case KAType.AABBOX3: {
|
||||||
|
const minimum = this.consumeVector3();
|
||||||
|
const maximum = this.consumeVector3();
|
||||||
|
|
||||||
|
return { minimum, maximum };
|
||||||
|
}
|
||||||
|
|
||||||
|
case KAType.FASTNAME: {
|
||||||
|
if (!stringTable) throw new Error('No string table provided');
|
||||||
|
const index = this.consumeUInt32();
|
||||||
|
return stringTable[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
case KAType.UNCONFIRMED_TRANSFORM: {
|
||||||
|
const position = this.consumeVector3();
|
||||||
|
const scale = this.consumeVector3();
|
||||||
|
const rotation = this.consumeVector3();
|
||||||
|
const UNCONFIRMED_scalar = this.consumeFloat();
|
||||||
|
|
||||||
|
return {
|
||||||
|
position,
|
||||||
|
scale,
|
||||||
|
rotation,
|
||||||
|
UNCONFIRMED_scalar,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new TypeError(
|
||||||
|
`Unhandled KA type: ${type} (${KAType[type] ?? 'unknown type'})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
consumeKA(stringTable?: Record<number, string>) {
|
||||||
|
this.skip(2); // "KA"
|
||||||
|
const version = this.consumeUInt16();
|
||||||
|
const pairs: Record<string, any> = {};
|
||||||
|
|
||||||
|
if (version === 0x0001) {
|
||||||
|
const count = this.consumeUInt32();
|
||||||
|
|
||||||
|
for (let index = 0; index < count; index++) {
|
||||||
|
const name = this.consumeKAValue(undefined, stringTable) as string;
|
||||||
|
const value = this.consumeKAValue(undefined, stringTable);
|
||||||
|
|
||||||
|
pairs[name] = value;
|
||||||
|
}
|
||||||
|
} else if (version === 0x0002) {
|
||||||
|
const count = this.consumeUInt32();
|
||||||
|
const strings: string[] = [];
|
||||||
|
const stringTable: Record<number, string> = {};
|
||||||
|
|
||||||
|
for (let index = 0; index < count; index++) {
|
||||||
|
const length = this.consumeUInt16();
|
||||||
|
const string = this.consumeAscii(length);
|
||||||
|
|
||||||
|
strings.push(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < count; index++) {
|
||||||
|
const id = this.consumeUInt32();
|
||||||
|
stringTable[id] = strings[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeCount = this.consumeUInt32();
|
||||||
|
|
||||||
|
for (let index = 0; index < nodeCount; index++) {
|
||||||
|
const keyId = this.consumeUInt32();
|
||||||
|
const value = this.consumeKAValue(undefined, stringTable);
|
||||||
|
const key = stringTable[keyId];
|
||||||
|
|
||||||
|
pairs[key] = value;
|
||||||
|
}
|
||||||
|
} else if (version === 0x0102) {
|
||||||
|
if (!stringTable) throw new Error('No string table provided');
|
||||||
|
|
||||||
|
const count = this.consumeUInt32();
|
||||||
|
|
||||||
|
for (let index = 0; index < count; index++) {
|
||||||
|
const keyIndex = this.consumeUInt32();
|
||||||
|
const key = stringTable[keyIndex];
|
||||||
|
const valueType = this.consumeUInt8();
|
||||||
|
|
||||||
|
if (valueType === KAType.STRING) {
|
||||||
|
const valueIndex = this.consumeUInt32();
|
||||||
|
const value = stringTable[valueIndex];
|
||||||
|
pairs[key] = value;
|
||||||
|
} else {
|
||||||
|
const value = this.consumeKAValue(valueType, stringTable);
|
||||||
|
pairs[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (version === 0xff02) {
|
||||||
|
// same as 0x0102 but empty and with no count
|
||||||
|
return {};
|
||||||
|
} else throw new RangeError(`Unhandled KA version: ${version}`);
|
||||||
|
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromDVPL(buffer: Buffer) {
|
||||||
|
return new SCPGStream(readDVPL(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fromDVPLFile(file: string) {
|
||||||
|
return new SCPGStream(await readDVPLFile(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
import fetchBlitz from './fetchBlitz';
|
import fetchBlitz from './fetchBlitz';
|
||||||
|
|
||||||
export interface TankopediaInfo {
|
export interface EncyclopediaInfo {
|
||||||
achievement_sections: Record<string, { name: string; order: number }>;
|
achievement_sections: Record<string, { name: string; order: number }>;
|
||||||
tanks_updated_at: number;
|
tanks_updated_at: number;
|
||||||
languages: Record<string, string>;
|
languages: Record<string, string>;
|
||||||
@@ -10,6 +10,6 @@ export interface TankopediaInfo {
|
|||||||
game_version: string;
|
game_version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tankopediaInfo = fetchBlitz<TankopediaInfo>(
|
export const encyclopediaInfo = fetchBlitz<EncyclopediaInfo>(
|
||||||
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`,
|
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`,
|
||||||
);
|
);
|
||||||
@@ -4,7 +4,11 @@ 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>> = {};
|
||||||
|
|
||||||
export default async function getRatingsInfo(region: Region) {
|
export default async function getRatingsInfo(region: Region) {
|
||||||
|
if (ratingsInfoCache[region]) return ratingsInfoCache[region]!;
|
||||||
|
|
||||||
const response = await patientFetch(
|
const response = await patientFetch(
|
||||||
context === 'website'
|
context === 'website'
|
||||||
? `/api/${region}/ratings/current/info`
|
? `/api/${region}/ratings/current/info`
|
||||||
@@ -14,6 +18,9 @@ export default async function getRatingsInfo(region: Region) {
|
|||||||
undefined,
|
undefined,
|
||||||
{ cache: 'no-store' },
|
{ cache: 'no-store' },
|
||||||
);
|
);
|
||||||
|
const data = (await response.json()) as RatingsInfo;
|
||||||
|
|
||||||
return (await response.json()) as Promise<RatingsInfo>;
|
ratingsInfoCache[region] = data;
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,9 @@ export default async function getTankStats(region: Region, id: number) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (tankStats[id] === null) {
|
if (tankStats[id] === null) {
|
||||||
throw new UserError('No tank stats available', {
|
throw new UserError(
|
||||||
cause:
|
"# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.",
|
||||||
"This player doesn't have any stats for tanks available. This may not be the player you are looking for.",
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tankStats[id];
|
return tankStats[id];
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { TreeTypeEnum } from '../../components/Tanks';
|
import { TreeTypeEnum } from '../../components/Tanks';
|
||||||
import { tankopedia } from '../blitzstars/tankopedia';
|
import { tankDefinitions } from '../blitzkrieg/definitions/tanks';
|
||||||
|
|
||||||
export default async function getTreeType(id: number) {
|
export default async function getTreeType(id: number) {
|
||||||
const entry = (await tankopedia)[id];
|
const entry = (await tankDefinitions)[id];
|
||||||
|
|
||||||
if (entry?.is_collectible) return TreeTypeEnum.Collector;
|
if (entry.tree_type === 'collector') return TreeTypeEnum.Collector;
|
||||||
if (entry?.is_premium) return TreeTypeEnum.Premium;
|
if (entry.tree_type === 'premium') return TreeTypeEnum.Premium;
|
||||||
|
|
||||||
return TreeTypeEnum.TechTree;
|
return TreeTypeEnum.TechTree;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import getRatingsInfo from './getRatingsInfo';
|
||||||
|
|
||||||
|
export async function isOnGoingRatingsSeason() {
|
||||||
|
const ratingsInfo = await getRatingsInfo('com');
|
||||||
|
return ratingsInfo.detail === undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { readDVPLFile } from './readDVPLFile';
|
||||||
|
|
||||||
|
export async function readBase64DVPL(file: string) {
|
||||||
|
const buffer = await readDVPLFile(file);
|
||||||
|
return buffer.toString('base64');
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { crc32 } from 'crc';
|
||||||
|
import { decodeBlock } from 'lz4';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thanks Maddoxkkm! Modernized heavily.
|
||||||
|
*
|
||||||
|
* https://github.com/Maddoxkkm/dvpl_converter/
|
||||||
|
*/
|
||||||
|
export function readDVPL(buffer: Buffer) {
|
||||||
|
const footerBuffer = buffer.subarray(buffer.length - 20, buffer.length);
|
||||||
|
|
||||||
|
if (
|
||||||
|
footerBuffer.toString('utf8', 16, 20) !== 'DVPL' ||
|
||||||
|
footerBuffer.length !== 20
|
||||||
|
) {
|
||||||
|
throw new SyntaxError('Invalid DVPL footer');
|
||||||
|
}
|
||||||
|
|
||||||
|
const footerData = {
|
||||||
|
oSize: footerBuffer.readUInt32LE(0),
|
||||||
|
cSize: footerBuffer.readUInt32LE(4),
|
||||||
|
crc32: footerBuffer.readUInt32LE(8),
|
||||||
|
type: footerBuffer.readUInt32LE(12),
|
||||||
|
};
|
||||||
|
const targetBlock = buffer.subarray(0, buffer.length - 20);
|
||||||
|
|
||||||
|
if (targetBlock.length !== footerData.cSize) {
|
||||||
|
throw new RangeError('DVPL size mismatch');
|
||||||
|
}
|
||||||
|
if (crc32(targetBlock) !== footerData.crc32) {
|
||||||
|
throw new TypeError('DVPL CRC32 mismatch');
|
||||||
|
}
|
||||||
|
if (footerData.type === 0) {
|
||||||
|
if (!(footerData.oSize === footerData.cSize && footerData.type === 0)) {
|
||||||
|
throw new RangeError('DVPL type and size mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetBlock;
|
||||||
|
} else if (footerData.type === 1 || footerData.type === 2) {
|
||||||
|
const deDVPLBlock = Buffer.alloc(footerData.oSize);
|
||||||
|
const DecompressedBlockSize = decodeBlock(targetBlock, deDVPLBlock);
|
||||||
|
|
||||||
|
if (DecompressedBlockSize !== footerData.oSize) {
|
||||||
|
throw new RangeError('Decompressed DVPL size mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
return deDVPLBlock;
|
||||||
|
} else {
|
||||||
|
throw new SyntaxError('Unknown DVPL format');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import { readDVPL } from './readDVPL';
|
||||||
|
|
||||||
|
export async function readDVPLFile(file: string) {
|
||||||
|
return readDVPL(await readFile(file));
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { readDVPLFile } from './readDVPLFile';
|
||||||
|
|
||||||
|
export async function readStringDVPL(file: string) {
|
||||||
|
const buffer = await readDVPLFile(file);
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
|
import { readStringDVPL } from './readStringDVPL';
|
||||||
|
|
||||||
|
export async function readXMLDVPL<Type>(file: string) {
|
||||||
|
const parser = new XMLParser();
|
||||||
|
const xml = await readStringDVPL(file);
|
||||||
|
|
||||||
|
return parser.parse(xml) as Type;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { parse } from 'yaml';
|
||||||
|
import { readStringDVPL } from './readStringDVPL';
|
||||||
|
|
||||||
|
export async function readYAMLDVPL<Type>(file: string) {
|
||||||
|
const yaml = await readStringDVPL(file);
|
||||||
|
|
||||||
|
return parse(yaml) as Type;
|
||||||
|
}
|
||||||
@@ -1,30 +1,34 @@
|
|||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
|
import markdownEscape from 'markdown-escape';
|
||||||
|
import {
|
||||||
|
tankDefinitions,
|
||||||
|
tanksDefinitionsArray,
|
||||||
|
} from '../blitzkrieg/definitions/tanks';
|
||||||
import { UserError } from '../blitzkrieg/userError';
|
import { UserError } from '../blitzkrieg/userError';
|
||||||
import { TANKS, tankopedia } from '../blitzstars/tankopedia';
|
|
||||||
|
|
||||||
export default async function resolveTankId(tank: string | number) {
|
export default async function resolveTankId(tank: string | number) {
|
||||||
const number = typeof tank === 'string' ? parseInt(tank) : tank;
|
const number = typeof tank === 'string' ? parseInt(tank) : tank;
|
||||||
|
|
||||||
if (Number.isNaN(number)) {
|
if (Number.isNaN(number)) {
|
||||||
const searchResult = go(`${tank}`, await TANKS, {
|
const searchResult = go(`${tank}`, await tanksDefinitionsArray, {
|
||||||
keys: ['name'],
|
keys: ['name'],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (searchResult.length === 0) {
|
if (searchResult.length === 0) {
|
||||||
throw new UserError('Tank not found', {
|
throw new UserError(
|
||||||
cause: `Could not find tank by the name "${tank}".`,
|
`# Tank not found\nCould not find tank by the name "${markdownEscape(`${tank}`)}".`,
|
||||||
});
|
);
|
||||||
} else {
|
} else {
|
||||||
return searchResult[0].obj.tank_id;
|
return searchResult[0].obj.id;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ((await tankopedia)[number]) {
|
if ((await tankDefinitions)[number]) {
|
||||||
return number;
|
return number;
|
||||||
} else {
|
} else {
|
||||||
throw new UserError('Tank not found', {
|
throw new UserError(
|
||||||
cause: `Could not find tank by the ID "${number}".`,
|
`# Tank not found\nCould not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`,
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { tankopedia } from '../blitzstars/tankopedia';
|
|
||||||
|
|
||||||
export default async function resolveTankName(tankId: number) {
|
|
||||||
const tankopediaName = (await tankopedia)[tankId]?.name;
|
|
||||||
return tankopediaName ? tankopediaName : `Unknown Tank ${tankId}`;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { NATION_IDS } from '../../constants/nations';
|
||||||
|
|
||||||
|
export function toUniqueId(nation: string, id: number) {
|
||||||
|
return (id << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function asset(path: string) {
|
||||||
|
return `https://raw.githubusercontent.com/tresabhi/blitzkrieg-assets/main/${path}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { octokit } from './octokit';
|
||||||
|
|
||||||
|
const TIME_PER_BLOB = 2 ** 4 * 1000;
|
||||||
|
|
||||||
|
export interface FileChange {
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
encoding: 'utf-8' | 'base64';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function commitMultipleFiles(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
branch: string,
|
||||||
|
message: string,
|
||||||
|
changes: FileChange[],
|
||||||
|
verbose = false,
|
||||||
|
) {
|
||||||
|
const latestCommitSha = (
|
||||||
|
await octokit.git.getRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: `heads/${branch}`,
|
||||||
|
})
|
||||||
|
).data.object.sha;
|
||||||
|
const treeSha = (
|
||||||
|
await octokit.git.getCommit({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
commit_sha: latestCommitSha,
|
||||||
|
})
|
||||||
|
).data.tree.sha;
|
||||||
|
const blobs: {
|
||||||
|
sha: string;
|
||||||
|
path: string;
|
||||||
|
mode: '100644';
|
||||||
|
type: 'blob';
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const changeIndexString in changes) {
|
||||||
|
const changeIndex = parseInt(changeIndexString);
|
||||||
|
const change = changes[changeIndex];
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
try {
|
||||||
|
const { sha } = (
|
||||||
|
await octokit.git.createBlob({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
content: change.content,
|
||||||
|
encoding: change.encoding,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
blobs.push({ sha, path: change.path, mode: '100644', type: 'blob' });
|
||||||
|
done = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Failed blob ${changeIndex + 1} / ${
|
||||||
|
changes.length
|
||||||
|
}; retrying in ${TIME_PER_BLOB}ms...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, TIME_PER_BLOB));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Created blob ${changeIndex + 1} / ${changes.length} (${(
|
||||||
|
(100 * (changeIndex + 1)) /
|
||||||
|
changes.length
|
||||||
|
).toFixed(2)}%)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: treeData } = await octokit.git.createTree({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
base_tree: treeSha,
|
||||||
|
tree: blobs,
|
||||||
|
});
|
||||||
|
const { data: newCommitData } = await octokit.git.createCommit({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
message,
|
||||||
|
tree: treeData.sha,
|
||||||
|
parents: [latestCommitSha],
|
||||||
|
});
|
||||||
|
await octokit.git.updateRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: `heads/${branch}`,
|
||||||
|
sha: newCommitData.sha,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { asset } from '../asset';
|
||||||
|
import { Tier } from './tanks';
|
||||||
|
|
||||||
|
export interface BlitzkriegGunDefinition {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
tier: Tier;
|
||||||
|
pitch: [number, number];
|
||||||
|
shells: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlitzkriegGunDefinitions = Record<number, BlitzkriegGunDefinition>;
|
||||||
|
|
||||||
|
export const gunDefinitions = fetch(asset('definitions/guns.json'), {
|
||||||
|
cache: 'no-cache',
|
||||||
|
}).then(
|
||||||
|
async (response) => response.json() as Promise<BlitzkriegGunDefinitions>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { asset } from '../asset';
|
||||||
|
|
||||||
|
export type ShellType = 'ap' | 'ap_cr' | 'hc' | 'he';
|
||||||
|
|
||||||
|
export interface BlitzkriegShellDefinition {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
speed: number;
|
||||||
|
damage: { armor: number; devices: number };
|
||||||
|
caliber: number;
|
||||||
|
normalization: number;
|
||||||
|
ricochet: number;
|
||||||
|
type: ShellType;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlitzkriegShellDefinitions = Record<
|
||||||
|
number,
|
||||||
|
BlitzkriegShellDefinition
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const shellDefinitions = fetch(asset('definitions/shells.json'), {
|
||||||
|
cache: 'no-cache',
|
||||||
|
}).then(
|
||||||
|
async (response) => response.json() as Promise<BlitzkriegShellDefinitions>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { deburr } from 'lodash';
|
||||||
|
import { TankType, TreeTypeString } from '../../../components/Tanks';
|
||||||
|
import { asset } from '../asset';
|
||||||
|
|
||||||
|
export interface BlitzkriegTankDefinition {
|
||||||
|
id: number;
|
||||||
|
nation: string;
|
||||||
|
name: string;
|
||||||
|
tree_type: TreeTypeString;
|
||||||
|
tier: Tier;
|
||||||
|
type: TankType;
|
||||||
|
testing?: boolean;
|
||||||
|
turrets: number[];
|
||||||
|
}
|
||||||
|
export type BlitzkriegTankDefinitions = Record<
|
||||||
|
number,
|
||||||
|
BlitzkriegTankDefinition
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const tankDefinitions = fetch(asset('definitions/tanks.json'), {
|
||||||
|
cache: 'no-cache',
|
||||||
|
}).then(
|
||||||
|
async (response) => response.json() as Promise<BlitzkriegTankDefinitions>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const entries = new Promise<BlitzkriegTankDefinition[]>(async (resolve) => {
|
||||||
|
resolve(Object.entries(await tankDefinitions).map(([, entry]) => entry));
|
||||||
|
});
|
||||||
|
export const tanksDefinitionsArray = new Promise<BlitzkriegTankDefinition[]>(
|
||||||
|
async (resolve) => {
|
||||||
|
resolve((await entries).map((entry) => entry));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
export const tankNames = new Promise<string[]>(async (resolve) => {
|
||||||
|
resolve((await entries).map(({ name }) => name));
|
||||||
|
});
|
||||||
|
export const tankNamesDiacritics = tankNames.then((tankNames) =>
|
||||||
|
Promise.all(
|
||||||
|
tankNames.map(async (tankName, index) => {
|
||||||
|
const { id } = (await tanksDefinitionsArray)[index];
|
||||||
|
const name = tankName ?? `Unknown Tank ${id}`;
|
||||||
|
const diacriticless = deburr(name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
original: name,
|
||||||
|
diacriticless,
|
||||||
|
combined: `${name}${diacriticless}`,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TANK_ICONS: Record<TankType, string> = {
|
||||||
|
tank_destroyer: 'https://i.imgur.com/BIHSEH0.png',
|
||||||
|
light: 'https://i.imgur.com/CSNha5V.png',
|
||||||
|
medium: 'https://i.imgur.com/wvf3ltm.png',
|
||||||
|
heavy: 'https://i.imgur.com/ECeqlZa.png',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TANK_ICONS_PREMIUM: Record<TankType, string> = {
|
||||||
|
tank_destroyer: 'https://i.imgur.com/TCu3EdR.png',
|
||||||
|
light: 'https://i.imgur.com/zdkpTRb.png',
|
||||||
|
medium: 'https://i.imgur.com/3z7eHX6.png',
|
||||||
|
heavy: 'https://i.imgur.com/P3vbmyA.png',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TANK_ICONS_COLLECTOR: Record<TankType, string> = {
|
||||||
|
tank_destroyer: 'https://i.imgur.com/WTjeirB.png',
|
||||||
|
light: 'https://i.imgur.com/EwhtKkU.png',
|
||||||
|
medium: 'https://i.imgur.com/u8YDMBh.png',
|
||||||
|
heavy: 'https://i.imgur.com/8xRf3nc.png',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const;
|
||||||
|
|
||||||
|
export type Tier = (typeof TIERS)[number];
|
||||||
|
|
||||||
|
export const TIER_ROMAN_NUMERALS: Record<Tier, string> = {
|
||||||
|
1: 'I',
|
||||||
|
2: 'II',
|
||||||
|
3: 'III',
|
||||||
|
4: 'IV',
|
||||||
|
5: 'V',
|
||||||
|
6: 'VI',
|
||||||
|
7: 'VII',
|
||||||
|
8: 'VIII',
|
||||||
|
9: 'IX',
|
||||||
|
10: 'X',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NATIONS = tanksDefinitionsArray.then((tanks) => {
|
||||||
|
const nationsObject: Record<string, true> = {};
|
||||||
|
|
||||||
|
tanks.forEach(({ nation }) => {
|
||||||
|
nationsObject[nation] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(nationsObject);
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { asset } from '../asset';
|
||||||
|
import { Tier } from './tanks';
|
||||||
|
|
||||||
|
export interface BlitzkriegTurretDefinition {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
tier: Tier;
|
||||||
|
yaw: [number, number];
|
||||||
|
guns: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlitzkriegTurretDefinitions = Record<
|
||||||
|
number,
|
||||||
|
BlitzkriegTurretDefinition
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const turretDefinitions = fetch(asset('definitions/turrets.json'), {
|
||||||
|
cache: 'no-cache',
|
||||||
|
}).then(
|
||||||
|
async (response) => response.json() as Promise<BlitzkriegTurretDefinitions>,
|
||||||
|
);
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { TREE_TYPE_NAMES } from '../../components/Tanks';
|
import { TREE_TYPE_NAMES } from '../../components/Tanks';
|
||||||
import resolveTankName from '../blitz/resolveTankName';
|
import { encyclopediaInfo } from '../blitz/encyclopediaInfo';
|
||||||
import { tankopediaInfo } from '../blitz/tankopediaInfo';
|
|
||||||
import { TIER_ROMAN_NUMERALS, Tier } from '../blitzstars/tankopedia';
|
|
||||||
import { StatFilters } from '../statistics/filterStats';
|
import { StatFilters } from '../statistics/filterStats';
|
||||||
|
import {
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
Tier,
|
||||||
|
tankDefinitions,
|
||||||
|
} from './definitions/tanks';
|
||||||
|
|
||||||
export async function filtersToDescription({
|
export async function filtersToDescription({
|
||||||
nation,
|
nation,
|
||||||
@@ -11,13 +14,14 @@ export async function filtersToDescription({
|
|||||||
treeType,
|
treeType,
|
||||||
tank,
|
tank,
|
||||||
}: StatFilters) {
|
}: StatFilters) {
|
||||||
const awaitedTankopediaInfo = await tankopediaInfo;
|
const awaitedEncyclopediaInfo = await encyclopediaInfo;
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
const info: string[] = [];
|
const info: string[] = [];
|
||||||
|
|
||||||
if (tank) return await resolveTankName(tank);
|
if (tank) return awaitedTankDefinitions[tank].name;
|
||||||
if (nation) info.push(awaitedTankopediaInfo.vehicle_nations[nation]);
|
if (nation) info.push(awaitedEncyclopediaInfo.vehicle_nations[nation]);
|
||||||
if (tier) info.push(`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]}`);
|
if (tier) info.push(`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]}`);
|
||||||
if (tankType) info.push(awaitedTankopediaInfo.vehicle_types[tankType]);
|
if (tankType) info.push(awaitedEncyclopediaInfo.vehicle_types[tankType]);
|
||||||
if (treeType) info.push(TREE_TYPE_NAMES[treeType]);
|
if (treeType) info.push(TREE_TYPE_NAMES[treeType]);
|
||||||
|
|
||||||
return info.length === 0 ? 'No filters' : info.join(', ');
|
return info.length === 0 ? 'No filters' : info.join(', ');
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
import getRatingsInfo from '../blitz/getRatingsInfo';
|
import getRatingsInfo from '../blitz/getRatingsInfo';
|
||||||
|
|
||||||
|
interface GitHubTrees {
|
||||||
|
tree: { path: string; url: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
let cachedLatestArchivedSeasonNumber: number | null = null;
|
let cachedLatestArchivedSeasonNumber: number | null = null;
|
||||||
|
|
||||||
export async function getArchivedLatestSeasonNumber() {
|
export async function getArchivedLatestSeasonNumber() {
|
||||||
|
if (cachedLatestArchivedSeasonNumber !== null) {
|
||||||
|
return cachedLatestArchivedSeasonNumber;
|
||||||
|
}
|
||||||
|
|
||||||
const ratingsInfo = await getRatingsInfo('com');
|
const ratingsInfo = await getRatingsInfo('com');
|
||||||
|
|
||||||
if (ratingsInfo.detail) {
|
if (ratingsInfo.detail) {
|
||||||
const comURL = (
|
const regionsURL = (
|
||||||
(await fetch(
|
(await fetch(
|
||||||
'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main/regions',
|
'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main',
|
||||||
).then((response) => response.json())) as {
|
).then((response) => response.json())) as GitHubTrees
|
||||||
tree: { path: string; url: string }[];
|
).tree.find(({ path }) => path === 'regions')!.url;
|
||||||
}
|
const comURL = (
|
||||||
|
(await fetch(regionsURL).then((response) =>
|
||||||
|
response.json(),
|
||||||
|
)) as GitHubTrees
|
||||||
).tree.find(({ path }) => path === 'com')!.url;
|
).tree.find(({ path }) => path === 'com')!.url;
|
||||||
const ratingsURL = (
|
const ratingsURL = (
|
||||||
(await fetch(comURL).then((response) => response.json())) as {
|
(await fetch(comURL).then((response) => response.json())) as {
|
||||||
@@ -20,14 +31,16 @@ export async function getArchivedLatestSeasonNumber() {
|
|||||||
).tree[0].url;
|
).tree[0].url;
|
||||||
const latestSeason = Math.max(
|
const latestSeason = Math.max(
|
||||||
...(
|
...(
|
||||||
(await fetch(ratingsURL).then((response) => {
|
(await fetch(ratingsURL).then((response) => response.json())) as {
|
||||||
response.json();
|
tree: { path: string }[];
|
||||||
})) as { tree: { path: string }[] }
|
}
|
||||||
).tree.map(({ path }) => parseInt(path)),
|
).tree.map(({ path }) => Number(path)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return latestSeason;
|
cachedLatestArchivedSeasonNumber = latestSeason;
|
||||||
} else {
|
} else {
|
||||||
return ratingsInfo.current_season - 1;
|
cachedLatestArchivedSeasonNumber = ratingsInfo.current_season - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cachedLatestArchivedSeasonNumber;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { throttling } from '@octokit/plugin-throttling';
|
||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest';
|
||||||
import { secrets } from './secrets';
|
import { secrets } from './secrets';
|
||||||
|
|
||||||
|
Octokit.plugin(throttling);
|
||||||
|
|
||||||
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
|
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { asset } from './asset';
|
||||||
|
|
||||||
|
export function tankIcon(id: number, size: 'big' | 'small' = 'big') {
|
||||||
|
return asset(`icons/${size}/${id}.webp`);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Discriminator for user caused errors to not report in logs
|
* Discriminator for user caused errors to not report in logs
|
||||||
*/
|
*/
|
||||||
export class UserError extends Error {}
|
export class UserError {
|
||||||
|
constructor(public message: string) {}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AllStats } from '../blitz/getAccountInfo';
|
import { AllStats } from '../blitz/getAccountInfo';
|
||||||
import { context } from '../blitzkrieg/context';
|
import { context } from '../blitzkrieg/context';
|
||||||
import { tankopedia } from './tankopedia';
|
import { tankDefinitions } from '../blitzkrieg/definitions/tanks';
|
||||||
|
|
||||||
export interface SpecialStats {
|
export interface SpecialStats {
|
||||||
winrate: number;
|
winrate: number;
|
||||||
@@ -80,7 +80,7 @@ export interface IndividualTankAverage {
|
|||||||
last_update: number;
|
last_update: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
tankopedia;
|
tankDefinitions;
|
||||||
|
|
||||||
export type TankAverages = Record<number, IndividualTankAverage>;
|
export type TankAverages = Record<number, IndividualTankAverage>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import { deburr } from 'lodash';
|
|
||||||
import { context } from '../blitzkrieg/context';
|
|
||||||
|
|
||||||
export interface TankopediaEntry {
|
|
||||||
name: string;
|
|
||||||
nation: string;
|
|
||||||
is_premium: boolean;
|
|
||||||
is_collectible: boolean;
|
|
||||||
tier: Tier;
|
|
||||||
cost: { price_credit: number; price_gold: number };
|
|
||||||
images: { preview: string; normal: string };
|
|
||||||
tank_id: number;
|
|
||||||
type: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
export interface Tankopedia {
|
|
||||||
[id: number]: TankopediaEntry | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tankopedia = fetch(
|
|
||||||
context === 'website'
|
|
||||||
? '/api/tankopedia'
|
|
||||||
: 'https://www.blitzstars.com/bs-tankopedia.json',
|
|
||||||
)
|
|
||||||
.then(async (response) => response.json())
|
|
||||||
.then((wrapperTankopedia) =>
|
|
||||||
context === 'server'
|
|
||||||
? (wrapperTankopedia as { data: Tankopedia }).data
|
|
||||||
: (wrapperTankopedia as Tankopedia),
|
|
||||||
);
|
|
||||||
const entries = new Promise<TankopediaEntry[]>(async (resolve) => {
|
|
||||||
resolve(Object.entries(await tankopedia).map(([, entry]) => entry));
|
|
||||||
});
|
|
||||||
export const TANKS = new Promise<TankopediaEntry[]>(async (resolve) => {
|
|
||||||
resolve((await entries).map((entry) => entry));
|
|
||||||
});
|
|
||||||
export const TANK_NAMES = new Promise<string[]>(async (resolve) => {
|
|
||||||
resolve((await entries).map(({ name }) => name));
|
|
||||||
});
|
|
||||||
export const TANK_NAMES_DIACRITICS = TANK_NAMES.then((tankNames) =>
|
|
||||||
Promise.all(
|
|
||||||
tankNames.map(async (tankName, index) => {
|
|
||||||
const id = (await TANKS)[index].tank_id;
|
|
||||||
const name = tankName ?? `Unknown Tank ${id}`;
|
|
||||||
const diacriticless = deburr(name);
|
|
||||||
|
|
||||||
return {
|
|
||||||
original: name,
|
|
||||||
diacriticless,
|
|
||||||
combined: `${name}${diacriticless}`,
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const TANK_ICONS: Record<string, string> = {
|
|
||||||
'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
|
|
||||||
lightTank: 'https://i.imgur.com/CSNha5V.png',
|
|
||||||
mediumTank: 'https://i.imgur.com/wvf3ltm.png',
|
|
||||||
heavyTank: 'https://i.imgur.com/ECeqlZa.png',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TANK_ICONS_PREMIUM: Record<string, string> = {
|
|
||||||
'AT-SPG': 'https://i.imgur.com/TCu3EdR.png',
|
|
||||||
lightTank: 'https://i.imgur.com/zdkpTRb.png',
|
|
||||||
mediumTank: 'https://i.imgur.com/3z7eHX6.png',
|
|
||||||
heavyTank: 'https://i.imgur.com/P3vbmyA.png',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TANK_ICONS_COLLECTOR: Record<string, string> = {
|
|
||||||
'AT-SPG': 'https://i.imgur.com/WTjeirB.png',
|
|
||||||
lightTank: 'https://i.imgur.com/EwhtKkU.png',
|
|
||||||
mediumTank: 'https://i.imgur.com/u8YDMBh.png',
|
|
||||||
heavyTank: 'https://i.imgur.com/8xRf3nc.png',
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Tier = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
|
|
||||||
|
|
||||||
export const TIER_ROMAN_NUMERALS: Record<Tier, string> = {
|
|
||||||
1: 'I',
|
|
||||||
2: 'II',
|
|
||||||
3: 'III',
|
|
||||||
4: 'IV',
|
|
||||||
5: 'V',
|
|
||||||
6: 'VI',
|
|
||||||
7: 'VII',
|
|
||||||
8: 'VIII',
|
|
||||||
9: 'IX',
|
|
||||||
10: 'X',
|
|
||||||
};
|
|
||||||
@@ -1,76 +1,60 @@
|
|||||||
import {
|
import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js';
|
||||||
SlashCommandBuilder,
|
|
||||||
SlashCommandSubcommandBuilder,
|
|
||||||
SlashCommandSubcommandGroupBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
|
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
|
||||||
import { tankopediaInfo } from '../blitz/tankopediaInfo';
|
import { EncyclopediaInfo } from '../blitz/encyclopediaInfo';
|
||||||
import addPeriodSubCommands from './addPeriodSubCommands';
|
|
||||||
import addTankChoices from './addTankChoices';
|
import addTankChoices from './addTankChoices';
|
||||||
import addTierChoices from './addTierChoices';
|
import addTierChoices from './addTierChoices';
|
||||||
|
|
||||||
export default async function addFilterOptions<
|
export function addFilterOptions<
|
||||||
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
|
Option extends SlashCommandBuilder | SlashCommandSubcommandBuilder,
|
||||||
>(
|
>(option: Option, encyclopediaInfo: EncyclopediaInfo) {
|
||||||
option: OptionType,
|
return option
|
||||||
extra: (
|
.addStringOption((option) =>
|
||||||
option: SlashCommandSubcommandBuilder,
|
option
|
||||||
) => SlashCommandSubcommandBuilder = (option) => option,
|
.setName('nation')
|
||||||
): Promise<OptionType> {
|
.setDescription('Nation')
|
||||||
const awaitedTankopediaInfo = await tankopediaInfo;
|
.addChoices(
|
||||||
|
...Object.entries(encyclopediaInfo.vehicle_nations).map(
|
||||||
addPeriodSubCommands(option, (option) => {
|
([type, name]) => ({ name, value: type }),
|
||||||
extra(option);
|
),
|
||||||
|
)
|
||||||
return option
|
.setRequired(false),
|
||||||
.addStringOption((option) =>
|
)
|
||||||
option
|
.addStringOption((option) => addTierChoices(option).setRequired(false))
|
||||||
.setName('nation')
|
.addStringOption((option) =>
|
||||||
.setDescription('Nation')
|
option
|
||||||
.addChoices(
|
.setName('tank-type')
|
||||||
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
|
.setDescription('Tank type')
|
||||||
([type, name]) => ({ name, value: type }),
|
.addChoices(
|
||||||
),
|
...Object.entries(encyclopediaInfo.vehicle_types).map(
|
||||||
)
|
([type, name]) => ({
|
||||||
.setRequired(false),
|
name: name.replace(' Tank', ''),
|
||||||
)
|
value: type,
|
||||||
.addStringOption((option) => addTierChoices(option).setRequired(false))
|
}),
|
||||||
.addStringOption((option) =>
|
),
|
||||||
option
|
)
|
||||||
.setName('tank-type')
|
.setRequired(false),
|
||||||
.setDescription('Tank type')
|
)
|
||||||
.addChoices(
|
.addStringOption((option) =>
|
||||||
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
|
option
|
||||||
([type, name]) => ({
|
.setName('tree-type')
|
||||||
name: name.replace(' Tank', ''),
|
.setDescription('Tech tree type')
|
||||||
value: type,
|
.addChoices(
|
||||||
}),
|
{
|
||||||
),
|
name: TREE_TYPE_NAMES['researchable'],
|
||||||
)
|
value: 'researchable' satisfies TreeTypeString,
|
||||||
.setRequired(false),
|
},
|
||||||
)
|
{
|
||||||
.addStringOption((option) =>
|
name: TREE_TYPE_NAMES.premium,
|
||||||
option
|
value: 'premium' satisfies TreeTypeString,
|
||||||
.setName('tree-type')
|
},
|
||||||
.setDescription('Tech tree type')
|
{
|
||||||
.addChoices(
|
name: TREE_TYPE_NAMES.collector,
|
||||||
{
|
value: 'collector' satisfies TreeTypeString,
|
||||||
name: TREE_TYPE_NAMES['tech-tree'],
|
},
|
||||||
value: 'tech-tree' satisfies TreeTypeString,
|
)
|
||||||
},
|
.setRequired(false),
|
||||||
{
|
)
|
||||||
name: TREE_TYPE_NAMES.premium,
|
.addStringOption((option) =>
|
||||||
value: 'premium' satisfies TreeTypeString,
|
addTankChoices(option).setRequired(false),
|
||||||
},
|
) as Option;
|
||||||
{
|
|
||||||
name: TREE_TYPE_NAMES.collector,
|
|
||||||
value: 'collector' satisfies TreeTypeString,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.setRequired(false),
|
|
||||||
)
|
|
||||||
.addStringOption((option) => addTankChoices(option).setRequired(false));
|
|
||||||
});
|
|
||||||
|
|
||||||
return option;
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
SlashCommandBuilder,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
SlashCommandSubcommandGroupBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import { encyclopediaInfo } from '../blitz/encyclopediaInfo';
|
||||||
|
import { addFilterOptions } from './addFilterOptions';
|
||||||
|
import addPeriodSubCommands from './addPeriodSubCommands';
|
||||||
|
|
||||||
|
export default async function addPeriodicFilterOptions<
|
||||||
|
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
|
||||||
|
>(
|
||||||
|
option: OptionType,
|
||||||
|
extra: (
|
||||||
|
option: SlashCommandSubcommandBuilder,
|
||||||
|
) => SlashCommandSubcommandBuilder = (option) => option,
|
||||||
|
): Promise<OptionType> {
|
||||||
|
const awaitedEncyclopediaInfo = await encyclopediaInfo;
|
||||||
|
|
||||||
|
addPeriodSubCommands(option, (option) => {
|
||||||
|
return addFilterOptions(extra(option), awaitedEncyclopediaInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
return option;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
CacheType,
|
CacheType,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import { TANK_NAMES_DIACRITICS } from '../blitzstars/tankopedia';
|
import { tankNamesDiacritics } from '../blitzkrieg/definitions/tanks';
|
||||||
import {
|
import {
|
||||||
DISCORD_CHOICES_MAX_NAME_SIZE,
|
DISCORD_CHOICES_MAX_NAME_SIZE,
|
||||||
OVERFLOW_SUFFIX,
|
OVERFLOW_SUFFIX,
|
||||||
@@ -19,7 +19,7 @@ export default async function autocompleteTanks(
|
|||||||
await interaction.respond(
|
await interaction.respond(
|
||||||
focusedOption.value
|
focusedOption.value
|
||||||
? await Promise.all(
|
? await Promise.all(
|
||||||
go(focusedOption.value, await TANK_NAMES_DIACRITICS, {
|
go(focusedOption.value, await tankNamesDiacritics, {
|
||||||
keys: ['combined'],
|
keys: ['combined'],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
}).map(async (item) => {
|
}).map(async (item) => {
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ import { PeriodType } from './addPeriodSubCommands';
|
|||||||
|
|
||||||
export function getCustomPeriodParams(
|
export function getCustomPeriodParams(
|
||||||
interaction: ChatInputCommandInteraction,
|
interaction: ChatInputCommandInteraction,
|
||||||
|
forcedNonCustomPeriod = false,
|
||||||
) {
|
) {
|
||||||
if ((interaction.options.getSubcommand() as PeriodType) === 'custom')
|
if (
|
||||||
|
!forcedNonCustomPeriod &&
|
||||||
|
(interaction.options.getSubcommand() as PeriodType) === 'custom'
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
start: interaction.options.getInteger('start', true),
|
start: interaction.options.getInteger('start', true),
|
||||||
end: interaction.options.getInteger('end', true),
|
end: interaction.options.getInteger('end', true),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ChatInputCommandInteraction } from 'discord.js';
|
import { ChatInputCommandInteraction } from 'discord.js';
|
||||||
|
import markdownEscape from 'markdown-escape';
|
||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
|
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
|
||||||
import { UserError } from '../blitzkrieg/userError';
|
import { UserError } from '../blitzkrieg/userError';
|
||||||
@@ -18,9 +19,9 @@ export default async function resolveClanFromCommand(
|
|||||||
if (accounts[0]) {
|
if (accounts[0]) {
|
||||||
return { region: accounts[0].region, id: accounts[0].clan_id };
|
return { region: accounts[0].region, id: accounts[0].clan_id };
|
||||||
} else {
|
} else {
|
||||||
throw new UserError('Could not find clan', {
|
throw new UserError(
|
||||||
cause: `I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
|
`# Could not find clan\nCouldn't find "${markdownEscape(clan)}". Try selecting a clan from the search result.`,
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,11 +20,14 @@ export interface ResolvedPeriod {
|
|||||||
export default function resolvePeriodFromCommand(
|
export default function resolvePeriodFromCommand(
|
||||||
region: Region,
|
region: Region,
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
|
forcedPeriod?: PeriodType,
|
||||||
) {
|
) {
|
||||||
let name: string;
|
let name: string;
|
||||||
let start: number;
|
let start: number;
|
||||||
let end: number;
|
let end: number;
|
||||||
const periodSubcommand = interaction.options.getSubcommand() as PeriodType;
|
const periodSubcommand = forcedPeriod
|
||||||
|
? forcedPeriod
|
||||||
|
: (interaction.options.getSubcommand() as PeriodType);
|
||||||
|
|
||||||
if (periodSubcommand === 'custom') {
|
if (periodSubcommand === 'custom') {
|
||||||
const startOption = interaction.options.getInteger('start', true);
|
const startOption = interaction.options.getInteger('start', true);
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ export default async function resolvePlayerFromCommand(
|
|||||||
id: accounts[0].account_id,
|
id: accounts[0].account_id,
|
||||||
} satisfies ResolvedPlayer;
|
} satisfies ResolvedPlayer;
|
||||||
} else {
|
} else {
|
||||||
throw new UserError('Could not find user', {
|
throw new UserError(
|
||||||
cause: `I couldn't find user "${markdownEscape(
|
`# Could not find user\nI couldn't find user "${markdownEscape(
|
||||||
commandUsername,
|
commandUsername,
|
||||||
)}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`,
|
)}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`,
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -47,9 +47,9 @@ export default async function resolvePlayerFromCommand(
|
|||||||
if (account) {
|
if (account) {
|
||||||
return { region: account.region, id: account.blitz };
|
return { region: account.region, id: account.blitz };
|
||||||
} else {
|
} else {
|
||||||
throw new UserError('Use the `/link` command', {
|
throw new UserError(
|
||||||
cause: 'Link your Blitz and Discord accounts to get started.',
|
"# I don't know who you are\nUse the `/link` command to get started.",
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,17 @@ import {
|
|||||||
emptyIndividualTankStats,
|
emptyIndividualTankStats,
|
||||||
} from '../../types/tanksStats';
|
} from '../../types/tanksStats';
|
||||||
|
|
||||||
export function deltaTankStats(a: NormalizedTankStats, b: NormalizedTankStats) {
|
export function deltaTankStats(
|
||||||
|
a: NormalizedTankStats,
|
||||||
|
b: NormalizedTankStats,
|
||||||
|
time?: number,
|
||||||
|
) {
|
||||||
const bTanks = Object.keys(b);
|
const bTanks = Object.keys(b);
|
||||||
const changedTanks = bTanks.filter((id) => {
|
const changedTanks = bTanks.filter((id) => {
|
||||||
return (
|
return time === undefined
|
||||||
a[id as unknown as number]?.all.battles !==
|
? a[id as unknown as number]?.all.battles !==
|
||||||
b[id as unknown as number].all.battles
|
b[id as unknown as number].all.battles
|
||||||
);
|
: b[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 aTank = a[id as unknown as number] ?? emptyIndividualTankStats;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TreeTypeString } from '../../components/Tanks';
|
import { TreeTypeString } from '../../components/Tanks';
|
||||||
|
import { tankDefinitions } from '../blitzkrieg/definitions/tanks';
|
||||||
import { DiffedTankStats } from '../blitzstars/getStatsInPeriod';
|
import { DiffedTankStats } from '../blitzstars/getStatsInPeriod';
|
||||||
import { tankAverages } from '../blitzstars/tankAverages';
|
import { tankAverages } from '../blitzstars/tankAverages';
|
||||||
import { tankopedia } from '../blitzstars/tankopedia';
|
|
||||||
import calculateWN8 from './calculateWN8';
|
import calculateWN8 from './calculateWN8';
|
||||||
import sumStats from './sumStats';
|
import sumStats from './sumStats';
|
||||||
|
|
||||||
@@ -20,10 +20,10 @@ export async function filterStats(
|
|||||||
// tank, if provided, takes priority over all other filters
|
// tank, if provided, takes priority over all other filters
|
||||||
if (filters.tank) filters = { tank: filters.tank };
|
if (filters.tank) filters = { tank: filters.tank };
|
||||||
|
|
||||||
const awaitedTankopedia = await tankopedia;
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
const awaitedTankAverages = await tankAverages;
|
const awaitedTankAverages = await tankAverages;
|
||||||
const filteredOrder = order.filter((id) => {
|
const filteredOrder = order.filter((id) => {
|
||||||
const entry = awaitedTankopedia[id];
|
const entry = awaitedTankDefinitions[id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
entry &&
|
entry &&
|
||||||
@@ -31,14 +31,11 @@ export async function filterStats(
|
|||||||
(filters.tier === undefined || entry.tier === filters.tier) &&
|
(filters.tier === undefined || entry.tier === filters.tier) &&
|
||||||
(filters.tankType === undefined || entry.type === filters.tankType) &&
|
(filters.tankType === undefined || entry.type === filters.tankType) &&
|
||||||
(filters.treeType === undefined ||
|
(filters.treeType === undefined ||
|
||||||
(filters.treeType === 'collector' && entry.is_collectible) ||
|
(filters.treeType === 'collector' && entry.tree_type === 'collector') ||
|
||||||
(filters.treeType === 'premium' &&
|
(filters.treeType === 'premium' && entry.tree_type === 'premium') ||
|
||||||
entry.is_premium &&
|
(filters.treeType === 'researchable' &&
|
||||||
!entry.is_collectible) ||
|
entry.tree_type === 'researchable')) &&
|
||||||
(filters.treeType === 'tech-tree' &&
|
(filters.tank === undefined || entry.id === filters.tank)
|
||||||
!entry.is_collectible &&
|
|
||||||
!entry.is_premium)) &&
|
|
||||||
(filters.tank === undefined || entry.tank_id === filters.tank)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const stats = sumStats(filteredOrder.map((id) => diff[id]));
|
const stats = sumStats(filteredOrder.map((id) => diff[id]));
|
||||||
@@ -47,9 +44,9 @@ export async function filterStats(
|
|||||||
awaitedTankAverages[id] ? accumulator + diff[id].battles : accumulator,
|
awaitedTankAverages[id] ? accumulator + diff[id].battles : accumulator,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const battlesOfTanksWithTankopediaEntry = filteredOrder.reduce<number>(
|
const battlesOfTanksWithTankDefinition = filteredOrder.reduce<number>(
|
||||||
(accumulator, id) =>
|
(accumulator, id) =>
|
||||||
awaitedTankopedia[id] ? accumulator + diff[id].battles : accumulator,
|
awaitedTankDefinitions[id] ? accumulator + diff[id].battles : accumulator,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const supplementary = {
|
const supplementary = {
|
||||||
@@ -66,11 +63,11 @@ export async function filterStats(
|
|||||||
tier:
|
tier:
|
||||||
filteredOrder.reduce<number>(
|
filteredOrder.reduce<number>(
|
||||||
(accumulator, id) =>
|
(accumulator, id) =>
|
||||||
awaitedTankopedia[id]
|
awaitedTankDefinitions[id]
|
||||||
? accumulator + awaitedTankopedia[id]!.tier * diff[id].battles
|
? accumulator + awaitedTankDefinitions[id]!.tier * diff[id].battles
|
||||||
: accumulator,
|
: accumulator,
|
||||||
0,
|
0,
|
||||||
) / battlesOfTanksWithTankopediaEntry,
|
) / battlesOfTanksWithTankDefinition,
|
||||||
};
|
};
|
||||||
|
|
||||||
return { stats, supplementary, filteredOrder };
|
return { stats, supplementary, filteredOrder };
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { TierWeightsRecord } from '../../components/TierWeights';
|
import { TierWeightsRecord } from '../../components/TierWeights';
|
||||||
import { AllStats } from '../blitz/getAccountInfo';
|
import { AllStats } from '../blitz/getAccountInfo';
|
||||||
import { tankopedia } from '../blitzstars/tankopedia';
|
import { tankDefinitions } from '../blitzkrieg/definitions/tanks';
|
||||||
|
|
||||||
export async function getTierWeights(
|
export async function getTierWeights(
|
||||||
stats: Record<number, AllStats>,
|
stats: Record<number, AllStats>,
|
||||||
order: number[] = Object.keys(stats).map(parseInt),
|
order: number[] = Object.keys(stats).map(parseInt),
|
||||||
) {
|
) {
|
||||||
const awaitedTankopedia = await tankopedia;
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
const tierWeights: TierWeightsRecord = {};
|
const tierWeights: TierWeightsRecord = {};
|
||||||
|
|
||||||
order.map((id) => {
|
order.forEach((id) => {
|
||||||
const entry = awaitedTankopedia[id];
|
const entry = awaitedTankDefinitions[id];
|
||||||
|
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
if (!tierWeights[entry.tier]) tierWeights[entry.tier] = 0;
|
if (!tierWeights[entry.tier]) tierWeights[entry.tier] = 0;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { commands } from '..';
|
|||||||
import discord from '../../../../discord.json' assert { type: 'json' };
|
import discord from '../../../../discord.json' assert { type: 'json' };
|
||||||
import { UserError } from '../../../core/blitzkrieg/userError';
|
import { UserError } from '../../../core/blitzkrieg/userError';
|
||||||
import buttonLink from '../../../core/discord/buttonLink';
|
import buttonLink from '../../../core/discord/buttonLink';
|
||||||
import embedNegative from '../../../core/discord/embedNegative';
|
|
||||||
import embedWarning from '../../../core/discord/embedWarning';
|
import embedWarning from '../../../core/discord/embedWarning';
|
||||||
import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable';
|
import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable';
|
||||||
import { psa } from '../../../core/discord/psa';
|
import { psa } from '../../../core/discord/psa';
|
||||||
@@ -19,7 +18,8 @@ import { psa } from '../../../core/discord/psa';
|
|||||||
export default async function handleChatInputCommand(
|
export default async function handleChatInputCommand(
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
) {
|
) {
|
||||||
const registry = (await commands)[interaction.commandName];
|
const awaitedCommands = await commands;
|
||||||
|
const registry = awaitedCommands[interaction.commandName];
|
||||||
|
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|
||||||
@@ -118,25 +118,15 @@ export default async function handleChatInputCommand(
|
|||||||
|
|
||||||
if (error instanceof UserError) {
|
if (error instanceof UserError) {
|
||||||
interaction.editReply({
|
interaction.editReply({
|
||||||
embeds: [
|
content: error.message,
|
||||||
embedNegative(
|
|
||||||
error.message,
|
|
||||||
(error.cause as string | undefined) ??
|
|
||||||
'No further information is available.',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
components,
|
components,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error(interaction.commandName, error);
|
console.error(interaction.commandName, error);
|
||||||
|
|
||||||
interaction.editReply({
|
interaction.editReply({
|
||||||
embeds: [
|
content:
|
||||||
embedNegative(
|
"# Blitzkrieg ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
|
||||||
'Blitzkrieg ran into an error!',
|
|
||||||
"We're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
components,
|
components,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { fullStatsCommand } from '../../commands/fullStats';
|
|||||||
import { inactiveCommand } from '../../commands/inactive';
|
import { inactiveCommand } from '../../commands/inactive';
|
||||||
import { verifyCommand } from '../../commands/link';
|
import { verifyCommand } from '../../commands/link';
|
||||||
import { ownedTanksCommand } from '../../commands/ownedTanks';
|
import { ownedTanksCommand } from '../../commands/ownedTanks';
|
||||||
|
import { permissionsCommand } from '../../commands/permissions';
|
||||||
import { pingCommand } from '../../commands/ping';
|
import { pingCommand } from '../../commands/ping';
|
||||||
import { playerAchievementsCommand } from '../../commands/playerAchievements';
|
import { playerAchievementsCommand } from '../../commands/playerAchievements';
|
||||||
import { playerInfoCommand } from '../../commands/playerInfo';
|
import { playerInfoCommand } from '../../commands/playerInfo';
|
||||||
@@ -31,6 +32,7 @@ import { searchClansCommand } from '../../commands/searchClans';
|
|||||||
import { searchPlayersCommand } from '../../commands/searchPlayers';
|
import { searchPlayersCommand } from '../../commands/searchPlayers';
|
||||||
import { searchTanksCommand } from '../../commands/searchTanks';
|
import { searchTanksCommand } from '../../commands/searchTanks';
|
||||||
import { statsCommand } from '../../commands/stats';
|
import { statsCommand } from '../../commands/stats';
|
||||||
|
import { todayCommand } from '../../commands/today';
|
||||||
import getClientId from '../../core/blitzkrieg/getClientId';
|
import getClientId from '../../core/blitzkrieg/getClientId';
|
||||||
import isDev from '../../core/blitzkrieg/isDev';
|
import isDev from '../../core/blitzkrieg/isDev';
|
||||||
import { secrets } from '../../core/blitzkrieg/secrets';
|
import { secrets } from '../../core/blitzkrieg/secrets';
|
||||||
@@ -52,38 +54,44 @@ export type InteractionReturnable =
|
|||||||
| InteractionIterableReturnable
|
| InteractionIterableReturnable
|
||||||
| Promise<InteractionIterableReturnable>;
|
| Promise<InteractionIterableReturnable>;
|
||||||
|
|
||||||
export interface Registry {
|
type CommandRegistryBase = {
|
||||||
inProduction: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommandRegistryRaw = Registry & {
|
|
||||||
inPublic: boolean;
|
|
||||||
inPreview?: boolean;
|
|
||||||
|
|
||||||
command:
|
command:
|
||||||
| SlashCommandBuilder
|
| SlashCommandBuilder
|
||||||
| SlashCommandSubcommandsOnlyBuilder
|
| SlashCommandSubcommandsOnlyBuilder
|
||||||
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
||||||
|
};
|
||||||
|
type CommandRegistryDefinitionBase = {
|
||||||
|
inProduction: boolean;
|
||||||
|
inPublic: boolean;
|
||||||
|
inPreview?: boolean;
|
||||||
|
|
||||||
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
||||||
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
|
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
|
||||||
} & (
|
};
|
||||||
| {
|
type CommandRegistryDefinitionHandlesInteraction = {
|
||||||
handlesInteraction: true;
|
handlesInteraction: true;
|
||||||
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
|
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
|
||||||
}
|
};
|
||||||
| {
|
type CommandRegistryDefinitionDoesHandleInteraction = {
|
||||||
handlesInteraction?: false;
|
handlesInteraction?: false;
|
||||||
handler: (
|
handler: (
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
) => InteractionReturnable;
|
) => InteractionReturnable;
|
||||||
}
|
};
|
||||||
);
|
export type CommandRegistryPromisable = CommandRegistryBase &
|
||||||
export type CommandRegistry = CommandRegistryRaw | Promise<CommandRegistryRaw>;
|
(CommandRegistryDefinitionBase &
|
||||||
|
(
|
||||||
|
| CommandRegistryDefinitionHandlesInteraction
|
||||||
|
| CommandRegistryDefinitionDoesHandleInteraction
|
||||||
|
));
|
||||||
|
export type CommandRegistry =
|
||||||
|
| CommandRegistryPromisable
|
||||||
|
| Promise<CommandRegistryPromisable>;
|
||||||
|
|
||||||
const rest = new REST().setToken(secrets.DISCORD_TOKEN);
|
const rest = new REST().setToken(secrets.DISCORD_TOKEN);
|
||||||
|
|
||||||
export const COMMANDS_RAW: CommandRegistry[] = [
|
export const COMMANDS_RAW: CommandRegistry[] = [
|
||||||
|
permissionsCommand,
|
||||||
debugCommand,
|
debugCommand,
|
||||||
eligibleCommand,
|
eligibleCommand,
|
||||||
aboutCommand,
|
aboutCommand,
|
||||||
@@ -101,10 +109,11 @@ export const COMMANDS_RAW: CommandRegistry[] = [
|
|||||||
evolutionCommand,
|
evolutionCommand,
|
||||||
statsCommand,
|
statsCommand,
|
||||||
ratingsCommand,
|
ratingsCommand,
|
||||||
|
todayCommand,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
|
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
|
||||||
rawCommands.reduce<Record<string, CommandRegistryRaw>>(
|
rawCommands.reduce<Record<string, CommandRegistryPromisable>>(
|
||||||
(accumulator, registry) => {
|
(accumulator, registry) => {
|
||||||
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
||||||
|
|
||||||
@@ -123,7 +132,7 @@ export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
|||||||
[];
|
[];
|
||||||
|
|
||||||
commands.then((awaitedCommands) => {
|
commands.then((awaitedCommands) => {
|
||||||
Object.entries(awaitedCommands).forEach(([, registry]) => {
|
Object.values(awaitedCommands).forEach((registry) => {
|
||||||
const json = registry.command.toJSON();
|
const json = registry.command.toJSON();
|
||||||
|
|
||||||
if (registry.inPublic) {
|
if (registry.inPublic) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
orangeDark,
|
orangeDark,
|
||||||
slate,
|
slate,
|
||||||
slateDark,
|
slateDark,
|
||||||
|
slateDarkA,
|
||||||
tealDark,
|
tealDark,
|
||||||
} from '@radix-ui/colors';
|
} from '@radix-ui/colors';
|
||||||
import { createBorderStyles, createColors, createSpaces } from 'bepaint';
|
import { createBorderStyles, createColors, createSpaces } from 'bepaint';
|
||||||
@@ -14,6 +15,7 @@ export const theme = {
|
|||||||
|
|
||||||
colors: {
|
colors: {
|
||||||
...createColors(slateDark),
|
...createColors(slateDark),
|
||||||
|
...createColors(slateDarkA, 'alpha'),
|
||||||
...createColors(slate, 'light'),
|
...createColors(slate, 'light'),
|
||||||
|
|
||||||
...createColors(amberDark, 'amber'),
|
...createColors(amberDark, 'amber'),
|
||||||
|
|||||||
+18
-20
@@ -1,6 +1,6 @@
|
|||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn';
|
import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn';
|
||||||
import { AccentColor, GrayColor } from '../constants/radixColors';
|
import { AccentColor, GrayColor } from '../constants/radixColors';
|
||||||
import { Region } from '../constants/regions';
|
import { Region } from '../constants/regions';
|
||||||
@@ -66,26 +66,24 @@ export function resetSession() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useSession = create<Session>()(
|
export const useSession = create<Session>()(
|
||||||
devtools(
|
persist(
|
||||||
persist(
|
(set) => ({
|
||||||
(set) => ({
|
title: 'Untitled session',
|
||||||
title: 'Untitled session',
|
isTracking: false,
|
||||||
isTracking: false,
|
resetPrompt: true,
|
||||||
resetPrompt: true,
|
showTotal: true,
|
||||||
showTotal: true,
|
showCareer: true,
|
||||||
showCareer: true,
|
embedPrompt: true,
|
||||||
embedPrompt: true,
|
|
||||||
|
|
||||||
customColumns: [
|
customColumns: [
|
||||||
{ display: 'battles', showDelta: false },
|
{ display: 'battles', showDelta: false },
|
||||||
{ display: 'winrate', showDelta: true },
|
{ display: 'winrate', showDelta: true },
|
||||||
{ display: 'wn8', showDelta: false },
|
{ display: 'wn8', showDelta: false },
|
||||||
{ display: 'damage', showDelta: true },
|
{ display: 'damage', showDelta: true },
|
||||||
],
|
],
|
||||||
color: 'slate',
|
color: 'slate',
|
||||||
}),
|
}),
|
||||||
{ name: 'session' },
|
{ name: 'session' },
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { produce } from 'immer';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { TankType, TreeTypeString } from '../components/Tanks';
|
||||||
|
import { Tier } from '../core/blitzkrieg/definitions/tanks';
|
||||||
|
|
||||||
|
export type TankopediaSortBy = 'tier' | 'name';
|
||||||
|
export type TankopediaSortDirection = 'ascending' | 'descending';
|
||||||
|
export type TankopediaTestTankDisplay = 'include' | 'exclude' | 'only';
|
||||||
|
|
||||||
|
type Tankopedia = {
|
||||||
|
sort: {
|
||||||
|
by: TankopediaSortBy;
|
||||||
|
direction: TankopediaSortDirection;
|
||||||
|
};
|
||||||
|
filters: {
|
||||||
|
tiers: Tier[];
|
||||||
|
types: TankType[];
|
||||||
|
treeTypes: TreeTypeString[];
|
||||||
|
nations: string[];
|
||||||
|
test: TankopediaTestTankDisplay;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTankopedia = create<Tankopedia>()(() => ({
|
||||||
|
sort: {
|
||||||
|
by: 'tier',
|
||||||
|
direction: 'descending',
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
tiers: [],
|
||||||
|
types: [],
|
||||||
|
treeTypes: [],
|
||||||
|
nations: [],
|
||||||
|
test: 'include',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function mutateTankopedia(recipe: (draft: Tankopedia) => void) {
|
||||||
|
useTankopedia.setState(produce(recipe));
|
||||||
|
}
|
||||||
+14
-3
@@ -10,7 +10,11 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
@@ -20,6 +24,13 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", ".next/types/**/*.ts", "dist/website/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"src/**/*",
|
||||||
|
"scripts/**/*",
|
||||||
|
"dist/website/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user