Compare commits

...
82 Commits
Author SHA1 Message Date
TrèsAbhi 331040bbdf Merge pull request #26 from tresabhi/1.12.3 2023-09-03 13:21:00 -05:00
TresAbhi 70e1d4b30d feat: changelogs 2023-09-03 18:20:19 +00:00
TresAbhi 03dbd797fb feat: no ongoing season proof 2023-09-03 18:15:10 +00:00
TrèsAbhi 28172f7f8f Merge pull request #25 from tresabhi/1.12.2 2023-08-23 09:52:48 -05:00
TresAbhi cb5347cbb8 feat: changelog 2023-08-23 14:51:51 +00:00
TresAbhi 1ce0781d2d feat: use application directory 2023-08-23 00:34:37 +00:00
TresAbhi 1257aa76d7 feat: code split bot 2023-08-23 00:33:26 +00:00
TresAbhi 6ec3835896 feat: updated dependencies 2023-08-23 00:18:25 +00:00
TresAbhi 506fe7d9bd feat: removed unused dependencies 2023-08-23 00:17:25 +00:00
TresAbhi 14ef9f6295 feat: removed unused files 2023-08-23 00:17:03 +00:00
TresAbhi 0d76e1a29e feat: added knip 2023-08-23 00:16:39 +00:00
TresAbhi 9e42d7e8f5 feat: removed powered by footer 2023-08-23 00:15:45 +00:00
TresAbhi 364f97332b feat: better and unified tier selection 2023-08-23 00:14:41 +00:00
TresAbhi caa7bb8a65 fix: removed random dev console log 2023-08-22 23:49:34 +00:00
TresAbhi 3e956b34c1 fix: clan name and icon now show up with multi-tank 2023-08-22 23:49:00 +00:00
TresAbhi 038374e5aa fix: changed examples to guide in navbar 2023-08-22 23:47:55 +00:00
TresAbhi dafd54f830 feat: github link 2023-08-22 23:47:35 +00:00
TresAbhi cb65e6b0af fix: corrected order of changelog versions 2023-08-22 23:46:59 +00:00
TresAbhi 4ca8590c83 fix: guide link 2023-08-22 23:46:15 +00:00
TresAbhi c5ef02d7e6 feat: updated dependencies 2023-08-22 23:45:47 +00:00
TrèsAbhi 8175aa8715 Merge pull request #24 from tresabhi/1.12.1 2023-08-11 13:03:52 -05:00
TresAbhi b426634659 feat: changelog 2023-08-11 18:03:41 +00:00
TresAbhi e92da1b700 fix: revert username pattern infrence 2023-08-11 18:01:31 +00:00
TresAbhi 7e0a2e88ee fix: tank id/index mismatch 2023-08-11 17:57:44 +00:00
TresAbhi 16afa8c2d4 feat: url change 2023-08-10 19:48:27 +00:00
TresAbhi 907288455a changed to js 2023-08-10 19:28:53 +00:00
TrèsAbhi 64689e4e3b Update package.json 2023-08-10 14:24:47 -05:00
TresAbhi 294377846a empty commit 2023-08-10 19:23:09 +00:00
TresAbhi 70e4cbaba2 fix: argv undefined 2023-08-10 19:14:43 +00:00
TrèsAbhi 9e2f9526fb Update package.dist.json 2023-08-10 13:58:15 -05:00
TrèsAbhi 84bec20fa5 Update cd-host.yml 2023-08-10 13:55:56 -05:00
TrèsAbhi e9a492fde6 Merge pull request #23 from tresabhi/1.12.0 2023-08-10 13:52:10 -05:00
TresAbhi 76a69f08b4 feat: docs 2023-08-10 18:51:22 +00:00
TresAbhi 893110d3ef feat: added 1 more decimal of precision in evolution 2023-08-10 01:49:05 +00:00
TresAbhi 70575fa9cf feat: accent-tolerant tank search 2023-08-10 01:46:39 +00:00
TresAbhi bb2450eb3c feat: added the new /today embeds 2023-08-10 01:31:05 +00:00
TresAbhi ab56689326 feat: updated /today with figma 2023-08-10 01:03:53 +00:00
TresAbhi a9cf07480a feat: added arabic numerals to owned tanks options 2023-08-10 00:37:54 +00:00
TresAbhi c81de2d758 feat: removed interaction based search param inference 2023-08-09 18:02:15 +00:00
TresAbhi aa1c7639f2 feat: auto refresh rate changed to every 10 seconds 2023-08-09 18:00:23 +00:00
TresAbhi d448101b32 fix: swapped start and end min and max 2023-08-09 17:56:35 +00:00
TresAbhi e806f0ede2 fix: swapped min and max for start and end 2023-08-09 17:51:19 +00:00
TresAbhi 8253c27333 fix: search parameters are now passed correctly 2023-08-09 17:39:21 +00:00
TresAbhi 254392370f feat: only interpret correctly formatter usernames 2023-08-09 17:27:38 +00:00
TresAbhi d2319688a5 fix: removed multi-tank from evolution 2023-08-09 17:25:05 +00:00
TresAbhi ffebe6c696 fix: use regional season info instead of hard-coded NA 2023-08-09 17:20:03 +00:00
TresAbhi 9e4094f3a9 feat: added ratings neighbours 2023-08-09 17:13:26 +00:00
TresAbhi f7eadb08a9 feat: made /ratings data-driven 2023-08-09 00:47:15 +00:00
TresAbhi d511cfa4cc fix: fixed ratings reward alignment 2023-08-09 00:35:33 +00:00
TresAbhi 270a3051b7 feat: updated /owned-tanks to new width 2023-08-09 00:19:50 +00:00
TresAbhi 81bc9f9ba6 feat: updated /inactive 2023-08-07 22:54:55 +00:00
TresAbhi 338cec4f18 feat: updated full-stats to new subcommands 2023-08-07 22:51:04 +00:00
TresAbhi b455e3380f feat: added minimum character limit to clan autocomplete 2023-08-06 19:13:28 +00:00
TresAbhi 92761f2c55 feat: made footer center aligned when without footer text 2023-08-06 19:08:02 +00:00
TresAbhi 7823b8ad2e feat: updated /evolution to new width 2023-08-06 19:07:44 +00:00
TresAbhi cac82f3124 fix: renamed statsfull to fullstats in power by bar 2023-08-06 19:00:32 +00:00
TresAbhi 4b892d06e4 fix: import types from stats instead of fullstats 2023-08-06 19:00:00 +00:00
TresAbhi 92149b0f00 feat: matched titlebar style to figma 2023-08-06 18:53:57 +00:00
TresAbhi 399aa4c03c feat: removed region text in title bar 2023-08-06 18:41:59 +00:00
TresAbhi d5de715032 fix: title bar spacing is now 4px just like figma 2023-08-06 18:39:23 +00:00
TresAbhi e6fb179184 feat: removed self-advert from title bar 2023-08-06 18:38:49 +00:00
TresAbhi af54f12ec3 feat: set width to 480px 2023-08-06 18:38:17 +00:00
TresAbhi 909cabd517 feat: filter functionality 2023-08-04 19:49:40 +00:00
TresAbhi a734a71d2f fix: duplicate efficienty cards 2023-08-04 18:25:20 +00:00
TresAbhi 91dc45c880 feat: added more mature periods 2023-08-04 18:24:20 +00:00
TresAbhi 91f364b76a feat: used dahses for command readibility 2023-08-04 02:24:14 +00:00
TresAbhi 23dd088466 feat: better subcommand organiztion 2023-08-04 02:13:21 +00:00
TresAbhi 2cabcc57f4 feat: moved ratings out of preview 2023-07-31 01:32:31 +00:00
TrèsAbhi dd6092a5e7 fix: GH_TOKEN 2023-07-17 19:37:12 -05:00
TrèsAbhi 3c2861f956 Merge pull request #22 from tresabhi/1.11.0 2023-07-17 19:30:50 -05:00
TresAbhi 894b841769 feat: changelogs 2023-07-18 00:29:49 +00:00
TresAbhi 62627c96a3 feat: added other regions 2023-07-18 00:23:59 +00:00
TresAbhi c173ece882 feat: completed /ratings league 2023-07-18 00:17:00 +00:00
TresAbhi eca7cbbbeb feat: added more mature secrets handling 2023-07-16 18:26:33 +00:00
TresAbhi c72bfa9f71 feat: added rewards 2023-07-16 18:15:34 +00:00
TresAbhi b4ae39a509 feat: added custom lighter ratings leaderboard 2023-07-16 17:56:37 +00:00
TresAbhi 631c051895 feat: ditch histories 2023-07-16 17:51:01 +00:00
TresAbhi 9a2e11a2a4 feat: added basic rendering 2023-07-16 17:45:29 +00:00
TresAbhi aa6011b18d feat: added basic infrastructure for ratings command 2023-07-15 18:44:20 +00:00
TresAbhi 9419aec116 fix: removed "Europe" from /verify example 2023-07-15 18:07:14 +00:00
TresAbhi 638402589f feat: switched scripts to typescript 2023-07-15 01:21:59 +00:00
TrèsAbhi 6d6a6bf738 fix: workflow region names 2023-07-14 12:41:45 -05:00
111 changed files with 3991 additions and 1722 deletions

No files matched your search

