mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f119b06dd8 | ||
|
|
e23a440508 | ||
|
|
6a9fa08500 | ||
|
|
8e86e81e91 | ||
|
|
8b359e220a | ||
|
|
690bc5dbb5 | ||
|
|
d6e96e17ad | ||
|
|
ab26b346e6 | ||
|
|
f02db6ef0b | ||
|
|
55ba2525d1 | ||
|
|
ea07d2646d | ||
|
|
ca111510f6 | ||
|
|
519558dd28 | ||
|
|
c4d1e305f6 | ||
|
|
59772afb04 | ||
|
|
8c9dcfa551 | ||
|
|
8bbf03a128 | ||
|
|
519fba84dc | ||
|
|
e7e74f706e | ||
|
|
12e2793c92 | ||
|
|
69040dc453 | ||
|
|
511151e101 | ||
|
|
7fc993c2eb | ||
|
|
030ca744e9 | ||
|
|
74756ab7bf | ||
|
|
e9a61d579d | ||
|
|
7c3cad2c7d | ||
|
|
5f39ba55f6 | ||
|
|
2bfd8286a4 | ||
|
|
f3421b6f70 | ||
|
|
abbbf11b3d | ||
|
|
797f5a90d2 | ||
|
|
818d13fade | ||
|
|
129d7c9688 | ||
|
|
28407b0f05 | ||
|
|
8a9d72d0e6 | ||
|
|
f98a4c7caa | ||
|
|
92b329a0fb | ||
|
|
f3d32a4e63 | ||
|
|
0e73d79c9c | ||
|
|
526b5785b7 | ||
|
|
02129feb25 | ||
|
|
0a38c164a4 | ||
|
|
2e0d52e30e | ||
|
|
6d48f9fe7f | ||
|
|
aba52c9968 | ||
|
|
b96b9fd574 | ||
|
|
fe4ee8b611 | ||
|
|
94558393d9 | ||
|
|
db1e4af20c | ||
|
|
be20173acd | ||
|
|
d90a70ed2a | ||
|
|
85d699dbfc | ||
|
|
5d0fe8726b | ||
|
|
850308b2ac | ||
|
|
88a85ef152 | ||
|
|
8160935081 | ||
|
|
c057e295d2 | ||
|
|
28d0c26268 | ||
|
|
52cb73a575 | ||
|
|
9f244452da | ||
|
|
d46d948c66 | ||
|
|
11b99ef211 | ||
|
|
85d350a912 | ||
|
|
145bc84fb1 | ||
|
|
b7486022eb | ||
|
|
eadb4bb7b1 | ||
|
|
79723157d2 | ||
|
|
4214ff0de5 | ||
|
|
f516c9275e | ||
|
|
f0b905593c | ||
|
|
11440bc654 | ||
|
|
2dab957a05 | ||
|
|
60f3951e7a | ||
|
|
4f03788f15 | ||
|
|
17f4435a30 | ||
|
|
04e42c929c | ||
|
|
4ba45b8fd8 | ||
|
|
0ece1f4b18 | ||
|
|
49d3e1d1c2 | ||
|
|
184ca522dd | ||
|
|
9f07d910b8 | ||
|
|
ac3f2ea93a | ||
|
|
a77ff1a638 | ||
|
|
96d802a09f | ||
|
|
1babd66649 | ||
|
|
2f2996a40c | ||
|
|
9d1e059ea5 | ||
|
|
48603b918f | ||
|
|
c1253e8121 | ||
|
|
ecc029402e | ||
|
|
1b88773e28 | ||
|
|
cf419a9ebb | ||
|
|
8565d75747 | ||
|
|
186ea8b458 | ||
|
|
4292081df6 | ||
|
|
4b54b61c35 | ||
|
|
5d098c46fa | ||
|
|
7575f4581c | ||
|
|
4669fa6ec5 | ||
|
|
49294d1906 | ||
|
|
3eee946978 | ||
|
|
7fff1c9737 | ||
|
|
5b4c3119dd | ||
|
|
0a41ee15ed | ||
|
|
ad57d284bb | ||
|
|
b929e9449d | ||
|
|
948ea0bb8d | ||
|
|
c1b1b8d304 | ||
|
|
3bc5a6d740 | ||
|
|
d25da724e1 | ||
|
|
753804b8cf | ||
|
|
096122d43c | ||
|
|
3448427de0 | ||
|
|
d8bbd02715 | ||
|
|
f696c5f3c9 | ||
|
|
bd412cdaa2 | ||
|
|
913411275b | ||
|
|
17ff8e0819 | ||
|
|
b5fdc4d5cf | ||
|
|
59c5d95aff | ||
|
|
16babbad90 | ||
|
|
a9dc9e7ebd | ||
|
|
52e8df560e | ||
|
|
87259ad515 | ||
|
|
c96655a6c0 | ||
|
|
43894567b8 | ||
|
|
76a952a8e6 | ||
|
|
e2efe428a2 | ||
|
|
887cec2dd4 | ||
|
|
04ad48bd33 | ||
|
|
3613f5d13b | ||
|
|
3362e3b091 | ||
|
|
3322fa97d4 | ||
|
|
1dc7748a04 | ||
|
|
6176b3b0d8 | ||
|
|
81c2af08c6 | ||
|
|
2d83519f93 | ||
|
|
869c1f0def | ||
|
|
7a64fb8176 | ||
|
|
9256244286 | ||
|
|
85e54e4a07 | ||
|
|
8075dc8d9f | ||
|
|
57dced109a | ||
|
|
dd19d5b979 | ||
|
|
68d3fe6805 | ||
|
|
70f7698466 | ||
|
|
26a5fdea0e | ||
|
|
367bd1386b | ||
|
|
fac773a8f8 | ||
|
|
6bd8b7d42f | ||
|
|
7093662130 |
No files matched your search
@@ -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
@@ -18,5 +18,4 @@ temp/
|
||||
/.VSCodeCounter
|
||||
/.yarn
|
||||
depots
|
||||
public/sitemap*.xml
|
||||
public/robots.txt
|
||||
public/sitemap*
|
||||
Vendored
+4
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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 |
@@ -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
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`,
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+15
-43
@@ -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>
|
||||
);
|
||||
}
|
||||
+11
-30
@@ -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>
|
||||
);
|
||||
}
|
||||
+11
-47
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
];
|
||||
|
||||
+24
-9
@@ -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,
|
||||
),
|
||||
];
|
||||
|
||||
+20
-6
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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,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);
|
||||
}
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -3,6 +3,5 @@ import { useAwait } from './useAwait';
|
||||
|
||||
export function useEquipmentPreset(preset: string) {
|
||||
const awaitedEquipmentDefinitions = useAwait(equipmentDefinitions);
|
||||
console.log(awaitedEquipmentDefinitions);
|
||||
return awaitedEquipmentDefinitions.presets[preset];
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export interface DuelMember {
|
||||
gun: GunDefinition;
|
||||
shell: ShellDefinition;
|
||||
track: TrackDefinition;
|
||||
equipment: EquipmentMatrix;
|
||||
equipmentMatrix: EquipmentMatrix;
|
||||
pitch: number;
|
||||
yaw: number;
|
||||
consumables: number[];
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user