Compare commits

...
152 Commits
Author SHA1 Message Date
TrèsAbhi f119b06dd8 fix: grammar 2024-04-12 09:24:15 -05:00
TrèsAbhi e23a440508 feat: logs 2024-04-12 09:23:34 -05:00
TrèsAbhi 6a9fa08500 fix: /research tank icon alignment 2024-04-12 09:17:05 -05:00
TrèsAbhi 8e86e81e91 fix: max probisions 2024-04-10 19:35:28 -05:00
TrèsAbhi 8b359e220a feat: more icon gap 2024-04-10 17:04:33 -05:00
TrèsAbhi 690bc5dbb5 feat: wrapping controls 2024-04-10 16:57:38 -05:00
TrèsAbhi d6e96e17ad feat: spall liner 2024-04-10 16:54:16 -05:00
TrèsAbhi ab26b346e6 feat: lenient psa 2024-04-10 16:29:12 -05:00
TrèsAbhi f02db6ef0b fix: clipping hero stat description 2024-04-10 16:19:24 -05:00
TrèsAbhi 55ba2525d1 fix: query ignorance on filter change 2024-04-10 16:13:39 -05:00
TrèsAbhi ea07d2646d feat: name draggable 2024-04-10 16:10:34 -05:00
TrèsAbhi ca111510f6 feat: remove caret icons 2024-04-10 16:05:30 -05:00
TrèsAbhi 519558dd28 fix: drag on mobile 2024-04-10 16:04:50 -05:00
TrèsAbhi c4d1e305f6 feat: grabbing cursor 2024-04-10 09:41:55 -05:00
TrèsAbhi 59772afb04 feat: drag and drop 2024-04-10 09:40:52 -05:00
TrèsAbhi 8c9dcfa551 feat: change name to just compare 2024-04-10 09:03:56 -05:00
TrèsAbhi 8bbf03a128 feat: version 2024-04-09 16:55:09 -05:00
TrèsAbhi 519fba84dc feat: ordered asset building 2024-04-09 16:03:07 -05:00
TrèsAbhi e7e74f706e feat: more provisions 2024-04-09 10:51:12 -05:00
TrèsAbhi 12e2793c92 feat: dynamic armor 2024-04-09 10:39:26 -05:00
TrèsAbhi 69040dc453 feat: reactive armor 2024-04-09 10:35:32 -05:00
TrèsAbhi 511151e101 feat: shells 2024-04-09 10:23:10 -05:00
TrèsAbhi 7fc993c2eb feat: shells 2024-04-09 10:04:43 -05:00
TrèsAbhi 030ca744e9 fix: correct shell count 2024-04-09 10:03:30 -05:00
TrèsAbhi 74756ab7bf feat: crew actions 2024-04-09 09:56:31 -05:00
TrèsAbhi e9a61d579d fix: update stats with crew skills 2024-04-09 09:51:22 -05:00
TrèsAbhi 7c3cad2c7d feat: crew button and missing traverse stats 2024-04-09 09:47:57 -05:00
TrèsAbhi 5f39ba55f6 feat: skip bot definitions 2024-04-08 22:04:50 -05:00
TrèsAbhi 2bfd8286a4 feat: cleanup 2024-04-08 21:50:29 -05:00
TrèsAbhi f3421b6f70 fix: types 2024-04-08 21:44:52 -05:00
TrèsAbhi abbbf11b3d feat: adsense 2024-04-08 21:43:47 -05:00
TrèsAbhi 797f5a90d2 feat: watermark 2 2024-04-08 21:24:24 -05:00
TrèsAbhi 818d13fade feat: watermark 2024-04-08 21:17:59 -05:00
TrèsAbhi 129d7c9688 feat: skills title 2024-04-08 19:26:49 -05:00
TrèsAbhi 28407b0f05 feat: universal crew skills 2024-04-08 19:24:49 -05:00
TrèsAbhi 8a9d72d0e6 feat: order 2024-04-08 19:12:29 -05:00
TrèsAbhi f98a4c7caa fix: button popover order 2024-04-08 19:10:02 -05:00
TrèsAbhi 92b329a0fb feat: module quick buttons 2024-04-08 19:04:07 -05:00
TrèsAbhi f3d32a4e63 feat: module swapping 2024-04-08 19:01:31 -05:00
TrèsAbhi 0e73d79c9c feat: trimmed module icons 2024-04-08 09:23:26 -05:00
TrèsAbhi 526b5785b7 feat: modules button 2024-04-08 09:22:27 -05:00
TrèsAbhi 02129feb25 fix: NaN deltas 2024-04-08 09:13:14 -05:00
TrèsAbhi 0a38c164a4 fix: removed debug console logs 2024-04-06 22:01:52 -05:00
TrèsAbhi 2e0d52e30e feat: enter to select first tank 2024-04-06 22:00:40 -05:00
TrèsAbhi 6d48f9fe7f idk 2024-04-06 21:40:21 -05:00
TrèsAbhi aba52c9968 feat: consistent icons 2024-04-06 20:35:30 -05:00
TrèsAbhi b96b9fd574 fix: damage decimals 2024-04-06 20:29:36 -05:00
TrèsAbhi fe4ee8b611 feat: more compact 2024-04-06 19:40:40 -05:00
TrèsAbhi 94558393d9 feat: consumables 2024-04-06 19:37:57 -05:00
TrèsAbhi db1e4af20c feat: clear provisions button 2024-04-06 19:19:46 -05:00
TrèsAbhi be20173acd feat: missing provisions 2024-04-06 19:16:16 -05:00
TrèsAbhi d90a70ed2a fix: max selected 2024-04-06 19:09:37 -05:00
TrèsAbhi 85d699dbfc feat: provisions 2024-04-06 19:04:57 -05:00
TrèsAbhi 5d0fe8726b feat: provision buttons 2024-04-06 18:55:34 -05:00
TrèsAbhi 850308b2ac feat: provision assignment 2024-04-06 18:37:21 -05:00
TrèsAbhi 88a85ef152 feat: rename to absolute 2024-04-06 18:30:36 -05:00
TrèsAbhi 8160935081 feat: better control layout 2024-04-06 18:28:16 -05:00
TrèsAbhi c057e295d2 feat: discard sorting on mutate 2024-04-06 18:19:45 -05:00
TrèsAbhi 28d0c26268 feat: change to clear 2024-04-06 17:55:39 -05:00
TrèsAbhi 52cb73a575 feat: gray sort buttons 2024-04-06 17:54:50 -05:00
TrèsAbhi 9f244452da feat: max tank name size 2024-04-06 17:53:22 -05:00
TrèsAbhi d46d948c66 feat: sort 2024-04-06 17:46:30 -05:00
TrèsAbhi 11b99ef211 feat: better compact search chunking 2024-04-06 17:28:45 -05:00
TrèsAbhi 85d350a912 feat: less CLS loader 2024-04-06 17:21:38 -05:00
TrèsAbhi 145bc84fb1 feat: no player selected 2024-04-06 13:18:17 -05:00
TrèsAbhi b7486022eb feat: cleaner home page 2024-04-06 13:12:07 -05:00
TrèsAbhi eadb4bb7b1 fix: no padding in embeds 2024-04-06 13:06:48 -05:00
TrèsAbhi 79723157d2 feat: ghost equipment buttons 2024-04-06 13:03:36 -05:00
TrèsAbhi 4214ff0de5 feat: trash button functionality 2024-04-06 12:51:52 -05:00
TrèsAbhi f516c9275e feat: flex 1 pages 2024-04-06 12:49:38 -05:00
TrèsAbhi f0b905593c feat: better loading spinners 2024-04-06 12:43:54 -05:00
TrèsAbhi 11440bc654 feat: non nested spans 2024-04-06 12:37:50 -05:00
TrèsAbhi 2dab957a05 fix: scroll padding 2024-04-06 12:28:29 -05:00
TrèsAbhi 60f3951e7a feat: remove scrollbar 2 2024-04-06 12:23:08 -05:00
TrèsAbhi 4f03788f15 feat: remove scrollbar 2024-04-06 12:22:56 -05:00
TrèsAbhi 17f4435a30 feat: isolated footer 2024-04-06 12:10:54 -05:00
TrèsAbhi 04e42c929c feat: blitz version 2024-04-06 11:15:39 -05:00
TrèsAbhi 4ba45b8fd8 feat: game definitions 2024-04-06 11:14:07 -05:00
TrèsAbhi 0ece1f4b18 feat: no tanks selected message 2024-04-06 10:57:02 -05:00
TrèsAbhi 49d3e1d1c2 feat: select all option 2024-04-06 10:36:07 -05:00
TrèsAbhi 184ca522dd feat: proper keying 2024-04-06 10:17:48 -05:00
TrèsAbhi 9f07d910b8 feat: generalized equipment matrix manager 2024-04-06 10:06:59 -05:00
TrèsAbhi ac3f2ea93a feat: themed equipment preview 2024-04-06 09:52:28 -05:00
TrèsAbhi a77ff1a638 fix: pen 0 decimals 2024-04-06 09:51:16 -05:00
TrèsAbhi 96d802a09f feat: more compact icons 2024-04-05 17:55:07 -05:00
TrèsAbhi 1babd66649 feat: loop icon 2024-04-05 17:53:56 -05:00
TrèsAbhi 2f2996a40c feat: equipment popover 2024-04-05 17:52:41 -05:00
TrèsAbhi 9d1e059ea5 feat: solid backgrounds 2024-04-05 17:44:31 -05:00
TrèsAbhi 48603b918f feat: equipment preview 2024-04-05 17:40:14 -05:00
TrèsAbhi c1253e8121 feat: equipment matrix visual start 2024-04-05 17:28:58 -05:00
TrèsAbhi ecc029402e feat: percentage 2024-04-05 14:05:15 -05:00
TrèsAbhi 1b88773e28 feat: nominal deltas 2024-04-05 13:59:30 -05:00
TrèsAbhi cf419a9ebb feat: better titles 2024-04-05 13:40:18 -05:00
TrèsAbhi 8565d75747 feat: double gain 2024-04-05 09:51:22 -05:00
TrèsAbhi 186ea8b458 feat: round 2024-04-05 09:38:48 -05:00
TrèsAbhi 4292081df6 feat: stronger colors 2024-04-05 09:35:55 -05:00
TrèsAbhi 4b54b61c35 feat: gradient interpolation 2024-04-05 09:35:03 -05:00
TrèsAbhi 5d098c46fa feat: titles 2024-04-05 09:31:21 -05:00
TrèsAbhi 7575f4581c feat: decimal shorthand 2024-04-05 09:20:07 -05:00
TrèsAbhi 4669fa6ec5 feat: significant change colors 2024-04-05 09:13:13 -05:00
TrèsAbhi 49294d1906 feat: volume 2024-04-05 09:07:29 -05:00
TrèsAbhi 3eee946978 feat: split volume 2024-04-05 09:06:44 -05:00
TrèsAbhi 7fff1c9737 feat: volume 2024-04-04 22:23:06 -05:00
TrèsAbhi 5b4c3119dd feat: average shell reload 2024-04-04 22:17:36 -05:00
TrèsAbhi 0a41ee15ed feat: lower is better 2024-04-04 22:15:33 -05:00
TrèsAbhi ad57d284bb feat: more stats 3 2024-04-04 22:14:30 -05:00
TrèsAbhi b929e9449d feat: more stats 2 2024-04-04 21:56:00 -05:00
TrèsAbhi 948ea0bb8d fix: types 2024-04-04 21:17:40 -05:00
TrèsAbhi c1b1b8d304 feat: more stats 2024-04-04 19:14:22 -05:00
TrèsAbhi 3bc5a6d740 fix: some bugs 2024-04-04 18:56:51 -05:00
TrèsAbhi d25da724e1 feat: some button functionality 2024-04-04 16:14:01 -05:00
TrèsAbhi 753804b8cf feat: some more stats 2 2024-04-04 15:52:05 -05:00
TrèsAbhi 096122d43c feat: some more stats 2024-04-04 15:47:27 -05:00
TrèsAbhi 3448427de0 feat: reintroduced links 2024-04-04 15:35:27 -05:00
TrèsAbhi d8bbd02715 feat: center aline 2024-04-04 13:51:39 -05:00
TrèsAbhi f696c5f3c9 feat: some stats 2024-04-04 13:51:07 -05:00
TrèsAbhi bd412cdaa2 feat: generalized tank stats 2024-04-04 13:16:12 -05:00
TrèsAbhi 913411275b feat: better header alignemnt 2024-04-04 11:05:56 -05:00
TrèsAbhi 17ff8e0819 feat: better sizing 2024-04-04 11:04:27 -05:00
TrèsAbhi b5fdc4d5cf feat: add functionality 2024-04-04 10:46:48 -05:00
TrèsAbhi 59c5d95aff feat: compare page 2024-04-04 10:21:37 -05:00
TrèsAbhi 16babbad90 feat: og tool icon 2024-04-04 10:18:04 -05:00
TrèsAbhi a9dc9e7ebd feat: cleaner tool shadows 2024-04-04 10:16:47 -05:00
TrèsAbhi 52e8df560e feat: added tank compare link 2024-04-04 10:15:01 -05:00
TrèsAbhi 87259ad515 feat: robots 2024-04-03 22:57:28 -05:00
TrèsAbhi c96655a6c0 feat: xml 2024-04-03 22:47:10 -05:00
TrèsAbhi 43894567b8 / 2024-04-03 22:37:38 -05:00
TrèsAbhi 76a952a8e6 fix: change to tests 2024-04-03 22:28:12 -05:00
TrèsAbhi e2efe428a2 feat: use require 2024-04-03 22:24:01 -05:00
TrèsAbhi 887cec2dd4 feat: use fs 2024-04-03 22:22:13 -05:00
TrèsAbhi 04ad48bd33 feat: use proper write file 2024-04-03 22:20:30 -05:00
TrèsAbhi 3613f5d13b feat: log 2024-04-03 22:18:41 -05:00
TrèsAbhi 3362e3b091 feat: js next config 2024-04-03 22:16:16 -05:00
TrèsAbhi 3322fa97d4 feat: ts next config 2024-04-03 22:14:55 -05:00
TrèsAbhi 1dc7748a04 feat: use script 2024-04-03 22:08:22 -05:00
TrèsAbhi 6176b3b0d8 feat: use sync 2024-04-03 22:04:36 -05:00
TrèsAbhi 81c2af08c6 fix: build sitemap before build 2024-04-03 22:02:02 -05:00
TrèsAbhi 2d83519f93 Merge branch 'dev' of https://github.com/tresabhi/blitzkrieg into dev 2024-04-03 21:58:49 -05:00
TrèsAbhi 869c1f0def feat: sitemap 2024-04-03 21:58:38 -05:00
TrèsAbhi 7a64fb8176 feat: preview image 2024-04-03 21:58:38 -05:00
TrèsAbhi 9256244286 fix: remove title from svg 2024-04-03 21:58:38 -05:00
TrèsAbhi 85e54e4a07 fix: build db 2 2024-04-03 21:58:38 -05:00
TrèsAbhi 8075dc8d9f fix: build db 2024-04-03 21:58:38 -05:00
TrèsAbhi 57dced109a feat: page descriptions 2024-04-03 21:58:38 -05:00
TrèsAbhi dd19d5b979 feat: gen prisma (#105) 2024-04-03 21:57:39 -05:00
TrèsAbhi 68d3fe6805 feat: sitemap 2024-04-03 21:56:20 -05:00
TrèsAbhi 70f7698466 feat: preview image 2024-04-03 19:48:28 -05:00
TrèsAbhi 26a5fdea0e fix: remove title from svg 2024-04-03 19:34:32 -05:00
TrèsAbhi 367bd1386b fix: build db 2 2024-04-03 19:31:10 -05:00
TrèsAbhi fac773a8f8 fix: build db 2024-04-03 19:28:25 -05:00
TrèsAbhi 6bd8b7d42f feat: page descriptions 2024-04-03 19:27:24 -05:00
TrèsAbhi 7093662130 feat: gen prisma 2024-04-03 15:34:39 -05:00
75 changed files with 4103 additions and 1210 deletions

No files matched your search

+3
View File
@@ -33,6 +33,9 @@ jobs:
- name: Install dependencies
run: yarn
- name: Build Prisma
run: yarn prisma generate
- name: Build
run: yarn build:bot --production
+1 -2
View File
@@ -18,5 +18,4 @@ temp/
/.VSCodeCounter
/.yarn
depots
public/sitemap*.xml
public/robots.txt
public/sitemap*
+4
View File
@@ -3,6 +3,7 @@
"AABBOX",
"Abhi",
"accountinfo",
"adsense",
"aeson",
"alphatest",
"APAC",
@@ -72,6 +73,7 @@
"leaderboards",
"lerp",
"LINEARSIZE",
"loadouts",
"loddist",
"lods",
"metallicness",
@@ -106,6 +108,7 @@
"Resvg",
"roboto",
"SCPG",
"scroller",
"searchclans",
"searchplayers",
"searchtanks",
@@ -147,6 +150,7 @@
"whitespaces",
"winrate",
"wireframe",
"Woah",
"WOTB",
"wotblitz",
"xffn",
+23
View File
@@ -0,0 +1,23 @@
# 1.19.0
Introducing the Blitzkrieg Compare tool!
## Features
- Added Compare
- Allows you to compare multiple tanks
- Shows deltas in the form of differences between values or percentages
- All stats from the Tankopedia are available here
- [CLS](https://web.dev/articles/cls) was reduced due to the new loading screens
- Clicking enter when searching for tanks now selects the first result
- The first tank name is also displayed in the search input
## Changes
- Removed the donation banner instead for a little note in the footer
- The footer now shows the version of Blitzkrieg and the version of WoTB its running on
- Removed frosted glass effect from most components apart from the navigation bar for performance reasons (I am looking at you, Apple!)
## Fixes
- Tank search is now not ignored when changing filters
-6
View File
@@ -1,6 +0,0 @@
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.SITE_URL || 'https://blitz-krieg.vercel.app',
generateRobotsTxt: true,
sourceDir: 'dist/website',
};
+4 -4
View File
@@ -1,12 +1,14 @@
{
"name": "blitzkrieg",
"version": "1.18.3",
"version": "1.19.0",
"description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts",
"type": "module",
"scripts": {
"build:db": "prisma generate",
"build:bot": "vite-node --script scripts/build --production",
"build:website": "next build && next-sitemap --config next-sitemap.config.cjs",
"build:website": "yarn build:db && yarn build:sitemap && next build",
"build:sitemap": "vite-node --script scripts/sitemap",
"build:docs": "vitepress build docs",
"build:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard",
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
@@ -56,7 +58,6 @@
"lz4js": "^0.2.0",
"markdown-escape": "^2.0.0",
"next": "^14.1.0",
"next-sitemap": "^4.2.3",
"parse-dds": "^1.2.1",
"postprocessing": "^6.34.1",
"react": "^18.2.0",
@@ -64,7 +65,6 @@
"react-dom": "^18.2.0",
"react-fuzzysort": "^1.0.1",
"react-promise-suspense": "^0.3.4",
"react-spinners": "^0.13.8",
"satori": "^0.10.13",
"sharp": "0.33.1",
"three": "0.159.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+7
View File
@@ -0,0 +1,7 @@
User-agent: *
Allow: /
Host: https://blitz-krieg.vercel.app
Sitemap: https://blitz-krieg.vercel.app/sitemap.txt
Sitemap: https://blitz-krieg.vercel.app/sitemap.xml
+17
View File
@@ -3,6 +3,7 @@ import { parse as parsePath } from 'path';
import { Vector3Tuple } from 'three';
import { parse as parseYaml } from 'yaml';
import { TankClass } from '../../src/components/Tanks';
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';
@@ -16,6 +17,7 @@ import {
EquipmentPreset,
EquipmentRow,
} from '../../src/core/blitzkrieg/equipmentDefinitions';
import { GameDefinitions } from '../../src/core/blitzkrieg/gameDefinitions';
import {
ModelArmor,
ModelDefinitions,
@@ -332,10 +334,18 @@ const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
const missingStrings: Record<string, string> = {
'#artefacts:tungstentip/name': 'Tungsten Shells',
};
const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/;
export async function definitions(production: boolean) {
console.log('Building definitions...');
const gameDefinitions: GameDefinitions = {
version: (await readStringDVPL(`${DATA}/version.txt.dvpl`))
.split(' ')[0]
.split('.')
.slice(0, 3)
.join('.'),
};
const tankDefinitions: TankDefinitions = {};
const modelDefinitions: ModelDefinitions = {};
const equipmentDefinitions: EquipmentDefinitions = {
@@ -411,6 +421,8 @@ export async function definitions(production: boolean) {
);
for (const tankKey in tankList.root) {
if (botPattern.test(tankKey)) continue;
const gunXps = new Map<number, number>();
const turretXps = new Map<number, number>();
const engineXps = new Map<number, number>();
@@ -1101,6 +1113,11 @@ export async function definitions(production: boolean) {
await commitAssets(
'definitions',
[
{
content: superCompress(gameDefinitions),
encoding: 'base64',
path: 'definitions/game.cdon.lz4',
},
{
content: superCompress(tankDefinitions),
encoding: 'base64',
+3 -3
View File
@@ -29,8 +29,9 @@ if (!targets && !allTargets) throw new Error('No target(s) specified');
const methods = [
definitions,
tankIcons,
scratchedFlags,
currencies,
skillIcons,
circleFlags,
shellIcons,
moduleIcons,
@@ -38,10 +39,9 @@ const methods = [
consumableProvisionIcons,
camouflageIcons,
boosterIcons,
tankIcons,
tankArmors,
tankModels,
currencies,
skillIcons,
];
for (const method of methods) {
+7 -4
View File
@@ -1,5 +1,6 @@
import { readdir } from 'fs/promises';
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
import sharp from 'sharp';
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
import { DATA, POI } from './constants';
@@ -16,9 +17,11 @@ export async function moduleIcons(production: boolean) {
.map(async (file) => {
const nameRaw = file.match(/vehicle(.+)\.packed\.webp\.dvpl/)![1];
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
const content = await readBase64DVPL(
`${DATA}/${POI.moduleIcons}/${file}`,
);
const content = (
await sharp(await readDVPLFile(`${DATA}/${POI.moduleIcons}/${file}`))
.trim()
.toBuffer()
).toString('base64');
return {
content,
+12 -5
View File
@@ -1,7 +1,8 @@
import { readdir } from 'fs/promises';
import sharp from 'sharp';
import { Vector3Tuple } from 'three';
import { NATION_IDS } from '../../src/constants/nations';
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
@@ -50,23 +51,29 @@ export async function tankIcons(production: boolean) {
const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
);
const small = `${DATA}/${parameters.resourcesPath.smallIconPath
const smallPath = `${DATA}/${parameters.resourcesPath.smallIconPath
.replace(/~res:\//, '')
.replace(/\..+/, '')}.packed.webp.dvpl`;
const big = `${DATA}/${parameters.resourcesPath.bigIconPath
const bigPath = `${DATA}/${parameters.resourcesPath.bigIconPath
.replace(/~res:\//, '')
.replace(/\..+/, '')}.packed.webp.dvpl`;
const big = await sharp(await readDVPLFile(bigPath))
.trim()
.toBuffer();
const small = await sharp(await readDVPLFile(smallPath))
.trim()
.toBuffer();
if (big) {
changes.push({
content: await readBase64DVPL(big),
content: big.toString('base64'),
encoding: 'base64',
path: `icons/tanks/big/${id}.webp`,
});
}
if (small) {
changes.push({
content: await readBase64DVPL(small),
content: small.toString('base64'),
encoding: 'base64',
path: `icons/tanks/small/${id}.webp`,
});
+26
View File
@@ -0,0 +1,26 @@
import { tankDefinitions } from '../src/core/blitzkrieg/tankDefinitions';
// random vercel bug forces require import
const { writeFileSync } = require('fs');
console.log('Building sitemap...');
const awaitedTankDefinitions = await tankDefinitions;
const values = Object.values(awaitedTankDefinitions);
const txt = values
.map((tank) => `https://blitz-krieg.vercel.app/tools/tankopedia/${tank.id}/`)
.join('\n');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
${values
.map(
(tank) =>
`<url><loc>https://blitz-krieg.vercel.app/tools/tankopedia/${tank.id}/</loc></url>`,
)
.join('\n')}
</urlset>`;
writeFileSync('public/sitemap.txt', txt);
writeFileSync('public/sitemap.xml', xml);
console.log(`Built sitemap for ${values.length} tanks`);
+40 -92
View File
@@ -1,21 +1,18 @@
'use client';
import { CaretRightIcon } from '@radix-ui/react-icons';
import { AlertDialog, Button, Flex, Text, Theme } from '@radix-ui/themes';
import { AlertDialog, Button, Flex, Theme } from '@radix-ui/themes';
import '@radix-ui/themes/styles.css';
import { config } from 'dotenv';
import { Roboto_Flex } from 'next/font/google';
import { usePathname } from 'next/navigation';
import { ReactNode, useEffect, useState } from 'react';
import { SkyLine } from '../assets/art/SkyLine';
import { Analytics } from '../components/Analytics';
import { Google } from '../components/Google';
import { Footer } from '../components/Footer';
import Navbar from '../components/Navbar';
import PageWrapper from '../components/PageWrapper';
import isDev from '../core/blitzkrieg/isDev';
import { isLocalhost } from '../core/blitzkrieg/isLocalhost';
import { useFullScreen } from '../hooks/useFullScreen';
import { useApp } from '../stores/app';
import { mintTheme } from '../themes/mint';
config();
@@ -53,110 +50,61 @@ export default function RootLayout({ children }: RootLayoutProps) {
name="description"
content="🎉 Tools for everything World of Tanks Blitz"
/>
<Analytics />
<Google />
</head>
<body
style={{
margin: 0,
paddingTop: isEmbed ? 0 : '3.25rem',
backgroundColor: isEmbed ? 'transparent' : undefined,
}}
>
<Theme
appearance="dark"
panelBackground="translucent"
panelBackground="solid"
radius="full"
suppressHydrationWarning
suppressContentEditableWarning
>
{!isEmbed && <Navbar />}
<AlertDialog.Root open={showDevBuildAlert}>
<AlertDialog.Content>
<AlertDialog.Title>Experimental version!</AlertDialog.Title>
<AlertDialog.Description>
This version may have a lot of issues. Report issues to{' '}
<a href="https://discord.gg/nDt7AjGJQH" target="_blank">
the official Discord server
</a>
. Also consider using{' '}
<a href="https://blitz-krieg.vercel.app/">
the more stable release version
</a>
. You will be asked again in 8 days.
</AlertDialog.Description>
<Flex
direction="column"
style={{ minHeight: '100vh', paddingTop: isEmbed ? undefined : 52 }}
>
{!isEmbed && <Navbar />}
<AlertDialog.Root open={showDevBuildAlert}>
<AlertDialog.Content>
<AlertDialog.Title>Experimental version!</AlertDialog.Title>
<AlertDialog.Description>
This version may have a lot of issues. Report issues to{' '}
<a href="https://discord.gg/nDt7AjGJQH" target="_blank">
the official Discord server
</a>
. Also consider using{' '}
<a href="https://blitz-krieg.vercel.app/">
the more stable release version
</a>
. You will be asked again in 8 days.
</AlertDialog.Description>
<Flex justify="end">
<Button
variant="solid"
onClick={() => {
setShowDevBuildAlert(false);
useApp.setState({ devBuildAgreementTime: Date.now() });
}}
>
Continue
</Button>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<Flex direction="column">{children}</Flex>
{!isEmbed && !isFullScreen && (
<PageWrapper>
<a
target="_blank"
href="https://www.patreon.com/tresabhi"
style={{ textDecoration: 'none' }}
>
<Flex
align="center"
style={{
padding: 16,
borderRadius: 8,
backgroundColor: mintTheme.colors.componentInteractive,
position: 'relative',
overflow: 'hidden',
}}
>
<Flex
direction="column"
gap="1"
style={{
flex: 1,
zIndex: 1,
<Flex justify="end">
<Button
variant="solid"
onClick={() => {
setShowDevBuildAlert(false);
useApp.setState({ devBuildAgreementTime: Date.now() });
}}
>
<Text size="4" color="mint">
Blitzkrieg. Free forever. For everyone.
</Text>
<Text color="mint">
<b>Consider supporting today 🍀</b>
</Text>
</Flex>
<Text
color="mint"
style={{
zIndex: 1,
}}
>
<Flex>
<CaretRightIcon width={32} height={32} />
</Flex>
</Text>
<SkyLine
style={{
position: 'absolute',
right: 'min(8vw, 15%)',
top: '5%',
}}
/>
Continue
</Button>
</Flex>
</a>
</PageWrapper>
)}
</AlertDialog.Content>
</AlertDialog.Root>
{children}
{!isEmbed && !isFullScreen && <Footer />}
</Flex>
</Theme>
</body>
</html>
+1 -1
View File
@@ -3,7 +3,7 @@ import { theme } from '../stitches.config';
export const tool = styleVariants({
enabled: {
boxShadow: `inset 0 -256px 512px -128px ${theme.colors.appBackground1}`,
boxShadow: `inset 0 -200px 128px -128px ${theme.colors.appBackground1}`,
':hover': {
boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`,
+63 -78
View File
@@ -1,94 +1,79 @@
'use client';
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { Flex, Text, TextField } from '@radix-ui/themes';
import { CaretRightIcon } from '@radix-ui/react-icons';
import { Flex } from '@radix-ui/themes';
import Link from 'next/link';
import { useRef } from 'react';
import { Search, SearchItem } from 'react-fuzzysort';
import PageWrapper from '../components/PageWrapper';
import { TOOLS } from '../constants/tools';
import { theme } from '../stitches.config';
import * as styles from './page.css';
export default function Page() {
const input = useRef<HTMLInputElement>(null);
return (
<PageWrapper>
<TextField.Root placeholder="Search tools...">
<TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot>
</TextField.Root>
<Flex direction="column" gap="4" justify="center" style={{ flex: 1 }}>
{/* <TextField.Root placeholder="Search anything...">
<TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot>
</TextField.Root> */}
<Flex gap="3" wrap="wrap" align="center" justify="center">
<Search
input={input}
list={TOOLS.map(
(tool) =>
({
node: (
<Link
href={tool.href ?? `/tools/${tool.id}`}
target={tool.href ? '_blank' : undefined}
className={
tool.disabled ? styles.tool.disabled : styles.tool.enabled
}
style={{
backgroundImage: `url(/assets/banners/${tool.id}.webp)`,
cursor: tool.disabled ? 'default' : 'pointer',
opacity: tool.disabled ? 0.25 : 1,
height: 128,
minWidth: 256,
flex: 1,
borderRadius: 8,
textDecoration: 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-end',
padding: 16,
gap: 4,
transition: `box-shadow ${theme.durations.regular}`,
}}
onClick={(event) => {
if (tool.disabled) event.preventDefault();
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{tool.title}
<CaretRightIcon style={{ width: '1em', height: '1em' }} />
</span>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{tool.disabled ? 'Coming soon!' : tool.description}
</span>
</Link>
),
query: `${tool.title} ${tool.description}`,
}) satisfies SearchItem,
)}
/>
<Flex gap="3" wrap="wrap" align="center" justify="center">
{TOOLS.map((tool) => (
<Link
href={tool.href ?? `/tools/${tool.id}`}
target={tool.href ? '_blank' : undefined}
className={
tool.disabled ? styles.tool.disabled : styles.tool.enabled
}
style={{
backgroundImage: `url(/assets/banners/${tool.id}.webp)`,
cursor: tool.disabled ? 'default' : 'pointer',
opacity: tool.disabled ? 0.25 : 1,
height: 128,
minWidth: 256,
flex: 1,
borderRadius: 8,
textDecoration: 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-end',
padding: 16,
gap: 4,
transition: `box-shadow ${theme.durations.regular}`,
}}
onClick={(event) => {
if (tool.disabled) event.preventDefault();
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{tool.title}
<CaretRightIcon style={{ width: '1em', height: '1em' }} />
</span>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{tool.disabled ? 'Coming soon!' : tool.description}
</span>
</Link>
))}
</Flex>
</Flex>
<Text size="1" color="gray" align="center">
Images courtesy of Wargaming Art Department
</Text>
</PageWrapper>
);
}
@@ -0,0 +1,67 @@
import { LoopIcon, TrashIcon } from '@radix-ui/react-icons';
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
import { use, useState } from 'react';
import { provisionDefinitions } from '../../../../core/blitzkrieg/provisionDefinitions';
import { tankToCompareMember } from '../../../../core/blitzkrieg/tankToCompareMember';
import { mutateCompareTemporary } from '../../../../stores/compare';
import { TankSearch } from '../../tankopedia/components/TankSearch';
interface TankControlProps {
index: number;
}
export function TankControl({ index }: TankControlProps) {
const awaitedProvisionDefinitions = use(provisionDefinitions);
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
return (
<Flex gap="3" justify="center" style={{ width: '100%' }}>
<IconButton
variant="ghost"
onClick={() => {
mutateCompareTemporary((draft) => {
draft.members.splice(index, 1);
draft.sorting = undefined;
});
}}
>
<TrashIcon />
</IconButton>
<Dialog.Root
open={switchTankDialogOpen}
onOpenChange={setSwitchTankDialogOpen}
>
<Dialog.Trigger>
<IconButton variant="ghost">
<LoopIcon />
</IconButton>
</Dialog.Trigger>
<Dialog.Content>
<Flex gap="4" direction="column">
<Flex
direction="column"
gap="4"
style={{ flex: 1 }}
justify="center"
>
<TankSearch
compact
onSelect={(tank) => {
mutateCompareTemporary((draft) => {
draft.members[index] = tankToCompareMember(
tank,
awaitedProvisionDefinitions,
);
draft.sorting = undefined;
});
setSwitchTankDialogOpen(false);
}}
/>
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
</Flex>
);
}
File diff suppressed because it is too large. Load diff
+6 -17
View File
@@ -1,33 +1,22 @@
import { blackA } from '@radix-ui/colors';
import { Flex, Text } from '@radix-ui/themes';
import { PuffLoader } from 'react-spinners';
import { Flex, Spinner, Text } from '@radix-ui/themes';
import { theme } from '../../../stitches.config';
const NAV_HEIGHT = 52.65;
interface LoaderProps {
naked?: boolean;
color?: string;
progress?: number;
}
export function Loader({ naked, color, progress }: LoaderProps) {
export function Loader({ naked, progress }: LoaderProps) {
const animation = (
<div style={{ position: 'relative' }}>
<Flex align="center" gap="2">
<Spinner />
{progress !== undefined && (
<Text
style={{
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(calc(-50% + 5px), calc(-50% + 5px))',
}}
>
{Math.round(progress * 100)}%
</Text>
<Text color="gray">{Math.round(progress * 100)}%</Text>
)}
<PuffLoader color={color ?? theme.colors.solidBackground} />
</div>
</Flex>
);
if (naked) return animation;
+22 -2
View File
@@ -19,7 +19,19 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
return (
<>
{tool && <title>{tool.title}</title>}
{tool && (
<>
<title>{tool.title}</title>
{tool.pageDescription !== undefined && (
<meta name="description" content={tool.pageDescription} />
)}
<meta
property="og:image"
content={`/assets/banners/${toolId}.webp`}
/>
</>
)}
{pathSplit.length === 3 && (
<div
@@ -73,7 +85,15 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
</div>
)}
<Suspense fallback={<Loader />}>{children}</Suspense>
<Suspense
fallback={
<div style={{ flex: 1 }}>
<Loader />
</div>
}
>
{children}
</Suspense>
</>
);
}
+15
View File
@@ -12,6 +12,7 @@ import {
Checkbox,
DropdownMenu,
Flex,
Heading,
Text,
TextField,
} from '@radix-ui/themes';
@@ -249,6 +250,20 @@ export default function Page() {
</div>
<SessionPage naked={false} />
{!session.isTracking && (
<Flex
align="center"
justify="center"
direction="column"
style={{ flex: 1 }}
>
<Heading color="gray">No player selected</Heading>
<Text color="gray">
Search for a <PersonIcon /> Player to get started
</Text>
</Flex>
)}
</PageWrapper>
);
}
@@ -8,7 +8,7 @@ import {
Text,
} from '@radix-ui/themes';
import { use } from 'react';
import { ConsumableButton } from '../../../../../../../components/ModuleButtons/ConsumableButton';
import { ConsumablesManager } from '../../../../../../../components/ConsumablesManager';
import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableProvisionInclusivity';
import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumableDefinitions';
import { useEquipment } from '../../../../../../../hooks/useEquipment';
@@ -68,48 +68,20 @@ export function Consumables() {
</Button>
</Flex>
<Flex wrap="wrap">
{consumablesList.map((consumable, index) => {
const selected = consumables.includes(consumable.id);
return (
<ConsumableButton
special={consumable.duration === undefined}
duration={
consumable.duration === undefined
? undefined
: consumable.duration * (hasHighEndConsumables ? 0.7 : 1)
}
cooldown={
consumable.cooldown *
(hasConsumableDeliverySystem ? 0.85 : 1) *
(1 - cooldownBooster * 0.1)
}
key={consumable.id}
first={index === 0}
last={index === consumablesList.length - 1}
rowChild
disabled={
protagonist.tank.consumables === consumables.length && !selected
}
consumable={consumable.id}
selected={selected}
onClick={() => {
mutateDuel((draft) => {
if (selected) {
draft.protagonist!.consumables =
draft.protagonist!.consumables.filter(
(id) => id !== consumable.id,
);
} else {
draft.protagonist!.consumables.push(consumable.id);
}
});
}}
/>
);
})}
</Flex>
<ConsumablesManager
consumables={consumablesList}
selected={consumables}
cooldownBooster={cooldownBooster}
disabled={protagonist.tank.consumables === consumables.length}
hasConsumableDeliverySystem={hasConsumableDeliverySystem}
hasHighEndConsumables={hasHighEndConsumables}
timers
onChange={(consumables) => {
mutateDuel((draft) => {
draft.protagonist!.consumables = consumables;
});
}}
/>
</ConfigurationChildWrapper>
);
}
@@ -1,6 +1,6 @@
import { Button, Flex, Heading } from '@radix-ui/themes';
import { use } from 'react';
import { EquipmentButton } from '../../../../../../../components/ModuleButtons/EquipmentButton';
import { EquipmentManager } from '../../../../../../../components/EquipmentManager';
import { equipmentDefinitions } from '../../../../../../../core/blitzkrieg/equipmentDefinitions';
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
@@ -8,9 +8,11 @@ import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
export function Equipment() {
const protagonist = useDuel((state) => state.protagonist!);
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
const equipmentRows =
const equipmentPreset =
awaitedEquipmentDefinitions.presets[protagonist.tank.equipment];
const equipmentMatrix = useDuel((state) => state.protagonist!.equipment);
const equipmentMatrix = useDuel(
(state) => state.protagonist!.equipmentMatrix,
);
return (
<ConfigurationChildWrapper>
@@ -21,7 +23,7 @@ export function Equipment() {
color="red"
onClick={() => {
mutateDuel((draft) => {
draft.protagonist!.equipment.forEach((row) => {
draft.protagonist!.equipmentMatrix.forEach((row) => {
row.forEach((_, index) => {
row[index] = 0;
});
@@ -33,47 +35,15 @@ export function Equipment() {
</Button>
</Flex>
<Flex direction="column" gap="2">
{equipmentRows.map((equipmentRow, rowIndex) => (
<Flex gap="2" key={rowIndex}>
{equipmentRow.map((equipment, columnIndex) => (
<Flex key={columnIndex}>
<EquipmentButton
equipment={equipment[0]}
first
rowChild
selected={equipmentMatrix[rowIndex][columnIndex] === -1}
onClick={() => {
mutateDuel((draft) => {
if (equipmentMatrix[rowIndex][columnIndex] === -1) {
draft.protagonist!.equipment[rowIndex][columnIndex] = 0;
} else {
draft.protagonist!.equipment[rowIndex][columnIndex] =
-1;
}
});
}}
/>
<EquipmentButton
equipment={equipment[1]}
last
rowChild
selected={equipmentMatrix[rowIndex][columnIndex] === 1}
onClick={() => {
mutateDuel((draft) => {
if (equipmentMatrix[rowIndex][columnIndex] === 1) {
draft.protagonist!.equipment[rowIndex][columnIndex] = 0;
} else {
draft.protagonist!.equipment[rowIndex][columnIndex] = 1;
}
});
}}
/>
</Flex>
))}
</Flex>
))}
</Flex>
<EquipmentManager
matrix={equipmentMatrix}
preset={equipmentPreset}
onChange={(matrix) => {
mutateDuel((draft) => {
draft.protagonist!.equipmentMatrix = matrix;
});
}}
/>
</ConfigurationChildWrapper>
);
}
@@ -1,7 +1,5 @@
import { Button, DropdownMenu, Flex, Heading } from '@radix-ui/themes';
import { useState } from 'react';
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
import { Button, Flex, Heading } from '@radix-ui/themes';
import { ModuleManager } from '../../../../../../../components/ModuleManager';
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
@@ -13,10 +11,6 @@ export function Modules() {
protagonist.tank.turrets[0].guns.length > 1 ||
protagonist.tank.engines.length > 1 ||
protagonist.tank.tracks.length > 1;
const [turretMenuOpen, setTurretMenuOpen] = useState(false);
const [gunMenuOpen, setGunMenuOpen] = useState(false);
const [engineMenuOpen, setEngineMenuOpen] = useState(false);
const [trackMenuOpen, setTrackMenuOpen] = useState(false);
return (
<ConfigurationChildWrapper>
@@ -64,181 +58,22 @@ export function Modules() {
)}
</Flex>
<Flex gap="1" wrap="wrap">
<ModuleButton
module="turret"
discriminator={TIER_ROMAN_NUMERALS[protagonist.turret.tier]}
first
last
rowChild
onClick={() => setTurretMenuOpen(true)}
/>
<DropdownMenu.Root
open={turretMenuOpen}
onOpenChange={setTurretMenuOpen}
>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup value={`${protagonist.turret.id}`}>
{[...protagonist.tank.turrets].reverse().map((turret) => {
return (
<DropdownMenu.RadioItem
key={turret.id}
value={`${turret.id}`}
onClick={() => {
mutateDuel((draft) => {
draft.protagonist!.turret = turret;
draft.protagonist!.gun = turret.guns.at(-1)!;
draft.protagonist!.shell =
turret.guns.at(-1)!.shells[0];
});
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
setTurretMenuOpen(false);
}}
>
<ModuleButton
variant="ghost"
module="turret"
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
/>{' '}
{turret.name}
</DropdownMenu.RadioItem>
);
})}
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
<ModuleButton
module="gun"
discriminator={TIER_ROMAN_NUMERALS[protagonist.gun.tier]}
first
last
rowChild
onClick={() => setGunMenuOpen(true)}
/>
<DropdownMenu.Root open={gunMenuOpen} onOpenChange={setGunMenuOpen}>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup value={`${protagonist.gun.id}`}>
{[...protagonist.turret.guns].reverse().map((gun) => {
return (
<DropdownMenu.RadioItem
key={gun.id}
value={`${gun.id}`}
onClick={() => {
mutateDuel((draft) => {
draft.protagonist!.gun = gun;
draft.protagonist!.shell = gun.shells[0];
});
}}
>
<ModuleButton
variant="ghost"
module="gun"
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
/>{' '}
{gun.name}
</DropdownMenu.RadioItem>
);
})}
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
<ModuleButton
module="engine"
discriminator={TIER_ROMAN_NUMERALS[protagonist.engine.tier]}
first
last
rowChild
onClick={() => setEngineMenuOpen(true)}
/>
<DropdownMenu.Root
open={engineMenuOpen}
onOpenChange={setEngineMenuOpen}
>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup value={`${protagonist.engine.id}`}>
{[...protagonist.tank.engines].reverse().map((engine) => {
return (
<DropdownMenu.RadioItem
key={engine.id}
value={`${engine.id}`}
onClick={() => {
mutateDuel((draft) => {
draft.protagonist!.engine = engine;
});
}}
>
<ModuleButton
variant="ghost"
module="engine"
discriminator={TIER_ROMAN_NUMERALS[engine.tier]}
/>{' '}
{engine.name}
</DropdownMenu.RadioItem>
);
})}
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
<ModuleButton
module="chassis"
discriminator={TIER_ROMAN_NUMERALS[protagonist.track.tier]}
first
last
rowChild
onClick={() => setTrackMenuOpen(true)}
/>
<DropdownMenu.Root open={trackMenuOpen} onOpenChange={setTrackMenuOpen}>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup value={`${protagonist.track.id}`}>
{[...protagonist.tank.tracks].reverse().map((track) => {
return (
<DropdownMenu.RadioItem
key={track.id}
value={`${track.id}`}
onClick={() => {
mutateDuel((draft) => {
draft.protagonist!.track = track;
});
}}
>
<ModuleButton
variant="ghost"
module="chassis"
discriminator={TIER_ROMAN_NUMERALS[track.tier]}
/>{' '}
{track.name}
</DropdownMenu.RadioItem>
);
})}
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Flex>
<ModuleManager
tank={protagonist.tank}
turret={protagonist.turret}
gun={protagonist.gun}
shell={protagonist.shell}
engine={protagonist.engine}
track={protagonist.track}
onChange={(modules) => {
mutateDuel((draft) => {
draft.protagonist = { ...draft.protagonist!, ...modules };
});
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
}}
/>
</ConfigurationChildWrapper>
);
}
@@ -1,6 +1,6 @@
import { Button, Flex, Heading } from '@radix-ui/themes';
import { use } from 'react';
import { ProvisionButton } from '../../../../../../../components/ModuleButtons/ProvisionButton';
import { ProvisionsManager } from '../../../../../../../components/ProvisionsManager';
import { availableProvisions } from '../../../../../../../core/blitzkrieg/availableProvisions';
import { provisionDefinitions } from '../../../../../../../core/blitzkrieg/provisionDefinitions';
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
@@ -33,35 +33,16 @@ export function Provisions() {
</Button>
</Flex>
<Flex wrap="wrap">
{provisionsList.map((provision, index) => {
const selected = provisions.includes(provision.id);
return (
<ProvisionButton
key={provision.id}
first={index === 0}
last={index === provisionsList.length - 1}
rowChild
disabled={tank.provisions === provisions.length && !selected}
provision={provision.id}
selected={selected}
onClick={() => {
mutateDuel((draft) => {
if (selected) {
draft.protagonist!.provisions =
draft.protagonist!.provisions.filter(
(id) => id !== provision.id,
);
} else {
draft.protagonist!.provisions.push(provision.id);
}
});
}}
/>
);
})}
</Flex>
<ProvisionsManager
provisions={provisionsList.map((provision) => provision.id)}
selected={provisions}
disabled={tank.provisions === provisions.length}
onChange={(provisions) => {
mutateDuel((draft) => {
draft.protagonist!.provisions = provisions;
});
}}
/>
</ConfigurationChildWrapper>
);
}
@@ -9,17 +9,14 @@ import {
Text,
} from '@radix-ui/themes';
import { use } from 'react';
import { GenericTankComponentButton } from '../../../../../../../../components/ModuleButtons/GenericTankComponentButton';
import { asset } from '../../../../../../../../core/blitzkrieg/asset';
import { CrewSkillManager } from '../../../../../../../../components/CrewSkillManager';
import { createDefaultSkills } from '../../../../../../../../core/blitzkrieg/createDefaultSkills';
import { skillDefinitions } from '../../../../../../../../core/blitzkrieg/skillDefinitions';
import { Tier } from '../../../../../../../../core/blitzkrieg/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../../../../../../../../core/blitzkrieg/tankDefinitions/constants';
import {
mutateTankopediaTemporary,
useTankopediaTemporary,
} from '../../../../../../../../stores/tankopedia';
import { ConfigurationChildWrapper } from '../ConfigurationChildWrapper';
import { permanentSkills } from './constants';
export function Skills() {
const awaitedSkillDefinitions = use(skillDefinitions);
@@ -27,11 +24,7 @@ export function Skills() {
if (Object.keys(skillLevels).length === 0) {
mutateTankopediaTemporary((draft) => {
Object.values(awaitedSkillDefinitions.classes).forEach((skills) => {
skills.forEach((skill) => {
draft.skills[skill] = permanentSkills.includes(skill) ? 7 : 0;
});
});
draft.skills = createDefaultSkills(awaitedSkillDefinitions);
});
return null;
@@ -92,43 +85,14 @@ export function Skills() {
</Button>
</Flex>
<Flex direction="column" gap="2">
{Object.entries(awaitedSkillDefinitions.classes).map(
([tankClass, skills]) => (
<Flex gap="2" key={tankClass}>
{skills.map((skill) => {
const level = skillLevels[skill];
return (
<GenericTankComponentButton
special={!permanentSkills.includes(skill)}
first
last
selected={level > 0}
discriminator={
level === 0
? undefined
: TIER_ROMAN_NUMERALS[level as Tier]
}
icon={asset(`icons/skills/${skill}.webp`)}
onClick={(event) => {
mutateTankopediaTemporary((draft) => {
if (event.shiftKey) {
draft.skills[skill] =
draft.skills[skill] === 0 ? 7 : 0;
} else {
draft.skills[skill] = level - 1;
if (draft.skills[skill] < 0) draft.skills[skill] = 7;
}
});
}}
/>
);
})}
</Flex>
),
)}
</Flex>
<CrewSkillManager
skillLevels={skillLevels}
onChange={(skills) => {
mutateTankopediaTemporary((draft) => {
draft.skills = skills;
});
}}
/>
</ConfigurationChildWrapper>
);
}
@@ -14,48 +14,53 @@ import { lerp } from 'three/src/math/MathUtils';
import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
import { coefficient } from '../../../../../../core/blitzkrieg/coefficient';
import { degressiveStat } from '../../../../../../core/blitzkrieg/degressiveStat';
import { equipmentDefinitions } from '../../../../../../core/blitzkrieg/equipmentDefinitions';
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
import { normalizeBoundingBox } from '../../../../../../core/blitzkrieg/normalizeBoundingBox';
import { progressiveStat } from '../../../../../../core/blitzkrieg/progressiveStat';
import { resolveDpm } from '../../../../../../core/blitzkrieg/resolveDpm';
import { sum } from '../../../../../../core/blitzkrieg/sum';
import { provisionDefinitions } from '../../../../../../core/blitzkrieg/provisionDefinitions';
import { tankCharacteristics } from '../../../../../../core/blitzkrieg/tankCharacteristics';
import {
CREW_MEMBER_NAMES,
GUN_TYPE_NAMES,
SHELL_NAMES,
} from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
import { useConsumable } from '../../../../../../hooks/useConsumable';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
import { useProvision } from '../../../../../../hooks/useProvision';
import { useProvisions } from '../../../../../../hooks/useProvisions';
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
import { useTankopediaTemporary } from '../../../../../../stores/tankopedia';
import { Info } from './components/Info';
import { InfoWithDelta } from './components/InfoWithDelta';
export function Characteristics() {
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
const awaitedModelDefinitions = use(modelDefinitions);
const awaitedProvisionDefinitions = use(provisionDefinitions);
const crewSkills = useTankopediaTemporary((state) => state.skills);
const { tank, turret, gun, engine, track, shell } = useDuel(
(state) => state.protagonist!,
);
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
const isFullScreen = useFullScreen();
const hasImprovedVentilation = useEquipment(102);
const provisions = useProvisions();
const crewMastery = useDuel((state) => state.protagonist!.crewMastery);
const [penetrationDistance, setPenetrationDistance] = useState(250);
const provisions = useDuel((state) => state.protagonist!.provisions);
const provisionCrewBonus =
provisions.reduce(
(total, provision) =>
provision.crew ? total + provision.crew / 100 : total,
awaitedProvisionDefinitions[provision].crew
? total + awaitedProvisionDefinitions[provision].crew! / 100
: total,
0,
) + (hasImprovedVentilation ? 0.05 : 0);
const commanderMastery = crewMastery + provisionCrewBonus;
const crewMemberMastery = commanderMastery * 1.1;
const consumables = useDuel((state) => state.protagonist!.consumables);
const camouflage = useDuel((state) => state.protagonist!.camouflage);
const equipmentMatrix = useDuel(
(state) => state.protagonist!.equipmentMatrix,
);
const { tank, turret, gun, engine, track, shell } = useDuel(
(state) => state.protagonist!,
);
const stockEngine = tank.engines[0];
const stockTrack = tank.tracks[0];
const stockTurret = tank.turrets[0];
@@ -63,187 +68,46 @@ export function Characteristics() {
const tankModelDefinition = awaitedModelDefinitions[tank.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const size = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.boundingBox,
turretModelDefinition.boundingBox,
),
const stats = tankCharacteristics(
{
tank,
camouflage,
consumables,
crewMastery,
crewSkills,
engine,
equipmentMatrix,
gun,
provisions,
shell,
turret,
track,
stockEngine,
stockGun,
stockTrack,
stockTurret,
applyReactive: false,
applyDynamicArmor: false,
applySpallLiner: false,
},
{
equipmentDefinitions: awaitedEquipmentDefinitions,
modelDefinitions: awaitedModelDefinitions,
provisionDefinitions: awaitedProvisionDefinitions,
},
);
const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
const weight =
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
const weightTons = weight / 1000;
const stockWeight =
tank.weight +
stockEngine.weight +
stockTrack.weight +
stockTurret.weight +
stockGun.weight;
const [penetrationDistance, setPenetrationDistance] = useState(250);
const camouflage = useDuel((state) => state.protagonist!.camouflage);
const hasRammer = useEquipment(100);
const hasCalibratedShells = useEquipment(103);
const hasEnhancedGunLayingDrive = useEquipment(104);
const hasSupercharger = useEquipment(107);
const hasVerticalStabilizer = useEquipment(105);
const hasRefinedGun = useEquipment(106);
const hasImprovedAssembly = useEquipment(111);
const hasImprovedOptics = useEquipment(114);
const hasImprovedControl = useEquipment(116);
const hasEngineAccelerator = useEquipment(117);
const hasCamouflageNet = useEquipment(115);
const hasImprovedVerticalStabilizer = useEquipment(122);
const hasImprovedSuspension = useEquipment(123);
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
const hasEnginePowerBoost = useConsumable(14);
const hasImprovedEnginePowerBoost = useConsumable(27);
const hasAdrenaline = useConsumable(18);
const hasTungsten = useConsumable(45);
const hasReticleCalibration = useConsumable(28);
const hasShellReloadBoost = useConsumable(29);
const hasStandardFuel = useProvision(18);
const hasImprovedFuel = useProvision(19);
const hasProtectiveKit = useProvision(22);
const hasSandbagArmor = useProvision(28);
const hasEnhancedSandbagArmor = useProvision(29);
const hasImprovedGunPowder = useProvision(46);
const hasGearOil = useProvision(44);
const hasImprovedGearOil = useProvision(45);
const healthCoefficient = coefficient(
[hasSandbagArmor, 0.03],
[hasEnhancedSandbagArmor, 0.06],
[hasImprovedAssembly, 0.06],
);
const reloadCoefficient =
(coefficient([hasRammer, -0.07]) *
coefficient([true, degressiveStat(crewMemberMastery)])) /
coefficient([hasAdrenaline && gun.type === 'regular', 0.2]);
const hasSupercharger = useEquipment(107);
const hasCalibratedShells = useEquipment(103);
const hasImprovedVerticalStabilizer = useEquipment(122);
const penetrationCoefficient = coefficient([
hasCalibratedShells,
isExplosive(shell.type) ? 0.1 : 0.05,
]);
const damageCoefficient = coefficient(
// TODO: add max rolls
[hasTungsten, 0.15],
);
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
const shellVelocityCoefficient = coefficient(
[hasSupercharger, 0.3],
[hasImprovedGunPowder, 0.3],
);
const penetrationLossOverDistanceCoefficient = coefficient([
hasSupercharger,
-0.5,
]);
const aimTimeCoefficient =
coefficient(
[hasEnhancedGunLayingDrive, -0.1],
[hasReticleCalibration, -0.4],
) * coefficient([true, degressiveStat(crewMemberMastery)]);
const dispersionStillCoefficient =
coefficient([hasRefinedGun, -0.1], [hasReticleCalibration, -0.4]) *
coefficient([true, degressiveStat(crewMemberMastery)]);
const dispersionDamagedCoefficient = coefficient([
hasVerticalStabilizer,
-0.15,
]);
const dispersionHullTraverseCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_turn / 100],
);
const dispersionAfterShotCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.soft_recoil / 100],
);
const dispersionMovingCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_driving / 100],
);
const dispersionTurretTraverseCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_turret * (2 / 100)],
);
const enginePowerCoefficient = coefficient(
[
hasEngineAccelerator &&
(tank.class === 'lightTank' || tank.class === 'mediumTank'),
0.05,
],
[
hasEngineAccelerator &&
(tank.class === 'heavyTank' || tank.class === 'AT-SPG'),
0.07,
],
[hasEnginePowerBoost, 0.2],
[hasImprovedEnginePowerBoost, 0.4],
[hasStandardFuel, 0.03],
[hasImprovedFuel, 0.1],
[hasGearOil, 0.03],
[hasImprovedGearOil, 0.06],
);
const turretTraverseCoefficient =
coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) *
coefficient([true, progressiveStat(crewMemberMastery)]);
const hullTraverseCoefficient =
coefficient(
[hasImprovedControl, 0.1],
[hasImprovedEnginePowerBoost, 0.05],
[true, crewSkills.virtuoso / 100],
) * coefficient([true, progressiveStat(crewMemberMastery)]);
const resistanceCoefficient = coefficient([hasImprovedSuspension, -0.25]);
const viewRangeCoefficient =
coefficient([
hasImprovedOptics,
tank.class === 'AT-SPG' ? 0.05 : tank.class === 'heavyTank' ? 0.07 : 0.1,
]) * coefficient([true, progressiveStat(commanderMastery)]);
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
const speedForwardsSum = sum(
[hasImprovedEnginePowerBoost, 5],
[hasGearOil, 2],
[hasImprovedGearOil, 4],
);
const speedBackwardsSum = sum(
[hasImprovedEnginePowerBoost, 10],
[hasGearOil, 2],
[hasImprovedGearOil, 4],
);
const camouflageSumMoving = sum(
[
hasCamouflageNet,
tank.class === 'heavyTank' ? 0.03 : tank.class === 'AT-SPG' ? 0.07 : 0.05,
],
[
camouflage,
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
],
);
const camouflageSumStill = sum(
[
hasCamouflageNet,
2 *
(tank.class === 'heavyTank'
? 0.03
: tank.class === 'AT-SPG'
? 0.07
: 0.05),
],
[
camouflage,
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
],
[true, crewSkills.camouflage * (3 / 100)],
);
const resolvedEnginePower = engine.power * enginePowerCoefficient;
const dpm = resolveDpm(
gun,
shell,
damageCoefficient,
reloadCoefficient,
intraClipCoefficient,
);
useEffect(() => {
if (penetrationDistanceInput.current) {
@@ -294,19 +158,12 @@ export function Characteristics() {
<Info name="Gun type">{GUN_TYPE_NAMES[gun.type]}</Info>
<InfoWithDelta name="DPM" decimals={0} unit="hp / min">
{dpm}
{stats.dpm}
</InfoWithDelta>
{gun.type === 'autoReloader' && (
<>
<InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min">
{gun.reload.at(-1)! < gun.reload.at(-2)!
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
60
: ((damageCoefficient * shell.damage.armor) /
(gun.reload[0] * reloadCoefficient)) *
60}
{stats.dpmMaximum!}
</InfoWithDelta>
<InfoWithDelta
decimals={0}
@@ -314,27 +171,7 @@ export function Characteristics() {
name="Effective at 60s"
unit="hp / min"
>
{gun.reload.at(-1)! < gun.reload.at(-2)!
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
(60 -
(gun.reload.slice(0, -1).length - 1) *
gun.intraClip *
intraClipCoefficient) +
damageCoefficient *
shell.damage.armor *
gun.reload.slice(0, -1).length
: ((damageCoefficient * shell.damage.armor) /
(gun.reload[0] * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
(60 -
(gun.reload.slice(1).length - 1) *
gun.intraClip *
intraClipCoefficient) +
damageCoefficient *
shell.damage.armor *
gun.reload.slice(1).length}
{stats.dpmEffective!}
</InfoWithDelta>
<Info
indent
@@ -349,14 +186,17 @@ export function Characteristics() {
/>
</>
)}
{gun.type !== 'regular' && (
<InfoWithDelta name="Shells">{stats.shells}</InfoWithDelta>
)}
{gun.type === 'autoReloader' ? (
<>
<InfoWithDelta name="Shells">{gun.reload.length}</InfoWithDelta>
<Info indent name="Most optimal">
{gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1}
{stats.mostOptimalShellIndex}
</Info>
<Info name="Shell reloads" unit="s" />
{gun.reload.map((reload, index) => (
{stats.shellReloads!.map((reload, index) => (
<InfoWithDelta
key={index}
indent
@@ -364,7 +204,7 @@ export function Characteristics() {
decimals={2}
deltaType="lowerIsBetter"
>
{reload * reloadCoefficient}
{reload}
</InfoWithDelta>
))}
</>
@@ -375,7 +215,7 @@ export function Characteristics() {
unit="s"
deltaType="lowerIsBetter"
>
{gun.reload * reloadCoefficient}
{stats.shellReload!}
</InfoWithDelta>
)}
{gun.type === 'autoLoader' ||
@@ -387,14 +227,14 @@ export function Characteristics() {
unit="s"
deltaType="lowerIsBetter"
>
{gun.intraClip * intraClipCoefficient}
{stats.intraClip!}
</InfoWithDelta>
))}
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
{shell.caliber}
{stats.caliber}
</InfoWithDelta>
<InfoWithDelta decimals={0} name="Penetration" unit="mm">
{resolveNearPenetration(shell.penetration) * penetrationCoefficient}
{stats.penetration}
</InfoWithDelta>
{typeof shell.penetration !== 'number' && (
<>
@@ -446,18 +286,18 @@ export function Characteristics() {
</>
)}
<InfoWithDelta name="Damage" unit="hp" decimals={0}>
{shell.damage.armor * damageCoefficient}
{stats.damage}
</InfoWithDelta>
<InfoWithDelta name="Module damage" unit="hp" decimals={0}>
{shell.damage.module * damageCoefficient}
{stats.moduleDamage}
</InfoWithDelta>
{isExplosive(shell.type) && (
<InfoWithDelta name="Splash radius" unit="m" decimals={0}>
{shell.explosionRadius!}
{stats.explosionRadius!}
</InfoWithDelta>
)}
<InfoWithDelta name="Shell velocity" unit="m/s" decimals={0}>
{shell.speed * shellVelocityCoefficient}
{stats.shellVelocity}
</InfoWithDelta>
<InfoWithDelta
decimals={2}
@@ -465,7 +305,7 @@ export function Characteristics() {
unit="s"
deltaType="lowerIsBetter"
>
{gun.aimTime * aimTimeCoefficient}
{stats.aimTime}
</InfoWithDelta>
<Info name="Dispersion at 100m" />
<InfoWithDelta
@@ -475,7 +315,7 @@ export function Characteristics() {
unit="m"
deltaType="lowerIsBetter"
>
{gun.dispersion.base * dispersionStillCoefficient}
{stats.dispersion}
</InfoWithDelta>
<InfoWithDelta
prefix="+ "
@@ -484,7 +324,7 @@ export function Characteristics() {
name="Moving"
deltaType="lowerIsBetter"
>
{track.dispersion.move * dispersionMovingCoefficient}
{stats.dispersionMoving}
</InfoWithDelta>
<InfoWithDelta
decimals={3}
@@ -493,7 +333,7 @@ export function Characteristics() {
name="Hull traversing"
deltaType="lowerIsBetter"
>
{track.dispersion.traverse * dispersionHullTraverseCoefficient}
{stats.dispersionHullTraversing}
</InfoWithDelta>
<InfoWithDelta
decimals={3}
@@ -502,7 +342,7 @@ export function Characteristics() {
name="Turret traversing"
deltaType="lowerIsBetter"
>
{gun.dispersion.traverse * dispersionTurretTraverseCoefficient}
{stats.dispersionTurretTraversing}
</InfoWithDelta>
<InfoWithDelta
decimals={3}
@@ -511,7 +351,7 @@ export function Characteristics() {
name="After shooting"
deltaType="lowerIsBetter"
>
{gun.dispersion.shot * dispersionAfterShotCoefficient}
{stats.dispersionShooting}
</InfoWithDelta>
<InfoWithDelta
decimals={1}
@@ -521,46 +361,42 @@ export function Characteristics() {
unit="scalar"
deltaType="lowerIsBetter"
>
{gun.dispersion.damaged * dispersionDamagedCoefficient}
{stats.dispersionGunDamaged}
</InfoWithDelta>
<Info name="Gun flexibility" unit="°" />
<InfoWithDelta decimals={1} indent name="Depression">
{gunModelDefinition.pitch.max +
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0)}
{stats.gunDepression}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Elevation">
{-gunModelDefinition.pitch.min -
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0)}
{stats.gunElevation}
</InfoWithDelta>
{gunModelDefinition.pitch.front && (
<>
<InfoWithDelta decimals={1} indent name="Frontal depression">
{gunModelDefinition.pitch.front.max}
{stats.gunFrontalDepression!}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Frontal elevation">
{-gunModelDefinition.pitch.front.min}
{stats.gunFrontalElevation!}
</InfoWithDelta>
</>
)}
{gunModelDefinition.pitch.back && (
<>
<InfoWithDelta decimals={1} indent name="Rear depression">
{gunModelDefinition.pitch.back.max}
{stats.gunRearDepression!}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Rear elevation">
{-gunModelDefinition.pitch.back.min}
{stats.gunRearElevation!}
</InfoWithDelta>
</>
)}
{turretModelDefinition.yaw && (
<>
<InfoWithDelta decimals={1} indent name="Azimuth left">
{-turretModelDefinition.yaw.min}
{stats.azimuthLeft!}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Azimuth right">
{turretModelDefinition.yaw.max}
{stats.azimuthRight!}
</InfoWithDelta>
</>
)}
@@ -570,29 +406,13 @@ export function Characteristics() {
<Heading size="5">Maneuverability</Heading>
<Info name="Speed" unit="km/hr" />
<InfoWithDelta decimals={0} indent name="Forwards">
{tank.speed.forwards + speedForwardsSum}
{stats.speedForwards}
</InfoWithDelta>
<InfoWithDelta decimals={0} indent name="Backwards">
{tank.speed.backwards + speedBackwardsSum}
{stats.speedBackwards}
</InfoWithDelta>
<InfoWithDelta decimals={0} name="Power" unit="hp">
{resolvedEnginePower}
</InfoWithDelta>
<Info name="Power to weight ratio" unit="hp/tn" />
<InfoWithDelta decimals={1} indent name="On hard terrain">
{resolvedEnginePower /
weightTons /
(track.resistance.hard * resistanceCoefficient)}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On medium terrain">
{resolvedEnginePower /
weightTons /
(track.resistance.medium * resistanceCoefficient)}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On soft terrain">
{resolvedEnginePower /
weightTons /
(track.resistance.soft * resistanceCoefficient)}
<InfoWithDelta decimals={0} name="Engine power" unit="hp">
{stats.enginePower}
</InfoWithDelta>
<InfoWithDelta
decimals={1}
@@ -600,71 +420,76 @@ export function Characteristics() {
unit="tn"
deltaType="lowerIsBetter"
>
{weightTons}
{stats.weight}
</InfoWithDelta>
<InfoWithDelta name="Turret traverse speed" unit="°/s" decimals={0}>
{turret.traverseSpeed * turretTraverseCoefficient}
</InfoWithDelta>
<Info name="Effective traverse speed" unit="°/s" />
<Info name="Power to weight ratio" unit="hp/tn" />
<InfoWithDelta decimals={1} indent name="On hard terrain">
{(resolvedEnginePower / stockEngine.power) *
track.traverseSpeed *
hullTraverseCoefficient *
(track.resistance.hard /
(track.resistance.hard * resistanceCoefficient)) *
(stockWeight / weight)}
{stats.powerToWeightRatioHardTerrain}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On medium terrain">
{(resolvedEnginePower / stockEngine.power) *
track.traverseSpeed *
hullTraverseCoefficient *
(track.resistance.hard /
(track.resistance.medium * resistanceCoefficient)) *
(stockWeight / weight)}
{stats.powerToWeightRatioMediumTerrain}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On soft terrain">
{(resolvedEnginePower / stockEngine.power) *
track.traverseSpeed *
hullTraverseCoefficient *
(track.resistance.hard /
(track.resistance.soft * resistanceCoefficient)) *
(stockWeight / weight)}
{stats.powerToWeightRatioSoftTerrain}
</InfoWithDelta>
<InfoWithDelta name="Turret traverse speed" unit="°/s" decimals={1}>
{stats.turretTraverseSpeed}
</InfoWithDelta>
<Info name="Hull traverse speed" unit="°/s" />
<InfoWithDelta decimals={1} indent name="On hard terrain">
{stats.hullTraverseHardTerrain}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On medium terrain">
{stats.hullTraverseMediumTerrain}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On soft terrain">
{stats.hullTraverseSoftTerrain}
</InfoWithDelta>
</Flex>
<Flex direction="column" gap="2">
<Heading size="5">Survivability</Heading>
<InfoWithDelta name="Health" unit="hp" decimals={0}>
{(tank.health + turret.health) * healthCoefficient}
{stats.health}
</InfoWithDelta>
<InfoWithDelta name="Fire chance" unit="%" deltaType="lowerIsBetter">
{Math.round(engine.fireChance * fireChanceCoefficient * 100)}
<InfoWithDelta
name="Fire chance"
unit="%"
deltaType="lowerIsBetter"
decimals={0}
>
{stats.fireChance * 100}
</InfoWithDelta>
<InfoWithDelta name="View range" unit="m" decimals={0}>
{turret.viewRange * viewRangeCoefficient}
{stats.viewRange}
</InfoWithDelta>
<Info name="Camouflage" unit="%" />
<InfoWithDelta indent name="Still" decimals={2}>
{(tank.camouflage.still + camouflageSumStill) * 100}
<InfoWithDelta indent name="Still" decimals={0}>
{stats.camouflageStill * 100}
</InfoWithDelta>
<InfoWithDelta indent name="Moving" decimals={2}>
{(tank.camouflage.moving + camouflageSumMoving) * 100}
<InfoWithDelta indent name="Moving" decimals={0}>
{stats.camouflageMoving * 100}
</InfoWithDelta>
<InfoWithDelta indent name="Shooting still" decimals={2}>
{(tank.camouflage.still * gun.camouflageLoss + camouflageSumStill) *
100}
<InfoWithDelta indent name="Shooting still" decimals={0}>
{stats.camouflageShootingStill * 100}
</InfoWithDelta>
<InfoWithDelta indent name="Shooting on move" decimals={2}>
{(tank.camouflage.moving * gun.camouflageLoss + camouflageSumMoving) *
100}
<InfoWithDelta indent name="Shooting moving" decimals={0}>
{stats.camouflageShootingMoving * 100}
</InfoWithDelta>
<InfoWithDelta indent name="Caught on fire" decimals={2}>
{(tank.camouflage.onFire * tank.camouflage.still +
camouflageSumStill) *
100}
<InfoWithDelta indent name="Caught on fire" decimals={0}>
{stats.camouflageCaughtOnFire * 100}
</InfoWithDelta>
<Info name="Size" unit="m">
{size[0].toFixed(2)} x {size[2].toFixed(2)} x {size[1].toFixed(2)}
<Info name="Width" unit="m" decimals={0} deltaType="lowerIsBetter">
{stats.width}
</Info>
<Info name="Height" unit="m" decimals={0} deltaType="lowerIsBetter">
{stats.height}
</Info>
<Info name="Length" unit="m" decimals={0} deltaType="lowerIsBetter">
{stats.length}
</Info>
<Info name="Volume" unit="m" decimals={0} deltaType="lowerIsBetter">
{stats.volume}
</Info>
</Flex>
@@ -69,7 +69,12 @@ export function Controls() {
);
async function handlePoseEvent(event: Pose) {
const hasImprovedVerticalStabilizer = await hasEquipment(122);
const duel = useDuel.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
122,
duel.protagonist!.tank.equipment,
duel.protagonist!.equipmentMatrix,
);
switch (event) {
case Pose.HullDown: {
@@ -1,5 +1,5 @@
import { LightningBoltIcon } from '@radix-ui/react-icons';
import { Button, Card, Flex, Tabs, Theme } from '@radix-ui/themes';
import { Button, Card, Flex, Tabs, Text, Theme } from '@radix-ui/themes';
import { PerspectiveCamera } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import { Suspense, use, useEffect, useRef, useState } from 'react';
@@ -14,6 +14,7 @@ import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
import { BlitzkriegWormWide } from '../../../../../../icons/BlitzkriegWormWide';
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
import mutateTankopediaPersistent, {
TankopediaMode,
@@ -150,112 +151,117 @@ export function TankSandbox() {
}}
ref={canvasWrapper}
>
<Theme radius="full" style={{ height: '100%' }}>
<Flex
style={{
height: isFullScreen ? '100%' : '75vh',
maxHeight: isFullScreen ? 'unset' : 576,
<Flex
style={{
height: isFullScreen ? '100%' : '75vh',
maxHeight: isFullScreen ? 'unset' : 576,
}}
direction="column"
gap="2"
>
<Tabs.Root
value={mode}
onValueChange={(mode) => {
mutateTankopediaPersistent((draft) => {
draft.mode = mode as TankopediaMode;
});
}}
direction="column"
gap="2"
>
<Tabs.Root
value={mode}
onValueChange={(mode) => {
mutateTankopediaPersistent((draft) => {
draft.mode = mode as TankopediaMode;
});
}}
>
<Tabs.List>
<Tabs.Trigger value="model">
<Flex gap="2" align="center">
Model
</Flex>
</Tabs.Trigger>
<Tabs.Trigger value="armor">Armor</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
<div
style={{
height: 'calc(100% - 64px)',
position: 'absolute',
width: '100%',
bottom: 0,
}}
>
{loadModel ? (
<div style={{ width: '100%', height: '100%' }}>
<Canvas
shadows
ref={canvas}
onPointerDown={handlePointerDown}
onPointerMissed={() => {
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
}}
>
<PerspectiveCamera makeDefault fov={25} far={32} />
<Controls />
<SceneProps />
<Suspense fallback={<ModelLoader />}>
{/* TODO: add rotation cube to the top right */}
<Lighting />
<TankModel />
<Armor />
<ShotDisplay />
</Suspense>
</Canvas>
</div>
) : (
<Flex
align="center"
justify="center"
style={{ height: '100%', position: 'relative' }}
>
<Button
style={{
zIndex: 1,
}}
onClick={() => {
setLoadModel(true);
}}
>
<LightningBoltIcon /> Load model
</Button>
<Flex
style={{
filter: 'blur(16px)',
width: 256,
height: 128,
display: 'flex',
position: 'absolute',
top: '50%',
left: '50%',
transformOrigin: 'center center',
transform: 'translate(25%, -25%) scale(300%)',
}}
>
<img src={tankIcon(duel.antagonist!.tank.id, 'big')} />
</Flex>
<Tabs.List>
<Tabs.Trigger value="model">
<Flex gap="2" align="center">
Model
</Flex>
)}
</div>
</Tabs.Trigger>
<Tabs.Trigger value="armor">Armor</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
<Options
canvasWrapper={canvasWrapper}
isFullScreen={isFullScreen}
/>
<Text
color="gray"
style={{
position: 'absolute',
left: '50%',
top: 11,
}}
>
<BlitzkriegWormWide height={32} />
</Text>
{isFullScreen && <AntagonistBar floating />}
<div
style={{
height: 'calc(100% - 64px)',
position: 'absolute',
width: '100%',
bottom: 0,
}}
>
{loadModel ? (
<div style={{ width: '100%', height: '100%' }}>
<Canvas
shadows
ref={canvas}
onPointerDown={handlePointerDown}
onPointerMissed={() => {
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
}}
>
<PerspectiveCamera makeDefault fov={25} far={32} />
<Controls />
<SceneProps />
{loadModel && <RotationInputs isFullScreen={isFullScreen} />}
</Flex>
</Theme>
<Suspense fallback={<ModelLoader />}>
<Lighting />
<TankModel />
<Armor />
<ShotDisplay />
</Suspense>
</Canvas>
</div>
) : (
<Flex
align="center"
justify="center"
style={{ height: '100%', position: 'relative' }}
>
<Button
style={{
zIndex: 1,
}}
onClick={() => {
setLoadModel(true);
}}
>
<LightningBoltIcon /> Load model
</Button>
<Flex
style={{
filter: 'blur(16px)',
width: 256,
height: 128,
display: 'flex',
position: 'absolute',
top: '50%',
left: '50%',
transformOrigin: 'center center',
transform: 'translate(25%, -25%) scale(300%)',
}}
>
<img src={tankIcon(duel.antagonist!.tank.id, 'big')} />
</Flex>
</Flex>
)}
</div>
<Options canvasWrapper={canvasWrapper} isFullScreen={isFullScreen} />
{isFullScreen && <AntagonistBar floating />}
{loadModel && <RotationInputs isFullScreen={isFullScreen} />}
</Flex>
</Card>
</Theme>
);
@@ -1,5 +1,4 @@
import { Html, useProgress } from '@react-three/drei';
import { theme } from '../../../../../../../stitches.config';
import { Loader } from '../../../../../components/Loader';
export function ModelLoader() {
@@ -7,11 +6,7 @@ export function ModelLoader() {
return (
<Html center position={[0, 1.5, 0]}>
<Loader
naked
color={theme.colors.textHighContrast_purple}
progress={progress / 100}
/>
<Loader naked progress={progress / 100} />
</Html>
);
}
@@ -162,7 +162,12 @@ export const TankModel = memo(() => {
}
}
async function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = await hasEquipment(122);
const duel = useDuel.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
122,
duel.protagonist!.tank.equipment,
duel.protagonist!.equipmentMatrix,
);
[pitch, yaw] = applyPitchYawLimits(
pitch,
yaw + event.movementX * (Math.PI / canvas.width),
@@ -223,7 +228,12 @@ export const TankModel = memo(() => {
window.addEventListener('pointerup', handlePointerUp);
}
async function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = await hasEquipment(122);
const duel = useDuel.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
122,
duel.protagonist!.tank.equipment,
duel.protagonist!.equipmentMatrix,
);
[pitch, yaw] = applyPitchYawLimits(
pitch - event.movementY * (Math.PI / canvas.height),
yaw + event.movementX * (Math.PI / canvas.width),
@@ -52,6 +52,7 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
>
<Flex gap="2" style={{ width: 256, pointerEvents: 'auto' }}>
<TextField.Root
radius="full"
style={{
flex: 1,
textAlign: 'right',
@@ -99,6 +100,7 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
</TextField.Root>
<TextField.Root
radius="full"
style={{
flex: 1,
textAlign: 'right',
@@ -159,7 +161,9 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
active={hasCalibratedShells}
onClick={() => {
mutateDuel((draft) => {
draft.antagonist!.equipment[0][0] = hasCalibratedShells ? 0 : 1;
draft.antagonist!.equipmentMatrix[0][0] = hasCalibratedShells
? 0
: 1;
});
}}
/>
@@ -168,7 +172,9 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
active={hasEnhancedArmor}
onClick={() => {
mutateDuel((draft) => {
draft.protagonist!.equipment[1][1] = hasEnhancedArmor ? 0 : -1;
draft.protagonist!.equipmentMatrix[1][1] = hasEnhancedArmor
? 0
: -1;
});
}}
/>
@@ -13,8 +13,6 @@ export function Title() {
return (
<>
<title>{protagonist.tank.name}</title>
<Flex gap="6" justify="center" align="center" style={{ padding: 16 }}>
<Flex gap="2" align="center">
<Heading
+30
View File
@@ -0,0 +1,30 @@
import { ReactNode } from 'react';
import { tankDefinitions } from '../../../../core/blitzkrieg/tankDefinitions';
import { tankIcon } from '../../../../core/blitzkrieg/tankIcon';
interface TankopediaLayoutProps {
children: ReactNode;
params: { id: string };
}
export default async function TankopediaLayout({
children,
params,
}: TankopediaLayoutProps) {
const id = Number(params.id);
const awaitedTankDefinitions = await tankDefinitions;
const tank = awaitedTankDefinitions[id];
return (
<>
<title>{tank.name}</title>
<meta
name="description"
content={`Statistics, armor profiles, equipment, and more about ${tank.name}`}
/>
<meta property="og:image" content={tankIcon(id)} />
{children}
</>
);
}
@@ -1,10 +1,12 @@
import { slateDark } from '@radix-ui/colors';
import {
CaretDownIcon,
CaretRightIcon,
MagnifyingGlassIcon,
TrashIcon,
} from '@radix-ui/react-icons';
import {
AlertDialog,
Button,
Card,
DropdownMenu,
@@ -51,11 +53,16 @@ import { Options } from './components/Options';
interface TankSearchProps {
compact?: boolean;
onSelect?: (tank: TankDefinition) => void;
onSelectAll?: (tanks: TankDefinition[]) => void;
}
const treeTypeOrder: TreeType[] = ['researchable', 'premium', 'collector'];
export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
export function TankSearch({
compact,
onSelect = () => {},
onSelectAll,
}: TankSearchProps) {
const tanksPerPage = compact ? 16 : 96;
const nations = use(NATIONS);
const filters = useTankopediaPersistent((state) => state.filters);
@@ -393,20 +400,28 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
.filter(([name]) => name !== 'page')
.map(([, value]) => value),
]);
const [searchResults, setSearchedList] = useState(searchableTanks);
const [searchQuery, setSearchQuery] = useState('');
const searchResults = useMemo(
() =>
go(searchQuery, searchableTanks, {
keys: ['name', 'nameFull', 'id'] satisfies (keyof TankDefinition)[],
all: true,
}).map(({ obj }) => obj),
[searchQuery, searchableTanks],
);
const page = useTankopediaPersistent((state) => state.filters.page);
const wideFormat = typeof window !== 'undefined' && window.innerWidth > 512;
const chunkSize = wideFormat ? tanksPerPage / 2 : Infinity;
const searchResultsPageSlice = searchResults.slice(
page * tanksPerPage,
(page + 1) * tanksPerPage,
);
const firstChunk = searchResultsPageSlice.slice(0, chunkSize);
const secondChunk = searchResultsPageSlice.slice(chunkSize);
const firstChunk = searchResultsPageSlice.slice(
0,
Math.ceil(searchResultsPageSlice.length / 2),
);
const secondChunk = searchResultsPageSlice.slice(
Math.ceil(searchResultsPageSlice.length / 2),
);
useEffect(() => {
setSearchedList(searchableTanks);
}, [searchableTanks]);
useEffect(() => {
mutateTankopediaPersistent((draft) => {
draft.filters.page = Math.min(
@@ -425,21 +440,36 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
ref={input}
placeholder="Search tanks..."
onChange={(event) => {
const results = go(event.target.value, searchableTanks, {
keys: [
'name',
'nameFull',
'id',
] satisfies (keyof TankDefinition)[],
all: true,
}).map(({ obj }) => obj);
setSearchedList(results);
setSearchQuery(event.target.value);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
if (
searchResults.length > 0 &&
input.current &&
input.current.value.length > 0
) {
onSelect(searchResults[0]);
}
}
}}
>
<TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot>
{searchResults.length > 0 &&
input.current &&
input.current.value.length > 0 && (
<TextField.Slot>
<Button
variant="ghost"
onClick={() => onSelect(searchResults[0])}
>
{searchResults[0].name} <CaretRightIcon />
</Button>
</TextField.Slot>
)}
</TextField.Root>
<DropdownMenu.Root>
@@ -796,16 +826,63 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
<Options />
</Flex>
<Flex justify="center">
<Flex justify="center" gap="4" align="center">
<Text color="gray">
Sorting: {SORT_NAMES[sort.by]}, {sort.direction}
</Text>
{onSelectAll && searchResults.length <= 8 && (
<Button variant="ghost" onClick={() => onSelectAll(searchResults)}>
Select all
</Button>
)}
{onSelectAll && searchResults.length > 8 && (
<AlertDialog.Root>
<AlertDialog.Trigger>
<Button variant="ghost">Select all</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Title>
Woah! That's a lot of tanks.
</AlertDialog.Title>
<AlertDialog.Description>
Would you like to select{' '}
<Text color="red">all {searchResults.length} tanks</Text>?
</AlertDialog.Description>
<Flex gap="2" justify="end">
<AlertDialog.Cancel>
<Button variant="soft" color="gray">
Cancel
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action onClick={() => onSelectAll(searchResults)}>
<Button variant="solid" color="red">
Select {searchResults.length} tanks
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
)}
</Flex>
{!compact && (
<PageTurner tanksPerPage={tanksPerPage} searchedList={searchResults} />
)}
<Flex wrap="wrap" gap="2" justify={compact ? 'between' : 'center'}>
<Flex
wrap="wrap"
gap="2"
style={{
justifyContent:
compact && firstChunk.length > 0 && secondChunk.length > 0
? 'space-around'
: 'center',
}}
>
{!compact &&
searchResultsPageSlice.map((tank) => (
<Link
@@ -927,7 +1004,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
</Link>
))}
{compact && (
{compact && firstChunk.length > 0 && (
<CompactSearchResultRow tanks={firstChunk} onSelect={onSelect} />
)}
@@ -935,6 +1012,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
<CompactSearchResultRow tanks={secondChunk} onSelect={onSelect} />
)}
</Flex>
<PageTurner tanksPerPage={tanksPerPage} searchedList={searchResults} />
</>
);
+8 -17
View File
@@ -385,26 +385,17 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
right: '5%',
}}
>
<div
<img
src={await tankIconPng(id)}
style={{
height: 120,
width: 240,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
height: 100,
width: 100,
objectFit: 'contain',
position: 'absolute',
left: '25%',
top: '5%',
top: 0,
}}
>
<img
src={await tankIconPng(id)}
style={{
height: '100%',
position: 'absolute',
objectFit: 'contain',
}}
/>
</div>
/>
<div
style={{
@@ -53,6 +53,7 @@ export function HeroStat({ stats, locale }: WN8DisplayProps) {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: -4,
}}
>
<span
@@ -70,8 +70,8 @@ export function CoreArmorSceneComponent({
material.uniforms.explosionRadius.value = shell.explosionRadius;
const duel = useDuel.getState();
await handleProtagonistEquipmentChange(duel.protagonist!.equipment);
await handleAntagonistEquipmentChange(duel.antagonist!.equipment);
await handleProtagonistEquipmentChange(duel.protagonist!.equipmentMatrix);
await handleAntagonistEquipmentChange(duel.antagonist!.equipmentMatrix);
}
function handleVisualChange(
visual: TankopediaPersistent['model']['visual'],
@@ -84,25 +84,40 @@ export function CoreArmorSceneComponent({
async function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix,
) {
const hasEnhancedArmor = await hasEquipment(110, false, equipment);
const duel = useDuel.getState();
const hasEnhancedArmor = await hasEquipment(
110,
duel.protagonist!.tank.equipment,
equipment,
);
material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.04
: thickness;
}
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const shell = useDuel.getState().antagonist!.shell;
const penetration = resolveNearPenetration(shell.penetration);
const hasCalibratedShells = await hasEquipment(103, true, equipment);
const duel = useDuel.getState();
const penetration = resolveNearPenetration(
duel.antagonist!.shell.penetration,
);
const hasCalibratedShells = await hasEquipment(
103,
duel.antagonist!.tank.equipment,
equipment,
);
material.uniforms.penetration.value = hasCalibratedShells
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05)
? penetration * (isExplosive(duel.antagonist!.shell.type) ? 1.1 : 1.05)
: penetration;
}
handleShellChange(useDuel.getState().antagonist!.shell);
handleVisualChange(useTankopediaPersistent.getState().model.visual);
handleProtagonistEquipmentChange(useDuel.getState().protagonist!.equipment);
handleAntagonistEquipmentChange(useDuel.getState().antagonist!.equipment);
handleProtagonistEquipmentChange(
useDuel.getState().protagonist!.equipmentMatrix,
);
handleAntagonistEquipmentChange(
useDuel.getState().antagonist!.equipmentMatrix,
);
const unsubscribes = [
useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange),
@@ -111,11 +126,11 @@ export function CoreArmorSceneComponent({
handleVisualChange,
),
useDuel.subscribe(
(state) => state.protagonist!.equipment,
(state) => state.protagonist!.equipmentMatrix,
handleProtagonistEquipmentChange,
),
useDuel.subscribe(
(state) => state.antagonist!.equipment,
(state) => state.antagonist!.equipmentMatrix,
handleAntagonistEquipmentChange,
),
];
@@ -50,32 +50,47 @@ export function SpacedArmorSubExternal({
async function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix,
) {
const hasEnhancedArmor = await hasEquipment(110, false, equipment);
const duel = useDuel.getState();
const hasEnhancedArmor = await hasEquipment(
110,
duel.protagonist!.tank.equipment,
equipment,
);
material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.04
: thickness;
}
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const shell = useDuel.getState().antagonist!.shell;
const penetration = resolveNearPenetration(shell.penetration);
const hasCalibratedShells = await hasEquipment(103, true, equipment);
const duel = useDuel.getState();
const penetration = resolveNearPenetration(
duel.antagonist!.shell.penetration,
);
const hasCalibratedShells = await hasEquipment(
103,
duel.antagonist!.tank.equipment,
equipment,
);
material.uniforms.penetration.value = hasCalibratedShells
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05)
? penetration * (isExplosive(duel.antagonist!.shell.type) ? 1.1 : 1.05)
: penetration;
}
handleShellChange(useDuel.getState().antagonist!.shell);
handleProtagonistEquipmentChange(useDuel.getState().protagonist!.equipment);
handleAntagonistEquipmentChange(useDuel.getState().antagonist!.equipment);
handleProtagonistEquipmentChange(
useDuel.getState().protagonist!.equipmentMatrix,
);
handleAntagonistEquipmentChange(
useDuel.getState().antagonist!.equipmentMatrix,
);
const unsubscribes = [
useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange),
useDuel.subscribe(
(state) => state.protagonist!.equipment,
(state) => state.protagonist!.equipmentMatrix,
handleProtagonistEquipmentChange,
),
useDuel.subscribe(
(state) => state.antagonist!.equipment,
(state) => state.antagonist!.equipmentMatrix,
handleAntagonistEquipmentChange,
),
];
@@ -62,32 +62,46 @@ export function SpacedArmorSubSpaced({
async function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix,
) {
const hasEnhancedArmor = await hasEquipment(110, false, equipment);
const duel = useDuel.getState();
const hasEnhancedArmor = await hasEquipment(
110,
duel.protagonist!.tank.equipment,
equipment,
);
material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.04
: thickness;
}
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = useDuel.getState();
const shell = useDuel.getState().antagonist!.shell;
const penetration = resolveNearPenetration(shell.penetration);
const hasCalibratedShells = await hasEquipment(103, true, equipment);
const hasCalibratedShells = await hasEquipment(
103,
duel.antagonist!.tank.equipment,
equipment,
);
material.uniforms.penetration.value = hasCalibratedShells
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05)
: penetration;
}
handleShellChange(useDuel.getState().antagonist!.shell);
handleProtagonistEquipmentChange(useDuel.getState().protagonist!.equipment);
handleAntagonistEquipmentChange(useDuel.getState().antagonist!.equipment);
handleProtagonistEquipmentChange(
useDuel.getState().protagonist!.equipmentMatrix,
);
handleAntagonistEquipmentChange(
useDuel.getState().antagonist!.equipmentMatrix,
);
const unsubscribes = [
useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange),
useDuel.subscribe(
(state) => state.protagonist!.equipment,
(state) => state.protagonist!.equipmentMatrix,
handleProtagonistEquipmentChange,
),
useDuel.subscribe(
(state) => state.antagonist!.equipment,
(state) => state.antagonist!.equipmentMatrix,
handleAntagonistEquipmentChange,
),
];
@@ -89,8 +89,17 @@ export function SpacedArmorSceneComponent({
async onClick(event) {
event.stopPropagation();
const hasCalibratedShells = await hasEquipment(103, true);
const hasEnhancedArmor = await hasEquipment(110);
const duel = useDuel.getState();
const hasCalibratedShells = await hasEquipment(
103,
duel.antagonist!.tank.equipment,
duel.antagonist!.equipmentMatrix,
);
const hasEnhancedArmor = await hasEquipment(
110,
duel.protagonist!.tank.equipment,
duel.protagonist!.equipmentMatrix,
);
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
const shell = useDuel.getState().antagonist!.shell;
const explosive = isExplosive(shell.type);
+73
View File
@@ -0,0 +1,73 @@
import { Flex } from '@radix-ui/themes';
import { ConsumableEntry } from '../core/blitzkrieg/consumableDefinitions';
import { ConsumableButton } from './ModuleButtons/ConsumableButton';
interface ConsumablesManagerProps {
consumables: ConsumableEntry[];
selected: number[];
onChange?: (consumables: number[]) => void;
disabled?: boolean;
timers?: boolean;
hasHighEndConsumables?: boolean;
hasConsumableDeliverySystem?: boolean;
cooldownBooster?: number;
}
export function ConsumablesManager({
consumables,
selected,
onChange,
disabled,
timers,
hasConsumableDeliverySystem,
cooldownBooster,
hasHighEndConsumables,
}: ConsumablesManagerProps) {
return (
<Flex wrap="wrap">
{consumables.map((consumable, index) => {
const isSelected = selected.some((id) => id === consumable.id);
return (
<ConsumableButton
special={consumable.duration === undefined}
duration={
timers
? consumable.duration === undefined
? undefined
: consumable.duration * (hasHighEndConsumables ? 0.7 : 1)
: undefined
}
cooldown={
timers
? consumable.cooldown *
(hasConsumableDeliverySystem ? 0.85 : 1) *
(1 - cooldownBooster! * 0.1)
: undefined
}
key={consumable.id}
first={index === 0}
last={index === consumables.length - 1}
rowChild
disabled={disabled && !isSelected}
consumable={consumable.id}
selected={isSelected}
onClick={() => {
if (!onChange) return;
const draft = [...selected];
if (draft.some((id) => id === consumable.id)) {
draft.splice(draft.indexOf(consumable.id), 1);
} else {
draft.push(consumable.id);
}
onChange(draft);
}}
/>
);
})}
</Flex>
);
}
+61
View File
@@ -0,0 +1,61 @@
import { Flex } from '@radix-ui/themes';
import { use } from 'react';
import { permanentSkills } from '../app/tools/tankopedia/[id]/components/Characteristics/components/Skills/constants';
import { asset } from '../core/blitzkrieg/asset';
import { skillDefinitions } from '../core/blitzkrieg/skillDefinitions';
import { Tier } from '../core/blitzkrieg/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../core/blitzkrieg/tankDefinitions/constants';
import { GenericTankComponentButton } from './ModuleButtons/GenericTankComponentButton';
interface CrewSkillManagerProps {
skillLevels: Record<string, number>;
onChange?: (skillLevels: Record<string, number>) => void;
}
export function CrewSkillManager({
skillLevels,
onChange,
}: CrewSkillManagerProps) {
const awaitedSkillDefinitions = use(skillDefinitions);
return (
<Flex direction="column" gap="2">
{Object.entries(awaitedSkillDefinitions.classes).map(
([tankClass, skills]) => (
<Flex gap="2" key={tankClass}>
{skills.map((skill) => {
const level = skillLevels[skill];
return (
<GenericTankComponentButton
special={!permanentSkills.includes(skill)}
first
last
selected={level > 0}
discriminator={
level === 0 ? undefined : TIER_ROMAN_NUMERALS[level as Tier]
}
icon={asset(`icons/skills/${skill}.webp`)}
onClick={(event) => {
if (!onChange) return;
const draft = { ...skillLevels };
if (event.shiftKey) {
draft[skill] = draft[skill] === 0 ? 7 : 0;
} else {
draft[skill] = level - 1;
if (draft[skill] < 0) draft[skill] = 7;
}
onChange(draft);
}}
/>
);
})}
</Flex>
),
)}
</Flex>
);
}
+64
View File
@@ -0,0 +1,64 @@
import { Flex } from '@radix-ui/themes';
import { cloneDeep } from 'lodash';
import { EquipmentPreset } from '../core/blitzkrieg/equipmentDefinitions';
import { EquipmentMatrix } from '../stores/duel';
import { EquipmentButton } from './ModuleButtons/EquipmentButton';
interface EquipmentManagerProps {
preset: EquipmentPreset;
matrix: EquipmentMatrix;
onChange: (matrix: EquipmentMatrix) => void;
}
export function EquipmentManager({
preset,
matrix,
onChange,
}: EquipmentManagerProps) {
return (
<Flex direction="column" gap="2">
{preset.map((equipmentRow, rowIndex) => (
<Flex gap="2" key={rowIndex}>
{equipmentRow.map((equipment, columnIndex) => (
<Flex key={columnIndex}>
<EquipmentButton
equipment={equipment[0]}
first
rowChild
selected={matrix[rowIndex][columnIndex] === -1}
onClick={() => {
const draft = cloneDeep(matrix);
if (draft[rowIndex][columnIndex] === -1) {
draft[rowIndex][columnIndex] = 0;
} else {
draft[rowIndex][columnIndex] = -1;
}
onChange(draft);
}}
/>
<EquipmentButton
equipment={equipment[1]}
last
rowChild
selected={matrix[rowIndex][columnIndex] === 1}
onClick={() => {
const draft = cloneDeep(matrix);
if (draft[rowIndex][columnIndex] === 1) {
draft[rowIndex][columnIndex] = 0;
} else {
draft[rowIndex][columnIndex] = 1;
}
onChange(draft);
}}
/>
</Flex>
))}
</Flex>
))}
</Flex>
);
}
@@ -0,0 +1,13 @@
import { Code } from '@radix-ui/themes';
import { use } from 'react';
import { gameDefinitions } from '../../../core/blitzkrieg/gameDefinitions';
export function WoTBVersion() {
const awaitedGameDefinitions = use(gameDefinitions);
return (
<Code color="gray" size="2">
{awaitedGameDefinitions.version}
</Code>
);
}
+54
View File
@@ -0,0 +1,54 @@
import { Card, Code, Flex, Link, Spinner, Text, Theme } from '@radix-ui/themes';
import { Suspense } from 'react';
import packageJSON from '../../../package.json';
import { useWideFormat } from '../../hooks/useWideFormat';
import { WoTBVersion } from './components/WoTBVersion';
export function Footer() {
const wideFormat = useWideFormat();
return (
<Theme radius="none">
<Card>
<Flex justify="center">
<Flex
justify={wideFormat ? 'between' : 'center'}
style={{
flex: 1,
maxWidth: 800,
padding: '0 16px',
}}
>
<Flex gap="1">
<Text color="gray" size="2">
Blitzkrieg
</Text>
<Code color="gray" size="2">
{packageJSON.version}
</Code>
<Text color="gray" size="2">
for WoTB
</Text>
<Suspense fallback={<Spinner />}>
<WoTBVersion />
</Suspense>
</Flex>
{wideFormat && (
<Link
size="2"
href="https://www.patreon.com/tresabhi"
target="_blank"
>
Consider donating on Patreon
</Link>
)}
</Flex>
</Flex>
</Card>
</Theme>
);
}
@@ -1,6 +1,7 @@
export function Analytics() {
export function Google() {
return (
<>
{/* analytics */}
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-98NTMRHRP9"
@@ -16,6 +17,13 @@ export function Analytics() {
`,
}}
/>
{/* adsense */}
<script
async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-1289406790225052"
crossOrigin="anonymous"
/>
</>
);
}
@@ -8,7 +8,7 @@ import { TankComponentButtonProps } from './TankComponentButton';
interface ConsumableButtonProps extends TankComponentButtonProps {
consumable: number;
duration?: number;
cooldown: number;
cooldown?: number;
}
export function ConsumableButton({
@@ -17,7 +17,7 @@ export function ConsumableButton({
duration,
...props
}: ConsumableButtonProps) {
const cooldownDelta = useDelta(cooldown);
const cooldownDelta = useDelta(cooldown ?? 0);
const durationDelta = useDelta(duration ?? 0);
return (
@@ -25,38 +25,42 @@ export function ConsumableButton({
icon={asset(`icons/consumables/${consumable}.webp`)}
{...props}
>
<Flex direction="column" style={{ transform: 'translateX(-10px)' }}>
{duration && (
<Text
color={
durationDelta < 0
? 'green'
: durationDelta > 0
? 'tomato'
: undefined
}
>
<Flex align="center" gap="1" style={{ marginBottom: -4 }}>
<ClockIcon width={12} height={12} />
{Math.round(duration)}s
</Flex>
</Text>
)}
<Text
color={
cooldownDelta < 0
? 'green'
: cooldownDelta > 0
? 'tomato'
: undefined
}
>
<Flex align="center" gap="1">
<ReloadIcon width={12} height={12} />
{Math.round(cooldown)}s
</Flex>
</Text>
</Flex>
{(cooldown || duration) && (
<Flex direction="column" style={{ transform: 'translateX(-10px)' }}>
{duration && (
<Text
color={
durationDelta < 0
? 'green'
: durationDelta > 0
? 'tomato'
: undefined
}
>
<Flex align="center" gap="1" style={{ marginBottom: -4 }}>
<ClockIcon width={12} height={12} />
{Math.round(duration)}s
</Flex>
</Text>
)}
{cooldown && (
<Text
color={
cooldownDelta < 0
? 'green'
: cooldownDelta > 0
? 'tomato'
: undefined
}
>
<Flex align="center" gap="1">
<ReloadIcon width={12} height={12} />
{Math.round(cooldown)}s
</Flex>
</Text>
)}
</Flex>
)}
</GenericTankComponentButton>
);
}
+213
View File
@@ -0,0 +1,213 @@
import { DropdownMenu, Flex } from '@radix-ui/themes';
import { useState } from 'react';
import {
EngineDefinition,
GunDefinition,
ShellDefinition,
TankDefinition,
TrackDefinition,
TurretDefinition,
} from '../core/blitzkrieg/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../core/blitzkrieg/tankDefinitions/constants';
import { ModuleButton } from './ModuleButtons/ModuleButton';
interface ModuleManagerProps {
tank: TankDefinition;
turret: TurretDefinition;
gun: GunDefinition;
shell: ShellDefinition;
engine: EngineDefinition;
track: TrackDefinition;
onChange?: (modules: {
turret: TurretDefinition;
gun: GunDefinition;
engine: EngineDefinition;
track: TrackDefinition;
}) => void;
}
export function ModuleManager({
tank,
onChange,
...modules
}: ModuleManagerProps) {
const [turretMenuOpen, setTurretMenuOpen] = useState(false);
const [gunMenuOpen, setGunMenuOpen] = useState(false);
const [engineMenuOpen, setEngineMenuOpen] = useState(false);
const [trackMenuOpen, setTrackMenuOpen] = useState(false);
return (
<Flex gap="1" wrap="wrap">
<ModuleButton
module="turret"
discriminator={TIER_ROMAN_NUMERALS[modules.turret.tier]}
first
last
rowChild
onClick={() => setTurretMenuOpen(true)}
/>
<DropdownMenu.Root open={turretMenuOpen} onOpenChange={setTurretMenuOpen}>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup value={`${modules.turret.id}`}>
{[...tank.turrets].reverse().map((turret) => {
return (
<DropdownMenu.RadioItem
key={turret.id}
value={`${turret.id}`}
onClick={() => {
const draft = { ...modules };
draft.turret = turret;
draft.gun = draft.turret.guns.at(-1)!;
draft.shell = draft.gun.shells.at(-1)!;
onChange?.(draft);
setTurretMenuOpen(false);
}}
>
<ModuleButton
variant="ghost"
module="turret"
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
/>{' '}
{turret.name}
</DropdownMenu.RadioItem>
);
})}
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
<ModuleButton
module="gun"
discriminator={TIER_ROMAN_NUMERALS[modules.gun.tier]}
first
last
rowChild
onClick={() => setGunMenuOpen(true)}
/>
<DropdownMenu.Root open={gunMenuOpen} onOpenChange={setGunMenuOpen}>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup value={`${modules.gun.id}`}>
{[...modules.turret.guns].reverse().map((gun) => {
return (
<DropdownMenu.RadioItem
key={gun.id}
value={`${gun.id}`}
onClick={() => {
const draft = { ...modules };
draft.gun = gun;
draft.shell = draft.gun.shells.at(-1)!;
onChange?.(draft);
setGunMenuOpen(false);
}}
>
<ModuleButton
variant="ghost"
module="gun"
discriminator={TIER_ROMAN_NUMERALS[modules.gun.tier]}
/>{' '}
{gun.name}
</DropdownMenu.RadioItem>
);
})}
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
<ModuleButton
module="engine"
discriminator={TIER_ROMAN_NUMERALS[modules.engine.tier]}
first
last
rowChild
onClick={() => setEngineMenuOpen(true)}
/>
<DropdownMenu.Root open={engineMenuOpen} onOpenChange={setEngineMenuOpen}>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup value={`${modules.engine.id}`}>
{[...tank.engines].reverse().map((engine) => {
return (
<DropdownMenu.RadioItem
key={engine.id}
value={`${engine.id}`}
onClick={() => {
const draft = { ...modules };
draft.engine = engine;
onChange?.(draft);
setEngineMenuOpen(false);
}}
>
<ModuleButton
variant="ghost"
module="engine"
discriminator={TIER_ROMAN_NUMERALS[engine.tier]}
/>{' '}
{engine.name}
</DropdownMenu.RadioItem>
);
})}
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
<ModuleButton
module="chassis"
discriminator={TIER_ROMAN_NUMERALS[modules.track.tier]}
first
last
rowChild
onClick={() => setTrackMenuOpen(true)}
/>
<DropdownMenu.Root open={trackMenuOpen} onOpenChange={setTrackMenuOpen}>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup value={`${modules.track.id}`}>
{[...tank.tracks].reverse().map((track) => {
return (
<DropdownMenu.RadioItem
key={track.id}
value={`${track.id}`}
onClick={() => {
const draft = { ...modules };
draft.track = track;
onChange?.(draft);
setTrackMenuOpen(false);
}}
>
<ModuleButton
variant="ghost"
module="chassis"
discriminator={TIER_ROMAN_NUMERALS[track.tier]}
/>{' '}
{track.name}
</DropdownMenu.RadioItem>
);
})}
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Flex>
);
}
+10 -2
View File
@@ -3,7 +3,7 @@ import { ComponentProps } from 'react';
type PageWrapperProps = FlexProps & {
color?: ComponentProps<typeof Theme>['accentColor'];
size?: number;
size?: number | string;
};
export default function PageWrapper({
@@ -13,7 +13,14 @@ export default function PageWrapper({
...props
}: PageWrapperProps) {
return (
<Theme accentColor={color}>
<Theme
accentColor={color}
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
}}
>
<Flex
direction="column"
gap="4"
@@ -23,6 +30,7 @@ export default function PageWrapper({
margin: 'auto',
padding: 16,
boxSizing: 'border-box',
flex: 1,
...style,
}}
+49
View File
@@ -0,0 +1,49 @@
import { Flex } from '@radix-ui/themes';
import { ProvisionButton } from './ModuleButtons/ProvisionButton';
interface ProvisionsManagerProps {
provisions: number[];
selected: number[];
disabled?: boolean;
onChange?: (provisions: number[]) => void;
}
export function ProvisionsManager({
provisions,
selected,
onChange,
disabled,
}: ProvisionsManagerProps) {
return (
<Flex wrap="wrap">
{provisions.map((provision, index) => {
const isSelected = selected.includes(provision);
return (
<ProvisionButton
key={provision}
first={index === 0}
last={index === provisions.length - 1}
rowChild
disabled={disabled && !isSelected}
provision={provision}
selected={isSelected}
onClick={() => {
if (!onChange) return;
const draft = [...selected];
if (isSelected) {
draft.splice(draft.indexOf(provision), 1);
} else {
draft.push(provision);
}
onChange(draft);
}}
/>
);
})}
</Flex>
);
}
+14 -12
View File
@@ -4,6 +4,7 @@ interface Tool {
description: string;
disabled?: boolean;
href?: string;
pageDescription?: string;
}
export const TOOLS: Tool[] = [
@@ -11,16 +12,29 @@ export const TOOLS: Tool[] = [
id: 'session',
title: 'Session tracker',
description: 'Live session performance tracker',
pageDescription:
'Track your stats in real time as you play World of Tanks Blitz',
},
{
id: 'tankopedia',
title: 'Tankopedia',
description: 'Blitz tank encyclopedia',
pageDescription:
'Statistics, armor, and more for all tanks in World of Tanks Blitz',
},
{
id: 'rating',
title: 'Rating leaderboard',
description: 'Full rating gamemode leaderboard',
pageDescription:
'Live and archived full rating leaderboard for World of Tanks Blitz',
},
{
id: 'compare',
title: 'Compare',
description: 'Compare tank statistics and loadouts',
pageDescription:
'Compare tanks statistics and loadouts in World of Tanks Blitz',
},
{
id: 'discord',
@@ -28,18 +42,6 @@ export const TOOLS: Tool[] = [
description: 'Stats and tools right in Discord',
href: 'https://discord.com/application-directory/1097673957865443370',
},
// {
// id: 'inactive',
// title: 'Inactivity tracker',
// description: 'Find inactive members of a clan',
// disabled: true,
// },
// {
// id: 'profile',
// title: 'Player profile',
// description: "A player's basic non-statistical info",
// disabled: true,
// },
{
id: 'more',
title: 'More coming soon',
-9
View File
@@ -161,15 +161,6 @@ export async function extractModel(data: string, path: string) {
const rotation = new Quaternion();
const scale = new Vector3();
if (
!component['tc.worldTranslation'] ||
!component['tc.worldScale'] ||
!component['tc.localTranslation'] ||
!component['tc.localScale']
) {
console.log(component);
}
new Matrix4()
.multiplyMatrices(
new Matrix4().compose(
+3 -35
View File
@@ -1,10 +1,7 @@
import { degToRad } from 'three/src/math/MathUtils';
import { DuelMember, mutateDuel } from '../../stores/duel';
import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
import { availableProvisions } from './availableProvisions';
import { modelDefinitions } from './modelDefinitions';
import { mutateDuel } from '../../stores/duel';
import { provisionDefinitions } from './provisionDefinitions';
import { tankDefinitions } from './tankDefinitions';
import { tankToDuelMember } from './tankToDuelMember';
export async function assignDuelMember(
type: 'antagonist' | 'protagonist' | 'both',
@@ -12,37 +9,8 @@ export async function assignDuelMember(
) {
const awaitedTankDefinitions = await tankDefinitions;
const awaitedProvisionDefinitions = await provisionDefinitions;
const awaitedModelDefinitions = await modelDefinitions;
const tankModelDefinition = awaitedModelDefinitions[id];
const tank = awaitedTankDefinitions[id];
const turret = tank.turrets.at(-1)!;
const gun = turret.guns.at(-1)!;
const shell = gun.shells[0];
const engine = tank.engines.at(-1)!;
const track = tank.tracks.at(-1)!;
const provisionsList = availableProvisions(
tank,
gun,
awaitedProvisionDefinitions,
);
const member: DuelMember = {
tank,
engine,
turret,
gun,
shell,
track,
equipment: genericDefaultEquipmentMatrix,
pitch: degToRad(tankModelDefinition.turretRotation?.pitch ?? 0),
yaw: 0,
camouflage: true,
consumables: [],
cooldownBooster: 0,
crewMastery: 1,
provisions: provisionsList
.filter((provision) => provision.crew !== undefined)
.map((provision) => provision.id),
};
const member = tankToDuelMember(tank, awaitedProvisionDefinitions);
mutateDuel((draft) => {
draft.assigned = true;
@@ -0,0 +1,14 @@
import { permanentSkills } from '../../app/tools/tankopedia/[id]/components/Characteristics/components/Skills/constants';
import { SkillDefinitions } from './skillDefinitions';
export function createDefaultSkills(skillDefinitions: SkillDefinitions) {
const skills: Record<string, number> = {};
Object.values(skillDefinitions.classes).forEach((tankSkills) => {
tankSkills.forEach((skill) => {
skills[skill] = permanentSkills.includes(skill) ? 7 : 0;
});
});
return skills;
}
+10
View File
@@ -0,0 +1,10 @@
import { asset } from './asset';
import { fetchCdonLz4 } from './fetchCdonLz4';
export interface GameDefinitions {
version: string;
}
export const gameDefinitions = fetchCdonLz4<GameDefinitions>(
asset('definitions/game.cdon.lz4'),
);
+4 -7
View File
@@ -1,16 +1,13 @@
import { useDuel } from '../../stores/duel';
import { EquipmentMatrix } from '../../stores/duel';
import { equipmentDefinitions } from './equipmentDefinitions';
export async function hasEquipment(
id: number,
antagonist = false,
equipmentMatrix = useDuel.getState()[
antagonist ? 'antagonist' : 'protagonist'
]!.equipment,
equipmentPreset: string,
equipmentMatrix: EquipmentMatrix,
) {
const awaitedEquipmentDefinitions = await equipmentDefinitions;
const member = useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!;
const presetRows = awaitedEquipmentDefinitions.presets[member.tank.equipment];
const presetRows = awaitedEquipmentDefinitions.presets[equipmentPreset];
return presetRows.some((equipmentRow, rowIndex) => {
return equipmentRow.some((equipment, columnIndex) => {
const choice = equipmentMatrix[rowIndex][columnIndex];
@@ -17,3 +17,5 @@ export interface ProvisionDefinitions {
export const provisionDefinitions = fetchCdonLz4<ProvisionDefinitions>(
asset('definitions/provisions.cdon.lz4'),
);
provisionDefinitions.then(console.log);
+484
View File
@@ -0,0 +1,484 @@
import { EquipmentMatrix } from '../../stores/duel';
import { isExplosive } from '../blitz/isExplosive';
import { resolveNearPenetration } from '../blitz/resolveNearPenetration';
import { coefficient } from './coefficient';
import { degressiveStat } from './degressiveStat';
import { EquipmentDefinitions } from './equipmentDefinitions';
import { ModelDefinitions } from './modelDefinitions';
import { normalizeBoundingBox } from './normalizeBoundingBox';
import { progressiveStat } from './progressiveStat';
import { ProvisionDefinitions } from './provisionDefinitions';
import { resolveDpm } from './resolveDpm';
import { sum } from './sum';
import {
EngineDefinition,
GunDefinition,
ShellDefinition,
TankDefinition,
TrackDefinition,
TurretDefinition,
} from './tankDefinitions';
import { unionBoundingBox } from './unionBoundingBox';
export function tankCharacteristics(
{
tank,
turret,
gun,
shell,
consumables,
equipmentMatrix,
crewMastery,
provisions,
engine,
crewSkills,
camouflage,
track,
stockEngine,
stockTrack,
stockTurret,
stockGun,
applyReactive,
applyDynamicArmor,
applySpallLiner,
}: {
tank: TankDefinition;
turret: TurretDefinition;
gun: GunDefinition;
shell: ShellDefinition;
track: TrackDefinition;
engine: EngineDefinition;
stockEngine: EngineDefinition;
stockTrack: TrackDefinition;
stockTurret: TurretDefinition;
stockGun: GunDefinition;
consumables: number[];
equipmentMatrix: EquipmentMatrix;
crewMastery: number;
provisions: number[];
crewSkills: Record<string, number>;
camouflage: boolean;
applyReactive: boolean;
applyDynamicArmor: boolean;
applySpallLiner: boolean;
},
{
modelDefinitions,
equipmentDefinitions,
provisionDefinitions,
}: {
modelDefinitions: ModelDefinitions;
equipmentDefinitions: EquipmentDefinitions;
provisionDefinitions: ProvisionDefinitions;
},
) {
const presetRows = equipmentDefinitions.presets[tank.equipment];
const tankModelDefinition = modelDefinitions[tank.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
function equipment(id: number) {
return presetRows.some((equipmentRow, rowIndex) => {
return equipmentRow.some((equipment, columnIndex) => {
const choice = equipmentMatrix[rowIndex][columnIndex];
if (choice === 0) return false;
const equipped = equipment[choice === -1 ? 0 : 1];
return equipped === id;
});
});
}
function consumable(id: number) {
return consumables.includes(id);
}
function provision(id: number) {
return provisions.includes(id);
}
const hasRammer = equipment(100);
const hasImprovedVentilation = equipment(102);
const hasCalibratedShells = equipment(103);
const hasEnhancedGunLayingDrive = equipment(104);
const hasSupercharger = equipment(107);
const hasVerticalStabilizer = equipment(105);
const hasRefinedGun = equipment(106);
const hasImprovedAssembly = equipment(111);
const hasImprovedOptics = equipment(114);
const hasImprovedControl = equipment(116);
const hasEngineAccelerator = equipment(117);
const hasCamouflageNet = equipment(115);
const hasImprovedVerticalStabilizer = equipment(122);
const hasImprovedSuspension = equipment(123);
const hasTungsten = consumable(45);
const hasAdrenaline = consumable(18);
const hasShellReloadBoost = consumable(29);
const hasEnginePowerBoost = consumable(14);
const hasImprovedEnginePowerBoost = consumable(27);
const hasReticleCalibration = consumable(28);
const hasStandardFuel = provision(18);
const hasImprovedFuel = provision(19);
const hasProtectiveKit = provision(22);
const hasSandbagArmor = provision(28);
const hasEnhancedSandbagArmor = provision(29);
const hasImprovedGunPowder = provision(46);
const hasGearOil = provision(44);
const hasImprovedGearOil = provision(45);
const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
const provisionCrewBonus =
provisions.reduce(
(total, provision) =>
provisionDefinitions[provision].crew
? total + provisionDefinitions[provision].crew! / 100
: total,
0,
) + (hasImprovedVentilation ? 0.05 : 0);
const commanderMastery = crewMastery + provisionCrewBonus;
const crewMemberMastery = commanderMastery * 1.1;
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
const damageCoefficient =
coefficient([hasTungsten, 0.15]) *
coefficient(
[applyReactive, -0.2],
[applyDynamicArmor, -0.1],
[applySpallLiner && shell.type === 'he', -0.2],
);
const reloadCoefficient =
(coefficient([hasRammer, -0.07]) *
coefficient([true, degressiveStat(crewMemberMastery)])) /
coefficient([hasAdrenaline && gun.type === 'regular', 0.2]);
const penetrationCoefficient = coefficient([
hasCalibratedShells,
isExplosive(shell.type) ? 0.1 : 0.05,
]);
const healthCoefficient = coefficient(
[hasSandbagArmor, 0.03],
[hasEnhancedSandbagArmor, 0.06],
[hasImprovedAssembly, 0.06],
);
const shellVelocityCoefficient = coefficient(
[hasSupercharger, 0.3],
[hasImprovedGunPowder, 0.3],
);
const penetrationLossOverDistanceCoefficient = coefficient([
hasSupercharger,
-0.5,
]);
const aimTimeCoefficient =
coefficient(
[hasEnhancedGunLayingDrive, -0.1],
[hasReticleCalibration, -0.4],
) * coefficient([true, degressiveStat(crewMemberMastery)]);
const dispersionStillCoefficient =
coefficient([hasRefinedGun, -0.1], [hasReticleCalibration, -0.4]) *
coefficient([true, degressiveStat(crewMemberMastery)]);
const dispersionDamagedCoefficient = coefficient([
hasVerticalStabilizer,
-0.15,
]);
const dispersionHullTraverseCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_turn / 100],
);
const dispersionAfterShotCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.soft_recoil / 100],
);
const dispersionMovingCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_driving / 100],
);
const dispersionTurretTraverseCoefficient = coefficient(
[hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_turret * (2 / 100)],
);
const enginePowerCoefficient = coefficient(
[
hasEngineAccelerator &&
(tank.class === 'lightTank' || tank.class === 'mediumTank'),
0.05,
],
[
hasEngineAccelerator &&
(tank.class === 'heavyTank' || tank.class === 'AT-SPG'),
0.07,
],
[hasEnginePowerBoost, 0.2],
[hasImprovedEnginePowerBoost, 0.4],
[hasStandardFuel, 0.03],
[hasImprovedFuel, 0.1],
[hasGearOil, 0.03],
[hasImprovedGearOil, 0.06],
);
const turretTraverseCoefficient =
coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) *
coefficient([true, progressiveStat(crewMemberMastery)]);
const hullTraverseCoefficient =
coefficient(
[hasImprovedControl, 0.1],
[hasImprovedEnginePowerBoost, 0.05],
[true, crewSkills.virtuoso / 100],
) * coefficient([true, progressiveStat(crewMemberMastery)]);
const resistanceCoefficient = coefficient([hasImprovedSuspension, -0.25]);
const viewRangeCoefficient =
coefficient([
hasImprovedOptics,
tank.class === 'AT-SPG' ? 0.05 : tank.class === 'heavyTank' ? 0.07 : 0.1,
]) * coefficient([true, progressiveStat(commanderMastery)]);
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
const speedForwardsSum = sum(
[hasImprovedEnginePowerBoost, 5],
[hasGearOil, 2],
[hasImprovedGearOil, 4],
);
const speedBackwardsSum = sum(
[hasImprovedEnginePowerBoost, 10],
[hasGearOil, 2],
[hasImprovedGearOil, 4],
);
const camouflageSumMoving = sum(
[
hasCamouflageNet,
tank.class === 'heavyTank' ? 0.03 : tank.class === 'AT-SPG' ? 0.07 : 0.05,
],
[
camouflage,
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
],
);
const camouflageSumStill = sum(
[
hasCamouflageNet,
2 *
(tank.class === 'heavyTank'
? 0.03
: tank.class === 'AT-SPG'
? 0.07
: 0.05),
],
[
camouflage,
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
],
[true, crewSkills.camouflage * (3 / 100)],
);
const size = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.boundingBox,
turretModelDefinition.boundingBox,
),
);
const weightKg =
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
const weightTons = weightKg / 1000;
const stockWeight =
tank.weight +
stockEngine.weight +
stockTrack.weight +
stockTurret.weight +
stockGun.weight;
const resolvedEnginePower = engine.power * enginePowerCoefficient;
const dpm = resolveDpm(
gun,
shell,
damageCoefficient,
reloadCoefficient,
intraClipCoefficient,
);
const dpmMaximum =
gun.type === 'autoReloader'
? gun.reload.at(-1)! < gun.reload.at(-2)!
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
60
: ((damageCoefficient * shell.damage.armor) /
(gun.reload[0] * reloadCoefficient)) *
60
: undefined;
const dpmEffective =
gun.type === 'autoReloader'
? gun.reload.at(-1)! < gun.reload.at(-2)!
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
(60 -
(gun.reload.slice(0, -1).length - 1) *
gun.intraClip *
intraClipCoefficient) +
damageCoefficient *
shell.damage.armor *
gun.reload.slice(0, -1).length
: ((damageCoefficient * shell.damage.armor) /
(gun.reload[0] * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
(60 -
(gun.reload.slice(1).length - 1) *
gun.intraClip *
intraClipCoefficient) +
damageCoefficient * shell.damage.armor * gun.reload.slice(1).length
: undefined;
const shells = gun.type === 'regular' ? 1 : gun.count;
const mostOptimalShellIndex =
gun.type === 'autoReloader'
? gun.reload.at(-1)! < gun.reload.at(-2)!
? gun.reload.length
: 1
: undefined;
const shellReloads =
gun.type === 'autoReloader'
? gun.reload.map((reload) => reload * reloadCoefficient)
: undefined;
const shellReload =
gun.type === 'autoReloader' ? undefined : gun.reload * reloadCoefficient;
const intraClip =
gun.type === 'regular' ? undefined : gun.intraClip * intraClipCoefficient;
const caliber = shell.caliber;
const penetration =
resolveNearPenetration(shell.penetration) * penetrationCoefficient;
const damage = shell.damage.armor * damageCoefficient;
const moduleDamage = shell.damage.module * damageCoefficient;
const explosionRadius = shell.explosionRadius;
const shellVelocity = shell.speed * shellVelocityCoefficient;
const aimTime = gun.aimTime * aimTimeCoefficient;
const dispersion = gun.dispersion.base * dispersionStillCoefficient;
const dispersionMoving = track.dispersion.move * dispersionMovingCoefficient;
const dispersionHullTraversing =
track.dispersion.traverse * dispersionHullTraverseCoefficient;
const dispersionTurretTraversing =
gun.dispersion.traverse * dispersionTurretTraverseCoefficient;
const dispersionShooting =
gun.dispersion.shot * dispersionAfterShotCoefficient;
const dispersionGunDamaged =
gun.dispersion.damaged * dispersionDamagedCoefficient;
const gunDepression =
gunModelDefinition.pitch.max +
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0);
const gunElevation =
-gunModelDefinition.pitch.min -
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0);
const gunFrontalDepression = gunModelDefinition.pitch.front?.max;
const gunFrontalElevation = gunModelDefinition.pitch.front
? -gunModelDefinition.pitch.front.min
: undefined;
const gunRearDepression = gunModelDefinition.pitch.back?.max;
const gunRearElevation = gunModelDefinition.pitch.back
? -gunModelDefinition.pitch.back.min
: undefined;
const azimuthLeft = turretModelDefinition.yaw
? -turretModelDefinition.yaw.min
: undefined;
const azimuthRight = turretModelDefinition.yaw?.max;
const speedForwards = tank.speed.forwards + speedForwardsSum;
const speedBackwards = tank.speed.backwards + speedBackwardsSum;
const enginePower = resolvedEnginePower;
const powerToWeightRatioHardTerrain =
resolvedEnginePower /
weightTons /
(track.resistance.hard * resistanceCoefficient);
const powerToWeightRatioMediumTerrain =
resolvedEnginePower /
weightTons /
(track.resistance.medium * resistanceCoefficient);
const powerToWeightRatioSoftTerrain =
resolvedEnginePower /
weightTons /
(track.resistance.soft * resistanceCoefficient);
const weight = weightTons;
const turretTraverseSpeed = turret.traverseSpeed * turretTraverseCoefficient;
const hullTraverseHardTerrain =
(resolvedEnginePower / stockEngine.power) *
track.traverseSpeed *
hullTraverseCoefficient *
(track.resistance.hard / (track.resistance.hard * resistanceCoefficient)) *
(stockWeight / weightKg);
const hullTraverseMediumTerrain =
(resolvedEnginePower / stockEngine.power) *
track.traverseSpeed *
hullTraverseCoefficient *
(track.resistance.hard /
(track.resistance.medium * resistanceCoefficient)) *
(stockWeight / weightKg);
const hullTraverseSoftTerrain =
(resolvedEnginePower / stockEngine.power) *
track.traverseSpeed *
hullTraverseCoefficient *
(track.resistance.hard / (track.resistance.soft * resistanceCoefficient)) *
(stockWeight / weightKg);
const health = (tank.health + turret.health) * healthCoefficient;
const fireChance = engine.fireChance * fireChanceCoefficient;
const viewRange = turret.viewRange * viewRangeCoefficient;
const camouflageStill = tank.camouflage.still + camouflageSumStill;
const camouflageMoving = tank.camouflage.moving + camouflageSumMoving;
const camouflageShootingStill =
tank.camouflage.still * gun.camouflageLoss + camouflageSumStill;
const camouflageShootingMoving =
tank.camouflage.moving * gun.camouflageLoss + camouflageSumMoving;
const camouflageCaughtOnFire =
tank.camouflage.onFire * tank.camouflage.still + camouflageSumStill;
const width = size[2];
const height = size[0];
const length = size[1];
const volume = width * height * length;
return {
dpm,
dpmMaximum,
dpmEffective,
shells,
mostOptimalShellIndex,
shellReloads,
shellReload,
intraClip,
caliber,
penetration,
damage,
moduleDamage,
explosionRadius,
shellVelocity,
aimTime,
dispersion,
dispersionMoving,
dispersionHullTraversing,
dispersionTurretTraversing,
dispersionShooting,
dispersionGunDamaged,
gunDepression,
gunElevation,
gunFrontalDepression,
gunFrontalElevation,
gunRearDepression,
gunRearElevation,
azimuthLeft,
azimuthRight,
speedForwards,
speedBackwards,
enginePower,
powerToWeightRatioHardTerrain,
powerToWeightRatioMediumTerrain,
powerToWeightRatioSoftTerrain,
weight,
turretTraverseSpeed,
hullTraverseHardTerrain,
hullTraverseMediumTerrain,
hullTraverseSoftTerrain,
health,
fireChance,
viewRange,
camouflageStill,
camouflageMoving,
camouflageShootingStill,
camouflageShootingMoving,
camouflageCaughtOnFire,
width,
height,
length,
volume,
};
}
@@ -0,0 +1,15 @@
import { uniqueId } from 'lodash';
import { CompareMember } from '../../stores/compare';
import { ProvisionDefinitions } from './provisionDefinitions';
import { TankDefinition } from './tankDefinitions';
import { tankToDuelMember } from './tankToDuelMember';
export function tankToCompareMember(
tank: TankDefinition,
provisionDefinitions: ProvisionDefinitions,
) {
return {
...tankToDuelMember(tank, provisionDefinitions),
key: uniqueId(),
} satisfies CompareMember;
}
+45
View File
@@ -0,0 +1,45 @@
import { DuelMember } from '../../stores/duel';
import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
import { availableProvisions } from './availableProvisions';
import { ProvisionDefinitions } from './provisionDefinitions';
import { TankDefinition } from './tankDefinitions';
const PROVISION_PREFERENCES = [
19, // improved fuel
18, // standard fuel
22, // protective kit
];
export function tankToDuelMember(
tank: TankDefinition,
provisionDefinitions: ProvisionDefinitions,
) {
const turret = tank.turrets.at(-1)!;
const gun = turret.guns.at(-1)!;
const provisionsList = availableProvisions(tank, gun, provisionDefinitions);
return {
camouflage: true,
consumables: [],
equipmentMatrix: genericDefaultEquipmentMatrix,
cooldownBooster: 0,
crewMastery: 1,
provisions: provisionsList
.sort(
(a, b) =>
PROVISION_PREFERENCES.indexOf(a.id) -
PROVISION_PREFERENCES.indexOf(b.id),
)
.sort((a, b) => (b.crew ?? 0) - (a.crew ?? 0))
.slice(0, tank.provisions)
.map(({ id }) => id),
engine: tank.engines.at(-1)!,
gun,
shell: tank.turrets.at(-1)!.guns.at(-1)!.shells[0],
tank,
track: tank.tracks.at(-1)!,
yaw: 0,
pitch: 0,
turret,
} satisfies DuelMember;
}
+6 -5
View File
@@ -6,16 +6,17 @@ export type PSAData = {
}[];
commands?: string[];
} & (
| {
type?: 'embed';
title: string;
description: string;
image?: string;
}
| {
type: 'image';
image: string;
title?: string;
}
| {
type: 'embed';
title: string;
description: string;
}
);
export interface PSA {
@@ -63,10 +63,14 @@ export default async function handleChatInputCommand(
(psa.data.commands === undefined ||
psa.data.commands.includes(interaction.commandName))
) {
if (psa.data.type === 'embed') {
if (psa.data.type === undefined || psa.data.type === 'embed') {
if (!reply.embeds) reply.embeds = [];
reply.embeds.push(embedWarning(psa.data.title, psa.data.description));
} else {
const embed = embedWarning(psa.data.title, psa.data.description);
if (psa.data.image) embed.setImage(psa.data.image);
reply.embeds.push(embed);
} else if (psa.data.type === 'image') {
if (!reply.files) reply.files = [];
reply.files.push(new AttachmentBuilder(psa.data.image));
reply.content = reply.content ?? psa.data.title;
@@ -101,12 +105,14 @@ export default async function handleChatInputCommand(
ephemeral: true,
};
if (psa.data.type === 'embed') {
if (psa.data.type === undefined || psa.data.type === 'embed') {
if (!followUp.embeds) followUp.embeds = [];
followUp.embeds.push(
embedWarning(psa.data.title, psa.data.description),
);
} else {
const embed = embedWarning(psa.data.title, psa.data.description);
if (psa.data.image) embed.setImage(psa.data.image);
followUp.embeds.push(embed);
} else if (psa.data.type === 'image') {
if (!followUp.files) followUp.files = [];
followUp.files.push(new AttachmentBuilder(psa.data.image));
followUp.content = followUp.content ?? psa.data.title;
+1 -4
View File
@@ -1,6 +1,3 @@
import { useDuel } from '../stores/duel';
export function useConsumable(id: number) {
const consumables = useDuel((state) => state.protagonist!.consumables);
export function useConsumable(id: number, consumables: number[]) {
return consumables.includes(id);
}
+2 -1
View File
@@ -10,7 +10,8 @@ export function useEquipment(id: number, antagonist = false) {
const equipmentRows =
awaitedEquipmentDefinitions.presets[member.tank.equipment];
const equipmentMatrix = useDuel(
(state) => state[antagonist ? 'antagonist' : 'protagonist']!.equipment,
(state) =>
state[antagonist ? 'antagonist' : 'protagonist']!.equipmentMatrix,
);
const value = useMemo(
() =>
-1
View File
@@ -3,6 +3,5 @@ import { useAwait } from './useAwait';
export function useEquipmentPreset(preset: string) {
const awaitedEquipmentDefinitions = useAwait(equipmentDefinitions);
console.log(awaitedEquipmentDefinitions);
return awaitedEquipmentDefinitions.presets[preset];
}
-9
View File
@@ -1,9 +0,0 @@
import { use } from 'react';
import { provisionDefinitions } from '../core/blitzkrieg/provisionDefinitions';
import { useDuel } from '../stores/duel';
export function useProvisions() {
const provisions = useDuel((state) => state.protagonist!.provisions);
const awaitedProvisionDefinitions = use(provisionDefinitions);
return provisions.map((id) => awaitedProvisionDefinitions[id]);
}
+38
View File
@@ -0,0 +1,38 @@
import { ComponentProps } from 'react';
export function BlitzkriegButtonWatermark(props: ComponentProps<'svg'>) {
return (
<svg
color="#ffffff"
opacity={1 / 16}
width="426"
height="426"
viewBox="0 0 426 426"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clip-path="url(#clip0_478_230)">
<path
d="M253.802 170.741C259.515 174.783 265.336 177.243 271.264 178.12C277.14 178.96 282.553 177.629 287.502 174.127C290.784 171.805 292.637 169.134 293.059 166.116C293.482 163.097 292.968 160.374 291.517 157.947L269.391 142.292C265.752 141.192 261.826 140.81 257.612 141.146C253.449 141.445 249.701 142.774 246.367 145.133C241.47 148.598 239.667 152.446 240.958 156.677C242.249 160.908 245.725 165.027 251.387 169.032L253.802 170.741Z"
fill="currentColor"
/>
<path
d="M186.616 242.201C182.454 242.5 178.58 242.154 174.992 241.164L152.555 225.289C151.104 222.788 150.59 220.065 151.012 217.12C151.487 214.138 153.418 211.449 156.804 209.053C161.701 205.588 167.036 204.276 172.808 205.116C178.581 205.883 184.323 208.287 190.036 212.33L192.374 213.983C198.087 218.026 201.667 222.217 203.114 226.559C204.561 230.827 202.993 234.582 198.408 237.826C194.709 240.444 190.778 241.902 186.616 242.201Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M235.625 52.6262L416.628 180.603C422.876 185.02 426 190.81 426 196.6V229C426 234.79 422.876 240.58 416.628 244.997L235.625 372.974C223.13 381.809 202.87 381.809 190.375 372.974L9.37173 244.997C3.1239 240.58 -3.95076e-06 234.79 0 229V196.6C0 190.81 3.12391 185.02 9.37173 180.603L190.375 52.6262C202.87 43.7913 223.13 43.7913 235.625 52.6262ZM323.986 189.104C322.987 194 319.935 198.254 314.829 201.866L191.273 289.287L208.958 301.8L336.968 211.228C349.42 202.418 355.761 192.897 355.991 182.665C356.274 172.469 349.715 162.631 336.315 153.15L269.393 105.8L243.995 123.77L253.265 130.551L253.031 130.716C247.831 129.912 242.108 130.25 235.863 131.729C229.617 133.208 223.994 135.717 218.992 139.255C209.302 146.112 205.228 153.919 206.769 162.677C208.311 171.361 214.535 179.562 225.442 187.279L227.935 189.043C238.842 196.76 250.511 201.072 262.941 201.978C275.371 202.811 286.223 199.947 295.497 193.386C301.488 189.147 305.321 185.039 306.994 181.062C308.667 177.085 309.04 173.183 308.112 169.357L314.812 174.097C321.979 179.168 325.037 184.171 323.986 189.104ZM60 196.406L174.835 277.657L200.155 259.742L191.897 253.788L192.053 253.678C197.201 254.371 202.638 253.942 208.364 252.39C214.089 250.838 219.531 248.237 224.688 244.588C234.066 237.953 238.089 229.999 236.756 220.725C235.424 211.452 229.33 202.975 218.475 195.295L215.982 193.531C205.179 185.887 193.458 181.686 180.82 180.927C168.129 180.131 157.095 183.05 147.717 189.686C142.559 193.335 139.066 197.166 137.236 201.18C135.355 205.157 134.8 209.188 135.572 213.272L85.8668 178.104L60 196.406Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_478_230">
<rect width="426" height="426" fill="currentColor" />
</clipPath>
</defs>
</svg>
);
}
-1
View File
@@ -3,7 +3,6 @@ import { ComponentProps } from 'react';
export function PatreonIcon(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 436 476" {...props}>
<title>Patreon logo</title>
<path
fill="currentColor"
data-fill="1"
+2
View File
@@ -3,6 +3,7 @@ import {
amberDarkA,
blueDark,
brownDark,
crimsonDark,
greenDark,
mauveDark,
orangeDark,
@@ -36,6 +37,7 @@ export const theme = {
...createColors(brownDark, 'brown'),
...createColors(mauveDark, 'mauve'),
...createColors(sandDark, 'sand'),
...createColors(crimsonDark, 'crimson'),
},
borderStyles: {
+52
View File
@@ -0,0 +1,52 @@
import { produce } from 'immer';
import { merge } from 'lodash';
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import { DuelMember } from './duel';
export interface CompareMember extends DuelMember {
key: string;
}
export interface CompareTemporary {
crewSkills: Record<string, number>;
members: CompareMember[];
sorting?: {
direction: 'ascending' | 'descending';
by: string;
};
}
export type DeltaMode = 'none' | 'percentage' | 'absolute';
export interface ComparePersistent {
deltaMode: DeltaMode;
}
export const useCompareTemporary = create<CompareTemporary>()(() => ({
crewSkills: {},
members: [],
}));
export const useComparePersistent = create<ComparePersistent>()(
persist(
subscribeWithSelector<ComparePersistent>(() => ({
deltaMode: 'none',
})),
{
name: 'compare',
merge: (a, b) => merge(b, a),
},
),
);
export function mutateCompareTemporary(
recipe: (draft: CompareTemporary) => void,
) {
useCompareTemporary.setState(produce(recipe));
}
export function mutateComparePersistent(
recipe: (draft: ComparePersistent) => void,
) {
useComparePersistent.setState(produce(recipe));
}
+1 -1
View File
@@ -29,7 +29,7 @@ export interface DuelMember {
gun: GunDefinition;
shell: ShellDefinition;
track: TrackDefinition;
equipment: EquipmentMatrix;
equipmentMatrix: EquipmentMatrix;
pitch: number;
yaw: number;
consumables: number[];
+509
View File
@@ -0,0 +1,509 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
<url>
<loc>https://blitz-krieg.vercel.app/</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>1.00</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/session</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/rating</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/24321</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/16897</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/13825</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/19969</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/15617</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/7169</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/6145</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/25857</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/22273</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/14337</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/13569</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/24577</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/14609</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/12305</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/23313</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/58641</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/9489</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/6929</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/21777</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/19217</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/12049</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/20257</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/21793</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/14113</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/15905</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/22817</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/14881</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/10785</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/24609</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/18977</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/13857</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/13089</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/28705</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/10289</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/4145</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/5681</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/11825</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/5425</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/11057</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/8497</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/6449</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/19537</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/7249</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/6225</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/19281</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/15697</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/18001</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/9297</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/17745</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/3649</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/8513</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/6209</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/4417</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/13889</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/3681</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/6753</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/3937</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/14977</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/5505</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/385</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/10113</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/13185</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/7297</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/4481</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/17793</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/12161</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/10369</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/23809</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/7937</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/11521</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/10753</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/26113</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/25089</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/12545</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/12033</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/8193</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/14865</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/10257</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/20753</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/9745</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/7441</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/27409</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/11281</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/16401</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/7953</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/25361</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/20001</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/15393</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/8993</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/23073</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/9505</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/27425</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/22305</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/8737</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/2593</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://blitz-krieg.vercel.app/tools/tankopedia/10033</loc>
<lastmod>2024-04-04T03:39:50+00:00</lastmod>
<priority>0.64</priority>
</url>
</urlset>
+2 -27
View File
@@ -339,11 +339,6 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
"@corex/deepmerge@^4.0.43":
version "4.0.43"
resolved "https://registry.yarnpkg.com/@corex/deepmerge/-/deepmerge-4.0.43.tgz#9bd42559ebb41cc5a7fb7cfeea5f231c20977dca"
integrity sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==
"@discordjs/builders@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.7.0.tgz#e2478c7e55b0f4c40837edb8f102bce977323a37"
@@ -790,11 +785,6 @@
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.0.tgz#43d92ebb53bc0ae43dcc64fb4d418f8f17d7a341"
integrity sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==
"@next/env@^13.4.3":
version "13.5.6"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.6.tgz#c1148e2e1aa166614f05161ee8f77ded467062bc"
integrity sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==
"@next/[email protected]":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz#70a57c87ab1ae5aa963a3ba0f4e59e18f4ecea39"
@@ -3470,7 +3460,7 @@ fast-deep-equal@^2.0.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==
[email protected], fast-glob@^3.2.12, fast-glob@^3.3.2:
[email protected], fast-glob@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
@@ -4295,7 +4285,7 @@ minimatch@^9.0.0, minimatch@^9.0.1:
dependencies:
brace-expansion "^2.0.1"
[email protected], minimist@^1.2.5, minimist@^1.2.8:
[email protected], minimist@^1.2.5:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
@@ -4371,16 +4361,6 @@ [email protected]:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
next-sitemap@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/next-sitemap/-/next-sitemap-4.2.3.tgz#5db3f650351a51e84b9fd6b58c5af2f9257b5058"
integrity sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==
dependencies:
"@corex/deepmerge" "^4.0.43"
"@next/env" "^13.4.3"
fast-glob "^3.2.12"
minimist "^1.2.8"
next@^14.1.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69"
@@ -4894,11 +4874,6 @@ [email protected]:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-spinners@^0.13.8:
version "0.13.8"
resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.8.tgz#5262571be0f745d86bbd49a1e6b49f9f9cb19acc"
integrity sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"