Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a90e9312d | ||
|
|
2c7082fe04 | ||
|
|
518a83ee73 | ||
|
|
25c40bcea2 | ||
|
|
f88ac8daf7 | ||
|
|
c4be6b78b6 | ||
|
|
2cfc65a990 | ||
|
|
a8eb751048 | ||
|
|
f030fd53b1 | ||
|
|
da506083c9 | ||
|
|
8bdc4d9b1b | ||
|
|
e0718d9ebb | ||
|
|
dbf1718d39 | ||
|
|
84dd13c0ac | ||
|
|
fdecef5f9c | ||
|
|
198e800958 | ||
|
|
29bb8ada06 | ||
|
|
662f116afe | ||
|
|
02f0c078a5 | ||
|
|
4dd1113b6d | ||
|
|
e656b83719 | ||
|
|
b90b355f8f | ||
|
|
165a9cd719 | ||
|
|
5b8da36fb8 | ||
|
|
bbe682fa73 | ||
|
|
af08df1526 | ||
|
|
47a60adac7 | ||
|
|
10f3bc0026 | ||
|
|
e7da1d582f | ||
|
|
df70816f7e | ||
|
|
091364d7f3 | ||
|
|
753c06eee8 | ||
|
|
17713243b5 | ||
|
|
4e4f070368 | ||
|
|
4064f163ec | ||
|
|
b911f64847 | ||
|
|
b86b6e09d0 | ||
|
|
d2e7a477bb | ||
|
|
f9c182e68c | ||
|
|
7c497ab681 | ||
|
|
c840086c11 | ||
|
|
8649d09a94 | ||
|
|
c764564a96 | ||
|
|
ad8cbb96b7 | ||
|
|
b409e26ca5 | ||
|
|
fbbfa5307c | ||
|
|
4165533838 | ||
|
|
fb47ab3e64 | ||
|
|
57971453ad | ||
|
|
c8cb490b48 | ||
|
|
232f24181f | ||
|
|
46441d74d8 | ||
|
|
a2274a6f0c | ||
|
|
0eb2827e7b | ||
|
|
41cb3f8511 | ||
|
|
9405dccee9 | ||
|
|
83b0c6abc3 | ||
|
|
bf1621001b | ||
|
|
8a5a4bf696 | ||
|
|
a99cce07dc | ||
|
|
d8c26ba24a | ||
|
|
f0b393c48f | ||
|
|
d0d205bb93 | ||
|
|
aeaf52d3ac | ||
|
|
e05358952a | ||
|
|
c1000e1608 | ||
|
|
46c86c5352 | ||
|
|
507b503560 | ||
|
|
37ca981d33 | ||
|
|
efe6d2b1a5 | ||
|
|
9f79f9c1e8 | ||
|
|
38b4c62777 | ||
|
|
df1bc56a4c | ||
|
|
c3d2b063bc | ||
|
|
1b94748668 | ||
|
|
f4f804727f | ||
|
|
3729072594 | ||
|
|
c174f32e15 | ||
|
|
2443ecde2f | ||
|
|
9c027b3dde | ||
|
|
22d794ac65 | ||
|
|
35073c184e | ||
|
|
4741406e4a | ||
|
|
24107433c6 | ||
|
|
05c6fcfb34 | ||
|
|
be7a16d399 | ||
|
|
c7f781dbb2 | ||
|
|
a66c6ebb27 | ||
|
|
83aeca2e5e | ||
|
|
89a3db05d8 | ||
|
|
6050145b16 | ||
|
|
77c78741b3 | ||
|
|
f35670d6a8 | ||
|
|
3392a57480 | ||
|
|
ed64ee8228 | ||
|
|
4f47d6ce85 | ||
|
|
92d56ac086 | ||
|
|
671b8b60b9 | ||
|
|
ba5a5245d9 | ||
|
|
eee262add7 | ||
|
|
e5eaf3fd75 | ||
|
|
5fdc9044ca | ||
|
|
0924a77776 | ||
|
|
1a46e176f5 | ||
|
|
efb8df9769 | ||
|
|
04e09b84ff | ||
|
|
b52c107af9 | ||
|
|
c5e6f5e2eb | ||
|
|
365c68aa4d | ||
|
|
ff9ec55da6 | ||
|
|
2a2994ae69 | ||
|
|
e3c6497d29 | ||
|
|
bdc013b4c8 | ||
|
|
e20d65d218 | ||
|
|
8b726c12d9 | ||
|
|
da704e6181 | ||
|
|
64d346ccb6 | ||
|
|
c090ad6492 | ||
|
|
8b6610151e | ||
|
|
545b655d4e | ||
|
|
0e6aa734cb | ||
|
|
2bef96475c | ||
|
|
45938c875f | ||
|
|
9d916a588d | ||
|
|
f0295079cc | ||
|
|
6d8387fa9d | ||
|
|
7a18de6650 | ||
|
|
cc6c4faa2d | ||
|
|
4253cd3e1b | ||
|
|
75cf827082 | ||
|
|
73c8807f14 | ||
|
|
9fac6dd40d | ||
|
|
c06c338536 | ||
|
|
9258623931 |
No files matched your search
@@ -50,6 +50,7 @@ env:
|
||||
PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }}
|
||||
PUBLIC_NITROPAY_ID: ${{ secrets.PUBLIC_NITROPAY_ID }}
|
||||
PUBLIC_DEBUG_MISSING_I18N: false
|
||||
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
||||
|
||||
jobs:
|
||||
download_client:
|
||||
|
||||
@@ -21,7 +21,7 @@ env:
|
||||
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
|
||||
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
|
||||
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
|
||||
ALLOW_ROBOTS: true
|
||||
ALLOW_ROBOTS: "true"
|
||||
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
||||
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
||||
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
||||
|
||||
@@ -27,4 +27,5 @@ target
|
||||
/.vercel
|
||||
/.astro
|
||||
.vercel
|
||||
/src
|
||||
/src
|
||||
/packages/bot/src/prisma
|
||||
@@ -1,3 +1,9 @@
|
||||
[submodule "submodules/blitzkit-closed"]
|
||||
path = submodules/blitzkit-closed
|
||||
url = https://github.com/tresabhi/blitzkit-closed
|
||||
[submodule "submodules/CUE4Parse"]
|
||||
path = submodules/CUE4Parse
|
||||
url = https://github.com/FabianFG/CUE4Parse
|
||||
[submodule "submodules/UAssetAPI"]
|
||||
path = submodules/UAssetAPI
|
||||
url = https://github.com/atenfyr/UAssetAPI
|
||||
@@ -36,6 +36,7 @@
|
||||
"cacheable",
|
||||
"Callouts",
|
||||
"Camo",
|
||||
"Carro",
|
||||
"CCPAOPTOUT",
|
||||
"CDON",
|
||||
"classname",
|
||||
@@ -43,6 +44,7 @@
|
||||
"closedkrieg",
|
||||
"cockroackdb",
|
||||
"Coef",
|
||||
"Combattimento",
|
||||
"cooldown",
|
||||
"corelations",
|
||||
"corsproxy",
|
||||
@@ -54,6 +56,7 @@
|
||||
"dbid",
|
||||
"Ddpf",
|
||||
"Ddsd",
|
||||
"deadrail",
|
||||
"debugflags",
|
||||
"deburr",
|
||||
"decalmask",
|
||||
@@ -131,6 +134,7 @@
|
||||
"outdir",
|
||||
"outfile",
|
||||
"ownedtanks",
|
||||
"padder",
|
||||
"partytown",
|
||||
"PIXELFORMAT",
|
||||
"playerachievements",
|
||||
@@ -184,6 +188,7 @@
|
||||
"TEXCOORD",
|
||||
"tresabhi",
|
||||
"tungstentip",
|
||||
"Turretless",
|
||||
"TYPELESS",
|
||||
"ubjson",
|
||||
"Unicum",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# BlitzKit 1.27.10
|
||||
|
||||
This update changes the URLs across the whole website, removing the "tools" prefix and simplifying other aspects too.
|
||||
|
||||
## Changes
|
||||
|
||||
- Removed "full" tank names
|
||||
- For example, tanks like the "Carro 45t" had full names like "Carro Combattimento 45t" which remained pretty much unused, so they have been removed
|
||||
- Reduced the shadow below the navigation bar
|
||||
- Removed the "tools" prefix from the URLs; here are the paths:
|
||||
- `/tools/tankopedia` -> `/tanks`
|
||||
- `/tools/tankopedia/12345` -> `/tanks/tank-name`
|
||||
- `/tools/compare` -> `/compare`
|
||||
- `/tools/embed` -> `/embed`
|
||||
- `/tools/embed/embed-name` -> `/embed/embed-name`
|
||||
- `/tools/gallery` -> `/gallery`
|
||||
- `/tools/performance` -> `/performance`
|
||||
- `/tools/session` -> `/session`
|
||||
- `/tools/tier-list` -> `/tier-list`
|
||||
|
||||
## Technical Features
|
||||
|
||||
- The asset mangler now uses http headers and even more heuristics to minimize re-creating unchanged assets
|
||||
@@ -0,0 +1,7 @@
|
||||
# BlitzKit 1.27.11
|
||||
|
||||
This update affects both the bot and the website.
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Added a proper CDN domain to avoid error 429s and any slowdowns
|
||||
@@ -0,0 +1,25 @@
|
||||
# BlitzKit 1.27.7
|
||||
|
||||

