Compare commits

...
62 Commits
Author SHA1 Message Date
tresabhi 6d6bae847b feat: crowdin 2024-12-19 11:18:05 -06:00
tresabhi cc70b3ccc3 feat: logs 2024-12-19 11:16:27 -06:00
tresabhi e3bf619808 fix: remove comments 2024-12-19 11:09:32 -06:00
tresabhi f6cb4f2518 more adrenaline 2024-12-19 11:09:13 -06:00
tresabhi f396d6c4a1 fix: use stricter localizaiton 2024-12-19 11:04:48 -06:00
tresabhi 768c420ae4 Merge branch 'main' into dev 2024-12-19 10:41:46 -06:00
TrèsAbhi 7fba197bef Merge pull request #133 from tresabhi/l10n_main
New Crowdin updates
2024-12-19 10:41:26 -06:00
tresabhi b074481e1a fix: performance tool use performance 2024-12-19 10:29:32 -06:00
tresabhi 987e35ef8f feat: sticky headers for performance 2024-12-19 10:28:44 -06:00
tresabhi 83c77b5945 fix: make tank loader icon smaller 2024-12-19 10:20:09 -06:00
tresabhi e4f0120d67 feat: use blitzkit icons by default 2024-12-19 10:19:26 -06:00
tresabhi 2055c00154 Merge branch 'main' into dev 2024-12-17 21:49:58 -06:00
tresabhi c814230dcb feat: better loader 2024-12-17 21:49:55 -06:00
tresabhi 2f31aadb29 feat: better text in header 2024-12-17 21:46:44 -06:00
tresabhi 3113bf3a47 fix: filters 2024-12-17 21:06:11 -06:00
tresabhi 70bd54e03b feat: remove naked lighting 2024-12-17 19:45:48 -06:00
tresabhi 8863ac5fde feat: some icon only lighting 2024-12-17 11:12:04 -06:00
TrèsAbhi b62009ce2b Update Party3.astro 2024-12-17 09:58:49 -06:00
tresabhi 0f3d230f42 feat: better sizing for new icons 2024-12-16 23:42:38 -06:00
tresabhi 9db6b997c3 fix: use tanks key 2024-12-16 22:44:02 -06:00
tresabhi d9fcbd028b fix: re add blitzkit tank icons 2024-12-16 22:42:27 -06:00
tresabhi db74058075 feat: new angle 2024-12-16 22:40:36 -06:00
tresabhi 4180736d37 feat: neater code 2024-12-16 13:32:31 -06:00
tresabhi 382f04229e feat: better icons and sizing 2024-12-16 13:30:35 -06:00
tresabhi 3c9ba9ec53 feat: increased max recently viewed tank count to 14 2024-12-16 13:21:06 -06:00
tresabhi 4848234264 feat: custom tank icons 2024-12-16 12:41:24 -06:00
tresabhi 2ad0fcfdf9 feat: better hdr 2024-12-16 12:24:37 -06:00
tresabhi 8f0894364b fix: remove nitro ads 2024-12-16 11:01:07 -06:00
tresabhi 3695b8f7f4 fix: tag 2024-12-16 10:59:26 -06:00
tresabhi 4f8c1d416f feat: delete rando 2024-12-16 10:50:52 -06:00
tresabhi 453286677a Merge branch 'l10n_main' into dev 2024-12-16 10:49:54 -06:00
TrèsAbhi 53a49f199a New translations en-us.json (Croatian) 2024-12-16 10:49:28 -06:00
TrèsAbhi fdfde46342 New translations en-us.json (Ukrainian) 2024-12-16 10:49:26 -06:00
TrèsAbhi 1d836b2053 New translations en-us.json (Russian) 2024-12-16 10:49:25 -06:00
tresabhi 6d448dc5c4 feat: new languages 2024-12-16 10:49:18 -06:00
tresabhi 75d4d60b30 Merge branch 'main' into l10n_main 2024-12-16 10:44:05 -06:00
tresabhi e7f47fd2fd feat: handle no next price 2024-12-16 10:41:10 -06:00
tresabhi 50a3d1e922 feat: logs 2024-12-16 10:34:14 -06:00
tresabhi e296267ccd feat: add cpr 2024-12-16 10:30:05 -06:00
tresabhi d9aca6a3ed fix: include .git 2024-12-15 19:35:25 -06:00
tresabhi d07a6e954e fix: remove dead code 2024-12-15 19:30:22 -06:00
tresabhi 6bcaf685f8 fix: names 2024-12-15 19:26:58 -06:00
tresabhi 066f17a2f3 fix: concurrent only on main 2024-12-15 19:26:36 -06:00
tresabhi 374d6c22f9 feat: use artifact 2024-12-15 19:25:13 -06:00
tresabhi 55927258ca feat: use better emoji 2024-12-15 19:08:31 -06:00
tresabhi bb99220497 feat: /auction strikethrough and warning 2024-12-15 19:06:05 -06:00
tresabhi 4f6b41e264 feat: start work on website deployment 2024-12-15 18:59:20 -06:00
tresabhi f9c4cee30c feat: disable vercel deployments 2024-12-15 18:46:59 -06:00
tresabhi d865043b34 fix: invalidate on advanced highlighting change 2024-12-15 18:39:24 -06:00
tresabhi a4080f21b9 feat: optimize using claude lol 2024-12-15 18:33:15 -06:00
tresabhi 853c520d8a feat: optimize depth sampling 2024-12-15 18:26:34 -06:00
tresabhi d78f8c6ec4 feat: optimize constants 2024-12-15 18:21:03 -06:00
tresabhi 318a71bd32 feat: advanced mode 2024-12-15 18:13:23 -06:00
TrèsAbhi 2458d2c80c New translations en-us.json (Chinese Simplified) 2024-12-15 01:07:40 -06:00
TrèsAbhi 37a72e3bb5 New translations en-us.json (Chinese Simplified) 2024-12-15 00:08:24 -06:00
tresabhi bc9c9ad4e7 feat: start of custom renders 2024-12-14 22:32:28 -06:00
tresabhi b38c732983 fix: missing image backup 2024-12-14 15:46:47 -06:00
TrèsAbhi 415861f6e8 New translations en-us.json (Chinese Simplified) 2024-12-14 15:26:03 -06:00
TrèsAbhi 3124afa253 New translations en-us.json (Ukrainian) 2024-12-14 15:26:02 -06:00
TrèsAbhi 8523b9f44f New translations en-us.json (Russian) 2024-12-14 15:26:01 -06:00
TrèsAbhi 71e0404890 New translations en-us.json (Croatian (disambiguation)) 2024-12-14 15:26:00 -06:00
TrèsAbhi c01fd1f4ff New translations en-us.json (Spanish) 2024-12-14 15:25:59 -06:00
63 changed files with 905 additions and 497 deletions

No files matched your search

