mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8175aa8715 | ||
|
|
b426634659 | ||
|
|
e92da1b700 | ||
|
|
7e0a2e88ee | ||
|
|
16afa8c2d4 | ||
|
|
907288455a | ||
|
|
64689e4e3b | ||
|
|
294377846a | ||
|
|
70e4cbaba2 | ||
|
|
9e2f9526fb | ||
|
|
84bec20fa5 | ||
|
|
e9a492fde6 | ||
|
|
76a69f08b4 | ||
|
|
893110d3ef | ||
|
|
70575fa9cf | ||
|
|
bb2450eb3c | ||
|
|
ab56689326 | ||
|
|
a9cf07480a | ||
|
|
c81de2d758 | ||
|
|
aa1c7639f2 | ||
|
|
d448101b32 | ||
|
|
e806f0ede2 | ||
|
|
8253c27333 | ||
|
|
254392370f | ||
|
|
d2319688a5 | ||
|
|
ffebe6c696 | ||
|
|
9e4094f3a9 | ||
|
|
f7eadb08a9 | ||
|
|
d511cfa4cc | ||
|
|
270a3051b7 | ||
|
|
81bc9f9ba6 | ||
|
|
338cec4f18 | ||
|
|
b455e3380f | ||
|
|
92761f2c55 | ||
|
|
7823b8ad2e | ||
|
|
cac82f3124 | ||
|
|
4b892d06e4 | ||
|
|
92149b0f00 | ||
|
|
399aa4c03c | ||
|
|
d5de715032 | ||
|
|
e6fb179184 | ||
|
|
af54f12ec3 | ||
|
|
909cabd517 | ||
|
|
a734a71d2f | ||
|
|
91dc45c880 | ||
|
|
91f364b76a | ||
|
|
23dd088466 | ||
|
|
2cabcc57f4 | ||
|
|
dd6092a5e7 | ||
|
|
3c2861f956 | ||
|
|
894b841769 | ||
|
|
62627c96a3 | ||
|
|
c173ece882 | ||
|
|
eca7cbbbeb | ||
|
|
c72bfa9f71 | ||
|
|
b4ae39a509 | ||
|
|
631c051895 | ||
|
|
9a2e11a2a4 | ||
|
|
aa6011b18d | ||
|
|
9419aec116 | ||
|
|
638402589f | ||
|
|
6d6a6bf738 | ||
|
|
31f6123a55 | ||
|
|
18a3e3092c | ||
|
|
5ded0f8744 | ||
|
|
4e0f0536fa | ||
|
|
3f081a5577 | ||
|
|
2447d663fa | ||
|
|
8a0afee97a | ||
|
|
3cca68a8f0 | ||
|
|
622cd92a13 | ||
|
|
6d9e6973d5 | ||
|
|
098e6c09b9 | ||
|
|
186d45e3d9 | ||
|
|
0fdc87a88c | ||
|
|
1d4ac2f133 | ||
|
|
21158e9081 | ||
|
|
fedad8df3e | ||
|
|
6c8f5ca478 | ||
|
|
463af106be | ||
|
|
feeb850ca9 | ||
|
|
80ffff322c | ||
|
|
0833375562 | ||
|
|
297f2f6580 | ||
|
|
438feefa83 | ||
|
|
8f6388b42b | ||
|
|
405f544774 | ||
|
|
dd0644e09f | ||
|
|
d8ac0b2ef6 | ||
|
|
87955214e3 | ||
|
|
f985cbf70b | ||
|
|
f64e72c312 | ||
|
|
ce6715cf2c | ||
|
|
18e5405c54 | ||
|
|
a0f15fc1c4 | ||
|
|
cf20ae38d4 | ||
|
|
2bf5eb78be | ||
|
|
57271e3c23 | ||
|
|
2258b0f936 | ||
|
|
aa8115d260 | ||
|
|
f8dfa386a0 | ||
|
|
00771e130f | ||
|
|
bd612bf777 | ||
|
|
ca208e44a1 | ||
|
|
72c4cd664b | ||
|
|
f907c79d68 | ||
|
|
ae3d0a08cd | ||
|
|
fb66027de0 |
No files matched your search
@@ -0,0 +1,34 @@
|
||||
name: CD Asia Ratings Latest
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --latest
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD Asia Ratings Leaderboards
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 17 * * *"
|
||||
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia
|
||||
@@ -16,18 +16,14 @@ jobs:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: Get code
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD Europe Ratings Latest
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --latest
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD Europe Ratings Leaderboards
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 23 * * *"
|
||||
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Host
|
||||
name: CD Host
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -12,15 +12,12 @@ concurrency:
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 18
|
||||
|
||||
- name: Download release assets
|
||||
run: |
|
||||
@@ -32,4 +29,4 @@ jobs:
|
||||
unzip dist.zip
|
||||
|
||||
- name: Run bot
|
||||
run: NODE_ENV=production DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs
|
||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD North America Ratings Latest
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --latest
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD North America Ratings Leaderboards
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na
|
||||
@@ -11,18 +11,14 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: Get code
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
|
||||
+3
-2
@@ -5,9 +5,10 @@ package-lock.json
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
dist/
|
||||
dist.*
|
||||
yarn-error.log
|
||||
dist.zip
|
||||
commands.txt
|
||||
/test.*
|
||||
/docs/.vitepress/dist
|
||||
/docs/.vitepress/cache
|
||||
/docs/.vitepress/cache
|
||||
servers.csv
|
||||
Vendored
+8
@@ -7,12 +7,16 @@
|
||||
"cleandist",
|
||||
"customperiod",
|
||||
"Everytime",
|
||||
"fullstats",
|
||||
"fuzzysort",
|
||||
"kranvagn",
|
||||
"Leaderboard",
|
||||
"leaderboards",
|
||||
"outfile",
|
||||
"ownedtanks",
|
||||
"playerachievements",
|
||||
"playerinfo",
|
||||
"ratingsleaderboard",
|
||||
"Resvg",
|
||||
"roboto",
|
||||
"searchclans",
|
||||
@@ -22,15 +26,19 @@
|
||||
"Smri",
|
||||
"startcase",
|
||||
"statsfull",
|
||||
"Stringifying",
|
||||
"strvk",
|
||||
"subnation",
|
||||
"tankaverages",
|
||||
"tankopedia",
|
||||
"tankstats",
|
||||
"tresabhi",
|
||||
"Unicum",
|
||||
"Unregistering",
|
||||
"vickers",
|
||||
"vitepress",
|
||||
"wargaming",
|
||||
"webp",
|
||||
"winrate"
|
||||
],
|
||||
"deno.enable": false,
|
||||
|
||||
+11
-11
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// https://vitepress.dev/guide/custom-theme
|
||||
import { h } from 'vue'
|
||||
import Theme from 'vitepress/theme'
|
||||
import './style.css'
|
||||
import { h } from 'vue';
|
||||
import Theme from 'vitepress/theme';
|
||||
import './style.css';
|
||||
|
||||
export default {
|
||||
extends: Theme,
|
||||
Layout: () => {
|
||||
return h(Theme.Layout, null, {
|
||||
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||
})
|
||||
});
|
||||
},
|
||||
enhanceApp({ app, router, siteData }) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -7,7 +7,7 @@
|
||||
* Colors
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
:root {
|
||||
--vp-c-brand: #646cff;
|
||||
--vp-c-brand-light: #747bff;
|
||||
--vp-c-brand-lighter: #9499ff;
|
||||
@@ -88,4 +88,3 @@
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand) !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Blitzkrieg 1.10.0
|
||||
|
||||
This update addresses many readability issues that users have reported over time.
|
||||
|
||||
## Changes
|
||||
|
||||
- `/ownedtanks`
|
||||
- Tanks are now displayed in two columns
|
||||
- Collector and premium tanks now have their own colors
|
||||
- Tank types are also displayed
|
||||
- A stylistic image of the tank is also displayed for each tank to the right
|
||||
- `/fullstats`
|
||||
- Was previously called `/statsfull`; this was changed to avoid the clutter that would show up when searching for `/stats`
|
||||
- Added four catagories in which all the stats are sorted into
|
||||
- Catagories now have backgrounds to compartmentalize them
|
||||
- `/evolution`
|
||||
- Gave the axis markings a high contrast color
|
||||
- Added a label for the x axis affirming that it is plotted over the number of battles
|
||||
- `/today`
|
||||
- Added a border to all the rows to compartmentalize them
|
||||
- This command can now generate embeds too
|
||||
- Removed survivability
|
||||
- Added a limit option to cut off after a certain number of tanks played (this is mainly intended to be used by streamers to limit the content of the embeds)
|
||||
- Tanks are now displayed in order of the most recent battle
|
||||
- `/help`
|
||||
- A significant amount of subcommands were added
|
||||
- These new subcommands now return normal Discord messages instead of embeds in favor of readability
|
||||
- The contents of the replies are directly fetched from the Blitzkrieg documentation
|
||||
- Time zones
|
||||
- What is considered a "day's worth of battles" has been adjusted as the "reset time" was the same for all Blitz servers
|
||||
- The reset time is now dependent on the server of the player
|
||||
- Use `/help timezones` to learn more
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- All themes have been merged to leverage Bepaint's suffixes
|
||||
- Switched to Node module resolution
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.10.1
|
||||
|
||||
This is a bit of an experiment.
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Every midnight in the Central North American Time, a permanent record of the entire ratings leaderboard will be stored for North America
|
||||
@@ -0,0 +1,10 @@
|
||||
# Blitzkrieg 1.10.2
|
||||
|
||||
The experiment in Blitzkrieg `1.10.1` was a bit of a failure so here are a few changes.
|
||||
|
||||
## Fixes
|
||||
|
||||
- North American leaderboards are now saved in the `na` directory instead of `asia`
|
||||
- Dependencies are now installed before the script is ran
|
||||
- Added leaderboards for all regions
|
||||
- All regions now have their own separate cron timings
|
||||
@@ -0,0 +1,22 @@
|
||||
# Blitzkrieg 1.11.0
|
||||
|
||||
I am going on vacation so I thought I'd publish this a bit prematurely without all the planned features.
|
||||
|
||||
## Changes
|
||||
|
||||
- Added `/ratings`
|
||||
- This command currently only has the `leagues` subcommand group
|
||||
- Displays the top players in a league
|
||||
- Also shows the changes in their positions and scores since last region midnight
|
||||
- Use `/help timezones` to know when Blitzkrieg records the leaderboard
|
||||
- Blitzkrieg now internally generates and keeps a permanent record of all leaderboards updated every `UTC+0` midnight
|
||||
- This approach will allow us to capture the absolute latest state of the leaderboard before it is wiped for the next season
|
||||
|
||||
## Fixes
|
||||
|
||||
- Removed region "Europe" in `/verify` example
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Add scripts were upgraded to TypeScript
|
||||
- Secrets now only throw errors if they're used when not provided
|
||||
@@ -0,0 +1,54 @@
|
||||
# Blitzkrieg 1.12.0
|
||||
|
||||
_Houston, permission to return to Earth? A new update dropped._
|
||||
|
||||
## Features
|
||||
|
||||
- Added `multi-tank` filters to `/stats` and `/full-stats`
|
||||
- This lets you pull up the stats by combining filters of `nation`, `tank-type`, `tier`, and `tree-type`
|
||||
- Added `/ratings neighbours`
|
||||
- Lets you see your rank and the neighbours around you
|
||||
- Added a bunch more `/today` parameters (thank you for the suggestions, HisRoyalFatness!)
|
||||
- `cutoff`: the max number of rows to show (excluding the total row)
|
||||
- `maximized`: the max number of rows that show the full non-collapsed stats (default: `4`)
|
||||
- `show-total`: show the total row or not (default: `true`)
|
||||
- Tank search is now accent-tolerant
|
||||
|
||||
## Changes
|
||||
|
||||
- `/ratings` has been moved out of preview and is now accessible to all
|
||||
- `/today` now has a more appealing look with more efficient space management
|
||||
- `/stats`, `/full-stats`, and `/evolution` now have less convoluted commands
|
||||
- All periods now fall under `period` and `custom`
|
||||
- `period` has preset periods like `today`, `career`, `30`, `60`, `90`
|
||||
- `custom` lets you set a start and end day
|
||||
- Added dashes to command names, subcommands, options, etc. to make them more readable
|
||||
- Titlebar
|
||||
- Removed the Blitzkrieg logo
|
||||
- Removed region
|
||||
- Center aligned items
|
||||
- Footers are now center alinged when they do no have footer text on the left
|
||||
- Moved several title discriminators into the description
|
||||
- `/ratings` is now data-driven
|
||||
- Embeds now auto refresh every `30` seconds instead of `60`
|
||||
- Added numbers besides roman numerals in tier choice
|
||||
- I forgot to implement this in places other than `/owned-tanks`... oh well!
|
||||
- Increased precision of winrate in `/evolution` to `2` decimals
|
||||
|
||||
## Fixes
|
||||
|
||||
- Removed duplicate "Efficiency" cards in `/full-stats`
|
||||
- Titlebar gap is now `4px` just like Figma
|
||||
- Renamed `/statsfull` to `/full-stats` in the `/stats` footer
|
||||
- Rewards in `/ratings` are now properly aligned
|
||||
- `/ratings` now uses the correct source of information per region instead of getting it from a cached North American source
|
||||
- Search parameters are now passed onto the URL parser correctly
|
||||
- Custom date ranges now work correctly once again as the start is now the maximum days ago from today and the end is the minimum
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- All generated images are now `480px` wide instead of `640px` to combat the unnecessary amounts of negative space
|
||||
- Changed all relevant typings to be sourced from `stats` instead of `full-stats`
|
||||
- Added minimum character limit to clan search
|
||||
- Username auto-completing now only takes in regex-accepted parts of the input
|
||||
- Search parameters are now longer automatically generated by inferring command interactions
|
||||
@@ -0,0 +1,8 @@
|
||||
# Blitzkrieg 1.12.1
|
||||
|
||||
A duo of nasty bugs fixed.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fixed a tank index and id mismatch
|
||||
- Reverted username pattern infrence in command username inputs
|
||||
@@ -0,0 +1,12 @@
|
||||
# Blitzkrieg 1.9.4
|
||||
|
||||
Haha, I lied. More fixes.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Tank names once again show when searching for tank stats
|
||||
- Tier weights now ignore tanks without entries in the `tankopedia`
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- All `tankopedia` derivatives are now promises
|
||||
@@ -0,0 +1,9 @@
|
||||
# Blitzkrieg 1.9.5
|
||||
|
||||
Hopefully this is the last round of fixes for Blitzkrieg `1.9`.
|
||||
|
||||
## Fixes
|
||||
|
||||
- All commands have been tweaked to work even with a lack of a tankopedia entry
|
||||
- This should fix test tanks not showing up
|
||||
- `/today` now properly preserves all WN8 entries
|
||||
@@ -0,0 +1,10 @@
|
||||
# About
|
||||
|
||||
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz.
|
||||
|
||||
- Track statistics
|
||||
- Find inactive players in a clan
|
||||
- Lookup owned tanks
|
||||
- Create embeds for streams
|
||||
- Track a player/tanks statistical evolution
|
||||
- ...and a variety of other similar things
|
||||
@@ -0,0 +1,23 @@
|
||||
# Embeds
|
||||
|
||||
Embeds are generated by a few commands as a URL that can be loaded in a browser/streaming software.
|
||||
|
||||
## Creating Embeds
|
||||
|
||||
1. Use a command of your liking (for example, `/stats`).
|
||||
2. Customize the command however you like and the URL generated for the embed will match your needs.
|
||||
3. Send the command and when the bot is "done thinking," an "Embed" button will appear below the bot's reply.
|
||||
|
||||
## Using Embeds in Streaming Softwares
|
||||
|
||||
1. Click on the "Embed" button.
|
||||
2. Copy the URL from your browser.
|
||||
3. Paste the URL into a Browser Source.
|
||||
4. Set the refresh rate/FPS to something very low like `0.0167` (that's 1 frame every 60 seconds).
|
||||
|
||||
## How Embeds Behave
|
||||
|
||||
- They load like any normal website albeit with a significant delay.
|
||||
- They auto refresh every `60s`.
|
||||
- All embeds and images generated by Blitzkrieg have a width of `640px` but the height varies and must be found manually through trial and error.
|
||||
- It's okay to have a Browser Source that has a height larger than needed as Blitzkrieg embeds are transparent.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Inviting the Bot
|
||||
|
||||
It's quite simple really. Just follow the steps below:
|
||||
|
||||
1. Join a Discord server that already has Blitzkrieg installed or [the official Discord Server](https://discord.gg/nDt7AjGJQH).
|
||||
2. Click on the bot's profile.
|
||||
3. Click the "Add to Server" button under the bot's name and ID.
|
||||
4. Select what server you want it in and see what permissions you seem fit for the bot.
|
||||
|
||||
Voila! You're ready to go.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Timezones
|
||||
|
||||
What is considered a "day's worth of games" is determined by the reset time. The reset time is generally at midnight everyday after which newer games are counted towards the next day. Reset times vary from region to region:
|
||||
|
||||
- North America: `UTC-5:00` (Central North American Time midnight)
|
||||
- Europe: `UTC+1:00` (Central European Time midnight)
|
||||
- Asia: `UTC+7:00` (Central Asia Standard Time midnight)
|
||||
|
||||
**Reset times are different from Blitz Stars** (Blitz Stars resets at 5:00 am locally relative to the user opening their website). Inconsistencies with Blitz Stars only occur with sleep-deprived players and players who play in the wrong region.
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"main": "server.cjs",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node --enable-source-maps --trace-warnings server.cjs"
|
||||
}
|
||||
|
||||
+11
-4
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "blitzkrieg",
|
||||
"version": "1.9.3",
|
||||
"description": "",
|
||||
"version": "1.12.1",
|
||||
"description": "🎉 All-in-one Discord bot for everything World of Tanks Blitz",
|
||||
"main": "src/server.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:bot": "node scripts/build --build=bot",
|
||||
"build:server": "node scripts/build --build=server",
|
||||
"build:docs": "vitepress build docs",
|
||||
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||
"build": "node scripts/build --build=all",
|
||||
"dev:bot": "yarn build:bot --dev && yarn start:bot",
|
||||
"dev:server": "yarn build:server --dev && yarn start:server",
|
||||
@@ -15,23 +16,28 @@
|
||||
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
|
||||
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
|
||||
"host:docs": "vitepress preview docs",
|
||||
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
||||
"zip": "zip -r dist.zip dist",
|
||||
"lint": "npx tsc"
|
||||
"lint": "npx tsc",
|
||||
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\""
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^20.0.1",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@resvg/resvg-js": "^2.4.1",
|
||||
"bepaint": "^0.1.0-beta.3",
|
||||
"discord.js": "^14.9.0",
|
||||
"discord.js": "^14.12.0",
|
||||
"esbuild": "^0.18.10",
|
||||
"escape-html": "^1.0.3",
|
||||
"express": "^4.18.2",
|
||||
"false": "^0.0.4",
|
||||
"fuzzysort": "^2.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-escape": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"satori": "^0.9.1",
|
||||
"vite-node": "^0.33.0",
|
||||
"vitepress-plugin-auto-sidebar": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -42,6 +48,7 @@
|
||||
"@types/markdown-escape": "^1.1.0",
|
||||
"@types/node": "^20.2.1",
|
||||
"@types/react": "^18.2.7",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vitepress": "^1.0.0-beta.3"
|
||||
}
|
||||
|
||||
+17
-18
@@ -1,25 +1,25 @@
|
||||
import { build } from 'esbuild';
|
||||
import { copyFile, mkdir, rm } from 'fs/promises';
|
||||
import { copyFile, mkdirSync, rmSync } from 'fs';
|
||||
import { argv } from 'process';
|
||||
|
||||
const isProd = !argv.includes('--dev');
|
||||
const buildAll = argv.includes('--build=all');
|
||||
const buildBot = argv.includes('--build=bot') | buildAll;
|
||||
const buildServer = argv.includes('--build=server') | buildAll;
|
||||
const buildBot = argv.includes('--build=bot') || buildAll;
|
||||
const buildServer = argv.includes('--build=server') || buildAll;
|
||||
|
||||
if (isProd) {
|
||||
// only remove in production to ensure no time is wasted in dev
|
||||
console.log('🟡 Removing dist...');
|
||||
await rm('dist', { recursive: true, force: true });
|
||||
console.log('✅ Dist removed');
|
||||
console.log('Removing dist...');
|
||||
rmSync('dist', { recursive: true, force: true });
|
||||
console.log('Dist removed');
|
||||
|
||||
console.log('🟡 Creating dist...');
|
||||
await mkdir('dist');
|
||||
console.log('✅ Dist created');
|
||||
console.log('Creating dist...');
|
||||
mkdirSync('dist');
|
||||
console.log('Dist created');
|
||||
|
||||
console.log('🟡 Copying package.json...');
|
||||
copyFile('package.dist.json', 'dist/package.json').then(() =>
|
||||
console.log('✅ package.json copied'),
|
||||
console.log('Copying package.json...');
|
||||
copyFile('package.dist.json', 'dist/package.json', () =>
|
||||
console.log('package.json copied'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const commonOptions = {
|
||||
'.node': 'copy',
|
||||
'.ttf': 'file',
|
||||
},
|
||||
format: 'cjs',
|
||||
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
@@ -40,25 +41,23 @@ const commonOptions = {
|
||||
};
|
||||
|
||||
if (buildBot) {
|
||||
console.log('🟡 Building bot...');
|
||||
console.log('Building bot...');
|
||||
build({
|
||||
...commonOptions,
|
||||
|
||||
entryPoints: ['src/bot.ts'],
|
||||
outfile: 'dist/bot.cjs',
|
||||
}).then(() => {
|
||||
console.log('✅ Bot built');
|
||||
});
|
||||
}).then(() => console.log('Bot built'));
|
||||
}
|
||||
|
||||
if (buildServer) {
|
||||
console.log('🟡 Building server...');
|
||||
console.log('Building server...');
|
||||
build({
|
||||
...commonOptions,
|
||||
|
||||
entryPoints: ['src/server.ts'],
|
||||
outfile: 'dist/server.cjs',
|
||||
}).then(() => {
|
||||
console.log('✅ Server built');
|
||||
console.log('Server built');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { rm, writeFile } from 'fs/promises';
|
||||
import { argv, env } from 'process';
|
||||
import {
|
||||
RatingsInfo,
|
||||
RatingsNeighbors,
|
||||
RatingsPlayer,
|
||||
} from '../src/commands/ratings';
|
||||
import commitMultipleFiles from './commitMultipleFiles.js';
|
||||
|
||||
/*
|
||||
* Central North American Time (UTC-5): use 0 5 * * *.
|
||||
* Central European Time (UTC+1): use 0 23 * * *.
|
||||
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
||||
*
|
||||
* na/ratings/49/info.json: season info like rewards, start & end time, etc.
|
||||
* na/ratings/49/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
||||
* na/ratings/49/midnight.json: last midnight's leaderboard relative to the region
|
||||
*/
|
||||
|
||||
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
|
||||
|
||||
const publish = argv.includes('--publish');
|
||||
const latest = argv.includes('--latest');
|
||||
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
|
||||
|
||||
if (!server) throw new Error('Region parameter not specified');
|
||||
|
||||
const NEIGHBORS = 1000;
|
||||
const PLAYERS = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
)
|
||||
.then((response) => response.json() as Promise<RatingsInfo>)
|
||||
.then((data) => data.count);
|
||||
|
||||
const players: Record<number, RatingsPlayer> = {};
|
||||
const leaderboard: BlitzkriegRatingsLeaderboard = [];
|
||||
let registered = 0;
|
||||
|
||||
async function branchFromPlayer(id: number) {
|
||||
const { neighbors } = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||
).then((response) => response.json() as Promise<RatingsNeighbors>);
|
||||
|
||||
const firstPlayer = neighbors[0];
|
||||
const lastPlayer = neighbors[neighbors.length - 1];
|
||||
|
||||
const branches: Promise<void>[] = [];
|
||||
if (!players[firstPlayer.number])
|
||||
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
||||
if (!players[lastPlayer.number])
|
||||
branches.push(branchFromPlayer(lastPlayer.spa_id));
|
||||
|
||||
neighbors.forEach((neighbor) => {
|
||||
if (players[neighbor.number]) return;
|
||||
|
||||
players[neighbor.number] = neighbor;
|
||||
registered++;
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Registered ${registered} / ${PLAYERS} (${(
|
||||
(registered / PLAYERS) *
|
||||
100
|
||||
).toFixed(2)}%)`,
|
||||
);
|
||||
|
||||
await Promise.all(branches);
|
||||
}
|
||||
|
||||
async function branchFromLeague(id: number) {
|
||||
const { result } = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||
).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>);
|
||||
|
||||
const firstPlayer = result[0];
|
||||
const lastPlayer = result[result.length - 1];
|
||||
|
||||
await Promise.all([
|
||||
branchFromPlayer(firstPlayer.spa_id),
|
||||
branchFromPlayer(lastPlayer.spa_id),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('Branching from leagues...');
|
||||
await Promise.all([
|
||||
branchFromLeague(0),
|
||||
branchFromLeague(1),
|
||||
branchFromLeague(2),
|
||||
branchFromLeague(3),
|
||||
branchFromLeague(4),
|
||||
]);
|
||||
|
||||
console.log('Converting to an array...');
|
||||
for (let index = 0; index < PLAYERS; index++) {
|
||||
const listing = players[index + 1];
|
||||
if (!listing) console.warn(`Position ${index + 1} had no player`);
|
||||
|
||||
leaderboard[index] = {
|
||||
id: listing.spa_id,
|
||||
score: listing.score,
|
||||
};
|
||||
}
|
||||
|
||||
const leaderboardJSON = JSON.stringify(leaderboard);
|
||||
|
||||
if (publish) {
|
||||
const octokit = new Octokit({ auth: env.GH_TOKEN });
|
||||
|
||||
const info = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
).then((response) => response.json() as Promise<RatingsInfo>);
|
||||
const normalizedServer = server === 'na' ? 'com' : server;
|
||||
|
||||
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
|
||||
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
|
||||
latest ? 'latest' : 'midnight'
|
||||
}.json`;
|
||||
const infoJSON = JSON.stringify(info);
|
||||
|
||||
console.log(`Publishing to season ${info.current_season}...`);
|
||||
commitMultipleFiles(
|
||||
octokit,
|
||||
'tresabhi',
|
||||
'blitzkrieg-db',
|
||||
'main',
|
||||
new Date().toString(),
|
||||
[
|
||||
{ path: infoPath, content: infoJSON },
|
||||
{ path: leaderboardPath, content: leaderboardJSON },
|
||||
],
|
||||
);
|
||||
} else {
|
||||
console.log('Writing to disk...');
|
||||
await rm('dist.json', { force: true });
|
||||
await writeFile('dist.json', leaderboardJSON);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
+9
-6
@@ -1,9 +1,10 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { DISCORD_TOKEN } from './core/node/arguments.js';
|
||||
import { registerErrorHandlers } from './events/error.js';
|
||||
import guildMemberAdd from './events/guildMemberAdd.js';
|
||||
import interactionCreate from './events/interactionCreate/index.js';
|
||||
import ready from './events/ready.js';
|
||||
import { secrets } from './core/node/secrets';
|
||||
import { registerErrorHandlers } from './events/error';
|
||||
import guildMemberAdd from './events/guildMemberAdd';
|
||||
import interactionCreate from './events/interactionCreate';
|
||||
import ready from './events/ready';
|
||||
|
||||
export const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
|
||||
@@ -12,5 +13,7 @@ export const client = new Client({
|
||||
.on('guildMemberAdd', guildMemberAdd)
|
||||
.on('interactionCreate', interactionCreate);
|
||||
|
||||
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
|
||||
|
||||
registerErrorHandlers();
|
||||
client.login(DISCORD_TOKEN);
|
||||
client.login(secrets.DISCORD_TOKEN);
|
||||
@@ -1,12 +1,12 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import packageJSON from '../../package.json' assert { type: 'json' };
|
||||
import { client } from '../bot.js';
|
||||
import { tankopedia } from '../core/blitz/tankopedia.js';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
||||
import embedInfo from '../core/discord/embedInfo.js';
|
||||
import markdownTable from '../core/discord/markdownTable.js';
|
||||
import getClientId from '../core/node/getClientId.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { client } from '../bot';
|
||||
import { tankopedia } from '../core/blitz/tankopedia';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import markdownTable from '../core/discord/markdownTable';
|
||||
import getClientId from '../core/node/getClientId';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
const executionStart = new Date().getTime();
|
||||
|
||||
|
||||
+41
-26
@@ -1,21 +1,21 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import calculateWN8 from '../core/blitz/calculateWN8.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import sumStats from '../core/blitz/sumStats.js';
|
||||
import { tankopedia } from '../core/blitz/tankopedia.js';
|
||||
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
|
||||
import getPeriodStart from '../core/blitzstars/getPeriodStart.js';
|
||||
import getTankStatsDiffed from '../core/blitzstars/getTankStatsDiffed.js';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
||||
import embedNegative from '../core/discord/embedNegative.js';
|
||||
import embedPositive from '../core/discord/embedPositive.js';
|
||||
import markdownTable, { TableInput } from '../core/discord/markdownTable.js';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import { PossiblyPromise } from '../types/possiblyPromise.js';
|
||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import sumStats from '../core/blitz/sumStats';
|
||||
import { tankopedia } from '../core/blitz/tankopedia';
|
||||
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
|
||||
import getPeriodNow from '../core/blitzstars/getPeriodNow';
|
||||
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import embedNegative from '../core/discord/embedNegative';
|
||||
import embedPositive from '../core/discord/embedPositive';
|
||||
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
||||
|
||||
export type SkilledClan = 'SKLLD' | 'SMRI';
|
||||
|
||||
@@ -49,9 +49,9 @@ export const eligibleCommand: CommandRegistry = {
|
||||
|
||||
async handler(interaction) {
|
||||
const clan = interaction.options.getString('clan') as SkilledClan;
|
||||
const { id, server } = await resolvePlayerFromCommand(interaction);
|
||||
const { id, region: server } = await resolvePlayerFromCommand(interaction);
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const problems: TableInput = [];
|
||||
const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
||||
@@ -59,24 +59,39 @@ export const eligibleCommand: CommandRegistry = {
|
||||
let body = '';
|
||||
|
||||
if (clan === 'SKLLD') {
|
||||
const tankStatsOverTime = await getTankStatsDiffed(
|
||||
const { diffed } = await getDiffedTankStats(
|
||||
server,
|
||||
id,
|
||||
getPeriodStart('30'),
|
||||
getTimeDaysAgo(server, 30),
|
||||
getPeriodNow(),
|
||||
);
|
||||
const entries = Object.entries(tankStatsOverTime);
|
||||
const entries = Object.entries(diffed);
|
||||
const stats = sumStats(entries.map(([, stats]) => stats));
|
||||
const averageTier =
|
||||
(await entries.reduce<PossiblyPromise<number>>(
|
||||
async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankTier = (await tankopedia)[tankId].tier;
|
||||
const tankopediaEntry = (await tankopedia)[tankId];
|
||||
|
||||
if (!tankopediaEntry) return accumulator;
|
||||
|
||||
const tankTier = tankopediaEntry.tier;
|
||||
|
||||
return (await accumulator) + tankTier * stats.battles;
|
||||
},
|
||||
0,
|
||||
)) / stats.battles;
|
||||
)) /
|
||||
(await entries.reduce<PossiblyPromise<number>>(
|
||||
async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankopediaEntry = (await tankopedia)[tankId];
|
||||
|
||||
if (!tankopediaEntry) return accumulator;
|
||||
|
||||
return stats.battles;
|
||||
},
|
||||
0,
|
||||
));
|
||||
const WN8 =
|
||||
(await entries.reduce<PossiblyPromise<number>>(
|
||||
async (accumulator, [tankIdString, stats]) => {
|
||||
@@ -101,7 +116,7 @@ export const eligibleCommand: CommandRegistry = {
|
||||
const tankId = parseInt(tankIdString);
|
||||
|
||||
if ((await tankopedia)[tankId]?.tier === 10) {
|
||||
const tankStats = tankStatsOverTime[tankId];
|
||||
const tankStats = diffed[tankId];
|
||||
return (await accumulator) + tankStats.damage_dealt;
|
||||
} else return accumulator;
|
||||
},
|
||||
@@ -112,7 +127,7 @@ export const eligibleCommand: CommandRegistry = {
|
||||
const tankId = parseInt(tankIdString);
|
||||
|
||||
if ((await tankopedia)[tankId]?.tier === 10) {
|
||||
const tankStats = tankStatsOverTime[tankId];
|
||||
const tankStats = diffed[tankId];
|
||||
return (await accumulator) + tankStats.battles;
|
||||
} else return accumulator;
|
||||
},
|
||||
|
||||
+90
-82
@@ -1,86 +1,94 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { CYCLIC_API } from '../constants/cyclic.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import resolveTankId from '../core/blitz/resolveTankId.js';
|
||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
|
||||
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
||||
import interactionToURL from '../core/discord/interactionToURL.js';
|
||||
import linkButton from '../core/discord/linkButton.js';
|
||||
import primaryButton from '../core/discord/primaryButton.js';
|
||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
|
||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import evolution from '../renderers/evolution.js';
|
||||
import { StatType } from '../renderers/statsfull.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankId from '../core/blitz/resolveTankId';
|
||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import interactionToURL from '../core/discord/interactionToURL';
|
||||
import linkButton from '../core/discord/linkButton';
|
||||
import primaryButton from '../core/discord/primaryButton';
|
||||
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 { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import evolution from '../renderers/evolution';
|
||||
import { StatType } from '../renderers/stats';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const evolutionCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('evolution')
|
||||
.setDescription('Evolution of statistics'),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId = commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return [
|
||||
await evolution(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}${
|
||||
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
||||
}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const period = resolvePeriodFromButton(interaction);
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
|
||||
return await evolution(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
||||
async (resolve) => {
|
||||
const command = await addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('evolution')
|
||||
.setDescription('Evolution of statistics'),
|
||||
false,
|
||||
);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command,
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return [
|
||||
await evolution(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.region}/${nickname}${
|
||||
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
||||
}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.region, interaction);
|
||||
|
||||
return await evolution(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
);
|
||||
@@ -0,0 +1,113 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { TreeTypeString } from '../components/Tanks';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankId from '../core/blitz/resolveTankId';
|
||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import interactionToURL from '../core/discord/interactionToURL';
|
||||
import linkButton from '../core/discord/linkButton';
|
||||
import primaryButton from '../core/discord/primaryButton';
|
||||
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 { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import fullStats from '../renderers/fullStats';
|
||||
import { MultiTankFilters, StatType } from '../renderers/stats';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
||||
async (resolve) => {
|
||||
const command = await addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('full-stats')
|
||||
.setDescription('Full in-game statistics'),
|
||||
);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command,
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const resolvedPeriod = resolvePeriodFromCommand(
|
||||
player.region,
|
||||
interaction,
|
||||
);
|
||||
const period = interaction.options.getString('period');
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
|
||||
return [
|
||||
await fullStats(
|
||||
commandGroup,
|
||||
resolvedPeriod,
|
||||
player,
|
||||
commandGroup === 'tank'
|
||||
? tankId
|
||||
: commandGroup === 'multi-tank'
|
||||
? ({
|
||||
'tank-type':
|
||||
interaction.options.getString('tank-type') ?? undefined,
|
||||
nation: interaction.options.getString('nation') ?? undefined,
|
||||
tier: interaction.options.getInteger('tier') ?? undefined,
|
||||
'tree-type': (interaction.options.getString('tree-type') ??
|
||||
undefined) as TreeTypeString | undefined,
|
||||
} satisfies Partial<MultiTankFilters>)
|
||||
: null,
|
||||
),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.region, interaction);
|
||||
|
||||
return await fullStats(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
+56
-25
@@ -1,36 +1,67 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import discord from '../../discord.json' assert { type: 'json' };
|
||||
import { client } from '../bot.js';
|
||||
import embedInfo from '../core/discord/embedInfo.js';
|
||||
import {
|
||||
CommandRegistry,
|
||||
guildCommands,
|
||||
publicCommands,
|
||||
} from '../events/interactionCreate/index.js';
|
||||
SlashCommandBuilder,
|
||||
SlashCommandSubcommandsOnlyBuilder,
|
||||
} from 'discord.js';
|
||||
import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`;
|
||||
const DOCS = {
|
||||
permissions: 'guide/permissions',
|
||||
embeds: 'guide/embeds',
|
||||
about: 'guide/about',
|
||||
invite: 'guide/invite',
|
||||
timezones: 'guide/timezones',
|
||||
};
|
||||
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
||||
permissions: 'The permissions needed for the bot to function',
|
||||
embeds: 'How embeds work',
|
||||
about: 'About the bot',
|
||||
invite: 'How to invite the bot',
|
||||
timezones: 'How Blitzkrieg infers and uses your timezone',
|
||||
};
|
||||
|
||||
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
||||
Object.entries(DOC_DESCRIPTIONS).forEach(([key, value]) => {
|
||||
option.addSubcommand((sub) => sub.setName(key).setDescription(value));
|
||||
});
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
export const helpCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('help')
|
||||
.setDescription('All the help you need about the bot'),
|
||||
command: addDocsSubcommands(
|
||||
new SlashCommandBuilder()
|
||||
.setName('help')
|
||||
.setDescription('All the help you need about the bot')
|
||||
.addSubcommand((option) =>
|
||||
option
|
||||
.setName('commands')
|
||||
.setDescription('List of all commands and their descriptions'),
|
||||
),
|
||||
),
|
||||
|
||||
handler(interaction) {
|
||||
return embedInfo(
|
||||
`${client.user?.username} help`,
|
||||
`**About**\n${
|
||||
client.user?.username
|
||||
} automates many mundane tasks and provide statistics in numerous flexible ways.\n\nMade by TresAbhi from the Skilled [SKLLD] clan.\nGitHub: https://github.com/tresabhi/blitzkrieg\nDiscord server: https://discord.gg/nDt7AjGJQH\nSkilled server: https://discord.gg/ZPvcEG7DS8\n\n**Commands**\n${(interaction.guildId ===
|
||||
discord.sklld_guild_id
|
||||
? [...guildCommands, ...publicCommands]
|
||||
: publicCommands
|
||||
async handler(interaction) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
if (subcommand === 'commands') {
|
||||
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
|
||||
await Promise.all(COMMANDS_RAW)
|
||||
)
|
||||
.map((command) => `**/${command.name}**: ${command.description}`)
|
||||
.join(
|
||||
'\n',
|
||||
)}\n\n*I am a new bot; I may make mistakes and go offline unexpectedly from time to time.*`,
|
||||
);
|
||||
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||
.map(
|
||||
(registry) =>
|
||||
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
||||
)
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
const url = `${RAW_PATH}${DOCS[subcommand as keyof typeof DOCS]}.md`;
|
||||
const content = fetch(url).then((response) => response.text());
|
||||
|
||||
return content;
|
||||
},
|
||||
};
|
||||
+18
-22
@@ -1,18 +1,17 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import GenericStats from '../components/GenericStats/index.js';
|
||||
import NoData, { NoDataType } from '../components/NoData.js';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy.js';
|
||||
import TitleBar from '../components/TitleBar.js';
|
||||
import Wrapper from '../components/Wrapper.js';
|
||||
import { BLITZ_SERVERS } from '../constants/servers.js';
|
||||
import getBlitzClan from '../core/blitz/getBlitzClan.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import addClanChoices from '../core/discord/addClanChoices.js';
|
||||
import autocompleteClan from '../core/discord/autocompleteClan.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import { ClanInfo } from '../types/clanInfo.js';
|
||||
import GenericStats from '../components/GenericStats';
|
||||
import NoData, { NoDataType } from '../components/NoData';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import getBlitzClan from '../core/blitz/getBlitzClan';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addClanChoices from '../core/discord/addClanChoices';
|
||||
import autocompleteClan from '../core/discord/autocompleteClan';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { ClanInfo } from '../types/clanInfo';
|
||||
|
||||
const DEFAULT_THRESHOLD = 7;
|
||||
|
||||
@@ -42,14 +41,14 @@ export const inactiveCommand: CommandRegistry = {
|
||||
const time = new Date().getTime() / 1000;
|
||||
const clanData = (
|
||||
await getWargamingResponse<ClanInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
||||
)
|
||||
)[id];
|
||||
const memberIds = clanData.members_ids;
|
||||
const accounts = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds.join(
|
||||
',',
|
||||
)}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${
|
||||
secrets.WARGAMING_APPLICATION_ID
|
||||
}&account_id=${memberIds.join(',')}`,
|
||||
);
|
||||
const inactive = memberIds
|
||||
.map((memberId) => {
|
||||
@@ -69,11 +68,8 @@ export const inactiveCommand: CommandRegistry = {
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={clanData.name}
|
||||
nameDiscriminator="(Inactivity)"
|
||||
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
|
||||
description={`${threshold}+ Days • ${new Date().toDateString()} • ${
|
||||
BLITZ_SERVERS[server]
|
||||
}`}
|
||||
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
|
||||
|
||||
+119
-51
@@ -1,24 +1,27 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import NoData, { NoDataType } from '../components/NoData.js';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy.js';
|
||||
import * as Tanks from '../components/Tanks/index.js';
|
||||
import TitleBar from '../components/TitleBar.js';
|
||||
import Wrapper from '../components/Wrapper.js';
|
||||
import { BLITZ_SERVERS } from '../constants/servers.js';
|
||||
import getTankStats from '../core/blitz/getTankStats.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import NoData, { NoDataType } from '../components/NoData';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import * as Tanks from '../components/Tanks';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import getTankStats from '../core/blitz/getTankStats';
|
||||
import getTreeType from '../core/blitz/getTreeType';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankName from '../core/blitz/resolveTankName';
|
||||
import {
|
||||
TIER_ROMAN_NUMERALS,
|
||||
TankopediaEntry,
|
||||
Tier,
|
||||
tankopedia,
|
||||
} from '../core/blitz/tankopedia.js';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import { PlayerClanData } from '../types/playerClanData.js';
|
||||
tankopediaInfo,
|
||||
} from '../core/blitz/tankopedia';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
|
||||
const COMP_TANKS = [
|
||||
// light tanks
|
||||
@@ -47,23 +50,23 @@ export const ownedTanksCommand: CommandRegistry = {
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('ownedtanks')
|
||||
.setName('owned-tanks')
|
||||
.setDescription("Shows a player's owned tanks")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('tier')
|
||||
.setDescription('The tier you want to see')
|
||||
.setChoices(
|
||||
{ name: 'Tier I', value: '1' },
|
||||
{ name: 'Tier II', value: '2' },
|
||||
{ name: 'Tier III', value: '3' },
|
||||
{ name: 'Tier IV', value: '4' },
|
||||
{ name: 'Tier V', value: '5' },
|
||||
{ name: 'Tier VI', value: '6' },
|
||||
{ name: 'Tier VII', value: '7' },
|
||||
{ name: 'Tier VIII', value: '8' },
|
||||
{ name: 'Tier IX', value: '9' },
|
||||
{ name: 'Tier X', value: '10' },
|
||||
{ name: 'Tier I (1)', value: '1' },
|
||||
{ name: 'Tier II (2)', value: '2' },
|
||||
{ name: 'Tier III (3)', value: '3' },
|
||||
{ name: 'Tier IV (4)', value: '4' },
|
||||
{ name: 'Tier V (5)', value: '5' },
|
||||
{ name: 'Tier VI (6)', value: '6' },
|
||||
{ name: 'Tier VII (7)', value: '7' },
|
||||
{ name: 'Tier VIII (8)', value: '8' },
|
||||
{ name: 'Tier IX (9)', value: '9' },
|
||||
{ name: 'Tier X (10)', value: '10' },
|
||||
)
|
||||
.setRequired(true),
|
||||
)
|
||||
@@ -72,50 +75,101 @@ export const ownedTanksCommand: CommandRegistry = {
|
||||
async handler(interaction) {
|
||||
const tier = Number(interaction.options.getString('tier'));
|
||||
const account = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = account;
|
||||
const { id, region: server } = account;
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const tankStats = await getTankStats(server, id);
|
||||
const tanks = (
|
||||
const filteredTanks = (
|
||||
await Promise.all(
|
||||
tankStats.map(async (tankData) => (await tankopedia)[tankData.tank_id]),
|
||||
tankStats.map(async (tankData) => ({
|
||||
tankopedia: (await tankopedia)[tankData.tank_id]!,
|
||||
tank_id: tankData.tank_id,
|
||||
})),
|
||||
)
|
||||
).filter((tank) => tank?.tier === tier);
|
||||
).filter((tank) => tank.tankopedia?.tier === tier);
|
||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||
);
|
||||
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
||||
const nations: string[] = [];
|
||||
|
||||
filteredTanks.forEach((tank) => {
|
||||
if (groupedTanks[tank.tankopedia.nation] === undefined) {
|
||||
groupedTanks[tank.tankopedia.nation] = [tank.tankopedia];
|
||||
nations.push(tank.tankopedia.nation);
|
||||
} else {
|
||||
groupedTanks[tank.tankopedia.nation].push(tank.tankopedia);
|
||||
}
|
||||
});
|
||||
|
||||
nations.sort();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{/* TODO: integrate some of these into title bar */}
|
||||
<TitleBar
|
||||
name={accountInfo[id].nickname}
|
||||
nameDiscriminator={`(Tier ${TIER_ROMAN_NUMERALS[tier as Tier]})`}
|
||||
nameDiscriminator={
|
||||
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
|
||||
}
|
||||
image={
|
||||
clanData[id]?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||
: undefined
|
||||
}
|
||||
description={`Owned tanks • ${new Date().toDateString()} • ${
|
||||
BLITZ_SERVERS[server]
|
||||
}`}
|
||||
description={`Tier ${
|
||||
TIER_ROMAN_NUMERALS[tier as Tier]
|
||||
} tanks • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{tanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||
|
||||
{tanks.length > 0 && (
|
||||
<Tanks.Root>
|
||||
{tanks.map((tank) => (
|
||||
<Tanks.Item
|
||||
key={tank.tank_id}
|
||||
name={tank.name}
|
||||
type={tank.type}
|
||||
icon={tank.images.normal}
|
||||
/>
|
||||
))}
|
||||
</Tanks.Root>
|
||||
)}
|
||||
{filteredTanks.length > 0 &&
|
||||
(await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tanks = groupedTanks[nation];
|
||||
const leftColumnSize = Math.ceil(tanks.length / 2);
|
||||
const leftColumn = tanks.slice(0, leftColumnSize);
|
||||
const rightColumn = tanks.slice(leftColumnSize);
|
||||
|
||||
return (
|
||||
<Tanks.Root>
|
||||
<Tanks.Title>
|
||||
{(await tankopediaInfo).vehicle_nations[nation]}
|
||||
</Tanks.Title>
|
||||
|
||||
<Tanks.Row>
|
||||
<Tanks.Column>
|
||||
{await Promise.all(
|
||||
leftColumn.map(async (tank) => (
|
||||
<Tanks.Item
|
||||
key={tank.tank_id}
|
||||
name={await resolveTankName(tank.tank_id)}
|
||||
tankType={tank.type}
|
||||
image={tank.images?.normal}
|
||||
treeType={await getTreeType(tank.tank_id)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Tanks.Column>
|
||||
<Tanks.Column>
|
||||
{await Promise.all(
|
||||
rightColumn.map(async (tank) => (
|
||||
<Tanks.Item
|
||||
key={tank.tank_id}
|
||||
name={await resolveTankName(tank.tank_id)}
|
||||
tankType={tank.type}
|
||||
image={tank.images?.normal}
|
||||
treeType={await getTreeType(tank.tank_id)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Tanks.Column>
|
||||
</Tanks.Row>
|
||||
</Tanks.Root>
|
||||
);
|
||||
}),
|
||||
))}
|
||||
|
||||
<PoweredBy type={PoweredByType.Wargaming} />
|
||||
</Wrapper>
|
||||
@@ -124,3 +178,17 @@ export const ownedTanksCommand: CommandRegistry = {
|
||||
|
||||
autocomplete: autocompleteUsername,
|
||||
};
|
||||
|
||||
// TODO: make this work in the future
|
||||
// console.log('Caching tank images...');
|
||||
// TANKS.then((tanks) =>
|
||||
// jsxToSvg(
|
||||
// <div style={{ display: 'flex' }}>
|
||||
// {tanks.map(({ images, tank_id }) => {
|
||||
// return (
|
||||
// images.normal && <img key={tank_id} src={images.normal} width={0} />
|
||||
// );
|
||||
// })}
|
||||
// </div>,
|
||||
// ),
|
||||
// ).then(() => console.log('Cached tank images'));
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
export const pingCommand: CommandRegistry<true> = {
|
||||
export const pingCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { startCase } from 'lodash';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
||||
import embedInfo from '../core/discord/embedInfo.js';
|
||||
import markdownTable, {
|
||||
TableInputEntry,
|
||||
} from '../core/discord/markdownTable.js';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { AccountAchievements } from '../types/accountAchievements.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountAchievements } from '../types/accountAchievements';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
type SortBy = 'name' | 'count';
|
||||
|
||||
@@ -22,7 +20,7 @@ export const playerAchievementsCommand: CommandRegistry = {
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('playerachievements')
|
||||
.setName('player-achievements')
|
||||
.setDescription("All the player's achievements")
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addStringOption((option) =>
|
||||
@@ -38,13 +36,13 @@ export const playerAchievementsCommand: CommandRegistry = {
|
||||
async handler(interaction) {
|
||||
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
||||
const account = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = account;
|
||||
const { id, region: server } = account;
|
||||
const accounts = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const accountsAchievements =
|
||||
await getWargamingResponse<AccountAchievements>(
|
||||
`https://api.wotblitz.${server}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/achievements/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const accountAchievements = accountsAchievements[id];
|
||||
const compound = {
|
||||
|
||||
+12
-12
@@ -1,14 +1,14 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
||||
import embedInfo from '../core/discord/embedInfo.js';
|
||||
import markdownTable from '../core/discord/markdownTable.js';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import markdownTable from '../core/discord/markdownTable';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const playerInfoCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
@@ -16,15 +16,15 @@ export const playerInfoCommand: CommandRegistry = {
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('playerinfo')
|
||||
.setName('player-info')
|
||||
.setDescription('Basic information about a player')
|
||||
.addStringOption(addUsernameChoices),
|
||||
|
||||
async handler(interaction) {
|
||||
const account = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = account;
|
||||
const { id, region: server } = account;
|
||||
const accounts = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const accountInfo = accounts[id];
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import {
|
||||
APIApplicationCommandOptionChoice,
|
||||
SlashCommandBuilder,
|
||||
} from 'discord.js';
|
||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||
import * as Leaderboard from '../components/Leaderboard';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
||||
import getMidnightLeaderboard from '../core/database/getMidnightLeaderboard';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
|
||||
export interface RatingsPlayer {
|
||||
spa_id: number;
|
||||
mmr: number;
|
||||
season_number: number;
|
||||
calibrationBattlesLeft: number;
|
||||
number: number;
|
||||
percentile: number;
|
||||
skip: boolean;
|
||||
updated_at: string;
|
||||
score: number;
|
||||
nickname: string;
|
||||
clan_tag: string;
|
||||
}
|
||||
|
||||
export interface LeagueTop {
|
||||
result: RatingsPlayer[];
|
||||
}
|
||||
|
||||
export type RatingsReward = {
|
||||
from_position: number;
|
||||
to_position: number;
|
||||
count: number;
|
||||
} & (
|
||||
| {
|
||||
type: 'vehicle';
|
||||
|
||||
vehicle: {
|
||||
id: number;
|
||||
name: string;
|
||||
nation: string;
|
||||
subnation: string;
|
||||
use_subnation_flag: boolean;
|
||||
type_slug: string;
|
||||
level: number;
|
||||
roman_level: string;
|
||||
user_string: string;
|
||||
image_url: string;
|
||||
preview_url: string;
|
||||
is_premium: boolean;
|
||||
is_collectible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'stuff';
|
||||
|
||||
stuff: {
|
||||
name: string;
|
||||
count: number;
|
||||
title: string;
|
||||
image_url: string;
|
||||
type: string;
|
||||
|
||||
sizes: {};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export interface RatingsLeague {
|
||||
title: string;
|
||||
small_icon: string;
|
||||
big_icon: string;
|
||||
background: string;
|
||||
index: number;
|
||||
percentile: number;
|
||||
}
|
||||
|
||||
export interface RatingsInfo {
|
||||
title: string;
|
||||
icon: string | null;
|
||||
start_at: string;
|
||||
finish_at: string;
|
||||
current_season: number;
|
||||
updated_at: string;
|
||||
count: number;
|
||||
|
||||
rewards: RatingsReward[];
|
||||
leagues: RatingsLeague[];
|
||||
}
|
||||
|
||||
export interface RatingsNeighbors {
|
||||
neighbors: RatingsPlayer[];
|
||||
}
|
||||
|
||||
console.log('Caching ratings info...');
|
||||
const ratingsInfo = getRatingsInfo('com').then((info) => {
|
||||
console.log('Cached ratings info');
|
||||
return info;
|
||||
});
|
||||
|
||||
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
async (resolve) => {
|
||||
const awaitedRatingsInfo = await ratingsInfo;
|
||||
|
||||
const command = new SlashCommandBuilder()
|
||||
.setName('ratings')
|
||||
.setDescription('Ratings battles statistics')
|
||||
.addSubcommand((option) =>
|
||||
option
|
||||
.setName('league')
|
||||
.setDescription('Top league players')
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('league')
|
||||
.setDescription('The league to display')
|
||||
.addChoices(
|
||||
...awaitedRatingsInfo.leagues.map(
|
||||
(league) =>
|
||||
({
|
||||
name: league.title.split(' ')[0].toLowerCase(),
|
||||
value: `${league.index}`,
|
||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||
),
|
||||
)
|
||||
.setRequired(true),
|
||||
)
|
||||
.addStringOption(addRegionChoices)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('limit')
|
||||
.setDescription('How many rows to display (default: 10)')
|
||||
.setRequired(false)
|
||||
.setMinValue(5)
|
||||
.setMaxValue(30),
|
||||
),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
option
|
||||
.setName('neighbours')
|
||||
.setDescription('Your position and neighbours')
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('limit')
|
||||
.setDescription('How many neighbours to display (default: 10)')
|
||||
.setRequired(false)
|
||||
.setMinValue(5)
|
||||
.setMaxValue(30),
|
||||
),
|
||||
);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command,
|
||||
|
||||
async handler(interaction) {
|
||||
const subcommand = interaction.options.getSubcommand(true);
|
||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
||||
let titleName: string;
|
||||
let titleImage: string | undefined;
|
||||
let titleNameDiscriminator: string | undefined;
|
||||
let titleDescription: string;
|
||||
let playersBefore: number;
|
||||
let playersAfter: number;
|
||||
let players: RatingsPlayer[];
|
||||
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
||||
let playerId: number | undefined;
|
||||
let regionRatingsInfo: RatingsInfo;
|
||||
|
||||
if (subcommand === 'league') {
|
||||
const leagueIndex = parseInt(
|
||||
interaction.options.getString('league')!,
|
||||
);
|
||||
const region = interaction.options.getString('region') as Region;
|
||||
regionRatingsInfo = await getRatingsInfo(region);
|
||||
const regionSubdomain = regionToRegionSubdomain(region);
|
||||
const result = await fetch(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
|
||||
)
|
||||
.then((response) => response.json() as Promise<LeagueTop>)
|
||||
.then(({ result }) => result.slice(0, limit));
|
||||
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
||||
midnightLeaderboard = await getMidnightLeaderboard(
|
||||
region,
|
||||
regionRatingsInfo.current_season,
|
||||
);
|
||||
|
||||
players = result;
|
||||
playersBefore = result[0].number - 1;
|
||||
playersAfter =
|
||||
regionRatingsInfo.count - result[result.length - 1].number;
|
||||
titleName = `${leagueInfo.title} - ${REGION_NAMES_SHORT[region]}`;
|
||||
titleImage = leagueInfo.big_icon.startsWith('http')
|
||||
? leagueInfo.big_icon
|
||||
: `https:${leagueInfo.big_icon}`;
|
||||
titleDescription = `Top ${limit} players`;
|
||||
} else {
|
||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||
regionRatingsInfo = await getRatingsInfo(region);
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const clan = (
|
||||
await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||
)
|
||||
)[id]?.clan;
|
||||
const regionSubdomain = regionToRegionSubdomain(region);
|
||||
const { neighbors } = await fetch(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
||||
limit / 2,
|
||||
)}`,
|
||||
).then((response) => response.json() as Promise<RatingsNeighbors>);
|
||||
midnightLeaderboard = await getMidnightLeaderboard(
|
||||
region,
|
||||
regionRatingsInfo.current_season,
|
||||
);
|
||||
|
||||
if (clan) titleNameDiscriminator = `[${clan.tag}]`;
|
||||
|
||||
players = neighbors;
|
||||
titleImage = clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||
: undefined;
|
||||
titleName = accountInfo[id].nickname;
|
||||
titleDescription = 'Ratings neighbours';
|
||||
playersBefore = neighbors[0].number - 1;
|
||||
playersAfter =
|
||||
regionRatingsInfo.count - neighbors[neighbors.length - 1].number;
|
||||
playerId = id;
|
||||
}
|
||||
|
||||
const items = players.map((player) => {
|
||||
const reward = regionRatingsInfo.rewards.find(
|
||||
(reward) =>
|
||||
player.number >= reward.from_position &&
|
||||
player.number <= reward.to_position,
|
||||
);
|
||||
const midnightIndex = midnightLeaderboard?.findIndex(
|
||||
(item) => item.id === player.spa_id,
|
||||
);
|
||||
const midnightScore =
|
||||
midnightIndex === undefined
|
||||
? player.score
|
||||
: midnightLeaderboard![midnightIndex].score;
|
||||
const midnightPosition =
|
||||
midnightIndex === undefined ? player.number : midnightIndex + 1;
|
||||
|
||||
return (
|
||||
<Leaderboard.Item
|
||||
nickname={player.nickname}
|
||||
score={player.score}
|
||||
position={player.number}
|
||||
clan={player.clan_tag}
|
||||
reward={reward}
|
||||
deltaScore={player.score - midnightScore}
|
||||
deltaPosition={midnightPosition - player.number}
|
||||
key={player.spa_id}
|
||||
highlight={player.spa_id === playerId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={titleName}
|
||||
image={titleImage}
|
||||
nameDiscriminator={titleNameDiscriminator}
|
||||
description={`${titleDescription} • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
<Leaderboard.Root>
|
||||
{playersBefore > 0 && <Leaderboard.Gap number={playersBefore} />}
|
||||
{items}
|
||||
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
|
||||
</Leaderboard.Root>
|
||||
</Wrapper>
|
||||
);
|
||||
},
|
||||
|
||||
autocomplete: autocompleteUsername,
|
||||
});
|
||||
},
|
||||
);
|
||||
+13
-13
@@ -1,13 +1,13 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { BLITZ_SERVERS, BlitzServer } from '../constants/servers.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import addClanChoices from '../core/discord/addClanChoices.js';
|
||||
import addServerChoices from '../core/discord/addServerChoices.js';
|
||||
import embedInfo from '../core/discord/embedInfo.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { ClanList } from '../types/clanList.js';
|
||||
import { REGION_NAMES, Region } from '../constants/regions';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addClanChoices from '../core/discord/addClanChoices';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { ClanList } from '../types/clanList';
|
||||
|
||||
export const searchClansCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
@@ -15,9 +15,9 @@ export const searchClansCommand: CommandRegistry = {
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('searchclans')
|
||||
.setName('search-clans')
|
||||
.setDescription('Search clans')
|
||||
.addStringOption(addServerChoices)
|
||||
.addStringOption(addRegionChoices)
|
||||
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
@@ -28,15 +28,15 @@ export const searchClansCommand: CommandRegistry = {
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const server = interaction.options.getString('server') as BlitzServer;
|
||||
const server = interaction.options.getString('region') as Region;
|
||||
const clan = interaction.options.getString('clan')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const clanList = await getWargamingResponse<ClanList>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
||||
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
||||
);
|
||||
|
||||
return embedInfo(
|
||||
`Clan search for "${markdownEscape(clan)}" in ${BLITZ_SERVERS[server]}`,
|
||||
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
|
||||
clanList.length === 0
|
||||
? 'No clans found.'
|
||||
: `\`\`\`\n${clanList
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { BLITZ_SERVERS, BlitzServer } from '../constants/servers.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import { usernamePattern } from '../core/blitz/listPlayers.js';
|
||||
import addServerChoices from '../core/discord/addServerChoices.js';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
||||
import embedInfo from '../core/discord/embedInfo.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { AccountList } from '../types/accountList.js';
|
||||
import { REGION_NAMES, Region } from '../constants/regions';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import { usernamePattern } from '../core/blitz/listPlayers';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountList } from '../types/accountList';
|
||||
|
||||
export const searchPlayersCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
@@ -16,9 +16,9 @@ export const searchPlayersCommand: CommandRegistry = {
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('searchplayers')
|
||||
.setName('search-players')
|
||||
.setDescription('Search players in a Blitz server')
|
||||
.addStringOption(addServerChoices)
|
||||
.addStringOption(addRegionChoices)
|
||||
.addStringOption((option) =>
|
||||
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
|
||||
)
|
||||
@@ -31,20 +31,20 @@ export const searchPlayersCommand: CommandRegistry = {
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const server = interaction.options.getString('server') as BlitzServer;
|
||||
const server = interaction.options.getString('region') as Region;
|
||||
const name = interaction.options.getString('username')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const trimmedSearch = name.trim();
|
||||
const players = usernamePattern.test(trimmedSearch)
|
||||
? await getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
|
||||
trimmedSearch,
|
||||
)}&limit=${limit}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${
|
||||
secrets.WARGAMING_APPLICATION_ID
|
||||
}&search=${encodeURIComponent(trimmedSearch)}&limit=${limit}`,
|
||||
)
|
||||
: [];
|
||||
|
||||
return embedInfo(
|
||||
`Player search for "${markdownEscape(name)}" in ${BLITZ_SERVERS[server]}`,
|
||||
`Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
|
||||
`\`\`\`${
|
||||
players.length === 0
|
||||
? 'No players found.'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { TANK_NAMES } from '../core/blitz/tankopedia.js';
|
||||
import addTankChoices from '../core/discord/addTankChoices.js';
|
||||
import embedInfo from '../core/discord/embedInfo.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { TANK_NAMES_DIACRITICS } from '../core/blitz/tankopedia';
|
||||
import addTankChoices from '../core/discord/addTankChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
export const searchTanksCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
@@ -12,7 +12,7 @@ export const searchTanksCommand: CommandRegistry = {
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('searchtanks')
|
||||
.setName('search-tanks')
|
||||
.setDescription('Search tanks')
|
||||
.addStringOption((option) => addTankChoices(option).setAutocomplete(false))
|
||||
.addIntegerOption((option) =>
|
||||
@@ -26,9 +26,10 @@ export const searchTanksCommand: CommandRegistry = {
|
||||
async handler(interaction) {
|
||||
const tank = interaction.options.getString('tank')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const results = go(tank, TANK_NAMES, { limit }).map(
|
||||
(result) => result.target,
|
||||
);
|
||||
const results = go(tank, await TANK_NAMES_DIACRITICS, {
|
||||
limit,
|
||||
keys: ['combined'],
|
||||
}).map((result) => result.obj.original);
|
||||
|
||||
return embedInfo(
|
||||
`Tank search for "${markdownEscape(tank)}"`,
|
||||
|
||||
+110
-76
@@ -1,85 +1,119 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { CYCLIC_API } from '../constants/cyclic.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import resolveTankId from '../core/blitz/resolveTankId.js';
|
||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
|
||||
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
||||
import interactionToURL from '../core/discord/interactionToURL.js';
|
||||
import linkButton from '../core/discord/linkButton.js';
|
||||
import primaryButton from '../core/discord/primaryButton.js';
|
||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
|
||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import stats from '../renderers/stats.js';
|
||||
import { StatType } from '../renderers/statsfull.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import { TreeTypeString } from '../components/Tanks';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankId from '../core/blitz/resolveTankId';
|
||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import interactionToURL from '../core/discord/interactionToURL';
|
||||
import linkButton from '../core/discord/linkButton';
|
||||
import primaryButton from '../core/discord/primaryButton';
|
||||
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 { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import stats, { MultiTankFilters, StatType } from '../renderers/stats';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const statsCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: addStatTypeSubCommandGroups(
|
||||
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
||||
const command = await addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('stats')
|
||||
.setDescription('In-game statistics'),
|
||||
),
|
||||
.setDescription('Regular battles statistics'),
|
||||
);
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
return [
|
||||
await stats(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
command,
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const resolvedPeriod = resolvePeriodFromCommand(
|
||||
player.region,
|
||||
interaction,
|
||||
);
|
||||
const period = interaction.options.getString('period');
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const period = resolvePeriodFromButton(interaction);
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
return [
|
||||
await stats(
|
||||
commandGroup,
|
||||
resolvedPeriod,
|
||||
player,
|
||||
commandGroup === 'tank'
|
||||
? tankId
|
||||
: commandGroup === 'multi-tank'
|
||||
? ({
|
||||
'tank-type':
|
||||
interaction.options.getString('tank-type') ?? undefined,
|
||||
nation: interaction.options.getString('nation') ?? undefined,
|
||||
tier: interaction.options.getInteger('tier') ?? undefined,
|
||||
'tree-type': (interaction.options.getString('tree-type') ??
|
||||
undefined) as TreeTypeString | undefined,
|
||||
} satisfies Partial<MultiTankFilters>)
|
||||
: null,
|
||||
),
|
||||
primaryButton(path, 'Refresh'),
|
||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
return await stats(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
);
|
||||
},
|
||||
};
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.region, interaction);
|
||||
|
||||
return await stats(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
commandGroup === 'tank'
|
||||
? parseInt(url.searchParams.get('tankId')!)
|
||||
: commandGroup === 'multi-tank'
|
||||
? ({
|
||||
'tank-type': url.searchParams.get('tank-type')!,
|
||||
nation: url.searchParams.get('nation')!,
|
||||
tier: parseInt(url.searchParams.get('tier')!),
|
||||
'tree-type': url.searchParams.get('tree-type') as TreeTypeString,
|
||||
} satisfies MultiTankFilters)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { CYCLIC_API } from '../constants/cyclic.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import resolveTankId from '../core/blitz/resolveTankId.js';
|
||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
|
||||
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
||||
import interactionToURL from '../core/discord/interactionToURL.js';
|
||||
import linkButton from '../core/discord/linkButton.js';
|
||||
import primaryButton from '../core/discord/primaryButton.js';
|
||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
|
||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import statsfull, { StatType } from '../renderers/statsfull.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
|
||||
export const statsFullCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('statsfull')
|
||||
.setDescription('Full in-game statistics'),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
|
||||
return [
|
||||
await statsfull(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const period = resolvePeriodFromButton(interaction);
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
|
||||
return await statsfull(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
);
|
||||
},
|
||||
};
|
||||
+51
-18
@@ -1,16 +1,17 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
||||
import interactionToURL from '../core/discord/interactionToURL.js';
|
||||
import linkButton from '../core/discord/linkButton.js';
|
||||
import primaryButton from '../core/discord/primaryButton.js';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import today from '../renderers/today.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import interactionToURL from '../core/discord/interactionToURL';
|
||||
import linkButton from '../core/discord/linkButton';
|
||||
import primaryButton from '../core/discord/primaryButton';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import today from '../renderers/today';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const todayCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
@@ -20,23 +21,55 @@ export const todayCommand: CommandRegistry = {
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('today')
|
||||
.setDescription('A general daily breakdown of your performance')
|
||||
.addStringOption(addUsernameChoices),
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('cutoff')
|
||||
.setDescription(
|
||||
'The maximum number of tanks to display (default: Infinity)',
|
||||
)
|
||||
.setRequired(false)
|
||||
.setMinValue(1),
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('maximized')
|
||||
.setDescription(
|
||||
'The number of rows with full stats after which all are collapsed (default: 4)',
|
||||
)
|
||||
.setMinValue(0)
|
||||
.setRequired(false),
|
||||
)
|
||||
.addBooleanOption((option) =>
|
||||
option
|
||||
.setName('show-total')
|
||||
.setDescription('Show the total stats at the top (default: true)')
|
||||
.setRequired(false),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const cutoff = interaction.options.getInteger('cutoff') ?? undefined;
|
||||
const maximized = interaction.options.getInteger('maximized') ?? undefined;
|
||||
const showTotal = interaction.options.getBoolean('show-total') ?? undefined;
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
const path = interactionToURL(interaction, player);
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
cutoff,
|
||||
maximized,
|
||||
'show-total': showTotal,
|
||||
});
|
||||
|
||||
return [
|
||||
await today(player),
|
||||
await today(player, cutoff, maximized, showTotal, false),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
|
||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
|
||||
+13
-13
@@ -1,16 +1,16 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import discord from '../../discord.json' assert { type: 'json' };
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
||||
import embedNegative from '../core/discord/embedNegative.js';
|
||||
import embedPositive from '../core/discord/embedPositive.js';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import { PlayerClanData } from '../types/playerClanData.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import embedNegative from '../core/discord/embedNegative';
|
||||
import embedPositive from '../core/discord/embedPositive';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
|
||||
export const verifyCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
@@ -24,12 +24,12 @@ export const verifyCommand: CommandRegistry = {
|
||||
|
||||
async handler(interaction) {
|
||||
const blitzAccount = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = blitzAccount;
|
||||
const { id, region: server } = blitzAccount;
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||
);
|
||||
const clan = clanData[id]?.clan;
|
||||
const clanTag = clan ? ` [${clan!.tag}]` : '';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Percentile } from '../../../../../constants/percentiles.js';
|
||||
import hslToHex from '../../../../../core/node/hslToHex.js';
|
||||
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator.js';
|
||||
import { Percentile } from '../../../../../constants/percentiles';
|
||||
import hslToHex from '../../../../../core/node/hslToHex';
|
||||
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator';
|
||||
|
||||
export enum GlowSide {
|
||||
Top,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Percentile } from '../../../../constants/percentiles.js';
|
||||
import getWN8Percentile from '../../../../core/blitz/getWN8Percentile.js';
|
||||
import { theme } from '../../../../stitches.config.js';
|
||||
import { PERCENTILE_COLORS } from '../../../PercentileIndicator.js';
|
||||
import { Glow, GlowSide } from './components/Glow.js';
|
||||
import { Percentile } from '../../../../constants/percentiles';
|
||||
import getWN8Percentile from '../../../../core/blitz/getWN8Percentile';
|
||||
import { theme } from '../../../../stitches.config';
|
||||
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
|
||||
import { Glow, GlowSide } from './components/Glow';
|
||||
|
||||
export interface WN8DisplayProps {
|
||||
WN8?: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { theme } from '../../../stitches.config.js';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export enum WN8SurroundingStatAlign {
|
||||
Left,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AllStats, SupplementaryStats } from '../../types/accountInfo.js';
|
||||
import { WN8Display } from './components/WN8Display/index.js';
|
||||
import { AllStats, SupplementaryStats } from '../../types/accountInfo';
|
||||
import { WN8Display } from './components/WN8Display';
|
||||
import {
|
||||
WN8SurroundingStat,
|
||||
WN8SurroundingStatAlign,
|
||||
} from './components/WN8SurroundingStat.js';
|
||||
} from './components/WN8SurroundingStat';
|
||||
|
||||
export interface AllStatsOverviewProps {
|
||||
stats: AllStats;
|
||||
@@ -69,7 +69,9 @@ export default function AllStatsOverview({
|
||||
>
|
||||
<WN8SurroundingStat
|
||||
label="Tier"
|
||||
value={supplementaryStats.tier.toFixed(2)}
|
||||
value={
|
||||
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
|
||||
}
|
||||
/>
|
||||
<WN8SurroundingStat
|
||||
label="Survival"
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface RootProps {
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import getWN8Percentile from '../../../core/blitz/getWN8Percentile.js';
|
||||
import { RowDiscriminator } from './RowDiscriminator.js';
|
||||
import { RowStat } from './RowStat.js';
|
||||
import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
|
||||
import { TankType } from '../../../core/blitz/tankopedia';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
||||
import { RowStat } from './RowStat';
|
||||
|
||||
export interface RowProps {
|
||||
name: string;
|
||||
@@ -10,14 +12,17 @@ export interface RowProps {
|
||||
careerWN8?: number;
|
||||
damage: number;
|
||||
careerDamage: number;
|
||||
survival: number;
|
||||
careerSurvival: number;
|
||||
battles: number;
|
||||
careerBattles: number;
|
||||
icon?: string;
|
||||
minimized: boolean;
|
||||
isTank: boolean;
|
||||
treeType?: TreeTypeEnum;
|
||||
tankType?: TankType;
|
||||
}
|
||||
|
||||
export function Row({
|
||||
isTank,
|
||||
minimized,
|
||||
name,
|
||||
winrate,
|
||||
careerWinrate,
|
||||
@@ -25,45 +30,87 @@ export function Row({
|
||||
careerWN8,
|
||||
damage,
|
||||
careerDamage,
|
||||
survival,
|
||||
careerSurvival,
|
||||
battles,
|
||||
careerBattles,
|
||||
icon,
|
||||
treeType,
|
||||
tankType,
|
||||
}: RowProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<RowDiscriminator name={name} icon={icon} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.colors.appBackground2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 8,
|
||||
gap: 4,
|
||||
backgroundColor: theme.colors.componentInteractive,
|
||||
}}
|
||||
>
|
||||
{isTank && treeType !== undefined && tankType !== undefined && (
|
||||
<img
|
||||
src={TREE_TYPE_ICONS[treeType][tankType]}
|
||||
style={{ width: 16, height: 16 }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
color: isTank
|
||||
? treeType === TreeTypeEnum.Collector
|
||||
? theme.colors.textLowContrast_blue
|
||||
: treeType === TreeTypeEnum.Premium
|
||||
? theme.colors.textLowContrast_amber
|
||||
: theme.colors.textHighContrast
|
||||
: theme.colors.textLowContrast,
|
||||
fontWeight: 900,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<RowStat
|
||||
name="Winrate"
|
||||
value={`${(winrate * 100).toFixed(2)}%`}
|
||||
career={`${(careerWinrate * 100).toFixed(2)}%`}
|
||||
delta={winrate - careerWinrate}
|
||||
/>
|
||||
<RowStat
|
||||
name="WN8"
|
||||
value={WN8 === undefined ? '--' : WN8.toFixed(0)}
|
||||
career={careerWN8 === undefined ? '--' : careerWN8.toFixed(0)}
|
||||
percentile={WN8 === undefined ? undefined : getWN8Percentile(WN8)}
|
||||
/>
|
||||
<RowStat
|
||||
name="Damage"
|
||||
value={damage.toFixed(2)}
|
||||
career={careerDamage.toFixed(2)}
|
||||
delta={damage - careerDamage}
|
||||
/>
|
||||
<RowStat
|
||||
name="Survival"
|
||||
value={`${(survival * 100).toFixed(2)}%`}
|
||||
career={`${(careerSurvival * 100).toFixed(2)}%`}
|
||||
delta={survival - careerSurvival}
|
||||
/>
|
||||
<RowStat
|
||||
name="Battles"
|
||||
value={battles.toFixed(0)}
|
||||
career={careerBattles.toFixed(0)}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Battles • ${careerBattles.toFixed(0)}`}
|
||||
value={battles.toFixed(0)}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`}
|
||||
value={`${(winrate * 100).toFixed(2)}%`}
|
||||
delta={winrate - careerWinrate}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`WN8 • ${
|
||||
careerWN8 === undefined ? '--' : careerWN8.toFixed(0)
|
||||
}`}
|
||||
value={WN8 === undefined ? '--' : WN8.toFixed(0)}
|
||||
percentile={WN8 === undefined ? undefined : getWN8Percentile(WN8)}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Damage • ${careerDamage.toFixed(0)}`}
|
||||
value={damage.toFixed(0)}
|
||||
delta={damage - careerDamage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { theme } from '../../../stitches.config.js';
|
||||
|
||||
export interface RowDiscriminatorProps {
|
||||
name: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export function RowDiscriminator({ name, icon }: RowDiscriminatorProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
width: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{icon && (
|
||||
<img
|
||||
src={icon}
|
||||
height={0}
|
||||
style={{ flex: 1, width: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'clip',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Percentile } from '../../../constants/percentiles.js';
|
||||
import { theme } from '../../../stitches.config.js';
|
||||
import PercentileIndicator from '../../PercentileIndicator.js';
|
||||
import { Percentile } from '../../../constants/percentiles';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import PercentileIndicator from '../../PercentileIndicator';
|
||||
|
||||
export interface RowStatProps {
|
||||
name: string;
|
||||
value: string;
|
||||
career: string;
|
||||
value: string | number;
|
||||
delta?: number;
|
||||
percentile?: Percentile;
|
||||
minimized?: boolean;
|
||||
}
|
||||
|
||||
export function RowStat({
|
||||
name,
|
||||
value,
|
||||
career,
|
||||
delta,
|
||||
percentile,
|
||||
minimized,
|
||||
}: RowStatProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -28,10 +28,6 @@ export function RowStat({
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 12 }}>
|
||||
{career}
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -57,17 +53,19 @@ export function RowStat({
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 12 }}>
|
||||
{name}
|
||||
</span>
|
||||
{!minimized && (
|
||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 12 }}>
|
||||
{name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './components/Root.js';
|
||||
export * from './components/Row.js';
|
||||
export * from './components/RowDiscriminator.js';
|
||||
export * from './components/RowStat.js';
|
||||
export * from './components/Root';
|
||||
export * from './components/Row';
|
||||
export * from './components/RowStat';
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import getWN8Percentile from '../core/blitz/getWN8Percentile.js';
|
||||
import isNumber from '../core/node/isNumber.js';
|
||||
import { AllStats, SupplementaryStats } from '../types/accountInfo.js';
|
||||
import GenericStats from './GenericStats/index.js';
|
||||
|
||||
export interface GenericAllStatsProps {
|
||||
stats: AllStats;
|
||||
supplementaryStats?: SupplementaryStats;
|
||||
}
|
||||
|
||||
export default function GenericAllStats({
|
||||
stats,
|
||||
supplementaryStats,
|
||||
}: GenericAllStatsProps) {
|
||||
const damageRatio = stats.damage_dealt / stats.damage_received;
|
||||
const killsToDeathRatio =
|
||||
stats.frags / (stats.battles - stats.survived_battles);
|
||||
|
||||
return (
|
||||
<GenericStats
|
||||
stats={[
|
||||
['Winrate', `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`],
|
||||
[
|
||||
'WN8',
|
||||
supplementaryStats
|
||||
? isNumber(supplementaryStats.WN8)
|
||||
? supplementaryStats.WN8!.toFixed(0)
|
||||
: '--'
|
||||
: undefined,
|
||||
isNumber(supplementaryStats?.WN8)
|
||||
? getWN8Percentile(supplementaryStats!.WN8!)
|
||||
: undefined,
|
||||
],
|
||||
[
|
||||
'Survival',
|
||||
`${(100 * (stats.survived_battles / stats.battles)).toFixed(2)}%`,
|
||||
],
|
||||
['Accuracy', `${((stats.hits / stats.shots) * 100).toFixed(2)}%`],
|
||||
['Battles', stats.battles],
|
||||
['Wins', stats.wins],
|
||||
['Losses', stats.losses],
|
||||
['Average damage', (stats.damage_dealt / stats.battles).toFixed(0)],
|
||||
['Average tier', supplementaryStats?.tier.toFixed(2)],
|
||||
['Average XP', (stats.xp / stats.battles).toFixed(0)],
|
||||
['Average shots', (stats.shots / stats.battles).toFixed(2)],
|
||||
['Average hits', (stats.hits / stats.battles).toFixed(2)],
|
||||
['Average kills', (stats.frags / stats.battles).toFixed(2)],
|
||||
['Average spots', (stats.spotted / stats.battles).toFixed(2)],
|
||||
['Damage ratio', isFinite(damageRatio) ? damageRatio.toFixed(2) : '--'],
|
||||
[
|
||||
'Kills to death ratio',
|
||||
isFinite(killsToDeathRatio) ? killsToDeathRatio.toFixed(2) : '--',
|
||||
],
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Percentile } from '../../../constants/percentiles';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import PercentileIndicator from '../../PercentileIndicator';
|
||||
|
||||
export interface CardItem {
|
||||
label: string;
|
||||
value: string | number | undefined;
|
||||
percentile?: Percentile;
|
||||
}
|
||||
|
||||
export interface CardProps {
|
||||
items: CardItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function Card({ items, title }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
flex: 1,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.colors.componentInteractive,
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
{item.label}
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 1,
|
||||
borderRadius: 0.5,
|
||||
backgroundColor: theme.colors.borderNonInteractive,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{item.percentile !== undefined && (
|
||||
<PercentileIndicator percentile={item.percentile} />
|
||||
)}
|
||||
<span
|
||||
style={{ fontSize: 16, color: theme.colors.textHighContrast }}
|
||||
>
|
||||
{item.value === undefined || Number.isNaN(item.value)
|
||||
? '--'
|
||||
: item.value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface RootProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface RowProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Row({ children }: RowProps) {
|
||||
return <div style={{ display: 'flex', gap: 8 }}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import getWN8Percentile from '../../core/blitz/getWN8Percentile';
|
||||
import isNumber from '../../core/node/isNumber';
|
||||
import { AllStats, SupplementaryStats } from '../../types/accountInfo';
|
||||
import { Card } from './components/Card';
|
||||
import { Root } from './components/Root';
|
||||
import { Row } from './components/Row';
|
||||
|
||||
// TODO: remove the .js endings
|
||||
|
||||
export interface GenericAllStatsProps {
|
||||
stats: AllStats;
|
||||
supplementaryStats?: SupplementaryStats;
|
||||
}
|
||||
|
||||
export default function GenericAllStats({
|
||||
stats,
|
||||
supplementaryStats,
|
||||
}: GenericAllStatsProps) {
|
||||
const damageRatio = stats.damage_dealt / stats.damage_received;
|
||||
const killsToDeathRatio =
|
||||
stats.frags / (stats.battles - stats.survived_battles);
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<Row>
|
||||
<Card
|
||||
title="Games"
|
||||
items={[
|
||||
{
|
||||
label: 'Winrate',
|
||||
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
label: 'Battles',
|
||||
value: stats.battles,
|
||||
},
|
||||
{
|
||||
label: 'Wins / Losses',
|
||||
value: `${stats.wins} / ${stats.losses}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
title="Efficiency"
|
||||
items={[
|
||||
{
|
||||
label: 'WN8',
|
||||
value:
|
||||
supplementaryStats && isNumber(supplementaryStats.WN8)
|
||||
? supplementaryStats.WN8!.toFixed(0)
|
||||
: undefined,
|
||||
percentile: isNumber(supplementaryStats?.WN8)
|
||||
? getWN8Percentile(supplementaryStats!.WN8!)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
label: 'Survival',
|
||||
value: `${(
|
||||
100 *
|
||||
(stats.survived_battles / stats.battles)
|
||||
).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
label: 'Accuracy',
|
||||
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Card
|
||||
title="Lethality"
|
||||
items={[
|
||||
{
|
||||
label: 'Average damage',
|
||||
value: (stats.damage_dealt / stats.battles).toFixed(0),
|
||||
},
|
||||
{
|
||||
label: 'Damage per tier',
|
||||
value: supplementaryStats?.tier
|
||||
? (
|
||||
stats.damage_dealt /
|
||||
stats.battles /
|
||||
supplementaryStats.tier
|
||||
).toFixed(0)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
label: 'Damage ratio',
|
||||
value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined,
|
||||
},
|
||||
{
|
||||
label: 'Average kills',
|
||||
value: (stats.frags / stats.battles).toFixed(0),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
title="Miscellaneous"
|
||||
items={[
|
||||
{
|
||||
label: 'Average tier',
|
||||
value: supplementaryStats?.tier?.toFixed(2),
|
||||
},
|
||||
{
|
||||
label: 'Average spots',
|
||||
value: (stats.spotted / stats.battles).toFixed(2),
|
||||
},
|
||||
{
|
||||
label: 'Average XP',
|
||||
value: (stats.xp / stats.battles).toFixed(0),
|
||||
},
|
||||
{
|
||||
label: 'Kills to death ratio',
|
||||
value: isFinite(killsToDeathRatio)
|
||||
? killsToDeathRatio.toFixed(2)
|
||||
: undefined,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Row>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Stat } from '../index.js';
|
||||
import GenericStatsRow from './GenericStatsRow.js';
|
||||
import { Stat } from '..';
|
||||
import GenericStatsRow from './GenericStatsRow';
|
||||
|
||||
export interface GenericStatsColumnProps {
|
||||
stats: Stat[];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { theme } from '../../../stitches.config.js';
|
||||
import PercentileIndicator from '../../PercentileIndicator.js';
|
||||
import { Stat } from '../index.js';
|
||||
import { Stat } from '..';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import PercentileIndicator from '../../PercentileIndicator';
|
||||
|
||||
export interface GenericStatsRowProps {
|
||||
stat: Stat;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Percentile } from '../../constants/percentiles.js';
|
||||
import GenericStatsColumn from './components/GenericStatsColumn.js';
|
||||
import { Percentile } from '../../constants/percentiles';
|
||||
import GenericStatsColumn from './components/GenericStatsColumn';
|
||||
|
||||
export type Stat =
|
||||
| [string, string | number | undefined]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { blue, green, red, slateDark } from '@radix-ui/colors';
|
||||
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './Root.js';
|
||||
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './Root';
|
||||
|
||||
export enum LineColor {
|
||||
Red,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { times } from 'lodash';
|
||||
import { theme } from '../../../stitches.config.js';
|
||||
import { MarginInputProps } from './Root.js';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import { MarginInputProps } from './Root';
|
||||
|
||||
export enum MarginOrientation {
|
||||
Vertical,
|
||||
@@ -37,7 +37,7 @@ export function Margin({
|
||||
key={index}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.colors.textLowContrast,
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
{`${(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { times } from 'lodash';
|
||||
import { ReactNode } from 'react';
|
||||
import { theme } from '../../../stitches.config.js';
|
||||
import { Margin, MarginOrientation } from './Margin.js';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import { Margin, MarginOrientation } from './Margin';
|
||||
|
||||
export const GRAPH_WIDTH = 544;
|
||||
export const GRAPH_WIDTH = 384;
|
||||
export const GRAPH_HEIGHT = 320;
|
||||
|
||||
export interface MarginInputProps {
|
||||
@@ -20,6 +20,7 @@ export interface RootProps {
|
||||
separations?: number;
|
||||
xMinLabel: string;
|
||||
xMaxLabel: string;
|
||||
xTitle?: string;
|
||||
}
|
||||
|
||||
export function Root({
|
||||
@@ -29,6 +30,7 @@ export function Root({
|
||||
separations = 5,
|
||||
xMinLabel,
|
||||
xMaxLabel,
|
||||
xTitle,
|
||||
}: RootProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
@@ -39,10 +41,10 @@ export function Root({
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
|
||||
{xMinLabel}
|
||||
</span>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
|
||||
{xMaxLabel}
|
||||
</span>
|
||||
</div>
|
||||
@@ -88,6 +90,21 @@ export function Root({
|
||||
orientation={MarginOrientation.Horizontal}
|
||||
/>
|
||||
)}
|
||||
|
||||
{xTitle !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
{xTitle}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './components/Line.js';
|
||||
export * from './components/Margin.js';
|
||||
export * from './components/Root.js';
|
||||
export * from './components/Line';
|
||||
export * from './components/Margin';
|
||||
export * from './components/Root';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface GapProps {
|
||||
number: number;
|
||||
}
|
||||
|
||||
export function Gap({ number }: GapProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
backgroundColor: theme.colors.componentNonInteractive,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
+ {number.toLocaleString()} more
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { RatingsReward } from '../../../commands/ratings';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface ItemProps {
|
||||
position: number;
|
||||
deltaPosition?: number;
|
||||
score: number;
|
||||
deltaScore?: number;
|
||||
reward?: RatingsReward;
|
||||
nickname: string;
|
||||
clan?: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
function normalizeImage(url: string) {
|
||||
return url.endsWith('.webp') ? 'https://i.imgur.com/ZniCzbO.png' : url;
|
||||
}
|
||||
|
||||
export function Item({
|
||||
position,
|
||||
deltaPosition,
|
||||
score,
|
||||
deltaScore,
|
||||
reward,
|
||||
nickname,
|
||||
clan,
|
||||
highlight,
|
||||
}: ItemProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
backgroundColor: highlight
|
||||
? theme.colors.componentInteractive_blue
|
||||
: theme.colors.componentInteractive,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{position.toLocaleString()}.
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{nickname}
|
||||
</span>
|
||||
{clan && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
[{clan}]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{deltaPosition !== undefined && deltaPosition !== 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
}}
|
||||
src={
|
||||
deltaPosition > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{Math.abs(deltaPosition).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{reward && (
|
||||
<img
|
||||
style={{ width: 16, height: 16, objectFit: 'cover' }}
|
||||
src={normalizeImage(
|
||||
reward.type === 'vehicle'
|
||||
? reward.vehicle.image_url
|
||||
: reward.stuff.image_url,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{reward && reward.count > 1 && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
x {reward.count.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
width: 96,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{Math.abs(deltaScore).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||
<img
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
}}
|
||||
src={
|
||||
deltaScore > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{score.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface RootProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './components/Gap';
|
||||
export * from './components/Item';
|
||||
export * from './components/Root';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { theme } from '../stitches.config.js';
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
export enum NoDataType {
|
||||
BattlesInPeriod,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
teal,
|
||||
yellow,
|
||||
} from '@radix-ui/colors';
|
||||
import { Percentile } from '../constants/percentiles.js';
|
||||
import { Percentile } from '../constants/percentiles';
|
||||
|
||||
export interface PercentileIndicatorProps {
|
||||
percentile: Percentile;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { theme } from '../stitches.config.js';
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
export enum PoweredByType {
|
||||
BlitzStars,
|
||||
@@ -24,8 +24,13 @@ const POWERED_BY_NAMES: Record<PoweredByType, string> = {
|
||||
|
||||
export default function PoweredBy({ type, footer }: PoweredByProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex' }}>{footer}</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: footer === undefined ? 'space-around' : 'space-between',
|
||||
}}
|
||||
>
|
||||
{footer !== undefined && <div style={{ display: 'flex' }}>{footer}</div>}
|
||||
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<img src={POWERED_BY_IMAGES[type]} style={{ height: 16, width: 16 }} />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface ColumnProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Column({ children }: ColumnProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +1,81 @@
|
||||
import { TANK_ICONS, TankType } from '../../../core/blitz/tankopedia.js';
|
||||
import {
|
||||
TANK_ICONS,
|
||||
TANK_ICONS_COLLECTOR,
|
||||
TANK_ICONS_PREMIUM,
|
||||
TankType,
|
||||
} from '../../../core/blitz/tankopedia';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface ItemProps {
|
||||
icon?: string;
|
||||
type: TankType;
|
||||
name: string;
|
||||
export enum TreeTypeEnum {
|
||||
TechTree,
|
||||
Premium,
|
||||
Collector,
|
||||
}
|
||||
|
||||
export function Item({ icon, type, name }: ItemProps) {
|
||||
export type TreeTypeString = 'techtree' | 'premium' | 'collector';
|
||||
|
||||
export interface ItemProps {
|
||||
image?: string;
|
||||
tankType?: TankType;
|
||||
name: string;
|
||||
treeType: TreeTypeEnum;
|
||||
}
|
||||
|
||||
const TREE_TYPE_COLOR = {
|
||||
[TreeTypeEnum.TechTree]: '',
|
||||
[TreeTypeEnum.Premium]: '_amber',
|
||||
[TreeTypeEnum.Collector]: '_blue',
|
||||
} as const;
|
||||
|
||||
export const TREE_TYPE_ICONS = {
|
||||
[TreeTypeEnum.TechTree]: TANK_ICONS,
|
||||
[TreeTypeEnum.Premium]: TANK_ICONS_PREMIUM,
|
||||
[TreeTypeEnum.Collector]: TANK_ICONS_COLLECTOR,
|
||||
};
|
||||
|
||||
export function Item({ image, tankType, name, treeType }: ItemProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 128,
|
||||
height: 128,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 32,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
backgroundColor:
|
||||
theme.colors[`componentInteractive${TREE_TYPE_COLOR[treeType]}`],
|
||||
}}
|
||||
>
|
||||
{icon && <img src={icon} style={{ flex: 1, objectFit: 'contain' }} />}
|
||||
|
||||
<div style={{ display: 'flex', margin: 'auto', gap: 4 }}>
|
||||
<img src={TANK_ICONS[type]} style={{ width: 14, height: 14 }} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{tankType && (
|
||||
<img
|
||||
src={TREE_TYPE_ICONS[treeType][tankType]}
|
||||
style={{ width: 14, height: 14 }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
style={{ fontSize: 16, textAlign: 'center', whiteSpace: 'nowrap' }}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
whiteSpace: 'nowrap',
|
||||
color: theme.colors[`textLowContrast${TREE_TYPE_COLOR[treeType]}`],
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{image && <img src={image} style={{ flex: 1 }} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,8 @@ export function Root({ children }: RootProps) {
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface RowProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Row({ children }: RowProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface TitleProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
export function Title({ children }: TitleProps) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
color: theme.colors.textHighContrast,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
export * from './components/Item.js';
|
||||
export * from './components/Root.js';
|
||||
export * from './components/Column';
|
||||
export * from './components/Item';
|
||||
export * from './components/Root';
|
||||
export * from './components/Row';
|
||||
export * from './components/Title';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TIER_ROMAN_NUMERALS, Tier } from '../core/blitz/tankopedia.js';
|
||||
import { themeLight } from '../stitches.config.js';
|
||||
import { TIER_ROMAN_NUMERALS, Tier } from '../core/blitz/tankopedia';
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function TierWeights({ weights }: TierWeightsProps) {
|
||||
<div
|
||||
key={tier}
|
||||
style={{
|
||||
backgroundColor: themeLight.colors.appBackground1,
|
||||
backgroundColor: theme.colors.appBackground1_light,
|
||||
flex: weight,
|
||||
color: themeLight.colors.textHighContrast,
|
||||
color: theme.colors.textHighContrast_light,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
+13
-16
@@ -1,5 +1,4 @@
|
||||
import isDev from '../core/node/isDev.js';
|
||||
import { theme } from '../stitches.config.js';
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
export interface TitleBarProps {
|
||||
name: string;
|
||||
@@ -19,10 +18,11 @@ export default function TitleBar({
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8, flex: 1 }}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{image && (
|
||||
<img
|
||||
style={{ width: 64, height: 64, objectFit: 'contain' }}
|
||||
@@ -31,13 +31,18 @@ export default function TitleBar({
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
@@ -46,6 +51,8 @@ export default function TitleBar({
|
||||
color: theme.colors.textHighContrast,
|
||||
fontWeight: 900,
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: nameDiscriminator === undefined ? Infinity : 240,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
@@ -53,7 +60,6 @@ export default function TitleBar({
|
||||
{nameDiscriminator && (
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 32,
|
||||
color: theme.colors.textLowContrast,
|
||||
fontWeight: 900,
|
||||
@@ -70,15 +76,6 @@ export default function TitleBar({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
style={{ width: 64, height: 64 }}
|
||||
src={
|
||||
isDev()
|
||||
? 'https://i.imgur.com/j6TItdy.png'
|
||||
: 'https://i.imgur.com/pP6RN0x.png'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { theme } from '../stitches.config.js';
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
export interface WrapperProps {
|
||||
children: ReactNode;
|
||||
naked?: boolean;
|
||||
}
|
||||
|
||||
export default function Wrapper({ children }: WrapperProps) {
|
||||
export default function Wrapper({ children, naked }: WrapperProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -13,9 +14,12 @@ export default function Wrapper({ children }: WrapperProps) {
|
||||
flexDirection: 'column',
|
||||
padding: 16,
|
||||
gap: 32,
|
||||
width: 640,
|
||||
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||
width: 480,
|
||||
color: theme.colors.textHighContrast,
|
||||
|
||||
...(!naked && {
|
||||
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const CYCLIC_API = 'https://blitzkrieg.cyclic.app';
|
||||
export const CYCLIC_API = 'https://blitzkrieg-embeds.cyclic.app';
|
||||
@@ -0,0 +1,21 @@
|
||||
export type Region = 'com' | 'eu' | 'asia';
|
||||
export type RegionSubdomain = 'na' | 'eu' | 'asia';
|
||||
|
||||
export const REGION_NAMES: Record<Region, string> = {
|
||||
com: 'North America',
|
||||
eu: 'Europe',
|
||||
asia: 'Asia',
|
||||
};
|
||||
|
||||
// https://planetarynames.wr.usgs.gov/Abbreviations
|
||||
export const REGION_NAMES_SHORT: Record<Region, string> = {
|
||||
com: 'NA',
|
||||
eu: 'EU',
|
||||
asia: 'AS',
|
||||
};
|
||||
|
||||
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
|
||||
na: 'North America',
|
||||
eu: 'Europe',
|
||||
asia: 'Asia',
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export type BlitzServer = 'com' | 'eu' | 'asia';
|
||||
|
||||
export const BLITZ_SERVERS: Record<BlitzServer, string> = {
|
||||
com: 'North America',
|
||||
eu: 'Europe',
|
||||
asia: 'Asia',
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AllStats } from '../../types/accountInfo.js';
|
||||
import { AllStats } from '../../types/accountInfo';
|
||||
|
||||
/**
|
||||
* Formula from https://web.archive.org/web/20190105064817/http://wiki.wnefficiency.net/pages/WN8
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AllStats } from '../../types/accountInfo.js';
|
||||
import { AllStats } from '../../types/accountInfo';
|
||||
|
||||
export default function diffStats(stats1: AllStats, stats2: AllStats) {
|
||||
function diff(value: (allStats: AllStats) => number) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { BlitzServer } from '../../constants/servers.js';
|
||||
import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand.js';
|
||||
import throwError from '../node/throwError.js';
|
||||
import listClans from './listClans.js';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand';
|
||||
import throwError from '../node/throwError';
|
||||
import listClans from './listClans';
|
||||
|
||||
export default async function getBlitzClan(clan: string) {
|
||||
if (serverAndIdPattern.test(clan)) {
|
||||
const [server, accountId] = clan.split('/');
|
||||
return { server: server as BlitzServer, id: Number(accountId) };
|
||||
return { server: server as Region, id: Number(accountId) };
|
||||
} else {
|
||||
const accounts = await listClans(clan);
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { RatingsInfo } from '../../commands/ratings';
|
||||
import { Region } from '../../constants/regions';
|
||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||
|
||||
export default async function getRatingsInfo(region: Region) {
|
||||
const response = await fetch(
|
||||
`https://${regionToRegionSubdomain(
|
||||
region,
|
||||
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
);
|
||||
return (await response.json()) as Promise<RatingsInfo>;
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import { BLITZ_SERVERS, BlitzServer } from '../../constants/servers.js';
|
||||
import { TanksStats } from '../../types/tanksStats.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../node/arguments.js';
|
||||
import throwError from '../node/throwError.js';
|
||||
import getWargamingResponse from './getWargamingResponse.js';
|
||||
import { REGION_NAMES, Region } from '../../constants/regions';
|
||||
import { TanksStats } from '../../types/tanksStats';
|
||||
import { secrets } from '../node/secrets';
|
||||
import throwError from '../node/throwError';
|
||||
import getWargamingResponse from './getWargamingResponse';
|
||||
|
||||
export default async function getTankStats(server: BlitzServer, id: number) {
|
||||
export default async function getTankStats(server: Region, id: number) {
|
||||
const tankStats = await getWargamingResponse<TanksStats>(
|
||||
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
|
||||
if (tankStats[id] === null) {
|
||||
throw throwError(
|
||||
'No tank stats available',
|
||||
`Wargaming says there is no tank stats for this account. This account may not have any battles/tanks or exist in the ${BLITZ_SERVERS[server]} server.`,
|
||||
`Wargaming says there is no tank stats for this account. This account may not have any battles/tanks or exist in the ${REGION_NAMES[server]} server.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TreeTypeEnum } from '../../components/Tanks';
|
||||
import { tankopedia } from './tankopedia';
|
||||
|
||||
export default async function getTreeType(id: number) {
|
||||
const entry = (await tankopedia)[id];
|
||||
|
||||
if (entry?.is_collectible) return TreeTypeEnum.Collector;
|
||||
if (entry?.is_premium) return TreeTypeEnum.Premium;
|
||||
|
||||
return TreeTypeEnum.TechTree;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Percentile } from '../../constants/percentiles.js';
|
||||
import { Percentile } from '../../constants/percentiles';
|
||||
|
||||
export const WN8_PERCENTILES: [number, Percentile][] = [
|
||||
[0, Percentile.VeryBad],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import throwError from '../node/throwError.js';
|
||||
import throwError from '../node/throwError';
|
||||
|
||||
export type WargamingResponse<Data extends object> =
|
||||
| {
|
||||
|
||||
+10
-10
@@ -1,7 +1,7 @@
|
||||
import { BlitzServer } from '../../constants/servers.js';
|
||||
import { Clan, ClanList } from '../../types/clanList.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../node/arguments.js';
|
||||
import getWargamingResponse from './getWargamingResponse.js';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { Clan, ClanList } from '../../types/clanList';
|
||||
import { secrets } from '../node/secrets';
|
||||
import getWargamingResponse from './getWargamingResponse';
|
||||
|
||||
export type ClanListWithServer = (Clan & {
|
||||
server: 'com' | 'eu' | 'asia';
|
||||
@@ -15,33 +15,33 @@ export default async function listClans(search: string, limit = 9) {
|
||||
return (
|
||||
await Promise.all([
|
||||
getWargamingResponse<ClanList>(
|
||||
`https://api.wotblitz.com/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.com/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'com' as BlitzServer,
|
||||
server: 'com' as Region,
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<ClanList>(
|
||||
`https://api.wotblitz.eu/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.eu/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'eu' as BlitzServer,
|
||||
server: 'eu' as Region,
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<ClanList>(
|
||||
`https://api.wotblitz.asia/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.asia/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'asia' as BlitzServer,
|
||||
server: 'asia' as Region,
|
||||
})),
|
||||
),
|
||||
])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BlitzServer } from '../../constants/servers.js';
|
||||
import { Account, AccountList } from '../../types/accountList.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../node/arguments.js';
|
||||
import getWargamingResponse from './getWargamingResponse.js';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { Account, AccountList } from '../../types/accountList';
|
||||
import { secrets } from '../node/secrets';
|
||||
import getWargamingResponse from './getWargamingResponse';
|
||||
|
||||
export type AccountListWithServer = (Account & {
|
||||
server: 'com' | 'eu' | 'asia';
|
||||
@@ -19,33 +19,33 @@ export default async function listPlayers(search: string, limit = 9) {
|
||||
return (
|
||||
await Promise.all([
|
||||
getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.com/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.com/wotb/account/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'com' as BlitzServer,
|
||||
server: 'com' as Region,
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.eu/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.eu/wotb/account/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'eu' as BlitzServer,
|
||||
server: 'eu' as Region,
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.asia/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.asia/wotb/account/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'asia' as BlitzServer,
|
||||
server: 'asia' as Region,
|
||||
})),
|
||||
),
|
||||
])
|
||||
|
||||
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user