+4 -4
View File
@@ -1,8 +1,8 @@
name: CD North America Ratings Latest name: CD Asia Ratings Latest
on: on:
schedule: schedule:
- cron: '0 0 * * *' - cron: "0 0 * * *"
jobs: jobs:
host: host:
@@ -16,7 +16,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: 'npm' cache: "npm"
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,7 +24,7 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: '**/node_modules' path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
@@ -1,8 +1,8 @@
name: CD North America Ratings Leaderboards name: CD Asia Ratings Leaderboards
on: on:
schedule: schedule:
- cron: '0 17 * * *' - cron: "0 17 * * *"
jobs: jobs:
host: host:
@@ -16,7 +16,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: 'npm' cache: "npm"
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,7 +24,7 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: '**/node_modules' path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
+4 -4
View File
@@ -1,8 +1,8 @@
name: CD North America Ratings Latest name: CD Europe Ratings Latest
on: on:
schedule: schedule:
- cron: '0 0 * * *' - cron: "0 0 * * *"
jobs: jobs:
host: host:
@@ -16,7 +16,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: 'npm' cache: "npm"
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,7 +24,7 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: '**/node_modules' path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
@@ -1,8 +1,8 @@
name: CD North America Ratings Leaderboards name: CD Europe Ratings Leaderboards
on: on:
schedule: schedule:
- cron: '0 23 * * *' - cron: "0 23 * * *"
jobs: jobs:
host: host:
@@ -16,7 +16,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: 'npm' cache: "npm"
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,7 +24,7 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: '**/node_modules' path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
+1 -1
View File
@@ -29,4 +29,4 @@ jobs:
unzip dist.zip unzip dist.zip
- name: Run bot - 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
+2
View File
@@ -28,6 +28,7 @@
"statsfull", "statsfull",
"Stringifying", "Stringifying",
"strvk", "strvk",
"subnation",
"tankaverages", "tankaverages",
"tankopedia", "tankopedia",
"tankstats", "tankstats",
@@ -37,6 +38,7 @@
"vickers", "vickers",
"vitepress", "vitepress",
"wargaming", "wargaming",
"webp",
"winrate" "winrate"
], ],
"deno.enable": false, "deno.enable": false,
+21 -8
View File
@@ -1,4 +1,7 @@
import { DefaultTheme, defineConfig } from 'vitepress'; import { compareVersions } from 'compare-versions';
import { readdirSync } from 'fs';
import { parse } from 'path';
import { defineConfig } from 'vitepress';
import { getSidebar } from 'vitepress-plugin-auto-sidebar'; import { getSidebar } from 'vitepress-plugin-auto-sidebar';
export default defineConfig({ export default defineConfig({
@@ -9,17 +12,27 @@ export default defineConfig({
themeConfig: { themeConfig: {
nav: [ nav: [
{ text: 'Home', link: '/' }, { text: 'Home', link: '/' },
{ text: 'Examples', link: '/markdown-examples' }, { text: 'Guide', link: '/guide/about' },
], ],
sidebar: getSidebar({ sidebar: [
collapsed: false, ...getSidebar({
contentDirs: ['guide', 'legal', 'changelogs'], collapsed: false,
contentRoot: '/docs/', contentDirs: ['guide', 'legal'],
}) as DefaultTheme.Sidebar, contentRoot: '/docs/',
}),
{
text: 'Changelogs',
items: readdirSync('./docs/changelogs')
.map((file) => parse(file).name)
.sort(compareVersions)
.map((name) => ({ text: name, link: `/changelogs/${name}.html` })),
},
],
socialLinks: [ socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }, { icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' },
], ],
}, },
}); });
+22
View File
@@ -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
+54
View File
@@ -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
+8
View File
@@ -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
+29
View File
@@ -0,0 +1,29 @@
# Blitzkrieg 1.12.2
A much needed room service is here!
## Changes
- Tier selection is now easier
- Tier options across all commands have been unified to a single style
- Tiers are now sorted from `10` to `1`
- Tiers now show up in the format `Tier {tier_number} - {tier_roman_numeral}` (example: `Tier 8 - VIII`)
- Removed powered by footer as links are provided regardless
- Changed invite instructions to instead use application directory
## Fixes
- Guide link on the website is now fixed
- Versions now show up in the correct order in the navigation bar
- Added Blitzkrieg's GitHub link instead of the default Vitepress
- Changed examples to guide in the navigation bar
- Clan name and icon now show up with multi-tank filters
- Removed a random `console.log`
## Technical Changes
- Added `knip`
- Removed a lot of unused files and dependencies
- Updated all dependencies to their latest
- Code split `bot.ts`
- `client` and `octokit` now reside separately
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.12.3
3-day downtime fixed!
## Changes
- Split `/ratings` into `/ratings current` and `/ratings archive`
- `current` now properly handles when there is no ongoing ratings season
- `archive` displays older archived seasons
## Fixes
- The bot is now no ratings seasons tolerant
+1 -8
View File
@@ -1,10 +1,3 @@
# Inviting the Bot # Inviting the Bot
It's quite simple really. Just follow the steps below: Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
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.
+1 -1
View File
@@ -15,7 +15,7 @@ hero:
link: guide/install link: guide/install
- theme: alt - theme: alt
text: Guide text: Guide
link: /guide link: /guide/about
features: features:
- title: Clean and Efficient UI - title: Clean and Efficient UI
+2 -1
View File
@@ -1,5 +1,6 @@
{ {
"main": "server.cjs", "main": "server.js",
"type": "module",
"scripts": { "scripts": {
"start": "node --enable-source-maps --trace-warnings server.cjs" "start": "node --enable-source-maps --trace-warnings server.cjs"
} }
+21 -17
View File
@@ -1,14 +1,14 @@
{ {
"name": "blitzkrieg", "name": "blitzkrieg",
"version": "1.10.2", "version": "1.12.1",
"description": "", "description": "🎉 All-in-one Discord bot for everything World of Tanks Blitz",
"main": "src/server.ts", "main": "src/server.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build:bot": "node scripts/build --build=bot", "build:bot": "node scripts/build --build=bot",
"build:server": "node scripts/build --build=server", "build:server": "node scripts/build --build=server",
"build:docs": "vitepress build docs", "build:docs": "vitepress build docs",
"build:ratingsleaderboard": "node scripts/generateRatingsLeaderboard", "build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
"build": "node scripts/build --build=all", "build": "node scripts/build --build=all",
"dev:bot": "yarn build:bot --dev && yarn start:bot", "dev:bot": "yarn build:bot --dev && yarn start:bot",
"dev:server": "yarn build:server --dev && yarn start:server", "dev:server": "yarn build:server --dev && yarn start:server",
@@ -16,38 +16,42 @@
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs", "start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs", "start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
"host:docs": "vitepress preview docs", "host:docs": "vitepress preview docs",
"publish:ratingsleaderboard": "node scripts/generateRatingsLeaderboard --publish", "publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
"zip": "zip -r dist.zip dist", "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)\"" "format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"knip": "knip",
"update": "yarn upgrade --latest"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/colors": "^0.1.8", "@octokit/rest": "^20.0.1",
"@radix-ui/colors": "^2.1.0",
"@resvg/resvg-js": "^2.4.1", "@resvg/resvg-js": "^2.4.1",
"bepaint": "^0.1.0-beta.3", "bepaint": "^0.1.0-beta.3",
"discord.js": "^14.9.0", "discord.js": "^14.13.0",
"esbuild": "^0.18.10", "esbuild": "^0.19.2",
"escape-html": "^1.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"fuzzysort": "^2.0.4", "fuzzysort": "^2.0.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-escape": "^2.0.0", "markdown-escape": "^2.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"satori": "^0.9.1", "satori": "^0.10.3",
"vite-node": "^0.34.2",
"vitepress-plugin-auto-sidebar": "^1.1.0" "vitepress-plugin-auto-sidebar": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@octokit/rest": "^20.0.1", "@total-typescript/ts-reset": "^0.5.1",
"@total-typescript/ts-reset": "^0.4.2",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/lodash": "^4.14.195", "@types/lodash": "^4.14.197",
"@types/markdown-escape": "^1.1.0", "@types/markdown-escape": "^1.1.0",
"@types/node": "^20.2.1", "@types/node": "^20.5.3",
"@types/react": "^18.2.7", "@types/react": "^18.2.21",
"prettier": "^3.0.0", "compare-versions": "^6.1.0",
"knip": "^2.19.9",
"prettier": "^3.0.2",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vitepress": "^1.0.0-beta.3" "vitepress": "^1.0.0-rc.4"
} }
} }
+6 -7
View File
@@ -1,5 +1,5 @@
import { build } from 'esbuild'; import { build } from 'esbuild';
import { copyFile, mkdir, rm } from 'fs/promises'; import { copyFile, mkdirSync, rmSync } from 'fs';
import { argv } from 'process'; import { argv } from 'process';
const isProd = !argv.includes('--dev'); const isProd = !argv.includes('--dev');
@@ -10,15 +10,15 @@ const buildServer = argv.includes('--build=server') || buildAll;
if (isProd) { if (isProd) {
// only remove in production to ensure no time is wasted in dev // only remove in production to ensure no time is wasted in dev
console.log('Removing dist...'); console.log('Removing dist...');
await rm('dist', { recursive: true, force: true }); rmSync('dist', { recursive: true, force: true });
console.log('Dist removed'); console.log('Dist removed');
console.log('Creating dist...'); console.log('Creating dist...');
await mkdir('dist'); mkdirSync('dist');
console.log('Dist created'); console.log('Dist created');
console.log('Copying package.json...'); console.log('Copying package.json...');
copyFile('package.dist.json', 'dist/package.json').then(() => copyFile('package.dist.json', 'dist/package.json', () =>
console.log('package.json copied'), console.log('package.json copied'),
); );
} }
@@ -29,6 +29,7 @@ const commonOptions = {
'.node': 'copy', '.node': 'copy',
'.ttf': 'file', '.ttf': 'file',
}, },
format: 'cjs',
bundle: true, bundle: true,
sourcemap: true, sourcemap: true,
@@ -46,9 +47,7 @@ if (buildBot) {
entryPoints: ['src/bot.ts'], entryPoints: ['src/bot.ts'],
outfile: 'dist/bot.cjs', outfile: 'dist/bot.cjs',
}).then(() => { }).then(() => console.log('Bot built'));
console.log('Bot built');
});
} }
if (buildServer) { if (buildServer) {
@@ -1,6 +1,11 @@
import { Octokit } from '@octokit/rest'; import { Octokit } from '@octokit/rest';
import { rm, writeFile } from 'fs/promises'; import { rm, writeFile } from 'fs/promises';
import { argv, env } from 'process'; import { argv, env } from 'process';
import {
RatingsInfo,
RatingsNeighbors,
RatingsPlayer,
} from '../src/commands/ratings';
import commitMultipleFiles from './commitMultipleFiles.js'; import commitMultipleFiles from './commitMultipleFiles.js';
/* /*
@@ -8,11 +13,13 @@ import commitMultipleFiles from './commitMultipleFiles.js';
* Central European Time (UTC+1): use 0 23 * * *. * Central European Time (UTC+1): use 0 23 * * *.
* Central Asia Standard Time (UTC+7): use 0 17 * * *. * Central Asia Standard Time (UTC+7): use 0 17 * * *.
* *
* :region/ratings/:season/info.json: season info like rewards, start & end time, etc. * na/ratings/49/info.json: season info like rewards, start & end time, etc.
* :region/ratings/:season/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight * na/ratings/49/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
* :region/ratings/:season/leaderboards/:time.json: collection of all recorded leaderboards at local midnights * 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 publish = argv.includes('--publish');
const latest = argv.includes('--latest'); const latest = argv.includes('--latest');
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1]; const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
@@ -23,22 +30,22 @@ const NEIGHBORS = 1000;
const PLAYERS = await fetch( const PLAYERS = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
) )
.then((response) => response.json()) .then((response) => response.json() as Promise<RatingsInfo>)
.then((data) => data.count); .then((data) => data.count);
const players = {}; const players: Record<number, RatingsPlayer> = {};
const leaderboard = []; const leaderboard: BlitzkriegRatingsLeaderboard = [];
let registered = 0; let registered = 0;
async function branchFromPlayer(id) { async function branchFromPlayer(id: number) {
const { neighbors } = await fetch( const { neighbors } = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
).then((response) => response.json()); ).then((response) => response.json() as Promise<RatingsNeighbors>);
const firstPlayer = neighbors[0]; const firstPlayer = neighbors[0];
const lastPlayer = neighbors[neighbors.length - 1]; const lastPlayer = neighbors[neighbors.length - 1];
const branches = []; const branches: Promise<void>[] = [];
if (!players[firstPlayer.number]) if (!players[firstPlayer.number])
branches.push(branchFromPlayer(firstPlayer.spa_id)); branches.push(branchFromPlayer(firstPlayer.spa_id));
if (!players[lastPlayer.number]) if (!players[lastPlayer.number])
@@ -61,10 +68,10 @@ async function branchFromPlayer(id) {
await Promise.all(branches); await Promise.all(branches);
} }
async function branchFromLeague(id) { async function branchFromLeague(id: number) {
const { result } = await fetch( const { result } = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
).then((response) => response.json()); ).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>);
const firstPlayer = result[0]; const firstPlayer = result[0];
const lastPlayer = result[result.length - 1]; const lastPlayer = result[result.length - 1];
@@ -75,7 +82,7 @@ async function branchFromLeague(id) {
]); ]);
} }
console.log('Branching form leagues...'); console.log('Branching from leagues...');
await Promise.all([ await Promise.all([
branchFromLeague(0), branchFromLeague(0),
branchFromLeague(1), branchFromLeague(1),
@@ -86,8 +93,13 @@ await Promise.all([
console.log('Converting to an array...'); console.log('Converting to an array...');
for (let index = 0; index < PLAYERS; index++) { for (let index = 0; index < PLAYERS; index++) {
leaderboard[index] = players[index + 1]; const listing = players[index + 1];
if (!leaderboard[index]) console.warn(`${index} had no player`); if (!listing) console.warn(`Position ${index + 1} had no player`);
leaderboard[index] = {
id: listing.spa_id,
score: listing.score,
};
} }
const leaderboardJSON = JSON.stringify(leaderboard); const leaderboardJSON = JSON.stringify(leaderboard);
@@ -97,14 +109,13 @@ if (publish) {
const info = await fetch( const info = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then((response) => response.json()); ).then((response) => response.json() as Promise<RatingsInfo>);
const time = Math.round(Date.now() / 1000);
const normalizedServer = server === 'na' ? 'com' : server; const normalizedServer = server === 'na' ? 'com' : server;
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`; const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${ const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
latest ? 'latest.json' : `leaderboards/${time}.json` latest ? 'latest' : 'midnight'
}`; }.json`;
const infoJSON = JSON.stringify(info); const infoJSON = JSON.stringify(info);
console.log(`Publishing to season ${info.current_season}...`); console.log(`Publishing to season ${info.current_season}...`);
@@ -113,7 +124,7 @@ if (publish) {
'tresabhi', 'tresabhi',
'blitzkrieg-db', 'blitzkrieg-db',
'main', 'main',
`${info.current_season}/${time}`, new Date().toString(),
[ [
{ path: infoPath, content: infoJSON }, { path: infoPath, content: infoJSON },
{ path: leaderboardPath, content: leaderboardJSON }, { path: leaderboardPath, content: leaderboardJSON },
@@ -1,10 +1,12 @@
import { Octokit } from '@octokit/rest';
export default async function commitMultipleFiles( export default async function commitMultipleFiles(
octokit, octokit: Octokit,
owner, owner: string,
repo, repo: string,
branch, branch: string,
message, message: string,
changes, changes: { path: string; content: string }[],
) { ) {
// Get the latest commit SHA on the specified branch // Get the latest commit SHA on the specified branch
const { data: refData } = await octokit.git.getRef({ const { data: refData } = await octokit.git.getRef({
@@ -31,12 +33,13 @@ export default async function commitMultipleFiles(
content: change.content, content: change.content,
encoding: 'utf-8', encoding: 'utf-8',
}); });
return { return {
sha: blobData.sha, sha: blobData.sha,
path: change.path, path: change.path,
mode: '100644', mode: '100644',
type: 'blob', type: 'blob',
}; } as const;
}), }),
); );
+3 -13
View File
@@ -1,16 +1,6 @@
import { Client, GatewayIntentBits } from 'discord.js'; import { client } from './core/discord/client';
import { DISCORD_TOKEN } from './core/node/arguments'; import { secrets } from './core/node/secrets';
import { registerErrorHandlers } from './events/error'; 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],
})
.on('ready', ready)
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate);
registerErrorHandlers(); registerErrorHandlers();
client.login(DISCORD_TOKEN); client.login(secrets.DISCORD_TOKEN);
+2 -2
View File
@@ -1,8 +1,8 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import packageJSON from '../../package.json' assert { type: 'json' }; import packageJSON from '../../package.json' assert { type: 'json' };
import { client } from '../bot';
import { tankopedia } from '../core/blitz/tankopedia'; import { tankopedia } from '../core/blitz/tankopedia';
import { tankAverages } from '../core/blitzstars/tankAverages'; import { tankAverages } from '../core/blitzstars/tankAverages';
import { client } from '../core/discord/client';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable'; import markdownTable from '../core/discord/markdownTable';
import getClientId from '../core/node/getClientId'; import getClientId from '../core/node/getClientId';
@@ -12,7 +12,7 @@ const executionStart = new Date().getTime();
export const debugCommand: CommandRegistry = { export const debugCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
+4 -4
View File
@@ -12,7 +12,7 @@ import embedNegative from '../core/discord/embedNegative';
import embedPositive from '../core/discord/embedPositive'; import embedPositive from '../core/discord/embedPositive';
import markdownTable, { TableInput } from '../core/discord/markdownTable'; import markdownTable, { TableInput } from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
import { PossiblyPromise } from '../types/possiblyPromise'; import { PossiblyPromise } from '../types/possiblyPromise';
@@ -26,7 +26,7 @@ export const SKILLED_CLANS: Record<SkilledClan, string> = {
export const eligibleCommand: CommandRegistry = { export const eligibleCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: false, inPublic: false,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -49,9 +49,9 @@ export const eligibleCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const clan = interaction.options.getString('clan') as SkilledClan; 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>( 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 problems: TableInput = [];
const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`; const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
+75 -68
View File
@@ -12,76 +12,83 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand'; import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import evolution from '../renderers/evolution'; import evolution from '../renderers/evolution';
import { StatType } from '../renderers/fullStats'; import { StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const evolutionCommand: CommandRegistry = { export const evolutionCommand = new Promise<CommandRegistryRaw>(
inProduction: true, async (resolve) => {
inDevelopment: false, const command = await addStatTypeSubCommandGroups(
inPublic: true, new SlashCommandBuilder()
.setName('evolution')
command: addStatTypeSubCommandGroups( .setDescription('Evolution of statistics'),
new SlashCommandBuilder() false,
.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(player.server, 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 player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.server, interaction);
return await evolution(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
); );
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')!),
);
},
});
}, },
}; );
+97 -66
View File
@@ -1,4 +1,5 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../components/Tanks';
import { CYCLIC_API } from '../constants/cyclic'; import { CYCLIC_API } from '../constants/cyclic';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankId from '../core/blitz/resolveTankId'; import resolveTankId from '../core/blitz/resolveTankId';
@@ -12,73 +13,103 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand'; import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import fullStats, { StatType } from '../renderers/fullStats'; import fullStats from '../renderers/fullStats';
import { MultiTankFilters, StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const fullStatsCommand: CommandRegistry = { export const fullStatsCommand = new Promise<CommandRegistryRaw>(
inProduction: true, async (resolve) => {
inDevelopment: false, const command = await addStatTypeSubCommandGroups(
inPublic: true, new SlashCommandBuilder()
.setName('full-stats')
command: addStatTypeSubCommandGroups( .setDescription('Full in-game statistics'),
new SlashCommandBuilder()
.setName('fullstats')
.setDescription('Full in-game statistics'),
),
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.server, 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 fullStats(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 player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.server, interaction);
return await fullStats(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
); );
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];
const tierOption = interaction.options.getString('tier');
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: tierOption ? parseInt(tierOption) : 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')!),
);
},
});
}, },
}; );
+5 -4
View File
@@ -30,7 +30,7 @@ function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
export const helpCommand: CommandRegistry = { export const helpCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: addDocsSubcommands( command: addDocsSubcommands(
@@ -44,13 +44,14 @@ export const helpCommand: CommandRegistry = {
), ),
), ),
handler(interaction) { async handler(interaction) {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();
if (subcommand === 'commands') { if (subcommand === 'commands') {
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${COMMANDS_RAW.filter( return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
(registry) => registry.inPublic && registry.inProduction, await Promise.all(COMMANDS_RAW)
) )
.filter((registry) => registry.inPublic && registry.inProduction)
.map( .map(
(registry) => (registry) =>
`- \`/${registry.command.name}\`: ${registry.command.description}`, `- \`/${registry.command.name}\`: ${registry.command.description}`,
+7 -14
View File
@@ -1,15 +1,13 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import GenericStats from '../components/GenericStats'; import GenericStats from '../components/GenericStats';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_DOMAIN_NAMES } from '../constants/regions';
import getBlitzClan from '../core/blitz/getBlitzClan'; import getBlitzClan from '../core/blitz/getBlitzClan';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import addClanChoices from '../core/discord/addClanChoices'; import addClanChoices from '../core/discord/addClanChoices';
import autocompleteClan from '../core/discord/autocompleteClan'; import autocompleteClan from '../core/discord/autocompleteClan';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
import { ClanInfo } from '../types/clanInfo'; import { ClanInfo } from '../types/clanInfo';
@@ -18,7 +16,7 @@ const DEFAULT_THRESHOLD = 7;
export const inactiveCommand: CommandRegistry = { export const inactiveCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -42,14 +40,14 @@ export const inactiveCommand: CommandRegistry = {
const time = new Date().getTime() / 1000; const time = new Date().getTime() / 1000;
const clanData = ( const clanData = (
await getWargamingResponse<ClanInfo>( 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]; )[id];
const memberIds = clanData.members_ids; const memberIds = clanData.members_ids;
const accounts = await getWargamingResponse<AccountInfo>( 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 const inactive = memberIds
.map((memberId) => { .map((memberId) => {
@@ -69,17 +67,12 @@ export const inactiveCommand: CommandRegistry = {
<Wrapper> <Wrapper>
<TitleBar <TitleBar
name={clanData.name} name={clanData.name}
nameDiscriminator="(Inactivity)"
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`} image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
description={`${threshold}+ Days • ${new Date().toDateString()} • ${ description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
REGION_DOMAIN_NAMES[server]
}`}
/> />
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />} {!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
{hasInactiveMembers && <GenericStats stats={inactive} />} {hasInactiveMembers && <GenericStats stats={inactive} />}
<PoweredBy type={PoweredByType.Wargaming} />
</Wrapper> </Wrapper>
); );
}, },
+92 -68
View File
@@ -1,23 +1,24 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import * as Tanks from '../components/Tanks'; import * as Tanks from '../components/Tanks';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_DOMAIN_NAMES } from '../constants/regions';
import getTankStats from '../core/blitz/getTankStats'; import getTankStats from '../core/blitz/getTankStats';
import getTreeType from '../core/blitz/getTreeType'; import getTreeType from '../core/blitz/getTreeType';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankName from '../core/blitz/resolveTankName'; import resolveTankName from '../core/blitz/resolveTankName';
import { import {
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
TankopediaEntry,
Tier, Tier,
tankopedia, tankopedia,
tankopediaInfo,
} from '../core/blitz/tankopedia'; } from '../core/blitz/tankopedia';
import addTierChoices from '../core/discord/addTierChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData'; import { PlayerClanData } from '../types/playerClanData';
@@ -45,107 +46,130 @@ const COMP_TANKS = [
export const ownedTanksCommand: CommandRegistry = { export const ownedTanksCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('ownedtanks') .setName('owned-tanks')
.setDescription("Shows a player's owned tanks") .setDescription("Shows a player's owned tanks")
.addStringOption((option) => .addStringOption(addTierChoices)
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' },
)
.setRequired(true),
)
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
const tier = Number(interaction.options.getString('tier')); const tier = Number(interaction.options.getString('tier'));
const account = await resolvePlayerFromCommand(interaction); const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account; const { id, region: server } = account;
const accountInfo = await getWargamingResponse<AccountInfo>( 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 tankStats = await getTankStats(server, id);
const tanks = ( const filteredTanks = (
await Promise.all( await Promise.all(
tankStats.map(async (tankData) => ({ tankStats.map(async (tankData) => ({
...(await tankopedia)[tankData.tank_id], tankopedia: (await tankopedia)[tankData.tank_id]!,
tank_id: tankData.tank_id, tank_id: tankData.tank_id,
})), })),
) )
).filter((tank) => tank?.tier === tier); ).filter((tank) => tank.tankopedia?.tier === tier);
const clanData = await getWargamingResponse<PlayerClanData>( 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 leftColumnSize = Math.ceil(tanks.length / 2); const groupedTanks: Record<string, TankopediaEntry[]> = {};
const leftColumn = tanks.slice(0, leftColumnSize); const nations: string[] = [];
const rightColumn = tanks.slice(leftColumnSize);
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 ( return (
<Wrapper> <Wrapper>
<TitleBar <TitleBar
name={accountInfo[id].nickname} name={accountInfo[id].nickname}
nameDiscriminator={`(Tier ${TIER_ROMAN_NUMERALS[tier as Tier]})`} nameDiscriminator={
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
}
image={ image={
clanData[id]?.clan clanData[id]?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
: undefined : undefined
} }
description={`Owned tanks • ${new Date().toDateString()} • ${ description={`Tier ${
REGION_DOMAIN_NAMES[server] 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 && ( {filteredTanks.length > 0 &&
<Tanks.Root> (await Promise.all(
<Tanks.Column> nations.map(async (nation) => {
{await Promise.all( const tanks = groupedTanks[nation];
leftColumn.map(async (tank) => ( const leftColumnSize = Math.ceil(tanks.length / 2);
<Tanks.Item const leftColumn = tanks.slice(0, leftColumnSize);
key={tank.tank_id} const rightColumn = tanks.slice(leftColumnSize);
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.Root>
)}
<PoweredBy type={PoweredByType.Wargaming} /> 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>
);
}),
))}
</Wrapper> </Wrapper>
); );
}, },
autocomplete: autocompleteUsername, 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'));
+2 -2
View File
@@ -1,9 +1,9 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const pingCommand: CommandRegistry<true> = { export const pingCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
handlesInteraction: true, handlesInteraction: true,
+6 -6
View File
@@ -7,7 +7,7 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable'; import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountAchievements } from '../types/accountAchievements'; import { AccountAchievements } from '../types/accountAchievements';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
@@ -16,11 +16,11 @@ type SortBy = 'name' | 'count';
export const playerAchievementsCommand: CommandRegistry = { export const playerAchievementsCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('playerachievements') .setName('player-achievements')
.setDescription("All the player's achievements") .setDescription("All the player's achievements")
.addStringOption(addUsernameChoices) .addStringOption(addUsernameChoices)
.addStringOption((option) => .addStringOption((option) =>
@@ -36,13 +36,13 @@ export const playerAchievementsCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy; const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
const account = await resolvePlayerFromCommand(interaction); const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account; const { id, region: server } = account;
const accounts = await getWargamingResponse<AccountInfo>( 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 = const accountsAchievements =
await getWargamingResponse<AccountAchievements>( 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 accountAchievements = accountsAchievements[id];
const compound = { const compound = {
+5 -5
View File
@@ -6,25 +6,25 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable'; import markdownTable from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const playerInfoCommand: CommandRegistry = { export const playerInfoCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('playerinfo') .setName('player-info')
.setDescription('Basic information about a player') .setDescription('Basic information about a player')
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
const account = await resolvePlayerFromCommand(interaction); const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account; const { id, region: server } = account;
const accounts = await getWargamingResponse<AccountInfo>( 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]; const accountInfo = accounts[id];
+545
View File
@@ -0,0 +1,545 @@
import {
APIApplicationCommandOptionChoice,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} 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 getArchivedRatingsInfo from '../core/blitz/getArchivedRatingsInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
import getMidnightLeaderboard, {
DATABASE_REPO,
} 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 { octokit } from '../core/express/octokit';
import { secrets } from '../core/node/secrets';
import throwError from '../core/node/throwError';
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 type RatingsInfo =
| {
detail: undefined;
title: string;
icon: string | null;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingsReward[];
leagues: RatingsLeague[];
}
| { detail: { error: string } };
export interface RatingsNeighbors {
neighbors: RatingsPlayer[];
}
const LEAGUES = [
{ name: 'Diamond', minScore: 5000 },
{ name: 'Platinum', minScore: 4000 },
{ name: 'Gold', minScore: 3000 },
{ name: 'Silver', minScore: 2000 },
{ name: 'Bronze', minScore: 0 },
];
function addSubcommands(
option: SlashCommandSubcommandGroupBuilder,
addOptions = (option: SlashCommandSubcommandBuilder) => option,
) {
return option
.addSubcommand((option) =>
addOptions(
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),
),
)
.addSubcommand((option) =>
addOptions(option.setName('league').setDescription('Top league players'))
.addStringOption((option) =>
option
.setName('league')
.setDescription('The league to display')
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name,
value: `${value}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
)
.addStringOption(addRegionChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many rows to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
);
}
export const ratingsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const seasonChoices = await octokit.repos
.getContent({
...DATABASE_REPO,
path: 'com/ratings',
})
.then(({ data }) => {
if (!Array.isArray(data)) {
throw throwError('Archived ratings data is malformed');
}
return data.map(
(folder) =>
({
name: `Season ${folder.name}`,
value: folder.name,
}) satisfies APIApplicationCommandOptionChoice<string>,
);
});
const command = new SlashCommandBuilder()
.setName('ratings')
.setDescription('Ratings battles statistics')
.addSubcommandGroup((option) =>
addSubcommands(
option.setName('archive').setDescription('Any previous season'),
(option) =>
option.addStringOption((option) =>
option
.setName('season')
.setDescription('The season number')
.setChoices(...seasonChoices)
.setRequired(true),
),
),
)
.addSubcommandGroup((option) =>
addSubcommands(
option
.setName('current')
.setDescription('The current running Ratings season'),
),
);
resolve({
inProduction: true,
inDevelopment: true,
inPublic: true,
command,
async handler(interaction) {
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
| 'archive'
| 'current';
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| 'neighbours';
const limit = interaction.options.getInteger('limit') ?? 10;
const seasonOption = interaction.options.getString('season');
const season = seasonOption ? parseInt(seasonOption) : undefined;
let titleName: string;
let titleImage: string | undefined;
let titleNameDiscriminator: string | undefined;
let titleDescription: string;
let playersBefore: number;
let playersAfter: number;
let players: SimplifiedPlayer[] | undefined;
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
let playerId: number | undefined;
let regionRatingsInfo: RatingsInfo;
if (subcommand === 'league') {
const leagueIndex = parseInt(
interaction.options.getString('league')!,
);
const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (subcommandGroup === 'current'
? getRatingsInfo(region)
: getArchivedRatingsInfo(season!, region));
const regionSubdomain = regionToRegionSubdomain(region);
const result =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await fetch(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
)
.then((response) => response.json() as Promise<LeagueTop>)
.then(async ({ result }) =>
result.slice(0, limit).map(
(player) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: undefined
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/latest.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings data is malformed');
}
const players = (await fetch(data.download_url!).then(
(response) => response.json(),
)) as BlitzkriegRatingsLeaderboard;
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const ids = trimmed.map((player) => player.id).join(',');
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
);
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name,
}) satisfies SimplifiedPlayer,
);
});
const leagueInfo =
regionRatingsInfo.detail === undefined
? regionRatingsInfo.leagues[leagueIndex]
: undefined;
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
players = result;
playersBefore = result ? result[0].position - 1 : 0;
playersAfter =
regionRatingsInfo.detail === undefined && result !== undefined
? regionRatingsInfo.count - result[result.length - 1].position
: 0;
titleName = `${
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
} - ${REGION_NAMES_SHORT[region]}`;
titleImage = leagueInfo
? leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`
: undefined;
titleDescription = `Top ${limit} players`;
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo =
subcommandGroup === 'current'
? await getRatingsInfo(region)
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/info.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings info is malformed');
}
const response = fetch(data.download_url!);
return (await response.then((response) =>
response.json(),
)) as RatingsInfo;
});
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 =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await fetch(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
limit / 2,
)}`,
)
.then(
(response) =>
response.json() as Promise<RatingsNeighbors>,
)
.then((players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: undefined
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/latest.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError(
'Archived ratings latest data is malformed',
);
}
const response = fetch(data.download_url!);
const players = (await response.then((response) =>
response.json(),
)) as BlitzkriegRatingsLeaderboard;
const playerIndex = players.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) {
throw throwError(
`${accountInfo[id].nickname} didn't player ratings in season ${season}`,
'This player did not participate in this season or did not get past calibration.',
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
);
const ids = trimmed.map((player) => player.id).join(',');
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name,
}));
});
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
players = neighbors;
titleImage = neighbors
? clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined
: undefined;
titleName = neighbors
? accountInfo[id].nickname
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
titleDescription = 'Ratings neighbours';
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
playersAfter =
regionRatingsInfo.detail === undefined
? neighbors
? regionRatingsInfo.count -
neighbors[neighbors.length - 1].position
: 0
: 0;
playerId = id;
}
const items = players?.map((player) => {
const reward =
regionRatingsInfo.detail === undefined
? regionRatingsInfo.rewards.find(
(reward) =>
player.position >= reward.from_position &&
player.position <= reward.to_position,
)
: undefined;
const midnightIndex = midnightLeaderboard?.findIndex(
(item) => item.id === player.id,
);
const midnightScore =
midnightIndex === undefined
? player.score
: midnightLeaderboard![midnightIndex].score;
const midnightPosition =
midnightIndex === undefined ? player.position : midnightIndex + 1;
return (
<Leaderboard.Item
nickname={player.nickname}
score={player.score}
position={player.position}
clan={player.clan}
reward={reward}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
return (
<Wrapper>
<TitleBar
name={titleName}
image={titleImage}
nameDiscriminator={titleNameDiscriminator}
description={`${titleDescription} • ${new Date().toDateString()}`}
/>
{items && (
<Leaderboard.Root>
{playersBefore > 0 && (
<Leaderboard.Gap number={playersBefore} />
)}
{items}
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
</Leaderboard.Root>
)}
</Wrapper>
);
},
autocomplete: autocompleteUsername,
});
},
);
+9 -11
View File
@@ -1,23 +1,23 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { REGION_DOMAIN_NAMES, RegionDomain } from '../constants/regions'; import { REGION_NAMES, Region } from '../constants/regions';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import addClanChoices from '../core/discord/addClanChoices'; import addClanChoices from '../core/discord/addClanChoices';
import addServerChoices from '../core/discord/addServerChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { ClanList } from '../types/clanList'; import { ClanList } from '../types/clanList';
export const searchClansCommand: CommandRegistry = { export const searchClansCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('searchclans') .setName('search-clans')
.setDescription('Search clans') .setDescription('Search clans')
.addStringOption(addServerChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => addClanChoices(option).setAutocomplete(false)) .addStringOption((option) => addClanChoices(option).setAutocomplete(false))
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
@@ -28,17 +28,15 @@ export const searchClansCommand: CommandRegistry = {
), ),
async handler(interaction) { async handler(interaction) {
const server = interaction.options.getString('server') as RegionDomain; const server = interaction.options.getString('region') as Region;
const clan = interaction.options.getString('clan')!; const clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger('limit') ?? 25;
const clanList = await getWargamingResponse<ClanList>( 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( return embedInfo(
`Clan search for "${markdownEscape(clan)}" in ${ `Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
REGION_DOMAIN_NAMES[server]
}`,
clanList.length === 0 clanList.length === 0
? 'No clans found.' ? 'No clans found.'
: `\`\`\`\n${clanList : `\`\`\`\n${clanList
+11 -13
View File
@@ -1,24 +1,24 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { REGION_DOMAIN_NAMES, RegionDomain } from '../constants/regions'; import { REGION_NAMES, Region } from '../constants/regions';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import { usernamePattern } from '../core/blitz/listPlayers'; import { usernamePattern } from '../core/blitz/listPlayers';
import addServerChoices from '../core/discord/addServerChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountList } from '../types/accountList'; import { AccountList } from '../types/accountList';
export const searchPlayersCommand: CommandRegistry = { export const searchPlayersCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('searchplayers') .setName('search-players')
.setDescription('Search players in a Blitz server') .setDescription('Search players in a Blitz server')
.addStringOption(addServerChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => .addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true), addUsernameChoices(option).setAutocomplete(false).setRequired(true),
) )
@@ -31,22 +31,20 @@ export const searchPlayersCommand: CommandRegistry = {
), ),
async handler(interaction) { async handler(interaction) {
const server = interaction.options.getString('server') as RegionDomain; const server = interaction.options.getString('region') as Region;
const name = interaction.options.getString('username')!; const name = interaction.options.getString('username')!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger('limit') ?? 25;
const trimmedSearch = name.trim(); const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch) const players = usernamePattern.test(trimmedSearch)
? await getWargamingResponse<AccountList>( ? await getWargamingResponse<AccountList>(
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent( `https://api.wotblitz.${server}/wotb/account/list/?application_id=${
trimmedSearch, secrets.WARGAMING_APPLICATION_ID
)}&limit=${limit}`, }&search=${encodeURIComponent(trimmedSearch)}&limit=${limit}`,
) )
: []; : [];
return embedInfo( return embedInfo(
`Player search for "${markdownEscape(name)}" in ${ `Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
REGION_DOMAIN_NAMES[server]
}`,
`\`\`\`${ `\`\`\`${
players.length === 0 players.length === 0
? 'No players found.' ? 'No players found.'
+7 -6
View File
@@ -1,18 +1,18 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { TANK_NAMES } from '../core/blitz/tankopedia'; import { TANK_NAMES_DIACRITICS } from '../core/blitz/tankopedia';
import addTankChoices from '../core/discord/addTankChoices'; import addTankChoices from '../core/discord/addTankChoices';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const searchTanksCommand: CommandRegistry = { export const searchTanksCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('searchtanks') .setName('search-tanks')
.setDescription('Search tanks') .setDescription('Search tanks')
.addStringOption((option) => addTankChoices(option).setAutocomplete(false)) .addStringOption((option) => addTankChoices(option).setAutocomplete(false))
.addIntegerOption((option) => .addIntegerOption((option) =>
@@ -26,9 +26,10 @@ export const searchTanksCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const tank = interaction.options.getString('tank')!; const tank = interaction.options.getString('tank')!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger('limit') ?? 25;
const results = go(tank, await TANK_NAMES, { limit }).map( const results = go(tank, await TANK_NAMES_DIACRITICS, {
(result) => result.target, limit,
); keys: ['combined'],
}).map((result) => result.obj.original);
return embedInfo( return embedInfo(
`Tank search for "${markdownEscape(tank)}"`, `Tank search for "${markdownEscape(tank)}"`,
+98 -62
View File
@@ -1,4 +1,5 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../components/Tanks';
import { CYCLIC_API } from '../constants/cyclic'; import { CYCLIC_API } from '../constants/cyclic';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankId from '../core/blitz/resolveTankId'; import resolveTankId from '../core/blitz/resolveTankId';
@@ -12,74 +13,109 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand'; import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import { StatType } from '../renderers/fullStats'; import stats, { MultiTankFilters, StatType } from '../renderers/stats';
import stats from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const statsCommand: CommandRegistry = { export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
inProduction: true, const command = await addStatTypeSubCommandGroups(
inDevelopment: false,
inPublic: true,
command: addStatTypeSubCommandGroups(
new SlashCommandBuilder() new SlashCommandBuilder()
.setName('stats') .setName('stats')
.setDescription('In-game statistics'), .setDescription('Regular battles statistics'),
), );
async handler(interaction) { resolve({
const commandGroup = interaction.options.getSubcommandGroup( inProduction: true,
true, inDevelopment: true,
) as StatType; inPublic: true,
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.server, 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 [ command,
await stats(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
'BlitzStars',
),
];
},
autocomplete: (interaction) => { async handler(interaction) {
autocompleteUsername(interaction); const commandGroup = interaction.options.getSubcommandGroup(
autocompleteTanks(interaction); 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 tierOption = interaction.options.getString('tier');
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.server, interaction);
return await stats( return [
commandGroup, await stats(
period, commandGroup,
player, resolvedPeriod,
parseInt(url.searchParams.get('tankId')!), player,
); commandGroup === 'tank'
}, ? tankId
}; : commandGroup === 'multi-tank'
? ({
'tank-type':
interaction.options.getString('tank-type') ?? undefined,
nation: interaction.options.getString('nation') ?? undefined,
tier: tierOption ? parseInt(tierOption) : 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 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,
);
},
});
});
+30 -8
View File
@@ -8,14 +8,14 @@ import linkButton from '../core/discord/linkButton';
import primaryButton from '../core/discord/primaryButton'; import primaryButton from '../core/discord/primaryButton';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import today from '../renderers/today'; import today from '../renderers/today';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const todayCommand: CommandRegistry = { export const todayCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -24,30 +24,52 @@ export const todayCommand: CommandRegistry = {
.addStringOption(addUsernameChoices) .addStringOption(addUsernameChoices)
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('limit') .setName('cutoff')
.setDescription( .setDescription(
'The maximum number of tanks to display (default: Infinity)', 'The maximum number of tanks to display (default: Infinity)',
) )
.setRequired(false) .setRequired(false)
.setMinValue(1), .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) { async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction); const player = await resolvePlayerFromCommand(interaction);
const limit = interaction.options.getInteger('limit') ?? Infinity; const cutoff = interaction.options.getInteger('cutoff') ?? undefined;
const maximized = interaction.options.getInteger('maximized') ?? undefined;
const showTotal = interaction.options.getBoolean('show-total') ?? undefined;
const { nickname } = ( const { nickname } = (
await getWargamingResponse<AccountInfo>( 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]; )[player.id];
const path = interactionToURL(interaction, player); const path = interactionToURL(interaction, {
...player,
cutoff,
maximized,
'show-total': showTotal,
});
return [ return [
await today(player, limit, false), await today(player, cutoff, maximized, showTotal, false),
primaryButton(path, 'Refresh'), primaryButton(path, 'Refresh'),
linkButton(`${CYCLIC_API}/${path}`, 'Embed'), linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton( linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`, `https://www.blitzstars.com/player/${player.region}/${nickname}`,
'BlitzStars', 'BlitzStars',
), ),
]; ];
+5 -5
View File
@@ -7,14 +7,14 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedNegative from '../core/discord/embedNegative'; import embedNegative from '../core/discord/embedNegative';
import embedPositive from '../core/discord/embedPositive'; import embedPositive from '../core/discord/embedPositive';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData'; import { PlayerClanData } from '../types/playerClanData';
export const verifyCommand: CommandRegistry = { export const verifyCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -24,12 +24,12 @@ export const verifyCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const blitzAccount = await resolvePlayerFromCommand(interaction); const blitzAccount = await resolvePlayerFromCommand(interaction);
const { id, server } = blitzAccount; const { id, region: server } = blitzAccount;
const accountInfo = await getWargamingResponse<AccountInfo>( 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>( 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 clan = clanData[id]?.clan;
const clanTag = clan ? ` [${clan!.tag}]` : ''; const clanTag = clan ? ` [${clan!.tag}]` : '';
+76 -35
View File
@@ -1,6 +1,7 @@
import getWN8Percentile from '../../../core/blitz/getWN8Percentile'; import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
import { TankType } from '../../../core/blitz/tankopedia';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
import { RowDiscriminator } from './RowDiscriminator'; import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
import { RowStat } from './RowStat'; import { RowStat } from './RowStat';
export interface RowProps { export interface RowProps {
@@ -13,14 +14,14 @@ export interface RowProps {
careerDamage: number; careerDamage: number;
battles: number; battles: number;
careerBattles: number; careerBattles: number;
icon?: string;
minimized: boolean; minimized: boolean;
isListing: boolean; isTank: boolean;
naked?: boolean; treeType?: TreeTypeEnum;
tankType?: TankType;
} }
export function Row({ export function Row({
isListing, isTank,
minimized, minimized,
name, name,
winrate, winrate,
@@ -31,45 +32,85 @@ export function Row({
careerDamage, careerDamage,
battles, battles,
careerBattles, careerBattles,
icon, treeType,
naked, tankType,
}: RowProps) { }: RowProps) {
return ( return (
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column',
borderRadius: 4, borderRadius: 4,
backgroundColor: isListing backgroundColor: theme.colors.appBackground2,
? theme.colors.componentInteractive overflow: 'hidden',
: theme.colors.appBackground2,
padding: 8,
}} }}
> >
<RowDiscriminator minimized={minimized} name={name} icon={icon} /> <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 <div
minimized={minimized} style={{
name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`} display: 'flex',
value={`${(winrate * 100).toFixed(2)}%`} paddingTop: 8,
delta={winrate - careerWinrate} paddingBottom: 8,
/> }}
<RowStat >
minimized={minimized} <RowStat
name={`WN8 • ${careerWN8 === undefined ? '--' : careerWN8.toFixed(0)}`} minimized={minimized}
value={WN8 === undefined ? '--' : WN8.toFixed(0)} name={`Battles • ${careerBattles.toFixed(0)}`}
percentile={WN8 === undefined ? undefined : getWN8Percentile(WN8)} value={battles.toFixed(0)}
/> />
<RowStat <RowStat
minimized={minimized} minimized={minimized}
name={`Damage • ${careerDamage.toFixed(0)}`} name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`}
value={damage.toFixed(0)} value={`${(winrate * 100).toFixed(2)}%`}
delta={damage - careerDamage} delta={winrate - careerWinrate}
/> />
<RowStat <RowStat
minimized={minimized} minimized={minimized}
name={`Battles • ${careerBattles.toFixed(0)}`} name={`WN8 • ${
value={battles.toFixed(0)} 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> </div>
); );
} }
@@ -1,51 +0,0 @@
import { theme } from '../../../stitches.config';
export interface RowDiscriminatorProps {
name: string;
icon?: string;
minimized: boolean;
}
export function RowDiscriminator({
name,
icon,
minimized,
}: RowDiscriminatorProps) {
return (
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
height: '100%',
boxSizing: 'border-box',
width: 0,
alignItems: 'center',
justifyContent: 'center',
}}
>
{!minimized && 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>
);
}
@@ -53,6 +53,7 @@ export function RowStat({
<span <span
style={{ style={{
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontSize: 16, fontSize: 16,
}} }}
> >
+1 -1
View File
@@ -1,4 +1,4 @@
export * from './components/Root'; export * from './components/Root';
export * from './components/Row'; export * from './components/Row';
export * from './components/RowDiscriminator';
export * from './components/RowStat'; export * from './components/RowStat';
+1 -1
View File
@@ -96,7 +96,7 @@ export default function GenericAllStats({
]} ]}
/> />
<Card <Card
title="Efficiency" title="Miscellaneous"
items={[ items={[
{ {
label: 'Average tier', label: 'Average tier',
+16 -14
View File
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
import { Margin, MarginOrientation } from './Margin'; import { Margin, MarginOrientation } from './Margin';
export const GRAPH_WIDTH = 544; export const GRAPH_WIDTH = 384;
export const GRAPH_HEIGHT = 320; export const GRAPH_HEIGHT = 320;
export interface MarginInputProps { export interface MarginInputProps {
@@ -20,7 +20,7 @@ export interface RootProps {
separations?: number; separations?: number;
xMinLabel: string; xMinLabel: string;
xMaxLabel: string; xMaxLabel: string;
xTitle: string; xTitle?: string;
} }
export function Root({ export function Root({
@@ -91,18 +91,20 @@ export function Root({
/> />
)} )}
<div {xTitle !== undefined && (
style={{ <div
paddingLeft: 64, style={{
display: 'flex', paddingLeft: 64,
alignItems: 'center', display: 'flex',
justifyContent: 'center', alignItems: 'center',
}} justifyContent: 'center',
> }}
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}> >
{xTitle} <span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
</span> {xTitle}
</div> </span>
</div>
)}
</div> </div>
); );
} }
@@ -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>
);
}
+3
View File
@@ -0,0 +1,3 @@
export * from './components/Gap';
export * from './components/Item';
export * from './components/Root';
-38
View File
@@ -1,38 +0,0 @@
import { ReactNode } from 'react';
import { theme } from '../stitches.config';
export enum PoweredByType {
BlitzStars,
Wargaming,
}
export interface PoweredByProps {
type: PoweredByType;
footer?: ReactNode;
}
const POWERED_BY_IMAGES: Record<PoweredByType, string> = {
[PoweredByType.BlitzStars]:
'https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png',
[PoweredByType.Wargaming]: 'https://i.imgur.com/jADGWm0.png',
};
const POWERED_BY_NAMES: Record<PoweredByType, string> = {
[PoweredByType.BlitzStars]: 'BlitzStars',
[PoweredByType.Wargaming]: 'Wargaming',
};
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', gap: 4 }}>
<img src={POWERED_BY_IMAGES[type]} style={{ height: 16, width: 16 }} />
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
Powered by {POWERED_BY_NAMES[type]}
</span>
</div>
</div>
);
}
+13 -10
View File
@@ -2,32 +2,35 @@ import {
TANK_ICONS, TANK_ICONS,
TANK_ICONS_COLLECTOR, TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM, TANK_ICONS_PREMIUM,
TankType,
} from '../../../core/blitz/tankopedia'; } from '../../../core/blitz/tankopedia';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
export enum TreeType { export enum TreeTypeEnum {
TechTree, TechTree,
Premium, Premium,
Collector, Collector,
} }
export type TreeTypeString = 'techtree' | 'premium' | 'collector';
export interface ItemProps { export interface ItemProps {
image?: string; image?: string;
tankType?: string; tankType?: TankType;
name: string; name: string;
treeType: TreeType; treeType: TreeTypeEnum;
} }
const TREE_TYPE_COLOR = { const TREE_TYPE_COLOR = {
[TreeType.TechTree]: '', [TreeTypeEnum.TechTree]: '',
[TreeType.Premium]: '_amber', [TreeTypeEnum.Premium]: '_amber',
[TreeType.Collector]: '_blue', [TreeTypeEnum.Collector]: '_blue',
} as const; } as const;
const TREE_TYPE_ICONS = { export const TREE_TYPE_ICONS = {
[TreeType.TechTree]: TANK_ICONS, [TreeTypeEnum.TechTree]: TANK_ICONS,
[TreeType.Premium]: TANK_ICONS_PREMIUM, [TreeTypeEnum.Premium]: TANK_ICONS_PREMIUM,
[TreeType.Collector]: TANK_ICONS_COLLECTOR, [TreeTypeEnum.Collector]: TANK_ICONS_COLLECTOR,
}; };
export function Item({ image, tankType, name, treeType }: ItemProps) { export function Item({ image, tankType, name, treeType }: ItemProps) {
+2 -1
View File
@@ -9,7 +9,8 @@ export function Root({ children }: RootProps) {
<div <div
style={{ style={{
display: 'flex', display: 'flex',
gap: 8, flexDirection: 'column',
gap: 16,
}} }}
> >
{children} {children}
+18
View File
@@ -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>
);
}
+19
View File
@@ -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>
);
}
+2
View File
@@ -1,3 +1,5 @@
export * from './components/Column'; export * from './components/Column';
export * from './components/Item'; export * from './components/Item';
export * from './components/Root'; export * from './components/Root';
export * from './components/Row';
export * from './components/Title';
+12 -15
View File
@@ -1,4 +1,3 @@
import isDev from '../core/node/isDev';
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
export interface TitleBarProps { export interface TitleBarProps {
@@ -19,10 +18,11 @@ export default function TitleBar({
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 16, justifyContent: 'center',
overflow: 'hidden',
}} }}
> >
<div style={{ display: 'flex', gap: 8, flex: 1 }}> <div style={{ display: 'flex', gap: 8 }}>
{image && ( {image && (
<img <img
style={{ width: 64, height: 64, objectFit: 'contain' }} style={{ width: 64, height: 64, objectFit: 'contain' }}
@@ -31,13 +31,18 @@ export default function TitleBar({
)} )}
<div <div
style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }} style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
gap: 4,
}}
> >
<div <div
style={{ style={{
display: 'flex', display: 'flex',
gap: 4, gap: 4,
overflow: 'hidden',
}} }}
> >
<span <span
@@ -46,6 +51,8 @@ export default function TitleBar({
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 900, fontWeight: 900,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
maxWidth: nameDiscriminator === undefined ? Infinity : 240,
overflow: 'hidden',
}} }}
> >
{name} {name}
@@ -53,7 +60,6 @@ export default function TitleBar({
{nameDiscriminator && ( {nameDiscriminator && (
<span <span
style={{ style={{
flex: 1,
fontSize: 32, fontSize: 32,
color: theme.colors.textLowContrast, color: theme.colors.textLowContrast,
fontWeight: 900, fontWeight: 900,
@@ -70,15 +76,6 @@ export default function TitleBar({
</span> </span>
</div> </div>
</div> </div>
<img
style={{ width: 64, height: 64 }}
src={
isDev()
? 'https://i.imgur.com/j6TItdy.png'
: 'https://i.imgur.com/pP6RN0x.png'
}
/>
</div> </div>
); );
} }
+1 -1
View File
@@ -14,7 +14,7 @@ export default function Wrapper({ children, naked }: WrapperProps) {
flexDirection: 'column', flexDirection: 'column',
padding: 16, padding: 16,
gap: 32, gap: 32,
width: 640, width: 480,
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
...(!naked && { ...(!naked && {
+1 -1
View File
@@ -1 +1 @@
export const CYCLIC_API = 'https://blitzkrieg.cyclic.app'; export const CYCLIC_API = 'https://blitzkrieg-embeds.cyclic.app';
+9 -2
View File
@@ -1,12 +1,19 @@
export type RegionDomain = 'com' | 'eu' | 'asia'; export type Region = 'com' | 'eu' | 'asia';
export type RegionSubdomain = 'na' | 'eu' | 'asia'; export type RegionSubdomain = 'na' | 'eu' | 'asia';
export const REGION_DOMAIN_NAMES: Record<RegionDomain, string> = { export const REGION_NAMES: Record<Region, string> = {
com: 'North America', com: 'North America',
eu: 'Europe', eu: 'Europe',
asia: 'Asia', 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> = { export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
na: 'North America', na: 'North America',
eu: 'Europe', eu: 'Europe',
-30
View File
@@ -1,30 +0,0 @@
import { AllStats } from '../../types/accountInfo';
export default function diffStats(stats1: AllStats, stats2: AllStats) {
function diff(value: (allStats: AllStats) => number) {
return value(stats2) - value(stats1);
}
function max(value: (allStats: AllStats) => number) {
return Math.max(value(stats1), value(stats2));
}
return {
battles: diff((a) => a.battles),
wins: diff((a) => a.wins),
capture_points: diff((a) => a.capture_points),
damage_dealt: diff((a) => a.damage_dealt),
damage_received: diff((a) => a.damage_received),
dropped_capture_points: diff((a) => a.dropped_capture_points),
frags: diff((a) => a.frags),
frags8p: diff((a) => a.frags8p),
hits: diff((a) => a.hits),
losses: diff((a) => a.losses),
max_frags: max((a) => a.max_frags),
max_xp: max((a) => a.max_xp),
shots: diff((a) => a.shots),
spotted: diff((a) => a.spotted),
survived_battles: diff((a) => a.survived_battles),
win_and_survived: diff((a) => a.win_and_survived),
xp: diff((a) => a.xp),
} satisfies AllStats;
}
+25
View File
@@ -0,0 +1,25 @@
import { RatingsInfo } from '../../commands/ratings';
import { Region } from '../../constants/regions';
import { DATABASE_REPO } from '../database/getMidnightLeaderboard';
import { octokit } from '../express/octokit';
import throwError from '../node/throwError';
export default async function getArchivedRatingsInfo(
season: number,
region: Region,
) {
const { data } = await octokit.repos.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/info.json`,
});
if (Array.isArray(data) || data.type !== 'file')
throw throwError('Archived ratings info is malformed');
const content = Buffer.from(data.content, 'base64').toString();
const jsonContent = JSON.parse(content) as RatingsInfo & {
detail: undefined;
};
return jsonContent;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand'; import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand';
import throwError from '../node/throwError'; import throwError from '../node/throwError';
import listClans from './listClans'; import listClans from './listClans';
@@ -6,7 +6,7 @@ import listClans from './listClans';
export default async function getBlitzClan(clan: string) { export default async function getBlitzClan(clan: string) {
if (serverAndIdPattern.test(clan)) { if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split('/'); const [server, accountId] = clan.split('/');
return { server: server as RegionDomain, id: Number(accountId) }; return { server: server as Region, id: Number(accountId) };
} else { } else {
const accounts = await listClans(clan); const accounts = await listClans(clan);
+13
View File
@@ -0,0 +1,13 @@
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>;
}
+5 -5
View File
@@ -1,18 +1,18 @@
import { REGION_DOMAIN_NAMES, RegionDomain } from '../../constants/regions'; import { REGION_NAMES, Region } from '../../constants/regions';
import { TanksStats } from '../../types/tanksStats'; import { TanksStats } from '../../types/tanksStats';
import { WARGAMING_APPLICATION_ID } from '../node/arguments'; import { secrets } from '../node/secrets';
import throwError from '../node/throwError'; import throwError from '../node/throwError';
import getWargamingResponse from './getWargamingResponse'; import getWargamingResponse from './getWargamingResponse';
export default async function getTankStats(server: RegionDomain, id: number) { export default async function getTankStats(server: Region, id: number) {
const tankStats = await getWargamingResponse<TanksStats>( 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) { if (tankStats[id] === null) {
throw throwError( throw throwError(
'No tank stats available', '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 ${REGION_DOMAIN_NAMES[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.`,
); );
} }
+4 -4
View File
@@ -1,11 +1,11 @@
import { TreeType } from '../../components/Tanks'; import { TreeTypeEnum } from '../../components/Tanks';
import { tankopedia } from './tankopedia'; import { tankopedia } from './tankopedia';
export default async function getTreeType(id: number) { export default async function getTreeType(id: number) {
const entry = (await tankopedia)[id]; const entry = (await tankopedia)[id];
if (entry?.is_collectible) return TreeType.Collector; if (entry?.is_collectible) return TreeTypeEnum.Collector;
if (entry?.is_premium) return TreeType.Premium; if (entry?.is_premium) return TreeTypeEnum.Premium;
return TreeType.TechTree; return TreeTypeEnum.TechTree;
} }
+8 -8
View File
@@ -1,6 +1,6 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { Clan, ClanList } from '../../types/clanList'; import { Clan, ClanList } from '../../types/clanList';
import { WARGAMING_APPLICATION_ID } from '../node/arguments'; import { secrets } from '../node/secrets';
import getWargamingResponse from './getWargamingResponse'; import getWargamingResponse from './getWargamingResponse';
export type ClanListWithServer = (Clan & { export type ClanListWithServer = (Clan & {
@@ -15,33 +15,33 @@ export default async function listClans(search: string, limit = 9) {
return ( return (
await Promise.all([ await Promise.all([
getWargamingResponse<ClanList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'com' as RegionDomain, server: 'com' as Region,
})), })),
), ),
getWargamingResponse<ClanList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'eu' as RegionDomain, server: 'eu' as Region,
})), })),
), ),
getWargamingResponse<ClanList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'asia' as RegionDomain, server: 'asia' as Region,
})), })),
), ),
]) ])
+8 -8
View File
@@ -1,6 +1,6 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { Account, AccountList } from '../../types/accountList'; import { Account, AccountList } from '../../types/accountList';
import { WARGAMING_APPLICATION_ID } from '../node/arguments'; import { secrets } from '../node/secrets';
import getWargamingResponse from './getWargamingResponse'; import getWargamingResponse from './getWargamingResponse';
export type AccountListWithServer = (Account & { export type AccountListWithServer = (Account & {
@@ -19,33 +19,33 @@ export default async function listPlayers(search: string, limit = 9) {
return ( return (
await Promise.all([ await Promise.all([
getWargamingResponse<AccountList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'com' as RegionDomain, server: 'com' as Region,
})), })),
), ),
getWargamingResponse<AccountList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'eu' as RegionDomain, server: 'eu' as Region,
})), })),
), ),
getWargamingResponse<AccountList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'asia' as RegionDomain, server: 'asia' as Region,
})), })),
), ),
]) ])
+32
View File
@@ -0,0 +1,32 @@
/**
* Thanks, August!
* https://stackoverflow.com/a/41358305/12294756
*/
const ROMAN = {
M: 1000,
CM: 900,
D: 500,
CD: 400,
C: 100,
XC: 90,
L: 50,
XL: 40,
X: 10,
IX: 9,
V: 5,
IV: 4,
I: 1,
};
const ROMAN_KEYS = Object.keys(ROMAN) as (keyof typeof ROMAN)[];
export default function numberToRomanNumeral(value: number) {
let string = '';
ROMAN_KEYS.forEach((index) => {
const part = Math.floor(value / ROMAN[index]);
value -= part * ROMAN[index];
string += index.repeat(part);
});
return string;
}
@@ -0,0 +1,7 @@
import { Region, RegionSubdomain } from '../../constants/regions';
export default function regionToRegionSubdomain(
regionDomain: Region,
): RegionSubdomain {
return regionDomain === 'com' ? 'na' : regionDomain;
}
+42 -4
View File
@@ -1,3 +1,9 @@
import { deburr } from 'lodash';
import { secrets } from '../node/secrets';
import getWargamingResponse from './getWargamingResponse';
export type TankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
export interface TankopediaEntry { export interface TankopediaEntry {
name: string; name: string;
nation: string; nation: string;
@@ -7,7 +13,7 @@ export interface TankopediaEntry {
cost: { price_credit: number; price_gold: number }; cost: { price_credit: number; price_gold: number };
images: { preview: string; normal: string }; images: { preview: string; normal: string };
tank_id: number; tank_id: number;
type: string; type: TankType;
description: string; description: string;
} }
@@ -31,22 +37,54 @@ export const TANKS = new Promise<TankopediaEntry[]>(async (resolve) => {
export const TANK_NAMES = new Promise<string[]>(async (resolve) => { export const TANK_NAMES = new Promise<string[]>(async (resolve) => {
resolve((await entries).map(({ name }) => name)); resolve((await entries).map(({ name }) => name));
}); });
export const TANK_NAMES_DIACRITICS = TANK_NAMES.then((tankNames) =>
Promise.all(
tankNames.map(async (tankName, index) => {
const id = (await TANKS)[index].tank_id;
const name = tankName ?? `Unknown Tank ${id}`;
const diacriticless = deburr(name);
export const TANK_ICONS: Record<string, string> = { return {
original: name,
diacriticless,
combined: `${name}${diacriticless}`,
id,
};
}),
),
);
export interface TankopediaInfo {
achievement_sections: Record<string, { name: string; order: number }>;
tanks_updated_at: number;
languages: Record<string, string>;
vehicle_types: Record<string, string>;
vehicle_nations: Record<string, string>;
game_version: string;
}
// this is blocking because info is needed for command creation
console.log('Caching tankopedia info...');
export const tankopediaInfo = getWargamingResponse<TankopediaInfo>(
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}`,
);
console.log('Cached tankopedia info');
export const TANK_ICONS: Record<TankType, string> = {
'AT-SPG': 'https://i.imgur.com/BIHSEH0.png', 'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
lightTank: 'https://i.imgur.com/CSNha5V.png', lightTank: 'https://i.imgur.com/CSNha5V.png',
mediumTank: 'https://i.imgur.com/wvf3ltm.png', mediumTank: 'https://i.imgur.com/wvf3ltm.png',
heavyTank: 'https://i.imgur.com/ECeqlZa.png', heavyTank: 'https://i.imgur.com/ECeqlZa.png',
}; };
export const TANK_ICONS_PREMIUM: Record<string, string> = { export const TANK_ICONS_PREMIUM: Record<TankType, string> = {
'AT-SPG': 'https://i.imgur.com/TCu3EdR.png', 'AT-SPG': 'https://i.imgur.com/TCu3EdR.png',
lightTank: 'https://i.imgur.com/zdkpTRb.png', lightTank: 'https://i.imgur.com/zdkpTRb.png',
mediumTank: 'https://i.imgur.com/3z7eHX6.png', mediumTank: 'https://i.imgur.com/3z7eHX6.png',
heavyTank: 'https://i.imgur.com/P3vbmyA.png', heavyTank: 'https://i.imgur.com/P3vbmyA.png',
}; };
export const TANK_ICONS_COLLECTOR: Record<string, string> = { export const TANK_ICONS_COLLECTOR: Record<TankType, string> = {
'AT-SPG': 'https://i.imgur.com/WTjeirB.png', 'AT-SPG': 'https://i.imgur.com/WTjeirB.png',
lightTank: 'https://i.imgur.com/EwhtKkU.png', lightTank: 'https://i.imgur.com/EwhtKkU.png',
mediumTank: 'https://i.imgur.com/u8YDMBh.png', mediumTank: 'https://i.imgur.com/u8YDMBh.png',
+2 -2
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { AllStats } from '../../types/accountInfo'; import { AllStats } from '../../types/accountInfo';
import getTankHistories, { import getTankHistories, {
TankHistories, TankHistories,
@@ -35,7 +35,7 @@ export const emptyTankHistoryNode: TankHistoryRaw = {
}; };
export default async function getDiffedTankStats( export default async function getDiffedTankStats(
server: RegionDomain, server: Region,
id: number, id: number,
start: number, start: number,
end: number, end: number,
+4 -13
View File
@@ -1,16 +1,7 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { Period } from '../discord/addPeriodSubCommands'; import { PeriodSize } from '../discord/addPeriodSubCommands';
import getTimeDaysAgo from './getTimeDaysAgo'; import getTimeDaysAgo from './getTimeDaysAgo';
export default function getPeriodStart( export default function getPeriodStart(region: Region, period: PeriodSize) {
server: RegionDomain, return period === 'career' ? 0 : getTimeDaysAgo(region, parseInt(period));
period: Exclude<Period, 'custom'>,
) {
if (period === 'career') {
return -Infinity;
} else if (period === 'today') {
return getTimeDaysAgo(server, 1);
} else {
return getTimeDaysAgo(server, parseInt(period));
}
} }
+5 -5
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { AccountInfo, AllStats } from '../../types/accountInfo'; import { AccountInfo, AllStats } from '../../types/accountInfo';
import { import {
GetHistoriesOptions, GetHistoriesOptions,
@@ -7,7 +7,7 @@ import {
getHistoriesDefaultOptions, getHistoriesDefaultOptions,
} from '../../types/histories'; } from '../../types/histories';
import getWargamingResponse from '../blitz/getWargamingResponse'; import getWargamingResponse from '../blitz/getWargamingResponse';
import { WARGAMING_APPLICATION_ID } from '../node/arguments'; import { secrets } from '../node/secrets';
import { emptyAllStats } from './getDiffedTankStats'; import { emptyAllStats } from './getDiffedTankStats';
export interface PlayerHistoryRaw { export interface PlayerHistoryRaw {
@@ -25,13 +25,13 @@ export interface PlayerHistoryRaw {
account_id: number; account_id: number;
[key: string]: number; [key: string]: number;
}; };
region: RegionDomain; region: Region;
} }
export type PlayerHistoriesRaw = PlayerHistoryRaw[]; export type PlayerHistoriesRaw = PlayerHistoryRaw[];
export default async function getPlayerHistories( export default async function getPlayerHistories(
server: RegionDomain, server: Region,
id: number, id: number,
options?: Partial<GetHistoriesOptions>, options?: Partial<GetHistoriesOptions>,
) { ) {
@@ -67,7 +67,7 @@ export default async function getPlayerHistories(
statistics: { all }, statistics: { all },
} = ( } = (
await getWargamingResponse<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}`,
) )
)[id]; )[id];
+2 -2
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { import {
GetHistoriesOptions, GetHistoriesOptions,
Histories, Histories,
@@ -32,7 +32,7 @@ export const getTankHistoriesDefaultOptions: GetTankHistoriesOptions = {
}; };
export default async function getTankHistories( export default async function getTankHistories(
server: RegionDomain, server: Region,
id: number, id: number,
options?: Partial<GetTankHistoriesOptions>, options?: Partial<GetTankHistoriesOptions>,
) { ) {
+4 -4
View File
@@ -1,18 +1,18 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
export const TIME_ZONE_MAPPINGS: Record<RegionDomain, number> = { export const TIME_ZONE_MAPPINGS: Record<Region, number> = {
com: -5, // Central North American Time com: -5, // Central North American Time
eu: +1, // Central European Time eu: +1, // Central European Time
asia: +7, // Central Asia Standard Time asia: +7, // Central Asia Standard Time
}; };
export default function getTimeDaysAgo(server: RegionDomain, daysAgo: number) { export default function getTimeDaysAgo(region: Region, daysAgo: number) {
const now = new Date(); const now = new Date();
if (daysAgo === 0) now.getTime() / 1000; if (daysAgo === 0) now.getTime() / 1000;
const time = new Date(); const time = new Date();
time.setUTCHours(-TIME_ZONE_MAPPINGS[server], 0, 0, 0); time.setUTCHours(-TIME_ZONE_MAPPINGS[region], 0, 0, 0);
if (time > now) time.setUTCDate(time.getUTCDate() - 1); if (time > now) time.setUTCDate(time.getUTCDate() - 1);
time.setUTCDate(time.getUTCDate() - daysAgo + 1); time.setUTCDate(time.getUTCDate() - daysAgo + 1);
-12
View File
@@ -1,12 +0,0 @@
import sumStats from '../blitz/sumStats';
import { emptyAllStats } from './getDiffedTankStats';
import { tankAverages } from './tankAverages';
console.log('Calculating player average...');
export const playerAverage = Object.entries(await tankAverages).reduce(
(accumulator, [, tankAverage]) => {
return sumStats([accumulator, tankAverage.all]);
},
emptyAllStats,
);
console.log('Calculated player average');
@@ -0,0 +1,27 @@
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
import { Region } from '../../constants/regions';
import { octokit } from '../express/octokit';
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
export default async function getMidnightLeaderboard(
region: Region,
season: number,
) {
try {
const { data } = await octokit.repos.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/midnight.json`,
});
if (!Array.isArray(data) && data.type === 'file') {
const response = await fetch(data.download_url!);
return (await response.json()) as BlitzkriegRatingsLeaderboard;
}
} catch (error) {
console.warn(
`No midnight leaderboard found for season ${season}, region ${region}`,
);
}
}
+24 -19
View File
@@ -2,9 +2,9 @@ import {
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder, SlashCommandSubcommandGroupBuilder,
} from 'discord.js'; } from 'discord.js';
import { PERIOD_NAMES } from '../discord/resolvePeriodFromCommand';
export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom'; export type PeriodType = 'period' | 'custom';
export type PeriodSize = `${number}` | 'career';
export default function addPeriodSubCommands( export default function addPeriodSubCommands(
option: SlashCommandSubcommandGroupBuilder, option: SlashCommandSubcommandGroupBuilder,
@@ -14,36 +14,41 @@ export default function addPeriodSubCommands(
) { ) {
return option return option
.addSubcommand((option) => .addSubcommand((option) =>
extra(option.setName('today').setDescription(PERIOD_NAMES.today)), extra(
) option
.addSubcommand((option) => .setName('period' satisfies PeriodType)
extra(option.setName('30').setDescription(PERIOD_NAMES[30])), .setDescription("Preset period's statistics")
) .addStringOption((option) =>
.addSubcommand((option) => option
extra(option.setName('60').setDescription(PERIOD_NAMES[60])), .setName('period')
) .setDescription('A preset period')
.addSubcommand((option) => .setChoices(
extra(option.setName('90').setDescription(PERIOD_NAMES[90])), { name: 'Today', value: '1' satisfies PeriodSize },
) { name: '30 days', value: '30' satisfies PeriodSize },
.addSubcommand((option) => { name: '60 days', value: '60' satisfies PeriodSize },
extra(option.setName('career').setDescription(PERIOD_NAMES.career)), { name: '90 days', value: '90' satisfies PeriodSize },
{ name: 'Career', value: 'career' satisfies PeriodSize },
)
.setRequired(true),
),
),
) )
.addSubcommand((option) => .addSubcommand((option) =>
extra( extra(
option option
.setName('custom') .setName('custom' satisfies PeriodType)
.setDescription(PERIOD_NAMES.custom) .setDescription("Custom period's statistics")
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('start') .setName('start')
.setDescription('Start of period in days ago from today') .setDescription('Days ago from today')
.setMinValue(0) .setMinValue(0)
.setRequired(true), .setRequired(true),
) )
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('end') .setName('end')
.setDescription('End of period in days ago from today') .setDescription('Days ago from today')
.setMinValue(0) .setMinValue(0)
.setRequired(true), .setRequired(true),
), ),
+14
View File
@@ -0,0 +1,14 @@
import { SlashCommandStringOption } from 'discord.js';
import { REGION_NAMES } from '../../constants/regions';
export default function addRegionChoices(option: SlashCommandStringOption) {
return option
.setName('region')
.setDescription('The Blitz region you are in')
.addChoices(
{ name: REGION_NAMES.com, value: 'com' },
{ name: REGION_NAMES.eu, value: 'eu' },
{ name: REGION_NAMES.asia, value: 'asia' },
)
.setRequired(true);
}
-14
View File
@@ -1,14 +0,0 @@
import { SlashCommandStringOption } from 'discord.js';
import { REGION_DOMAIN_NAMES } from '../../constants/regions';
export default function addServerChoices(option: SlashCommandStringOption) {
return option
.setName('server')
.setDescription('The Blitz server you are in')
.addChoices(
{ name: REGION_DOMAIN_NAMES.com, value: 'com' },
{ name: REGION_DOMAIN_NAMES.eu, value: 'eu' },
{ name: REGION_DOMAIN_NAMES.asia, value: 'asia' },
)
.setRequired(true);
}
@@ -1,18 +1,25 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../../components/Tanks';
import { StatType } from '../../renderers/stats';
import { tankopediaInfo } from '../blitz/tankopedia';
import addPeriodSubCommands from './addPeriodSubCommands'; import addPeriodSubCommands from './addPeriodSubCommands';
import addTankChoices from './addTankChoices'; import addTankChoices from './addTankChoices';
import addTierChoices from './addTierChoices';
import addUsernameChoices from './addUsernameChoices'; import addUsernameChoices from './addUsernameChoices';
export default function addStatTypeSubCommandGroups( export default async function addStatTypeSubCommandGroups(
option: SlashCommandBuilder, option: SlashCommandBuilder,
addMultiTank = true,
) { ) {
return option const awaitedTankopediaInfo = await tankopediaInfo;
option
.addSubcommandGroup((option) => .addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) => addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameChoices), option.addStringOption(addUsernameChoices),
) )
.setName('player') .setName('player' satisfies StatType)
.setDescription("Player's statistics"), .setDescription('Player statistics'),
) )
.addSubcommandGroup((option) => .addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) => addPeriodSubCommands(option, (option) =>
@@ -20,7 +27,65 @@ export default function addStatTypeSubCommandGroups(
.addStringOption(addTankChoices) .addStringOption(addTankChoices)
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
) )
.setName('tank') .setName('tank' satisfies StatType)
.setDescription("Tank's statistics"), .setDescription('Tank statistics'),
); );
if (addMultiTank)
option.addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) =>
option
.addStringOption(addUsernameChoices)
.addStringOption((option) => {
option
.setName('nation')
.setDescription('Nation')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
([type, name]) => ({ name, value: type }),
),
);
return option;
})
.addStringOption((option) =>
option
.setName('tank-type')
.setDescription('Type')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
([type, name]) => ({
name: name.replace(' Tank', ''),
value: type,
}),
),
)
.setRequired(false),
)
.addStringOption((option) =>
addTierChoices(option).setRequired(false),
)
.addStringOption((option) =>
option
.setName('tree-type')
.setDescription('Tech tree type')
.addChoices(
{
name: 'Tech Tree',
value: 'techtree' satisfies TreeTypeString,
},
{ name: 'Premium', value: 'premium' satisfies TreeTypeString },
{
name: 'Collector',
value: 'collector' satisfies TreeTypeString,
},
)
.setRequired(false),
),
)
.setName('multi-tank' satisfies StatType)
.setDescription('Multiple tanks'),
);
return option;
} }
+16
View File
@@ -0,0 +1,16 @@
import { SlashCommandStringOption } from 'discord.js';
import { range } from 'lodash';
import numberToRomanNumeral from '../blitz/numberToRomanNumeral';
export default function addTierChoices(option: SlashCommandStringOption) {
return option
.setName('tier')
.setDescription('The tier you want to see')
.setChoices(
...range(10, 0).map((tier) => ({
name: `Tier ${tier} - ${numberToRomanNumeral(tier)}`,
value: `${tier}`,
})),
)
.setRequired(true);
}
+3 -2
View File
@@ -3,7 +3,7 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { REGION_DOMAIN_NAMES } from '../../constants/regions'; import { REGION_NAMES } from '../../constants/regions';
import listClans from '../blitz/listClans'; import listClans from '../blitz/listClans';
export default async function autocompleteClan( export default async function autocompleteClan(
@@ -11,6 +11,7 @@ export default async function autocompleteClan(
) { ) {
const focusedOption = interaction.options.getFocused(true); const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'clan') return; if (focusedOption.name !== 'clan') return;
if (focusedOption.value.length < 2) return interaction.respond([]);
const players = await listClans(focusedOption.value); const players = await listClans(focusedOption.value);
await interaction.respond( await interaction.respond(
@@ -18,7 +19,7 @@ export default async function autocompleteClan(
? players.map( ? players.map(
(player) => (player) =>
({ ({
name: `${player.name} (${REGION_DOMAIN_NAMES[player.server]})`, name: `${player.name} (${REGION_NAMES[player.server]})`,
value: `${player.server}/${player.clan_id}`, value: `${player.server}/${player.clan_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>, }) satisfies ApplicationCommandOptionChoiceData<string>,
) )
+5 -8
View File
@@ -4,8 +4,7 @@ import {
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import resolveTankName from '../blitz/resolveTankName'; import { TANK_NAMES_DIACRITICS } from '../blitz/tankopedia';
import { TANKS } from '../blitz/tankopedia';
export default async function autocompleteTanks( export default async function autocompleteTanks(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
@@ -16,16 +15,14 @@ export default async function autocompleteTanks(
await interaction.respond( await interaction.respond(
focusedOption.value focusedOption.value
? await Promise.all( ? await Promise.all(
go(focusedOption.value, await TANKS, { go(focusedOption.value, await TANK_NAMES_DIACRITICS, {
keys: ['name'], keys: ['combined'],
limit: 10, limit: 10,
}).map( }).map(
async (item) => async (item) =>
({ ({
name: item.obj.name name: item.obj.original,
? item.obj.name value: `${item.obj.id}`,
: await resolveTankName(item.obj.tank_id),
value: `${item.obj.tank_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>, }) satisfies ApplicationCommandOptionChoiceData<string>,
), ),
) )
+2 -4
View File
@@ -3,7 +3,7 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { REGION_DOMAIN_NAMES } from '../../constants/regions'; import { REGION_NAMES } from '../../constants/regions';
import listPlayers from '../blitz/listPlayers'; import listPlayers from '../blitz/listPlayers';
export default async function autocompleteUsername( export default async function autocompleteUsername(
@@ -19,9 +19,7 @@ export default async function autocompleteUsername(
? players.map( ? players.map(
(player) => (player) =>
({ ({
name: `${player.nickname} (${ name: `${player.nickname} (${REGION_NAMES[player.server]})`,
REGION_DOMAIN_NAMES[player.server]
})`,
value: `${player.server}/${player.account_id}`, value: `${player.server}/${player.account_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>, }) satisfies ApplicationCommandOptionChoiceData<string>,
) )
+11
View File
@@ -0,0 +1,11 @@
import { Client, GatewayIntentBits } from 'discord.js';
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],
})
.on('ready', ready)
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate);
+1 -6
View File
@@ -19,12 +19,7 @@ export default function interactionToURL(
] ]
.filter(Boolean) .filter(Boolean)
.join('/'); .join('/');
const params = [ const params = (parameters ? Object.entries(parameters) : [])
...interaction.options.data.map(
(option) => [option.name, option.value] as const,
),
...Object.entries(parameters ?? {}),
]
.filter(([, value]) => value !== undefined && value !== null) .filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
.join('&'); .join('&');
+2 -2
View File
@@ -1,10 +1,10 @@
import { ButtonInteraction, CacheType } from 'discord.js'; import { ButtonInteraction, CacheType } from 'discord.js';
import { CYCLIC_API } from '../../constants/cyclic'; import { CYCLIC_API } from '../../constants/cyclic';
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import resolvePeriodFromURL from '../express/resolvePeriodFromURL'; import resolvePeriodFromURL from '../express/resolvePeriodFromURL';
export default function resolvePeriodFromButton( export default function resolvePeriodFromButton(
server: RegionDomain, server: Region,
interaction: ButtonInteraction<CacheType>, interaction: ButtonInteraction<CacheType>,
) { ) {
return resolvePeriodFromURL(server, `${CYCLIC_API}/${interaction.customId}`); return resolvePeriodFromURL(server, `${CYCLIC_API}/${interaction.customId}`);
+20 -33
View File
@@ -1,27 +1,13 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzstars/getPeriodNow'; import getPeriodNow from '../blitzstars/getPeriodNow';
import getPeriodStart from '../blitzstars/getPeriodStart'; import getPeriodStart from '../blitzstars/getPeriodStart';
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo'; import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
import { Period } from '../discord/addPeriodSubCommands'; import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
export const PERIOD_NAMES: Record<Period, string> = { export function getPeriodOptionName(period: PeriodSize) {
today: "Today's statistics", return period === 'career' ? 'Career' : `${period}-day`;
30: "30 days' statistics", }
60: "60 days' statistics",
90: "90 days' statistics",
career: 'Career statistics',
custom: 'Custom period',
};
export const EVOLUTION_PERIOD_NAMES: Record<Period, string> = {
today: "Today's evolution",
30: "30 days' evolution",
60: "60 days' evolution",
90: "90 days' evolution",
career: 'Career evolution',
custom: 'Custom period',
};
export interface ResolvedPeriod { export interface ResolvedPeriod {
statsName: string; statsName: string;
@@ -31,29 +17,30 @@ export interface ResolvedPeriod {
} }
export default function resolvePeriodFromCommand( export default function resolvePeriodFromCommand(
server: RegionDomain, region: Region,
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) { ) {
let statsName: string; let statsName: string;
let evolutionName: string; let evolutionName: string;
let start: number; let start: number;
let end: number; let end: number;
const period = interaction.options.getSubcommand() as Period; const periodSubcommand = interaction.options.getSubcommand() as PeriodType;
const periodOption = interaction.options.getString('period') as PeriodSize;
if (period === 'custom') { if (periodSubcommand === 'custom') {
const startRaw = interaction.options.getInteger('start')!; const startRaw = interaction.options.getInteger('start', true);
const endRaw = interaction.options.getInteger('end')!; const endRaw = interaction.options.getInteger('end', true);
const startMin = Math.min(startRaw, endRaw); const startDaysAgoMin = Math.min(startRaw, endRaw);
const endMax = Math.max(startRaw, endRaw); const endDaysAgoMax = Math.max(startRaw, endRaw);
statsName = `${startMin} to ${endMax} days' statistics`; statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
evolutionName = `${startMin} to ${endMax} days' evolution`; evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
start = getTimeDaysAgo(server, startMin); start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(server, endMax); end = getTimeDaysAgo(region, startDaysAgoMin);
} else { } else {
statsName = PERIOD_NAMES[period]; statsName = `${getPeriodOptionName(periodOption)} Statistics`;
evolutionName = EVOLUTION_PERIOD_NAMES[period]; evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
start = getPeriodStart(server, period); start = getPeriodStart(region, periodOption);
end = getPeriodNow(); end = getPeriodNow();
} }
+4 -4
View File
@@ -1,5 +1,5 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import listPlayers, { import listPlayers, {
usernamePatternWithoutPosition, usernamePatternWithoutPosition,
} from '../blitz/listPlayers'; } from '../blitz/listPlayers';
@@ -8,7 +8,7 @@ import throwError from '../node/throwError';
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export interface ResolvedPlayer { export interface ResolvedPlayer {
server: RegionDomain; region: Region;
id: number; id: number;
} }
@@ -31,7 +31,7 @@ export default async function resolvePlayerFromCommand(
if (serverAndIdPattern.test(username)) { if (serverAndIdPattern.test(username)) {
const [server, accountId] = username.split('/'); const [server, accountId] = username.split('/');
return { return {
server: server as RegionDomain, region: server as Region,
id: Number(accountId), id: Number(accountId),
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} else { } else {
@@ -39,7 +39,7 @@ export default async function resolvePlayerFromCommand(
if (accounts[0]) { if (accounts[0]) {
return { return {
server: accounts[0].server, region: accounts[0].server,
id: accounts[0].account_id, id: accounts[0].account_id,
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} else { } else {
-7
View File
@@ -1,7 +0,0 @@
import { ButtonBuilder, ButtonStyle } from 'discord.js';
// TODO: ADD THIS AGAIN IMMEDIATELY
export const supportBlitzStars = new ButtonBuilder()
.setLabel('Support BlitzStars')
.setURL('https://www.blitzstars.com/supporters')
.setStyle(ButtonStyle.Link);
+4
View File
@@ -0,0 +1,4 @@
import { Octokit } from '@octokit/rest';
import { secrets } from '../node/secrets';
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
+3 -3
View File
@@ -1,11 +1,11 @@
import { Request } from 'express'; import { Request } from 'express';
import { CYCLIC_API } from '../../constants/cyclic'; import { CYCLIC_API } from '../../constants/cyclic';
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import resolvePeriodFromURL from './resolvePeriodFromURL'; import resolvePeriodFromURL from './resolvePeriodFromURL';
export default function resolvePeriodFromRequest( export default function resolvePeriodFromRequest(
server: RegionDomain, server: Region,
request: Request, request: Request,
) { ) {
return resolvePeriodFromURL(server, `${CYCLIC_API}${request.path}`); return resolvePeriodFromURL(server, `${CYCLIC_API}${request.url}`);
} }
+17 -22
View File
@@ -1,16 +1,15 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzstars/getPeriodNow'; import getPeriodNow from '../blitzstars/getPeriodNow';
import getPeriodStart from '../blitzstars/getPeriodStart'; import getPeriodStart from '../blitzstars/getPeriodStart';
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo'; import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
import { Period } from '../discord/addPeriodSubCommands'; import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
import { import {
EVOLUTION_PERIOD_NAMES,
PERIOD_NAMES,
ResolvedPeriod, ResolvedPeriod,
getPeriodOptionName,
} from '../discord/resolvePeriodFromCommand'; } from '../discord/resolvePeriodFromCommand';
export default function resolvePeriodFromURL( export default function resolvePeriodFromURL(
server: RegionDomain, server: Region,
urlString: string, urlString: string,
) { ) {
let statsName: string; let statsName: string;
@@ -19,29 +18,25 @@ export default function resolvePeriodFromURL(
let end: number; let end: number;
const url = new URL(urlString); const url = new URL(urlString);
const path = url.pathname.split('/').filter(Boolean); const path = url.pathname.split('/').filter(Boolean);
const period = path[path.length - 1] as Period; const periodSubcommand = path[path.length - 1] as PeriodType;
const periodOption = url.searchParams.get('period') as PeriodSize;
if (period === 'custom') { if (periodSubcommand === 'custom') {
const startRaw = parseInt(url!.searchParams.get('start')!); const startRaw = parseInt(url!.searchParams.get('start')!);
const endRaw = parseInt(url!.searchParams.get('end')!); const endRaw = parseInt(url!.searchParams.get('end')!);
const startMin = Math.min(startRaw, endRaw); const startDaysAgoMin = Math.min(startRaw, endRaw);
const endMax = Math.max(startRaw, endRaw); const endDaysAgoMax = Math.max(startRaw, endRaw);
statsName = `${startMin} to ${endMax} days' statistics`; statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
evolutionName = `${startMin} to ${endMax} days' evolution`; evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
start = getTimeDaysAgo(server, startMin); start = getTimeDaysAgo(server, endDaysAgoMax);
end = getTimeDaysAgo(server, endMax); end = getTimeDaysAgo(server, startDaysAgoMin);
} else { } else {
statsName = PERIOD_NAMES[period]; statsName = `${getPeriodOptionName(periodOption)} Statistics`;
evolutionName = EVOLUTION_PERIOD_NAMES[period]; evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
start = getPeriodStart(server, period); start = getPeriodStart(server, periodOption);
end = getPeriodNow(); end = getPeriodNow();
} }
return { return { statsName, evolutionName, start, end } satisfies ResolvedPeriod;
statsName,
evolutionName,
start,
end,
} satisfies ResolvedPeriod;
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { ResolvedPlayer } from '../discord/resolvePlayerFromCommand'; import { ResolvedPlayer } from '../discord/resolvePlayerFromCommand';
export default function resolvePlayerFromURL(urlString: string) { export default function resolvePlayerFromURL(urlString: string) {
@@ -6,6 +6,6 @@ export default function resolvePlayerFromURL(urlString: string) {
return { return {
id: parseInt(url.searchParams.get('id')!), id: parseInt(url.searchParams.get('id')!),
server: url.searchParams.get('server') as RegionDomain, region: url.searchParams.get('region') as Region,
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} }
-9
View File
@@ -1,9 +0,0 @@
export const DISCORD_TOKEN = process.env.DISCORD_TOKEN!;
export const WARGAMING_APPLICATION_ID = process.env.WARGAMING_APPLICATION_ID!;
if (!DISCORD_TOKEN) {
throw new Error('No Discord token provided');
}
if (!WARGAMING_APPLICATION_ID) {
throw new Error('No Wargaming application id provided}');
}
+1 -1
View File
@@ -32,7 +32,7 @@ Promise.all(
export default async function jsxToSvg(jsx: JSX.Element) { export default async function jsxToSvg(jsx: JSX.Element) {
return await satori(jsx, { return await satori(jsx, {
width: 640, width: 480,
fonts: [ fonts: [
{ data: roboto, name: FONT_NAME, weight: 400 }, { data: roboto, name: FONT_NAME, weight: 400 },
{ data: robotoBold, name: FONT_NAME, weight: 700 }, { data: robotoBold, name: FONT_NAME, weight: 700 },
+21
View File
@@ -0,0 +1,21 @@
export const SECRET_KEYS = [
'DISCORD_TOKEN',
'WARGAMING_APPLICATION_ID',
'GH_TOKEN',
] as const;
export type SECRET = typeof SECRET_KEYS extends readonly (infer T)[]
? T
: never;
export const secrets = SECRET_KEYS.reduce<Partial<Record<SECRET, string>>>(
(accumulator, key) => ({
...accumulator,
get [key](): string {
if (!(key in process.env)) throw new Error(`Key ${key} not provided`);
return process.env[key]!;
},
}),
{},
) as Record<SECRET, string>;
+1 -1
View File
@@ -1,7 +1,7 @@
import { EmbedBuilder, TextChannel } from 'discord.js'; import { EmbedBuilder, TextChannel } from 'discord.js';
import discord from '../../discord.json' assert { type: 'json' }; import discord from '../../discord.json' assert { type: 'json' };
import { client } from '../bot';
import { NEGATIVE_COLOR } from '../constants/colors'; import { NEGATIVE_COLOR } from '../constants/colors';
import { client } from '../core/discord/client';
import isDev from '../core/node/isDev'; import isDev from '../core/node/isDev';
const PROCESS_ERROR_EVENTS = ['uncaughtException']; const PROCESS_ERROR_EVENTS = ['uncaughtException'];
+1 -1
View File
@@ -15,7 +15,7 @@ export default async function (member: GuildMember) {
.setColor(ACCENT_COLOR) .setColor(ACCENT_COLOR)
.setTitle(`Welcome ${member.user.username}`) .setTitle(`Welcome ${member.user.username}`)
.setDescription( .setDescription(
`Welcome to the Skilled server, **${member.user.username}**! To continue, please use the \`/verify\` command.\n\nExample: \`/verify Europe Sabina244\``, `Welcome to the Skilled server, **${member.user.username}**! To continue, please use the \`/verify\` command.\n\nExample: \`/verify Sabina244\``,
), ),
], ],
}); });
@@ -1,10 +1,10 @@
import { AutocompleteInteraction, CacheType } from 'discord.js'; import { AutocompleteInteraction, CacheType } from 'discord.js';
import { commands } from '..'; import { commands } from '..';
export default function handleAutocomplete( export default async function handleAutocomplete(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
) { ) {
const command = commands[interaction.commandName]; const command = (await commands)[interaction.commandName];
if (!command) { if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`); console.error(`No command matching ${interaction.commandName} was found.`);
@@ -11,7 +11,7 @@ export default async function handleButton(
const url = new URL(`${CYCLIC_API}/${interaction.customId}`); const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const commandName = url.pathname.split('/')[1]; const commandName = url.pathname.split('/')[1];
const button = commands[commandName]?.button; const button = (await commands)[commandName]?.button;
if (!button) { if (!button) {
throwError(`Button handler not found for "${commandName}"`); throwError(`Button handler not found for "${commandName}"`);
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more