mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4062e56b4c | ||
|
|
0885e7e68a | ||
|
|
3cfa837682 | ||
|
|
1982604878 | ||
|
|
9ef6eea4a9 | ||
|
|
78e6b2696c | ||
|
|
40b6f30092 | ||
|
|
de2d9a1a3e | ||
|
|
b7ad12c698 | ||
|
|
886c2aa1d1 | ||
|
|
9818c9868a | ||
|
|
31055f1d66 | ||
|
|
6cef753053 | ||
|
|
5b889f8483 | ||
|
|
d0d905ccfb | ||
|
|
9a91a8bb13 | ||
|
|
c12cc9b8c5 | ||
|
|
fdbfd0bc10 | ||
|
|
286b178218 | ||
|
|
54e012f736 | ||
|
|
cc6de50de6 | ||
|
|
194e5fceca | ||
|
|
879a5d534c | ||
|
|
29aaf60da0 | ||
|
|
3c8f8b49e4 | ||
|
|
0b88f71f26 | ||
|
|
65e239a22b | ||
|
|
7727e64931 | ||
|
|
7b0465322d | ||
|
|
099e5eb228 | ||
|
|
eaff26c8d9 | ||
|
|
d79129df91 | ||
|
|
b363c76639 |
No files matched your search
@@ -53,7 +53,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
||||||
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
||||||
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}"
|
$env:DATABASE_URL = "${{ secrets.DATABASE_URL }}"
|
||||||
$env:WOTB_DLC_CDN = "${{ secrets.WOTB_DLC_CDN }}"
|
$env:WOTB_DLC_CDN = "${{ secrets.WOTB_DLC_CDN }}"
|
||||||
yarn setup:patches --depot
|
yarn setup:patches --depot
|
||||||
|
|
||||||
@@ -61,5 +61,5 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
||||||
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
||||||
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}"
|
$env:DATABASE_URL = "${{ secrets.DATABASE_URL }}"
|
||||||
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
|
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
|
||||||
@@ -33,4 +33,4 @@ jobs:
|
|||||||
unzip bot.zip
|
unzip bot.zip
|
||||||
|
|
||||||
- name: Run bot
|
- name: Run bot
|
||||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} node --enable-source-maps index.cjs
|
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} node --enable-source-maps index.cjs
|
||||||
@@ -31,4 +31,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=asia --target=midnight --production
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=asia --target=midnight --production
|
||||||
@@ -31,4 +31,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=eu --target=midnight --production
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=eu --target=midnight --production
|
||||||
@@ -31,4 +31,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate all leaderboards
|
- name: Generate all leaderboards
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=na,asia,eu --target=latest --production
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=na,asia,eu --target=latest --production
|
||||||
@@ -31,4 +31,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=na --target=midnight --production
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=na --target=midnight --production
|
||||||
Vendored
+2
@@ -125,6 +125,7 @@
|
|||||||
"Synx",
|
"Synx",
|
||||||
"tankaverages",
|
"tankaverages",
|
||||||
"Tankerch",
|
"Tankerch",
|
||||||
|
"tankmen",
|
||||||
"tankopedia",
|
"tankopedia",
|
||||||
"tankopedias",
|
"tankopedias",
|
||||||
"tankstats",
|
"tankstats",
|
||||||
@@ -143,6 +144,7 @@
|
|||||||
"Vovko",
|
"Vovko",
|
||||||
"wargaming",
|
"wargaming",
|
||||||
"webp",
|
"webp",
|
||||||
|
"whitespaces",
|
||||||
"winrate",
|
"winrate",
|
||||||
"wireframe",
|
"wireframe",
|
||||||
"WOTB",
|
"WOTB",
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Blitzkrieg 1.18.3
|
||||||
|
|
||||||
|
Making Tankopedia more pleasing to use. This update only affects the website.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added crew skills
|
||||||
|
- Added quick actions
|
||||||
|
- For example, crew skills have the "clear" and "maximize" actions and equipment simply has a "clear" action
|
||||||
|
- Added info buttons
|
||||||
|
- Beside the titles of a few sections, a new info buttons have been added for extended descriptions
|
||||||
|
- Tank configurations have been condensed thanks to a new layout that works well on most screen sizes
|
||||||
|
- The Tankopedia now takes advantage of wider screens than before; hence, getting rid of the useless whitespaces on the sides
|
||||||
|
- Tank modules are now neatly wrapped into dropdown menus
|
||||||
|
- Sorting tanks by "size" has been renamed to "volume" and length has been added as a new option
|
||||||
|
- The antagonist (the shooting tank) bar has been shrunk to only fit the its contents
|
||||||
|
- Fullscreen mode now features all the missing dropdowns and options that were previously hidden
|
||||||
|
- Added icons for donations on Patreon and Ko-fi in the navigation bar
|
||||||
|
- Tank search now uses regular links allowing for things like opening in new tabs, copying links, etc.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Armor highlighting once again works properly even after switching around tank modules
|
||||||
|
- Inputting invalid numbers into the pitch and yaw fields is now handled correctly
|
||||||
|
- Removed unnecessary tooltips that didn't show up anyway due to Radix UI 3
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Upgraded to Radix UI 3
|
||||||
+4
-4
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:bot": "vite-node --script scripts/build",
|
"build:bot": "vite-node --script scripts/build --production",
|
||||||
"build:website": "next build && next-sitemap --config next-sitemap.config.cjs",
|
"build:website": "next build && next-sitemap --config next-sitemap.config.cjs",
|
||||||
"build:docs": "vitepress build docs",
|
"build:docs": "vitepress build docs",
|
||||||
"build:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard",
|
"build:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard",
|
||||||
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
|
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
|
||||||
"build:test": "vite-node --script scripts/buildTest",
|
"build:test": "vite-node --script scripts/buildTest",
|
||||||
"dev:bot": "vite-node --script scripts/build --production && yarn start:bot",
|
"dev:bot": "vite-node --script scripts/build && yarn start:bot",
|
||||||
"dev:docs": "vitepress dev docs",
|
"dev:docs": "vitepress dev docs",
|
||||||
"dev:website": "next dev",
|
"dev:website": "next dev",
|
||||||
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
|
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"@prisma/client": "^5.4.1",
|
"@prisma/client": "^5.4.1",
|
||||||
"@radix-ui/colors": "^3.0.0",
|
"@radix-ui/colors": "^3.0.0",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/themes": "^2.0.1",
|
"@radix-ui/themes": "^3.0.1",
|
||||||
"@react-three/drei": "^9.93.0",
|
"@react-three/drei": "^9.93.0",
|
||||||
"@react-three/fiber": "^8.15.14",
|
"@react-three/fiber": "^8.15.14",
|
||||||
"@resvg/resvg-js": "^2.4.1",
|
"@resvg/resvg-js": "^2.4.1",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "discord_blitz" (
|
|
||||||
"discord" INT8 NOT NULL,
|
|
||||||
"region" STRING NOT NULL,
|
|
||||||
"blitz" INT4 NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord")
|
|
||||||
);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Please do not edit this file manually
|
|
||||||
# It should be added in your version-control system (i.e. Git)
|
|
||||||
provider = "cockroachdb"
|
|
||||||
@@ -4,11 +4,15 @@ generator client {
|
|||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "cockroachdb"
|
provider = "cockroachdb"
|
||||||
url = env("DISCORD_BLITZ_DB")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model discord_blitz {
|
model Users {
|
||||||
|
blitz Int @id
|
||||||
|
last_used DateTime? @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model DiscordUsers {
|
||||||
discord BigInt @id
|
discord BigInt @id
|
||||||
region String
|
|
||||||
blitz Int
|
blitz Int
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
google.com, pub-1289406790225052, DIRECT, f08c47fec0942fa0
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readdir, writeFile } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import { parse as parsePath } from 'path';
|
import { parse as parsePath } from 'path';
|
||||||
import { Vector3Tuple } from 'three';
|
import { Vector3Tuple } from 'three';
|
||||||
import { parse as parseYaml } from 'yaml';
|
import { parse as parseYaml } from 'yaml';
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
ModelDefinitions,
|
ModelDefinitions,
|
||||||
} from '../../src/core/blitzkrieg/modelDefinitions';
|
} from '../../src/core/blitzkrieg/modelDefinitions';
|
||||||
import { ProvisionDefinitions } from '../../src/core/blitzkrieg/provisionDefinitions';
|
import { ProvisionDefinitions } from '../../src/core/blitzkrieg/provisionDefinitions';
|
||||||
|
import { SkillDefinitions } from '../../src/core/blitzkrieg/skillDefinitions';
|
||||||
import { superCompress } from '../../src/core/blitzkrieg/superCompress';
|
import { superCompress } from '../../src/core/blitzkrieg/superCompress';
|
||||||
import {
|
import {
|
||||||
Crew,
|
Crew,
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
Tier,
|
Tier,
|
||||||
} from '../../src/core/blitzkrieg/tankDefinitions';
|
} from '../../src/core/blitzkrieg/tankDefinitions';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
|
import { Avatar } from './skillIcons';
|
||||||
import { TankParameters } from './tankIcons';
|
import { TankParameters } from './tankIcons';
|
||||||
|
|
||||||
interface Strings {
|
interface Strings {
|
||||||
@@ -116,6 +118,7 @@ interface VehicleDefinitions {
|
|||||||
weight: number;
|
weight: number;
|
||||||
terrainResistance: string;
|
terrainResistance: string;
|
||||||
rotationSpeed: number;
|
rotationSpeed: number;
|
||||||
|
userString: string;
|
||||||
shotDispersionFactors: {
|
shotDispersionFactors: {
|
||||||
vehicleMovement: number;
|
vehicleMovement: number;
|
||||||
vehicleRotation: number;
|
vehicleRotation: number;
|
||||||
@@ -341,6 +344,9 @@ export async function definitions(production: boolean) {
|
|||||||
};
|
};
|
||||||
const consumableDefinitions: ConsumableDefinitions = {};
|
const consumableDefinitions: ConsumableDefinitions = {};
|
||||||
const provisionDefinitions: ProvisionDefinitions = {};
|
const provisionDefinitions: ProvisionDefinitions = {};
|
||||||
|
const skillDefinitions: SkillDefinitions = {
|
||||||
|
classes: { lightTank: [], mediumTank: [], heavyTank: [], 'AT-SPG': [] },
|
||||||
|
};
|
||||||
const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
|
const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
|
||||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||||
);
|
);
|
||||||
@@ -367,6 +373,9 @@ export async function definitions(production: boolean) {
|
|||||||
const provisions = await readXMLDVPL<{ root: ProvisionsCommon }>(
|
const provisions = await readXMLDVPL<{ root: ProvisionsCommon }>(
|
||||||
`${DATA}/${POI.provisionsCommon}.dvpl`,
|
`${DATA}/${POI.provisionsCommon}.dvpl`,
|
||||||
);
|
);
|
||||||
|
const avatar = await readXMLDVPL<{ root: Avatar }>(
|
||||||
|
`${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`,
|
||||||
|
);
|
||||||
|
|
||||||
const tankXps = new Map<number, number>();
|
const tankXps = new Map<number, number>();
|
||||||
|
|
||||||
@@ -558,6 +567,7 @@ export async function definitions(production: boolean) {
|
|||||||
tankDefinitions[tankId].tracks.push({
|
tankDefinitions[tankId].tracks.push({
|
||||||
id: trackId,
|
id: trackId,
|
||||||
weight: track.weight,
|
weight: track.weight,
|
||||||
|
name: strings[track.userString],
|
||||||
traverseSpeed: track.rotationSpeed,
|
traverseSpeed: track.rotationSpeed,
|
||||||
dispersion: {
|
dispersion: {
|
||||||
move: track.shotDispersionFactors.vehicleMovement,
|
move: track.shotDispersionFactors.vehicleMovement,
|
||||||
@@ -1084,10 +1094,10 @@ export async function definitions(production: boolean) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
writeFile(
|
Object.entries(avatar.root.skillsByClasses).forEach(([tankClass, skills]) => {
|
||||||
'test.tanks.cdon.lz4',
|
skillDefinitions.classes[tankClass as TankClass] = skills.split(' ');
|
||||||
Buffer.from(superCompress(tankDefinitions), 'base64'),
|
});
|
||||||
);
|
|
||||||
await commitAssets(
|
await commitAssets(
|
||||||
'definitions',
|
'definitions',
|
||||||
[
|
[
|
||||||
@@ -1116,6 +1126,11 @@ export async function definitions(production: boolean) {
|
|||||||
encoding: 'base64',
|
encoding: 'base64',
|
||||||
path: 'definitions/provisions.cdon.lz4',
|
path: 'definitions/provisions.cdon.lz4',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
content: superCompress(skillDefinitions),
|
||||||
|
encoding: 'base64',
|
||||||
|
path: 'definitions/skills.cdon.lz4',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
production,
|
production,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { equipmentIcons } from './equipmentIcons';
|
|||||||
import { moduleIcons } from './moduleIcons';
|
import { moduleIcons } from './moduleIcons';
|
||||||
import { scratchedFlags } from './scratchedFlags';
|
import { scratchedFlags } from './scratchedFlags';
|
||||||
import { shellIcons } from './shellIcons';
|
import { shellIcons } from './shellIcons';
|
||||||
|
import { skillIcons } from './skillIcons';
|
||||||
import { tankArmors } from './tankArmors';
|
import { tankArmors } from './tankArmors';
|
||||||
import { tankIcons } from './tankIcons';
|
import { tankIcons } from './tankIcons';
|
||||||
import { tankModels } from './tankModels';
|
import { tankModels } from './tankModels';
|
||||||
@@ -40,6 +41,7 @@ const methods = [
|
|||||||
tankArmors,
|
tankArmors,
|
||||||
tankModels,
|
tankModels,
|
||||||
currencies,
|
currencies,
|
||||||
|
skillIcons,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const method of methods) {
|
for (const method of methods) {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
import { TankClass } from '../../src/components/Tanks';
|
||||||
|
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||||
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
|
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||||
|
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||||
|
import { DATA } from './constants';
|
||||||
|
|
||||||
|
interface SkillIcon {
|
||||||
|
name: string;
|
||||||
|
state: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Avatar {
|
||||||
|
roles: unknown;
|
||||||
|
skillsByClasses: Record<TankClass, string>;
|
||||||
|
skills: {
|
||||||
|
[name: string]: {
|
||||||
|
userString: string;
|
||||||
|
effectDescription: string;
|
||||||
|
tipDescription: string;
|
||||||
|
icon: SkillIcon | SkillIcon[];
|
||||||
|
type: 'continuous' | 'trigger';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function skillIcons(production: boolean) {
|
||||||
|
console.log('Building skill icons...');
|
||||||
|
|
||||||
|
const avatar = await readXMLDVPL<{ root: Avatar }>(
|
||||||
|
`${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`,
|
||||||
|
);
|
||||||
|
const changes = await Promise.all(
|
||||||
|
Object.values(avatar.root.skills).map(async (skill) => {
|
||||||
|
const icon = Array.isArray(skill.icon) ? skill.icon[0] : skill.icon;
|
||||||
|
const name = icon.name.split('/').at(-1)!.replace(/_\d$/, '');
|
||||||
|
const path = `${DATA}${icon.name.replace('~res:', '')}.packed.webp.dvpl`;
|
||||||
|
const image = sharp(await readDVPLFile(path)).trim();
|
||||||
|
const content = (await image.toBuffer()).toString('base64');
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
encoding: 'base64',
|
||||||
|
path: `icons/skills/${name}.webp`,
|
||||||
|
} satisfies FileChange;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
commitAssets('skill icons', changes, production);
|
||||||
|
}
|
||||||
+4
-2
@@ -13,6 +13,7 @@ import Navbar from '../components/Navbar';
|
|||||||
import PageWrapper from '../components/PageWrapper';
|
import PageWrapper from '../components/PageWrapper';
|
||||||
import isDev from '../core/blitzkrieg/isDev';
|
import isDev from '../core/blitzkrieg/isDev';
|
||||||
import { isLocalhost } from '../core/blitzkrieg/isLocalhost';
|
import { isLocalhost } from '../core/blitzkrieg/isLocalhost';
|
||||||
|
import { useFullScreen } from '../hooks/useFullScreen';
|
||||||
import { useApp } from '../stores/app';
|
import { useApp } from '../stores/app';
|
||||||
import { mintTheme } from '../themes/mint';
|
import { mintTheme } from '../themes/mint';
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isEmbed = pathname.split('/')[1] === 'embeds';
|
const isEmbed = pathname.split('/')[1] === 'embeds';
|
||||||
const [showDevBuildAlert, setShowDevBuildAlert] = useState(false);
|
const [showDevBuildAlert, setShowDevBuildAlert] = useState(false);
|
||||||
|
const isFullScreen = useFullScreen();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowDevBuildAlert(
|
setShowDevBuildAlert(
|
||||||
@@ -100,11 +102,11 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
|
|
||||||
<Flex direction="column">{children}</Flex>
|
<Flex direction="column">{children}</Flex>
|
||||||
|
|
||||||
{!isEmbed && (
|
{!isEmbed && !isFullScreen && (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://ko-fi.com/tresabhi"
|
href="https://www.patreon.com/tresabhi"
|
||||||
style={{ textDecoration: 'none' }}
|
style={{ textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
+1
-2
@@ -15,11 +15,10 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<TextField.Root>
|
<TextField.Root placeholder="Search tools...">
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
<TextField.Input ref={input} placeholder="Search tools..." />
|
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
<Flex gap="3" wrap="wrap" align="center" justify="center">
|
<Flex gap="3" wrap="wrap" align="center" justify="center">
|
||||||
|
|||||||
@@ -53,29 +53,26 @@ export function PageTurner({
|
|||||||
<CaretLeftIcon />
|
<CaretLeftIcon />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<TextField.Root>
|
|
||||||
|
<TextField.Root
|
||||||
|
type="number"
|
||||||
|
ref={pageInput}
|
||||||
|
style={{ textAlign: 'center' }}
|
||||||
|
min={1}
|
||||||
|
max={pages}
|
||||||
|
onBlur={(event) => {
|
||||||
|
onPageChange(
|
||||||
|
Math.max(0, Math.min(pages - 1, event.target.valueAsNumber - 1)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
(event.target as HTMLInputElement).blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TextField.Slot>Page</TextField.Slot>
|
<TextField.Slot>Page</TextField.Slot>
|
||||||
<TextField.Input
|
|
||||||
type="number"
|
|
||||||
ref={pageInput}
|
|
||||||
style={{ width: 64, textAlign: 'center' }}
|
|
||||||
min={1}
|
|
||||||
max={pages}
|
|
||||||
onBlur={(event) => {
|
|
||||||
onPageChange(
|
|
||||||
Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(pages - 1, event.target.valueAsNumber - 1),
|
|
||||||
),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
(event.target as HTMLInputElement).blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField.Slot>out of {pages}</TextField.Slot>
|
<TextField.Slot>out of {pages}</TextField.Slot>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -605,7 +605,7 @@ export default function Page() {
|
|||||||
|
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Flex gap="4" justify="center">
|
<Flex gap="4" justify="center">
|
||||||
<TextField.Input
|
<TextField.Root
|
||||||
ref={scoreInput}
|
ref={scoreInput}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Type a score..."
|
placeholder="Type a score..."
|
||||||
@@ -638,7 +638,7 @@ export default function Page() {
|
|||||||
|
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Flex gap="4" justify="center">
|
<Flex gap="4" justify="center">
|
||||||
<TextField.Input
|
<TextField.Root
|
||||||
ref={positionInput}
|
ref={positionInput}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Type a position..."
|
placeholder="Type a position..."
|
||||||
@@ -759,105 +759,100 @@ export default function Page() {
|
|||||||
|
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Flex gap="4" direction="column">
|
<Flex gap="4" direction="column">
|
||||||
<TextField.Root>
|
<TextField.Root
|
||||||
|
onChange={debounce(
|
||||||
|
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const trimmedSearch = event.target.value.trim();
|
||||||
|
const encodedSearch = encodeURIComponent(trimmedSearch);
|
||||||
|
|
||||||
|
if (trimmedSearch) {
|
||||||
|
if (season === 0) {
|
||||||
|
setSearchLoading(true);
|
||||||
|
|
||||||
|
const accountList = await searchCurrentRatingPlayers(
|
||||||
|
region,
|
||||||
|
trimmedSearch,
|
||||||
|
);
|
||||||
|
const values = Object.values(accountList).filter(
|
||||||
|
(searchedPlayer) => !searchedPlayer.skip,
|
||||||
|
);
|
||||||
|
const valuesWithTypes =
|
||||||
|
values as ((typeof values)[number] & {
|
||||||
|
skip: false;
|
||||||
|
})[];
|
||||||
|
|
||||||
|
useLeaderboardCache.setState(
|
||||||
|
produce((draft: LeaderboardCache) => {
|
||||||
|
if (draft) {
|
||||||
|
values.forEach((player) => {
|
||||||
|
const playerWithTypes =
|
||||||
|
player as typeof player & { skip: false };
|
||||||
|
|
||||||
|
draft[region][season][
|
||||||
|
playerWithTypes.number - 1
|
||||||
|
] = playerWithTypes.spa_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
useNameCache.setState(
|
||||||
|
produce((draft: typeof names) => {
|
||||||
|
valuesWithTypes.forEach((searchedPlayer) => {
|
||||||
|
if (searchedPlayer.nickname) {
|
||||||
|
draft[region][searchedPlayer.spa_id] = {
|
||||||
|
nickname: searchedPlayer.nickname,
|
||||||
|
clan: searchedPlayer.clan_tag,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setSearchResults(
|
||||||
|
valuesWithTypes.map((searchedPlayer) => ({
|
||||||
|
account_id: searchedPlayer.spa_id,
|
||||||
|
nickname: searchedPlayer.nickname,
|
||||||
|
number: searchedPlayer.number,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
setSearchLoading(false);
|
||||||
|
} else {
|
||||||
|
setSearchLoading(true);
|
||||||
|
const accountList = await fetchBlitz<AccountList>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}`,
|
||||||
|
);
|
||||||
|
const playerKeys = Object.keys(
|
||||||
|
leaderboard[region][season],
|
||||||
|
);
|
||||||
|
|
||||||
|
setSearchResults(
|
||||||
|
accountList
|
||||||
|
.map((searchedPlayer) => ({
|
||||||
|
...searchedPlayer,
|
||||||
|
number: parseInt(
|
||||||
|
playerKeys.find(
|
||||||
|
(indexAsString) =>
|
||||||
|
leaderboard[region][season][
|
||||||
|
parseInt(indexAsString)
|
||||||
|
] === searchedPlayer.account_id,
|
||||||
|
)!,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter((player) => !Number.isNaN(player.number)),
|
||||||
|
);
|
||||||
|
|
||||||
|
setSearchLoading(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSearchResults(undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
)}
|
||||||
|
placeholder="Search for player..."
|
||||||
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
|
|
||||||
<TextField.Input
|
|
||||||
onChange={debounce(
|
|
||||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const trimmedSearch = event.target.value.trim();
|
|
||||||
const encodedSearch = encodeURIComponent(trimmedSearch);
|
|
||||||
|
|
||||||
if (trimmedSearch) {
|
|
||||||
if (season === 0) {
|
|
||||||
setSearchLoading(true);
|
|
||||||
|
|
||||||
const accountList =
|
|
||||||
await searchCurrentRatingPlayers(
|
|
||||||
region,
|
|
||||||
trimmedSearch,
|
|
||||||
);
|
|
||||||
const values = Object.values(accountList).filter(
|
|
||||||
(searchedPlayer) => !searchedPlayer.skip,
|
|
||||||
);
|
|
||||||
const valuesWithTypes =
|
|
||||||
values as ((typeof values)[number] & {
|
|
||||||
skip: false;
|
|
||||||
})[];
|
|
||||||
|
|
||||||
useLeaderboardCache.setState(
|
|
||||||
produce((draft: LeaderboardCache) => {
|
|
||||||
if (draft) {
|
|
||||||
values.forEach((player) => {
|
|
||||||
const playerWithTypes =
|
|
||||||
player as typeof player & { skip: false };
|
|
||||||
|
|
||||||
draft[region][season][
|
|
||||||
playerWithTypes.number - 1
|
|
||||||
] = playerWithTypes.spa_id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
useNameCache.setState(
|
|
||||||
produce((draft: typeof names) => {
|
|
||||||
valuesWithTypes.forEach((searchedPlayer) => {
|
|
||||||
if (searchedPlayer.nickname) {
|
|
||||||
draft[region][searchedPlayer.spa_id] = {
|
|
||||||
nickname: searchedPlayer.nickname,
|
|
||||||
clan: searchedPlayer.clan_tag,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setSearchResults(
|
|
||||||
valuesWithTypes.map((searchedPlayer) => ({
|
|
||||||
account_id: searchedPlayer.spa_id,
|
|
||||||
nickname: searchedPlayer.nickname,
|
|
||||||
number: searchedPlayer.number,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
setSearchLoading(false);
|
|
||||||
} else {
|
|
||||||
setSearchLoading(true);
|
|
||||||
const accountList = await fetchBlitz<AccountList>(
|
|
||||||
`https://api.wotblitz.${region}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}`,
|
|
||||||
);
|
|
||||||
const playerKeys = Object.keys(
|
|
||||||
leaderboard[region][season],
|
|
||||||
);
|
|
||||||
|
|
||||||
setSearchResults(
|
|
||||||
accountList
|
|
||||||
.map((searchedPlayer) => ({
|
|
||||||
...searchedPlayer,
|
|
||||||
number: parseInt(
|
|
||||||
playerKeys.find(
|
|
||||||
(indexAsString) =>
|
|
||||||
leaderboard[region][season][
|
|
||||||
parseInt(indexAsString)
|
|
||||||
] === searchedPlayer.account_id,
|
|
||||||
)!,
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
.filter(
|
|
||||||
(player) => !Number.isNaN(player.number),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
setSearchLoading(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSearchResults(undefined);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
500,
|
|
||||||
)}
|
|
||||||
placeholder="Search for player..."
|
|
||||||
/>
|
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
|
|||||||
@@ -37,19 +37,18 @@ export function Menu({ Builder, reset }: ContextMenuProps) {
|
|||||||
<Heading size="4">Edit title</Heading>
|
<Heading size="4">Edit title</Heading>
|
||||||
|
|
||||||
<Flex gap="2" style={{ width: '100%' }}>
|
<Flex gap="2" style={{ width: '100%' }}>
|
||||||
<TextField.Root style={{ flex: 1 }}>
|
<TextField.Root
|
||||||
<TextField.Input
|
style={{ flex: 1 }}
|
||||||
ref={titleInput}
|
ref={titleInput}
|
||||||
defaultValue={session.title}
|
defaultValue={session.title}
|
||||||
placeholder="Enter title..."
|
placeholder="Enter title..."
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter' && titleInput.current) {
|
if (event.key === 'Enter' && titleInput.current) {
|
||||||
setShowTitleEditor(false);
|
setShowTitleEditor(false);
|
||||||
useSession.setState({ title: titleInput.current!.value });
|
useSession.setState({ title: titleInput.current!.value });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TextField.Root>
|
|
||||||
|
|
||||||
<Dialog.Close>
|
<Dialog.Close>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -56,38 +56,36 @@ export default function Page() {
|
|||||||
<PageWrapper color="blue">
|
<PageWrapper color="blue">
|
||||||
<div className={styles.toolBar}>
|
<div className={styles.toolBar}>
|
||||||
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
|
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
|
||||||
<TextField.Root>
|
<TextField.Root
|
||||||
|
defaultValue={session.isTracking ? session.nickname : undefined}
|
||||||
|
ref={input}
|
||||||
|
onChange={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (event.target.value) {
|
||||||
|
setShowSearchResults(true);
|
||||||
|
setSearchResults(undefined);
|
||||||
|
} else {
|
||||||
|
setShowSearchResults(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(event);
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
// TODO: remove this hack when https://github.com/radix-ui/primitives/issues/2193 (https://github.com/radix-ui/primitives/issues/1969) is fixed
|
||||||
|
if (showSearchResults) event.target.focus();
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setShowSearchResults(false);
|
||||||
|
input.current?.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Search for a player..."
|
||||||
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<PersonIcon height="16" width="16" />
|
<PersonIcon height="16" width="16" />
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
|
|
||||||
<TextField.Input
|
|
||||||
defaultValue={session.isTracking ? session.nickname : undefined}
|
|
||||||
ref={input}
|
|
||||||
onChange={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
if (event.target.value) {
|
|
||||||
setShowSearchResults(true);
|
|
||||||
setSearchResults(undefined);
|
|
||||||
} else {
|
|
||||||
setShowSearchResults(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange(event);
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
// TODO: remove this hack when https://github.com/radix-ui/primitives/issues/2193 is fixed
|
|
||||||
if (showSearchResults) event.target.focus();
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
setShowSearchResults(false);
|
|
||||||
input.current?.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="Search for a player..."
|
|
||||||
/>
|
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
<DropdownMenu.Root open={showSearchResults} modal={false}>
|
<DropdownMenu.Root open={showSearchResults} modal={false}>
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import { CaretRightIcon, Cross1Icon, ShuffleIcon } from '@radix-ui/react-icons';
|
import { Cross1Icon, ShuffleIcon } from '@radix-ui/react-icons';
|
||||||
import {
|
import { Button, Card, Dialog, Flex, Heading, Tabs } from '@radix-ui/themes';
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Dialog,
|
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
Tabs,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
} from '@radix-ui/themes';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ModuleButton } from '../../../../../components/ModuleButtons/ModuleButton';
|
import { ModuleButton } from '../../../../../components/ModuleButtons/ModuleButton';
|
||||||
import { ShellButton } from '../../../../../components/ModuleButtons/ShellButton';
|
import { ShellButton } from '../../../../../components/ModuleButtons/ShellButton';
|
||||||
@@ -37,58 +27,70 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
|
|||||||
const [tab, setTab] = useState('search');
|
const [tab, setTab] = useState('search');
|
||||||
const [antagonistSelectorOpen, setAntagonistSelectorVisible] =
|
const [antagonistSelectorOpen, setAntagonistSelectorVisible] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
if (mode !== 'armor') return null;
|
if (mode !== 'armor') return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Flex justify="center">
|
||||||
style={
|
<Card
|
||||||
floating
|
style={
|
||||||
? {
|
floating
|
||||||
position: 'absolute',
|
? {
|
||||||
bottom: 8,
|
position: 'absolute',
|
||||||
left: '50%',
|
bottom: 8,
|
||||||
transform: 'translateX(-50%)',
|
left: '50%',
|
||||||
}
|
transform: 'translateX(-50%)',
|
||||||
: undefined
|
}
|
||||||
}
|
: undefined
|
||||||
>
|
}
|
||||||
<Flex align="center" justify="between" gap="6">
|
>
|
||||||
<Flex>
|
<Flex
|
||||||
{antagonist.gun.shells.map((shell, index) => {
|
align="center"
|
||||||
return (
|
justify="center"
|
||||||
<ShellButton
|
wrap="wrap"
|
||||||
selected={antagonist.shell.id === shell.id}
|
style={{
|
||||||
shell={shell.icon}
|
gap: 'min(5vw, 32px)',
|
||||||
rowChild
|
}}
|
||||||
first={index === 0}
|
>
|
||||||
key={shell.id}
|
<Flex>
|
||||||
last={index === antagonist.gun.shells.length - 1}
|
{antagonist.gun.shells.map((shell, index) => {
|
||||||
onClick={() => {
|
return (
|
||||||
mutateDuel((draft) => {
|
<ShellButton
|
||||||
draft.antagonist!.shell = shell;
|
selected={antagonist.shell.id === shell.id}
|
||||||
mutateTankopediaTemporary((draft) => {
|
shell={shell.icon}
|
||||||
draft.shot = undefined;
|
rowChild
|
||||||
|
first={index === 0}
|
||||||
|
key={shell.id}
|
||||||
|
last={index === antagonist.gun.shells.length - 1}
|
||||||
|
onClick={() => {
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
draft.antagonist!.shell = shell;
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</Flex>
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex align="center" gap="4">
|
<Button
|
||||||
{!floating && <Text>Versus</Text>}
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
[draft.protagonist, draft.antagonist] = [
|
||||||
|
draft.antagonist,
|
||||||
|
draft.protagonist,
|
||||||
|
];
|
||||||
|
pushTankopediaPath(draft.antagonist!.tank.id);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShuffleIcon /> Swap
|
||||||
|
</Button>
|
||||||
|
|
||||||
{floating && (
|
<Flex align="center" gap="4">
|
||||||
<Flex gap="2" align="center">
|
|
||||||
Versus {antagonist.tank.name}
|
|
||||||
<SmallTankIcon id={antagonist.tank.id} size={16} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!floating && (
|
|
||||||
<Dialog.Root
|
<Dialog.Root
|
||||||
open={antagonistSelectorOpen}
|
open={antagonistSelectorOpen}
|
||||||
onOpenChange={setAntagonistSelectorVisible}
|
onOpenChange={setAntagonistSelectorVisible}
|
||||||
@@ -96,9 +98,8 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
|
|||||||
<Dialog.Trigger>
|
<Dialog.Trigger>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<Flex gap="2" align="center">
|
<Flex gap="2" align="center">
|
||||||
{antagonist.tank.name}
|
Versus {antagonist.tank.name}
|
||||||
<SmallTankIcon id={antagonist.tank.id} size={16} />
|
<SmallTankIcon id={antagonist.tank.id} size={16} />
|
||||||
<CaretRightIcon />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
@@ -160,61 +161,51 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
|
|||||||
<Flex gap="2" wrap="wrap">
|
<Flex gap="2" wrap="wrap">
|
||||||
<Flex>
|
<Flex>
|
||||||
{antagonist.tank.turrets.map((turret, index) => (
|
{antagonist.tank.turrets.map((turret, index) => (
|
||||||
<Tooltip content={turret.name} key={turret.id}>
|
<ModuleButton
|
||||||
<ModuleButton
|
rowChild
|
||||||
rowChild
|
first={index === 0}
|
||||||
first={index === 0}
|
last={
|
||||||
last={
|
index === antagonist.tank.turrets.length - 1
|
||||||
index ===
|
}
|
||||||
antagonist.tank.turrets.length - 1
|
key={turret.id}
|
||||||
}
|
onClick={() => {
|
||||||
key={turret.id}
|
mutateDuel((draft) => {
|
||||||
onClick={() => {
|
draft.antagonist!.turret = turret;
|
||||||
mutateDuel((draft) => {
|
draft.antagonist!.gun =
|
||||||
draft.antagonist!.turret = turret;
|
turret.guns.at(-1)!;
|
||||||
draft.antagonist!.gun =
|
draft.antagonist!.shell =
|
||||||
turret.guns.at(-1)!;
|
draft.antagonist!.gun.shells[0];
|
||||||
draft.antagonist!.shell =
|
});
|
||||||
draft.antagonist!.gun.shells[0];
|
mutateTankopediaTemporary((draft) => {
|
||||||
});
|
draft.shot = undefined;
|
||||||
mutateTankopediaTemporary((draft) => {
|
});
|
||||||
draft.shot = undefined;
|
}}
|
||||||
});
|
selected={antagonist.turret.id === turret.id}
|
||||||
}}
|
discriminator={
|
||||||
selected={
|
TIER_ROMAN_NUMERALS[turret.tier]
|
||||||
antagonist.turret.id === turret.id
|
}
|
||||||
}
|
module="turret"
|
||||||
discriminator={
|
/>
|
||||||
TIER_ROMAN_NUMERALS[turret.tier]
|
|
||||||
}
|
|
||||||
module="turret"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex>
|
<Flex>
|
||||||
{antagonist.turret.guns.map((gun, index) => (
|
{antagonist.turret.guns.map((gun, index) => (
|
||||||
<Tooltip content={gun.name} key={gun.id}>
|
<ModuleButton
|
||||||
<ModuleButton
|
rowChild
|
||||||
rowChild
|
first={index === 0}
|
||||||
first={index === 0}
|
last={
|
||||||
last={
|
index === antagonist.turret.guns.length - 1
|
||||||
index ===
|
}
|
||||||
antagonist.turret.guns.length - 1
|
onClick={() => {
|
||||||
}
|
mutateDuel((draft) => {
|
||||||
onClick={() => {
|
draft.antagonist!.gun = gun;
|
||||||
mutateDuel((draft) => {
|
draft.antagonist!.shell = gun.shells[0];
|
||||||
draft.antagonist!.gun = gun;
|
});
|
||||||
draft.antagonist!.shell = gun.shells[0];
|
}}
|
||||||
});
|
selected={antagonist.gun.id === gun.id}
|
||||||
}}
|
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
||||||
selected={antagonist.gun.id === gun.id}
|
module="gun"
|
||||||
discriminator={
|
/>
|
||||||
TIER_ROMAN_NUMERALS[gun.tier]
|
|
||||||
}
|
|
||||||
module="gun"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -251,24 +242,9 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
|
|||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
)}
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Card>
|
||||||
<Button
|
</Flex>
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
[draft.protagonist, draft.antagonist] = [
|
|
||||||
draft.antagonist,
|
|
||||||
draft.protagonist,
|
|
||||||
];
|
|
||||||
pushTankopediaPath(draft.antagonist!.tank.id);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShuffleIcon /> Swap
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+6
-2
@@ -1,5 +1,5 @@
|
|||||||
|
import { Flex } from '@radix-ui/themes';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import * as styles from '../../../../page.css';
|
|
||||||
|
|
||||||
interface ConfigurationChildWrapperProps {
|
interface ConfigurationChildWrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -8,5 +8,9 @@ interface ConfigurationChildWrapperProps {
|
|||||||
export function ConfigurationChildWrapper({
|
export function ConfigurationChildWrapper({
|
||||||
children,
|
children,
|
||||||
}: ConfigurationChildWrapperProps) {
|
}: ConfigurationChildWrapperProps) {
|
||||||
return <div className={styles.configurationChild}>{children}</div>;
|
return (
|
||||||
|
<Flex gap="2" direction="column">
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
+45
-5
@@ -1,4 +1,12 @@
|
|||||||
import { Flex, Heading } from '@radix-ui/themes';
|
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
Text,
|
||||||
|
} from '@radix-ui/themes';
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import { ConsumableButton } from '../../../../../../../components/ModuleButtons/ConsumableButton';
|
import { ConsumableButton } from '../../../../../../../components/ModuleButtons/ConsumableButton';
|
||||||
import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableProvisionInclusivity';
|
import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableProvisionInclusivity';
|
||||||
@@ -27,7 +35,38 @@ export function Consumables() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigurationChildWrapper>
|
<ConfigurationChildWrapper>
|
||||||
<Heading size="4">Consumables</Heading>
|
<Flex gap="4" align="center">
|
||||||
|
<Flex gap="2" align="center">
|
||||||
|
<Heading size="4">Consumables</Heading>
|
||||||
|
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<IconButton variant="ghost">
|
||||||
|
<InfoCircledIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Content>
|
||||||
|
<Text>
|
||||||
|
<Text color="amber">Yellow consumables</Text> do not affect the
|
||||||
|
statistics.
|
||||||
|
</Text>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
draft.protagonist!.consumables = [];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Flex wrap="wrap">
|
<Flex wrap="wrap">
|
||||||
{consumablesList.map((consumable, index) => {
|
{consumablesList.map((consumable, index) => {
|
||||||
@@ -35,10 +74,11 @@ export function Consumables() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ConsumableButton
|
<ConsumableButton
|
||||||
|
special={consumable.duration === undefined}
|
||||||
duration={
|
duration={
|
||||||
typeof consumable.duration === 'number'
|
consumable.duration === undefined
|
||||||
? consumable.duration * (hasHighEndConsumables ? 0.7 : 1)
|
? undefined
|
||||||
: undefined
|
: consumable.duration * (hasHighEndConsumables ? 0.7 : 1)
|
||||||
}
|
}
|
||||||
cooldown={
|
cooldown={
|
||||||
consumable.cooldown *
|
consumable.cooldown *
|
||||||
|
|||||||
+20
-3
@@ -1,11 +1,11 @@
|
|||||||
import { Flex, Heading } from '@radix-ui/themes';
|
import { Button, Flex, Heading } from '@radix-ui/themes';
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import { EquipmentButton } from '../../../../../../../components/ModuleButtons/EquipmentButton';
|
import { EquipmentButton } from '../../../../../../../components/ModuleButtons/EquipmentButton';
|
||||||
import { equipmentDefinitions } from '../../../../../../../core/blitzkrieg/equipmentDefinitions';
|
import { equipmentDefinitions } from '../../../../../../../core/blitzkrieg/equipmentDefinitions';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||||
|
|
||||||
export function Equipments() {
|
export function Equipment() {
|
||||||
const protagonist = useDuel((state) => state.protagonist!);
|
const protagonist = useDuel((state) => state.protagonist!);
|
||||||
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
|
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
|
||||||
const equipmentRows =
|
const equipmentRows =
|
||||||
@@ -14,7 +14,24 @@ export function Equipments() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigurationChildWrapper>
|
<ConfigurationChildWrapper>
|
||||||
<Heading size="4">Equipments</Heading>
|
<Flex gap="4" align="center">
|
||||||
|
<Heading size="4">Equipment</Heading>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
draft.protagonist!.equipment.forEach((row) => {
|
||||||
|
row.forEach((_, index) => {
|
||||||
|
row[index] = 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
{equipmentRows.map((equipmentRow, rowIndex) => (
|
{equipmentRows.map((equipmentRow, rowIndex) => (
|
||||||
+27
-28
@@ -87,36 +87,35 @@ export function Miscellaneous() {
|
|||||||
direction="column"
|
direction="column"
|
||||||
gap="1"
|
gap="1"
|
||||||
justify="end"
|
justify="end"
|
||||||
style={{ width: 180 }}
|
style={{ width: 120 }}
|
||||||
>
|
>
|
||||||
<TextField.Root>
|
<TextField.Root
|
||||||
|
ref={crewInput}
|
||||||
|
defaultValue={Math.round(crewMastery * 100)}
|
||||||
|
style={{ textAlign: 'right' }}
|
||||||
|
onBlur={() => {
|
||||||
|
const newValue = Number(crewInput.current!.value) / 100;
|
||||||
|
|
||||||
|
if (isNaN(newValue)) {
|
||||||
|
crewInput.current!.value = `${Math.round(crewMastery * 100)}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
draft.protagonist!.crewMastery = clamp(
|
||||||
|
newValue / 100,
|
||||||
|
0.5,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onKeyUp={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
crewInput.current?.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TextField.Slot>Crew</TextField.Slot>
|
<TextField.Slot>Crew</TextField.Slot>
|
||||||
<TextField.Input
|
|
||||||
ref={crewInput}
|
|
||||||
defaultValue={Math.round(crewMastery * 100)}
|
|
||||||
style={{ textAlign: 'right' }}
|
|
||||||
onBlur={() => {
|
|
||||||
const newValue = Number(crewInput.current!.value) / 100;
|
|
||||||
|
|
||||||
if (isNaN(newValue)) {
|
|
||||||
crewInput.current!.value = `${Math.round(crewMastery * 100)}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist!.crewMastery = clamp(
|
|
||||||
newValue / 100,
|
|
||||||
0.5,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onKeyUp={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
crewInput.current?.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField.Slot>%</TextField.Slot>
|
<TextField.Slot>%</TextField.Slot>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Flex, Heading } from '@radix-ui/themes';
|
import { Button, DropdownMenu, Flex, Heading } from '@radix-ui/themes';
|
||||||
|
import { useState } from 'react';
|
||||||
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
|
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
|
||||||
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||||
@@ -7,101 +8,236 @@ import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
|||||||
|
|
||||||
export function Modules() {
|
export function Modules() {
|
||||||
const protagonist = useDuel((state) => state.protagonist!);
|
const protagonist = useDuel((state) => state.protagonist!);
|
||||||
|
const hasUpgrades =
|
||||||
|
protagonist.tank.turrets.length > 1 ||
|
||||||
|
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 (
|
return (
|
||||||
<ConfigurationChildWrapper>
|
<ConfigurationChildWrapper>
|
||||||
<Heading size="4">Modules</Heading>
|
<Flex gap="4" align="center">
|
||||||
|
<Heading size="4">Modules</Heading>
|
||||||
|
{hasUpgrades && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
draft.protagonist!.turret =
|
||||||
|
draft.protagonist!.tank.turrets[0];
|
||||||
|
draft.protagonist!.gun = draft.protagonist!.turret.guns[0];
|
||||||
|
draft.protagonist!.shell = draft.protagonist!.gun.shells[0];
|
||||||
|
draft.protagonist!.engine =
|
||||||
|
draft.protagonist!.tank.engines[0];
|
||||||
|
draft.protagonist!.track = draft.protagonist!.tank.tracks[0];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Stock
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
draft.protagonist!.turret =
|
||||||
|
draft.protagonist!.tank.turrets.at(-1)!;
|
||||||
|
draft.protagonist!.gun =
|
||||||
|
draft.protagonist!.turret.guns.at(-1)!;
|
||||||
|
draft.protagonist!.shell =
|
||||||
|
draft.protagonist!.gun.shells.at(-1)!;
|
||||||
|
draft.protagonist!.engine =
|
||||||
|
draft.protagonist!.tank.engines.at(-1)!;
|
||||||
|
draft.protagonist!.track =
|
||||||
|
draft.protagonist!.tank.tracks.at(-1)!;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Flex gap="2" wrap="wrap">
|
<Flex gap="1" wrap="wrap">
|
||||||
<Flex>
|
<ModuleButton
|
||||||
{protagonist.tank.turrets.map((turret, index) => {
|
module="turret"
|
||||||
return (
|
discriminator={TIER_ROMAN_NUMERALS[protagonist.turret.tier]}
|
||||||
<ModuleButton
|
first
|
||||||
key={turret.id}
|
last
|
||||||
selected={protagonist.turret.id === turret.id}
|
rowChild
|
||||||
module="turret"
|
onClick={() => setTurretMenuOpen(true)}
|
||||||
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
|
/>
|
||||||
first={index === 0}
|
|
||||||
last={index === protagonist.tank.turrets.length - 1}
|
|
||||||
rowChild
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex>
|
<DropdownMenu.Root
|
||||||
{protagonist.turret.guns.map((gun, index) => {
|
open={turretMenuOpen}
|
||||||
return (
|
onOpenChange={setTurretMenuOpen}
|
||||||
<ModuleButton
|
>
|
||||||
key={gun.id}
|
<DropdownMenu.Trigger>
|
||||||
module="gun"
|
<div />
|
||||||
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
</DropdownMenu.Trigger>
|
||||||
selected={protagonist.gun.id === gun.id}
|
|
||||||
first={index === 0}
|
|
||||||
last={index === protagonist.turret.guns.length - 1}
|
|
||||||
rowChild
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist!.gun = gun;
|
|
||||||
draft.protagonist!.shell = gun.shells[0];
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex>
|
<DropdownMenu.Content>
|
||||||
{protagonist.tank.engines.map((engine, index) => {
|
<DropdownMenu.RadioGroup value={`${protagonist.turret.id}`}>
|
||||||
return (
|
{[...protagonist.tank.turrets].reverse().map((turret) => {
|
||||||
<ModuleButton
|
return (
|
||||||
key={engine.id}
|
<DropdownMenu.RadioItem
|
||||||
module="engine"
|
key={turret.id}
|
||||||
discriminator={TIER_ROMAN_NUMERALS[engine.tier]}
|
value={`${turret.id}`}
|
||||||
selected={protagonist.engine.id === engine.id}
|
onClick={() => {
|
||||||
first={index === 0}
|
mutateDuel((draft) => {
|
||||||
last={index === protagonist.tank.engines.length - 1}
|
draft.protagonist!.turret = turret;
|
||||||
rowChild
|
draft.protagonist!.gun = turret.guns.at(-1)!;
|
||||||
onClick={() => {
|
draft.protagonist!.shell =
|
||||||
mutateDuel((draft) => {
|
turret.guns.at(-1)!.shells[0];
|
||||||
draft.protagonist!.engine = engine;
|
});
|
||||||
});
|
mutateTankopediaTemporary((draft) => {
|
||||||
}}
|
draft.shot = undefined;
|
||||||
/>
|
});
|
||||||
);
|
setTurretMenuOpen(false);
|
||||||
})}
|
}}
|
||||||
</Flex>
|
>
|
||||||
|
<ModuleButton
|
||||||
|
variant="ghost"
|
||||||
|
module="turret"
|
||||||
|
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
|
||||||
|
/>{' '}
|
||||||
|
{turret.name}
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
<Flex>
|
<ModuleButton
|
||||||
{protagonist.tank.tracks.map((track, index) => {
|
module="gun"
|
||||||
return (
|
discriminator={TIER_ROMAN_NUMERALS[protagonist.gun.tier]}
|
||||||
<ModuleButton
|
first
|
||||||
key={track.id}
|
last
|
||||||
module="chassis"
|
rowChild
|
||||||
discriminator={TIER_ROMAN_NUMERALS[track.tier]}
|
onClick={() => setGunMenuOpen(true)}
|
||||||
selected={protagonist.track.id === track.id}
|
/>
|
||||||
first={index === 0}
|
|
||||||
last={index === protagonist.tank.tracks.length - 1}
|
<DropdownMenu.Root open={gunMenuOpen} onOpenChange={setGunMenuOpen}>
|
||||||
rowChild
|
<DropdownMenu.Trigger>
|
||||||
onClick={() => {
|
<div />
|
||||||
mutateDuel((draft) => {
|
</DropdownMenu.Trigger>
|
||||||
draft.protagonist!.track = track;
|
|
||||||
});
|
<DropdownMenu.Content>
|
||||||
}}
|
<DropdownMenu.RadioGroup value={`${protagonist.gun.id}`}>
|
||||||
/>
|
{[...protagonist.turret.guns].reverse().map((gun) => {
|
||||||
);
|
return (
|
||||||
})}
|
<DropdownMenu.RadioItem
|
||||||
</Flex>
|
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>
|
</Flex>
|
||||||
</ConfigurationChildWrapper>
|
</ConfigurationChildWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Flex, Heading } from '@radix-ui/themes';
|
import { Button, Flex, Heading } from '@radix-ui/themes';
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import { ProvisionButton } from '../../../../../../../components/ModuleButtons/ProvisionButton';
|
import { ProvisionButton } from '../../../../../../../components/ModuleButtons/ProvisionButton';
|
||||||
import { availableProvisions } from '../../../../../../../core/blitzkrieg/availableProvisions';
|
import { availableProvisions } from '../../../../../../../core/blitzkrieg/availableProvisions';
|
||||||
@@ -18,7 +18,20 @@ export function Provisions() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigurationChildWrapper>
|
<ConfigurationChildWrapper>
|
||||||
<Heading size="4">Provisions</Heading>
|
<Flex gap="4" align="center">
|
||||||
|
<Heading size="4">Provisions</Heading>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
draft.protagonist!.provisions = [];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Flex wrap="wrap">
|
<Flex wrap="wrap">
|
||||||
{provisionsList.map((provision, index) => {
|
{provisionsList.map((provision, index) => {
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
export const permanentSkills = [
|
||||||
|
'smooth_turn',
|
||||||
|
'soft_recoil',
|
||||||
|
'smooth_driving',
|
||||||
|
'smooth_turret',
|
||||||
|
'virtuoso',
|
||||||
|
'camouflage',
|
||||||
|
];
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Kbd,
|
||||||
|
Popover,
|
||||||
|
Text,
|
||||||
|
} from '@radix-ui/themes';
|
||||||
|
import { use } from 'react';
|
||||||
|
import { GenericTankComponentButton } from '../../../../../../../../components/ModuleButtons/GenericTankComponentButton';
|
||||||
|
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 {
|
||||||
|
mutateTankopediaTemporary,
|
||||||
|
useTankopediaTemporary,
|
||||||
|
} from '../../../../../../../../stores/tankopedia';
|
||||||
|
import { ConfigurationChildWrapper } from '../ConfigurationChildWrapper';
|
||||||
|
import { permanentSkills } from './constants';
|
||||||
|
|
||||||
|
export function Skills() {
|
||||||
|
const awaitedSkillDefinitions = use(skillDefinitions);
|
||||||
|
const skillLevels = useTankopediaTemporary((state) => state.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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigurationChildWrapper>
|
||||||
|
<Flex gap="4" align="center">
|
||||||
|
<Flex gap="2" align="center">
|
||||||
|
<Heading size="4">Crew skills</Heading>
|
||||||
|
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<IconButton variant="ghost">
|
||||||
|
<InfoCircledIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Content>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<Text>
|
||||||
|
<Text color="amber">Yellow skills</Text> only apply under
|
||||||
|
special circumstances.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
Hold <Kbd>Shift</Kbd> to quickly toggle between level 0 and 7.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
Object.keys(draft.skills).forEach((skill) => {
|
||||||
|
draft.skills[skill] = 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
Object.keys(draft.skills).forEach((skill) => {
|
||||||
|
draft.skills[skill] = 7;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Maximize
|
||||||
|
</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>
|
||||||
|
</ConfigurationChildWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
import { AccessibilityIcon } from '@radix-ui/react-icons';
|
import { AccessibilityIcon, InfoCircledIcon } from '@radix-ui/react-icons';
|
||||||
import { Flex, Heading, Slider, Text, TextField } from '@radix-ui/themes';
|
import {
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
Slider,
|
||||||
|
Text,
|
||||||
|
TextField,
|
||||||
|
} from '@radix-ui/themes';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { use, useEffect, useRef, useState } from 'react';
|
import { use, useEffect, useRef, useState } from 'react';
|
||||||
import { lerp } from 'three/src/math/MathUtils';
|
import { lerp } from 'three/src/math/MathUtils';
|
||||||
@@ -22,17 +30,21 @@ import {
|
|||||||
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
|
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
|
||||||
import { useConsumable } from '../../../../../../hooks/useConsumable';
|
import { useConsumable } from '../../../../../../hooks/useConsumable';
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
|
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
||||||
import { useProvision } from '../../../../../../hooks/useProvision';
|
import { useProvision } from '../../../../../../hooks/useProvision';
|
||||||
import { useProvisions } from '../../../../../../hooks/useProvisions';
|
import { useProvisions } from '../../../../../../hooks/useProvisions';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
||||||
|
import { useTankopediaTemporary } from '../../../../../../stores/tankopedia';
|
||||||
import { Info } from './components/Info';
|
import { Info } from './components/Info';
|
||||||
import { InfoWithDelta } from './components/InfoWithDelta';
|
import { InfoWithDelta } from './components/InfoWithDelta';
|
||||||
|
|
||||||
export function Characteristics() {
|
export function Characteristics() {
|
||||||
const awaitedModelDefinitions = use(modelDefinitions);
|
const awaitedModelDefinitions = use(modelDefinitions);
|
||||||
|
const crewSkills = useTankopediaTemporary((state) => state.skills);
|
||||||
const { tank, turret, gun, engine, track, shell } = useDuel(
|
const { tank, turret, gun, engine, track, shell } = useDuel(
|
||||||
(state) => state.protagonist!,
|
(state) => state.protagonist!,
|
||||||
);
|
);
|
||||||
|
const isFullScreen = useFullScreen();
|
||||||
const hasImprovedVentilation = useEquipment(102);
|
const hasImprovedVentilation = useEquipment(102);
|
||||||
const provisions = useProvisions();
|
const provisions = useProvisions();
|
||||||
const crewMastery = useDuel((state) => state.protagonist!.crewMastery);
|
const crewMastery = useDuel((state) => state.protagonist!.crewMastery);
|
||||||
@@ -132,10 +144,26 @@ export function Characteristics() {
|
|||||||
const dispersionStillCoefficient =
|
const dispersionStillCoefficient =
|
||||||
coefficient([hasRefinedGun, -0.1], [hasReticleCalibration, -0.4]) *
|
coefficient([hasRefinedGun, -0.1], [hasReticleCalibration, -0.4]) *
|
||||||
coefficient([true, degressiveStat(crewMemberMastery)]);
|
coefficient([true, degressiveStat(crewMemberMastery)]);
|
||||||
const dispersionMovingCoefficient = coefficient([
|
const dispersionDamagedCoefficient = coefficient([
|
||||||
hasVerticalStabilizer,
|
hasVerticalStabilizer,
|
||||||
-0.15,
|
-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(
|
const enginePowerCoefficient = coefficient(
|
||||||
[
|
[
|
||||||
hasEngineAccelerator &&
|
hasEngineAccelerator &&
|
||||||
@@ -161,6 +189,7 @@ export function Characteristics() {
|
|||||||
coefficient(
|
coefficient(
|
||||||
[hasImprovedControl, 0.1],
|
[hasImprovedControl, 0.1],
|
||||||
[hasImprovedEnginePowerBoost, 0.05],
|
[hasImprovedEnginePowerBoost, 0.05],
|
||||||
|
[true, crewSkills.virtuoso / 100],
|
||||||
) * coefficient([true, progressiveStat(crewMemberMastery)]);
|
) * coefficient([true, progressiveStat(crewMemberMastery)]);
|
||||||
const resistanceCoefficient = coefficient([hasImprovedSuspension, -0.25]);
|
const resistanceCoefficient = coefficient([hasImprovedSuspension, -0.25]);
|
||||||
const viewRangeCoefficient =
|
const viewRangeCoefficient =
|
||||||
@@ -204,6 +233,7 @@ export function Characteristics() {
|
|||||||
camouflage,
|
camouflage,
|
||||||
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
|
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
|
||||||
],
|
],
|
||||||
|
[true, crewSkills.camouflage * (3 / 100)],
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolvedEnginePower = engine.power * enginePowerCoefficient;
|
const resolvedEnginePower = engine.power * enginePowerCoefficient;
|
||||||
@@ -232,6 +262,8 @@ export function Characteristics() {
|
|||||||
});
|
});
|
||||||
}, [hasImprovedVerticalStabilizer]);
|
}, [hasImprovedVerticalStabilizer]);
|
||||||
|
|
||||||
|
if (isFullScreen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="8" style={{ width: '100%' }}>
|
<Flex direction="column" gap="8" style={{ width: '100%' }}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
@@ -390,25 +422,25 @@ export function Characteristics() {
|
|||||||
setPenetrationDistance(value);
|
setPenetrationDistance(value);
|
||||||
}, 500)}
|
}, 500)}
|
||||||
/>
|
/>
|
||||||
<TextField.Root style={{ width: 64 }}>
|
<TextField.Root
|
||||||
<TextField.Input
|
style={{ width: 64 }}
|
||||||
ref={penetrationDistanceInput}
|
ref={penetrationDistanceInput}
|
||||||
defaultValue={penetrationDistance}
|
defaultValue={penetrationDistance}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setPenetrationDistance(
|
setPenetrationDistance(
|
||||||
Math.min(
|
Math.min(
|
||||||
parseInt(penetrationDistanceInput.current!.value),
|
parseInt(penetrationDistanceInput.current!.value),
|
||||||
500,
|
500,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.currentTarget.blur();
|
event.currentTarget.blur();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<TextField.Slot>m</TextField.Slot>
|
<TextField.Slot side="right">m</TextField.Slot>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
@@ -461,7 +493,7 @@ export function Characteristics() {
|
|||||||
name="Hull traversing"
|
name="Hull traversing"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{track.dispersion.traverse * dispersionMovingCoefficient}
|
{track.dispersion.traverse * dispersionHullTraverseCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={3}
|
decimals={3}
|
||||||
@@ -470,7 +502,7 @@ export function Characteristics() {
|
|||||||
name="Turret traversing"
|
name="Turret traversing"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.dispersion.traverse * dispersionMovingCoefficient}
|
{gun.dispersion.traverse * dispersionTurretTraverseCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={3}
|
decimals={3}
|
||||||
@@ -479,7 +511,7 @@ export function Characteristics() {
|
|||||||
name="After shooting"
|
name="After shooting"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.dispersion.shot * dispersionMovingCoefficient}
|
{gun.dispersion.shot * dispersionAfterShotCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={1}
|
decimals={1}
|
||||||
@@ -489,7 +521,7 @@ export function Characteristics() {
|
|||||||
unit="scalar"
|
unit="scalar"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.dispersion.damaged * dispersionMovingCoefficient}
|
{gun.dispersion.damaged * dispersionDamagedCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<Info name="Gun flexibility" unit="°" />
|
<Info name="Gun flexibility" unit="°" />
|
||||||
<InfoWithDelta decimals={1} indent name="Depression">
|
<InfoWithDelta decimals={1} indent name="Depression">
|
||||||
@@ -637,7 +669,24 @@ export function Characteristics() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Heading size="5">Crew training</Heading>
|
<Flex gap="2" align="center">
|
||||||
|
<Heading size="5">Crew training</Heading>
|
||||||
|
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<IconButton variant="ghost">
|
||||||
|
<InfoCircledIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Content>
|
||||||
|
<Flex gap="1" align="center">
|
||||||
|
<AccessibilityIcon />
|
||||||
|
<Text>represents the other roles a crew member can take.</Text>
|
||||||
|
</Flex>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{tank.crew.map((member) => {
|
{tank.crew.map((member) => {
|
||||||
const count = member.count ?? 1;
|
const count = member.count ?? 1;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTran
|
|||||||
import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose';
|
import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose';
|
||||||
import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon';
|
import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon';
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
|
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
||||||
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
||||||
import mutateTankopediaPersistent, {
|
import mutateTankopediaPersistent, {
|
||||||
@@ -33,7 +34,6 @@ export function TankSandbox() {
|
|||||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
const awaitedModelDefinitions = use(modelDefinitions);
|
const awaitedModelDefinitions = use(modelDefinitions);
|
||||||
const canvasWrapper = useRef<HTMLDivElement>(null);
|
const canvasWrapper = useRef<HTMLDivElement>(null);
|
||||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
|
||||||
const mode = useTankopediaPersistent((state) => state.mode);
|
const mode = useTankopediaPersistent((state) => state.mode);
|
||||||
const protagonist = useDuel((state) => state.protagonist!);
|
const protagonist = useDuel((state) => state.protagonist!);
|
||||||
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
|
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
|
||||||
@@ -43,6 +43,7 @@ export function TankSandbox() {
|
|||||||
const wideFormat = useWideFormat();
|
const wideFormat = useWideFormat();
|
||||||
const [loadModel, setLoadModel] = useState(wideFormat || !isMobile);
|
const [loadModel, setLoadModel] = useState(wideFormat || !isMobile);
|
||||||
const duel = useDuel();
|
const duel = useDuel();
|
||||||
|
const isFullScreen = useFullScreen();
|
||||||
|
|
||||||
function handlePointerDown() {
|
function handlePointerDown() {
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
@@ -59,10 +60,6 @@ export function TankSandbox() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleFullScreenChange() {
|
|
||||||
setIsFullScreen(document.fullscreenElement !== null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePoseEvent(pose: Pose) {
|
function handlePoseEvent(pose: Pose) {
|
||||||
switch (pose) {
|
switch (pose) {
|
||||||
case Pose.HullDown: {
|
case Pose.HullDown: {
|
||||||
@@ -121,10 +118,8 @@ export function TankSandbox() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
poseEvent.on(handlePoseEvent);
|
poseEvent.on(handlePoseEvent);
|
||||||
document.addEventListener('fullscreenchange', handleFullScreenChange);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('fullscreenchange', handleFullScreenChange);
|
|
||||||
poseEvent.off(handlePoseEvent);
|
poseEvent.off(handlePoseEvent);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -145,8 +140,13 @@ export function TankSandbox() {
|
|||||||
<Theme radius={isFullScreen ? 'none' : undefined}>
|
<Theme radius={isFullScreen ? 'none' : undefined}>
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: isFullScreen ? 'fixed' : 'relative',
|
||||||
border: isFullScreen ? 'none' : 'unset',
|
border: isFullScreen ? 'none' : 'unset',
|
||||||
|
width: isFullScreen ? '100vw' : 'unset',
|
||||||
|
height: isFullScreen ? 'calc(100vh)' : 'unset',
|
||||||
|
top: isFullScreen ? 0 : 'unset',
|
||||||
|
left: isFullScreen ? 0 : 'unset',
|
||||||
|
zIndex: isFullScreen ? 2 : 'unset',
|
||||||
}}
|
}}
|
||||||
ref={canvasWrapper}
|
ref={canvasWrapper}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface OptionsProps {
|
|||||||
canvasWrapper: RefObject<HTMLElement>;
|
canvasWrapper: RefObject<HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
export function Options({ isFullScreen }: OptionsProps) {
|
||||||
const showGrid = useTankopediaPersistent(
|
const showGrid = useTankopediaPersistent(
|
||||||
(state) => state.model.visual.showGrid,
|
(state) => state.model.visual.showGrid,
|
||||||
);
|
);
|
||||||
@@ -50,136 +50,132 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
|||||||
right: 18,
|
right: 18,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isFullScreen && (
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Trigger>
|
<Button variant="ghost">
|
||||||
<Button variant="ghost">
|
<EyeOpenIcon />
|
||||||
<EyeOpenIcon />
|
</Button>
|
||||||
</Button>
|
</DropdownMenu.Trigger>
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.HullDown)}>
|
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.HullDown)}>
|
||||||
Hull down
|
Hull down
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.FaceHug)}>
|
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.FaceHug)}>
|
||||||
Face hug
|
Face hug
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.Default)}>
|
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.Default)}>
|
||||||
Default
|
Default
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
)}
|
|
||||||
|
|
||||||
{!isFullScreen && (
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Trigger>
|
<Button variant="ghost">
|
||||||
<Button variant="ghost">
|
<GearIcon />
|
||||||
<GearIcon />
|
</Button>
|
||||||
</Button>
|
</DropdownMenu.Trigger>
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
<DropdownMenu.Label>Armor</DropdownMenu.Label>
|
<DropdownMenu.Label>Armor</DropdownMenu.Label>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={showSpacedArmor}
|
checked={showSpacedArmor}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.showSpacedArmor = checked;
|
draft.model.visual.showSpacedArmor = checked;
|
||||||
});
|
});
|
||||||
mutateTankopediaTemporary((draft) => {
|
mutateTankopediaTemporary((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Spaced armor
|
Spaced armor
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={greenPenetration}
|
checked={greenPenetration}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.greenPenetration = checked;
|
draft.model.visual.greenPenetration = checked;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Green penetration
|
Green penetration
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={wireframe}
|
checked={wireframe}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.wireframe = checked;
|
draft.model.visual.wireframe = checked;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Wireframe
|
Wireframe
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={opaque}
|
checked={opaque}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.opaque = checked;
|
draft.model.visual.opaque = checked;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Opaque
|
Opaque
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
<DropdownMenu.Label>Environment</DropdownMenu.Label>
|
<DropdownMenu.Label>Environment</DropdownMenu.Label>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={showGrid}
|
checked={showGrid}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.showGrid = checked;
|
draft.model.visual.showGrid = checked;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Show grid
|
Show grid
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={showEnvironment}
|
checked={showEnvironment}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.showEnvironment = checked;
|
draft.model.visual.showEnvironment = checked;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View environment
|
View environment
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger>Lighting</DropdownMenu.SubTrigger>
|
<DropdownMenu.SubTrigger>Lighting</DropdownMenu.SubTrigger>
|
||||||
|
|
||||||
<DropdownMenu.SubContent>
|
<DropdownMenu.SubContent>
|
||||||
<DropdownMenu.RadioGroup value={environment}>
|
<DropdownMenu.RadioGroup value={environment}>
|
||||||
{ENVIRONMENTS.map((environment) => (
|
{ENVIRONMENTS.map((environment) => (
|
||||||
<DropdownMenu.RadioItem
|
<DropdownMenu.RadioItem
|
||||||
key={environment}
|
key={environment}
|
||||||
value={environment}
|
value={environment}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.environment = environment;
|
draft.model.visual.environment = environment;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{environment[0].toUpperCase() + environment.slice(1)}
|
{environment[0].toUpperCase() + environment.slice(1)}
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
</DropdownMenu.Sub>
|
</DropdownMenu.Sub>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
)}
|
|
||||||
|
|
||||||
{fullScreenAvailable && (
|
{fullScreenAvailable && (
|
||||||
<Button
|
<Button
|
||||||
@@ -187,7 +183,7 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isFullScreen) {
|
if (isFullScreen) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
} else canvasWrapper.current?.requestFullscreen();
|
} else document.body.requestFullscreen();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
|
{isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
|
||||||
|
|||||||
@@ -54,7 +54,39 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
textAlign: 'right',
|
||||||
}}
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const value = Number(turretYawInput.current!.value);
|
||||||
|
if (isNaN(value)) {
|
||||||
|
turretYawInput.current!.value = radToDeg(protagonist.yaw).toFixed(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
|
protagonist.pitch,
|
||||||
|
degToRad(value),
|
||||||
|
gunModelDefinition.pitch,
|
||||||
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
|
);
|
||||||
|
modelTransformEvent.emit({ pitch, yaw });
|
||||||
|
mutateDuel((state) => {
|
||||||
|
state.protagonist!.pitch = pitch;
|
||||||
|
state.protagonist!.yaw = yaw;
|
||||||
|
});
|
||||||
|
turretYawInput.current!.value = radToDeg(protagonist.yaw).toFixed(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
turretYawInput.current?.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={() => turretYawInput.current?.focus()}
|
||||||
|
ref={turretYawInput}
|
||||||
>
|
>
|
||||||
<TextField.Slot
|
<TextField.Slot
|
||||||
style={{
|
style={{
|
||||||
@@ -63,40 +95,45 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
>
|
>
|
||||||
Yaw
|
Yaw
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
<TextField.Input
|
|
||||||
onBlur={() => {
|
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
|
||||||
protagonist.pitch,
|
|
||||||
degToRad(Number(turretYawInput.current!.value)),
|
|
||||||
gunModelDefinition.pitch,
|
|
||||||
turretModelDefinition.yaw,
|
|
||||||
hasImprovedVerticalStabilizer,
|
|
||||||
);
|
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
|
||||||
mutateDuel((state) => {
|
|
||||||
state.protagonist!.pitch = pitch;
|
|
||||||
state.protagonist!.yaw = yaw;
|
|
||||||
});
|
|
||||||
turretYawInput.current!.value = radToDeg(protagonist.yaw).toFixed(
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
turretYawInput.current?.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFocus={() => turretYawInput.current?.focus()}
|
|
||||||
ref={turretYawInput}
|
|
||||||
style={{ textAlign: 'right' }}
|
|
||||||
/>
|
|
||||||
<TextField.Slot style={{ userSelect: 'none' }}>°</TextField.Slot>
|
<TextField.Slot style={{ userSelect: 'none' }}>°</TextField.Slot>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
textAlign: 'right',
|
||||||
}}
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const value = Number(gunPitchInput.current!.value);
|
||||||
|
if (isNaN(value)) {
|
||||||
|
gunPitchInput.current!.value = (
|
||||||
|
-radToDeg(protagonist.pitch) + initialGunPitch
|
||||||
|
).toFixed(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
|
degToRad(-value + initialGunPitch),
|
||||||
|
protagonist.yaw,
|
||||||
|
gunModelDefinition.pitch,
|
||||||
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
|
);
|
||||||
|
modelTransformEvent.emit({ pitch, yaw });
|
||||||
|
mutateDuel((state) => {
|
||||||
|
state.protagonist!.pitch = pitch;
|
||||||
|
state.protagonist!.yaw = yaw;
|
||||||
|
});
|
||||||
|
gunPitchInput.current!.value = (
|
||||||
|
-radToDeg(protagonist.pitch) + initialGunPitch
|
||||||
|
).toFixed(1);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
gunPitchInput.current?.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={() => gunPitchInput.current?.focus()}
|
||||||
|
ref={gunPitchInput}
|
||||||
>
|
>
|
||||||
<TextField.Slot
|
<TextField.Slot
|
||||||
style={{
|
style={{
|
||||||
@@ -105,35 +142,6 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
>
|
>
|
||||||
Pitch
|
Pitch
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
<TextField.Input
|
|
||||||
onBlur={() => {
|
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
|
||||||
degToRad(
|
|
||||||
-Number(gunPitchInput.current!.value) + initialGunPitch,
|
|
||||||
),
|
|
||||||
protagonist.yaw,
|
|
||||||
gunModelDefinition.pitch,
|
|
||||||
turretModelDefinition.yaw,
|
|
||||||
hasImprovedVerticalStabilizer,
|
|
||||||
);
|
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
|
||||||
mutateDuel((state) => {
|
|
||||||
state.protagonist!.pitch = pitch;
|
|
||||||
state.protagonist!.yaw = yaw;
|
|
||||||
});
|
|
||||||
gunPitchInput.current!.value = (
|
|
||||||
-radToDeg(protagonist.pitch) + initialGunPitch
|
|
||||||
).toFixed(1);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
gunPitchInput.current?.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFocus={() => gunPitchInput.current?.focus()}
|
|
||||||
ref={gunPitchInput}
|
|
||||||
style={{ textAlign: 'right' }}
|
|
||||||
/>
|
|
||||||
<TextField.Slot
|
<TextField.Slot
|
||||||
style={{
|
style={{
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import { mutateTankopediaTemporary } from '../../../../stores/tankopedia';
|
|||||||
import { AntagonistBar } from './components/AntagonistBar';
|
import { AntagonistBar } from './components/AntagonistBar';
|
||||||
import { Characteristics } from './components/Characteristics';
|
import { Characteristics } from './components/Characteristics';
|
||||||
import { Consumables } from './components/Characteristics/components/Consumables';
|
import { Consumables } from './components/Characteristics/components/Consumables';
|
||||||
import { Equipments } from './components/Characteristics/components/Equipments';
|
import { Equipment } from './components/Characteristics/components/Equipment';
|
||||||
import { Miscellaneous } from './components/Characteristics/components/Miscellaneous';
|
import { Miscellaneous } from './components/Characteristics/components/Miscellaneous';
|
||||||
import { Modules } from './components/Characteristics/components/Modules';
|
import { Modules } from './components/Characteristics/components/Modules';
|
||||||
import { Provisions } from './components/Characteristics/components/Provisions';
|
import { Provisions } from './components/Characteristics/components/Provisions';
|
||||||
|
import { Skills } from './components/Characteristics/components/Skills';
|
||||||
import { TankSandbox } from './components/Model/TankSandbox';
|
import { TankSandbox } from './components/Model/TankSandbox';
|
||||||
import { Title } from './components/Title';
|
import { Title } from './components/Title';
|
||||||
|
|
||||||
@@ -63,10 +64,8 @@ export default function Page({ params }: { params: { id: string } }) {
|
|||||||
};
|
};
|
||||||
}, [initialId, assigned]);
|
}, [initialId, assigned]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="purple" size="double">
|
<PageWrapper color="purple" size={1600}>
|
||||||
{assigned && (
|
{assigned && (
|
||||||
<>
|
<>
|
||||||
<Title />
|
<Title />
|
||||||
@@ -79,7 +78,7 @@ export default function Page({ params }: { params: { id: string } }) {
|
|||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
gap="4"
|
gap="8"
|
||||||
direction="column"
|
direction="column"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -88,15 +87,22 @@ export default function Page({ params }: { params: { id: string } }) {
|
|||||||
position: wideFormat ? 'sticky' : undefined,
|
position: wideFormat ? 'sticky' : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TankSandbox />
|
<Flex gap="4" direction="column">
|
||||||
<AntagonistBar />
|
<TankSandbox />
|
||||||
|
<AntagonistBar />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Flex direction="column" gap="4">
|
<Flex gap="5" wrap="wrap">
|
||||||
<Modules />
|
<Modules />
|
||||||
<Provisions />
|
<Provisions />
|
||||||
<Equipments />
|
|
||||||
<Miscellaneous />
|
|
||||||
<Consumables />
|
<Consumables />
|
||||||
|
|
||||||
|
<Flex gap="5" wrap="wrap">
|
||||||
|
<Skills />
|
||||||
|
<Equipment />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Miscellaneous />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
|||||||
@@ -32,32 +32,31 @@ export function PageTurner({ searchedList, tanksPerPage }: PageTurnerProps) {
|
|||||||
>
|
>
|
||||||
<CaretLeftIcon />
|
<CaretLeftIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<TextField.Root>
|
<TextField.Root
|
||||||
|
defaultValue={1}
|
||||||
|
type="number"
|
||||||
|
ref={pageInput}
|
||||||
|
min={1}
|
||||||
|
max={Math.floor(searchedList.length / tanksPerPage) + 1}
|
||||||
|
style={{ textAlign: 'center' }}
|
||||||
|
onBlur={(event) => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.filters.page = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
Math.floor(searchedList.length / tanksPerPage),
|
||||||
|
event.target.valueAsNumber - 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
(event.target as HTMLInputElement).blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TextField.Slot>Page</TextField.Slot>
|
<TextField.Slot>Page</TextField.Slot>
|
||||||
<TextField.Input
|
|
||||||
defaultValue={1}
|
|
||||||
type="number"
|
|
||||||
ref={pageInput}
|
|
||||||
min={1}
|
|
||||||
max={Math.floor(searchedList.length / tanksPerPage) + 1}
|
|
||||||
style={{ width: 64, textAlign: 'center' }}
|
|
||||||
onBlur={(event) => {
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.filters.page = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(
|
|
||||||
Math.floor(searchedList.length / tanksPerPage),
|
|
||||||
event.target.valueAsNumber - 1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
(event.target as HTMLInputElement).blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
out of {Math.floor(searchedList.length / tanksPerPage) + 1}
|
out of {Math.floor(searchedList.length / tanksPerPage) + 1}
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
|
import Link from 'next/link';
|
||||||
import { use, useEffect, useMemo, useRef, useState } from 'react';
|
import { use, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { TreeType } from '../../../../../components/Tanks';
|
import { TreeType } from '../../../../../components/Tanks';
|
||||||
import { TANK_CLASSES } from '../../../../../components/Tanks/components/Item/constants';
|
import { TANK_CLASSES } from '../../../../../components/Tanks/components/Item/constants';
|
||||||
@@ -151,7 +152,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.size':
|
case 'survivability.volume':
|
||||||
sorted = filtered.sort((a, b) => {
|
sorted = filtered.sort((a, b) => {
|
||||||
const aTankModelDefinition = awaitedModelDefinitions[a.id];
|
const aTankModelDefinition = awaitedModelDefinitions[a.id];
|
||||||
const bTankModelDefinition = awaitedModelDefinitions[b.id];
|
const bTankModelDefinition = awaitedModelDefinitions[b.id];
|
||||||
@@ -178,6 +179,33 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'survivability.length':
|
||||||
|
sorted = filtered.sort((a, b) => {
|
||||||
|
const aTankModelDefinition = awaitedModelDefinitions[a.id];
|
||||||
|
const bTankModelDefinition = awaitedModelDefinitions[b.id];
|
||||||
|
const aTurretModelDefinition =
|
||||||
|
aTankModelDefinition.turrets[a.turrets.at(-1)!.id];
|
||||||
|
const bTurretModelDefinition =
|
||||||
|
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
|
||||||
|
const aSize = normalizeBoundingBox(
|
||||||
|
unionBoundingBox(
|
||||||
|
aTankModelDefinition.boundingBox,
|
||||||
|
aTurretModelDefinition.boundingBox,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const bSize = normalizeBoundingBox(
|
||||||
|
unionBoundingBox(
|
||||||
|
bTankModelDefinition.boundingBox,
|
||||||
|
bTurretModelDefinition.boundingBox,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const aLength = Math.max(aSize[0], aSize[1], aSize[2]);
|
||||||
|
const bLength = Math.max(bSize[0], bSize[1], bSize[2]);
|
||||||
|
|
||||||
|
return aLength - bLength;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
case 'fire.dpm':
|
case 'fire.dpm':
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
@@ -392,27 +420,26 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
<>
|
<>
|
||||||
<Flex direction="column" gap="4">
|
<Flex direction="column" gap="4">
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<TextField.Root style={{ flex: 1 }}>
|
<TextField.Root
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
|
|
||||||
<TextField.Input
|
|
||||||
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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
@@ -699,14 +726,24 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
Camouflage shooting
|
Camouflage shooting
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={sort.by === 'survivability.size'}
|
checked={sort.by === 'survivability.volume'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.sort.by = 'survivability.size';
|
draft.sort.by = 'survivability.volume';
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Size
|
Volume
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={sort.by === 'survivability.length'}
|
||||||
|
onClick={() => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.sort.by = 'survivability.length';
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Length
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
</DropdownMenu.Sub>
|
</DropdownMenu.Sub>
|
||||||
@@ -771,117 +808,123 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
<Flex wrap="wrap" gap="2" justify={compact ? 'between' : 'center'}>
|
<Flex wrap="wrap" gap="2" justify={compact ? 'between' : 'center'}>
|
||||||
{!compact &&
|
{!compact &&
|
||||||
searchResultsPageSlice.map((tank) => (
|
searchResultsPageSlice.map((tank) => (
|
||||||
<Card
|
<Link
|
||||||
key={tank.id}
|
href={`/tools/tankopedia/${tank.id}`}
|
||||||
onClick={() => onSelect(tank)}
|
|
||||||
className={styles.listing}
|
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cursor: 'pointer',
|
minWidth: 256,
|
||||||
height: 64,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Inset
|
<Card
|
||||||
key={tank.id}
|
key={tank.id}
|
||||||
|
className={styles.listing}
|
||||||
style={{
|
style={{
|
||||||
minWidth: 256,
|
cursor: 'pointer',
|
||||||
minHeight: 64,
|
height: 64,
|
||||||
position: 'relative',
|
width: '100%',
|
||||||
display: 'flex',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<Inset
|
||||||
className={styles.flag}
|
key={tank.id}
|
||||||
src={asset(`flags/scratched/${tank.nation}.webp`)}
|
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
minHeight: 64,
|
||||||
height: '100%',
|
position: 'relative',
|
||||||
objectFit: 'contain',
|
display: 'flex',
|
||||||
position: 'absolute',
|
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={styles.listingShadow}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
position: 'absolute',
|
|
||||||
transition: `box-shadow ${theme.durations.regular}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 256,
|
|
||||||
height: 128,
|
|
||||||
}}
|
|
||||||
className={styles.listingImage}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={tankIcon(tank.id)}
|
className={styles.flag}
|
||||||
|
src={asset(`flags/scratched/${tank.nation}.webp`)}
|
||||||
style={{
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
objectPosition: 'left center',
|
position: 'absolute',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Flex
|
<div
|
||||||
align="center"
|
className={styles.listingShadow}
|
||||||
justify="between"
|
style={{
|
||||||
className={styles.listingLabel}
|
width: '100%',
|
||||||
style={{
|
height: '100%',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 8,
|
transition: `box-shadow ${theme.durations.regular}`,
|
||||||
padding: '0 12px',
|
}}
|
||||||
width: '100%',
|
/>
|
||||||
boxSizing: 'border-box',
|
|
||||||
}}
|
<div
|
||||||
>
|
style={{
|
||||||
<Flex align="center" justify="center" gap="1">
|
width: 256,
|
||||||
|
height: 128,
|
||||||
|
}}
|
||||||
|
className={styles.listingImage}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={
|
src={tankIcon(tank.id)}
|
||||||
(tank.treeType === 'collector'
|
|
||||||
? TANK_ICONS_COLLECTOR
|
|
||||||
: tank.treeType === 'premium'
|
|
||||||
? TANK_ICONS_PREMIUM
|
|
||||||
: TANK_ICONS)[tank.class]
|
|
||||||
}
|
|
||||||
style={{ width: '1em', height: '1em' }}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
size="4"
|
|
||||||
color={
|
|
||||||
tank.treeType === 'collector'
|
|
||||||
? 'blue'
|
|
||||||
: tank.treeType === 'premium'
|
|
||||||
? 'amber'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
weight="medium"
|
|
||||||
style={{
|
style={{
|
||||||
color:
|
objectFit: 'contain',
|
||||||
tank.treeType === 'researchable'
|
objectPosition: 'left center',
|
||||||
? slateDark.slate12
|
|
||||||
: undefined,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
maxWidth: 160,
|
|
||||||
display: 'block',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{tank.name}
|
</div>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="between"
|
||||||
|
className={styles.listingLabel}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 8,
|
||||||
|
padding: '0 12px',
|
||||||
|
width: '100%',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex align="center" justify="center" gap="1">
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
(tank.treeType === 'collector'
|
||||||
|
? TANK_ICONS_COLLECTOR
|
||||||
|
: tank.treeType === 'premium'
|
||||||
|
? TANK_ICONS_PREMIUM
|
||||||
|
: TANK_ICONS)[tank.class]
|
||||||
|
}
|
||||||
|
style={{ width: '1em', height: '1em' }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
size="4"
|
||||||
|
color={
|
||||||
|
tank.treeType === 'collector'
|
||||||
|
? 'blue'
|
||||||
|
: tank.treeType === 'premium'
|
||||||
|
? 'amber'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
weight="medium"
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
tank.treeType === 'researchable'
|
||||||
|
? slateDark.slate12
|
||||||
|
: undefined,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
maxWidth: 160,
|
||||||
|
display: 'block',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tank.name}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Text size="4" color="gray" highContrast={false}>
|
||||||
|
{TIER_ROMAN_NUMERALS[tank.tier]}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Inset>
|
||||||
<Text size="4" color="gray" highContrast={false}>
|
</Card>
|
||||||
{TIER_ROMAN_NUMERALS[tank.tier]}
|
</Link>
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</Inset>
|
|
||||||
</Card>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{compact && (
|
{compact && (
|
||||||
|
|||||||
@@ -74,12 +74,6 @@ export const characteristics = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const configurationChild = style({
|
|
||||||
display: 'flex',
|
|
||||||
gap: 8,
|
|
||||||
flexDirection: 'column',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const configuration = style({
|
export const configuration = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function Page() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper size="double" color="purple">
|
<PageWrapper size={1200} color="purple">
|
||||||
<TankSearch
|
<TankSearch
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
router.push(`./tankopedia/${tank.id}`);
|
router.push(`./tankopedia/${tank.id}`);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Glow } from '../components/AllStatsOverview/components/HeroStat/compone
|
|||||||
import CommandWrapper from '../components/CommandWrapper';
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitzLink';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
@@ -29,11 +29,11 @@ export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
|
|||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const { t, translate } = translator(interaction.locale);
|
const { t, translate } = translator(interaction.locale);
|
||||||
const { id, region } = await resolvePlayerFromCommand(interaction);
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
const discordId = parseInt(interaction.user.id);
|
const discordId = BigInt(interaction.user.id);
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
|
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
|
||||||
|
|
||||||
await linkBlitzAndDiscord(discordId, region, id);
|
await linkBlitzAndDiscord(discordId, id);
|
||||||
|
|
||||||
if (interaction.guildId === discord.sklld_guild_id) {
|
if (interaction.guildId === discord.sklld_guild_id) {
|
||||||
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const CoreArmorScene = memo(() => {
|
|||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null);
|
||||||
const initialTankopediaState = useTankopediaPersistent.getState();
|
const initialTankopediaState = useTankopediaPersistent.getState();
|
||||||
|
const protagonist = useDuel((draft) => draft.protagonist!);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!modelDefinitions) return;
|
if (!modelDefinitions) return;
|
||||||
@@ -88,7 +89,7 @@ export const CoreArmorScene = memo(() => {
|
|||||||
turretContainer.current?.rotation.copy(turretRotation);
|
turretContainer.current?.rotation.copy(turretRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModelTransform(useDuel.getState().protagonist!);
|
handleModelTransform(protagonist);
|
||||||
modelTransformEvent.on(handleModelTransform);
|
modelTransformEvent.on(handleModelTransform);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export function CoreArmorSceneComponent({
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
};
|
};
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
useFrame(({ gl, camera }) => {
|
useFrame(({ gl, camera }) => {
|
||||||
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null);
|
||||||
const initialTankopediaState = useTankopediaPersistent.getState();
|
const initialTankopediaState = useTankopediaPersistent.getState();
|
||||||
|
const protagonist = useDuel((draft) => draft.protagonist!);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!modelDefinitions) return;
|
if (!modelDefinitions) return;
|
||||||
@@ -99,7 +100,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
turretContainer.current?.rotation.copy(turretRotation);
|
turretContainer.current?.rotation.copy(turretRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModelTransform(useDuel.getState().protagonist!);
|
handleModelTransform(protagonist);
|
||||||
modelTransformEvent.on(handleModelTransform);
|
modelTransformEvent.on(handleModelTransform);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@ export function SpacedArmorSubExternal({
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
};
|
};
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@ export function SpacedArmorSubSpaced({
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
};
|
};
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -11,23 +11,28 @@ export interface TankComponentButtonProps
|
|||||||
discriminator?: ReactNode;
|
discriminator?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
banner?: string;
|
banner?: string;
|
||||||
|
special?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TankComponentButton({
|
export function TankComponentButton({
|
||||||
|
variant,
|
||||||
selected,
|
selected,
|
||||||
first = false,
|
first = false,
|
||||||
last = false,
|
last = false,
|
||||||
rowChild,
|
rowChild,
|
||||||
discriminator,
|
discriminator,
|
||||||
|
special,
|
||||||
children,
|
children,
|
||||||
banner,
|
banner,
|
||||||
|
disabled,
|
||||||
|
style,
|
||||||
...props
|
...props
|
||||||
}: TankComponentButtonProps) {
|
}: TankComponentButtonProps) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
radius="medium"
|
radius="medium"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? (special ? 'amber' : undefined) : 'gray'}
|
||||||
variant={selected ? 'surface' : 'soft'}
|
variant={variant ?? (selected ? 'surface' : 'soft')}
|
||||||
style={{
|
style={{
|
||||||
padding: 0,
|
padding: 0,
|
||||||
minWidth: 48,
|
minWidth: 48,
|
||||||
@@ -39,7 +44,11 @@ export function TankComponentButton({
|
|||||||
borderBottomRightRadius: last ? undefined : 0,
|
borderBottomRightRadius: last ? undefined : 0,
|
||||||
margin: rowChild ? -0.5 : 'unset',
|
margin: rowChild ? -0.5 : 'unset',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
cursor: 'unset',
|
||||||
|
pointerEvents: variant === 'ghost' ? 'none' : undefined,
|
||||||
|
...style,
|
||||||
}}
|
}}
|
||||||
|
disabled={variant === 'ghost' || disabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{banner !== undefined && (
|
{banner !== undefined && (
|
||||||
@@ -56,23 +65,23 @@ export function TankComponentButton({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
{discriminator !== undefined && (
|
{discriminator !== undefined && (
|
||||||
<Text
|
<Text
|
||||||
size="1"
|
size="1"
|
||||||
style={{
|
style={{
|
||||||
textShadow: '0 0 4px black',
|
textShadow: '0 0 4px black',
|
||||||
zIndex: 1,
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '50%',
|
top: '50%',
|
||||||
right: 8,
|
right: 8,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
}}
|
}}
|
||||||
|
color={variant === 'ghost' ? 'gray' : undefined}
|
||||||
>
|
>
|
||||||
{discriminator}
|
{discriminator}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{children}
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import { DiscordLogoIcon } from '@radix-ui/react-icons';
|
import { DiscordLogoIcon } from '@radix-ui/react-icons';
|
||||||
import { Flex, Text, Theme } from '@radix-ui/themes';
|
import { Flex, Text, Theme } from '@radix-ui/themes';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useFullScreen } from '../../hooks/useFullScreen';
|
||||||
import { BlitzkriegWormWide } from '../../icons/BlitzkriegWormWide';
|
import { BlitzkriegWormWide } from '../../icons/BlitzkriegWormWide';
|
||||||
|
import { PatreonIcon } from '../../icons/Patreon';
|
||||||
import { theme } from '../../stitches.config';
|
import { theme } from '../../stitches.config';
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
|
const isFullScreen = useFullScreen();
|
||||||
|
|
||||||
|
if (isFullScreen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Theme radius="none">
|
<Theme radius="none">
|
||||||
<div
|
<div
|
||||||
@@ -51,9 +57,10 @@ export default function Navbar() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex justify="center" align="center" gap="2">
|
<Flex justify="center" align="center" gap="4">
|
||||||
<Link
|
<Link
|
||||||
href="https://discord.gg/nDt7AjGJQH"
|
href="https://discord.gg/nDt7AjGJQH"
|
||||||
|
target="_blank"
|
||||||
style={{
|
style={{
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -61,7 +68,40 @@ export default function Navbar() {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DiscordLogoIcon />
|
<DiscordLogoIcon width={16} height={16} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://ko-fi.com/tresabhi"
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://i.imgur.com/QdqgdP2.png"
|
||||||
|
style={{
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://www.patreon.com/tresabhi"
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PatreonIcon width={14} height={14} />
|
||||||
</Link>
|
</Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Flex, Theme } from '@radix-ui/themes';
|
import { Flex, FlexProps, Theme } from '@radix-ui/themes';
|
||||||
import { ComponentProps } from 'react';
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
interface PageWrapperProps extends ComponentProps<typeof Flex> {
|
type PageWrapperProps = FlexProps & {
|
||||||
color?: ComponentProps<typeof Theme>['accentColor'];
|
color?: ComponentProps<typeof Theme>['accentColor'];
|
||||||
size?: 'regular' | 'wide' | 'double';
|
size?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function PageWrapper({
|
export default function PageWrapper({
|
||||||
style,
|
style,
|
||||||
color,
|
color,
|
||||||
size = 'regular',
|
size = 800,
|
||||||
...props
|
...props
|
||||||
}: PageWrapperProps) {
|
}: PageWrapperProps) {
|
||||||
return (
|
return (
|
||||||
@@ -19,7 +19,7 @@ export default function PageWrapper({
|
|||||||
gap="4"
|
gap="4"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: size === 'regular' ? 800 : size === 'double' ? 1200 : 960,
|
maxWidth: size,
|
||||||
margin: 'auto',
|
margin: 'auto',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
|
||||||
|
export function idToRange(id: number): Region {
|
||||||
|
if (id >= 42e8) {
|
||||||
|
throw new Error('bot id');
|
||||||
|
} else if (id >= 31e8) {
|
||||||
|
throw new Error('china id');
|
||||||
|
} else if (id >= 20e8) {
|
||||||
|
return 'asia';
|
||||||
|
} else if (id >= 10e8) {
|
||||||
|
return 'com';
|
||||||
|
} else if (id >= 5e8) {
|
||||||
|
return 'eu';
|
||||||
|
} else if (id >= 0) {
|
||||||
|
throw new Error('ru id');
|
||||||
|
} else {
|
||||||
|
throw new Error(`id ${id} is out of range`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Region } from '../../constants/regions';
|
|
||||||
import { discordBlitzDB } from './discordBlitzDB';
|
|
||||||
|
|
||||||
export function linkBlitzAndDiscord(
|
|
||||||
discord: number,
|
|
||||||
region: Region,
|
|
||||||
blitz: number,
|
|
||||||
) {
|
|
||||||
return discordBlitzDB.discord_blitz.upsert({
|
|
||||||
where: { discord },
|
|
||||||
update: { region, blitz },
|
|
||||||
create: { discord, region, blitz },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBlitzFromDiscord(discord: number) {
|
|
||||||
return discordBlitzDB.discord_blitz.findUnique({
|
|
||||||
where: { discord },
|
|
||||||
select: { region: true, blitz: true },
|
|
||||||
}) as Promise<null | { region: Region; blitz: number }>;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
export const discordBlitzDB = new PrismaClient();
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { usersDatabase } from '../../databases/users';
|
||||||
|
import { idToRange } from '../blitz/idToRegion';
|
||||||
|
|
||||||
|
export async function flagUserActivity(blitz: number) {
|
||||||
|
await usersDatabase.users.upsert({
|
||||||
|
where: { blitz },
|
||||||
|
update: {},
|
||||||
|
create: { blitz },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function linkBlitzAndDiscord(discord: bigint, blitz: number) {
|
||||||
|
await usersDatabase.discordUsers.upsert({
|
||||||
|
where: { discord },
|
||||||
|
update: { blitz },
|
||||||
|
create: { discord, blitz },
|
||||||
|
});
|
||||||
|
await flagUserActivity(blitz);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBlitzFromDiscord(discord: bigint) {
|
||||||
|
const unique = await usersDatabase.discordUsers.findUnique({
|
||||||
|
where: { discord },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unique === null) return null;
|
||||||
|
|
||||||
|
await flagUserActivity(unique.blitz);
|
||||||
|
return { id: unique.blitz, region: idToRange(unique.blitz) };
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { usersDatabase } from '../../databases/users';
|
||||||
import { client } from '../discord/client';
|
import { client } from '../discord/client';
|
||||||
import { discordBlitzDB } from './discordBlitzDB';
|
|
||||||
|
|
||||||
const EXIT_EVENTS = [
|
const EXIT_EVENTS = [
|
||||||
'beforeExit',
|
'beforeExit',
|
||||||
@@ -13,7 +13,7 @@ const EXIT_EVENTS = [
|
|||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
console.log('Cleaning up...');
|
console.log('Cleaning up...');
|
||||||
|
|
||||||
await Promise.all([discordBlitzDB.$disconnect(), client.destroy()]);
|
await Promise.all([usersDatabase.$disconnect(), client.destroy()]);
|
||||||
|
|
||||||
console.log('Gracefully exiting...');
|
console.log('Gracefully exiting...');
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ config();
|
|||||||
const SECRET_KEYS = [
|
const SECRET_KEYS = [
|
||||||
'DISCORD_TOKEN',
|
'DISCORD_TOKEN',
|
||||||
'GH_TOKEN',
|
'GH_TOKEN',
|
||||||
'DISCORD_BLITZ_DB',
|
'DATABASE_URL',
|
||||||
'WOTB_DLC_CDN',
|
'WOTB_DLC_CDN',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TankClass } from '../../components/Tanks';
|
||||||
|
import { asset } from './asset';
|
||||||
|
import { fetchCdonLz4 } from './fetchCdonLz4';
|
||||||
|
|
||||||
|
export interface SkillDefinitions {
|
||||||
|
classes: Record<TankClass, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const skillDefinitions = fetchCdonLz4<SkillDefinitions>(
|
||||||
|
asset('definitions/skills.cdon.lz4'),
|
||||||
|
);
|
||||||
@@ -55,6 +55,7 @@ export interface TankDefinition {
|
|||||||
export interface TrackDefinition {
|
export interface TrackDefinition {
|
||||||
id: number;
|
id: number;
|
||||||
tier: Tier;
|
tier: Tier;
|
||||||
|
name: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
traverseSpeed: number;
|
traverseSpeed: number;
|
||||||
xp?: number;
|
xp?: number;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import markdownEscape from 'markdown-escape';
|
|||||||
import { Region } from '../../../constants/regions';
|
import { Region } from '../../../constants/regions';
|
||||||
import { UserError } from '../../../hooks/userError';
|
import { UserError } from '../../../hooks/userError';
|
||||||
import searchPlayersAcrossRegions from '../../blitz/searchPlayersAcrossRegions';
|
import searchPlayersAcrossRegions from '../../blitz/searchPlayersAcrossRegions';
|
||||||
import { getBlitzFromDiscord } from '../../blitzkrieg/discordBlitz';
|
import { getBlitzFromDiscord } from '../../blitzkrieg/discordBlitzLink';
|
||||||
import { translator } from '../../localization/translator';
|
import { translator } from '../../localization/translator';
|
||||||
import { serverAndIdPattern } from './constants';
|
import { serverAndIdPattern } from './constants';
|
||||||
|
|
||||||
@@ -43,12 +43,10 @@ export default async function resolvePlayerFromCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const account = await getBlitzFromDiscord(parseInt(interaction.user.id));
|
const account = await getBlitzFromDiscord(BigInt(interaction.user.id));
|
||||||
|
|
||||||
if (account) {
|
if (account) return account;
|
||||||
return { region: account.region, id: account.blitz };
|
|
||||||
} else {
|
throw new UserError(t`bot.common.errors.player_not_linked`);
|
||||||
throw new UserError(t`bot.common.errors.player_not_linked`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export const usersDatabase = new PrismaClient();
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useFullScreen() {
|
||||||
|
const [isFullScreen, setIsFullScreen] = useState(
|
||||||
|
typeof document !== 'undefined' && document.fullscreenElement !== null,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleFullScreenChange() {
|
||||||
|
setIsFullScreen(document.fullscreenElement !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('fullscreenchange', handleFullScreenChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleFullScreenChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isFullScreen;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
|
d="M436 143c-.084-60.778-47.57-110.591-103.285-128.565C263.528-7.884 172.279-4.649 106.214 26.424 26.142 64.089.988 146.596.051 228.883c-.77 67.653 6.004 245.841 106.83 247.11 74.917.948 86.072-95.279 120.737-141.623 24.662-32.972 56.417-42.285 95.507-51.929C390.309 265.865 436.097 213.011 436 143Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ export interface TankopediaPersistent {
|
|||||||
|
|
||||||
interface TankopediaTemporary {
|
interface TankopediaTemporary {
|
||||||
shot?: Shot;
|
shot?: Shot;
|
||||||
|
skills: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShotLayerBase {
|
export interface ShotLayerBase {
|
||||||
@@ -98,7 +99,8 @@ export const SORT_NAMES = {
|
|||||||
'survivability.camouflageStill': 'camouflage still',
|
'survivability.camouflageStill': 'camouflage still',
|
||||||
'survivability.camouflageMoving': 'camouflage moving',
|
'survivability.camouflageMoving': 'camouflage moving',
|
||||||
'survivability.camouflageShooting': 'camouflage shooting',
|
'survivability.camouflageShooting': 'camouflage shooting',
|
||||||
'survivability.size': 'size',
|
'survivability.volume': 'volume',
|
||||||
|
'survivability.length': 'length',
|
||||||
'fire.dpm': 'DPM',
|
'fire.dpm': 'DPM',
|
||||||
'fire.reload': 'reload',
|
'fire.reload': 'reload',
|
||||||
'fire.caliber': 'caliber',
|
'fire.caliber': 'caliber',
|
||||||
@@ -153,7 +155,9 @@ export const useTankopediaPersistent = create<TankopediaPersistent>()(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const useTankopediaTemporary = create<TankopediaTemporary>()(
|
export const useTankopediaTemporary = create<TankopediaTemporary>()(
|
||||||
subscribeWithSelector<TankopediaTemporary>(() => ({})),
|
subscribeWithSelector<TankopediaTemporary>(() => ({
|
||||||
|
skills: {},
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function mutateTankopediaPersistent(
|
export default function mutateTankopediaPersistent(
|
||||||
|
|||||||
@@ -1259,7 +1259,7 @@
|
|||||||
"@radix-ui/react-primitive" "1.0.3"
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
"@radix-ui/react-slot" "1.0.2"
|
"@radix-ui/react-slot" "1.0.2"
|
||||||
|
|
||||||
"@radix-ui/[email protected]":
|
"@radix-ui/[email protected]", "@radix-ui/react-compose-refs@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
|
||||||
integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==
|
integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==
|
||||||
@@ -1279,7 +1279,7 @@
|
|||||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
|
||||||
"@radix-ui/[email protected]":
|
"@radix-ui/[email protected]", "@radix-ui/react-context@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c"
|
||||||
integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==
|
integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==
|
||||||
@@ -1432,6 +1432,27 @@
|
|||||||
aria-hidden "^1.1.1"
|
aria-hidden "^1.1.1"
|
||||||
react-remove-scroll "2.5.5"
|
react-remove-scroll "2.5.5"
|
||||||
|
|
||||||
|
"@radix-ui/react-navigation-menu@^1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.4.tgz#654151310c3f9a29afd19fb60ddc7977e54b8a3d"
|
||||||
|
integrity sha512-Cc+seCS3PmWmjI51ufGG7zp1cAAIRqHVw7C9LOA2TZ+R4hG6rDvHcTqIsEEFLmZO3zNVH72jOOE7kKNy8W+RtA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-collection" "1.0.3"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-direction" "1.0.1"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.0.5"
|
||||||
|
"@radix-ui/react-id" "1.0.1"
|
||||||
|
"@radix-ui/react-presence" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||||
|
"@radix-ui/react-use-previous" "1.0.1"
|
||||||
|
"@radix-ui/react-visually-hidden" "1.0.3"
|
||||||
|
|
||||||
"@radix-ui/react-popover@^1.0.7":
|
"@radix-ui/react-popover@^1.0.7":
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
|
||||||
@@ -1488,7 +1509,7 @@
|
|||||||
"@radix-ui/react-compose-refs" "1.0.1"
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||||
|
|
||||||
"@radix-ui/[email protected]":
|
"@radix-ui/[email protected]", "@radix-ui/react-primitive@^1.0.3":
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
|
||||||
integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==
|
integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==
|
||||||
@@ -1496,6 +1517,15 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-slot" "1.0.2"
|
"@radix-ui/react-slot" "1.0.2"
|
||||||
|
|
||||||
|
"@radix-ui/react-progress@^1.0.3":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.0.3.tgz#8380272fdc64f15cbf263a294dea70a7d5d9b4fa"
|
||||||
|
integrity sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
|
||||||
"@radix-ui/react-radio-group@^1.1.3":
|
"@radix-ui/react-radio-group@^1.1.3":
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz#3197f5dcce143bcbf961471bf89320735c0212d3"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz#3197f5dcce143bcbf961471bf89320735c0212d3"
|
||||||
@@ -1513,7 +1543,7 @@
|
|||||||
"@radix-ui/react-use-previous" "1.0.1"
|
"@radix-ui/react-use-previous" "1.0.1"
|
||||||
"@radix-ui/react-use-size" "1.0.1"
|
"@radix-ui/react-use-size" "1.0.1"
|
||||||
|
|
||||||
"@radix-ui/[email protected]":
|
"@radix-ui/[email protected]", "@radix-ui/react-roving-focus@^1.0.4":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
|
||||||
integrity sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==
|
integrity sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==
|
||||||
@@ -1573,14 +1603,6 @@
|
|||||||
aria-hidden "^1.1.1"
|
aria-hidden "^1.1.1"
|
||||||
react-remove-scroll "2.5.5"
|
react-remove-scroll "2.5.5"
|
||||||
|
|
||||||
"@radix-ui/react-separator@^1.0.3":
|
|
||||||
version "1.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.0.3.tgz#be5a931a543d5726336b112f465f58585c04c8aa"
|
|
||||||
integrity sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
"@radix-ui/react-primitive" "1.0.3"
|
|
||||||
|
|
||||||
"@radix-ui/react-slider@^1.1.2":
|
"@radix-ui/react-slider@^1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.1.2.tgz#330ff2a0e1f6c19aace76590004f229a7e8fbe6c"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.1.2.tgz#330ff2a0e1f6c19aace76590004f229a7e8fbe6c"
|
||||||
@@ -1636,6 +1658,30 @@
|
|||||||
"@radix-ui/react-roving-focus" "1.0.4"
|
"@radix-ui/react-roving-focus" "1.0.4"
|
||||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle-group@^1.0.4":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz#f5b5c8c477831b013bec3580c55e20a68179d6ec"
|
||||||
|
integrity sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-direction" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-roving-focus" "1.0.4"
|
||||||
|
"@radix-ui/react-toggle" "1.0.3"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/[email protected]":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz#aecb2945630d1dc5c512997556c57aba894e539e"
|
||||||
|
integrity sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
|
||||||
"@radix-ui/react-tooltip@^1.0.7":
|
"@radix-ui/react-tooltip@^1.0.7":
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e"
|
||||||
@@ -1662,7 +1708,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
"@radix-ui/[email protected]":
|
"@radix-ui/[email protected]", "@radix-ui/react-use-controllable-state@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286"
|
||||||
integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==
|
integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==
|
||||||
@@ -1723,10 +1769,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
"@radix-ui/themes@^2.0.1":
|
"@radix-ui/themes@^3.0.1":
|
||||||
version "2.0.3"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/themes/-/themes-2.0.3.tgz#c9e8f4029d73769f356dbe29c97ce3cbade601a8"
|
resolved "https://registry.yarnpkg.com/@radix-ui/themes/-/themes-3.0.1.tgz#24b685166109f93bd2212ea6bc5114b9c0d943df"
|
||||||
integrity sha512-yaXQ8aWT2P1CQ0Xe6YCRD9HXsfMTvKkrIYkrc4aitCzhGTLS0sjtTqKmrxIWMVA+3DIbEuG9K/8aAMRJBhep8g==
|
integrity sha512-w0+2XqaifspZlds2ealxgVU2xtZlJ0gSTgHuLd56ectYxnC4hkg/PEOP5kPHOpCQ+vtV9+LI+Cy4RWipGgg3aQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/colors" "^3.0.0"
|
"@radix-ui/colors" "^3.0.0"
|
||||||
"@radix-ui/primitive" "^1.0.1"
|
"@radix-ui/primitive" "^1.0.1"
|
||||||
@@ -1735,26 +1781,34 @@
|
|||||||
"@radix-ui/react-aspect-ratio" "^1.0.3"
|
"@radix-ui/react-aspect-ratio" "^1.0.3"
|
||||||
"@radix-ui/react-avatar" "^1.0.4"
|
"@radix-ui/react-avatar" "^1.0.4"
|
||||||
"@radix-ui/react-checkbox" "^1.0.4"
|
"@radix-ui/react-checkbox" "^1.0.4"
|
||||||
|
"@radix-ui/react-compose-refs" "^1.0.1"
|
||||||
|
"@radix-ui/react-context" "^1.0.1"
|
||||||
"@radix-ui/react-context-menu" "^2.1.5"
|
"@radix-ui/react-context-menu" "^2.1.5"
|
||||||
"@radix-ui/react-dialog" "^1.0.5"
|
"@radix-ui/react-dialog" "^1.0.5"
|
||||||
"@radix-ui/react-direction" "^1.0.1"
|
"@radix-ui/react-direction" "^1.0.1"
|
||||||
"@radix-ui/react-dropdown-menu" "^2.0.6"
|
"@radix-ui/react-dropdown-menu" "^2.0.6"
|
||||||
"@radix-ui/react-form" "^0.0.3"
|
"@radix-ui/react-form" "^0.0.3"
|
||||||
"@radix-ui/react-hover-card" "^1.0.7"
|
"@radix-ui/react-hover-card" "^1.0.7"
|
||||||
|
"@radix-ui/react-navigation-menu" "^1.1.4"
|
||||||
"@radix-ui/react-popover" "^1.0.7"
|
"@radix-ui/react-popover" "^1.0.7"
|
||||||
"@radix-ui/react-portal" "^1.0.4"
|
"@radix-ui/react-portal" "^1.0.4"
|
||||||
|
"@radix-ui/react-primitive" "^1.0.3"
|
||||||
|
"@radix-ui/react-progress" "^1.0.3"
|
||||||
"@radix-ui/react-radio-group" "^1.1.3"
|
"@radix-ui/react-radio-group" "^1.1.3"
|
||||||
|
"@radix-ui/react-roving-focus" "^1.0.4"
|
||||||
"@radix-ui/react-scroll-area" "^1.0.5"
|
"@radix-ui/react-scroll-area" "^1.0.5"
|
||||||
"@radix-ui/react-select" "^2.0.0"
|
"@radix-ui/react-select" "^2.0.0"
|
||||||
"@radix-ui/react-separator" "^1.0.3"
|
|
||||||
"@radix-ui/react-slider" "^1.1.2"
|
"@radix-ui/react-slider" "^1.1.2"
|
||||||
"@radix-ui/react-slot" "^1.0.2"
|
"@radix-ui/react-slot" "^1.0.2"
|
||||||
"@radix-ui/react-switch" "^1.0.3"
|
"@radix-ui/react-switch" "^1.0.3"
|
||||||
"@radix-ui/react-tabs" "^1.0.4"
|
"@radix-ui/react-tabs" "^1.0.4"
|
||||||
|
"@radix-ui/react-toggle-group" "^1.0.4"
|
||||||
"@radix-ui/react-tooltip" "^1.0.7"
|
"@radix-ui/react-tooltip" "^1.0.7"
|
||||||
"@radix-ui/react-use-callback-ref" "^1.0.1"
|
"@radix-ui/react-use-callback-ref" "^1.0.1"
|
||||||
|
"@radix-ui/react-use-controllable-state" "^1.0.1"
|
||||||
"@radix-ui/react-visually-hidden" "^1.0.3"
|
"@radix-ui/react-visually-hidden" "^1.0.3"
|
||||||
classnames "^2.3.2"
|
classnames "^2.3.2"
|
||||||
|
react-remove-scroll-bar "2.3.4"
|
||||||
|
|
||||||
"@react-spring/animated@~9.6.1":
|
"@react-spring/animated@~9.6.1":
|
||||||
version "9.6.1"
|
version "9.6.1"
|
||||||
@@ -4813,6 +4867,14 @@ react-reconciler@^0.27.0:
|
|||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
scheduler "^0.21.0"
|
scheduler "^0.21.0"
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
version "2.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9"
|
||||||
|
integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==
|
||||||
|
dependencies:
|
||||||
|
react-style-singleton "^2.2.1"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
react-remove-scroll-bar@^2.3.3:
|
react-remove-scroll-bar@^2.3.3:
|
||||||
version "2.3.5"
|
version "2.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz#cd2543b3ed7716c7c5b446342d21b0e0b303f47c"
|
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz#cd2543b3ed7716c7c5b446342d21b0e0b303f47c"
|
||||||
|
|||||||
Reference in new issue
Block a user