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

|
||||

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