Compare commits

...
5 Commits
Author SHA1 Message Date
TrèsAbhi 5594c0ec3b feat: some progress 2025-10-01 22:45:21 -05:00
TrèsAbhi 3d69d464f5 feat: start on amending slugs 2025-10-01 22:41:23 -05:00
TrèsAbhi dd0b2bade7 feat: named slugs 2025-10-01 22:41:15 -05:00
TrèsAbhi 99c90cfa9e Merge branch 'dev' of https://github.com/blitzkit/blitzkit into dev 2025-10-01 22:24:10 -05:00
TrèsAbhi dd3ce21508 feat: gun rotation 2025-10-01 22:24:08 -05:00
14 changed files with 440 additions and 579 deletions

No files matched your search

+1
View File
@@ -86,6 +86,7 @@
"Fullscreen",
"fullstats",
"fuzzysort",
"gamemode",
"gameplay",
"glsl",
"GLTF",
-144
View File
@@ -1,144 +0,0 @@
import {
BlitzTankClass,
Region,
regionToRegionSubdomain,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { tankDefinitions } from '../core/blitzkit/nonBlockingPromises';
import { chunkLines } from '../core/discord/chunkLines';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
type Auction = null | {
count: number;
has_next: boolean;
results: Result[];
};
type Result =
| {
id?: number;
type: 'vehicle';
countable?: boolean;
entity?: Entity;
initial_count: number;
current_count: number;
saleable?: boolean;
available_from?: Date;
available_before?: Date;
price?: Price;
next_price: Price | null;
available: true;
display?: boolean;
next_price_datetime: Date;
next_price_timestamp: number;
}
| {
available: false;
type: 'vehicle';
};
interface Entity {
id: number;
name: string;
nation: string;
subnation: string;
use_subnation_flag: boolean;
type_slug: BlitzTankClass;
level: number;
roman_level: string;
user_string: string;
image_url: string;
preview_image_url: string;
is_premium: boolean;
is_collectible: boolean;
}
interface Price {
currency: Currency;
value: number;
}
interface Currency {
name: 'gold';
count: number;
title: 'Gold';
image_url: string;
sizes: unknown;
type: 'currency';
}
const WARNING_COUNT = 500;
export const auctionCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('auction'),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const region: Region = 'com';
const pageSize = 80;
const saleable = true;
const data = await fetch(
`https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/events/items/auction/?page_size=${
pageSize
}&type[]=vehicle&saleable=${saleable}`,
).then((response) => response.json() as Promise<Auction>);
if (data === null) {
return strings.bot.commands.auction.errors.no_auction;
}
const saleableTanks = data.results.filter((result) => result.available);
const nextPricesTimestamp = saleableTanks[0].next_price_timestamp;
const title = `# ${strings.bot.commands.auction.body.title}`;
const subtitle = `${literals(strings.bot.commands.auction.body.subtitle, [
`${saleableTanks.length} / ${data.results.length}`,
`<t:${nextPricesTimestamp}:R>`,
'<https://wotblitz.com/auction/>',
])}`;
const body = saleableTanks.map((data, index) => {
const tank = awaitedTankDefinitions.tanks[data.entity!.id];
if (!tank) return '';
const name = `[${tank.name}](<https://blitzkit.app/tanks/${
tank.slug
}>)`;
const next =
data.next_price === null
? ''
: `\n-# ${literals(strings.bot.commands.auction.body.next, [
`<:gold:1317173197082333244> ${data.next_price.value.toLocaleString(
interaction.locale,
)}`,
])}`;
const available = literals(
strings.bot.commands.auction.body.available,
[
data.current_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 `${index + 1}. ${
outString
}${name} <:gold:1317173197082333244> ${data.price!.value.toLocaleString(
interaction.locale,
)}${outString}\n-#${isLow ? ' ‼️ ' : ' '}${available}${isOut ? '' : next}`;
});
const header = `${title}\n${subtitle}`;
const lines = [header, ...body];
return chunkLines(lines, '\n\n');
},
});
});
+28 -28
View File
@@ -1,44 +1,44 @@
import { getAccountInfo, getClanInfo } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { CommandWrapper } from '../components/CommandWrapper';
import { GenericStats } from '../components/GenericStats';
import { NoData } from '../components/NoData';
import { TitleBar } from '../components/TitleBar';
import { addClanChoices } from '../core/discord/addClanChoices';
import { autocompleteClan } from '../core/discord/autocompleteClan';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import { resolveClanFromCommand } from '../core/discord/resolveClanFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { getAccountInfo, getClanInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { GenericStats } from "../components/GenericStats";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { addClanChoices } from "../core/discord/addClanChoices";
import { autocompleteClan } from "../core/discord/autocompleteClan";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolveClanFromCommand } from "../core/discord/resolveClanFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_THRESHOLD = 7;
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('inactive')
command: createLocalizedCommand("inactive")
.addStringOption(addClanChoices)
.addNumberOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName('threshold')
.setName("threshold")
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.inactive.options.threshold.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.inactive.options.threshold.description,
strings.bot.commands.inactive.options.threshold.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.inactive.options.threshold.description,
),
strings.bot.commands.inactive.options.threshold.description
)
)
.setMinValue(0);
}),
@@ -47,7 +47,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(interaction.locale);
const { region, id } = await resolveClanFromCommand(interaction);
const threshold =
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
interaction.options.getNumber("threshold")! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
const clanInfo = await getClanInfo(region, id);
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
@@ -57,7 +57,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
[
account.nickname,
(time - account.last_battle_time) / 60 / 60 / 24,
] as [string, number],
] as [string, number]
)
.filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1])
@@ -65,10 +65,10 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
([name, days]) =>
[
name,
literals(strings.bot.commands.inactive.body.listing, [
days.toFixed(0),
]),
] as [string, string],
literals(strings.bot.commands.inactive.body.listing, {
days: days.toFixed(0),
}),
] as [string, string]
);
const hasInactiveMembers = inactive.length > 0;
@@ -79,7 +79,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`${literals(
strings.bot.commands.inactive.body.subtitle,
[`${threshold}`],
{ days: threshold }
)} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
+22 -22
View File
@@ -5,31 +5,31 @@ import {
flags,
getAccountInfo,
getTankStats,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import markdownEscape from 'markdown-escape';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import {
gameDefinitions,
tankDefinitions,
} from '../core/blitzkit/nonBlockingPromises';
import { addTierChoices } from '../core/discord/addTierChoices';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { chunkLines } from '../core/discord/chunkLines';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
} from "../core/blitzkit/nonBlockingPromises";
import { addTierChoices } from "../core/discord/addTierChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { chunkLines } from "../core/discord/chunkLines";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('owned-tanks')
command: createLocalizedCommand("owned-tanks")
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale);
const tier = Number(interaction.options.getString('tier'));
const tier = Number(interaction.options.getString("tier"));
const { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
const tankStats = await getTankStats(region, id);
@@ -43,7 +43,7 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!,
id: tankData.tank_id,
})),
}))
)
).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {};
@@ -60,25 +60,25 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
const awaitedTankDefinitions = await tankDefinitions;
const awaitedGameDefinitions = await gameDefinitions;
const title = literals(strings.bot.commands.owned_tanks.body.title, [
markdownEscape(accountInfo.nickname),
TIER_ROMAN_NUMERALS[tier],
]);
const title = literals(strings.bot.commands.owned_tanks.body.title, {
name: markdownEscape(accountInfo.nickname),
tier: TIER_ROMAN_NUMERALS[tier],
});
const tankList = tankStats
.map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id])
.filter((tank) => tank.tier === tier)
.sort(
(a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class)
)
.sort(
(a, b) =>
awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation),
awaitedGameDefinitions.nations.indexOf(b.nation)
)
.map(
(tank) =>
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name))}](<https://blitzkit.app/tanks/${tank.slug}>)`,
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name))}](<https://blitzkit.app/tanks/${tank.slug}>)`
);
const lines = [title, ...tankList];
+13 -13
View File
@@ -1,11 +1,11 @@
import { literals } from '@blitzkit/i18n';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('permissions'),
command: createLocalizedCommand("permissions"),
async handler(interaction) {
const { strings } = translator(interaction.locale);
@@ -14,33 +14,33 @@ export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [
[
interaction.appPermissions?.has('ViewChannel'),
interaction.appPermissions?.has("ViewChannel"),
strings.bot.commands.permissions.body.view_channels,
],
[
interaction.appPermissions?.has('ReadMessageHistory'),
interaction.appPermissions?.has("ReadMessageHistory"),
strings.bot.commands.permissions.body.read_message_history,
],
[
interaction.appPermissions?.has('AttachFiles'),
interaction.appPermissions?.has("AttachFiles"),
strings.bot.commands.permissions.body.attach_files,
],
[
interaction.appPermissions?.has('UseExternalEmojis'),
interaction.appPermissions?.has("UseExternalEmojis"),
strings.bot.commands.permissions.body.external_emojis,
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
return `${isFridayThe13th ? "😇" : "✅"} ${description}`;
} else {
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
return `${isFridayThe13th ? "💀" : "❌"} ${description}`;
}
})
.join('\n');
.join("\n");
return literals(strings.bot.commands.permissions.body.title, [body]);
return literals(strings.bot.commands.permissions.body.title, { body });
},
});
});
+12 -12
View File
@@ -1,15 +1,15 @@
import { literals } from '@blitzkit/i18n';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const pingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
handlesInteraction: true,
command: createLocalizedCommand('ping', [
{ subcommand: 'blitzkit' },
{ subcommand: 'wotb' },
command: createLocalizedCommand("ping", [
{ subcommand: "blitzkit" },
{ subcommand: "wotb" },
]),
async handler(interaction) {
@@ -17,17 +17,17 @@ export const pingCommand = new Promise<CommandRegistry>((resolve) => {
const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
if (subcommand === 'blitzkit') {
if (subcommand === "blitzkit") {
await interaction.editReply(strings.bot.commands.ping.body.pong);
} else {
await fetch('https://api.wotblitz.com/');
await fetch("https://api.wotblitz.com/");
}
const executionTime = Date.now() - executionStart;
interaction.editReply(
literals(strings.bot.commands.ping.body.pong_with_time, [
`${Math.round(executionTime)}`,
]),
literals(strings.bot.commands.ping.body.pong_with_time, {
time: executionTime.toFixed(0),
})
);
},
});
+18 -18
View File
@@ -1,19 +1,19 @@
import { getAccountInfo } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import markdownEscape from 'markdown-escape';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { markdownTable } from '../core/discord/markdownTable';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { getAccountInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { markdownTable } from "../core/discord/markdownTable";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command:
createLocalizedCommand('player-info').addStringOption(addUsernameChoices),
createLocalizedCommand("player-info").addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings } = translator(interaction.locale);
@@ -22,9 +22,9 @@ export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
const accountInfo = await getAccountInfo(region, id);
return embedInfo(
literals(strings.bot.commands.player_info.body.title, [
markdownEscape(accountInfo.nickname),
]),
literals(strings.bot.commands.player_info.body.title, {
name: markdownEscape(accountInfo.nickname),
}),
markdownTable([
[
@@ -51,16 +51,16 @@ export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
[
strings.bot.commands.player_info.body.created,
new Date(accountInfo.created_at * 1000).toLocaleDateString(
interaction.locale,
interaction.locale
),
],
[
strings.bot.commands.player_info.body.last_battle,
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
interaction.locale,
interaction.locale
),
],
]),
])
);
},
+118 -118
View File
@@ -8,42 +8,42 @@ import {
getRatingInfo,
getRatingNeighbors,
imgur,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { SlashCommandSubcommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
import { CommandWrapper } from '../components/CommandWrapper';
import { DeltaCaret } from '../components/DeltaCaret';
import { TitleBar } from '../components/TitleBar';
import { normalizeLeagueIcon } from '../core/blitz/normalizeLeagueIcon';
import { iconPng } from '../core/blitzkit/iconPng';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { SlashCommandSubcommandBuilder } from "discord.js";
import markdownEscape from "markdown-escape";
import { Glow } from "../components/AllStatsOverview/components/HeroStat/components/Glow";
import { CommandWrapper } from "../components/CommandWrapper";
import { DeltaCaret } from "../components/DeltaCaret";
import { TitleBar } from "../components/TitleBar";
import { normalizeLeagueIcon } from "../core/blitz/normalizeLeagueIcon";
import { iconPng } from "../core/blitzkit/iconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
const LEAGUE_ACCENT = [
'_purple',
'_sand',
'_amber',
'_mauve',
'_brown',
"_purple",
"_sand",
"_amber",
"_mauve",
"_brown",
] as const;
export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('rating', [
command: createLocalizedCommand("rating", [
{
subcommand: 'today',
subcommand: "today",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
},
{
subcommand: 'season',
subcommand: "season",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
@@ -52,11 +52,11 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
async handler(interaction) {
const subcommand = interaction.options.getSubcommand(true) as
| 'season'
| 'today';
| "season"
| "today";
const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const ratingInfo = await getRatingInfo(region);
@@ -67,20 +67,20 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const leaderboard = await getArchivedRatingLeaderboard(
region,
ratingInfo.current_season - (subcommand === 'today' ? 0 : 1),
ratingInfo.current_season - (subcommand === "today" ? 0 : 1)
);
if (leaderboard.version!.$case === 'v1') {
if (leaderboard.version!.$case === "v1") {
throw new Error(
'Encountered bkrl minimal format in latest season. Wait till next cron?',
"Encountered bkrl minimal format in latest season. Wait till next cron?"
);
}
const accountInfo = await getAccountInfo(region, id, [
'statistics.rating',
"statistics.rating",
]);
const statsIndex = leaderboard.version!.value.entries.findIndex(
(entry) => entry.id === id,
(entry) => entry.id === id
);
const statsA = leaderboard
? leaderboard.version!.value.entries[statsIndex!]
@@ -90,9 +90,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors;
if (statsB2Array === undefined) {
return literals(strings.bot.commands.rating.errors.no_participation, [
markdownEscape(accountInfo.nickname),
]);
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const [statsB2] = statsB2Array;
@@ -102,15 +102,15 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
return strings.bot.commands.rating.errors.unrecorded_stats;
}
if (!statsB1) {
return literals(strings.bot.commands.rating.errors.no_participation, [
markdownEscape(accountInfo.nickname),
]);
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const reward = ratingInfo.rewards.find(
(reward) =>
statsB2.number >= reward.from_position &&
statsB2.number <= reward.to_position,
statsB2.number <= reward.to_position
);
const positionDelta = leaderboard
? statsB2.number - positionA!
@@ -118,11 +118,11 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const delta = deltaBkrlBlitzStats(
statsA,
accountInfo.statistics.rating!,
statsB2,
statsB2
);
const leagueTheme = LEAGUE_ACCENT[league.index];
let rewardImage = reward
? reward.type === 'vehicle'
? reward.type === "vehicle"
? reward.vehicle.preview_image_url
: reward.stuff.image_url
: undefined;
@@ -131,7 +131,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const { ok } = await fetch(rewardImage);
if (!ok) {
rewardImage = imgur('kP88vqr');
rewardImage = imgur("kP88vqr");
}
}
@@ -147,24 +147,24 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
position: 'relative',
display: 'flex',
overflow: 'hidden',
position: "relative",
display: "flex",
overflow: "hidden",
}}
>
<div
style={{
position: 'relative',
position: "relative",
padding: 16,
backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
borderRadius: 4,
border: `1px solid ${
theme.colors[`componentInteractive${leagueTheme}`]
}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
{reward === undefined && (
@@ -172,17 +172,17 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: 'translateX(-16px) rotate(90deg)',
transform: "translateX(-16px) rotate(90deg)",
}}
/>
)}
<div
style={{
display: 'flex',
display: "flex",
gap: 8,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
}}
>
<img
@@ -190,24 +190,24 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
style={{
width: 64,
height: 64,
objectFit: 'contain',
objectFit: "contain",
}}
src={normalizeLeagueIcon(
ratingInfo.leagues[league.index].big_icon,
ratingInfo.leagues[league.index].big_icon
)}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
@@ -232,7 +232,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
{Math.abs(delta.score).toLocaleString(
interaction.locale,
interaction.locale
)}
</span>
</>
@@ -241,9 +241,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
@@ -267,7 +267,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
{Math.abs(positionDelta).toLocaleString(
interaction.locale,
interaction.locale
)}
</span>
</>
@@ -279,23 +279,23 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
{reward && (
<div
style={{
display: 'flex',
display: "flex",
gap: 8,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
@@ -303,13 +303,13 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 16,
maxWidth: 136,
textAlign: 'right',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
textAlign: "right",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{reward.type === 'vehicle'
{reward.type === "vehicle"
? reward.vehicle.user_string
: reward.stuff.title}
</span>
@@ -317,9 +317,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
@@ -329,7 +329,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
Reward
{reward.type === 'stuff' ? ` x${reward.count}` : ''}
{reward.type === "stuff" ? ` x${reward.count}` : ""}
</span>
</div>
</div>
@@ -338,7 +338,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
style={{
width: 64,
height: 64,
objectFit: 'cover',
objectFit: "cover",
}}
src={await iconPng(rewardImage!)}
/>
@@ -350,7 +350,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: 'translateX(16px) rotate(-90deg)',
transform: "translateX(16px) rotate(-90deg)",
}}
/>
)}
@@ -362,10 +362,10 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
direction="reverse"
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: 'absolute',
position: "absolute",
top: -40,
left: '50%',
transform: 'translateX(-50%)',
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
@@ -373,32 +373,32 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<Glow
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: 'absolute',
position: "absolute",
bottom: -40,
left: '50%',
transform: 'translateX(-50%)',
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
</div>
{delta.battles > 0 && (
<div style={{ padding: '8px 0', display: 'flex' }}>
<div style={{ padding: "8px 0", display: "flex" }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{delta.battles.toLocaleString(interaction.locale)}
@@ -414,19 +414,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{(100 * (delta.wins / delta.battles)).toFixed(0)}%
@@ -442,23 +442,23 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{Math.round(delta.damage / delta.battles).toLocaleString(
interaction.locale,
interaction.locale
)}
</span>
<span
@@ -472,19 +472,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{(100 * (delta.survived / delta.battles)).toFixed(0)}%
@@ -500,19 +500,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{(delta.kills / delta.battles).toFixed(2)}
+79 -80
View File
@@ -13,27 +13,27 @@ import {
RatingInfo,
RatingLeaderboard,
Region,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import {
APIApplicationCommandOptionChoice,
Locale,
SlashCommandSubcommandBuilder,
} from 'discord.js';
import { range } from 'lodash-es';
import markdownEscape from 'markdown-escape';
import { CommandWrapper } from '../components/CommandWrapper';
import * as Leaderboard from '../components/Leaderboard';
import { TitleBar } from '../components/TitleBar';
import { UserError } from '../core/blitzkit/userError';
import { addRegionChoices } from '../core/discord/addRegionChoices';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
} from "discord.js";
import { range } from "lodash-es";
import markdownEscape from "markdown-escape";
import { CommandWrapper } from "../components/CommandWrapper";
import * as Leaderboard from "../components/Leaderboard";
import { TitleBar } from "../components/TitleBar";
import { UserError } from "../core/blitzkit/userError";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
interface SimplifiedPlayer {
id: number;
@@ -51,7 +51,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
const onGoingSeason = await isOnGoingRatingSeason();
const seasonNumbers = range(
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1
);
function addOptions(option: SlashCommandSubcommandBuilder) {
@@ -66,51 +66,50 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit.name,
undefined,
true,
),
true
)
)
.setDescription(
literals(
strings.bot.commands.rating_leaderboard.options.limit
.description,
[`${DEFAULT_LIMIT}`],
),
{ count: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit
.description,
[`${DEFAULT_LIMIT}`],
),
{ count: DEFAULT_LIMIT }
)
)
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
.setMaxValue(30)
)
.addStringOption((option) =>
option
.setName(
strings.bot.commands.rating_leaderboard.options.season.name,
strings.bot.commands.rating_leaderboard.options.season.name
)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.season
.description,
strings.bot.commands.rating_leaderboard.options.season.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season
.description,
),
.description
)
)
.setRequired(false)
.addChoices(
@@ -124,7 +123,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? literals(
strings.bot.commands.rating_leaderboard.options.season
.choices.current,
[`${number}`],
{ season: number }
)
: `${number}`,
value: `${number}`,
@@ -133,27 +132,27 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) =>
strings.bot.commands.rating_leaderboard.options
.season.choices.current,
[`${number}`],
{ season: number }
)
: undefined,
} satisfies APIApplicationCommandOptionChoice<string>;
})
.reverse(),
),
.reverse()
)
);
}
resolve({
command: createLocalizedCommand('rating-leaderboard', [
command: createLocalizedCommand("rating-leaderboard", [
{
subcommand: 'neighbors',
subcommand: "neighbors",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
addOptions(option);
},
},
{
subcommand: 'league',
subcommand: "league",
modify(option: SlashCommandSubcommandBuilder) {
option
.addStringOption((option) => {
@@ -161,7 +160,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
return option
.setName(
strings.bot.commands.rating_leaderboard.options.league.name,
strings.bot.commands.rating_leaderboard.options.league.name
)
.setNameLocalizations(
localizationObject(
@@ -169,19 +168,19 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
strings.bot.commands.rating_leaderboard.options.league
.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.league
.description,
.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.league
.description,
),
.description
)
)
.addChoices(
...LEAGUES.map(
@@ -195,10 +194,10 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) =>
(
strings.common.leagues as Record<string, string>
)[name],
)[name]
),
}) satisfies APIApplicationCommandOptionChoice<string>,
),
}) satisfies APIApplicationCommandOptionChoice<string>
)
)
.setRequired(true);
})
@@ -213,10 +212,10 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
async handler(interaction) {
const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| 'neighbors';
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString('season');
| "league"
| "neighbors";
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString("season");
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
@@ -232,11 +231,11 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
let playerId: number | undefined;
let regionRatingInfo: RatingInfo | null;
if (subcommand === 'league') {
if (subcommand === "league") {
const leagueIndex = parseInt(
interaction.options.getString('league')!,
interaction.options.getString("league")!
);
const region = interaction.options.getString('region') as Region;
const region = interaction.options.getString("region") as Region;
regionRatingInfo = await (isCurrentSeason
? getRatingInfo(region)
: null);
@@ -259,8 +258,8 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
}) satisfies SimplifiedPlayer
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
@@ -269,17 +268,17 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? 0
: players.version!.value.entries.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
player.score < LEAGUES[leagueIndex - 1].minScore
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.version!.value.entries.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
lastPlayerIndex + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
['clan'],
["clan"]
);
return trimmed.map(
@@ -294,16 +293,16 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
literals(
strings.bot.commands.rating_leaderboard.body
.deleted_player,
[`${player.id}`],
{ id: player.id }
),
}) satisfies SimplifiedPlayer,
}) satisfies SimplifiedPlayer
);
},
}
);
const leagueInfo = LEAGUES[leagueIndex];
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season,
season
);
players = result;
@@ -316,12 +315,12 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? (strings.common.leagues as Record<string, string>)[
LEAGUES[leagueInfo.index].name
]
: ''
: ""
} - ${strings.common.regions.short[region]}`;
titleImage = imgur(leagueInfo.icon);
titleDescription = literals(
strings.bot.commands.rating_leaderboard.body.top_players,
[`${limit}`],
{ count: limit }
);
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
@@ -338,7 +337,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
}
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const neighbors = isCurrentSeason
? await getRatingNeighbors(region, id, Math.round(limit / 2)).then(
(players) =>
@@ -350,13 +349,13 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
}) satisfies SimplifiedPlayer
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.version!.value.entries.findIndex(
(player) => player.id === id,
(player) => player.id === id
);
if (playerIndex === -1) {
@@ -364,20 +363,20 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
literals(
strings.bot.commands.rating_leaderboard.body
.no_participation,
[markdownEscape(accountInfo.nickname), `${season}`],
),
{ player: markdownEscape(accountInfo.nickname), season }
)
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.version!.value.entries.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
playerIndex + halfRange + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
['clan'],
["clan"]
);
return trimmed.map((player, index) => ({
@@ -389,18 +388,18 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
},
}
);
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season,
season
);
players = neighbors;
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
titleName = neighbors ? accountInfo.nickname : '';
titleName = neighbors ? accountInfo.nickname : "";
titleDescription =
strings.bot.commands.rating_leaderboard.body.neighbors;
playersAfter =
@@ -414,7 +413,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
const items = players.map((player) => {
const midnightIndex =
archivedLeaderboard?.version!.value.entries.findIndex(
(item) => item.id === player.id,
(item) => item.id === player.id
);
const midnightScore =
midnightIndex === undefined
@@ -453,7 +452,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
<Leaderboard.Gap
message={literals(
strings.bot.commands.rating_leaderboard.body.more,
[`${playersAfter.toLocaleString(interaction.locale)}`],
{ count: playersAfter.toLocaleString(interaction.locale) }
)}
/>
)}
@@ -465,5 +464,5 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
autocomplete: autocompleteUsername,
});
},
}
);
@@ -1,15 +1,15 @@
import { BlitzKitStrings, literals as literalsFunc } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { SUPPORTED_LOCALES_DISCORD } from '../localization/strings/constants';
import { translator } from '../localization/translator';
import { BlitzKitStrings, literals as literalsFunc } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { SUPPORTED_LOCALES_DISCORD } from "../localization/strings/constants";
import { translator } from "../localization/translator";
const validNameRegex =
/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u;
export function localizationObject(
path: (strings: BlitzKitStrings) => string,
literals: string[] = [],
strict = false,
literals?: Record<string, string | number>,
strict = false
) {
return SUPPORTED_LOCALES_DISCORD.reduce<Partial<Record<Locale, string>>>(
(localizations, locale) => {
@@ -20,7 +20,7 @@ export function localizationObject(
if (!isValid) {
console.warn(
`Invalid localization for ${locale} (${path}): ${translation}; skipping...`,
`Invalid localization for ${locale} (${path}): ${translation}; skipping...`
);
return localizations;
}
@@ -30,6 +30,6 @@ export function localizationObject(
[locale]: translation,
};
},
{},
{}
);
}
+9 -4
View File
@@ -1,5 +1,10 @@
export function literals(string: string, literals: string[]) {
return literals.reduce((accumulator, literal, index) => {
return accumulator.replace(`%s${index + 1}`, literal);
}, string);
export function literals(
string: string,
literals?: Record<string, string | number>
) {
for (const key in literals) {
string = string.replaceAll(`{${key}}`, `${literals[key]}`);
}
return string;
}
+75 -94
View File
@@ -1,7 +1,7 @@
{
"test": {
"default": "test",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is %s1 because it has the color of %s2. I like it so much that I will say its name 3 times! %s1! %s1! %s1! Once again, it's %s2, not %s3.",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"fox": "The quick brown fox jumps over the lazy dog."
},
@@ -9,13 +9,13 @@
"name": "BlitzKit",
"units": {
"m": "%s1m",
"mm": "%s1mm",
"deg": "%s1°",
"hp": "%s1hp",
"x": "x%s1",
"percentage": "%s1%",
"deg_s": "%s1°/s"
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value}°",
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
},
"gun_types": {
@@ -206,11 +206,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"%s1\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"%s1\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"player_not_linked": "# Use `/link` to get started\nI don't know who you are but you can tell me today!",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"%s1\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"%s1\". Try typing the name of the tank instead of a number.",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". 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.",
"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.",
@@ -315,7 +315,7 @@
"description": "Tier",
"choices": {
"tier": "Tier %s1 - %s2"
"tier": "Tier {tier_1} - {tier_2}"
}
}
},
@@ -323,13 +323,13 @@
"periods": {
"career": "Career",
"today": "Today",
"days": "%s1 days",
"custom": "%s1 - %s2 days"
"days": "{days} days",
"custom": "{day_1} - {day_2} days"
},
"filters": {
"none": "No filters",
"tier": "Tier %s1"
"tier": "Tier {tier}"
},
"no_data": {
@@ -356,24 +356,6 @@
},
"commands": {
"auction": {
"name": "auction",
"description": "Tanks available right now in the auction",
"body": {
"title": "Blitz Auction",
"subtitle": "%s1 available • next prices %s2\nBid now: %s3",
"next": "Next price: %s1",
"available": "Available: %s1 / %s2"
},
"errors": {
"no_auction": "# No ongoing auction\n\nCurrently, there are no tanks available for purchase in the auction. If you think this is an error, please report this discrepancy in [the official Discord Server](https://discord.gg/nDt7AjGJQH)."
},
"actions": {
"bid": "Bid now"
}
},
"replay": {
"name": "replay",
"description": "Display replay results"
@@ -397,20 +379,20 @@
"body": {
"subtitle": "Research",
"total": "Total",
"research": "Research: %s1",
"upgrades": "Upgrades: %s1",
"purchase": "Purchase: %s1",
"equipment": "Equipment: %s1",
"research": "Research: {cost}",
"upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}",
"equipment": "Equipment: {cost}",
"estimation": "# This is an estimation!\n\nThis list may include already researched tanks because of the inaccurate information Wargaming provides. Try providing a `starting-tank` from the line when using the command if you can."
},
"errors": {
"non_tech_tree": "# %s1 is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# %s1 is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# %s1 is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# %s1 was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# %s1 and %s2 are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank."
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank."
}
},
@@ -456,7 +438,7 @@
"wn8": "WN8",
"winrate": "Winrate",
"damage": "Damage",
"unknown_tank": "Unknown tank %s1"
"unknown_tank": "Unknown tank {id}"
}
},
@@ -499,13 +481,13 @@
"options": {
"threshold": {
"name": "threshold",
"description": "The number of days inactive (default: %s1 days)"
"description": "The number of days inactive (default: {days} days)"
}
},
"body": {
"subtitle": "Inactive for %s1+ days",
"listing": "%s1 days"
"subtitle": "Inactive for {days}+ days",
"listing": "{days} days"
}
},
@@ -552,7 +534,7 @@
"body": {
"accounts_linked": "Accounts linked",
"no_role_permissions": {
"title": "%s1 failed to verify",
"title": "{name} failed to verify",
"description": "I don't have the permission to manage roles."
}
},
@@ -570,12 +552,12 @@
"options": {
"limit": {
"name": "limit",
"description": "The max number of clan results (default: %s1)"
"description": "The max number of clan results (default: {limit})"
}
},
"body": {
"title": "Clan search for %s1 in region %s2",
"title": "Clan search for {search} in region {region}",
"no_results": "No clans found"
}
},
@@ -587,12 +569,12 @@
"options": {
"limit": {
"name": "limit",
"description": "The max number of player results (default: %s1)"
"description": "The max number of player results (default: {limit})"
}
},
"body": {
"title": "Player search for %s1 in region %s2",
"title": "Player search for {search} in region {region}",
"no_results": "No players found"
}
},
@@ -616,7 +598,7 @@
"options": {
"limit": {
"name": "limit",
"description": "How many neighbors to display (default: %s1)"
"description": "How many neighbors to display (default: {count})"
},
"season": {
@@ -624,7 +606,7 @@
"description": "Season number (default: current)",
"choices": {
"current": "%s1 (current)"
"current": "{season} (current)"
}
},
@@ -635,12 +617,12 @@
},
"body": {
"top_players": "Top %s1 players",
"no_participation": "# %s1 didn't play rating in season %s2\n\nThis player did not participate in this season or did not get past calibration.",
"top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
"deleted_player": "Deleted %s1",
"deleted_player": "Deleted {id}",
"neighbors": "Rating neighbors",
"more": "+ %s1 more"
"more": "+ {count} more"
}
},
@@ -659,7 +641,7 @@
"description": "Shows all own tanks in a tier",
"body": {
"title": "# %s1's owned tier %s2 tanks\n\n"
"title": "# {name}'s owned tier {tier} tanks\n\n"
}
},
@@ -668,7 +650,7 @@
"description": "Checks wether the bot has the required permissions to function",
"body": {
"title": "# Permissions\n\n%s1",
"title": "# Permissions\n\n{body}",
"view_channels": "View channels: needed to find the message the refresh button was used on",
"read_message_history": "Read message history: needed to find the message to refresh",
"attach_files": "Attach files: needed to add images to image-based commands",
@@ -682,7 +664,7 @@
"body": {
"pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms"
"pong_with_time": "Pong 🏓 - {time}ms"
},
"subcommands": {
@@ -702,7 +684,7 @@
"description": "Basic information about a player",
"body": {
"title": "%s1's information",
"title": "{name}'s information",
"nickname": "Nickname",
"battles": "Battles",
"winrate": "Winrate",
@@ -735,7 +717,7 @@
},
"errors": {
"no_participation": "# %s1 hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"unrecorded_stats": "# Statistics currently unavailable\n\nWargaming may be currently be facing server downtime. If there is an on-going season, statistics should be available withing 24 hours."
}
}
@@ -772,8 +754,8 @@
"recent": "Recently viewed",
"select_all": "Select all",
"count_singular": "1 tank",
"count_plural": "%s1 tanks",
"sorting_by": "Sorting by %s1",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"no_tanks_found": {
"body": "No tanks found.",
"search_again": "Try searching again.",
@@ -853,16 +835,16 @@
},
"hero": {
"credit": "By %s1 ❤️"
"credit": "By {name} ❤️"
},
"hot_tanks": {
"title": "Popular tanks",
"subtitle": "Last %s1 days"
"subtitle": "Last {days} days"
},
"other_projects": {
"title": "Other cool projects"
},
"help_translate": "Please help translate %s1 to %s2!",
"help_translate": "Please help translate {name} to {language_2}!",
"seo_description": "Tools for everything World of Tanks Blitz"
},
@@ -895,7 +877,7 @@
"next": "Next",
"no_results": "No results"
},
"stats": "%s1 / %s2 guessed correctly"
"stats": "{correct} / {total} guessed correctly"
},
"tankopedia": {
@@ -905,8 +887,8 @@
"button": "Find tanks",
"description": "Blitz tank encyclopedia",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"visualizers": {
"reload": {
@@ -936,7 +918,6 @@
"view_range": {
"camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
@@ -999,15 +980,15 @@
"track": "Tracks"
},
"status": {
"penetration": "%s1hp penetration",
"penetration": "{damage}hp penetration",
"blocked": "Blocked",
"ricochet": "Ricochet",
"splash": "%s1hp splash"
"splash": "{damage}hp splash"
},
"stats": {
"thickness_and_angle": "%s1mm %s2°",
"nominal": "%s1mm nominal",
"ricochet_loss": "%s1% penetration"
"thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetration"
}
}
},
@@ -1047,8 +1028,8 @@
"tech_tree": {
"title": "Tech tree",
"battle": "1 battle",
"battles": "%s1 battles",
"route": "Route %s1 of %s2"
"battles": "{count} battles",
"route": "Route {index} of {count}"
},
"game_modes": {
@@ -1057,7 +1038,7 @@
"review": {
"title": "Review videos",
"join": "Apply to %s1 YouTuber program"
"join": "Apply to {name} YouTuber program"
},
"configuration": {
@@ -1096,15 +1077,15 @@
"title": "Characteristics",
"compare": {
"label": "Compare",
"class": "Tier %s1 %s2",
"tier": "All of tier %s1",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"all": "All tanks"
},
"values": {
"shell_index": "Shell %s1",
"at_distance": "At %s1m",
"shell_index": "Shell {index}",
"at_distance": "At {distance}m",
"at_distance_slider_label": "Distance",
"dispersion_at_distance": "Dispersion at %s1m",
"dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "Gun flexibility",
"speed": "Speed",
"terrain_coefficients": "Terrain coefficients",
@@ -1260,9 +1241,9 @@
"description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": {
"body": "Estimated stats based on %s1 players. Updated %s2 ago.",
"hours": "%s1 hours",
"minutes": "%s1 minutes"
"body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "{count} hours",
"minutes": "{count} minutes"
},
"player_count": {
"label": "Player count"
@@ -1294,10 +1275,10 @@
"button": "Browse all",
"description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on %s1",
"last_updated": "Last updated on {time}",
"search": {
"hint": "Search avatars...",
"results": "%s1 avatars"
"results": "{count} avatars"
}
},
"session": {
@@ -1312,8 +1293,8 @@
"no_results": "No players found"
},
"controls": {
"title": "Tracking %s1",
"subtitle": "Since %s1",
"title": "Tracking {count}",
"subtitle": "Since {time}",
"reset": "Reset",
"change": "Change"
},
@@ -1376,7 +1357,7 @@
"cancel": "Cancel",
"success": {
"title": "Copy URL",
"info": "Generated the URL for %s1. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"back": "Back",
"copy": "Copy URL"
}
@@ -47,6 +47,7 @@ export function Model() {
const pointer = useRef(new Vector2());
const delta = useRef(new Vector2());
const enablePitchRotation = useRef(true);
const handlePointerDown = useCallback((event: ThreeEvent<PointerEvent>) => {
event.stopPropagation();
@@ -65,8 +66,10 @@ export function Model() {
modelTransformEvent.dispatch({
pitch:
modelTransformEvent.last!.pitch +
delta.current.y * (Math.PI / bounds.height),
modelTransformEvent.last!.pitch -
(enablePitchRotation.current
? delta.current.y * (Math.PI / bounds.height)
: 0),
yaw:
modelTransformEvent.last!.yaw +
delta.current.x * (Math.PI / bounds.width),
@@ -84,6 +87,7 @@ export function Model() {
event: QuicklimeEvent<ModelTransformEventData>
) {
turretGroup.current.rotation.z = event.data.yaw;
gunGroup.current.rotation.x = event.data.pitch;
}
modelTransformEvent.on(handleModelTransform);
@@ -110,19 +114,14 @@ export function Model() {
});
})}
<mesh>
<icosahedronGeometry args={[0.1]} />
<meshNormalMaterial depthTest={false} depthWrite={false} />
</mesh>
<group
ref={turretGroup}
rotation={[0, 0, modelTransformEvent.last!.yaw]}
position={[
tankModel.turret_origin.x + trackModel.origin.x,
tankModel.turret_origin.z + trackModel.origin.z,
tankModel.turret_origin.y + trackModel.origin.y,
]}
ref={turretGroup}
rotation={[0, 0, modelTransformEvent.last!.yaw]}
>
<group
position={[
@@ -147,7 +146,10 @@ export function Model() {
key={key}
castShadow
receiveShadow
onPointerDown={handlePointerDown}
onPointerDown={(event) => {
enablePitchRotation.current = false;
handlePointerDown(event);
}}
/>
);
},
@@ -155,44 +157,62 @@ export function Model() {
})}
<group
ref={gunGroup}
rotation={[modelTransformEvent.last!.pitch, 0, 0]}
position={[
turretTankModel.turret_origin.x +
turretTrackModel.origin.x +
turretModel.gun_origin.x -
gunTankModel.turret_origin.x -
gunTrackModel.origin.x -
gunTurretModel.gun_origin.x,
turretModel.gun_origin.x,
turretTankModel.turret_origin.z +
turretTrackModel.origin.z +
turretModel.gun_origin.z -
gunTankModel.turret_origin.z -
gunTrackModel.origin.z -
gunTurretModel.gun_origin.z,
turretModel.gun_origin.z,
turretTankModel.turret_origin.y +
turretTrackModel.origin.y +
turretModel.gun_origin.y -
gunTankModel.turret_origin.y -
gunTrackModel.origin.y -
gunTurretModel.gun_origin.y,
turretModel.gun_origin.y,
]}
>
{gunNodes.map((node) => {
const isCurrentMantlet =
node.name ===
`gun_${gunModel.model_id.toString().padStart(2, "0")}_mask`;
const isCurrentGun =
node.name ===
`gun_${gunModel.model_id.toString().padStart(2, "0")}`;
const isVisible = isCurrentGun || isCurrentMantlet;
<group
position={[
-gunTankModel.turret_origin.x -
gunTrackModel.origin.x -
gunTurretModel.gun_origin.x,
-gunTankModel.turret_origin.z -
gunTrackModel.origin.z -
gunTurretModel.gun_origin.z,
-gunTankModel.turret_origin.y -
gunTrackModel.origin.y -
gunTurretModel.gun_origin.y,
]}
>
{gunNodes.map((node) => {
const isCurrentMantlet =
node.name ===
`gun_${gunModel.model_id.toString().padStart(2, "0")}_mask`;
const isCurrentGun =
node.name ===
`gun_${gunModel.model_id.toString().padStart(2, "0")}`;
const isVisible = isCurrentGun || isCurrentMantlet;
if (!isVisible) return null;
if (!isVisible) return null;
return jsxTree(node, {
mesh(_, props, key) {
return <mesh {...props} key={key} castShadow receiveShadow />;
},
});
})}
return jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
castShadow
receiveShadow
onPointerDown={(event) => {
enablePitchRotation.current = true;
handlePointerDown(event);
}}
/>
);
},
});
})}
</group>
</group>
</group>
</group>
@@ -57,7 +57,6 @@ const title = literals(strings.website.tools.tankopedia.tank_seo_title, [
],
strings.common.tank_class_short[tank.class],
"BlitzKit",
strings.common.nations[tank.nation as keyof typeof strings.common.nations],
]);
const url = import.meta.env.SITE + Astro.url.pathname;