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 }}
|
PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }}
|
||||||
PUBLIC_NITROPAY_ID: ${{ secrets.PUBLIC_NITROPAY_ID }}
|
PUBLIC_NITROPAY_ID: ${{ secrets.PUBLIC_NITROPAY_ID }}
|
||||||
PUBLIC_DEBUG_MISSING_I18N: false
|
PUBLIC_DEBUG_MISSING_I18N: false
|
||||||
|
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
download_client:
|
download_client:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ env:
|
|||||||
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
|
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
|
||||||
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
|
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
|
||||||
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
|
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
|
||||||
ALLOW_ROBOTS: true
|
ALLOW_ROBOTS: "true"
|
||||||
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
||||||
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
||||||
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
||||||
|
|||||||
@@ -27,4 +27,5 @@ target
|
|||||||
/.vercel
|
/.vercel
|
||||||
/.astro
|
/.astro
|
||||||
.vercel
|
.vercel
|
||||||
/src
|
/src
|
||||||
|
/packages/bot/src/prisma
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
[submodule "submodules/blitzkit-closed"]
|
[submodule "submodules/blitzkit-closed"]
|
||||||
path = submodules/blitzkit-closed
|
path = submodules/blitzkit-closed
|
||||||
url = https://github.com/tresabhi/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",
|
"cacheable",
|
||||||
"Callouts",
|
"Callouts",
|
||||||
"Camo",
|
"Camo",
|
||||||
|
"Carro",
|
||||||
"CCPAOPTOUT",
|
"CCPAOPTOUT",
|
||||||
"CDON",
|
"CDON",
|
||||||
"classname",
|
"classname",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"closedkrieg",
|
"closedkrieg",
|
||||||
"cockroackdb",
|
"cockroackdb",
|
||||||
"Coef",
|
"Coef",
|
||||||
|
"Combattimento",
|
||||||
"cooldown",
|
"cooldown",
|
||||||
"corelations",
|
"corelations",
|
||||||
"corsproxy",
|
"corsproxy",
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
"dbid",
|
"dbid",
|
||||||
"Ddpf",
|
"Ddpf",
|
||||||
"Ddsd",
|
"Ddsd",
|
||||||
|
"deadrail",
|
||||||
"debugflags",
|
"debugflags",
|
||||||
"deburr",
|
"deburr",
|
||||||
"decalmask",
|
"decalmask",
|
||||||
@@ -131,6 +134,7 @@
|
|||||||
"outdir",
|
"outdir",
|
||||||
"outfile",
|
"outfile",
|
||||||
"ownedtanks",
|
"ownedtanks",
|
||||||
|
"padder",
|
||||||
"partytown",
|
"partytown",
|
||||||
"PIXELFORMAT",
|
"PIXELFORMAT",
|
||||||
"playerachievements",
|
"playerachievements",
|
||||||
@@ -184,6 +188,7 @@
|
|||||||
"TEXCOORD",
|
"TEXCOORD",
|
||||||
"tresabhi",
|
"tresabhi",
|
||||||
"tungstentip",
|
"tungstentip",
|
||||||
|
"Turretless",
|
||||||
"TYPELESS",
|
"TYPELESS",
|
||||||
"ubjson",
|
"ubjson",
|
||||||
"Unicum",
|
"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",
|
"name": "blitzkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.27.6",
|
"version": "1.27.11",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./types.d.ts",
|
"types": "./types.d.ts",
|
||||||
@@ -40,15 +40,17 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sharp": "^0.33.5",
|
"googleapis": "^144.0.0",
|
||||||
"lodash-es": "^4.17.21"
|
"lodash-es": "^4.17.21",
|
||||||
|
"quicklime": "^1.2.0",
|
||||||
|
"sharp": "^0.33.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.14.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.5.3",
|
||||||
"prisma": "^6.0.0",
|
"prisma": "^6.6.0",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,8 +107,8 @@ export const auctionCommand = new Promise<CommandRegistry>((resolve) => {
|
|||||||
|
|
||||||
if (!tank) return '';
|
if (!tank) return '';
|
||||||
|
|
||||||
const name = `[${tank.name}](<https://blitzkit.app/tools/tankopedia/${
|
const name = `[${tank.name}](<https://blitzkit.app/tanks/${
|
||||||
tank.id
|
tank.slug
|
||||||
}>)`;
|
}>)`;
|
||||||
const next =
|
const next =
|
||||||
data.next_price === null
|
data.next_price === null
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import {
|
import { getAccountInfo, getClanAccountInfo } from '@blitzkit/core';
|
||||||
getAccountInfo,
|
|
||||||
getClanAccountInfo,
|
|
||||||
linkBlitzAndDiscord,
|
|
||||||
} from '@blitzkit/core';
|
|
||||||
import { teal } from '@radix-ui/colors';
|
import { teal } from '@radix-ui/colors';
|
||||||
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
|
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
|
||||||
import { CommandWrapper } from '../components/CommandWrapper';
|
import { CommandWrapper } from '../components/CommandWrapper';
|
||||||
|
import { linkBlitzAndDiscord } from '../core/db/linkBlitzAndDiscord';
|
||||||
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
|
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
|
||||||
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
|
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
|
||||||
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
|||||||
)
|
)
|
||||||
.map(
|
.map(
|
||||||
(tank) =>
|
(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];
|
const lines = [title, ...tankList];
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { idToRegion, usersDatabase } from '@blitzkit/core';
|
import { idToRegion } from '@blitzkit/core';
|
||||||
import { flagUserActivity } from '@blitzkit/core/src/blitzkit/flagUserActivity';
|
import { flagUserActivity } from '../db/flagUserActivity';
|
||||||
|
import { usersDatabase } from '../db/users';
|
||||||
|
|
||||||
export async function getBlitzFromDiscord(discordId: bigint) {
|
export async function getBlitzFromDiscord(discordId: bigint) {
|
||||||
const unique = await usersDatabase.discordUser.findUnique({
|
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';
|
import { client } from '../discord/client';
|
||||||
|
|
||||||
const EXIT_EVENTS = [
|
const EXIT_EVENTS = [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { usersDatabase } from '@blitzkit/core';
|
import { usersDatabase } from './users';
|
||||||
|
|
||||||
export async function flagUserActivity(blitzId: number) {
|
export async function flagUserActivity(blitzId: number) {
|
||||||
await usersDatabase.user.upsert({
|
await usersDatabase.user.upsert({
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { usersDatabase } from '@blitzkit/core';
|
|
||||||
import { flagUserActivity } from './flagUserActivity';
|
import { flagUserActivity } from './flagUserActivity';
|
||||||
|
import { usersDatabase } from './users';
|
||||||
|
|
||||||
export async function linkBlitzAndDiscord(discordId: bigint, blitzId: number) {
|
export async function linkBlitzAndDiscord(discordId: bigint, blitzId: number) {
|
||||||
await usersDatabase.discordUser.upsert({
|
await usersDatabase.discordUser.upsert({
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '../../prisma';
|
||||||
|
|
||||||
export const usersDatabase = new PrismaClient();
|
export const usersDatabase = new PrismaClient();
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/plugin-throttling": "^8.2.0",
|
"@octokit/plugin-throttling": "^8.2.0",
|
||||||
"@octokit/rest": "^20.1.1",
|
"@octokit/rest": "^20.1.1",
|
||||||
"@prisma/client": "^5.19.1",
|
"@prisma/client": "^6.6.0",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"crc": "^4.3.2",
|
"crc": "^4.3.2",
|
||||||
"dotenv": "^16.4.5",
|
"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) {
|
export async function fetchGlossary(locale: string) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -4,6 +4,7 @@ export * from './classes';
|
|||||||
export * from './constants';
|
export * from './constants';
|
||||||
export * from './equipmentPrice';
|
export * from './equipmentPrice';
|
||||||
export * from './fetchBlitz';
|
export * from './fetchBlitz';
|
||||||
|
export * from './fetchGlossary';
|
||||||
export * from './generateStats';
|
export * from './generateStats';
|
||||||
export * from './getAccountAchievements';
|
export * from './getAccountAchievements';
|
||||||
export * from './getAccountInfo';
|
export * from './getAccountInfo';
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { assertSecret } from './assertSecret';
|
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) {
|
export function asset(path: string) {
|
||||||
return `https://raw.githubusercontent.com/${assertSecret(
|
return branch === 'main'
|
||||||
import.meta.env.PUBLIC_ASSET_REPO,
|
? `https://${fastUser}.github.io/${fastRepo}/${path}`
|
||||||
)}/${assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH)}/${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 './emblemURL';
|
||||||
export * from './equipmentDefinitions';
|
export * from './equipmentDefinitions';
|
||||||
export * from './eventManager';
|
export * from './eventManager';
|
||||||
// export * from './flagUserActivity';
|
|
||||||
export * from './gameDefinitions';
|
export * from './gameDefinitions';
|
||||||
export * from './getArchivedLatestSeasonNumber';
|
export * from './getArchivedLatestSeasonNumber';
|
||||||
// export * from './getArchivedRatingInfo';
|
// export * from './getArchivedRatingInfo';
|
||||||
@@ -21,7 +20,6 @@ export * from './getArchivedRatingLeaderboard';
|
|||||||
// export * from './getArchivedRatingMidnightLeaderboard';
|
// export * from './getArchivedRatingMidnightLeaderboard';
|
||||||
export * from './getTimeDaysAgo';
|
export * from './getTimeDaysAgo';
|
||||||
export * from './isExplosive';
|
export * from './isExplosive';
|
||||||
export * from './linkBlitzAndDiscord';
|
|
||||||
export * from './mapDefinitions';
|
export * from './mapDefinitions';
|
||||||
export * from './modelDefinitions';
|
export * from './modelDefinitions';
|
||||||
export * from './normalizeBoundingBox';
|
export * from './normalizeBoundingBox';
|
||||||
@@ -44,6 +42,8 @@ export * from './unwrapBlitzkitResponse';
|
|||||||
export * from './votes';
|
export * from './votes';
|
||||||
// export * from './withCORSProxy';
|
// export * from './withCORSProxy';
|
||||||
export * from './createDefaultProvisions';
|
export * from './createDefaultProvisions';
|
||||||
|
export * from './gallery';
|
||||||
export * from './metaSortTank';
|
export * from './metaSortTank';
|
||||||
|
export * from './popularTanks';
|
||||||
export * from './romanize';
|
export * from './romanize';
|
||||||
export * from './youtubers';
|
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) => {
|
tankDefinitionsArray.map(async (tank) => {
|
||||||
const searchableNameDeburr: I18nString = { locales: {} };
|
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);
|
searchableNameDeburr.locales[key] = deburr(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: tank.id,
|
id: tank.id,
|
||||||
name: tank.name,
|
name: tank.name,
|
||||||
nameFull: tank.name_full,
|
searchableName: tank.name,
|
||||||
searchableName: tank.name_full ?? tank.name,
|
|
||||||
searchableNameDeburr,
|
searchableNameDeburr,
|
||||||
camouflages: tank.camouflages
|
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()
|
.flat()
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(deburr)
|
.map(deburr)
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './users';
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export * from './blitz';
|
export * from './blitz';
|
||||||
export * from './blitzkit';
|
export * from './blitzkit';
|
||||||
export * from './databases';
|
|
||||||
export * from './imgur';
|
export * from './imgur';
|
||||||
export * from './math';
|
export * from './math';
|
||||||
export * from './patreon';
|
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 './camouflage_definitions.ts';
|
||||||
export * from './consumable_definitions.ts';
|
export * from './consumable_definitions.ts';
|
||||||
export * from './equipment_definitions.ts';
|
export * from './equipment_definitions.ts';
|
||||||
|
export * from './gallery.ts';
|
||||||
export * from './game_definitions.ts';
|
export * from './game_definitions.ts';
|
||||||
export * from './i18n.ts';
|
export * from './i18n.ts';
|
||||||
export * from './map_definitions.ts';
|
export * from './map_definitions.ts';
|
||||||
export * from './model_definitions.ts';
|
export * from './model_definitions.ts';
|
||||||
|
export * from './popular_tanks.ts';
|
||||||
export * from './provision_definitions.ts';
|
export * from './provision_definitions.ts';
|
||||||
export * from './rating_leaderboard.ts';
|
export * from './rating_leaderboard.ts';
|
||||||
export * from './reviews.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 {
|
message TankDefinition {
|
||||||
required uint32 id = 1;
|
required uint32 id = 1;
|
||||||
map<uint32, uint32> roles = 2;
|
required string slug = 2;
|
||||||
required bool fixed_camouflage = 4;
|
map<uint32, uint32> roles = 3;
|
||||||
repeated int32 camouflages = 5;
|
required bool fixed_camouflage = 5;
|
||||||
repeated int32 ancestors = 6;
|
repeated int32 camouflages = 6;
|
||||||
repeated int32 successors = 7;
|
repeated int32 ancestors = 7;
|
||||||
repeated Crew crew = 8;
|
repeated int32 successors = 8;
|
||||||
required uint32 health = 9;
|
repeated Crew crew = 9;
|
||||||
required string nation = 10;
|
required uint32 health = 10;
|
||||||
required I18nString name = 11;
|
required string nation = 11;
|
||||||
optional I18nString name_full = 12;
|
required I18nString name = 12;
|
||||||
required TankType type = 13;
|
required TankType type = 13;
|
||||||
required uint32 max_consumables = 14;
|
required uint32 max_consumables = 14;
|
||||||
required uint32 max_provisions = 15;
|
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",
|
"mm": "%s1mm",
|
||||||
"deg": "%s1°",
|
"deg": "%s1°",
|
||||||
"hp": "%s1hp",
|
"hp": "%s1hp",
|
||||||
"x": "x%s1"
|
"x": "x%s1",
|
||||||
|
"percentage": "%s1%",
|
||||||
|
"deg_s": "%s1°/s"
|
||||||
},
|
},
|
||||||
|
|
||||||
"gun_types": {
|
"gun_types": {
|
||||||
@@ -739,6 +741,11 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"website": {
|
"website": {
|
||||||
|
"redirect": {
|
||||||
|
"title": "301: moved permanently",
|
||||||
|
"body": "This page has moved. Redirecting..."
|
||||||
|
},
|
||||||
|
|
||||||
"settings": {
|
"settings": {
|
||||||
"dev_mode": "Development mode",
|
"dev_mode": "Development mode",
|
||||||
"language": "Language"
|
"language": "Language"
|
||||||
@@ -756,7 +763,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
"clear_filters": "Clear",
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Search tanks...",
|
"search_bar_hint": "Search tanks...",
|
||||||
"recent": "Recently viewed",
|
"recent": "Recently viewed",
|
||||||
"select_all": "Select all",
|
"select_all": "Select all",
|
||||||
@@ -853,7 +860,24 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tools": {
|
"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": {
|
"tankopedia": {
|
||||||
|
"search": "Search a stat...",
|
||||||
|
|
||||||
"name": "Tankopedia",
|
"name": "Tankopedia",
|
||||||
"button": "Find tanks",
|
"button": "Find tanks",
|
||||||
"description": "Blitz tank encyclopedia",
|
"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_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.",
|
"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": {
|
"sandbox": {
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Hull down",
|
"hull_down": "Hull down",
|
||||||
@@ -1010,7 +1060,7 @@
|
|||||||
"characteristics": {
|
"characteristics": {
|
||||||
"title": "Characteristics",
|
"title": "Characteristics",
|
||||||
"compare": {
|
"compare": {
|
||||||
"label": "Compare against",
|
"label": "Compare",
|
||||||
"class": "Tier %s1 %s2",
|
"class": "Tier %s1 %s2",
|
||||||
"tier": "All of tier %s1",
|
"tier": "All of tier %s1",
|
||||||
"all": "All tanks"
|
"all": "All tanks"
|
||||||
@@ -1047,7 +1097,7 @@
|
|||||||
"explosionRadius": "Explosion radius",
|
"explosionRadius": "Explosion radius",
|
||||||
"shellVelocity": "Shell velocity",
|
"shellVelocity": "Shell velocity",
|
||||||
"aimTime": "Aim time",
|
"aimTime": "Aim time",
|
||||||
"dispersion": "Still",
|
"dispersion": "Dispersion still",
|
||||||
"dispersionMoving": "Moving",
|
"dispersionMoving": "Moving",
|
||||||
"dispersionHullTraversing": "Hull traversing",
|
"dispersionHullTraversing": "Hull traversing",
|
||||||
"dispersionTurretTraversing": "Turret traversing",
|
"dispersionTurretTraversing": "Turret traversing",
|
||||||
@@ -1061,28 +1111,28 @@
|
|||||||
"gunRearElevation": "Rear elevation",
|
"gunRearElevation": "Rear elevation",
|
||||||
"azimuthLeft": "Azimuth left",
|
"azimuthLeft": "Azimuth left",
|
||||||
"azimuthRight": "Azimuth right",
|
"azimuthRight": "Azimuth right",
|
||||||
"speedForwards": "Forwards",
|
"speedForwards": "Speed forwards",
|
||||||
"speedBackwards": "Backwards",
|
"speedBackwards": "Backwards",
|
||||||
"hardTerrainCoefficient": "Hard",
|
"hardTerrainCoefficient": "Terrain coefficient hard",
|
||||||
"hardTerrainCoefficientRaw": "Hard",
|
"hardTerrainCoefficientRaw": "Raw terrain coefficient hard",
|
||||||
"mediumTerrainCoefficient": "Medium",
|
"mediumTerrainCoefficient": "Medium",
|
||||||
"mediumTerrainCoefficientRaw": "Medium",
|
"mediumTerrainCoefficientRaw": "Medium",
|
||||||
"softTerrainCoefficient": "Soft",
|
"softTerrainCoefficient": "Soft",
|
||||||
"softTerrainCoefficientRaw": "Soft",
|
"softTerrainCoefficientRaw": "Soft",
|
||||||
"enginePower": "Engine power",
|
"enginePower": "Engine power",
|
||||||
"powerToWeightRatioHardTerrain": "Hard",
|
"powerToWeightRatioHardTerrain": "Power to weight ratio hard",
|
||||||
"powerToWeightRatioMediumTerrain": "Medium",
|
"powerToWeightRatioMediumTerrain": "Medium",
|
||||||
"powerToWeightRatioSoftTerrain": "Soft",
|
"powerToWeightRatioSoftTerrain": "Soft",
|
||||||
"weight": "Weight",
|
"weight": "Weight",
|
||||||
"turretTraverseSpeed": "Turret traverse speed",
|
"turretTraverseSpeed": "Turret traverse speed",
|
||||||
"gunTraverseSpeed": "Gun traverse speed",
|
"gunTraverseSpeed": "Gun traverse speed",
|
||||||
"hullTraverseHardTerrain": "Hard",
|
"hullTraverseHardTerrain": "Hull traverse hard",
|
||||||
"hullTraverseMediumTerrain": "Medium",
|
"hullTraverseMediumTerrain": "Medium",
|
||||||
"hullTraverseSoftTerrain": "Soft",
|
"hullTraverseSoftTerrain": "Soft",
|
||||||
"health": "Health",
|
"health": "Health",
|
||||||
"fireChance": "Fire chance",
|
"fireChance": "Fire chance",
|
||||||
"viewRange": "View range",
|
"viewRange": "View range",
|
||||||
"camouflageStill": "Still",
|
"camouflageStill": "Camouflage still",
|
||||||
"camouflageMoving": "Moving",
|
"camouflageMoving": "Moving",
|
||||||
"camouflageShootingStill": "Still",
|
"camouflageShootingStill": "Still",
|
||||||
"camouflageShootingMoving": "Moving",
|
"camouflageShootingMoving": "Moving",
|
||||||
|
|||||||
@@ -665,6 +665,7 @@
|
|||||||
"copied": "Copiado!"
|
"copied": "Copiado!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Buscar tanques...",
|
"search_bar_hint": "Buscar tanques...",
|
||||||
"recent": "Visto recientemente",
|
"recent": "Visto recientemente",
|
||||||
"select_all": "Seleccionar todo",
|
"select_all": "Seleccionar todo",
|
||||||
@@ -731,6 +732,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"discord": {
|
||||||
|
"join_short": "Join cool people!",
|
||||||
|
"join_long": "Join super duper cool people!",
|
||||||
|
"action": "Discord"
|
||||||
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"credit": "Por %s1 ❤️"
|
"credit": "Por %s1 ❤️"
|
||||||
},
|
},
|
||||||
@@ -744,6 +750,12 @@
|
|||||||
"help_translate": "¡Por favor ayuda a traducir %s1 a %s2!",
|
"help_translate": "¡Por favor ayuda a traducir %s1 a %s2!",
|
||||||
"seo_description": "Herramientas para todo lo que es World of Tanks Blitz"
|
"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": {
|
"tools": {
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"name": "Tankopedia",
|
"name": "Tankopedia",
|
||||||
@@ -888,7 +900,7 @@
|
|||||||
"characteristics": {
|
"characteristics": {
|
||||||
"title": "Características",
|
"title": "Características",
|
||||||
"compare": {
|
"compare": {
|
||||||
"label": "Comparar con",
|
"label": "Comparar",
|
||||||
"class": "Nivel %s1 - %s2",
|
"class": "Nivel %s1 - %s2",
|
||||||
"tier": "Todos de nivel %s1",
|
"tier": "Todos de nivel %s1",
|
||||||
"all": "Todos los tanques"
|
"all": "Todos los tanques"
|
||||||
|
|||||||
@@ -129,9 +129,9 @@
|
|||||||
"asia": "Asie-Pacifique"
|
"asia": "Asie-Pacifique"
|
||||||
},
|
},
|
||||||
"short": {
|
"short": {
|
||||||
"com": "NA",
|
"com": "Amérique du Nord",
|
||||||
"eu": "Europe",
|
"eu": "Europe",
|
||||||
"asia": "APAC"
|
"asia": "Asie-Pacifique"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"leagues": {
|
"leagues": {
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"subtitle": "Research",
|
"subtitle": "Recherche",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"research": "Recherche: %s1",
|
"research": "Recherche: %s1",
|
||||||
"upgrades": "Améliorations : %s1",
|
"upgrades": "Améliorations : %s1",
|
||||||
@@ -377,7 +377,7 @@
|
|||||||
"body": "# Commandes\n\nBlitzKit offre les commandes suivantes :\n\n"
|
"body": "# Commandes\n\nBlitzKit offre les commandes suivantes :\n\n"
|
||||||
},
|
},
|
||||||
"embeds": {
|
"embeds": {
|
||||||
"name": "embeds",
|
"name": "intègre",
|
||||||
"description": "How embeds work"
|
"description": "How embeds work"
|
||||||
},
|
},
|
||||||
"introduction": {
|
"introduction": {
|
||||||
@@ -457,7 +457,7 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"cards": {
|
"cards": {
|
||||||
"games": {
|
"games": {
|
||||||
"title": "Games",
|
"title": "Parties",
|
||||||
"winrate": "Taux de victoires",
|
"winrate": "Taux de victoires",
|
||||||
"battles": "Batailles",
|
"battles": "Batailles",
|
||||||
"wins_losses": "Victoires/Défaites"
|
"wins_losses": "Victoires/Défaites"
|
||||||
@@ -505,45 +505,45 @@
|
|||||||
"description": "Search clans",
|
"description": "Search clans",
|
||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "limit",
|
"name": "limite",
|
||||||
"description": "The max number of clan results (default: %s1)"
|
"description": "The max number of clan results (default: %s1)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"title": "Clan search for %s1 in region %s2",
|
"title": "Clan search for %s1 in region %s2",
|
||||||
"no_results": "No clans found"
|
"no_results": "Pas de clan trouvé"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_players": {
|
"search_players": {
|
||||||
"name": "search-players",
|
"name": "recherche de joueur",
|
||||||
"description": "Search players",
|
"description": "Recherche des joueurs",
|
||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "limit",
|
"name": "limite",
|
||||||
"description": "The max number of player results (default: %s1)"
|
"description": "Le nombre maximum de résultats de joueur (par défaut: %s1)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"title": "Player search for %s1 in region %s2",
|
"title": "Recherche de %s1 dans la région %s2",
|
||||||
"no_results": "No players found"
|
"no_results": "Pas de joueur de trouvé"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rating_leaderboard": {
|
"rating_leaderboard": {
|
||||||
"name": "rating-leaderboard",
|
"name": "Classement de côté",
|
||||||
"description": "Rating battles statistics",
|
"description": "Statistiques des batailles de cotés",
|
||||||
"subcommands": {
|
"subcommands": {
|
||||||
"neighbors": {
|
"neighbors": {
|
||||||
"name": "neighbors",
|
"name": "neighbors",
|
||||||
"description": "Neighboring players on the leaderboard"
|
"description": "Neighboring players on the leaderboard"
|
||||||
},
|
},
|
||||||
"league": {
|
"league": {
|
||||||
"name": "league",
|
"name": "ligue",
|
||||||
"description": "Top players of a league"
|
"description": "Top players of a league"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "limit",
|
"name": "limite",
|
||||||
"description": "How many neighbors to display (default: %s1)"
|
"description": "How many neighbors to display (default: %s1)"
|
||||||
},
|
},
|
||||||
"season": {
|
"season": {
|
||||||
@@ -554,7 +554,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"league": {
|
"league": {
|
||||||
"name": "league",
|
"name": "ligue",
|
||||||
"description": "The league to display"
|
"description": "The league to display"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -572,7 +572,7 @@
|
|||||||
"description": "Today's played tanks and statistics"
|
"description": "Today's played tanks and statistics"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"name": "stats",
|
"name": "statistiques",
|
||||||
"description": "Quick statistics for a period"
|
"description": "Quick statistics for a period"
|
||||||
},
|
},
|
||||||
"owned_tanks": {
|
"owned_tanks": {
|
||||||
@@ -665,6 +665,7 @@
|
|||||||
"copied": "Copied!"
|
"copied": "Copied!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Search tanks...",
|
"search_bar_hint": "Search tanks...",
|
||||||
"recent": "Recently viewed",
|
"recent": "Recently viewed",
|
||||||
"select_all": "Select all",
|
"select_all": "Select all",
|
||||||
@@ -731,6 +732,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"discord": {
|
||||||
|
"join_short": "Join cool people!",
|
||||||
|
"join_long": "Join super duper cool people!",
|
||||||
|
"action": "Discord"
|
||||||
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"credit": "By %s1 ❤️"
|
"credit": "By %s1 ❤️"
|
||||||
},
|
},
|
||||||
@@ -744,6 +750,12 @@
|
|||||||
"help_translate": "Please help translate %s1 to %s2!",
|
"help_translate": "Please help translate %s1 to %s2!",
|
||||||
"seo_description": "Tools for everything World of Tanks Blitz"
|
"seo_description": "Tools for everything World of Tanks Blitz"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"patches": {
|
||||||
|
"name": "BlitzKit Changelogs",
|
||||||
|
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"name": "Tankopedia",
|
"name": "Tankopedia",
|
||||||
@@ -888,7 +900,7 @@
|
|||||||
"characteristics": {
|
"characteristics": {
|
||||||
"title": "Characteristics",
|
"title": "Characteristics",
|
||||||
"compare": {
|
"compare": {
|
||||||
"label": "Compare against",
|
"label": "Compare",
|
||||||
"class": "Tier %s1 %s2",
|
"class": "Tier %s1 %s2",
|
||||||
"tier": "All of tier %s1",
|
"tier": "All of tier %s1",
|
||||||
"all": "All tanks"
|
"all": "All tanks"
|
||||||
@@ -1092,7 +1104,7 @@
|
|||||||
"lookup": "Look up a player to get started",
|
"lookup": "Look up a player to get started",
|
||||||
"hint": "Search players...",
|
"hint": "Search players...",
|
||||||
"searching": "Searching...",
|
"searching": "Searching...",
|
||||||
"no_results": "No players found"
|
"no_results": "Pas de joueur de trouvé"
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
"title": "Tracking %s1",
|
"title": "Tracking %s1",
|
||||||
|
|||||||
@@ -665,6 +665,7 @@
|
|||||||
"copied": "コピーしました!"
|
"copied": "コピーしました!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "車輌名を入力…",
|
"search_bar_hint": "車輌名を入力…",
|
||||||
"recent": "最近の閲覧",
|
"recent": "最近の閲覧",
|
||||||
"select_all": "全て選択",
|
"select_all": "全て選択",
|
||||||
@@ -731,6 +732,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"discord": {
|
||||||
|
"join_short": "Join cool people!",
|
||||||
|
"join_long": "Join super duper cool people!",
|
||||||
|
"action": "Discord"
|
||||||
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"credit": "By %s1 ❤️"
|
"credit": "By %s1 ❤️"
|
||||||
},
|
},
|
||||||
@@ -744,6 +750,12 @@
|
|||||||
"help_translate": "%s1の %s2への翻訳にご協力ください!",
|
"help_translate": "%s1の %s2への翻訳にご協力ください!",
|
||||||
"seo_description": "Tools for everything World of Tanks Blitz"
|
"seo_description": "Tools for everything World of Tanks Blitz"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"patches": {
|
||||||
|
"name": "BlitzKit Changelogs",
|
||||||
|
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"name": "Tankopedia",
|
"name": "Tankopedia",
|
||||||
@@ -888,7 +900,7 @@
|
|||||||
"characteristics": {
|
"characteristics": {
|
||||||
"title": "性能値",
|
"title": "性能値",
|
||||||
"compare": {
|
"compare": {
|
||||||
"label": "比較対象",
|
"label": "性能比較",
|
||||||
"class": "Tier %s1 %s2",
|
"class": "Tier %s1 %s2",
|
||||||
"tier": "Tier %s1 全車輌",
|
"tier": "Tier %s1 全車輌",
|
||||||
"all": "全車輌"
|
"all": "全車輌"
|
||||||
|
|||||||
@@ -665,6 +665,7 @@
|
|||||||
"copied": "Copied!"
|
"copied": "Copied!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Search tanks...",
|
"search_bar_hint": "Search tanks...",
|
||||||
"recent": "Recently viewed",
|
"recent": "Recently viewed",
|
||||||
"select_all": "Select all",
|
"select_all": "Select all",
|
||||||
@@ -731,6 +732,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"discord": {
|
||||||
|
"join_short": "Join cool people!",
|
||||||
|
"join_long": "Join super duper cool people!",
|
||||||
|
"action": "Discord"
|
||||||
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"credit": "By %s1 ❤️"
|
"credit": "By %s1 ❤️"
|
||||||
},
|
},
|
||||||
@@ -744,6 +750,12 @@
|
|||||||
"help_translate": "Please help translate %s1 to %s2!",
|
"help_translate": "Please help translate %s1 to %s2!",
|
||||||
"seo_description": "Tools for everything World of Tanks Blitz"
|
"seo_description": "Tools for everything World of Tanks Blitz"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"patches": {
|
||||||
|
"name": "BlitzKit Changelogs",
|
||||||
|
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"name": "Tankopedia",
|
"name": "Tankopedia",
|
||||||
@@ -888,7 +900,7 @@
|
|||||||
"characteristics": {
|
"characteristics": {
|
||||||
"title": "Characteristics",
|
"title": "Characteristics",
|
||||||
"compare": {
|
"compare": {
|
||||||
"label": "Compare against",
|
"label": "Compare",
|
||||||
"class": "Tier %s1 %s2",
|
"class": "Tier %s1 %s2",
|
||||||
"tier": "All of tier %s1",
|
"tier": "All of tier %s1",
|
||||||
"all": "All tanks"
|
"all": "All tanks"
|
||||||
|
|||||||
@@ -665,6 +665,7 @@
|
|||||||
"copied": "Copiado!"
|
"copied": "Copiado!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Procurar tanques...",
|
"search_bar_hint": "Procurar tanques...",
|
||||||
"recent": "Visto recentemente",
|
"recent": "Visto recentemente",
|
||||||
"select_all": "Selecionar todos",
|
"select_all": "Selecionar todos",
|
||||||
@@ -731,6 +732,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"discord": {
|
||||||
|
"join_short": "Join cool people!",
|
||||||
|
"join_long": "Join super duper cool people!",
|
||||||
|
"action": "Discord"
|
||||||
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"credit": "Por %s1 ❤️"
|
"credit": "Por %s1 ❤️"
|
||||||
},
|
},
|
||||||
@@ -744,6 +750,12 @@
|
|||||||
"help_translate": "Por favor, ajude a traduzir %s1 para %s2!",
|
"help_translate": "Por favor, ajude a traduzir %s1 para %s2!",
|
||||||
"seo_description": "Ferramentas para tudo World of Tanks Blitz"
|
"seo_description": "Ferramentas para tudo World of Tanks Blitz"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"patches": {
|
||||||
|
"name": "BlitzKit Changelogs",
|
||||||
|
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"name": "Tanquepedia",
|
"name": "Tanquepedia",
|
||||||
@@ -888,7 +900,7 @@
|
|||||||
"characteristics": {
|
"characteristics": {
|
||||||
"title": "Características",
|
"title": "Características",
|
||||||
"compare": {
|
"compare": {
|
||||||
"label": "Comparar com",
|
"label": "Comparar",
|
||||||
"class": "Nível %s1 %s2",
|
"class": "Nível %s1 %s2",
|
||||||
"tier": "Tudo de nível %s1",
|
"tier": "Tudo de nível %s1",
|
||||||
"all": "Todos os tanques"
|
"all": "Todos os tanques"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
"local": "dev",
|
"local": "dev",
|
||||||
"dev": "бета",
|
"dev": "бета",
|
||||||
"opentest": "открытый тест",
|
"opentest": "открытый тест",
|
||||||
"preview": "предварительный просмотр"
|
"preview": "превью"
|
||||||
},
|
},
|
||||||
"locales": {
|
"locales": {
|
||||||
"en": "Английский",
|
"en": "Английский",
|
||||||
@@ -665,6 +665,7 @@
|
|||||||
"copied": "Скопировано!"
|
"copied": "Скопировано!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"clear_filters": "Сбросить фильтры",
|
||||||
"search_bar_hint": "Поиск танков...",
|
"search_bar_hint": "Поиск танков...",
|
||||||
"recent": "Недавние",
|
"recent": "Недавние",
|
||||||
"select_all": "Выбрать всё",
|
"select_all": "Выбрать всё",
|
||||||
@@ -731,6 +732,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"discord": {
|
||||||
|
"join_short": "Присоединяйтесь к крутым ребятам!",
|
||||||
|
"join_long": "Присоединяйся к супер-дупер крутым ребятам!",
|
||||||
|
"action": "Discord"
|
||||||
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"credit": "От %s1 ❤️"
|
"credit": "От %s1 ❤️"
|
||||||
},
|
},
|
||||||
@@ -744,6 +750,12 @@
|
|||||||
"help_translate": "Пожалуйста, помогите перевести %s1 на %s2!",
|
"help_translate": "Пожалуйста, помогите перевести %s1 на %s2!",
|
||||||
"seo_description": "Инструменты для World of Tanks Blitz"
|
"seo_description": "Инструменты для World of Tanks Blitz"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"patches": {
|
||||||
|
"name": "Списки изменений BlitzKit",
|
||||||
|
"seo_description": "Список изменений для бота Discord и сайта BlitzKit"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"name": "Танкопедия",
|
"name": "Танкопедия",
|
||||||
@@ -888,7 +900,7 @@
|
|||||||
"characteristics": {
|
"characteristics": {
|
||||||
"title": "Характеристики",
|
"title": "Характеристики",
|
||||||
"compare": {
|
"compare": {
|
||||||
"label": "Сравнивать с",
|
"label": "Сравнение",
|
||||||
"class": "%s2 %s1 уровня",
|
"class": "%s2 %s1 уровня",
|
||||||
"tier": "Все %s1 уровня",
|
"tier": "Все %s1 уровня",
|
||||||
"all": "Все танки"
|
"all": "Все танки"
|
||||||
@@ -1131,7 +1143,7 @@
|
|||||||
"description": "Встраиваемые элементы для стримов",
|
"description": "Встраиваемые элементы для стримов",
|
||||||
"seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации",
|
"seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации",
|
||||||
"coming_soon": "Скоро появятся новые встраиваемые элементы.",
|
"coming_soon": "Скоро появятся новые встраиваемые элементы.",
|
||||||
"suggest": "Предложить идеи.",
|
"suggest": "Предложить идеи",
|
||||||
"preview": {
|
"preview": {
|
||||||
"title": "Предпросмотр",
|
"title": "Предпросмотр",
|
||||||
"upload": "Загрузить фон для теста"
|
"upload": "Загрузить фон для теста"
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
"m": "%s1м",
|
"m": "%s1м",
|
||||||
"mm": "%s1мм",
|
"mm": "%s1мм",
|
||||||
"deg": "%s1°",
|
"deg": "%s1°",
|
||||||
"hp": "%s1hp",
|
"hp": "%s1хп",
|
||||||
"x": "x%s1"
|
"x": "х%s1"
|
||||||
},
|
},
|
||||||
"gun_types": {
|
"gun_types": {
|
||||||
"auto_loader": "Магазинна система заряджання",
|
"auto_loader": "Магазинна система заряджання",
|
||||||
@@ -184,11 +184,11 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"clan_not_found": "# Не вдалося знайти клан\nНе вдалося знайти клан \"%s1\". Спробуйте вибрати клан з пошукового результату.",
|
"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_found": "# Не вдалося знайти користувача\nЯ не зміг знайти користувача \"%s1\". Спробуйте вибрати користувача з результату пошуку, ввести коректний нікнейм, або використати команду '/link'.",
|
||||||
"player_not_linked": "# Use `/link` to get started\nI don't know who you are but you can tell me today!",
|
"player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!",
|
||||||
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"%s1\".",
|
"tank_name_not_found": "# Танк не знайдено\nНе вдалось знайти танк за ім'ям \"%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.",
|
"tank_id_not_found": "# Танк не знайдено\nНе вдалось знайти танк за ID \"%s1\". Спробуйте ввести назву танка замість числа.",
|
||||||
"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.",
|
"no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.",
|
||||||
"get_help": "Допомогти Discord серверу",
|
"get_help": "Допомогти Discord серверу",
|
||||||
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
|
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
|
||||||
"preview": "# Ця команда є в попередньому перегляді\n\nВи можете отримати доступ лише до цієї команди на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH). Команда в цілому буде доступна найближчим часом.",
|
"preview": "# Ця команда є в попередньому перегляді\n\nВи можете отримати доступ лише до цієї команди на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH). Команда в цілому буде доступна найближчим часом.",
|
||||||
@@ -399,8 +399,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"breakdown": {
|
"breakdown": {
|
||||||
"name": "breakdown",
|
"name": "аналіз",
|
||||||
"description": "A period's breakdown by tanks played",
|
"description": "Аналіз періоду за зіграними танками",
|
||||||
"body": {
|
"body": {
|
||||||
"total": "Загалом",
|
"total": "Загалом",
|
||||||
"battles": "Бої",
|
"battles": "Бої",
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"title": "Clan search for %s1 in region %s2",
|
"title": "Clan search for %s1 in region %s2",
|
||||||
"no_results": "No clans found"
|
"no_results": "Жодного клану не знайдено"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_players": {
|
"search_players": {
|
||||||
@@ -665,6 +665,7 @@
|
|||||||
"copied": "Скопійовано!"
|
"copied": "Скопійовано!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Шукайте техніку...",
|
"search_bar_hint": "Шукайте техніку...",
|
||||||
"recent": "Недавно переглянуті",
|
"recent": "Недавно переглянуті",
|
||||||
"select_all": "Вибрати все",
|
"select_all": "Вибрати все",
|
||||||
@@ -731,6 +732,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"discord": {
|
||||||
|
"join_short": "Join cool people!",
|
||||||
|
"join_long": "Join super duper cool people!",
|
||||||
|
"action": "Discord"
|
||||||
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"credit": "Зроблено %s1 ❤"
|
"credit": "Зроблено %s1 ❤"
|
||||||
},
|
},
|
||||||
@@ -744,6 +750,12 @@
|
|||||||
"help_translate": "Будь ласка, допоможіть перекласти %s1 на %s2!",
|
"help_translate": "Будь ласка, допоможіть перекласти %s1 на %s2!",
|
||||||
"seo_description": "Інструменти для всього, пов'язаного з WoT Blitz"
|
"seo_description": "Інструменти для всього, пов'язаного з WoT Blitz"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"patches": {
|
||||||
|
"name": "BlitzKit Changelogs",
|
||||||
|
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"name": "Танкопедія",
|
"name": "Танкопедія",
|
||||||
@@ -888,7 +900,7 @@
|
|||||||
"characteristics": {
|
"characteristics": {
|
||||||
"title": "Characteristics",
|
"title": "Characteristics",
|
||||||
"compare": {
|
"compare": {
|
||||||
"label": "Compare against",
|
"label": "Порівняти",
|
||||||
"class": "Tier %s1 %s2",
|
"class": "Tier %s1 %s2",
|
||||||
"tier": "All of tier %s1",
|
"tier": "All of tier %s1",
|
||||||
"all": "Усі танки"
|
"all": "Усі танки"
|
||||||
|
|||||||
@@ -665,6 +665,7 @@
|
|||||||
"copied": "已複製!"
|
"copied": "已複製!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "搜尋戰車....",
|
"search_bar_hint": "搜尋戰車....",
|
||||||
"recent": "最近查看",
|
"recent": "最近查看",
|
||||||
"select_all": "選擇全部",
|
"select_all": "選擇全部",
|
||||||
@@ -731,6 +732,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"discord": {
|
||||||
|
"join_short": "Join cool people!",
|
||||||
|
"join_long": "Join super duper cool people!",
|
||||||
|
"action": "Discord"
|
||||||
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"credit": "由 %s1 製作❤️"
|
"credit": "由 %s1 製作❤️"
|
||||||
},
|
},
|
||||||
@@ -744,6 +750,12 @@
|
|||||||
"help_translate": "請幫忙將 %s1 翻譯成 %s2!",
|
"help_translate": "請幫忙將 %s1 翻譯成 %s2!",
|
||||||
"seo_description": "戰車世界閃擊戰 全方位工具"
|
"seo_description": "戰車世界閃擊戰 全方位工具"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"patches": {
|
||||||
|
"name": "BlitzKit Changelogs",
|
||||||
|
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"name": "戰車百科",
|
"name": "戰車百科",
|
||||||
@@ -888,7 +900,7 @@
|
|||||||
"characteristics": {
|
"characteristics": {
|
||||||
"title": "属性",
|
"title": "属性",
|
||||||
"compare": {
|
"compare": {
|
||||||
"label": "比较",
|
"label": "比較",
|
||||||
"class": "%s1 %s2 级",
|
"class": "%s1 %s2 级",
|
||||||
"tier": "所有级别 %s1",
|
"tier": "所有级别 %s1",
|
||||||
"all": "所有坦克"
|
"all": "所有坦克"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
SUPPORTED_LOCALES,
|
SUPPORTED_LOCALES,
|
||||||
} from '@blitzkit/i18n';
|
} from '@blitzkit/i18n';
|
||||||
import { readdir } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
|
import { deburr } from 'lodash-es';
|
||||||
import { parse as parsePath } from 'path';
|
import { parse as parsePath } from 'path';
|
||||||
import { Vector3Tuple } from 'three';
|
import { Vector3Tuple } from 'three';
|
||||||
import { parse as parseYaml } from 'yaml';
|
import { parse as parseYaml } from 'yaml';
|
||||||
@@ -48,6 +49,23 @@ import { DATA } from './constants';
|
|||||||
import { Avatar } from './skillIcons';
|
import { Avatar } from './skillIcons';
|
||||||
import { TankParameters } from './tankIcons';
|
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) {
|
function parseResearchCost(raw: number | string) {
|
||||||
if (typeof raw === 'number') {
|
if (typeof raw === 'number') {
|
||||||
return {
|
return {
|
||||||
@@ -502,7 +520,7 @@ const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
|
|||||||
HIGH_EXPLOSIVE: ShellType.HE,
|
HIGH_EXPLOSIVE: ShellType.HE,
|
||||||
HOLLOW_CHARGE: ShellType.HEAT,
|
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> = {
|
const blitzModuleTypeToBlitzkit: Record<keyof BlitzModuleType, ModuleType> = {
|
||||||
chassis: ModuleType.TRACKS,
|
chassis: ModuleType.TRACKS,
|
||||||
engine: ModuleType.ENGINE,
|
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(
|
await Promise.all(
|
||||||
nations.map(async (nation) => {
|
nations.map(async (nation) => {
|
||||||
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||||
@@ -787,6 +892,13 @@ export async function definitions() {
|
|||||||
const hullArmor: Armor = { spaced: [], thickness: {} };
|
const hullArmor: Armor = { spaced: [], thickness: {} };
|
||||||
const equipment = tankDefinition.root.optDevicePreset;
|
const equipment = tankDefinition.root.optDevicePreset;
|
||||||
tankStringIdMap[`${nation}:${tankKey}`] = tankId;
|
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) {
|
if (tank.sellPrice) {
|
||||||
tankPrice = {
|
tankPrice = {
|
||||||
@@ -903,7 +1015,7 @@ export async function definitions() {
|
|||||||
(tank.shortUserString
|
(tank.shortUserString
|
||||||
? getString(tank.shortUserString)
|
? getString(tank.shortUserString)
|
||||||
: undefined) ?? getString(tank.userString),
|
: undefined) ?? getString(tank.userString),
|
||||||
name_full: getString(tank.userString),
|
slug,
|
||||||
nation,
|
nation,
|
||||||
type: tankTags.includes('collectible')
|
type: tankTags.includes('collectible')
|
||||||
? TankType.COLLECTOR
|
? 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] = {
|
modelDefinitions.models[tankId] = {
|
||||||
armor: hullArmor,
|
armor: hullArmor,
|
||||||
turret_origin: vector3TupleToBlitzkit(turretOrigin),
|
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 { equipmentIcons } from './equipmentIcons';
|
||||||
import { flags } from './flags';
|
import { flags } from './flags';
|
||||||
import { gameModeBanners } from './gameModeBanners';
|
import { gameModeBanners } from './gameModeBanners';
|
||||||
|
import { glossary } from './glossary';
|
||||||
|
import { migration } from './migration';
|
||||||
import { moduleIcons } from './moduleIcons';
|
import { moduleIcons } from './moduleIcons';
|
||||||
|
import { popularTanks } from './popularTanks';
|
||||||
import { shellIcons } from './shellIcons';
|
import { shellIcons } from './shellIcons';
|
||||||
import { skillIcons } from './skillIcons';
|
import { skillIcons } from './skillIcons';
|
||||||
import { tankArmors } from './tankArmors';
|
import { tankArmors } from './tankArmors';
|
||||||
@@ -28,6 +31,7 @@ import { tankModels } from './tankModels';
|
|||||||
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
||||||
|
|
||||||
const methods = [
|
const methods = [
|
||||||
|
migration,
|
||||||
definitions,
|
definitions,
|
||||||
tankModels,
|
tankModels,
|
||||||
tankArmors,
|
tankArmors,
|
||||||
@@ -43,6 +47,8 @@ const methods = [
|
|||||||
boosterIcons,
|
boosterIcons,
|
||||||
tankIcons,
|
tankIcons,
|
||||||
blitzkitTankIcons,
|
blitzkitTankIcons,
|
||||||
|
popularTanks,
|
||||||
|
glossary,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const method of methods) {
|
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;
|
break;
|
||||||
|
|
||||||
case 'TransformComponent': {
|
case 'TransformComponent': {
|
||||||
const translation = new Vector3();
|
node.setTranslation(component['tc.localTranslation']);
|
||||||
const rotation = new Quaternion();
|
node.setRotation(component['tc.localRotation']);
|
||||||
const scale = new Vector3();
|
node.setScale(component['tc.localScale']);
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { assertSecret } from '@blitzkit/core';
|
import { assertSecret } from '@blitzkit/core';
|
||||||
|
import { chunk } from 'lodash-es';
|
||||||
import { commitMultipleFiles, FileChange } from './commitMultipleFiles';
|
import { commitMultipleFiles, FileChange } from './commitMultipleFiles';
|
||||||
|
|
||||||
export async function commitAssets(message: string, changes: FileChange[]) {
|
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;
|
if (changes.length === 0) return;
|
||||||
|
|
||||||
await commitMultipleFiles(
|
const chunks = chunk(changes, 64);
|
||||||
assertSecret(import.meta.env.PUBLIC_ASSET_REPO),
|
let chunkIndex = 0;
|
||||||
assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
|
|
||||||
`${message} - ${new Date().toDateString()}`,
|
for (const chunk of chunks) {
|
||||||
changes,
|
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;
|
content: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const heuristicFormats = ['glb', 'webp', 'jpg', 'png'];
|
||||||
|
|
||||||
export async function commitMultipleFiles(
|
export async function commitMultipleFiles(
|
||||||
repoRaw: string,
|
repoRaw: string,
|
||||||
branch: string,
|
branch: string,
|
||||||
@@ -21,29 +23,69 @@ export async function commitMultipleFiles(
|
|||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://raw.githubusercontent.com/${blobPath}`,
|
`https://raw.githubusercontent.com/${blobPath}`,
|
||||||
);
|
);
|
||||||
|
let isNew: boolean;
|
||||||
|
let isDifferent: boolean;
|
||||||
|
let diff: number;
|
||||||
|
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
console.log(
|
isNew = true;
|
||||||
`🟢 (+${change.content.length.toLocaleString()}B) ${blobPath}`,
|
diff = change.content.length;
|
||||||
);
|
isDifferent = true;
|
||||||
changes.push(change);
|
} 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) {
|
} else if (response.status === 200) {
|
||||||
|
isNew = false;
|
||||||
const buffer = Buffer.from(await response.arrayBuffer());
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
|
|
||||||
if (!buffer.equals(change.content)) {
|
if (buffer.equals(change.content)) {
|
||||||
const diff = change.content.length - buffer.length;
|
diff = 0;
|
||||||
|
isDifferent = false;
|
||||||
console.log(
|
} else {
|
||||||
`🟡 (${diff > 0 ? '+' : ''}${diff.toLocaleString()}B) ${blobPath}`,
|
diff = change.content.length - buffer.length;
|
||||||
);
|
isDifferent = true;
|
||||||
|
|
||||||
changes.push(change);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unexpected status code ${response.status} for ${blobPath}`,
|
`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 react from '@astrojs/react';
|
||||||
import sitemap from '@astrojs/sitemap';
|
import sitemap from '@astrojs/sitemap';
|
||||||
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
|
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
|
||||||
import playformInline from '@playform/inline';
|
|
||||||
import compressor from 'astro-compressor';
|
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -23,14 +21,7 @@ export default defineConfig({
|
|||||||
outDir: '../../dist/website',
|
outDir: '../../dist/website',
|
||||||
prefetch: true,
|
prefetch: true,
|
||||||
|
|
||||||
integrations: [
|
integrations: [react(), sitemap(), partytown()],
|
||||||
react(),
|
|
||||||
sitemap(),
|
|
||||||
partytown(),
|
|
||||||
// playformCompress(),
|
|
||||||
compressor(),
|
|
||||||
playformInline(),
|
|
||||||
],
|
|
||||||
vite: {
|
vite: {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@astrolib/seo": "^1.0.0-beta.8",
|
"@astrolib/seo": "^1.0.0-beta.8",
|
||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
|
"@fontsource/inter": "^5.2.5",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@nanostores/react": "^0.7.3",
|
"@nanostores/react": "^0.7.3",
|
||||||
"@nivo/bar": "^0.87.0",
|
"@nivo/bar": "^0.87.0",
|
||||||
@@ -25,11 +26,11 @@
|
|||||||
"@react-three/fiber": "^8.17.10",
|
"@react-three/fiber": "^8.17.10",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"googleapis": "^144.0.0",
|
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"markdown-to-jsx": "^7.5.0",
|
"markdown-to-jsx": "^7.5.0",
|
||||||
"nanostores": "^0.11.3",
|
"nanostores": "^0.11.3",
|
||||||
|
"quicklime": "^1.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "^18.3.1",
|
"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' }}
|
style={{ pointerEvents: highlightArmor.editingPlate ? 'auto' : 'none' }}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
|
variant="classic"
|
||||||
mb="4"
|
mb="4"
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
@@ -72,6 +73,7 @@ export function ArmorPlateDisplay() {
|
|||||||
{highlightArmor.editingPlate && (
|
{highlightArmor.editingPlate && (
|
||||||
<Flex gap="2" align="center">
|
<Flex gap="2" align="center">
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
|
variant="classic"
|
||||||
style={{ width: 96 }}
|
style={{ width: 96 }}
|
||||||
mt="2"
|
mt="2"
|
||||||
ref={input}
|
ref={input}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import {
|
|||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { invalidate, useFrame } from '@react-three/fiber';
|
import { invalidate, useFrame } from '@react-three/fiber';
|
||||||
import { useEffect } from 'react';
|
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 { degToRad } from 'three/src/math/MathUtils.js';
|
||||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
||||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
||||||
@@ -36,27 +43,33 @@ export function PrimaryArmorSceneComponent({
|
|||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
|
|
||||||
|
fog: true,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
|
||||||
uniforms: {
|
uniforms: UniformsUtils.merge([
|
||||||
thickness: { value: null },
|
UniformsLib.common,
|
||||||
penetration: { value: null },
|
UniformsLib.fog,
|
||||||
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() },
|
thickness: { value: null },
|
||||||
spacedArmorBuffer: { value: null },
|
penetration: { value: null },
|
||||||
spacedArmorDepth: { 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(() => {
|
useEffect(() => {
|
||||||
@@ -198,16 +211,30 @@ export function PrimaryArmorSceneComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{jsxTree(
|
{jsxTree(node, {
|
||||||
node,
|
mesh(_, props, key) {
|
||||||
{ mesh: { renderOrder: 0, material: excludeMaterial } },
|
return (
|
||||||
`${node.uuid}-exclude`,
|
<mesh
|
||||||
)}
|
{...props}
|
||||||
{jsxTree(
|
key={`${key}-exclude`}
|
||||||
node,
|
renderOrder={0}
|
||||||
{ mesh: { renderOrder: 1, material } },
|
material={excludeMaterial}
|
||||||
`${node.uuid}-include`,
|
/>
|
||||||
)}
|
);
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
{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 vNormal;
|
||||||
varying vec3 vViewPosition;
|
varying vec3 vViewPosition;
|
||||||
|
|
||||||
@@ -39,6 +43,8 @@ vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
#include <clipping_planes_fragment>
|
||||||
|
|
||||||
vec2 screenCoord = gl_FragCoord.xy / resolution;
|
vec2 screenCoord = gl_FragCoord.xy / resolution;
|
||||||
vec4 spacedArmorData = texture2D(spacedArmorBuffer, screenCoord);
|
vec4 spacedArmorData = texture2D(spacedArmorBuffer, screenCoord);
|
||||||
bool isUnderSpacedArmor = spacedArmorData.a != 0.0;
|
bool isUnderSpacedArmor = spacedArmorData.a != 0.0;
|
||||||
@@ -111,4 +117,6 @@ void main() {
|
|||||||
} else {
|
} else {
|
||||||
gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
|
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 vNormal;
|
||||||
varying vec3 vViewPosition;
|
varying vec3 vViewPosition;
|
||||||
|
|
||||||
void main() {
|
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;
|
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];
|
const inTitleColor = shotStatusColors[shot.in.status];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card style={{ width: 300 }} {...props}>
|
<Card variant="classic" style={{ width: 300 }} {...props}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Flex direction="column" gap="1">
|
<Flex direction="column" gap="1">
|
||||||
<Text color={inTitleColor} weight="bold">
|
<Text color={inTitleColor} weight="bold">
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
return (
|
return (
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
scene={scene}
|
scene={scene}
|
||||||
key={node.uuid}
|
key={`${node.uuid}-hull`}
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
node={node}
|
node={node}
|
||||||
@@ -93,7 +93,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
return (
|
return (
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
scene={scene}
|
scene={scene}
|
||||||
key={node.uuid}
|
key={`${node.uuid}-track`}
|
||||||
type={ArmorType.External}
|
type={ArmorType.External}
|
||||||
thickness={trackModelDefinition.thickness}
|
thickness={trackModelDefinition.thickness}
|
||||||
variant="track"
|
variant="track"
|
||||||
@@ -127,7 +127,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
<group position={turretOrigin}>
|
<group position={turretOrigin}>
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
scene={scene}
|
scene={scene}
|
||||||
key={node.uuid}
|
key={`${node.uuid}-turret`}
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
node={node}
|
node={node}
|
||||||
@@ -158,7 +158,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={hullOrigin} key={node.uuid}>
|
<group position={hullOrigin} key={`${node.uuid}-gun-spaced}`}>
|
||||||
<group position={turretOrigin.clone().add(gunOrigin)}>
|
<group position={turretOrigin.clone().add(gunOrigin)}>
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
scene={scene}
|
scene={scene}
|
||||||
@@ -185,7 +185,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
return (
|
return (
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
scene={scene}
|
scene={scene}
|
||||||
key={node.uuid}
|
key={`${node.uuid}-gun-external`}
|
||||||
type={ArmorType.External}
|
type={ArmorType.External}
|
||||||
thickness={gunModelDefinition.thickness}
|
thickness={gunModelDefinition.thickness}
|
||||||
variant="gun"
|
variant="gun"
|
||||||
|
|||||||
@@ -114,33 +114,57 @@ export function SpacedArmorSubExternal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{jsxTree(
|
{jsxTree(node, {
|
||||||
node,
|
group(_, props, key) {
|
||||||
{
|
return (
|
||||||
mesh: {
|
<group {...props} key={`${key}-spaced-sub-external-exclude`} />
|
||||||
renderOrder: 3,
|
);
|
||||||
material: new MeshBasicMaterial({
|
|
||||||
colorWrite: false,
|
|
||||||
depthTest: true,
|
|
||||||
depthWrite: true,
|
|
||||||
...(clip ? { clippingPlanes: [clip] } : {}),
|
|
||||||
}),
|
|
||||||
onClick() {},
|
|
||||||
userData: {
|
|
||||||
type: ArmorType.External,
|
|
||||||
thickness,
|
|
||||||
variant,
|
|
||||||
} satisfies ArmorUserData,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
`${node.uuid}-exclude`,
|
|
||||||
)}
|
|
||||||
|
|
||||||
{jsxTree(
|
mesh(_, props, key) {
|
||||||
node,
|
return (
|
||||||
{ mesh: { renderOrder: 4, material } },
|
<mesh
|
||||||
`${node.uuid}-include`,
|
{...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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{jsxTree(
|
{jsxTree(node, {
|
||||||
node,
|
group(_, props, key) {
|
||||||
{
|
return <group {...props} key={`${key}-spaced-sub-spaced-exclude`} />;
|
||||||
mesh: {
|
|
||||||
renderOrder: 2,
|
|
||||||
material,
|
|
||||||
onClick() {},
|
|
||||||
userData: {
|
|
||||||
type: ArmorType.Spaced,
|
|
||||||
thickness,
|
|
||||||
} satisfies ArmorUserData,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
`${node.uuid}-exclude`,
|
|
||||||
)}
|
|
||||||
|
|
||||||
{jsxTree(
|
mesh(_, props, key) {
|
||||||
node,
|
return (
|
||||||
{
|
<mesh
|
||||||
mesh: {
|
{...props}
|
||||||
renderOrder: 5,
|
key={`${key}-spaced-sub-spaced-exclude`}
|
||||||
material: depthWriteMaterial,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{props.type === ArmorType.Primary &&
|
{props.type === ArmorType.Primary &&
|
||||||
jsxTree(
|
jsxTree(node, {
|
||||||
node,
|
mesh(_, props, key) {
|
||||||
{
|
return (
|
||||||
mesh: {
|
<mesh
|
||||||
renderOrder: 0,
|
{...props}
|
||||||
material: omitMaterial,
|
key={`${key}-spaced-component`}
|
||||||
userData: {
|
renderOrder={0}
|
||||||
type: ArmorType.Primary,
|
material={omitMaterial}
|
||||||
thickness,
|
userData={
|
||||||
} satisfies ArmorUserData,
|
{
|
||||||
async onClick(event) {
|
type: ArmorType.Primary,
|
||||||
event.stopPropagation();
|
thickness,
|
||||||
const shot = (await shoot(
|
} satisfies ArmorUserData
|
||||||
event.point,
|
}
|
||||||
event.intersections,
|
onClick={async (event) => {
|
||||||
true,
|
event.stopPropagation();
|
||||||
))!;
|
const shot = (await shoot(
|
||||||
tankopediaEphemeralStore.setState({ shot });
|
event.point,
|
||||||
},
|
event.intersections,
|
||||||
},
|
true,
|
||||||
|
))!;
|
||||||
|
tankopediaEphemeralStore.setState({ shot });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
node.uuid,
|
})}
|
||||||
)}
|
|
||||||
|
|
||||||
{props.type === ArmorType.Spaced && (
|
{props.type === ArmorType.Spaced && (
|
||||||
<SpacedArmorSubSpaced node={node} thickness={thickness} />
|
<SpacedArmorSubSpaced node={node} thickness={thickness} />
|
||||||
|
|||||||
@@ -215,50 +215,62 @@ export function StaticArmorSceneComponent({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{jsxTree(node, {
|
{jsxTree(node, {
|
||||||
mesh: {
|
mesh(_, meshProps, key) {
|
||||||
material: surfaceMaterial,
|
return (
|
||||||
onPointerDown,
|
<mesh
|
||||||
userData: {
|
{...meshProps}
|
||||||
type: props.type,
|
key={key}
|
||||||
variant: props.type === ArmorType.External ? props.variant : 'gun',
|
material={surfaceMaterial}
|
||||||
thickness,
|
onPointerDown={onPointerDown}
|
||||||
} satisfies ArmorUserData,
|
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) {
|
event.stopPropagation();
|
||||||
if (discardClippingPlane(event.object, event.point)) return;
|
|
||||||
|
|
||||||
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);
|
mutateTankopediaEphemeralStore((draft) => {
|
||||||
const point = bounds.min
|
draft.highlightArmor = {
|
||||||
.clone()
|
type: props.type,
|
||||||
.add(bounds.max)
|
name,
|
||||||
.divideScalar(2)
|
point,
|
||||||
.setY(bounds.max.y);
|
thickness,
|
||||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
thicknessAngled,
|
||||||
const surfaceNormal = event
|
angle,
|
||||||
.normal!.clone()
|
color: `#${color.getHexString()}`,
|
||||||
.applyQuaternion(
|
editingPlate: editStatic,
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { memo, useState } from 'react';
|
||||||
import { DepthTexture, Scene, Vector2 } from 'three';
|
import { DepthTexture, Scene, Vector2 } from 'three';
|
||||||
import { PrimaryArmorScene } from './components/PrimaryArmorScene';
|
import { PrimaryArmorScene } from './components/PrimaryArmorScene';
|
||||||
@@ -6,8 +6,14 @@ import { spacedArmorRenderTarget } from './components/PrimaryArmorSceneComponent
|
|||||||
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
||||||
|
|
||||||
export const Armor = memo(() => {
|
export const Armor = memo(() => {
|
||||||
|
const rootScene = useThree((state) => state.scene);
|
||||||
const [spacedArmorScene] = useState(() => new 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(
|
const spacedArmorPortal = createPortal(
|
||||||
<SpacedArmorScene scene={spacedArmorScene} />,
|
<SpacedArmorScene scene={spacedArmorScene} />,
|
||||||
spacedArmorScene,
|
spacedArmorScene,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||||
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
||||||
|
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from '../../hooks/useLocale';
|
||||||
@@ -15,7 +16,10 @@ interface ControlsProps {
|
|||||||
onAddTankDialogOpenChange: (open: boolean) => void;
|
onAddTankDialogOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
||||||
|
awaitableProvisionDefinitions,
|
||||||
|
awaitableModelDefinitions,
|
||||||
|
]);
|
||||||
|
|
||||||
export function Controls({
|
export function Controls({
|
||||||
addTankDialogOpen,
|
addTankDialogOpen,
|
||||||
@@ -56,9 +60,11 @@ export function Controls({
|
|||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
|
const model = modelDefinitions.models[tank.id];
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
mutateCompareEphemeral((draft) => {
|
||||||
draft.members.push(
|
draft.members.push(
|
||||||
tankToCompareMember(tank, provisionDefinitions),
|
tankToCompareMember(tank, model, provisionDefinitions),
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -67,9 +73,15 @@ export function Controls({
|
|||||||
onSelectAll={(tanks) => {
|
onSelectAll={(tanks) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
mutateCompareEphemeral((draft) => {
|
||||||
draft.members.push(
|
draft.members.push(
|
||||||
...tanks.map((tank) =>
|
...tanks.map((tank) => {
|
||||||
tankToCompareMember(tank, provisionDefinitions),
|
const model = modelDefinitions.models[tank.id];
|
||||||
),
|
|
||||||
|
return tankToCompareMember(
|
||||||
|
tank,
|
||||||
|
model,
|
||||||
|
provisionDefinitions,
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -95,6 +107,7 @@ export function Controls({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<SegmentedControl.Root
|
<SegmentedControl.Root
|
||||||
|
variant="classic"
|
||||||
value={deltaMode}
|
value={deltaMode}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateComparePersistent((draft) => {
|
mutateComparePersistent((draft) => {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
return (
|
return (
|
||||||
<StickyColumnHeaderCell>
|
<StickyColumnHeaderCell>
|
||||||
<Flex direction="column" align="center" justify="between" gap="2">
|
<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
|
<Flex
|
||||||
ref={draggable}
|
ref={draggable}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from '../../hooks/useLocale';
|
||||||
@@ -10,12 +11,15 @@ import { TankSearch } from '../TankSearch';
|
|||||||
|
|
||||||
interface TankControlProps {
|
interface TankControlProps {
|
||||||
index: number;
|
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 { locale } = useLocale();
|
||||||
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||||
@@ -57,9 +61,12 @@ export function TankControl({ index, id }: TankControlProps) {
|
|||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
|
const model = modelDefinitions.models[tank.id];
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
mutateCompareEphemeral((draft) => {
|
||||||
draft.members[index] = tankToCompareMember(
|
draft.members[index] = tankToCompareMember(
|
||||||
tank,
|
tank,
|
||||||
|
model,
|
||||||
provisionDefinitions,
|
provisionDefinitions,
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
@@ -72,11 +79,7 @@ export function TankControl({ index, id }: TankControlProps) {
|
|||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
<LinkI18n
|
<LinkI18n locale={locale} href={`/tanks/${slug}`} target="_blank">
|
||||||
locale={locale}
|
|
||||||
href={`/tools/tankopedia/${id}`}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<IconButton variant="ghost">
|
<IconButton variant="ghost">
|
||||||
<ExternalLinkIcon />
|
<ExternalLinkIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Switch } from '@radix-ui/themes';
|
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';
|
import { EmbedState } from '../../stores/embedState';
|
||||||
|
|
||||||
export function Boolean({ configKey }: EmbedPreviewControllerProps) {
|
export function Boolean({ configKey }: EmbedPreviewControllerProps) {
|
||||||
@@ -7,7 +7,7 @@ export function Boolean({ configKey }: EmbedPreviewControllerProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
variant="soft"
|
variant="classic"
|
||||||
checked={EmbedState.use((state) => state[configKey] as boolean)}
|
checked={EmbedState.use((state) => state[configKey] as boolean)}
|
||||||
onCheckedChange={() => {
|
onCheckedChange={() => {
|
||||||
mutateEmbedState((draft) => {
|
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 { EmbedState } from '../../stores/embedState';
|
||||||
import { ColorControllerRaw } from './ColorControllerRaw';
|
import { ColorControllerRaw } from './ColorControllerRaw';
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function ColorControllerRaw({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
variant="soft"
|
variant="classic"
|
||||||
min={0}
|
min={0}
|
||||||
max={255}
|
max={255}
|
||||||
value={[alpha]}
|
value={[alpha]}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Select } from '@radix-ui/themes';
|
import { Select } from '@radix-ui/themes';
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
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 { EmbedState } from '../../stores/embedState';
|
||||||
import type {
|
import type {
|
||||||
EmbedConfigItemType,
|
EmbedConfigItemType,
|
||||||
@@ -25,7 +25,8 @@ export function Enum({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Select.Trigger />
|
<Select.Trigger variant="classic" />
|
||||||
|
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{config.options.map((option) => (
|
{config.options.map((option) => (
|
||||||
<Select.Item key={option.value} value={option.value}>
|
<Select.Item key={option.value} value={option.value}>
|
||||||
|
|||||||