|
||||
|
||||
This update only affects the website.
|
||||
|
||||
## Features
|
||||
|
||||
- Added a new foggy-wireframe transition between tabs to smooth out the jarring jumps
|
||||
- Added animations pop-in and out animations on a few elements
|
||||
- The tank name title now shrinks in size as soon as a disturbance is caused
|
||||
- The shell selector, antagonist switcher, and thicknesses range now ease into frame
|
||||
- The Tankopedia search page now loads as a skeleton by default before populating with tanks, allowing for the recently viewed tanks to load without cumulative shift
|
||||
- Something similar was done to the Gallery tool
|
||||
- Gave the meta section of the website some zest with nation-based backdrops
|
||||
- The compare tabs in the Tankopedia are now centered
|
||||
|
||||
## Fixes
|
||||
|
||||
- The camera no longer snaps to the initial position when switching between tabs
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Backported some technologies from BlitzKit Reforged; you will not notice any differences in usage
|
||||
- The popular tanks and gallery indexings have been moved to the asynchronous cron job
|
||||
@@ -0,0 +1,12 @@
|
||||
# BlitzKit 1.27.8
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- The website now uses Inter as the font
|
||||
- Added a cool new non-disturbed state for the Tankopedia because why not
|
||||
|
||||
## Changes
|
||||
|
||||
- Removed the Discord plug and language changer from the homepage
|
||||
@@ -0,0 +1,27 @@
|
||||
# BlitzKit 1.27.9
|
||||
|
||||
BlitzKit's great, but there's far too many numbers. This update adds a few visualizers to help new players understand how things relate numerically.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Added the turret and hull traverse speed visualizer
|
||||
- Added the shell ricochet and normalization angles visualizer
|
||||
- Added the view-range visualizer
|
||||
- Added a search bar to the Tankopedia characteristics
|
||||
- Bumped the decimal places for vehicle dimensions and volume
|
||||
- Made tank ranks softer (the bars beneath the tank characteristics)
|
||||
- Gave the navigation bar better distinction from the rest of the page
|
||||
- Increased the number of characteristics rows to 3 on select devices
|
||||
- Switched to a more contrasting classic style of components across the website (it's subtle, but beautiful)
|
||||
|
||||
## Changes
|
||||
|
||||
- Removed units
|
||||
|
||||
## Fixes
|
||||
|
||||
- Clarified a few confusing row labels in the Compare tool
|
||||
- Made the Tankopedia hero section dynamically smaller on mobile phones to allow scrolling
|
||||
- Tank ranks no longer reach 0 or go above the number of vehicles in the comparison
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "blitzkit",
|
||||
"private": true,
|
||||
"version": "1.27.6",
|
||||
"version": "1.27.11",
|
||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||
"type": "module",
|
||||
"types": "./types.d.ts",
|
||||
@@ -40,15 +40,17 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sharp": "^0.33.5",
|
||||
"lodash-es": "^4.17.21"
|
||||
"googleapis": "^144.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"quicklime": "^1.2.0",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.5.4",
|
||||
"prettier": "^3.3.3",
|
||||
"prisma": "^6.0.0",
|
||||
"typescript": "^5.5.4"
|
||||
"@types/node": "^22.14.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prisma": "^6.6.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -107,8 +107,8 @@ export const auctionCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
|
||||
if (!tank) return '';
|
||||
|
||||
const name = `[${tank.name}](<https://blitzkit.app/tools/tankopedia/${
|
||||
tank.id
|
||||
const name = `[${tank.name}](<https://blitzkit.app/tanks/${
|
||||
tank.slug
|
||||
}>)`;
|
||||
const next =
|
||||
data.next_price === null
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import {
|
||||
getAccountInfo,
|
||||
getClanAccountInfo,
|
||||
linkBlitzAndDiscord,
|
||||
} from '@blitzkit/core';
|
||||
import { getAccountInfo, getClanAccountInfo } from '@blitzkit/core';
|
||||
import { teal } from '@radix-ui/colors';
|
||||
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
|
||||
import { CommandWrapper } from '../components/CommandWrapper';
|
||||
import { linkBlitzAndDiscord } from '../core/db/linkBlitzAndDiscord';
|
||||
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
|
||||
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
|
||||
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||
|
||||
@@ -78,7 +78,7 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
)
|
||||
.map(
|
||||
(tank) =>
|
||||
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name))}](<https://blitzkit.app/tools/tankopedia/${tank.id}>)`,
|
||||
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name))}](<https://blitzkit.app/tanks/${tank.slug}>)`,
|
||||
);
|
||||
const lines = [title, ...tankList];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { idToRegion, usersDatabase } from '@blitzkit/core';
|
||||
import { flagUserActivity } from '@blitzkit/core/src/blitzkit/flagUserActivity';
|
||||
import { idToRegion } from '@blitzkit/core';
|
||||
import { flagUserActivity } from '../db/flagUserActivity';
|
||||
import { usersDatabase } from '../db/users';
|
||||
|
||||
export async function getBlitzFromDiscord(discordId: bigint) {
|
||||
const unique = await usersDatabase.discordUser.findUnique({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { usersDatabase } from '@blitzkit/core';
|
||||
import { usersDatabase } from '../db/users';
|
||||
import { client } from '../discord/client';
|
||||
|
||||
const EXIT_EVENTS = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { usersDatabase } from '@blitzkit/core';
|
||||
import { usersDatabase } from './users';
|
||||
|
||||
export async function flagUserActivity(blitzId: number) {
|
||||
await usersDatabase.user.upsert({
|
||||
@@ -1,5 +1,5 @@
|
||||
import { usersDatabase } from '@blitzkit/core';
|
||||
import { flagUserActivity } from './flagUserActivity';
|
||||
import { usersDatabase } from './users';
|
||||
|
||||
export async function linkBlitzAndDiscord(discordId: bigint, blitzId: number) {
|
||||
await usersDatabase.discordUser.upsert({
|
||||
@@ -1,3 +1,3 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaClient } from '../../prisma';
|
||||
|
||||
export const usersDatabase = new PrismaClient();
|
||||
@@ -6,7 +6,7 @@
|
||||
"dependencies": {
|
||||
"@octokit/plugin-throttling": "^8.2.0",
|
||||
"@octokit/rest": "^20.1.1",
|
||||
"@prisma/client": "^5.19.1",
|
||||
"@prisma/client": "^6.6.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"crc": "^4.3.2",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { assertSecret, type BlitzGlossary } from '@blitzkit/core';
|
||||
import { assertSecret } from '../blitzkit';
|
||||
import { BlitzGlossary } from '../types';
|
||||
|
||||
export async function fetchGlossary(locale: string) {
|
||||
const response = await fetch(
|
||||
@@ -4,6 +4,7 @@ export * from './classes';
|
||||
export * from './constants';
|
||||
export * from './equipmentPrice';
|
||||
export * from './fetchBlitz';
|
||||
export * from './fetchGlossary';
|
||||
export * from './generateStats';
|
||||
export * from './getAccountAchievements';
|
||||
export * from './getAccountInfo';
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { assertSecret } from './assertSecret';
|
||||
|
||||
const repo = assertSecret(import.meta.env.PUBLIC_ASSET_REPO);
|
||||
const branch = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
const fastUser = assertSecret(import.meta.env.PUBLIC_ASSET_FAST_USER);
|
||||
const fastRepo = assertSecret(import.meta.env.PUBLIC_ASSET_FAST_REPO);
|
||||
|
||||
export function asset(path: string) {
|
||||
return `https://raw.githubusercontent.com/${assertSecret(
|
||||
import.meta.env.PUBLIC_ASSET_REPO,
|
||||
)}/${assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH)}/${path}`;
|
||||
return branch === 'main'
|
||||
? `https://${fastUser}.github.io/${fastRepo}/${path}`
|
||||
: `https://raw.githubusercontent.com/${repo}/${branch}/${path}`;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { fetchPB } from '../protobuf';
|
||||
import { Gallery } from '../protos';
|
||||
import { asset } from './asset';
|
||||
|
||||
export function fetchGallery() {
|
||||
return fetchPB(asset('definitions/gallery.pb'), Gallery);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ export * from './discoveredIdDefinitions';
|
||||
export * from './emblemURL';
|
||||
export * from './equipmentDefinitions';
|
||||
export * from './eventManager';
|
||||
// export * from './flagUserActivity';
|
||||
export * from './gameDefinitions';
|
||||
export * from './getArchivedLatestSeasonNumber';
|
||||
// export * from './getArchivedRatingInfo';
|
||||
@@ -21,7 +20,6 @@ export * from './getArchivedRatingLeaderboard';
|
||||
// export * from './getArchivedRatingMidnightLeaderboard';
|
||||
export * from './getTimeDaysAgo';
|
||||
export * from './isExplosive';
|
||||
export * from './linkBlitzAndDiscord';
|
||||
export * from './mapDefinitions';
|
||||
export * from './modelDefinitions';
|
||||
export * from './normalizeBoundingBox';
|
||||
@@ -44,6 +42,8 @@ export * from './unwrapBlitzkitResponse';
|
||||
export * from './votes';
|
||||
// export * from './withCORSProxy';
|
||||
export * from './createDefaultProvisions';
|
||||
export * from './gallery';
|
||||
export * from './metaSortTank';
|
||||
export * from './popularTanks';
|
||||
export * from './romanize';
|
||||
export * from './youtubers';
|
||||
@@ -0,0 +1,7 @@
|
||||
import { fetchPB } from '../protobuf';
|
||||
import { PopularTanks } from '../protos';
|
||||
import { asset } from './asset';
|
||||
|
||||
export function fetchPopularTanks() {
|
||||
return fetchPB(asset('definitions/popular-tanks.pb'), PopularTanks);
|
||||
}
|
||||
@@ -15,18 +15,22 @@ export async function fetchTankNames() {
|
||||
tankDefinitionsArray.map(async (tank) => {
|
||||
const searchableNameDeburr: I18nString = { locales: {} };
|
||||
|
||||
Object.entries(tank.name_full ?? tank.name).forEach(([key, value]) => {
|
||||
Object.entries(tank.name).forEach(([key, value]) => {
|
||||
searchableNameDeburr.locales[key] = deburr(value);
|
||||
});
|
||||
|
||||
return {
|
||||
id: tank.id,
|
||||
name: tank.name,
|
||||
nameFull: tank.name_full,
|
||||
searchableName: tank.name_full ?? tank.name,
|
||||
searchableName: tank.name,
|
||||
searchableNameDeburr,
|
||||
camouflages: tank.camouflages
|
||||
?.map((id) => SUPPORTED_LOCALES.map((locale) => camouflageDefinitions.camouflages[id]?.name.locales[locale]))
|
||||
?.map((id) =>
|
||||
SUPPORTED_LOCALES.map(
|
||||
(locale) =>
|
||||
camouflageDefinitions.camouflages[id]?.name.locales[locale],
|
||||
),
|
||||
)
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
.map(deburr)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './users';
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './blitz';
|
||||
export * from './blitzkit';
|
||||
export * from './databases';
|
||||
export * from './imgur';
|
||||
export * from './math';
|
||||
export * from './patreon';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package blitzkit;
|
||||
|
||||
import "packages/core/src/protos/i18n.proto";
|
||||
|
||||
message Gallery {
|
||||
repeated Avatar avatars = 1;
|
||||
}
|
||||
|
||||
message Avatar {
|
||||
required string id = 1;
|
||||
required I18nString name = 2;
|
||||
required string extension = 3;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.2.2
|
||||
// protoc v5.28.2
|
||||
// source: packages/core/src/protos/gallery.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire';
|
||||
import { I18nString, createBaseI18nString } from './i18n';
|
||||
|
||||
export const protobufPackage = 'blitzkit';
|
||||
|
||||
export interface Gallery {
|
||||
avatars: Avatar[];
|
||||
}
|
||||
|
||||
export interface Avatar {
|
||||
id: string;
|
||||
name: I18nString;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
function createBaseGallery(): Gallery {
|
||||
return { avatars: [] };
|
||||
}
|
||||
|
||||
export const Gallery: MessageFns<Gallery> = {
|
||||
encode(
|
||||
message: Gallery,
|
||||
writer: BinaryWriter = new BinaryWriter(),
|
||||
): BinaryWriter {
|
||||
for (const v of message.avatars) {
|
||||
Avatar.encode(v!, writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Gallery {
|
||||
const reader =
|
||||
input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseGallery();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.avatars.push(Avatar.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Gallery {
|
||||
return {
|
||||
avatars: globalThis.Array.isArray(object?.avatars)
|
||||
? object.avatars.map((e: any) => Avatar.fromJSON(e))
|
||||
: [],
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Gallery): unknown {
|
||||
const obj: any = {};
|
||||
if (message.avatars?.length) {
|
||||
obj.avatars = message.avatars.map((e) => Avatar.toJSON(e));
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Gallery>, I>>(base?: I): Gallery {
|
||||
return Gallery.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Gallery>, I>>(object: I): Gallery {
|
||||
const message = createBaseGallery();
|
||||
message.avatars = object.avatars?.map((e) => Avatar.fromPartial(e)) || [];
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseAvatar(): Avatar {
|
||||
return { id: '', name: createBaseI18nString(), extension: '' };
|
||||
}
|
||||
|
||||
export const Avatar: MessageFns<Avatar> = {
|
||||
encode(
|
||||
message: Avatar,
|
||||
writer: BinaryWriter = new BinaryWriter(),
|
||||
): BinaryWriter {
|
||||
if (message.id !== '') {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
if (message.name !== undefined) {
|
||||
I18nString.encode(message.name, writer.uint32(18).fork()).join();
|
||||
}
|
||||
if (message.extension !== '') {
|
||||
writer.uint32(26).string(message.extension);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Avatar {
|
||||
const reader =
|
||||
input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAvatar();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.name = I18nString.decode(reader, reader.uint32());
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.extension = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Avatar {
|
||||
return {
|
||||
id: globalThis.String(assertSet('Avatar.id', object.id)),
|
||||
name: I18nString.fromJSON(assertSet('Avatar.name', object.name)),
|
||||
extension: globalThis.String(
|
||||
assertSet('Avatar.extension', object.extension),
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Avatar): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== '') {
|
||||
obj.id = message.id;
|
||||
}
|
||||
if (message.name !== undefined) {
|
||||
obj.name = I18nString.toJSON(message.name);
|
||||
}
|
||||
if (message.extension !== '') {
|
||||
obj.extension = message.extension;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Avatar>, I>>(base?: I): Avatar {
|
||||
return Avatar.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Avatar>, I>>(object: I): Avatar {
|
||||
const message = createBaseAvatar();
|
||||
message.id = object.id ?? '';
|
||||
message.name =
|
||||
object.name !== undefined && object.name !== null
|
||||
? I18nString.fromPartial(object.name)
|
||||
: createBaseI18nString();
|
||||
message.extension = object.extension ?? '';
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
type Builtin =
|
||||
| Date
|
||||
| Function
|
||||
| Uint8Array
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends globalThis.Array<infer U>
|
||||
? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends { $case: string; value: unknown }
|
||||
? { $case: T['$case']; value?: DeepPartial<T['value']> }
|
||||
: T extends {}
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
export type Exact<P, I extends P> = P extends Builtin
|
||||
? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
|
||||
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
|
||||
};
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
function assertSet<T>(field: string, value: T | undefined): T {
|
||||
if (!isSet(value)) {
|
||||
throw new TypeError(`Required field ${field} is not set`);
|
||||
}
|
||||
|
||||
return value as T;
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
@@ -3,10 +3,12 @@ export * from './average_definitions.ts';
|
||||
export * from './camouflage_definitions.ts';
|
||||
export * from './consumable_definitions.ts';
|
||||
export * from './equipment_definitions.ts';
|
||||
export * from './gallery.ts';
|
||||
export * from './game_definitions.ts';
|
||||
export * from './i18n.ts';
|
||||
export * from './map_definitions.ts';
|
||||
export * from './model_definitions.ts';
|
||||
export * from './popular_tanks.ts';
|
||||
export * from './provision_definitions.ts';
|
||||
export * from './rating_leaderboard.ts';
|
||||
export * from './reviews.ts';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package blitzkit;
|
||||
|
||||
message PopularTanks {
|
||||
repeated PopularTank tanks = 1;
|
||||
}
|
||||
|
||||
message PopularTank {
|
||||
required uint32 id = 1;
|
||||
required uint32 views = 2; // views in the last 7 days
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.2.2
|
||||
// protoc v5.28.2
|
||||
// source: packages/core/src/protos/popular_tanks.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
|
||||
export const protobufPackage = "blitzkit";
|
||||
|
||||
export interface PopularTanks {
|
||||
tanks: PopularTank[];
|
||||
}
|
||||
|
||||
export interface PopularTank {
|
||||
id: number;
|
||||
/** views in the last 7 days */
|
||||
views: number;
|
||||
}
|
||||
|
||||
function createBasePopularTanks(): PopularTanks {
|
||||
return { tanks: [] };
|
||||
}
|
||||
|
||||
export const PopularTanks: MessageFns<PopularTanks> = {
|
||||
encode(message: PopularTanks, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.tanks) {
|
||||
PopularTank.encode(v!, writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): PopularTanks {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBasePopularTanks();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.tanks.push(PopularTank.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): PopularTanks {
|
||||
return {
|
||||
tanks: globalThis.Array.isArray(object?.tanks) ? object.tanks.map((e: any) => PopularTank.fromJSON(e)) : [],
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: PopularTanks): unknown {
|
||||
const obj: any = {};
|
||||
if (message.tanks?.length) {
|
||||
obj.tanks = message.tanks.map((e) => PopularTank.toJSON(e));
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<PopularTanks>, I>>(base?: I): PopularTanks {
|
||||
return PopularTanks.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<PopularTanks>, I>>(object: I): PopularTanks {
|
||||
const message = createBasePopularTanks();
|
||||
message.tanks = object.tanks?.map((e) => PopularTank.fromPartial(e)) || [];
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBasePopularTank(): PopularTank {
|
||||
return { id: 0, views: 0 };
|
||||
}
|
||||
|
||||
export const PopularTank: MessageFns<PopularTank> = {
|
||||
encode(message: PopularTank, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== 0) {
|
||||
writer.uint32(8).uint32(message.id);
|
||||
}
|
||||
if (message.views !== 0) {
|
||||
writer.uint32(16).uint32(message.views);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): PopularTank {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBasePopularTank();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.uint32();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.views = reader.uint32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): PopularTank {
|
||||
return {
|
||||
id: globalThis.Number(assertSet("PopularTank.id", object.id)),
|
||||
views: globalThis.Number(assertSet("PopularTank.views", object.views)),
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: PopularTank): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== 0) {
|
||||
obj.id = Math.round(message.id);
|
||||
}
|
||||
if (message.views !== 0) {
|
||||
obj.views = Math.round(message.views);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<PopularTank>, I>>(base?: I): PopularTank {
|
||||
return PopularTank.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<PopularTank>, I>>(object: I): PopularTank {
|
||||
const message = createBasePopularTank();
|
||||
message.id = object.id ?? 0;
|
||||
message.views = object.views ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends { $case: string; value: unknown } ? { $case: T["$case"]; value?: DeepPartial<T["value"]> }
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
export type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
function assertSet<T>(field: string, value: T | undefined): T {
|
||||
if (!isSet(value)) {
|
||||
throw new TypeError(`Required field ${field} is not set`);
|
||||
}
|
||||
|
||||
return value as T;
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
@@ -10,16 +10,16 @@ message TankDefinitions {
|
||||
|
||||
message TankDefinition {
|
||||
required uint32 id = 1;
|
||||
map<uint32, uint32> roles = 2;
|
||||
required bool fixed_camouflage = 4;
|
||||
repeated int32 camouflages = 5;
|
||||
repeated int32 ancestors = 6;
|
||||
repeated int32 successors = 7;
|
||||
repeated Crew crew = 8;
|
||||
required uint32 health = 9;
|
||||
required string nation = 10;
|
||||
required I18nString name = 11;
|
||||
optional I18nString name_full = 12;
|
||||
required string slug = 2;
|
||||
map<uint32, uint32> roles = 3;
|
||||
required bool fixed_camouflage = 5;
|
||||
repeated int32 camouflages = 6;
|
||||
repeated int32 ancestors = 7;
|
||||
repeated int32 successors = 8;
|
||||
repeated Crew crew = 9;
|
||||
required uint32 health = 10;
|
||||
required string nation = 11;
|
||||
required I18nString name = 12;
|
||||
required TankType type = 13;
|
||||
required uint32 max_consumables = 14;
|
||||
required uint32 max_provisions = 15;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"default": "en",
|
||||
"supported": [
|
||||
{ "locale": "en" },
|
||||
{ "locale": "es" },
|
||||
{ "locale": "fr" },
|
||||
{ "locale": "ja" },
|
||||
{ "locale": "pl" },
|
||||
{ "locale": "pt" },
|
||||
{ "locale": "ru" },
|
||||
{ "locale": "uk" },
|
||||
{ "locale": "zh", "blitz": "zh_Hans_SG" }
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,9 @@
|
||||
"mm": "%s1mm",
|
||||
"deg": "%s1°",
|
||||
"hp": "%s1hp",
|
||||
"x": "x%s1"
|
||||
"x": "x%s1",
|
||||
"percentage": "%s1%",
|
||||
"deg_s": "%s1°/s"
|
||||
},
|
||||
|
||||
"gun_types": {
|
||||
@@ -739,6 +741,11 @@
|
||||
},
|
||||
|
||||
"website": {
|
||||
"redirect": {
|
||||
"title": "301: moved permanently",
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"dev_mode": "Development mode",
|
||||
"language": "Language"
|
||||
@@ -756,7 +763,7 @@
|
||||
},
|
||||
|
||||
"tank_search": {
|
||||
"clear_filters": "Clear",
|
||||
"clear_filters": "Clear filters",
|
||||
"search_bar_hint": "Search tanks...",
|
||||
"recent": "Recently viewed",
|
||||
"select_all": "Select all",
|
||||
@@ -853,7 +860,24 @@
|
||||
},
|
||||
|
||||
"tools": {
|
||||
"guess": {
|
||||
"name": "Guess the tank",
|
||||
"button": "Guess away",
|
||||
"description": "Silhouette mini-game",
|
||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||
"search": {
|
||||
"placeholder": "Type to guess...",
|
||||
"skip": "Skip",
|
||||
"guess": "Guess",
|
||||
"next": "Next",
|
||||
"no_results": "No results"
|
||||
},
|
||||
"stats": "%s1 / %s2 guessed correctly"
|
||||
},
|
||||
|
||||
"tankopedia": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
"name": "Tankopedia",
|
||||
"button": "Find tanks",
|
||||
"description": "Blitz tank encyclopedia",
|
||||
@@ -861,6 +885,32 @@
|
||||
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
||||
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
||||
|
||||
"visualizers": {
|
||||
"rotator": {
|
||||
"hull": "Hull",
|
||||
"turret": "Turret",
|
||||
"hard": "Hard",
|
||||
"medium": "Medium",
|
||||
"soft": "Soft",
|
||||
"effective": "Effective"
|
||||
},
|
||||
|
||||
"ricochet": {
|
||||
"angle": "Angle",
|
||||
"normalization": "Normalization",
|
||||
"effective_angle": "Effective angle",
|
||||
"armor": "Armor",
|
||||
"no_ricochet": "Explosive shells don't ricochet"
|
||||
},
|
||||
|
||||
"view_range": {
|
||||
"camo": "Camo",
|
||||
"percentage": "%s1%",
|
||||
"ghost_factory": "Ghost Factory",
|
||||
"dead_rail": "Dead Rail"
|
||||
}
|
||||
},
|
||||
|
||||
"sandbox": {
|
||||
"poses": {
|
||||
"hull_down": "Hull down",
|
||||
@@ -1010,7 +1060,7 @@
|
||||
"characteristics": {
|
||||
"title": "Characteristics",
|
||||
"compare": {
|
||||
"label": "Compare against",
|
||||
"label": "Compare",
|
||||
"class": "Tier %s1 %s2",
|
||||
"tier": "All of tier %s1",
|
||||
"all": "All tanks"
|
||||
@@ -1047,7 +1097,7 @@
|
||||
"explosionRadius": "Explosion radius",
|
||||
"shellVelocity": "Shell velocity",
|
||||
"aimTime": "Aim time",
|
||||
"dispersion": "Still",
|
||||
"dispersion": "Dispersion still",
|
||||
"dispersionMoving": "Moving",
|
||||
"dispersionHullTraversing": "Hull traversing",
|
||||
"dispersionTurretTraversing": "Turret traversing",
|
||||
@@ -1061,28 +1111,28 @@
|
||||
"gunRearElevation": "Rear elevation",
|
||||
"azimuthLeft": "Azimuth left",
|
||||
"azimuthRight": "Azimuth right",
|
||||
"speedForwards": "Forwards",
|
||||
"speedForwards": "Speed forwards",
|
||||
"speedBackwards": "Backwards",
|
||||
"hardTerrainCoefficient": "Hard",
|
||||
"hardTerrainCoefficientRaw": "Hard",
|
||||
"hardTerrainCoefficient": "Terrain coefficient hard",
|
||||
"hardTerrainCoefficientRaw": "Raw terrain coefficient hard",
|
||||
"mediumTerrainCoefficient": "Medium",
|
||||
"mediumTerrainCoefficientRaw": "Medium",
|
||||
"softTerrainCoefficient": "Soft",
|
||||
"softTerrainCoefficientRaw": "Soft",
|
||||
"enginePower": "Engine power",
|
||||
"powerToWeightRatioHardTerrain": "Hard",
|
||||
"powerToWeightRatioHardTerrain": "Power to weight ratio hard",
|
||||
"powerToWeightRatioMediumTerrain": "Medium",
|
||||
"powerToWeightRatioSoftTerrain": "Soft",
|
||||
"weight": "Weight",
|
||||
"turretTraverseSpeed": "Turret traverse speed",
|
||||
"gunTraverseSpeed": "Gun traverse speed",
|
||||
"hullTraverseHardTerrain": "Hard",
|
||||
"hullTraverseHardTerrain": "Hull traverse hard",
|
||||
"hullTraverseMediumTerrain": "Medium",
|
||||
"hullTraverseSoftTerrain": "Soft",
|
||||
"health": "Health",
|
||||
"fireChance": "Fire chance",
|
||||
"viewRange": "View range",
|
||||
"camouflageStill": "Still",
|
||||
"camouflageStill": "Camouflage still",
|
||||
"camouflageMoving": "Moving",
|
||||
"camouflageShootingStill": "Still",
|
||||
"camouflageShootingMoving": "Moving",
|
||||
|
||||
@@ -665,6 +665,7 @@
|
||||
"copied": "Copiado!"
|
||||
},
|
||||
"tank_search": {
|
||||
"clear_filters": "Clear filters",
|
||||
"search_bar_hint": "Buscar tanques...",
|
||||
"recent": "Visto recientemente",
|
||||
"select_all": "Seleccionar todo",
|
||||
@@ -731,6 +732,11 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"discord": {
|
||||
"join_short": "Join cool people!",
|
||||
"join_long": "Join super duper cool people!",
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "Por %s1 ❤️"
|
||||
},
|
||||
@@ -744,6 +750,12 @@
|
||||
"help_translate": "¡Por favor ayuda a traducir %s1 a %s2!",
|
||||
"seo_description": "Herramientas para todo lo que es World of Tanks Blitz"
|
||||
},
|
||||
"docs": {
|
||||
"patches": {
|
||||
"name": "BlitzKit Changelogs",
|
||||
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"tankopedia": {
|
||||
"name": "Tankopedia",
|
||||
@@ -888,7 +900,7 @@
|
||||
"characteristics": {
|
||||
"title": "Características",
|
||||
"compare": {
|
||||
"label": "Comparar con",
|
||||
"label": "Comparar",
|
||||
"class": "Nivel %s1 - %s2",
|
||||
"tier": "Todos de nivel %s1",
|
||||
"all": "Todos los tanques"
|
||||
|
||||
@@ -129,9 +129,9 @@
|
||||
"asia": "Asie-Pacifique"
|
||||
},
|
||||
"short": {
|
||||
"com": "NA",
|
||||
"com": "Amérique du Nord",
|
||||
"eu": "Europe",
|
||||
"asia": "APAC"
|
||||
"asia": "Asie-Pacifique"
|
||||
}
|
||||
},
|
||||
"leagues": {
|
||||
@@ -350,7 +350,7 @@
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"subtitle": "Research",
|
||||
"subtitle": "Recherche",
|
||||
"total": "Total",
|
||||
"research": "Recherche: %s1",
|
||||
"upgrades": "Améliorations : %s1",
|
||||
@@ -377,7 +377,7 @@
|
||||
"body": "# Commandes\n\nBlitzKit offre les commandes suivantes :\n\n"
|
||||
},
|
||||
"embeds": {
|
||||
"name": "embeds",
|
||||
"name": "intègre",
|
||||
"description": "How embeds work"
|
||||
},
|
||||
"introduction": {
|
||||
@@ -457,7 +457,7 @@
|
||||
"body": {
|
||||
"cards": {
|
||||
"games": {
|
||||
"title": "Games",
|
||||
"title": "Parties",
|
||||
"winrate": "Taux de victoires",
|
||||
"battles": "Batailles",
|
||||
"wins_losses": "Victoires/Défaites"
|
||||
@@ -505,45 +505,45 @@
|
||||
"description": "Search clans",
|
||||
"options": {
|
||||
"limit": {
|
||||
"name": "limit",
|
||||
"name": "limite",
|
||||
"description": "The max number of clan results (default: %s1)"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"title": "Clan search for %s1 in region %s2",
|
||||
"no_results": "No clans found"
|
||||
"no_results": "Pas de clan trouvé"
|
||||
}
|
||||
},
|
||||
"search_players": {
|
||||
"name": "search-players",
|
||||
"description": "Search players",
|
||||
"name": "recherche de joueur",
|
||||
"description": "Recherche des joueurs",
|
||||
"options": {
|
||||
"limit": {
|
||||
"name": "limit",
|
||||
"description": "The max number of player results (default: %s1)"
|
||||
"name": "limite",
|
||||
"description": "Le nombre maximum de résultats de joueur (par défaut: %s1)"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"title": "Player search for %s1 in region %s2",
|
||||
"no_results": "No players found"
|
||||
"title": "Recherche de %s1 dans la région %s2",
|
||||
"no_results": "Pas de joueur de trouvé"
|
||||
}
|
||||
},
|
||||
"rating_leaderboard": {
|
||||
"name": "rating-leaderboard",
|
||||
"description": "Rating battles statistics",
|
||||
"name": "Classement de côté",
|
||||
"description": "Statistiques des batailles de cotés",
|
||||
"subcommands": {
|
||||
"neighbors": {
|
||||
"name": "neighbors",
|
||||
"description": "Neighboring players on the leaderboard"
|
||||
},
|
||||
"league": {
|
||||
"name": "league",
|
||||
"name": "ligue",
|
||||
"description": "Top players of a league"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"limit": {
|
||||
"name": "limit",
|
||||
"name": "limite",
|
||||
"description": "How many neighbors to display (default: %s1)"
|
||||
},
|
||||
"season": {
|
||||
@@ -554,7 +554,7 @@
|
||||
}
|
||||
},
|
||||
"league": {
|
||||
"name": "league",
|
||||
"name": "ligue",
|
||||
"description": "The league to display"
|
||||
}
|
||||
},
|
||||
@@ -572,7 +572,7 @@
|
||||
"description": "Today's played tanks and statistics"
|
||||
},
|
||||
"stats": {
|
||||
"name": "stats",
|
||||
"name": "statistiques",
|
||||
"description": "Quick statistics for a period"
|
||||
},
|
||||
"owned_tanks": {
|
||||
@@ -665,6 +665,7 @@
|
||||
"copied": "Copied!"
|
||||
},
|
||||
"tank_search": {
|
||||
"clear_filters": "Clear filters",
|
||||
"search_bar_hint": "Search tanks...",
|
||||
"recent": "Recently viewed",
|
||||
"select_all": "Select all",
|
||||
@@ -731,6 +732,11 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"discord": {
|
||||
"join_short": "Join cool people!",
|
||||
"join_long": "Join super duper cool people!",
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "By %s1 ❤️"
|
||||
},
|
||||
@@ -744,6 +750,12 @@
|
||||
"help_translate": "Please help translate %s1 to %s2!",
|
||||
"seo_description": "Tools for everything World of Tanks Blitz"
|
||||
},
|
||||
"docs": {
|
||||
"patches": {
|
||||
"name": "BlitzKit Changelogs",
|
||||
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"tankopedia": {
|
||||
"name": "Tankopedia",
|
||||
@@ -888,7 +900,7 @@
|
||||
"characteristics": {
|
||||
"title": "Characteristics",
|
||||
"compare": {
|
||||
"label": "Compare against",
|
||||
"label": "Compare",
|
||||
"class": "Tier %s1 %s2",
|
||||
"tier": "All of tier %s1",
|
||||
"all": "All tanks"
|
||||
@@ -1092,7 +1104,7 @@
|
||||
"lookup": "Look up a player to get started",
|
||||
"hint": "Search players...",
|
||||
"searching": "Searching...",
|
||||
"no_results": "No players found"
|
||||
"no_results": "Pas de joueur de trouvé"
|
||||
},
|
||||
"controls": {
|
||||
"title": "Tracking %s1",
|
||||
|
||||
@@ -665,6 +665,7 @@
|
||||
"copied": "コピーしました!"
|
||||
},
|
||||
"tank_search": {
|
||||
"clear_filters": "Clear filters",
|
||||
"search_bar_hint": "車輌名を入力…",
|
||||
"recent": "最近の閲覧",
|
||||
"select_all": "全て選択",
|
||||
@@ -731,6 +732,11 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"discord": {
|
||||
"join_short": "Join cool people!",
|
||||
"join_long": "Join super duper cool people!",
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "By %s1 ❤️"
|
||||
},
|
||||
@@ -744,6 +750,12 @@
|
||||
"help_translate": "%s1の %s2への翻訳にご協力ください!",
|
||||
"seo_description": "Tools for everything World of Tanks Blitz"
|
||||
},
|
||||
"docs": {
|
||||
"patches": {
|
||||
"name": "BlitzKit Changelogs",
|
||||
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"tankopedia": {
|
||||
"name": "Tankopedia",
|
||||
@@ -888,7 +900,7 @@
|
||||
"characteristics": {
|
||||
"title": "性能値",
|
||||
"compare": {
|
||||
"label": "比較対象",
|
||||
"label": "性能比較",
|
||||
"class": "Tier %s1 %s2",
|
||||
"tier": "Tier %s1 全車輌",
|
||||
"all": "全車輌"
|
||||
|
||||
@@ -665,6 +665,7 @@
|
||||
"copied": "Copied!"
|
||||
},
|
||||
"tank_search": {
|
||||
"clear_filters": "Clear filters",
|
||||
"search_bar_hint": "Search tanks...",
|
||||
"recent": "Recently viewed",
|
||||
"select_all": "Select all",
|
||||
@@ -731,6 +732,11 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"discord": {
|
||||
"join_short": "Join cool people!",
|
||||
"join_long": "Join super duper cool people!",
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "By %s1 ❤️"
|
||||
},
|
||||
@@ -744,6 +750,12 @@
|
||||
"help_translate": "Please help translate %s1 to %s2!",
|
||||
"seo_description": "Tools for everything World of Tanks Blitz"
|
||||
},
|
||||
"docs": {
|
||||
"patches": {
|
||||
"name": "BlitzKit Changelogs",
|
||||
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"tankopedia": {
|
||||
"name": "Tankopedia",
|
||||
@@ -888,7 +900,7 @@
|
||||
"characteristics": {
|
||||
"title": "Characteristics",
|
||||
"compare": {
|
||||
"label": "Compare against",
|
||||
"label": "Compare",
|
||||
"class": "Tier %s1 %s2",
|
||||
"tier": "All of tier %s1",
|
||||
"all": "All tanks"
|
||||
|
||||
@@ -665,6 +665,7 @@
|
||||
"copied": "Copiado!"
|
||||
},
|
||||
"tank_search": {
|
||||
"clear_filters": "Clear filters",
|
||||
"search_bar_hint": "Procurar tanques...",
|
||||
"recent": "Visto recentemente",
|
||||
"select_all": "Selecionar todos",
|
||||
@@ -731,6 +732,11 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"discord": {
|
||||
"join_short": "Join cool people!",
|
||||
"join_long": "Join super duper cool people!",
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "Por %s1 ❤️"
|
||||
},
|
||||
@@ -744,6 +750,12 @@
|
||||
"help_translate": "Por favor, ajude a traduzir %s1 para %s2!",
|
||||
"seo_description": "Ferramentas para tudo World of Tanks Blitz"
|
||||
},
|
||||
"docs": {
|
||||
"patches": {
|
||||
"name": "BlitzKit Changelogs",
|
||||
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"tankopedia": {
|
||||
"name": "Tanquepedia",
|
||||
@@ -888,7 +900,7 @@
|
||||
"characteristics": {
|
||||
"title": "Características",
|
||||
"compare": {
|
||||
"label": "Comparar com",
|
||||
"label": "Comparar",
|
||||
"class": "Nível %s1 %s2",
|
||||
"tier": "Tudo de nível %s1",
|
||||
"all": "Todos os tanques"
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"local": "dev",
|
||||
"dev": "бета",
|
||||
"opentest": "открытый тест",
|
||||
"preview": "предварительный просмотр"
|
||||
"preview": "превью"
|
||||
},
|
||||
"locales": {
|
||||
"en": "Английский",
|
||||
@@ -665,6 +665,7 @@
|
||||
"copied": "Скопировано!"
|
||||
},
|
||||
"tank_search": {
|
||||
"clear_filters": "Сбросить фильтры",
|
||||
"search_bar_hint": "Поиск танков...",
|
||||
"recent": "Недавние",
|
||||
"select_all": "Выбрать всё",
|
||||
@@ -731,6 +732,11 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"discord": {
|
||||
"join_short": "Присоединяйтесь к крутым ребятам!",
|
||||
"join_long": "Присоединяйся к супер-дупер крутым ребятам!",
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "От %s1 ❤️"
|
||||
},
|
||||
@@ -744,6 +750,12 @@
|
||||
"help_translate": "Пожалуйста, помогите перевести %s1 на %s2!",
|
||||
"seo_description": "Инструменты для World of Tanks Blitz"
|
||||
},
|
||||
"docs": {
|
||||
"patches": {
|
||||
"name": "Списки изменений BlitzKit",
|
||||
"seo_description": "Список изменений для бота Discord и сайта BlitzKit"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"tankopedia": {
|
||||
"name": "Танкопедия",
|
||||
@@ -888,7 +900,7 @@
|
||||
"characteristics": {
|
||||
"title": "Характеристики",
|
||||
"compare": {
|
||||
"label": "Сравнивать с",
|
||||
"label": "Сравнение",
|
||||
"class": "%s2 %s1 уровня",
|
||||
"tier": "Все %s1 уровня",
|
||||
"all": "Все танки"
|
||||
@@ -1131,7 +1143,7 @@
|
||||
"description": "Встраиваемые элементы для стримов",
|
||||
"seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации",
|
||||
"coming_soon": "Скоро появятся новые встраиваемые элементы.",
|
||||
"suggest": "Предложить идеи.",
|
||||
"suggest": "Предложить идеи",
|
||||
"preview": {
|
||||
"title": "Предпросмотр",
|
||||
"upload": "Загрузить фон для теста"
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"m": "%s1м",
|
||||
"mm": "%s1мм",
|
||||
"deg": "%s1°",
|
||||
"hp": "%s1hp",
|
||||
"x": "x%s1"
|
||||
"hp": "%s1хп",
|
||||
"x": "х%s1"
|
||||
},
|
||||
"gun_types": {
|
||||
"auto_loader": "Магазинна система заряджання",
|
||||
@@ -184,11 +184,11 @@
|
||||
"common": {
|
||||
"errors": {
|
||||
"clan_not_found": "# Не вдалося знайти клан\nНе вдалося знайти клан \"%s1\". Спробуйте вибрати клан з пошукового результату.",
|
||||
"player_not_found": "# Could not find user\nI couldn't find user \"%s1\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
|
||||
"player_not_linked": "# Use `/link` to get started\nI don't know who you are but you can tell me today!",
|
||||
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"%s1\".",
|
||||
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"%s1\". Try typing the name of the tank instead of a number.",
|
||||
"no_tank_stats": "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.",
|
||||
"player_not_found": "# Не вдалося знайти користувача\nЯ не зміг знайти користувача \"%s1\". Спробуйте вибрати користувача з результату пошуку, ввести коректний нікнейм, або використати команду '/link'.",
|
||||
"player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!",
|
||||
"tank_name_not_found": "# Танк не знайдено\nНе вдалось знайти танк за ім'ям \"%s1\".",
|
||||
"tank_id_not_found": "# Танк не знайдено\nНе вдалось знайти танк за ID \"%s1\". Спробуйте ввести назву танка замість числа.",
|
||||
"no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.",
|
||||
"get_help": "Допомогти Discord серверу",
|
||||
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
|
||||
"preview": "# Ця команда є в попередньому перегляді\n\nВи можете отримати доступ лише до цієї команди на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH). Команда в цілому буде доступна найближчим часом.",
|
||||
@@ -399,8 +399,8 @@
|
||||
}
|
||||
},
|
||||
"breakdown": {
|
||||
"name": "breakdown",
|
||||
"description": "A period's breakdown by tanks played",
|
||||
"name": "аналіз",
|
||||
"description": "Аналіз періоду за зіграними танками",
|
||||
"body": {
|
||||
"total": "Загалом",
|
||||
"battles": "Бої",
|
||||
@@ -511,7 +511,7 @@
|
||||
},
|
||||
"body": {
|
||||
"title": "Clan search for %s1 in region %s2",
|
||||
"no_results": "No clans found"
|
||||
"no_results": "Жодного клану не знайдено"
|
||||
}
|
||||
},
|
||||
"search_players": {
|
||||
@@ -665,6 +665,7 @@
|
||||
"copied": "Скопійовано!"
|
||||
},
|
||||
"tank_search": {
|
||||
"clear_filters": "Clear filters",
|
||||
"search_bar_hint": "Шукайте техніку...",
|
||||
"recent": "Недавно переглянуті",
|
||||
"select_all": "Вибрати все",
|
||||
@@ -731,6 +732,11 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"discord": {
|
||||
"join_short": "Join cool people!",
|
||||
"join_long": "Join super duper cool people!",
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "Зроблено %s1 ❤"
|
||||
},
|
||||
@@ -744,6 +750,12 @@
|
||||
"help_translate": "Будь ласка, допоможіть перекласти %s1 на %s2!",
|
||||
"seo_description": "Інструменти для всього, пов'язаного з WoT Blitz"
|
||||
},
|
||||
"docs": {
|
||||
"patches": {
|
||||
"name": "BlitzKit Changelogs",
|
||||
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"tankopedia": {
|
||||
"name": "Танкопедія",
|
||||
@@ -888,7 +900,7 @@
|
||||
"characteristics": {
|
||||
"title": "Characteristics",
|
||||
"compare": {
|
||||
"label": "Compare against",
|
||||
"label": "Порівняти",
|
||||
"class": "Tier %s1 %s2",
|
||||
"tier": "All of tier %s1",
|
||||
"all": "Усі танки"
|
||||
|
||||
@@ -665,6 +665,7 @@
|
||||
"copied": "已複製!"
|
||||
},
|
||||
"tank_search": {
|
||||
"clear_filters": "Clear filters",
|
||||
"search_bar_hint": "搜尋戰車....",
|
||||
"recent": "最近查看",
|
||||
"select_all": "選擇全部",
|
||||
@@ -731,6 +732,11 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"discord": {
|
||||
"join_short": "Join cool people!",
|
||||
"join_long": "Join super duper cool people!",
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "由 %s1 製作❤️"
|
||||
},
|
||||
@@ -744,6 +750,12 @@
|
||||
"help_translate": "請幫忙將 %s1 翻譯成 %s2!",
|
||||
"seo_description": "戰車世界閃擊戰 全方位工具"
|
||||
},
|
||||
"docs": {
|
||||
"patches": {
|
||||
"name": "BlitzKit Changelogs",
|
||||
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"tankopedia": {
|
||||
"name": "戰車百科",
|
||||
@@ -888,7 +900,7 @@
|
||||
"characteristics": {
|
||||
"title": "属性",
|
||||
"compare": {
|
||||
"label": "比较",
|
||||
"label": "比較",
|
||||
"class": "%s1 %s2 级",
|
||||
"tier": "所有级别 %s1",
|
||||
"all": "所有坦克"
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
SUPPORTED_LOCALES,
|
||||
} from '@blitzkit/i18n';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { deburr } from 'lodash-es';
|
||||
import { parse as parsePath } from 'path';
|
||||
import { Vector3Tuple } from 'three';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
@@ -48,6 +49,23 @@ import { DATA } from './constants';
|
||||
import { Avatar } from './skillIcons';
|
||||
import { TankParameters } from './tankIcons';
|
||||
|
||||
const nonAlphanumericRegex = /[^a-z0-9]/g;
|
||||
const multipleDashesRegex = /--+/g;
|
||||
const trailingDashRegex = /-$/g;
|
||||
const leadingDashRegex = /^-/g;
|
||||
|
||||
const nationSlugDiscriminators = {
|
||||
china: 'cn',
|
||||
european: 'eu',
|
||||
france: 'fr',
|
||||
germany: 'de',
|
||||
japan: 'jp',
|
||||
other: 'ot',
|
||||
uk: 'gb',
|
||||
usa: 'us',
|
||||
ussr: 'ru',
|
||||
};
|
||||
|
||||
function parseResearchCost(raw: number | string) {
|
||||
if (typeof raw === 'number') {
|
||||
return {
|
||||
@@ -502,7 +520,7 @@ const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
|
||||
HIGH_EXPLOSIVE: ShellType.HE,
|
||||
HOLLOW_CHARGE: ShellType.HEAT,
|
||||
};
|
||||
export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/;
|
||||
export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/;
|
||||
const blitzModuleTypeToBlitzkit: Record<keyof BlitzModuleType, ModuleType> = {
|
||||
chassis: ModuleType.TRACKS,
|
||||
engine: ModuleType.ENGINE,
|
||||
@@ -703,6 +721,93 @@ export async function definitions() {
|
||||
};
|
||||
});
|
||||
|
||||
const slugRequesters = new Map<string, { id: number; key: string }[]>();
|
||||
const idToNation: Record<number, string> = {};
|
||||
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
|
||||
for (const tankKey in tankList.root) {
|
||||
if (botPattern.test(tankKey)) continue;
|
||||
|
||||
const tank = tankList.root[tankKey];
|
||||
const tankId = toUniqueId(nation, tank.id);
|
||||
const name = (
|
||||
(tank.shortUserString
|
||||
? getString(tank.shortUserString)
|
||||
: undefined) ?? getString(tank.userString)
|
||||
).locales.en;
|
||||
|
||||
let slug = deburr(name).toLowerCase();
|
||||
slug = slug.replaceAll(nonAlphanumericRegex, '-');
|
||||
slug = slug.replaceAll(multipleDashesRegex, '-');
|
||||
slug = slug.replaceAll(trailingDashRegex, '');
|
||||
slug = slug.replaceAll(leadingDashRegex, '');
|
||||
|
||||
idToNation[tankId] = nation;
|
||||
|
||||
if (slugRequesters.has(slug)) {
|
||||
slugRequesters.get(slug)!.push({ id: tankId, key: tankKey });
|
||||
} else {
|
||||
slugRequesters.set(slug, [{ id: tankId, key: tankKey }]);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const slugs = new Map<number, string>();
|
||||
|
||||
slugRequesters.forEach((requesters, slug) => {
|
||||
if (requesters.length === 1) {
|
||||
slugs.set(requesters[0].id, slug);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Multiple tanks share slug ${slug}: ${requesters.map(({ key }) => key).join(', ')}`,
|
||||
);
|
||||
|
||||
if (requesters.length !== 2) {
|
||||
throw new Error('Unresolvable number of duplicates :(');
|
||||
}
|
||||
|
||||
const nonCanonical = requesters.find(({ key }) => key.endsWith('TUR'));
|
||||
|
||||
if (nonCanonical === undefined) {
|
||||
console.log('Using nations to discriminate');
|
||||
// both are non-tutorial tanks, will have to discriminate using nation
|
||||
|
||||
requesters.forEach(({ id, key }) => {
|
||||
const nation = idToNation[id];
|
||||
const discriminator =
|
||||
nationSlugDiscriminators[
|
||||
nation as keyof typeof nationSlugDiscriminators
|
||||
];
|
||||
|
||||
console.log(`Solution: ${key} -> ${slug}-${discriminator}`);
|
||||
slugs.set(id, `${slug}-${discriminator}`);
|
||||
});
|
||||
} else {
|
||||
console.log('Using tutorial bot suffix to discriminate');
|
||||
|
||||
const canonical = requesters.find(({ key }) => !key.endsWith('TUR'));
|
||||
|
||||
if (canonical === undefined) {
|
||||
throw new Error(
|
||||
'Two tutorial bots share the same slug? The world is truly broken.',
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Solution: ${canonical.key} -> ${slug}`);
|
||||
slugs.set(canonical.id, slug);
|
||||
console.log(`Solution: ${nonCanonical.key} -> ${slug}-tur`);
|
||||
slugs.set(nonCanonical.id, `${slug}-tur`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
@@ -787,6 +892,13 @@ export async function definitions() {
|
||||
const hullArmor: Armor = { spaced: [], thickness: {} };
|
||||
const equipment = tankDefinition.root.optDevicePreset;
|
||||
tankStringIdMap[`${nation}:${tankKey}`] = tankId;
|
||||
const slug = slugs.get(tankId);
|
||||
|
||||
if (slug === undefined) {
|
||||
throw new Error(
|
||||
`Could not find slug for ${nation}/${tankKey} (${tankId})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (tank.sellPrice) {
|
||||
tankPrice = {
|
||||
@@ -903,7 +1015,7 @@ export async function definitions() {
|
||||
(tank.shortUserString
|
||||
? getString(tank.shortUserString)
|
||||
: undefined) ?? getString(tank.userString),
|
||||
name_full: getString(tank.userString),
|
||||
slug,
|
||||
nation,
|
||||
type: tankTags.includes('collectible')
|
||||
? TankType.COLLECTOR
|
||||
@@ -939,13 +1051,6 @@ export async function definitions() {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
tankDefinitions.tanks[tankId].name ===
|
||||
tankDefinitions.tanks[tankId].name_full
|
||||
) {
|
||||
delete tankDefinitions.tanks[tankId].name_full;
|
||||
}
|
||||
|
||||
modelDefinitions.models[tankId] = {
|
||||
armor: hullArmor,
|
||||
turret_origin: vector3TupleToBlitzkit(turretOrigin),
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Avatar, encodePBBuffer, fetchGlossary, Gallery } from '@blitzkit/core';
|
||||
import locales from '@blitzkit/i18n/locales.json';
|
||||
import { extname } from 'path';
|
||||
import ProgressBar from 'progress';
|
||||
import { commitAssets } from '../core/github/commitAssets';
|
||||
import { FileChange } from '../core/github/commitMultipleFiles';
|
||||
|
||||
export async function glossary() {
|
||||
console.log('Building glossary...');
|
||||
|
||||
const avatars: Record<string, { avatar: Avatar; url: string }> = {};
|
||||
|
||||
await Promise.all(
|
||||
locales.supported.map(async (supported) => {
|
||||
const glossary = await fetchGlossary(supported.blitz ?? supported.locale);
|
||||
|
||||
console.log(
|
||||
`Found ${Object.keys(glossary).length} things for ${supported.locale}`,
|
||||
);
|
||||
|
||||
for (const key in glossary) {
|
||||
const glossaryEntry = glossary[key];
|
||||
|
||||
if (
|
||||
!key.startsWith('avatar') ||
|
||||
key.endsWith('_part') ||
|
||||
glossaryEntry.image_url === null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key in avatars) {
|
||||
avatars[key].avatar.name.locales[supported.locale] =
|
||||
glossaryEntry.title;
|
||||
} else {
|
||||
const extension = extname(glossaryEntry.image_url);
|
||||
|
||||
avatars[key] = {
|
||||
url: glossaryEntry.image_url,
|
||||
avatar: {
|
||||
id: key,
|
||||
name: { locales: { [supported.locale]: glossaryEntry.title } },
|
||||
extension,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
const gallery: Gallery = { avatars: [] };
|
||||
const totalCount = Object.keys(avatars).length;
|
||||
|
||||
const bar = new ProgressBar(
|
||||
`Fetching avatars of ${totalCount} things :bar`,
|
||||
totalCount,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(avatars).map(async ([key, { url, avatar }]) => {
|
||||
if (!(locales.default in avatars[key].avatar.name.locales)) {
|
||||
console.warn(`Avatar ${key} has no ${locales.default} name`);
|
||||
bar.tick();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
gallery.avatars.push(avatar);
|
||||
|
||||
const content = await fetch(url).then((response) =>
|
||||
response.arrayBuffer(),
|
||||
);
|
||||
|
||||
changes.push({
|
||||
path: `gallery/avatars/${key}${avatar.extension}`,
|
||||
content: Buffer.from(content),
|
||||
});
|
||||
|
||||
bar.tick();
|
||||
}),
|
||||
);
|
||||
|
||||
changes.push({
|
||||
path: 'definitions/gallery.pb',
|
||||
content: encodePBBuffer(Gallery, gallery),
|
||||
});
|
||||
|
||||
await commitAssets('glossary', changes);
|
||||
}
|
||||
@@ -18,7 +18,10 @@ import { definitions } from './definitions';
|
||||
import { equipmentIcons } from './equipmentIcons';
|
||||
import { flags } from './flags';
|
||||
import { gameModeBanners } from './gameModeBanners';
|
||||
import { glossary } from './glossary';
|
||||
import { migration } from './migration';
|
||||
import { moduleIcons } from './moduleIcons';
|
||||
import { popularTanks } from './popularTanks';
|
||||
import { shellIcons } from './shellIcons';
|
||||
import { skillIcons } from './skillIcons';
|
||||
import { tankArmors } from './tankArmors';
|
||||
@@ -28,6 +31,7 @@ import { tankModels } from './tankModels';
|
||||
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
||||
|
||||
const methods = [
|
||||
migration,
|
||||
definitions,
|
||||
tankModels,
|
||||
tankArmors,
|
||||
@@ -43,6 +47,8 @@ const methods = [
|
||||
boosterIcons,
|
||||
tankIcons,
|
||||
blitzkitTankIcons,
|
||||
popularTanks,
|
||||
glossary,
|
||||
];
|
||||
|
||||
for (const method of methods) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { toUniqueId } from '@blitzkit/core';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
|
||||
import { commitAssets } from '../core/github/commitAssets';
|
||||
import { DATA } from './constants';
|
||||
import { VehicleDefinitionList } from './definitions';
|
||||
|
||||
export async function migration() {
|
||||
console.log('Building migration map...');
|
||||
|
||||
const map: Record<string, number> = {};
|
||||
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
|
||||
for (const tankKey in tankList.root) {
|
||||
const tank = tankList.root[tankKey];
|
||||
const tankId = toUniqueId(nation, tank.id);
|
||||
const davaKey = `${nation}:${tankKey}`;
|
||||
|
||||
map[davaKey] = tankId;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await commitAssets('migration', [
|
||||
{
|
||||
path: 'definitions/migration.json',
|
||||
content: Buffer.from(JSON.stringify(map)),
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
assertSecret,
|
||||
encodePBBuffer,
|
||||
fetchTankDefinitions,
|
||||
PopularTanks,
|
||||
} from '@blitzkit/core';
|
||||
import { google } from 'googleapis';
|
||||
import { commitAssets } from '../core/github/commitAssets';
|
||||
|
||||
export async function popularTanks() {
|
||||
const tankDefinitions = await fetchTankDefinitions();
|
||||
|
||||
const auth = await google.auth.getClient({
|
||||
keyFile: import.meta.env.GOOGLE_APPLICATION_CREDENTIALS,
|
||||
scopes: ['https://www.googleapis.com/auth/analytics.readonly'],
|
||||
});
|
||||
const analytics = google.analyticsdata({ version: 'v1beta', auth });
|
||||
const report = await analytics.properties.runReport({
|
||||
property: `properties/${assertSecret(import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID)}`,
|
||||
requestBody: {
|
||||
dateRanges: [{ startDate: '7daysAgo', endDate: 'today' }],
|
||||
dimensions: [{ name: 'pagePath' }],
|
||||
metrics: [{ name: 'screenPageViews' }],
|
||||
orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }],
|
||||
dimensionFilter: {
|
||||
filter: {
|
||||
fieldName: 'pagePath',
|
||||
stringFilter: {
|
||||
matchType: 'BEGINS_WITH',
|
||||
value: '/tanks/',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!report.data.rows) {
|
||||
throw new Error('No rows in report');
|
||||
}
|
||||
|
||||
const tanks = report.data.rows
|
||||
.filter(
|
||||
(row) =>
|
||||
row.dimensionValues &&
|
||||
row.dimensionValues[0].value &&
|
||||
row.dimensionValues[0].value !== '/tanks/' &&
|
||||
row.metricValues &&
|
||||
row.metricValues[0].value,
|
||||
)
|
||||
.map((row) => ({
|
||||
id: Number(
|
||||
row.dimensionValues![0].value!.match(
|
||||
/\/tools\/tankopedia\/(\d+)\/?/,
|
||||
)?.[1],
|
||||
),
|
||||
views: Number(row.metricValues![0].value!),
|
||||
}))
|
||||
.filter((row) => {
|
||||
if (!(row.id in tankDefinitions.tanks)) {
|
||||
console.warn('Skipping unknown popular tank', row.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
return row.id in tankDefinitions.tanks;
|
||||
})
|
||||
.slice(0, 8)
|
||||
.map(({ id, views }) => ({ id, views }));
|
||||
const popularTanks: PopularTanks = { tanks };
|
||||
|
||||
await commitAssets('popular tanks', [
|
||||
{
|
||||
path: 'definitions/popular-tanks.pb',
|
||||
content: encodePBBuffer(PopularTanks, popularTanks),
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -166,28 +166,9 @@ export async function extractModel(data: string, path: string) {
|
||||
break;
|
||||
|
||||
case 'TransformComponent': {
|
||||
const translation = new Vector3();
|
||||
const rotation = new Quaternion();
|
||||
const scale = new Vector3();
|
||||
|
||||
new Matrix4()
|
||||
.multiplyMatrices(
|
||||
new Matrix4().compose(
|
||||
new Vector3().fromArray(component['tc.worldTranslation']),
|
||||
new Quaternion().fromArray(component['tc.worldRotation']),
|
||||
new Vector3().fromArray(component['tc.worldScale']),
|
||||
),
|
||||
new Matrix4().compose(
|
||||
new Vector3().fromArray(component['tc.localTranslation']),
|
||||
new Quaternion().fromArray(component['tc.localRotation']),
|
||||
new Vector3().fromArray(component['tc.localScale']),
|
||||
),
|
||||
)
|
||||
.decompose(translation, rotation, scale);
|
||||
|
||||
node.setTranslation(translation.toArray());
|
||||
node.setRotation(rotation.toArray() as Vector4Tuple);
|
||||
node.setScale(scale.toArray());
|
||||
node.setTranslation(component['tc.localTranslation']);
|
||||
node.setRotation(component['tc.localRotation']);
|
||||
node.setScale(component['tc.localScale']);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { assertSecret } from '@blitzkit/core';
|
||||
import { chunk } from 'lodash-es';
|
||||
import { commitMultipleFiles, FileChange } from './commitMultipleFiles';
|
||||
|
||||
export async function commitAssets(message: string, changes: FileChange[]) {
|
||||
@@ -6,10 +7,19 @@ export async function commitAssets(message: string, changes: FileChange[]) {
|
||||
|
||||
if (changes.length === 0) return;
|
||||
|
||||
await commitMultipleFiles(
|
||||
assertSecret(import.meta.env.PUBLIC_ASSET_REPO),
|
||||
assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
|
||||
`${message} - ${new Date().toDateString()}`,
|
||||
changes,
|
||||
);
|
||||
const chunks = chunk(changes, 64);
|
||||
let chunkIndex = 0;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
console.log(`Committing chunk ${chunkIndex + 1}/${chunks.length}`);
|
||||
|
||||
await commitMultipleFiles(
|
||||
assertSecret(import.meta.env.PUBLIC_ASSET_REPO),
|
||||
assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
|
||||
`${message} - ${new Date().toDateString()} (${chunkIndex + 1}/${chunks.length})`,
|
||||
chunk,
|
||||
);
|
||||
|
||||
chunkIndex++;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ export interface FileChange {
|
||||
content: Buffer;
|
||||
}
|
||||
|
||||
const heuristicFormats = ['glb', 'webp', 'jpg', 'png'];
|
||||
|
||||
export async function commitMultipleFiles(
|
||||
repoRaw: string,
|
||||
branch: string,
|
||||
@@ -21,29 +23,69 @@ export async function commitMultipleFiles(
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/${blobPath}`,
|
||||
);
|
||||
let isNew: boolean;
|
||||
let isDifferent: boolean;
|
||||
let diff: number;
|
||||
|
||||
if (response.status === 404) {
|
||||
console.log(
|
||||
`🟢 (+${change.content.length.toLocaleString()}B) ${blobPath}`,
|
||||
);
|
||||
changes.push(change);
|
||||
isNew = true;
|
||||
diff = change.content.length;
|
||||
isDifferent = true;
|
||||
} else if (
|
||||
response.headers.has('Content-Length') &&
|
||||
parseInt(response.headers.get('Content-Length')!) !==
|
||||
change.content.length
|
||||
) {
|
||||
isNew = false;
|
||||
diff =
|
||||
change.content.length -
|
||||
parseInt(response.headers.get('Content-Length')!);
|
||||
isDifferent = true;
|
||||
} else if (response.status === 200) {
|
||||
isNew = false;
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
|
||||
if (!buffer.equals(change.content)) {
|
||||
const diff = change.content.length - buffer.length;
|
||||
|
||||
console.log(
|
||||
`🟡 (${diff > 0 ? '+' : ''}${diff.toLocaleString()}B) ${blobPath}`,
|
||||
);
|
||||
|
||||
changes.push(change);
|
||||
if (buffer.equals(change.content)) {
|
||||
diff = 0;
|
||||
isDifferent = false;
|
||||
} else {
|
||||
diff = change.content.length - buffer.length;
|
||||
isDifferent = true;
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unexpected status code ${response.status} for ${blobPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic: if it's a large blob like a .glb, a diff = 0 might end up
|
||||
* being a false positive with isDifferent = true. So, it's okay to
|
||||
* assume it's unchanged.
|
||||
*/
|
||||
if (
|
||||
diff === 0 &&
|
||||
heuristicFormats.some((format) => change.path.endsWith(`.${format}`))
|
||||
) {
|
||||
isDifferent = false;
|
||||
isNew = false;
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
console.log(
|
||||
`🟢 (+${change.content.length.toLocaleString()}B) ${blobPath}`,
|
||||
);
|
||||
changes.push(change);
|
||||
} else if (isDifferent) {
|
||||
console.log(
|
||||
`🟡 (${diff > 0 ? '+' : ''}${diff.toLocaleString()}B) ${blobPath}`,
|
||||
);
|
||||
changes.push(change);
|
||||
} else {
|
||||
console.log(
|
||||
`🔵 (${change.content.length.toLocaleString()}B) ${blobPath}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ import partytown from '@astrojs/partytown';
|
||||
import react from '@astrojs/react';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
|
||||
import playformInline from '@playform/inline';
|
||||
import compressor from 'astro-compressor';
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -23,14 +21,7 @@ export default defineConfig({
|
||||
outDir: '../../dist/website',
|
||||
prefetch: true,
|
||||
|
||||
integrations: [
|
||||
react(),
|
||||
sitemap(),
|
||||
partytown(),
|
||||
// playformCompress(),
|
||||
compressor(),
|
||||
playformInline(),
|
||||
],
|
||||
integrations: [react(), sitemap(), partytown()],
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@astrolib/seo": "^1.0.0-beta.8",
|
||||
"@blitzkit/core": "workspace:*",
|
||||
"@blitzkit/i18n": "workspace:*",
|
||||
"@fontsource/inter": "^5.2.5",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
"@nivo/bar": "^0.87.0",
|
||||
@@ -25,11 +26,11 @@
|
||||
"@react-three/fiber": "^8.17.10",
|
||||
"compare-versions": "^6.1.1",
|
||||
"echarts": "^5.5.1",
|
||||
"googleapis": "^144.0.0",
|
||||
"immer": "^10.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"markdown-to-jsx": "^7.5.0",
|
||||
"nanostores": "^0.11.3",
|
||||
"quicklime": "^1.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
|
Before Width: | Height: | Size: 611 KiB After Width: | Height: | Size: 490 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 365 KiB |
|
After Width: | Height: | Size: 771 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 371 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 97 B |
|
Before Width: | Height: | Size: 404 KiB After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 188 KiB |
@@ -36,6 +36,7 @@ export function ArmorPlateDisplay() {
|
||||
style={{ pointerEvents: highlightArmor.editingPlate ? 'auto' : 'none' }}
|
||||
>
|
||||
<Card
|
||||
variant="classic"
|
||||
mb="4"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
@@ -72,6 +73,7 @@ export function ArmorPlateDisplay() {
|
||||
{highlightArmor.editingPlate && (
|
||||
<Flex gap="2" align="center">
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
style={{ width: 96 }}
|
||||
mt="2"
|
||||
ref={input}
|
||||
|
||||
@@ -5,7 +5,14 @@ import {
|
||||
} from '@blitzkit/core';
|
||||
import { invalidate, useFrame } from '@react-three/fiber';
|
||||
import { useEffect } from 'react';
|
||||
import { MeshBasicMaterial, Object3D, ShaderMaterial, Vector2 } from 'three';
|
||||
import {
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
ShaderMaterial,
|
||||
UniformsLib,
|
||||
UniformsUtils,
|
||||
Vector2,
|
||||
} from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
||||
@@ -36,27 +43,33 @@ export function PrimaryArmorSceneComponent({
|
||||
fragmentShader,
|
||||
vertexShader,
|
||||
|
||||
fog: true,
|
||||
transparent: true,
|
||||
|
||||
uniforms: {
|
||||
thickness: { value: null },
|
||||
penetration: { value: null },
|
||||
caliber: { value: null },
|
||||
ricochet: { value: null },
|
||||
normalization: { value: null },
|
||||
isExplosive: { value: null },
|
||||
canSplash: { value: null },
|
||||
damage: { value: null },
|
||||
explosionRadius: { value: null },
|
||||
greenPenetration: { value: null },
|
||||
advancedHighlighting: { value: null },
|
||||
opaque: { value: null },
|
||||
uniforms: UniformsUtils.merge([
|
||||
UniformsLib.common,
|
||||
UniformsLib.fog,
|
||||
|
||||
inverseProjectionMatrix: { value: null },
|
||||
resolution: { value: new Vector2() },
|
||||
spacedArmorBuffer: { value: null },
|
||||
spacedArmorDepth: { value: null },
|
||||
},
|
||||
{
|
||||
thickness: { value: null },
|
||||
penetration: { value: null },
|
||||
caliber: { value: null },
|
||||
ricochet: { value: null },
|
||||
normalization: { value: null },
|
||||
isExplosive: { value: null },
|
||||
canSplash: { value: null },
|
||||
damage: { value: null },
|
||||
explosionRadius: { value: null },
|
||||
greenPenetration: { value: null },
|
||||
advancedHighlighting: { value: null },
|
||||
opaque: { value: null },
|
||||
|
||||
inverseProjectionMatrix: { value: null },
|
||||
resolution: { value: new Vector2() },
|
||||
spacedArmorBuffer: { value: null },
|
||||
spacedArmorDepth: { value: null },
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -198,16 +211,30 @@ export function PrimaryArmorSceneComponent({
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsxTree(
|
||||
node,
|
||||
{ mesh: { renderOrder: 0, material: excludeMaterial } },
|
||||
`${node.uuid}-exclude`,
|
||||
)}
|
||||
{jsxTree(
|
||||
node,
|
||||
{ mesh: { renderOrder: 1, material } },
|
||||
`${node.uuid}-include`,
|
||||
)}
|
||||
{jsxTree(node, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={`${key}-exclude`}
|
||||
renderOrder={0}
|
||||
material={excludeMaterial}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})}
|
||||
{jsxTree(node, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={`${key}-include`}
|
||||
renderOrder={1}
|
||||
material={material}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
#define USE_FOG
|
||||
|
||||
#include <fog_pars_fragment>
|
||||
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vViewPosition;
|
||||
|
||||
@@ -39,6 +43,8 @@ vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
#include <clipping_planes_fragment>
|
||||
|
||||
vec2 screenCoord = gl_FragCoord.xy / resolution;
|
||||
vec4 spacedArmorData = texture2D(spacedArmorBuffer, screenCoord);
|
||||
bool isUnderSpacedArmor = spacedArmorData.a != 0.0;
|
||||
@@ -111,4 +117,6 @@ void main() {
|
||||
} else {
|
||||
gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
|
||||
}
|
||||
|
||||
#include <fog_fragment>
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
#define USE_FOG
|
||||
|
||||
#include <fog_pars_vertex>
|
||||
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vViewPosition;
|
||||
|
||||
void main() {
|
||||
vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0);
|
||||
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||||
|
||||
vViewPosition = vViewPosition4.xyz;
|
||||
vViewPosition = mvPosition.xyz;
|
||||
vNormal = normalMatrix * normal;
|
||||
|
||||
gl_Position = projectionMatrix * vViewPosition4;
|
||||
}
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
|
||||
#include <fog_vertex>
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
const inTitleColor = shotStatusColors[shot.in.status];
|
||||
|
||||
return (
|
||||
<Card style={{ width: 300 }} {...props}>
|
||||
<Card variant="classic" style={{ width: 300 }} {...props}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex direction="column" gap="1">
|
||||
<Text color={inTitleColor} weight="bold">
|
||||
|
||||
@@ -74,7 +74,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
||||
return (
|
||||
<SpacedArmorSceneComponent
|
||||
scene={scene}
|
||||
key={node.uuid}
|
||||
key={`${node.uuid}-hull`}
|
||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||
thickness={thickness}
|
||||
node={node}
|
||||
@@ -93,7 +93,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
||||
return (
|
||||
<SpacedArmorSceneComponent
|
||||
scene={scene}
|
||||
key={node.uuid}
|
||||
key={`${node.uuid}-track`}
|
||||
type={ArmorType.External}
|
||||
thickness={trackModelDefinition.thickness}
|
||||
variant="track"
|
||||
@@ -127,7 +127,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
||||
<group position={turretOrigin}>
|
||||
<SpacedArmorSceneComponent
|
||||
scene={scene}
|
||||
key={node.uuid}
|
||||
key={`${node.uuid}-turret`}
|
||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||
thickness={thickness}
|
||||
node={node}
|
||||
@@ -158,7 +158,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
||||
return null;
|
||||
|
||||
return (
|
||||
<group position={hullOrigin} key={node.uuid}>
|
||||
<group position={hullOrigin} key={`${node.uuid}-gun-spaced}`}>
|
||||
<group position={turretOrigin.clone().add(gunOrigin)}>
|
||||
<SpacedArmorSceneComponent
|
||||
scene={scene}
|
||||
@@ -185,7 +185,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
||||
return (
|
||||
<SpacedArmorSceneComponent
|
||||
scene={scene}
|
||||
key={node.uuid}
|
||||
key={`${node.uuid}-gun-external`}
|
||||
type={ArmorType.External}
|
||||
thickness={gunModelDefinition.thickness}
|
||||
variant="gun"
|
||||
|
||||
@@ -114,33 +114,57 @@ export function SpacedArmorSubExternal({
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsxTree(
|
||||
node,
|
||||
{
|
||||
mesh: {
|
||||
renderOrder: 3,
|
||||
material: new MeshBasicMaterial({
|
||||
colorWrite: false,
|
||||
depthTest: true,
|
||||
depthWrite: true,
|
||||
...(clip ? { clippingPlanes: [clip] } : {}),
|
||||
}),
|
||||
onClick() {},
|
||||
userData: {
|
||||
type: ArmorType.External,
|
||||
thickness,
|
||||
variant,
|
||||
} satisfies ArmorUserData,
|
||||
},
|
||||
{jsxTree(node, {
|
||||
group(_, props, key) {
|
||||
return (
|
||||
<group {...props} key={`${key}-spaced-sub-external-exclude`} />
|
||||
);
|
||||
},
|
||||
`${node.uuid}-exclude`,
|
||||
)}
|
||||
|
||||
{jsxTree(
|
||||
node,
|
||||
{ mesh: { renderOrder: 4, material } },
|
||||
`${node.uuid}-include`,
|
||||
)}
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={`${key}-spaced-sub-external-exclude`}
|
||||
renderOrder={3}
|
||||
material={
|
||||
new MeshBasicMaterial({
|
||||
colorWrite: false,
|
||||
depthTest: true,
|
||||
depthWrite: true,
|
||||
...(clip ? { clippingPlanes: [clip] } : {}),
|
||||
})
|
||||
}
|
||||
userData={
|
||||
{
|
||||
type: ArmorType.External,
|
||||
thickness,
|
||||
variant,
|
||||
} satisfies ArmorUserData
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})}
|
||||
|
||||
{jsxTree(node, {
|
||||
group(_, props, key) {
|
||||
return (
|
||||
<group {...props} key={`${key}-spaced-sub-external-include`} />
|
||||
);
|
||||
},
|
||||
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={`${key}-spaced-sub-external-include`}
|
||||
renderOrder={4}
|
||||
material={material}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -124,32 +124,46 @@ export function SpacedArmorSubSpaced({
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsxTree(
|
||||
node,
|
||||
{
|
||||
mesh: {
|
||||
renderOrder: 2,
|
||||
material,
|
||||
onClick() {},
|
||||
userData: {
|
||||
type: ArmorType.Spaced,
|
||||
thickness,
|
||||
} satisfies ArmorUserData,
|
||||
},
|
||||
{jsxTree(node, {
|
||||
group(_, props, key) {
|
||||
return <group {...props} key={`${key}-spaced-sub-spaced-exclude`} />;
|
||||
},
|
||||
`${node.uuid}-exclude`,
|
||||
)}
|
||||
|
||||
{jsxTree(
|
||||
node,
|
||||
{
|
||||
mesh: {
|
||||
renderOrder: 5,
|
||||
material: depthWriteMaterial,
|
||||
},
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={`${key}-spaced-sub-spaced-exclude`}
|
||||
renderOrder={2}
|
||||
material={material}
|
||||
onClick={() => {}}
|
||||
userData={
|
||||
{
|
||||
type: ArmorType.Spaced,
|
||||
thickness,
|
||||
} satisfies ArmorUserData
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
`${node.uuid}-include`,
|
||||
)}
|
||||
})}
|
||||
|
||||
{jsxTree(node, {
|
||||
group(_, props, key) {
|
||||
return <group {...props} key={`${key}-spaced-sub-spaced-include`} />;
|
||||
},
|
||||
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={`${key}-spaced-sub-spaced-include`}
|
||||
renderOrder={5}
|
||||
material={depthWriteMaterial}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -372,29 +372,33 @@ export function SpacedArmorSceneComponent({
|
||||
return (
|
||||
<>
|
||||
{props.type === ArmorType.Primary &&
|
||||
jsxTree(
|
||||
node,
|
||||
{
|
||||
mesh: {
|
||||
renderOrder: 0,
|
||||
material: omitMaterial,
|
||||
userData: {
|
||||
type: ArmorType.Primary,
|
||||
thickness,
|
||||
} satisfies ArmorUserData,
|
||||
async onClick(event) {
|
||||
event.stopPropagation();
|
||||
const shot = (await shoot(
|
||||
event.point,
|
||||
event.intersections,
|
||||
true,
|
||||
))!;
|
||||
tankopediaEphemeralStore.setState({ shot });
|
||||
},
|
||||
},
|
||||
jsxTree(node, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={`${key}-spaced-component`}
|
||||
renderOrder={0}
|
||||
material={omitMaterial}
|
||||
userData={
|
||||
{
|
||||
type: ArmorType.Primary,
|
||||
thickness,
|
||||
} satisfies ArmorUserData
|
||||
}
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
const shot = (await shoot(
|
||||
event.point,
|
||||
event.intersections,
|
||||
true,
|
||||
))!;
|
||||
tankopediaEphemeralStore.setState({ shot });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
node.uuid,
|
||||
)}
|
||||
})}
|
||||
|
||||
{props.type === ArmorType.Spaced && (
|
||||
<SpacedArmorSubSpaced node={node} thickness={thickness} />
|
||||
|
||||
@@ -215,50 +215,62 @@ export function StaticArmorSceneComponent({
|
||||
return (
|
||||
<>
|
||||
{jsxTree(node, {
|
||||
mesh: {
|
||||
material: surfaceMaterial,
|
||||
onPointerDown,
|
||||
userData: {
|
||||
type: props.type,
|
||||
variant: props.type === ArmorType.External ? props.variant : 'gun',
|
||||
thickness,
|
||||
} satisfies ArmorUserData,
|
||||
mesh(_, meshProps, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...meshProps}
|
||||
key={key}
|
||||
material={surfaceMaterial}
|
||||
onPointerDown={onPointerDown}
|
||||
userData={
|
||||
{
|
||||
type: props.type,
|
||||
variant:
|
||||
props.type === ArmorType.External ? props.variant : 'gun',
|
||||
thickness,
|
||||
} satisfies ArmorUserData
|
||||
}
|
||||
onClick={(event) => {
|
||||
if (discardClippingPlane(event.object, event.point)) return;
|
||||
|
||||
onClick(event) {
|
||||
if (discardClippingPlane(event.object, event.point)) return;
|
||||
event.stopPropagation();
|
||||
|
||||
event.stopPropagation();
|
||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
||||
|
||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
||||
const bounds = new Box3().setFromObject(event.object);
|
||||
const point = bounds.min
|
||||
.clone()
|
||||
.add(bounds.max)
|
||||
.divideScalar(2)
|
||||
.setY(bounds.max.y);
|
||||
const cameraNormal = camera.position
|
||||
.clone()
|
||||
.sub(point)
|
||||
.normalize();
|
||||
const surfaceNormal = event
|
||||
.normal!.clone()
|
||||
.applyQuaternion(
|
||||
event.object.getWorldQuaternion(new Quaternion()),
|
||||
);
|
||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||
const thicknessAngled =
|
||||
thickness / Math.sin(Math.PI / 2 - angle);
|
||||
|
||||
const bounds = new Box3().setFromObject(event.object);
|
||||
const point = bounds.min
|
||||
.clone()
|
||||
.add(bounds.max)
|
||||
.divideScalar(2)
|
||||
.setY(bounds.max.y);
|
||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||
const surfaceNormal = event
|
||||
.normal!.clone()
|
||||
.applyQuaternion(
|
||||
event.object.getWorldQuaternion(new Quaternion()),
|
||||
);
|
||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||
const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle);
|
||||
|
||||
mutateTankopediaEphemeralStore((draft) => {
|
||||
draft.highlightArmor = {
|
||||
type: props.type,
|
||||
name,
|
||||
point,
|
||||
thickness,
|
||||
thicknessAngled,
|
||||
angle,
|
||||
color: `#${color.getHexString()}`,
|
||||
editingPlate: editStatic,
|
||||
};
|
||||
});
|
||||
},
|
||||
mutateTankopediaEphemeralStore((draft) => {
|
||||
draft.highlightArmor = {
|
||||
type: props.type,
|
||||
name,
|
||||
point,
|
||||
thickness,
|
||||
thicknessAngled,
|
||||
angle,
|
||||
color: `#${color.getHexString()}`,
|
||||
editingPlate: editStatic,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createPortal, useFrame } from '@react-three/fiber';
|
||||
import { createPortal, useFrame, useThree } from '@react-three/fiber';
|
||||
import { memo, useState } from 'react';
|
||||
import { DepthTexture, Scene, Vector2 } from 'three';
|
||||
import { PrimaryArmorScene } from './components/PrimaryArmorScene';
|
||||
@@ -6,8 +6,14 @@ import { spacedArmorRenderTarget } from './components/PrimaryArmorSceneComponent
|
||||
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
||||
|
||||
export const Armor = memo(() => {
|
||||
const rootScene = useThree((state) => state.scene);
|
||||
const [spacedArmorScene] = useState(() => new Scene());
|
||||
const [primaryArmorScene] = useState(() => new Scene());
|
||||
const [primaryArmorScene] = useState(() => {
|
||||
const primaryArmorScene = new Scene();
|
||||
primaryArmorScene.fog = rootScene.fog;
|
||||
|
||||
return primaryArmorScene;
|
||||
});
|
||||
const spacedArmorPortal = createPortal(
|
||||
<SpacedArmorScene scene={spacedArmorScene} />,
|
||||
spacedArmorScene,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
@@ -15,7 +16,10 @@ interface ControlsProps {
|
||||
onAddTankDialogOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
||||
awaitableProvisionDefinitions,
|
||||
awaitableModelDefinitions,
|
||||
]);
|
||||
|
||||
export function Controls({
|
||||
addTankDialogOpen,
|
||||
@@ -56,9 +60,11 @@ export function Controls({
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
mutateCompareEphemeral((draft) => {
|
||||
draft.members.push(
|
||||
tankToCompareMember(tank, provisionDefinitions),
|
||||
tankToCompareMember(tank, model, provisionDefinitions),
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
@@ -67,9 +73,15 @@ export function Controls({
|
||||
onSelectAll={(tanks) => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
draft.members.push(
|
||||
...tanks.map((tank) =>
|
||||
tankToCompareMember(tank, provisionDefinitions),
|
||||
),
|
||||
...tanks.map((tank) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
return tankToCompareMember(
|
||||
tank,
|
||||
model,
|
||||
provisionDefinitions,
|
||||
);
|
||||
}),
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
@@ -95,6 +107,7 @@ export function Controls({
|
||||
</Button>
|
||||
|
||||
<SegmentedControl.Root
|
||||
variant="classic"
|
||||
value={deltaMode}
|
||||
onValueChange={(value) => {
|
||||
mutateComparePersistent((draft) => {
|
||||
|
||||
@@ -105,7 +105,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
return (
|
||||
<StickyColumnHeaderCell>
|
||||
<Flex direction="column" align="center" justify="between" gap="2">
|
||||
<TankControl index={index} key={tank.id} id={tank.id} />
|
||||
<TankControl index={index} key={tank.id} slug={tank.slug} />
|
||||
|
||||
<Flex
|
||||
ref={draggable}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
@@ -10,12 +11,15 @@ import { TankSearch } from '../TankSearch';
|
||||
|
||||
interface TankControlProps {
|
||||
index: number;
|
||||
id: number;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
||||
awaitableProvisionDefinitions,
|
||||
awaitableModelDefinitions,
|
||||
]);
|
||||
|
||||
export function TankControl({ index, id }: TankControlProps) {
|
||||
export function TankControl({ index, slug }: TankControlProps) {
|
||||
const { locale } = useLocale();
|
||||
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
@@ -57,9 +61,12 @@ export function TankControl({ index, id }: TankControlProps) {
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
mutateCompareEphemeral((draft) => {
|
||||
draft.members[index] = tankToCompareMember(
|
||||
tank,
|
||||
model,
|
||||
provisionDefinitions,
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
@@ -72,11 +79,7 @@ export function TankControl({ index, id }: TankControlProps) {
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<LinkI18n
|
||||
locale={locale}
|
||||
href={`/tools/tankopedia/${id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<LinkI18n locale={locale} href={`/tanks/${slug}`} target="_blank">
|
||||
<IconButton variant="ghost">
|
||||
<ExternalLinkIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Switch } from '@radix-ui/themes';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
||||
import { EmbedState } from '../../stores/embedState';
|
||||
|
||||
export function Boolean({ configKey }: EmbedPreviewControllerProps) {
|
||||
@@ -7,7 +7,7 @@ export function Boolean({ configKey }: EmbedPreviewControllerProps) {
|
||||
|
||||
return (
|
||||
<Switch
|
||||
variant="soft"
|
||||
variant="classic"
|
||||
checked={EmbedState.use((state) => state[configKey] as boolean)}
|
||||
onCheckedChange={() => {
|
||||
mutateEmbedState((draft) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
||||
import { EmbedState } from '../../stores/embedState';
|
||||
import { ColorControllerRaw } from './ColorControllerRaw';
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export function ColorControllerRaw({
|
||||
}}
|
||||
/>
|
||||
<Slider
|
||||
variant="soft"
|
||||
variant="classic"
|
||||
min={0}
|
||||
max={255}
|
||||
value={[alpha]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Select } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
||||
import { EmbedState } from '../../stores/embedState';
|
||||
import type {
|
||||
EmbedConfigItemType,
|
||||
@@ -25,7 +25,8 @@ export function Enum({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Trigger variant="classic" />
|
||||
|
||||
<Select.Content>
|
||||
{config.options.map((option) => (
|
||||
<Select.Item key={option.value} value={option.value}>
|
||||
|
||||