@@ -1,8 +1,7 @@
name: Build Website name: Website
on: on:
push: push:
branches: main
workflow_dispatch: workflow_dispatch:
schedule: schedule:
@@ -15,7 +14,7 @@ permissions:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
env: env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }} PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
@@ -52,14 +51,41 @@ jobs:
with: with:
path: dist/website path: dist/website
deploy: deploy-github-pages:
environment: if: github.ref == 'refs/heads/main'
name: github-pages
url: ${{ steps.deployment.outputs.page_url }} name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Deploy needs: build
environment:
name: Production
url: ${{ steps.deployment.outputs.page_url }}
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v4 uses: actions/deploy-pages@v4
deploy-vercel:
name: Deploy to Vercel
needs: build
runs-on: ubuntu-latest
environment:
name: Preview
steps:
# vercel begs for the .git dir so here it is my guy
- uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v4
with:
path: ./dist/website
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID}}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
working-directory: ./dist/website
+2 -1
View File
@@ -8,7 +8,7 @@ package-lock.json
dist/ dist/
dist.* dist.*
yarn-error.log yarn-error.log
/test.* test.*
/docs/.vitepress/dist /docs/.vitepress/dist
/docs/.vitepress/cache /docs/.vitepress/cache
servers.csv servers.csv
@@ -26,3 +26,4 @@ target
# !packages/core/src/protos/*.proto # !packages/core/src/protos/*.proto
/.vercel /.vercel
/.astro /.astro
.vercel
+1
View File
@@ -189,6 +189,7 @@
"unpickled", "unpickled",
"Unregistering", "Unregistering",
"unrotate", "unrotate",
"vercel",
"vickers", "vickers",
"vitepress", "vitepress",
"Vovko", "Vovko",
BIN
View File
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
# BlitzKit 1.26.4
## Features
- `/auction` now warns if a tank stock is at or below 500 and now strikes through the tank name if its out
- Added advanced armor highlighting mode
- Green: penetration
- Red: blocked
- Pink: ricochet
- Cyan: penetration with 3 calibers rule
- Optimized the armor highlighting for about a 15% increase in frame rate
## Fixes
- Added 5000 doses of adrenaline, incase the BOT DECIDES TO DIE AGAIN FOR NO REASON
+19
View File
@@ -0,0 +1,19 @@
# BlitzKit 1.26.5
¡Ahora en 6 idiomas! This version of BlitzKit adds support for 4 new languages totalling up to 6! [Please help translate on Crowdin](https://crowdin.com/project/blitzkrieg).
## Features
- Added Croatian, Russian, Ukrainian, and Chinese
- Pre-existing languages: English, Spanish
- Added better lighting making tanks a little less yellow and a bit more realistic
- Added custom tank icons (some may be missing when the update drops; just give them an hour or so to update)
- Increased the maximum recently viewed tanks list size to 14
- Tweaked the tank name in the hero section of the Tankopedia to allow the tank to be in the center of the page
- The headers in the Performance tool are now sticky just like the Compare tool
## Fixes
- Removed unnecessary third party script that did absolutely nothing
- Localizations are now stricter in what they allow, reducing errors in the Discord bot
- Added 50 gallons of tomato soup to the bot in an attempt to keep it alive
+2 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "blitzkit", "name": "blitzkit",
"private": true, "private": true,
"version": "1.26.3", "version": "1.26.5",
"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",
@@ -39,6 +39,7 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"sharp": "^0.33.5",
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"
}, },
"devDependencies": { "devDependencies": {
+18 -10
View File
@@ -27,7 +27,7 @@ type Result =
available_from?: Date; available_from?: Date;
available_before?: Date; available_before?: Date;
price?: Price; price?: Price;
next_price?: Price | null; next_price: Price | null;
available: true; available: true;
display?: boolean; display?: boolean;
next_price_datetime: Date; next_price_datetime: Date;
@@ -68,6 +68,8 @@ interface Currency {
type: 'currency'; type: 'currency';
} }
const WARNING_COUNT = 500;
export const auctionCommand = new Promise<CommandRegistry>((resolve) => { export const auctionCommand = new Promise<CommandRegistry>((resolve) => {
resolve({ resolve({
command: createLocalizedCommand('auction'), command: createLocalizedCommand('auction'),
@@ -107,21 +109,27 @@ export const auctionCommand = new Promise<CommandRegistry>((resolve) => {
const name = `[${tank.name}](<https://blitzkit.app/tools/tankopedia/${ const name = `[${tank.name}](<https://blitzkit.app/tools/tankopedia/${
tank.id tank.id
}>)`; }>)`;
const next = translate('bot.commands.auction.body.next', [ const next =
`<:gold:1317173197082333244> ${data.next_price!.value.toLocaleString( data.next_price === null
interaction.locale, ? ''
)}`, : `\n-# ${translate('bot.commands.auction.body.next', [
]); `<:gold:1317173197082333244> ${data.next_price.value.toLocaleString(
interaction.locale,
)}`,
])}`;
const available = translate('bot.commands.auction.body.available', [ const available = translate('bot.commands.auction.body.available', [
data.current_count.toLocaleString(interaction.locale), data.current_count.toLocaleString(interaction.locale),
data.initial_count.toLocaleString(interaction.locale), data.initial_count.toLocaleString(interaction.locale),
]); ]);
const isOut = data.current_count === 0;
const outString = isOut ? '~~' : '';
const isLow = data.current_count <= WARNING_COUNT;
return `${ return `${index + 1}. ${
index + 1 outString
}. ${name} <:gold:1317173197082333244> ${data.price!.value.toLocaleString( }${name} <:gold:1317173197082333244> ${data.price!.value.toLocaleString(
interaction.locale, interaction.locale,
)}\n-# ${available}\n-# ${next}`; )}${outString}\n-#${isLow ? ' ‼️ ' : ' '}${available}${isOut ? '' : next}`;
}); });
const header = `${title}\n${subtitle}`; const header = `${title}\n${subtitle}`;
const lines = [header, ...body]; const lines = [header, ...body];
+5 -1
View File
@@ -24,7 +24,11 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
return option return option
.setName('threshold') .setName('threshold')
.setNameLocalizations( .setNameLocalizations(
localizationObject('bot.commands.inactive.options.threshold'), localizationObject(
'bot.commands.inactive.options.threshold',
undefined,
true,
),
) )
.setDescription( .setDescription(
translate('bot.commands.inactive.options.threshold.description'), translate('bot.commands.inactive.options.threshold.description'),
+15 -5
View File
@@ -7,6 +7,7 @@ import {
getLeagueFromScore, getLeagueFromScore,
getRatingInfo, getRatingInfo,
getRatingNeighbors, getRatingNeighbors,
imgur,
} from '@blitzkit/core'; } from '@blitzkit/core';
import { SlashCommandSubcommandBuilder } from 'discord.js'; import { SlashCommandSubcommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
@@ -119,6 +120,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
statsB2, statsB2,
); );
const leagueTheme = LEAGUE_ACCENT[league.index]; const leagueTheme = LEAGUE_ACCENT[league.index];
let rewardImage = reward
? reward.type === 'vehicle'
? reward.vehicle.preview_image_url
: reward.stuff.image_url
: undefined;
if (rewardImage) {
const { ok } = await fetch(rewardImage);
if (!ok) {
rewardImage = imgur('kP88vqr');
}
}
return ( return (
<CommandWrapper> <CommandWrapper>
@@ -325,11 +339,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
height: 64, height: 64,
objectFit: 'cover', objectFit: 'cover',
}} }}
src={await iconPng( src={await iconPng(rewardImage!)}
reward.type === 'vehicle'
? reward.vehicle.preview_image_url
: reward.stuff.image_url,
)}
/> />
</div> </div>
)} )}
@@ -63,6 +63,8 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
.setNameLocalizations( .setNameLocalizations(
localizationObject( localizationObject(
'bot.commands.rating_leaderboard.options.limit', 'bot.commands.rating_leaderboard.options.limit',
undefined,
true,
), ),
) )
.setDescription( .setDescription(
@@ -87,6 +89,8 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
.setNameLocalizations( .setNameLocalizations(
localizationObject( localizationObject(
'bot.commands.rating_leaderboard.options.season', 'bot.commands.rating_leaderboard.options.season',
undefined,
true,
), ),
) )
.setDescription( .setDescription(
@@ -148,6 +152,8 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
.setNameLocalizations( .setNameLocalizations(
localizationObject( localizationObject(
'bot.commands.rating_leaderboard.options.league', 'bot.commands.rating_leaderboard.options.league',
undefined,
true,
), ),
) )
.setDescription( .setDescription(
+10 -2
View File
@@ -36,7 +36,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
option option
.setName(t`bot.commands.research.options.target_tank`) .setName(t`bot.commands.research.options.target_tank`)
.setNameLocalizations( .setNameLocalizations(
localizationObject('bot.commands.research.options.target_tank'), localizationObject(
'bot.commands.research.options.target_tank',
undefined,
true,
),
) )
.setDescription( .setDescription(
t`bot.commands.research.options.target_tank.description`, t`bot.commands.research.options.target_tank.description`,
@@ -53,7 +57,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
option option
.setName(t`bot.commands.research.options.starting_tank`) .setName(t`bot.commands.research.options.starting_tank`)
.setNameLocalizations( .setNameLocalizations(
localizationObject('bot.commands.research.options.starting_tank'), localizationObject(
'bot.commands.research.options.starting_tank',
undefined,
true,
),
) )
.setDescription( .setDescription(
t`bot.commands.research.options.starting_tank.description`, t`bot.commands.research.options.starting_tank.description`,
+5 -1
View File
@@ -24,7 +24,11 @@ export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
return option return option
.setName(t`bot.commands.search_clans.options.limit`) .setName(t`bot.commands.search_clans.options.limit`)
.setNameLocalizations( .setNameLocalizations(
localizationObject('bot.commands.search_clans.options.limit'), localizationObject(
'bot.commands.search_clans.options.limit',
undefined,
true,
),
) )
.setDescription( .setDescription(
translate('bot.commands.search_clans.options.limit.description', [ translate('bot.commands.search_clans.options.limit.description', [
+5 -1
View File
@@ -29,7 +29,11 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
option option
.setName(t`bot.commands.search_players.options.limit`) .setName(t`bot.commands.search_players.options.limit`)
.setNameLocalizations( .setNameLocalizations(
localizationObject('bot.commands.search_players.options.limit'), localizationObject(
'bot.commands.search_players.options.limit',
undefined,
true,
),
) )
.setDescription( .setDescription(
translate('bot.commands.search_players.options.limit.description', [ translate('bot.commands.search_players.options.limit.description', [
@@ -7,7 +7,9 @@ export function addClanChoices(option: SlashCommandStringOption) {
return option return option
.setName('clan') .setName('clan')
.setNameLocalizations(localizationObject('bot.common.options.clan')) .setNameLocalizations(
localizationObject('bot.common.options.clan', undefined, true),
)
.setDescription(translate('bot.common.options.clan.description')) .setDescription(translate('bot.common.options.clan.description'))
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject('bot.common.options.clan.description'), localizationObject('bot.common.options.clan.description'),
@@ -25,7 +25,11 @@ export function addFilterOptions<
option option
.setName(translate('bot.common.options.filters.nation')) .setName(translate('bot.common.options.filters.nation'))
.setNameLocalizations( .setNameLocalizations(
localizationObject('bot.common.options.filters.nation'), localizationObject(
'bot.common.options.filters.nation',
undefined,
true,
),
) )
.setDescription( .setDescription(
translate('bot.common.options.filters.nation.description'), translate('bot.common.options.filters.nation.description'),
@@ -52,7 +56,11 @@ export function addFilterOptions<
option option
.setName(translate('bot.common.options.filters.class')) .setName(translate('bot.common.options.filters.class'))
.setNameLocalizations( .setNameLocalizations(
localizationObject('bot.common.options.filters.class'), localizationObject(
'bot.common.options.filters.class',
undefined,
true,
),
) )
.setDescription( .setDescription(
translate('bot.common.options.filters.class.description'), translate('bot.common.options.filters.class.description'),
@@ -78,7 +86,11 @@ export function addFilterOptions<
option option
.setName(translate('bot.common.options.filters.type')) .setName(translate('bot.common.options.filters.type'))
.setNameLocalizations( .setNameLocalizations(
localizationObject('bot.common.options.filters.type'), localizationObject(
'bot.common.options.filters.type',
undefined,
true,
),
) )
.setDescription( .setDescription(
translate('bot.common.options.filters.type.description'), translate('bot.common.options.filters.type.description'),
@@ -18,12 +18,20 @@ export function addPeriodSubCommands(
) { ) {
const { translate } = translator(Locale.EnglishUS); const { translate } = translator(Locale.EnglishUS);
const subcommandNameLocalizations = { const subcommandNameLocalizations = {
today: localizationObject('bot.common.subcommands.today'), today: localizationObject('bot.common.subcommands.today', undefined, true),
30: localizationObject('bot.common.subcommands.30'), 30: localizationObject('bot.common.subcommands.30', undefined, true),
60: localizationObject('bot.common.subcommands.60'), 60: localizationObject('bot.common.subcommands.60', undefined, true),
90: localizationObject('bot.common.subcommands.90'), 90: localizationObject('bot.common.subcommands.90', undefined, true),
career: localizationObject('bot.common.subcommands.career'), career: localizationObject(
custom: localizationObject('bot.common.subcommands.custom'), 'bot.common.subcommands.career',
undefined,
true,
),
custom: localizationObject(
'bot.common.subcommands.custom',
undefined,
true,
),
}; };
const subcommandDescriptionLocalizations = { const subcommandDescriptionLocalizations = {
today: localizationObject('bot.common.subcommands.today.description'), today: localizationObject('bot.common.subcommands.today.description'),
@@ -12,7 +12,9 @@ export function addRegionChoices(option: SlashCommandStringOption) {
return option return option
.setName(t`bot.common.options.region`) .setName(t`bot.common.options.region`)
.setNameLocalizations(localizationObject('bot.common.options.region')) .setNameLocalizations(
localizationObject('bot.common.options.region', undefined, true),
)
.setDescription(t`bot.common.options.region.description`) .setDescription(t`bot.common.options.region.description`)
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject('bot.common.options.region.description'), localizationObject('bot.common.options.region.description'),
@@ -7,7 +7,9 @@ export function addTankChoices(option: SlashCommandStringOption) {
return option return option
.setName(translate('bot.common.options.tank')) .setName(translate('bot.common.options.tank'))
.setNameLocalizations(localizationObject('bot.common.options.tank')) .setNameLocalizations(
localizationObject('bot.common.options.tank', undefined, true),
)
.setDescription(translate('bot.common.options.tank.description')) .setDescription(translate('bot.common.options.tank.description'))
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject('bot.common.options.tank.description'), localizationObject('bot.common.options.tank.description'),
@@ -13,7 +13,9 @@ export function addTierChoices(option: SlashCommandStringOption) {
return option return option
.setName(t`bot.common.options.tier`) .setName(t`bot.common.options.tier`)
.setNameLocalizations(localizationObject('bot.common.options.tier')) .setNameLocalizations(
localizationObject('bot.common.options.tier', undefined, true),
)
.setDescription(t`bot.common.options.tier.description`) .setDescription(t`bot.common.options.tier.description`)
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject('bot.common.options.tier.description'), localizationObject('bot.common.options.tier.description'),
@@ -7,7 +7,9 @@ export function addUsernameChoices(option: SlashCommandStringOption) {
return option return option
.setName(t`bot.common.options.username`) .setName(t`bot.common.options.username`)
.setNameLocalizations(localizationObject('bot.common.options.username')) .setNameLocalizations(
localizationObject('bot.common.options.username', undefined, true),
)
.setDescription(t`bot.common.options.username.description`) .setDescription(t`bot.common.options.username.description`)
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject('bot.common.options.username.description'), localizationObject('bot.common.options.username.description'),
@@ -27,7 +27,9 @@ export function createLocalizedCommand(
const { translate } = translator(Locale.EnglishUS); const { translate } = translator(Locale.EnglishUS);
const slashCommand = new SlashCommandBuilder() const slashCommand = new SlashCommandBuilder()
.setName(command) .setName(command)
.setNameLocalizations(localizationObject(`bot.commands.${commandPathItem}`)) .setNameLocalizations(
localizationObject(`bot.commands.${commandPathItem}`, undefined, true),
)
.setDescription(translate(`bot.commands.${commandPathItem}.description`)) .setDescription(translate(`bot.commands.${commandPathItem}.description`))
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject(`bot.commands.${commandPathItem}.description`), localizationObject(`bot.commands.${commandPathItem}.description`),
@@ -38,6 +40,8 @@ export function createLocalizedCommand(
if ('subcommand' in subItem) { if ('subcommand' in subItem) {
const subcommandNameLocalizations = localizationObject( const subcommandNameLocalizations = localizationObject(
`bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}`, `bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}`,
undefined,
true,
); );
const subcommandDescriptionLocalizations = localizationObject( const subcommandDescriptionLocalizations = localizationObject(
`bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}.description`, `bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}.description`,
@@ -59,6 +63,8 @@ export function createLocalizedCommand(
} else { } else {
const subcommandNameLocalizations = localizationObject( const subcommandNameLocalizations = localizationObject(
`bot.commands.${commandPathItem}.groups.${subItem.group}`, `bot.commands.${commandPathItem}.groups.${subItem.group}`,
undefined,
true,
); );
const subcommandDescriptionLocalizations = localizationObject( const subcommandDescriptionLocalizations = localizationObject(
`bot.commands.${commandPathItem}.groups.${subItem.group}.description`, `bot.commands.${commandPathItem}.groups.${subItem.group}.description`,
@@ -2,13 +2,29 @@ import { Locale } from 'discord.js';
import { SUPPORTED_LOCALES } from '../localization/strings/constants'; import { SUPPORTED_LOCALES } from '../localization/strings/constants';
import { translator } from '../localization/translator'; import { translator } from '../localization/translator';
export function localizationObject(path: string, literals?: string[]) { const validNameRegex = /^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u;
export function localizationObject(
path: string,
literals?: string[],
strict = false,
) {
return SUPPORTED_LOCALES.reduce<Partial<Record<Locale, string>>>( return SUPPORTED_LOCALES.reduce<Partial<Record<Locale, string>>>(
(localizations, locale) => { (localizations, locale) => {
const { translate } = translator(locale); const { translate } = translator(locale);
const translation = translate(path, literals);
const isValid = !strict || validNameRegex.test(translation);
if (!isValid) {
console.warn(
`Invalid localization for ${locale} (${path}): ${translation}; skipping...`,
);
return localizations;
}
return { return {
...localizations, ...localizations,
[locale]: translate(path, literals), [locale]: translation,
}; };
}, },
{}, {},
@@ -1,3 +1,10 @@
import { Locale } from 'discord.js'; import { Locale } from 'discord.js';
export const SUPPORTED_LOCALES: Locale[] = [Locale.EnglishUS, Locale.SpanishES]; export const SUPPORTED_LOCALES: Locale[] = [
Locale.EnglishUS,
Locale.SpanishES,
Locale.Croatian,
Locale.Russian,
Locale.Ukrainian,
Locale.ChineseCN,
];
@@ -126,9 +126,7 @@ commands.then((awaitedCommands) => {
rest.put( rest.put(
Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)), Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)),
{ { body },
body,
},
); );
}); });
+92 -53
View File
@@ -5,63 +5,102 @@ import {
} from './core/blitzkit/nonBlockingPromises'; } from './core/blitzkit/nonBlockingPromises';
import { manager } from './core/discord/manager'; import { manager } from './core/discord/manager';
console.log('pre manager event listeners and spawn'); async function setupShardManager() {
console.log('Pre-manager setup starting...');
const shards = await manager const readyShards = new Set();
.on('shardCreate', (shard) => { const totalShards = manager.totalShards;
console.log(`🟡 Launching shard ${shard.id}`);
shard.on('ready', () => console.log(`🟢 Launched shard ${shard.id}`));
})
.spawn();
console.log('post manager event listeners and spawn'); const allShardsReady = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Shard initialization timed out after 60 seconds'));
}, 60000);
try { manager.on('shardCreate', (shard) => {
let servers = 0; console.log(`🟡 Launching shard ${shard.id}`);
let channels = 0;
let users = 0;
for (const [, shard] of shards) { shard.on('ready', () => {
// wtf is this typing bro??? console.log(`🟢 Launched shard ${shard.id}`);
const guilds = (await shard.fetchClientValue('guilds.cache')) as any; readyShards.add(shard.id);
servers += guilds.length; if (readyShards.size === totalShards) {
clearTimeout(timeout);
for (const guild of guilds) { resolve(true);
channels += guild.channels.length; }
users += guild.memberCount;
}
}
const awaitedTankDefinitions = await tankDefinitions;
const tanks = Object.values(awaitedTankDefinitions.tanks).length;
const awaitedDiscoveredIdsDefinitions = await discoveredIdsDefinitions;
const usernames = awaitedDiscoveredIdsDefinitions.count;
manager.broadcastEval(
(client, { servers, users, channels, tanks, usernames }) => {
const bios = [
`Living in ${servers.toLocaleString()} servers`,
`Serving ${users.toLocaleString()} users`,
`Observing ${channels.toLocaleString()} channels`,
`Tinkering with ${tanks.toLocaleString()} tanks`,
`Memorizing ${usernames.toLocaleString()} usernames`,
];
client.user?.setPresence({
activities: [
{
type: ActivityType.Custom,
name: bios[Math.floor(Math.random() * bios.length)],
},
],
}); });
},
{ context: { servers, channels, users, tanks, usernames } }, shard.on('error', (error) => {
); console.error(`❌ Shard ${shard.id} encountered an error:`, error);
} catch (error) { });
console.warn(
'An error occurred when setting up presence, ignoring to keep the process alive', shard.on('disconnect', () => {
error, console.warn(`⚠️ Shard ${shard.id} disconnected`);
); });
});
});
try {
console.log('Spawning shards...');
const shards = await manager.spawn();
console.log('Shards spawned, waiting for ready state...');
await allShardsReady;
console.log('All shards ready, setting up presence...');
let servers = 0;
let channels = 0;
let users = 0;
for (const [, shard] of shards) {
try {
const guilds = (await shard.fetchClientValue('guilds.cache')) as any;
servers += guilds.length;
for (const guild of guilds) {
channels += guild.channels.length;
users += guild.memberCount;
}
} catch (error) {
console.error(`Failed to fetch statistics from shard:`, error);
}
}
const [awaitedTankDefinitions, awaitedDiscoveredIdsDefinitions] =
await Promise.all([tankDefinitions, discoveredIdsDefinitions]);
const tanks = Object.values(awaitedTankDefinitions.tanks).length;
const usernames = awaitedDiscoveredIdsDefinitions.count;
await manager.broadcastEval(
(client, { servers, users, channels, tanks, usernames }) => {
const bios = [
`Living in ${servers.toLocaleString()} servers`,
`Serving ${users.toLocaleString()} users`,
`Observing ${channels.toLocaleString()} channels`,
`Tinkering with ${tanks.toLocaleString()} tanks`,
`Memorizing ${usernames.toLocaleString()} usernames`,
];
client.user?.setPresence({
activities: [
{
type: ActivityType.Custom,
name: bios[Math.floor(Math.random() * bios.length)],
},
],
});
},
{ context: { servers, channels, users, tanks, usernames } },
);
console.log('Bot startup complete!');
} catch (error) {
console.error('Critical error during startup:', error);
throw error;
}
} }
setupShardManager().catch((error) => {
console.error('Failed to start the bot:', error);
process.exit(1);
});
+9 -7
View File
@@ -142,7 +142,9 @@
"no_tank_stats": "# No hay estadísticas de tanque disponibles\nEste jugador no tiene estadísticas disponibles. Este podría no ser el jugador que estás buscando.", "no_tank_stats": "# No hay estadísticas de tanque disponibles\nEste jugador no tiene estadísticas disponibles. Este podría no ser el jugador que estás buscando.",
"get_help": "Servidor de ayuda de Discord", "get_help": "Servidor de ayuda de Discord",
"uncaught_error": "# ¡Blitzkrieg sufrió un error!\nLo sentimos mucho. Siéntete libre de [unirte al servidor oficial de Discord](https://discord.gg/nDt7AjGJQH) para obtener ayuda.", "uncaught_error": "# ¡Blitzkrieg sufrió un error!\nLo sentimos mucho. Siéntete libre de [unirte al servidor oficial de Discord](https://discord.gg/nDt7AjGJQH) para obtener ayuda.",
"preview": "# Este comando es una vista previa\n\nSolo puedes acceder a este comando en [el servidor oficial de Discord](https://discord.gg/nDt7AjGJQH). Generalmente este comando estará disponible pronto." "preview": "# Este comando es una vista previa\n\nSolo puedes acceder a este comando en [el servidor oficial de Discord](https://discord.gg/nDt7AjGJQH). Generalmente este comando estará disponible pronto.",
"query_too_small": "# Your search is too short\nYour search must be at least 3 characters long.",
"query_too_long": "# Your search is too long\nYour search must be at most 100 characters long."
}, },
"wn8": { "wn8": {
"$": "WN8", "$": "WN8",
@@ -211,13 +213,13 @@
"$": "nación", "$": "nación",
"description": "Nación" "description": "Nación"
}, },
"tank_class": { "class": {
"$": "clase-de-tanque", "$": "class",
"description": "Clase de tanque" "description": "Class"
}, },
"tree_type": { "type": {
"$": "tipo-de-árbol", "$": "type",
"description": "Tipo de árbol" "description": "Type"
} }
}, },
"region": { "region": {
+74 -72
View File
@@ -1,16 +1,16 @@
{ {
"common": { "common": {
"wn8_percentile": { "wn8_percentile": {
"0": "Jako loše", "0": "Very bad",
"1": "Loše", "1": "Bad",
"2": "Ispod prosečnog", "2": "Below average",
"3": "Prosečno", "3": "Average",
"4": "Nad prosečno", "4": "Above average",
"5": "Dobro", "5": "Good",
"6": "Jako dobro", "6": "Very good",
"7": "Super", "7": "Great",
"8": "Legendarno", "8": "Unicum",
"9": "Super Legendarno" "9": "Super unicum"
}, },
"bkni_percentile": { "bkni_percentile": {
"0": "Super malum", "0": "Super malum",
@@ -26,15 +26,15 @@
"10": "Super unicum" "10": "Super unicum"
}, },
"nations": { "nations": {
"ussr": "S.S.S.R.", "ussr": "U.S.S.R.",
"germany": "Nemačka", "germany": "Germany",
"usa": "S.A.D.", "usa": "U.S.A.",
"china": "Kina", "china": "China",
"uk": "Vel. Brit.", "uk": "U.K.",
"france": "Francuska", "france": "France",
"japan": "Japan", "japan": "Japan",
"european": "Evropska Unija", "european": "European Nation",
"other": "Hibridna nacija" "other": "Hybrid Nation"
}, },
"nations_adjectives": { "nations_adjectives": {
"ussr": "U.S.S.R.", "ussr": "U.S.S.R.",
@@ -75,9 +75,9 @@
}, },
"regions": { "regions": {
"normal": { "normal": {
"com": "Severna Amerika", "com": "North America",
"eu": "Evropa", "eu": "Europe",
"asia": "Azija-Pasifik" "asia": "Asia-Pacific"
}, },
"short": { "short": {
"com": "NA", "com": "NA",
@@ -86,11 +86,11 @@
} }
}, },
"leagues": { "leagues": {
"diamond": "Dijamant", "diamond": "Diamond",
"platinum": "Platina", "platinum": "Platinum",
"gold": "Zlato", "gold": "Gold",
"silver": "Srebro", "silver": "Silver",
"bronze": "Bronza" "bronze": "Bronze"
}, },
"composite_stats": { "composite_stats": {
"normalized_damage_dealt": "Damage", "normalized_damage_dealt": "Damage",
@@ -136,58 +136,60 @@
"errors": { "errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"%s\". Try selecting a clan from the search result.", "clan_not_found": "# Could not find clan\nCouldn't find \"%s\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"%s\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.", "player_not_found": "# Could not find user\nI couldn't find user \"%s\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"player_not_linked": "# I don't know who you are\nUse the `/link` command to get started.", "player_not_linked": "# Use `/link` to get started\nI don't know who you are but you can tell me today!",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"%s\".", "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"%s\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"%s\". Try typing the name of the tank instead of a number.", "tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"%s\". Try typing the name of the tank instead of a number.",
"no_tank_stats": "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.", "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.",
"get_help": "Help Discord server", "get_help": "Help Discord server",
"uncaught_error": "# Blitzkrieg ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.", "uncaught_error": "# BlitzKit ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
"preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon." "preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon.",
"query_too_small": "# Your search is too short\nYour search must be at least 3 characters long.",
"query_too_long": "# Your search is too long\nYour search must be at most 100 characters long."
}, },
"wn8": { "wn8": {
"$": "WN8", "$": "WN8",
"very_bad": "Jako loše", "very_bad": "Very bad",
"bad": "Loše", "bad": "Bad",
"below_average": "Ispod prosečnog", "below_average": "Below average",
"average": "Prosečno", "average": "Average",
"above_average": "Nad prosečno", "above_average": "Above average",
"good": "Dobro", "good": "Good",
"very_good": "Jako dobro", "very_good": "Very good",
"great": "Super", "great": "Great",
"unicum": "Legendarno", "unicum": "Unicum",
"super_unicum": "Super Legendarno" "super_unicum": "Super unicum"
}, },
"subcommands": { "subcommands": {
"today": { "today": {
"$": "danas", "$": "today",
"description": "Danas" "description": "Today"
}, },
"30": { "30": {
"$": "30", "$": "30",
"description": "30 dana" "description": "30 days"
}, },
"60": { "60": {
"$": "60", "$": "60",
"description": "60 dana" "description": "60 days"
}, },
"90": { "90": {
"$": "90", "$": "90",
"description": "90 dana" "description": "90 days"
}, },
"career": { "career": {
"$": "karijera", "$": "career",
"description": "Karijera" "description": "Career"
}, },
"custom": { "custom": {
"$": "proizvoljno", "$": "custom",
"description": "Proizvoljna daljina", "description": "Custom range",
"options": { "options": {
"start": { "start": {
"$": "kreni", "$": "start",
"description": "Start in number of days ago from today" "description": "Start in number of days ago from today"
}, },
"end": { "end": {
"$": "kraj", "$": "end",
"description": "End in number of days ago from today" "description": "End in number of days ago from today"
} }
} }
@@ -195,7 +197,7 @@
}, },
"options": { "options": {
"clan": { "clan": {
"$": "klan", "$": "clan",
"description": "The clan name or tag" "description": "The clan name or tag"
}, },
"tank": { "tank": {
@@ -211,13 +213,13 @@
"$": "nation", "$": "nation",
"description": "Nation" "description": "Nation"
}, },
"tank_class": { "class": {
"$": "tank-class", "$": "class",
"description": "Tank class" "description": "Class"
}, },
"tree_type": { "type": {
"$": "tree-type", "$": "type",
"description": "Tree type" "description": "Type"
} }
}, },
"region": { "region": {
@@ -321,23 +323,23 @@
"commands": { "commands": {
"$": "commands", "$": "commands",
"description": "List of all commands and their descriptions", "description": "List of all commands and their descriptions",
"body": "# Commands\n\nBlitzkrieg offers the following commands:\n\n" "body": "# Commands\n\nBlitzKit offers the following commands:\n\n"
}, },
"embeds": { "embeds": {
"$": "embeds", "$": "embeds",
"description": "Kako embed radi" "description": "How embeds work"
}, },
"introduction": { "introduction": {
"$": "uvod", "$": "introduction",
"description": "O bot-u" "description": "About the bot"
}, },
"invite": { "invite": {
"$": "pozovi", "$": "invite",
"description": "Kako pozvati robota" "description": "How to invite the bot"
}, },
"timezones": { "timezones": {
"$": "vremenske zone", "$": "timezones",
"description": "Kako \"Blitzkrieg\" koristi podatke o tvojoj vremenskoj zoni" "description": "How BlitzKit infers and uses your timezone"
}, },
"credits": { "credits": {
"$": "credits", "$": "credits",
@@ -346,19 +348,19 @@
} }
}, },
"breakdown": { "breakdown": {
"$": "analiza", "$": "breakdown",
"description": "A period's breakdown by tanks played", "description": "A period's breakdown by tanks played",
"body": { "body": {
"total": "Total", "total": "Total",
"battles": "Borbe", "battles": "Battles",
"wn8": "WN8", "wn8": "WN8",
"winrate": "Procenat pobede", "winrate": "Winrate",
"damage": "Šteta", "damage": "Damage",
"unknown_tank": "Ne poznat tenk %s" "unknown_tank": "Unknown tank %s"
} }
}, },
"debug": { "debug": {
"$": "odklanjanje-grešaka", "$": "debug",
"description": "Debug information about the bot", "description": "Debug information about the bot",
"body": { "body": {
"title": "# Debug\n\n", "title": "# Debug\n\n",
@@ -572,7 +574,7 @@
} }
}, },
"rating": { "rating": {
"$": "rating-session", "$": "rating",
"description": "Rating player's statistics", "description": "Rating player's statistics",
"subcommands": { "subcommands": {
"today": { "today": {
@@ -142,7 +142,9 @@
"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": "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.",
"get_help": "Help Discord server", "get_help": "Help Discord server",
"uncaught_error": "# BlitzKit ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.", "uncaught_error": "# BlitzKit ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
"preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon." "preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon.",
"query_too_small": "# Your search is too short\nYour search must be at least 3 characters long.",
"query_too_long": "# Your search is too long\nYour search must be at most 100 characters long."
}, },
"wn8": { "wn8": {
"$": "WN8", "$": "WN8",
@@ -211,13 +213,13 @@
"$": "nation", "$": "nation",
"description": "Nation" "description": "Nation"
}, },
"tank_class": { "class": {
"$": "tank-class", "$": "class",
"description": "Tank class" "description": "Class"
}, },
"tree_type": { "type": {
"$": "tree-type", "$": "type",
"description": "Tree type" "description": "Type"
} }
}, },
"region": { "region": {
@@ -142,7 +142,9 @@
"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": "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.",
"get_help": "Help Discord server", "get_help": "Help Discord server",
"uncaught_error": "# BlitzKit ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.", "uncaught_error": "# BlitzKit ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
"preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon." "preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon.",
"query_too_small": "# Your search is too short\nYour search must be at least 3 characters long.",
"query_too_long": "# Your search is too long\nYour search must be at most 100 characters long."
}, },
"wn8": { "wn8": {
"$": "WN8", "$": "WN8",
@@ -211,13 +213,13 @@
"$": "nation", "$": "nation",
"description": "Nation" "description": "Nation"
}, },
"tank_class": { "class": {
"$": "tank-class", "$": "class",
"description": "Tank class" "description": "Class"
}, },
"tree_type": { "type": {
"$": "tree-type", "$": "type",
"description": "Tree type" "description": "Type"
} }
}, },
"region": { "region": {
+91 -89
View File
@@ -1,14 +1,14 @@
{ {
"common": { "common": {
"wn8_percentile": { "wn8_percentile": {
"0": "Very bad", "0": "非常差",
"1": "Bad", "1": "差",
"2": "Below average", "2": "较差",
"3": "Average", "3": "平均",
"4": "Above average", "4": "高于平均",
"5": "Good", "5": "好",
"6": "Very good", "6": "非常好",
"7": "Great", "7": "很好",
"8": "Unicum", "8": "Unicum",
"9": "Super unicum" "9": "Super unicum"
}, },
@@ -16,53 +16,53 @@
"0": "Super malum", "0": "Super malum",
"1": "Malum", "1": "Malum",
"2": "Very bad", "2": "Very bad",
"3": "Bad", "3": "差",
"4": "Below average", "4": "较差",
"5": "Average", "5": "平均",
"6": "Above average", "6": "高于平均",
"7": "Good", "7": "好",
"8": "Very good", "8": "非常好",
"9": "Unicum", "9": "Unicum",
"10": "Super unicum" "10": "Super unicum"
}, },
"nations": { "nations": {
"ussr": "U.S.S.R.", "ussr": "苏联 (S系)",
"germany": "Germany", "germany": "德国 (D系)",
"usa": "U.S.A.", "usa": "美国 (M系)",
"china": "China", "china": "中国 (C系)",
"uk": "U.K.", "uk": "英国 (Y系)",
"france": "France", "france": "法国 (F系)",
"japan": "Japan", "japan": "日本 (R系)",
"european": "European Nation", "european": "欧盟 (E系)",
"other": "Hybrid Nation" "other": "X系"
}, },
"nations_adjectives": { "nations_adjectives": {
"ussr": "U.S.S.R.", "ussr": "苏联 (S系)",
"germany": "German", "germany": "德国 (D系)",
"usa": "U.S.A.", "usa": "美国 (M系)",
"china": "Chinese", "china": "中国 (C系)",
"uk": "U.K.", "uk": "英国 (Y系)",
"france": "French", "france": "法国 (F系)",
"japan": "Japanese", "japan": "日本 (R系)",
"european": "European", "european": "欧盟 (E系)",
"other": "Hybrid" "other": "X系"
}, },
"tank_class_short": { "tank_class_short": {
"-1": "unrecognized", "-1": "无法识别",
"0": "Light", "0": "轻型坦克",
"1": "Medium", "1": "中型坦克",
"2": "Heavy", "2": "重型坦克",
"3": "TD" "3": "坦克歼击车"
}, },
"tank_class_short_plural_lowercase": { "tank_class_short_plural_lowercase": {
"0": "lights", "0": "轻型坦克",
"1": "mediums", "1": "中型坦克",
"2": "heavies", "2": "重型坦克",
"3": "TDs" "3": "坦克歼击车"
}, },
"tank_class_medium": { "tank_class_medium": {
"-1": "unrecognized", "-1": "无法识别",
"0": "Light", "0": "轻型坦克",
"1": "Medium", "1": "Medium",
"2": "Heavy", "2": "Heavy",
"3": "Tank destroyer" "3": "Tank destroyer"
@@ -118,84 +118,86 @@
"cumulative_frags": "Total kills", "cumulative_frags": "Total kills",
"cumulative_hits": "Total hits", "cumulative_hits": "Total hits",
"cumulative_inaccuracy": "Inaccuracy", "cumulative_inaccuracy": "Inaccuracy",
"cumulative_kills_to_death_ratio": "Kills to death ratio", "cumulative_kills_to_death_ratio": "战损比",
"cumulative_losses": "Losses", "cumulative_losses": "失败",
"cumulative_misses": "Total misses", "cumulative_misses": "总射击失误数",
"cumulative_shots": "Total shots", "cumulative_shots": "总射击数",
"cumulative_spotted": "Total spots", "cumulative_spotted": "场均发现敌人",
"cumulative_survived_battles": "Survived battles", "cumulative_survived_battles": "幸存场次",
"cumulative_win_and_survived": "Won and survived battles", "cumulative_win_and_survived": "胜利和幸存场次",
"cumulative_wins": "Wins", "cumulative_wins": "胜利",
"cumulative_xp": "Total XP", "cumulative_xp": "总经验",
"cumulative_wn8": "WN8", "cumulative_wn8": "WN8",
"cumulative_battle_life_time": "Total battle time" "cumulative_battle_life_time": "总战斗时间"
} }
}, },
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"%s\". Try selecting a clan from the search result.", "clan_not_found": "# 找不到军团\n找不到\"%s\"。请尝试从搜索结果中选择一个军团。",
"player_not_found": "# Could not find user\nI couldn't find user \"%s\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.", "player_not_found": "# 找不到用户\n无法找到用户 \"%s\"。 尝试从搜索结果中挑选用户,输入一个有效的用户名或使用 \"/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 \"%s\".", "tank_name_not_found": "# 坦克找不到\n无法找到名为 \"%s \"的坦克。",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"%s\". Try typing the name of the tank instead of a number.", "tank_id_not_found": "# 坦克找不到\n无法找到ID \"%s\"的坦克。 试着输入坦克的名称而不是数字。",
"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这个玩家没有数据。你可能找错人了。",
"get_help": "Help Discord server", "get_help": "Help Discord server",
"uncaught_error": "# BlitzKit ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.", "uncaught_error": "# BlitzKit 崩溃了!\n非常抱歉。请加入[官方Discord](https://discord.gg/nDt7AjGJQH),以方便帮助你的问题。",
"preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon." "preview": "# 此命令处于预览\n\n您只能在[官方Discord服务器](https://discord.gg/nDt7AjGJQH)访问此命令。该命令将很快可用。",
"query_too_small": "# 您的搜索太短了\n您的搜索必须至少 3 个字符长。",
"query_too_long": "# 您的搜索太长了\n您的搜索必须最长为 100 个字符。"
}, },
"wn8": { "wn8": {
"$": "WN8", "$": "WN8",
"very_bad": "Very bad", "very_bad": "非常差",
"bad": "Bad", "bad": "差",
"below_average": "Below average", "below_average": "较差",
"average": "Average", "average": "平均",
"above_average": "Above average", "above_average": "高于平均",
"good": "Good", "good": "好",
"very_good": "Very good", "very_good": "非常好",
"great": "Great", "great": "很好",
"unicum": "Unicum", "unicum": "Unicum",
"super_unicum": "Super unicum" "super_unicum": "Super unicum"
}, },
"subcommands": { "subcommands": {
"today": { "today": {
"$": "today", "$": "今日:",
"description": "Today" "description": "今日:"
}, },
"30": { "30": {
"$": "30", "$": "30",
"description": "30 days" "description": "30天"
}, },
"60": { "60": {
"$": "60", "$": "60",
"description": "60 days" "description": "60天"
}, },
"90": { "90": {
"$": "90", "$": "90",
"description": "90 days" "description": "90天"
}, },
"career": { "career": {
"$": "career", "$": "玩家资料",
"description": "Career" "description": "玩家资料"
}, },
"custom": { "custom": {
"$": "custom", "$": "自定义",
"description": "Custom range", "description": "自定义范围",
"options": { "options": {
"start": { "start": {
"$": "start", "$": "开始时间",
"description": "Start in number of days ago from today" "description": "从今天开始于数天前"
}, },
"end": { "end": {
"$": "end", "$": "结束时间",
"description": "End in number of days ago from today" "description": "从今天开始于数天前"
} }
} }
} }
}, },
"options": { "options": {
"clan": { "clan": {
"$": "clan", "$": "军团",
"description": "The clan name or tag" "description": "The clan name or tag"
}, },
"tank": { "tank": {
@@ -211,13 +213,13 @@
"$": "nation", "$": "nation",
"description": "Nation" "description": "Nation"
}, },
"tank_class": { "class": {
"$": "tank-class", "$": "class",
"description": "Tank class" "description": "Class"
}, },
"tree_type": { "type": {
"$": "tree-type", "$": "type",
"description": "Tree type" "description": "Type"
} }
}, },
"region": { "region": {
-1
View File
@@ -13,7 +13,6 @@
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
"pickleparser": "^0.2.1", "pickleparser": "^0.2.1",
"protobufjs": "^7.4.0", "protobufjs": "^7.4.0",
"sharp": "^0.33.5",
"ts-proto": "^2.2.2", "ts-proto": "^2.2.2",
"yaml": "^2.5.0" "yaml": "^2.5.0"
}, },
+6 -1
View File
@@ -1,5 +1,10 @@
import { asset } from '@blitzkit/core'; import { asset } from '@blitzkit/core';
export function tankIcon(id: number, size: 'big' | 'small' = 'big') { export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
export function tankIcon(
id: number,
size: 'big' | 'small' | 'blitzkit' = 'blitzkit',
) {
return asset(`icons/tanks/${size}/${id}.webp`); return asset(`icons/tanks/${size}/${id}.webp`);
} }
+3 -2
View File
@@ -13,15 +13,16 @@
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts" "build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts"
}, },
"dependencies": { "dependencies": {
"esbuild": "^0.19.12",
"@blitzkit/core": "workspace:*", "@blitzkit/core": "workspace:*",
"@gltf-transform/core": "^4.0.10", "@gltf-transform/core": "^4.0.10",
"@gltf-transform/extensions": "^4.0.10", "@gltf-transform/extensions": "^4.0.10",
"@gltf-transform/functions": "^4.0.10", "@gltf-transform/functions": "^4.0.10",
"csv-parse": "^5.5.6", "csv-parse": "^5.5.6",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"esbuild": "^0.19.12",
"fast-xml-parser": "^4.4.1", "fast-xml-parser": "^4.4.1",
"progress": "^2.0.3" "progress": "^2.0.3",
"puppeteer": "^23.10.4"
}, },
"devDependencies": { "devDependencies": {
"@types/decompress": "^4.2.7", "@types/decompress": "^4.2.7",
@@ -0,0 +1,51 @@
import { BLITZKIT_TANK_ICON_SIZE, fetchTankDefinitions } from '@blitzkit/core';
import { exec } from 'child_process';
import { launch } from 'puppeteer';
import sharp from 'sharp';
import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles';
export async function blitzkitTankIcons() {
console.log('Building blitzkit tank icons...');
console.log('Starting website dev server...');
const child = exec('cd ../.. && bun dev:website');
const browser = await launch();
const page = await browser.newPage();
const tanks = Object.values((await fetchTankDefinitions()).tanks);
const files: FileChange[] = [];
let index = 0;
for (const { id, name } of tanks) {
await page.setViewport({
width: BLITZKIT_TANK_ICON_SIZE.width - 1,
height: BLITZKIT_TANK_ICON_SIZE.height,
});
await page.goto(`http://localhost:4321/api/tankopedia/tank-icon/${id}/`, {
waitUntil: 'networkidle0',
});
if (index === 0) {
console.log('Dev server started!');
}
const screenshot = await page.screenshot({
type: 'webp',
omitBackground: true,
});
const content = await sharp(screenshot).trim({ threshold: 100 }).toBuffer();
index++;
console.log(
`Rendered ${id} (${name}) ${((index / tanks.length) * 100).toFixed(2)}%`,
);
files.push({ path: `icons/tanks/blitzkit/${id}.webp`, content });
}
child.kill('SIGHUP');
await browser.close();
await commitAssets('tank icons', files);
}
@@ -39,9 +39,9 @@ import { readStringDVPL } from '../core/blitz/readStringDVPL';
import { readXMLDVPL } from '../core/blitz/readXMLDVPL'; import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL'; import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
import { commitAssets } from '../core/github/commitAssets'; import { commitAssets } from '../core/github/commitAssets';
import { TankParameters } from './blitzkitTankIcons';
import { DATA } from './constants'; import { DATA } from './constants';
import { Avatar } from './skillIcons'; import { Avatar } from './skillIcons';
import { TankParameters } from './tankIcons';
function parseResearchCost(raw: number | string) { function parseResearchCost(raw: number | string) {
if (typeof raw === 'number') { if (typeof raw === 'number') {
+3 -1
View File
@@ -9,6 +9,7 @@ const targets = argv
?.split('=')[1] ?.split('=')[1]
.split(','); .split(',');
import { blitzkitTankIcons } from './blitzkitTankIcons';
import { boosterIcons } from './boosterIcons'; import { boosterIcons } from './boosterIcons';
import { camouflageIcons } from './camouflageIcons'; import { camouflageIcons } from './camouflageIcons';
import { consumableProvisionIcons } from './consumableProvisionIcons'; import { consumableProvisionIcons } from './consumableProvisionIcons';
@@ -28,7 +29,6 @@ if (!targets && !allTargets) throw new Error('No target(s) specified');
const methods = [ const methods = [
definitions, definitions,
tankIcons,
tankModels, tankModels,
tankArmors, tankArmors,
gameModeBanners, gameModeBanners,
@@ -41,6 +41,8 @@ const methods = [
consumableProvisionIcons, consumableProvisionIcons,
camouflageIcons, camouflageIcons,
boosterIcons, boosterIcons,
tankIcons,
blitzkitTankIcons,
]; ];
for (const method of methods) { for (const method of methods) {
@@ -7,9 +7,9 @@ import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL'; import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
import { commitAssets } from '../core/github/commitAssets'; import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles'; import { FileChange } from '../core/github/commitMultipleFiles';
import { TankParameters } from './blitzkitTankIcons';
import { DATA } from './constants'; import { DATA } from './constants';
import { VehicleDefinitionList } from './definitions'; import { VehicleDefinitionList } from './definitions';
import { TankParameters } from './tankIcons';
export async function tankModels() { export async function tankModels() {
console.log('Building tank models...'); console.log('Building tank models...');
@@ -49,6 +49,7 @@ export function PrimaryArmorSceneComponent({
damage: { value: null }, damage: { value: null },
explosionRadius: { value: null }, explosionRadius: { value: null },
greenPenetration: { value: null }, greenPenetration: { value: null },
advancedHighlighting: { value: null },
opaque: { value: null }, opaque: { value: null },
inverseProjectionMatrix: { value: null }, inverseProjectionMatrix: { value: null },
@@ -90,6 +91,10 @@ export function PrimaryArmorSceneComponent({
function handleGreenPenetrationChange(greenPenetration: boolean) { function handleGreenPenetrationChange(greenPenetration: boolean) {
material.uniforms.greenPenetration.value = greenPenetration; material.uniforms.greenPenetration.value = greenPenetration;
} }
function handleAdvancedHighlightingChange(advancedHighlighting: boolean) {
material.uniforms.advancedHighlighting.value = advancedHighlighting;
invalidate();
}
function handleOpaqueChange(opaque: boolean) { function handleOpaqueChange(opaque: boolean) {
material.uniforms.opaque.value = opaque; material.uniforms.opaque.value = opaque;
} }
@@ -133,12 +138,13 @@ export function PrimaryArmorSceneComponent({
if (!noInvalidate) invalidate(); if (!noInvalidate) invalidate();
} }
const initialState = tankopediaPersistentStore.getState();
handleShellChange(); handleShellChange();
handleGreenPenetrationChange( handleGreenPenetrationChange(initialState.greenPenetration);
tankopediaPersistentStore.getState().greenPenetration, handleAdvancedHighlightingChange(initialState.advancedHighlighting);
); handleOpaqueChange(initialState.opaque);
handleOpaqueChange(tankopediaPersistentStore.getState().opaque); handleWireframeChange(initialState.wireframe);
handleWireframeChange(tankopediaPersistentStore.getState().wireframe);
handleProtagonistEquipmentChange( handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix, duelStore.getState().protagonist.equipmentMatrix,
); );
@@ -156,6 +162,10 @@ export function PrimaryArmorSceneComponent({
(state) => state.greenPenetration, (state) => state.greenPenetration,
handleGreenPenetrationChange, handleGreenPenetrationChange,
), ),
tankopediaPersistentStore.subscribe(
(state) => state.advancedHighlighting,
handleAdvancedHighlightingChange,
),
tankopediaPersistentStore.subscribe( tankopediaPersistentStore.subscribe(
(state) => state.opaque, (state) => state.opaque,
handleOpaqueChange, handleOpaqueChange,
@@ -12,82 +12,102 @@ uniform bool canSplash;
uniform float damage; uniform float damage;
uniform float explosionRadius; uniform float explosionRadius;
uniform bool greenPenetration; uniform bool greenPenetration;
uniform bool advancedHighlighting;
uniform bool opaque; uniform bool opaque;
uniform mat4 inverseProjectionMatrix; uniform mat4 inverseProjectionMatrix;
uniform highp sampler2D spacedArmorBuffer; uniform highp sampler2D spacedArmorBuffer;
uniform highp sampler2D spacedArmorDepth; uniform highp sampler2D spacedArmorDepth;
float depthToDistance(float depth) { const float HALF = 0.5;
vec4 clipPosition = vec4(gl_FragCoord.xy / resolution * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); const float EXPLOSION_DAMAGE_FACTOR = 1.1;
vec4 eyePosition = inverseProjectionMatrix * clipPosition; const float RANDOMIZATION_FACTOR = 0.05;
return length(eyePosition.xyz / eyePosition.w); const float GREEN_VALUE = 0.392;
float getDistance(vec2 coord, float depth) {
vec4 clipPos = vec4(coord * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
vec4 eyePos = inverseProjectionMatrix * clipPos;
return length(eyePos.xyz / eyePos.w);
}
vec3 getPenetrationColor(bool isThreeCalibersRule) {
if (advancedHighlighting) {
return vec3(0.0, 1.0, isThreeCalibersRule ? 1.0 : 0.0);
}
return vec3(0.0, 1.0, 0.0);
} }
void main() { void main() {
float penetrationChance = -1.0; vec2 screenCoord = gl_FragCoord.xy / resolution;
float splashChance = -1.0; vec4 spacedArmorData = texture2D(spacedArmorBuffer, screenCoord);
bool isUnderSpacedArmor = spacedArmorData.a != 0.0;
vec2 screenCoordinates = gl_FragCoord.xy / resolution; float viewDistance = length(vViewPosition);
float angle = acos(dot(vNormal, -vViewPosition) / length(vViewPosition)); float angle = acos(dot(vNormal, -vViewPosition) / viewDistance);
vec4 spacedArmorBufferFragment = texture2D(spacedArmorBuffer, screenCoordinates);
bool isUnderSpacedArmor = spacedArmorBufferFragment.a != 0.0;
bool threeCalibersRule = caliber > thickness * 3.0 || isUnderSpacedArmor; bool threeCalibersRule = caliber > thickness * 3.0 || isUnderSpacedArmor;
float penetrationChance = -1.0;
float splashChance = 0.0;
bool didRicochet = false;
if (!threeCalibersRule && angle >= ricochet) { if (!threeCalibersRule && angle >= ricochet) {
penetrationChance = 0.0; penetrationChance = 0.0;
didRicochet = true;
} else { } else {
bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0; float thicknessRatio = thickness > 0.0 ? caliber / thickness : 0.0;
float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization; bool twoCalibersRule = thicknessRatio > 2.0;
float finalThickness = thickness / cos(max(0.0, angle - finalNormalization)); float finalNorm = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization;
float finalThickness = thickness / cos(max(0.0, angle - finalNorm));
// world space in meters float remainingPen = penetration;
vec4 spacedArmorDepthFragment = texture2D(spacedArmorDepth, screenCoordinates);
float spacedArmorDistance = depthToDistance(spacedArmorDepthFragment.r);
float primaryArmorDistance = depthToDistance(gl_FragCoord.z);
float distanceFromSpacedArmor = primaryArmorDistance - spacedArmorDistance;
if (canSplash && isUnderSpacedArmor) { if (isUnderSpacedArmor) {
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration; float spacedArmorThickness = spacedArmorData.r * penetration;
float finalDamage = 0.5 * damage * (1.0 - distanceFromSpacedArmor / explosionRadius) - 1.1 * (finalThickness + spacedArmorThickness); remainingPen -= spacedArmorThickness;
penetrationChance = 0.0; if (isExplosive && remainingPen > 0.0) {
splashChance = finalDamage > 0.0 ? 1.0 : 0.0; float spacedDist = getDistance(screenCoord, texture2D(spacedArmorDepth, screenCoord).r);
} else { float primaryDist = getDistance(screenCoord, gl_FragCoord.z);
float remainingPenetration = penetration; float distFromArmor = primaryDist - spacedDist;
if (isUnderSpacedArmor) { if (canSplash) {
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration; float finalDamage = HALF * damage * (1.0 - distFromArmor / explosionRadius) - EXPLOSION_DAMAGE_FACTOR * (finalThickness + spacedArmorThickness);
remainingPenetration -= spacedArmorThickness; splashChance = step(0.0, finalDamage);
penetrationChance = 0.0;
if (isExplosive && remainingPenetration > 0.0) { } else {
remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor; remainingPen -= HALF * remainingPen * distFromArmor;
} }
} }
}
remainingPenetration = max(0.0, remainingPenetration); if (penetrationChance < 0.0) {
float delta = finalThickness - remainingPenetration; remainingPen = max(0.0, remainingPen);
float randomization = remainingPenetration * 0.05; float delta = finalThickness - remainingPen;
float randomization = remainingPen * RANDOMIZATION_FACTOR;
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0); penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
if (canSplash) { if (canSplash) {
float splashDamage = 0.5 * damage - 1.1 * finalThickness; float splashDamage = HALF * damage - EXPLOSION_DAMAGE_FACTOR * finalThickness;
splashChance = splashDamage > 0.0 ? 1.0 : 0.0; splashChance = step(0.0, splashDamage);
} else {
splashChance = 0.0;
} }
} }
} }
float alpha = opaque ? 1.0 : 0.5; float alpha = opaque ? 1.0 : HALF;
vec3 color = vec3(1.0, splashChance * 0.392, 0.0); vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
if (greenPenetration) {
// non-trigonometric red-green mixing: https://www.desmos.com/calculator/ceo1oq7l67 if (advancedHighlighting && didRicochet) {
float falling = -pow(penetrationChance, 2.0) + 1.0; baseColor = vec3(1.0, baseColor.g, 1.0);
float gaining = -pow(penetrationChance - 1.0, 2.0) + 1.0; }
gl_FragColor = vec4(falling * color + gaining * vec3(0.0, 1.0, 0.0), alpha);
if (greenPenetration || advancedHighlighting) {
float falling = -penetrationChance * penetrationChance + 1.0;
float gaining = -(penetrationChance - 1.0) * (penetrationChance - 1.0) + 1.0;
vec3 penColor = getPenetrationColor(threeCalibersRule);
gl_FragColor = vec4(falling * baseColor + gaining * penColor, alpha);
} else { } else {
gl_FragColor = vec4(color, (1.0 - penetrationChance) * alpha); gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
} }
} }
+12 -5
View File
@@ -1,25 +1,30 @@
--- ---
import { assertSecret } from '@blitzkit/core'; import { assertSecret } from '@blitzkit/core';
const PUBLIC_GOOGLE_TAG_MANAGER_ID = assertSecret(
import.meta.env.PUBLIC_GOOGLE_TAG_MANAGER_ID,
);
--- ---
{/* google analytics */} {/* google analytics */}
<script <script
is:inline is:inline
async async
src={`https://www.googletagmanager.com/gtag/js?id=${assertSecret( src={`https://www.googletagmanager.com/gtag/js?id=${PUBLIC_GOOGLE_TAG_MANAGER_ID}`}
import.meta.env.PUBLIC_GOOGLE_TAG_MANAGER_ID, ></script>
)}`}></script> <script is:inline define:vars={{ PUBLIC_GOOGLE_TAG_MANAGER_ID }}>
<script is:inline>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag() { function gtag() {
dataLayer.push(arguments); dataLayer.push(arguments);
} }
gtag('js', new Date()); gtag('js', new Date());
gtag('config', assertSecret(import.meta.env.PUBLIC_GOOGLE_TAG_MANAGER_ID)); gtag('config', PUBLIC_GOOGLE_TAG_MANAGER_ID);
</script> </script>
{/* nitro ads */} {/* nitro ads */}
{
/*
<script is:inline data-cfasync="false"> <script is:inline data-cfasync="false">
window.nitroAds = window.nitroAds || { window.nitroAds = window.nitroAds || {
createAd: function () { createAd: function () {
@@ -33,3 +38,5 @@ import { assertSecret } from '@blitzkit/core';
queue: [], queue: [],
}; };
</script> </script>
*/
}
@@ -6,11 +6,16 @@ import { Tanks } from './Tanks';
export function TankTable({ skeleton }: MaybeSkeletonComponentProps) { export function TankTable({ skeleton }: MaybeSkeletonComponentProps) {
return ( return (
<Flex justify="center"> <Flex justify="center" maxWidth="100%" flexGrow="1" position="relative">
<StickyTableRoot <StickyTableRoot
size={{ initial: '1', sm: '2' }} size={{ initial: '1', sm: '2' }}
variant="surface" variant="surface"
style={{ maxWidth: '100%' }} style={{
position: 'absolute',
maxWidth: '100%',
maxHeight: '100%',
height: '100%',
}}
> >
<Header /> <Header />
<Tanks skeleton={skeleton} /> <Tanks skeleton={skeleton} />
@@ -23,9 +23,10 @@
.tank-search-card .image { .tank-search-card .image {
width: 100%; width: 100%;
height: 4rem; height: 6rem;
object-position: center right; object-position: center right;
object-fit: contain; object-fit: contain;
filter: brightness(1.25) drop-shadow(-1px -1px 4px black);
} }
.tank-search-card .class-icon { .tank-search-card .class-icon {
@@ -1,4 +1,4 @@
import { asset, tankIcon, TankType, type TankDefinition } from '@blitzkit/core'; import { asset, TankType, type TankDefinition } from '@blitzkit/core';
import { Flex, Link, Text, type TextProps } from '@radix-ui/themes'; import { Flex, Link, Text, type TextProps } from '@radix-ui/themes';
import { uniq } from 'lodash-es'; import { uniq } from 'lodash-es';
import { forwardRef, type ReactNode } from 'react'; import { forwardRef, type ReactNode } from 'react';
@@ -34,7 +34,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
<Text <Text
ref={ref} ref={ref}
tabIndex={onSelect ? 0 : undefined} tabIndex={onSelect ? 0 : undefined}
size="1" size="2"
color={ color={
tank.type === TankType.COLLECTOR tank.type === TankType.COLLECTOR
? 'blue' ? 'blue'
@@ -70,7 +70,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
> >
<img <img
alt={tank.name} alt={tank.name}
src={tankIcon(tank.id)} src={asset(`icons/tanks/blitzkit/${tank.id}.webp`)}
className="image" className="image"
draggable={false} draggable={false}
/> />
@@ -81,6 +81,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
align="center" align="center"
overflow="hidden" overflow="hidden"
width="100%" width="100%"
mt="1"
> >
<Icon className="class-icon" /> <Icon className="class-icon" />
<Text align="center" className="name"> <Text align="center" className="name">
@@ -16,7 +16,7 @@ export const SkeletonTankCard = memo(
return ( return (
<Flex direction="column" gap="2" ref={card}> <Flex direction="column" gap="2" ref={card}>
<Skeleton height="64px" /> <Skeleton height="5rem" />
<Skeleton height="1em" /> <Skeleton height="1em" />
</Flex> </Flex>
); );
@@ -4,7 +4,10 @@ export function TankCardWrapper(props: GridProps) {
return ( return (
<Grid <Grid
py="4" py="4"
columns="repeat(auto-fill, minmax(100px, 1fr))" columns={{
initial: 'repeat(auto-fill, minmax(6rem, 1fr))',
sm: 'repeat(auto-fill, minmax(8rem, 1fr))',
}}
flow="dense" flow="dense"
gap="2" gap="2"
gapY="6" gapY="6"
@@ -1 +1 @@
export const MAX_RECENTLY_VIEWED = 9; export const MAX_RECENTLY_VIEWED = 14;
@@ -39,8 +39,8 @@ type TankSearchProps = Omit<FlexProps, 'onSelect'> & {
onSelectAll?: (tanks: TankDefinition[]) => void; onSelectAll?: (tanks: TankDefinition[]) => void;
}; };
const PREVIEW_COUNT = 25; const PREVIEW_COUNT = 20;
const DEFAULT_LOADED_CARDS = 75; const DEFAULT_LOADED_CARDS = 64;
const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] = const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] =
await Promise.all([ await Promise.all([
@@ -17,6 +17,7 @@ import {
import { import {
Box, Box,
Button, Button,
Checkbox,
Dialog, Dialog,
DropdownMenu, DropdownMenu,
Flex, Flex,
@@ -68,6 +69,9 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
const greenPenetration = TankopediaPersistent.use( const greenPenetration = TankopediaPersistent.use(
(state) => state.greenPenetration, (state) => state.greenPenetration,
); );
const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting,
);
const wireframe = TankopediaPersistent.use((state) => state.wireframe); const wireframe = TankopediaPersistent.use((state) => state.wireframe);
const opaque = TankopediaPersistent.use((state) => state.opaque); const opaque = TankopediaPersistent.use((state) => state.opaque);
const fullScreenAvailable = useFullscreenAvailability(); const fullScreenAvailable = useFullscreenAvailability();
@@ -325,53 +329,71 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
style={{ transform: 'translateX(-50%)' }} style={{ transform: 'translateX(-50%)' }}
> >
{display === TankopediaDisplay.DynamicArmor && ( {display === TankopediaDisplay.DynamicArmor && (
<Flex align="center" gap="2"> <>
<Text color="gray" size="2"> <Flex
Shooter: align="center"
</Text> gap="2"
<Dialog.Root mb="1"
open={antagonistSelectorOpen} style={{ cursor: 'pointer' }}
onOpenChange={setAntagonistSelectorOpen} onClick={() => {
mutateTankopediaPersistent((draft) => {
draft.advancedHighlighting = !draft.advancedHighlighting;
});
}}
> >
<Dialog.Trigger> <Checkbox checked={advancedHighlighting} />
<Button variant="ghost"> <Text color="gray" size="2">
<Flex gap="2" align="center"> Advanced highlighting
<SmallTankIcon id={antagonistTank.id} size={16} /> </Text>
{antagonistTank.name} </Flex>
</Flex>
</Button>
</Dialog.Trigger>
<Dialog.Content> <Flex align="center" gap="2">
<Dialog.Title align="center">Select tank</Dialog.Title> <Text color="gray" size="2">
Shooter:
</Text>
<Dialog.Root
open={antagonistSelectorOpen}
onOpenChange={setAntagonistSelectorOpen}
>
<Dialog.Trigger>
<Button variant="ghost">
<Flex gap="2" align="center">
<SmallTankIcon id={antagonistTank.id} size={16} />
{antagonistTank.name}
</Flex>
</Button>
</Dialog.Trigger>
<Tabs.Root <Dialog.Content>
value={tab} <Dialog.Title align="center">Select tank</Dialog.Title>
onValueChange={setTab}
style={{ position: 'relative' }} <Tabs.Root
> value={tab}
<TankSearch onValueChange={setTab}
compact style={{ position: 'relative' }}
onSelect={(tank) => { >
mutateDuel((draft) => { <TankSearch
draft.antagonist.tank = tank; compact
draft.antagonist.engine = tank.engines.at(-1)!; onSelect={(tank) => {
draft.antagonist.track = tank.tracks.at(-1)!; mutateDuel((draft) => {
draft.antagonist.turret = tank.turrets.at(-1)!; draft.antagonist.tank = tank;
draft.antagonist.gun = draft.antagonist.engine = tank.engines.at(-1)!;
draft.antagonist.turret.guns.at(-1)!; draft.antagonist.track = tank.tracks.at(-1)!;
draft.antagonist.shell = draft.antagonist.turret = tank.turrets.at(-1)!;
draft.antagonist.gun.gun_type!.value.base.shells[0]; draft.antagonist.gun =
}); draft.antagonist.turret.guns.at(-1)!;
setAntagonistSelectorOpen(false); draft.antagonist.shell =
}} draft.antagonist.gun.gun_type!.value.base.shells[0];
/> });
</Tabs.Root> setAntagonistSelectorOpen(false);
</Dialog.Content> }}
</Dialog.Root> />
</Flex> </Tabs.Root>
</Dialog.Content>
</Dialog.Root>
</Flex>
</>
)} )}
<Flex gap="3" align="center" mt="2"> <Flex gap="3" align="center" mt="2">
<SegmentedControl.Root <SegmentedControl.Root
value={`${display}`} value={`${display}`}
@@ -8,10 +8,6 @@ import { degToRad } from 'three/src/math/MathUtils.js';
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment'; import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import {
modelTransformEvent,
type ModelTransformEventData,
} from '../../../../../../core/blitzkit/modelTransform';
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose'; import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
import { Duel } from '../../../../../../stores/duel'; import { Duel } from '../../../../../../stores/duel';
import { import {
@@ -35,7 +31,11 @@ const ARCADE_MODE_DISTANCE = 19;
const ARCADE_MODE_ANGLE = degToRad(10); const ARCADE_MODE_ANGLE = degToRad(10);
const ARCADE_MODE_FOV = 54; const ARCADE_MODE_FOV = 54;
export function Controls() { interface ControlsProps {
naked?: boolean;
}
export function Controls({ naked }: ControlsProps) {
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const display = TankopediaEphemeral.use((state) => state.display); const display = TankopediaEphemeral.use((state) => state.display);
const duelStore = Duel.useStore(); const duelStore = Duel.useStore();
@@ -82,13 +82,17 @@ export function Controls() {
antagonistModelDefinition.turret_origin.y + antagonistModelDefinition.turret_origin.y +
antagonistTurretModelDefinition.gun_origin.y; antagonistTurretModelDefinition.gun_origin.y;
const [autoRotate, setAutoRotate] = useState( const [autoRotate, setAutoRotate] = useState(
display !== TankopediaDisplay.ShootingRange, display !== TankopediaDisplay.ShootingRange && !naked,
); );
const gunHeight = const gunHeight =
protagonistHullOrigin.y + protagonistHullOrigin.y +
protagonistTurretOrigin.y + protagonistTurretOrigin.y +
protagonistGunOrigin.y; protagonistGunOrigin.y;
const inspectModeInitialPosition = [-8, gunHeight + 4, -13] as const; const inspectModeInitialPosition = [
-8,
gunHeight + (naked ? 10 : 4),
-13,
] as const;
const protagonistGunOriginOnlyY = new Vector3( const protagonistGunOriginOnlyY = new Vector3(
0, 0,
protagonistTurretModelDefinition.gun_origin.y, protagonistTurretModelDefinition.gun_origin.y,
@@ -249,10 +253,6 @@ export function Controls() {
canvas.addEventListener('wheel', handleWheel); canvas.addEventListener('wheel', handleWheel);
document.body.addEventListener('scroll', handleScroll); document.body.addEventListener('scroll', handleScroll);
const sniperModeCameraOffset = new Vector3();
let pitch = 0;
let yaw = 0;
function updateCamera() { function updateCamera() {
if (!orbitControls.current) return; if (!orbitControls.current) return;
@@ -298,22 +298,15 @@ export function Controls() {
} }
} else { } else {
camera.position.set(...inspectModeInitialPosition); camera.position.set(...inspectModeInitialPosition);
orbitControls.current.target.set(0, gunHeight / 2, 0); orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
orbitControls.current.enablePan = true; orbitControls.current.enablePan = true;
orbitControls.current.enableZoom = true; orbitControls.current.enableZoom = true;
camera.fov = 25; camera.fov = naked ? 20 : 25;
} }
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
} }
function handleModelTransform(event: ModelTransformEventData) {
pitch = event.pitch;
yaw = event.yaw ?? yaw;
}
modelTransformEvent.on(handleModelTransform);
updateCamera(); updateCamera();
const unsubscribeDisplay = tankopediaEphemeralStore.subscribe( const unsubscribeDisplay = tankopediaEphemeralStore.subscribe(
@@ -337,7 +330,6 @@ export function Controls() {
window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown);
canvas.removeEventListener('wheel', handleWheel); canvas.removeEventListener('wheel', handleWheel);
document.body.removeEventListener('scroll', handleScroll); document.body.removeEventListener('scroll', handleScroll);
modelTransformEvent.off(handleModelTransform);
}; };
}, [display === TankopediaDisplay.ShootingRange]); }, [display === TankopediaDisplay.ShootingRange]);
@@ -348,7 +340,6 @@ export function Controls() {
ref={orbitControls} ref={orbitControls}
enabled={tankopediaEphemeralStore.getState().controlsEnabled} enabled={tankopediaEphemeralStore.getState().controlsEnabled}
enableDamping={false} enableDamping={false}
maxPolarAngle={degToRad(100)}
autoRotate={autoRotate} autoRotate={autoRotate}
autoRotateSpeed={1 / 4} autoRotateSpeed={1 / 4}
/> />
@@ -1,9 +1,10 @@
import { Environment } from '@react-three/drei'; import { Environment } from '@react-three/drei';
import { times } from 'lodash-es'; import { times } from 'lodash-es';
import { Euler } from 'three';
import { degToRad } from 'three/src/math/MathUtils.js';
import { useModel } from '../../../../../../hooks/useModel'; import { useModel } from '../../../../../../hooks/useModel';
import { Duel } from '../../../../../../stores/duel'; import { Duel } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
const LIGHTS_COUNT = 5; const LIGHTS_COUNT = 5;
@@ -14,17 +15,13 @@ export function Lighting() {
const { hasPbr } = useModel(protagonist.tank.id); const { hasPbr } = useModel(protagonist.tank.id);
const isBrighterLighting = const isBrighterLighting =
!hasPbr && display !== TankopediaDisplay.StaticArmor; !hasPbr && display !== TankopediaDisplay.StaticArmor;
const showEnvironment = TankopediaPersistent.use(
(state) => state.showEnvironment,
);
const environment = TankopediaPersistent.use((state) => state.environment);
return ( return (
<> <>
<Environment <Environment
preset={environment} files="https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/winter_river_1k.hdr"
background={showEnvironment} environmentIntensity={isBrighterLighting ? 1.5 : 1}
environmentIntensity={isBrighterLighting ? 3 : 1} environmentRotation={new Euler(0, degToRad(180), 0)}
/> />
{display === TankopediaDisplay.ShootingRange && ( {display === TankopediaDisplay.ShootingRange && (
<> <>
@@ -3,7 +3,7 @@ import { Box, type BoxProps } from '@radix-ui/themes';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { Var } from '../../../../../../../../core/radix/var'; import { Var } from '../../../../../../../../core/radix/var';
const DOTS = 32; // const DOTS = 32;
type TargetCircleProps = BoxProps & { type TargetCircleProps = BoxProps & {
variant: 'client' | 'server'; variant: 'client' | 'server';
@@ -1,3 +1,4 @@
import { BLITZKIT_TANK_ICON_SIZE } from '@blitzkit/core';
import { Canvas } from '@react-three/fiber'; import { Canvas } from '@react-three/fiber';
import { import {
forwardRef, forwardRef,
@@ -31,10 +32,11 @@ import { TankModel } from './components/TankModel';
interface TankSandboxProps { interface TankSandboxProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
naked?: boolean;
} }
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>( export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
({ thicknessRange }, ref) => { ({ thicknessRange, naked }, ref) => {
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122); const hasImprovedVerticalStabilizer = useEquipment(122);
@@ -179,10 +181,15 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
}} }}
style={{ userSelect: 'none' }} style={{
userSelect: 'none',
width: naked ? BLITZKIT_TANK_ICON_SIZE.width : '100%',
height: naked ? BLITZKIT_TANK_ICON_SIZE.height : '100%',
outline: naked ? '1rem red solid' : undefined,
}}
> >
<Controls /> <Controls naked={naked} />
<SceneProps /> {!naked && <SceneProps />}
{display !== TankopediaDisplay.StaticArmor && <TankModel />} {display !== TankopediaDisplay.StaticArmor && <TankModel />}
<ShotDisplay /> <ShotDisplay />
<ArmorPlateDisplay /> <ArmorPlateDisplay />
@@ -1,4 +1,4 @@
import { tankIcon } from '@blitzkit/core'; import { asset } from '@blitzkit/core';
import { Flex, type FlexProps, Progress } from '@radix-ui/themes'; import { Flex, type FlexProps, Progress } from '@radix-ui/themes';
import { useProgress } from '@react-three/drei'; import { useProgress } from '@react-three/drei';
@@ -11,11 +11,14 @@ export function TankSandboxLoader({ id, ...props }: TankSandboxLoaderProps) {
return ( return (
<Flex width="100%" height="100%" align="center" justify="center" {...props}> <Flex width="100%" height="100%" align="center" justify="center" {...props}>
<img src={tankIcon(id)} style={{ height: '60%', filter: 'blur(4px)' }} /> <img
src={asset(`icons/tanks/blitzkit/${id}.webp`)}
style={{ height: '75%', filter: 'blur(1rem)' }}
/>
<Progress <Progress
size="3" size="3"
value={data.progress} value={data.progress}
style={{ position: 'absolute', width: 128 }} style={{ position: 'absolute', width: '16rem', maxWidth: '50vw' }}
/> />
</Flex> </Flex>
); );
@@ -84,7 +84,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
height={ height={
isFullScreen ? '100vh' : `calc(100vh - ${NAVBAR_HEIGHT}px - 8rem)` isFullScreen ? '100vh' : `calc(100vh - ${NAVBAR_HEIGHT}px - 8rem)`
} }
maxHeight={isFullScreen ? undefined : '40rem'} maxHeight={isFullScreen ? undefined : '60rem'}
maxWidth={isFullScreen ? undefined : '120rem'} maxWidth={isFullScreen ? undefined : '120rem'}
flexGrow="1" flexGrow="1"
width={isFullScreen ? '100vw' : undefined} width={isFullScreen ? '100vw' : undefined}
@@ -92,47 +92,44 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
top={isFullScreen ? '0' : undefined} top={isFullScreen ? '0' : undefined}
left={isFullScreen ? '0' : undefined} left={isFullScreen ? '0' : undefined}
> >
<Flex justify="center"> <Flex
<Flex justify="center"
display={isFullScreen ? 'none' : 'flex'} position={{ initial: 'relative', md: 'absolute' }}
style={{ userSelect: 'none' }} left={{ initial: '0', md: '5', lg: '9' }}
direction="column" py="4"
align={{ initial: 'center', md: 'start' }} top={{ initial: '8', md: '50%' }}
justify="center" style={{ transform: 'translateY(-50%)' }}
pl={{ initial: '0', md: '9' }} direction="column"
pt={{ initial: '5', md: '0' }} gap={{ initial: '0', lg: '2' }}
overflow="hidden" align={{ initial: 'center', md: 'start' }}
flexGrow="1" >
maxWidth="40rem" <Flex align="center" gap="3">
> <Heading
<Flex align="center" gap="2"> color={treeColor}
<Heading trim="end"
color={treeColor} size={{ initial: '7', lg: '8' }}
trim="end"
size={{ initial: '7', lg: '8' }}
>
<Icon width="1em" height="1em" />
</Heading>
<Heading
weight="bold"
size={{ initial: '8', lg: '9' }}
wrap="nowrap"
color={treeColor}
>
{protagonist.name}
</Heading>
</Flex>
<Text
color="gray"
size="3"
weight="light"
ml={{ initial: '0', md: '7', lg: '8' }}
> >
BlitzKit Tankopedia <Icon width="1em" height="1em" />
</Text> </Heading>
<Heading
weight="bold"
size={{ initial: '7', lg: '9' }}
wrap="nowrap"
color={treeColor}
>
{protagonist.name}
</Heading>
</Flex> </Flex>
<Text
color="gray"
size={{ initial: '3', lg: '4' }}
weight="light"
ml={{ initial: '0', md: '7', lg: '8' }}
>
BlitzKit Tankopedia
</Text>
</Flex> </Flex>
<Box <Box
@@ -68,7 +68,7 @@ export function TierListTile(props: TierListTileProps) {
lastPosition.current = { x: event.clientX, y: event.clientY }; lastPosition.current = { x: event.clientX, y: event.clientY };
}, []); }, []);
const handlePointerUp = useCallback((event: PointerEvent) => { const handlePointerUp = useCallback(() => {
if (!card.current) return; if (!card.current) return;
const cardRect = card.current.getBoundingClientRect(); const cardRect = card.current.getBoundingClientRect();
@@ -0,0 +1,40 @@
import { TankSandbox } from '../../../../../components/Tankopedia/HeroSection/components/TankSandbox';
import { awaitableModelDefinitions } from '../../../../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
import { awaitableTankDefinitions } from '../../../../../core/awaitables/tankDefinitions';
import { App } from '../../../../../stores/app';
import { Duel } from '../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
const [provisionDefinitions, tankDefinitions, modelDefinitions] =
await Promise.all([
awaitableProvisionDefinitions,
awaitableTankDefinitions,
awaitableModelDefinitions,
]);
interface PageProps {
id: number;
}
export function Page({ id }: PageProps) {
const tank = tankDefinitions.tanks[id];
const model = modelDefinitions.models[id];
return (
<App.Provider>
<TankopediaPersistent.Provider>
<Duel.Provider data={{ provisionDefinitions, tank }}>
<TankopediaEphemeral.Provider data={model}>
<Content />
</TankopediaEphemeral.Provider>
</Duel.Provider>
</TankopediaPersistent.Provider>
</App.Provider>
);
}
function Content() {
return <TankSandbox naked thicknessRange={{ value: 0 }} />;
}
@@ -0,0 +1,30 @@
---
import { assertSecret } from '@blitzkit/core';
import type { GetStaticPaths } from 'astro';
import { getStaticPaths as tankopediaGetStaticPaths } from '../../../../tools/tankopedia/[id]/index.astro';
import { Page } from './_index';
export const getStaticPaths = (async () => {
if (assertSecret(import.meta.env.MODE) === 'development') {
return await tankopediaGetStaticPaths();
} else {
return [];
}
}) satisfies GetStaticPaths;
const { id } = Astro.params;
---
<html>
<head>
<style is:global>
body {
margin: 0;
}
</style>
</head>
<body>
<Page {id} client:load />
</body>
</html>
@@ -1,7 +1,5 @@
import { PageWrapper } from '../../../components/PageWrapper'; import { PageWrapper } from '../../../components/PageWrapper';
import { Charts } from '../../../components/Performance/Charts'; import { Charts } from '../../../components/Performance/Charts';
import { ModeSwitcher } from '../../../components/Performance/ModeSwitcher';
import { PerformanceInfo } from '../../../components/Performance/PerformanceInfo';
import { PlayerCountControl } from '../../../components/Performance/PlayerCountControl'; import { PlayerCountControl } from '../../../components/Performance/PlayerCountControl';
import { TankTable } from '../../../components/Performance/Table'; import { TankTable } from '../../../components/Performance/Table';
import { FilterControl } from '../../../components/TankSearch/components/FilterControl'; import { FilterControl } from '../../../components/TankSearch/components/FilterControl';
@@ -26,10 +24,10 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
const mode = TankPerformanceEphemeral.use((state) => state.mode); const mode = TankPerformanceEphemeral.use((state) => state.mode);
return ( return (
<PageWrapper color="jade" maxWidth="unset"> <PageWrapper color="jade" maxWidth="100%">
<ModeSwitcher /> {/* <ModeSwitcher /> */}
<PerformanceInfo skeleton={skeleton} /> {/* <PerformanceInfo skeleton={skeleton} /> */}
<FilterControl /> <FilterControl />
<PlayerCountControl /> <PlayerCountControl />
@@ -1,11 +1,14 @@
--- ---
import ToolOpenGraph from '../../../components/ToolOpenGraph.astro';
import { performanceTool } from '../../../constants/tools'; import { performanceTool } from '../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { Page } from './_index'; import { Page } from './_index';
--- ---
<ToolLayout tool={performanceTool}> <ToolOpenGraph tool={performanceTool} />
<BaseLayout noFooter>
<Page client:only="react"> <Page client:only="react">
<Page skeleton slot="fallback" /> <Page skeleton slot="fallback" />
</Page> </Page>
</ToolLayout> </BaseLayout>
@@ -5,7 +5,7 @@ import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
export function Page() { export function Page() {
return ( return (
<PageWrapper color="purple" maxWidth="64rem"> <PageWrapper color="purple" maxWidth="80rem">
<TankopediaPersistent.Provider> <TankopediaPersistent.Provider>
<App.Provider> <App.Provider>
<TankSearch /> <TankSearch />
@@ -1,7 +1,6 @@
import * as lodash from 'lodash-es'; import * as lodash from 'lodash-es';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware'; import { persist, subscribeWithSelector } from 'zustand/middleware';
import { ENVIRONMENTS } from '../../constants/lightingEnvironments';
import { createContextualStore } from '../../core/zustand/createContextualStore'; import { createContextualStore } from '../../core/zustand/createContextualStore';
import { SORT_NAMES } from './constants'; import { SORT_NAMES } from './constants';
@@ -12,10 +11,9 @@ export type TankopediaTestTankDisplay = 'include' | 'exclude' | 'only';
export interface TankopediaPersistentStore { export interface TankopediaPersistentStore {
wireframe: boolean; wireframe: boolean;
opaque: boolean; opaque: boolean;
environment: (typeof ENVIRONMENTS)[number]; advancedHighlighting: boolean;
showGrid: boolean; showGrid: boolean;
greenPenetration: boolean; greenPenetration: boolean;
showEnvironment: boolean;
showSpacedArmor: boolean; showSpacedArmor: boolean;
showExternalModules: boolean; showExternalModules: boolean;
showPrimaryArmor: boolean; showPrimaryArmor: boolean;
@@ -33,10 +31,9 @@ export const TankopediaPersistent = createContextualStore(() =>
subscribeWithSelector<TankopediaPersistentStore>(() => ({ subscribeWithSelector<TankopediaPersistentStore>(() => ({
wireframe: false, wireframe: false,
opaque: false, opaque: false,
environment: ENVIRONMENTS[0], advancedHighlighting: false,
showGrid: true, showGrid: true,
greenPenetration: false, greenPenetration: false,
showEnvironment: false,
showSpacedArmor: true, showSpacedArmor: true,
showExternalModules: true, showExternalModules: true,
showPrimaryArmor: true, showPrimaryArmor: true,
+5
View File
@@ -0,0 +1,5 @@
{
"github": {
"enabled": false
}
}