feat: migrate all

This commit is contained in:
TrèsAbhi committed 2025-10-02 10:15:38 -05:00
1 parent 5594c0ec3b
commit 320a764d45
38 files changed
+632 -600

No files matched your search

@@ -193,9 +193,9 @@ export async function renderBreakdown(
title={
tankDefinition
? unwrap(tankDefinition.name)
: literals(strings.bot.commands.breakdown.body.unknown_tank, [
`${id}`,
])
: literals(strings.bot.commands.breakdown.body.unknown_tank, {
id,
})
}
stats={[
{
+136 -137
View File
@@ -8,31 +8,31 @@ import {
TANK_ICONS,
TankType,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { escapeMarkdown, Locale } from 'discord.js';
import { CommandWrapper } from '../components/CommandWrapper';
import { TitleBar } from '../components/TitleBar';
import { resolveTankId } from '../core/blitz/resolveTankId';
import { buildTechTreeLine } from '../core/blitzkit/buildTechTreeLine';
import { iconPng } from '../core/blitzkit/iconPng';
import { tankDefinitions } from '../core/blitzkit/nonBlockingPromises';
import { resolveAncestry } from '../core/blitzkit/resolveAncestry';
import { tankIconPng } from '../core/blitzkit/tankIconPng';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteTanks } from '../core/discord/autocompleteTanks';
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';
import { theme } from '../stitches.config';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { escapeMarkdown, Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { resolveTankId } from "../core/blitz/resolveTankId";
import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine";
import { iconPng } from "../core/blitzkit/iconPng";
import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { resolveAncestry } from "../core/blitzkit/resolveAncestry";
import { tankIconPng } from "../core/blitzkit/tankIconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
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";
import { theme } from "../stitches.config";
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand('research')
command: createLocalizedCommand("research")
.addStringOption((option) =>
option
.setName(strings.bot.commands.research.options.target_tank.name)
@@ -41,20 +41,20 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
(strings) =>
strings.bot.commands.research.options.target_tank.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.research.options.target_tank.description,
strings.bot.commands.research.options.target_tank.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.target_tank.description,
),
strings.bot.commands.research.options.target_tank.description
)
)
.setAutocomplete(true)
.setRequired(true),
.setRequired(true)
)
.addStringOption((option) =>
option
@@ -64,20 +64,20 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
(strings) =>
strings.bot.commands.research.options.starting_tank.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.research.options.starting_tank.description,
strings.bot.commands.research.options.starting_tank.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.starting_tank.description,
),
strings.bot.commands.research.options.starting_tank.description
)
)
.setAutocomplete(true)
.setRequired(false),
.setRequired(false)
)
.addStringOption(addUsernameChoices),
@@ -85,11 +85,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings, unwrap } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const targetTankId = await resolveTankId(
interaction.options.getString('target-tank', true),
interaction.options.getString("target-tank", true),
interaction.locale,
true,
true
);
const startingTankRaw = interaction.options.getString('starting-tank');
const startingTankRaw = interaction.options.getString("starting-tank");
const targetTank = awaitedTankDefinitions.tanks[targetTankId];
const { id, region } = await resolvePlayerFromCommand(interaction);
let startingTankId: number;
@@ -99,46 +99,45 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
startingTankId = await resolveTankId(
startingTankRaw,
interaction.locale,
true,
true
);
if (targetTankId === startingTankId) {
return literals(
strings.bot.commands.research.errors.start_end_equal,
[escapeMarkdown(unwrap(targetTank.name))],
{ tank: escapeMarkdown(unwrap(targetTank.name)) }
);
}
const startingTank = awaitedTankDefinitions.tanks[startingTankId];
if (startingTank.tier > targetTank.tier) {
return literals(strings.bot.commands.research.errors.unordered, [
escapeMarkdown(unwrap(startingTank.name)),
escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(targetTank.name)),
]);
return literals(strings.bot.commands.research.errors.unordered, {
tank_1: escapeMarkdown(unwrap(startingTank.name)),
tank_2: escapeMarkdown(unwrap(targetTank.name)),
});
}
if (!targetTankAncestry.includes(startingTankId)) {
return literals(
strings.bot.commands.research.errors.tanks_not_on_line,
[
escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(startingTank.name)),
],
{
tank_1: escapeMarkdown(unwrap(targetTank.name)),
tank_2: escapeMarkdown(unwrap(startingTank.name)),
}
);
}
} else {
if (targetTank.type !== TankType.RESEARCHABLE) {
return literals(strings.bot.commands.research.errors.non_tech_tree, [
escapeMarkdown(unwrap(targetTank.name)),
]);
return literals(strings.bot.commands.research.errors.non_tech_tree, {
tank: escapeMarkdown(unwrap(targetTank.name)),
});
}
if (targetTank.tier === 1) {
return literals(strings.bot.commands.research.errors.tier_1, [
escapeMarkdown(unwrap(targetTank.name)),
]);
return literals(strings.bot.commands.research.errors.tier_1, {
tank: escapeMarkdown(unwrap(targetTank.name)),
});
}
const tankStats = await getTankStats(region, id);
@@ -150,18 +149,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return literals(
strings.bot.commands.research.errors.already_researched,
[escapeMarkdown(unwrap(targetTank.name))],
{ tank: escapeMarkdown(unwrap(targetTank.name)) }
);
}
const foundAncestor = targetTankAncestry.find(
(id) =>
awaitedTankDefinitions.tanks[id].tier === 1 ||
tankStats.some(({ tank_id }) => tank_id === id),
tankStats.some(({ tank_id }) => tank_id === id)
);
if (foundAncestor === undefined) {
throw new Error('No first owned tech tree found.');
throw new Error("No first owned tech tree found.");
}
startingTankId = foundAncestor;
@@ -169,12 +168,12 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const techTreeLine = await buildTechTreeLine(
startingTankId,
targetTankId,
targetTankId
);
const line = [...techTreeLine, startingTankId].reverse();
const { nickname } = await getAccountInfo(region, id);
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 costs = await Promise.all(
line.map(async (id) => {
@@ -187,18 +186,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.turrets.forEach((turret) => {
if (
turret.research_cost !== undefined &&
typeof turret.research_cost.research_cost_type!.value === 'number'
typeof turret.research_cost.research_cost_type!.value === "number"
) {
turretXps.set(
turret.id,
turret.research_cost.research_cost_type!.value,
turret.research_cost.research_cost_type!.value
);
}
turret.guns.forEach((gun) => {
if (
gun.research_cost !== undefined &&
typeof gun.research_cost.research_cost_type!.value === 'number'
typeof gun.research_cost.research_cost_type!.value === "number"
) {
gunXps.set(gun.id, gun.research_cost.research_cost_type!.value);
}
@@ -208,11 +207,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.engines.forEach((engine) => {
if (
engine.research_cost !== undefined &&
typeof engine.research_cost.research_cost_type!.value === 'number'
typeof engine.research_cost.research_cost_type!.value === "number"
) {
engineXps.set(
engine.id,
engine.research_cost.research_cost_type!.value,
engine.research_cost.research_cost_type!.value
);
}
});
@@ -220,11 +219,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.tracks.forEach((track) => {
if (
track.research_cost !== undefined &&
typeof track.research_cost.research_cost_type!.value === 'number'
typeof track.research_cost.research_cost_type!.value === "number"
) {
trackXps.set(
track.id,
track.research_cost.research_cost_type!.value,
track.research_cost.research_cost_type!.value
);
}
});
@@ -249,10 +248,10 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
purchase: tank.price.value,
equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b,
0,
0
),
};
}),
})
);
const image = (
@@ -267,14 +266,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
<div
style={{
display: 'flex',
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
@@ -285,10 +284,10 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
right: 24,
}}
>
@@ -304,23 +303,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: '16px 0',
padding: "16px 0",
}}
>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset('icons/currencies/xp.webp'))}
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
@@ -330,7 +329,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast,
}}
>
Experience:{' '}
Experience:{" "}
{costs
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
.toLocaleString(interaction.locale)}
@@ -339,14 +338,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset('icons/currencies/silver.webp'))}
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
@@ -356,7 +355,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast,
}}
>
Purchase:{' '}
Purchase:{" "}
{costs
.reduce((a, b) => a + b.purchase, 0)
.toLocaleString(interaction.locale)}
@@ -365,14 +364,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset('icons/currencies/silver.webp'))}
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
@@ -382,7 +381,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast,
}}
>
Equipment:{' '}
Equipment:{" "}
{costs
.reduce((a, b) => a + b.equipment, 0)
.toLocaleString(interaction.locale)}
@@ -399,7 +398,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
key={id}
style={{
display: 'flex',
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
@@ -408,9 +407,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
width: 48,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -426,9 +425,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
position: 'relative',
right: '5%',
display: "flex",
position: "relative",
right: "5%",
}}
>
<img
@@ -437,23 +436,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
style={{
height: 100,
width: 100,
objectFit: 'contain',
position: 'absolute',
left: '25%',
objectFit: "contain",
position: "absolute",
left: "25%",
top: 0,
}}
/>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
alignItems: "center",
justifyContent: "center",
position: "absolute",
bottom: 8,
left: '50%',
transform: 'translateX(-50%)',
left: "50%",
transform: "translateX(-50%)",
}}
>
<img
@@ -476,23 +475,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: '16px 0',
padding: "16px 0",
}}
>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset('icons/currencies/xp.webp'))}
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
@@ -504,11 +503,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.research,
[
`${costs[index].research?.toLocaleString(
interaction.locale,
)}`,
],
{
cost: costs[index].research?.toLocaleString(
interaction.locale
),
}
)}
</span>
</div>
@@ -516,15 +515,15 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
{costs[index].upgrades > 0 && (
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Free XP icon"
src={await iconPng(
asset('icons/currencies/free-xp.webp'),
asset("icons/currencies/free-xp.webp")
)}
width={16}
height={16}
@@ -537,7 +536,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.upgrades,
[costs[index].upgrades.toLocaleString()],
{ cost: costs[index].upgrades.toLocaleString() }
)}
</span>
</div>
@@ -545,15 +544,15 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset('icons/currencies/silver.webp'),
asset("icons/currencies/silver.webp")
)}
width={16}
height={16}
@@ -566,26 +565,26 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.purchase,
[
costs[index].purchase.toLocaleString(
interaction.locale,
{
cost: costs[index].purchase.toLocaleString(
interaction.locale
),
],
}
)}
</span>
</div>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset('icons/currencies/silver.webp'),
asset("icons/currencies/silver.webp")
)}
width={16}
height={16}
@@ -598,18 +597,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.equipment,
[
costs[index].equipment.toLocaleString(
interaction.locale,
{
cost: costs[index].equipment.toLocaleString(
interaction.locale
),
],
}
)}
</span>
</div>
</div>
</div>
);
}),
})
)}
</div>
</CommandWrapper>
@@ -621,7 +620,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
},
autocomplete(interaction) {
autocompleteTanks(interaction, true, ['target-tank', 'starting-tank']);
autocompleteTanks(interaction, true, ["target-tank", "starting-tank"]);
},
});
});
+28 -28
View File
@@ -1,23 +1,23 @@
import { ClanList, fetchBlitz, Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { addClanChoices } from '../core/discord/addClanChoices';
import { addRegionChoices } from '../core/discord/addRegionChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { ClanList, fetchBlitz, Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addClanChoices } from "../core/discord/addClanChoices";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('search-clans')
command: createLocalizedCommand("search-clans")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addClanChoices(option).setAutocomplete(false),
addClanChoices(option).setAutocomplete(false)
)
.addIntegerOption((option) => {
const { strings } = translator(Locale.EnglishUS);
@@ -28,21 +28,21 @@ export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
localizationObject(
(strings) => strings.bot.commands.search_clans.options.limit.name,
undefined,
true,
),
true
)
)
.setDescription(
literals(
strings.bot.commands.search_clans.options.limit.description,
[`${DEFAULT_LIMIT}`],
),
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_clans.options.limit.description,
[`${DEFAULT_LIMIT}`],
),
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100);
@@ -50,24 +50,24 @@ export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString('region') as Region;
const clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, 'clans/list', {
const region = interaction.options.getString("region") as Region;
const clan = interaction.options.getString("clan")!;
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, "clans/list", {
search: clan,
limit,
});
return embedInfo(
literals(strings.bot.commands.search_clans.body.title, [
markdownEscape(clan),
strings.common.regions.normal[region],
]),
literals(strings.bot.commands.search_clans.body.title, {
search: markdownEscape(clan),
region: strings.common.regions.normal[region],
}),
clanList.length === 0
? strings.bot.commands.search_clans.body.no_results
: `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`)
.join('\n')}\n\`\`\``,
.join("\n")}\n\`\`\``
);
},
});
+30 -30
View File
@@ -3,17 +3,17 @@ import {
fetchBlitz,
Region,
usernamePattern,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { addRegionChoices } from '../core/discord/addRegionChoices';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
@@ -21,10 +21,10 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand('search-players')
command: createLocalizedCommand("search-players")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
addUsernameChoices(option).setAutocomplete(false).setRequired(true)
)
.addIntegerOption((option) =>
option
@@ -34,49 +34,49 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
(strings) =>
strings.bot.commands.search_players.options.limit.name,
undefined,
true,
),
true
)
)
.setDescription(
literals(
strings.bot.commands.search_players.options.limit.description,
[`${DEFAULT_LIMIT}`],
),
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_players.options.limit.description,
[`${DEFAULT_LIMIT}`],
),
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100),
.setMaxValue(100)
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString('region') as Region;
const name = interaction.options.getString('username')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const region = interaction.options.getString("region") as Region;
const name = interaction.options.getString("username")!;
const limit = interaction.options.getInteger("limit") ?? 25;
const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch)
? await fetchBlitz<AccountList>(region, 'account/list', {
? await fetchBlitz<AccountList>(region, "account/list", {
search: trimmedSearch,
limit,
})
: [];
return embedInfo(
literals(strings.bot.commands.search_players.body.title, [
markdownEscape(trimmedSearch),
strings.common.regions.normal[region],
]),
literals(strings.bot.commands.search_players.body.title, {
search: markdownEscape(trimmedSearch),
region: strings.common.regions.normal[region],
}),
`\`\`\`${
players.length === 0
? strings.bot.commands.search_players.body.no_results
: players.map((player) => player.nickname).join('\n')
}\`\`\``,
: players.map((player) => player.nickname).join("\n")
}\`\`\``
);
},
});
+18 -16
View File
@@ -1,33 +1,33 @@
import { SEARCH_KEYS } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { go } from 'fuzzysort';
import markdownEscape from 'markdown-escape';
import { tankDefinitions, tankNames } from '../blitzkit/nonBlockingPromises';
import { UserError } from '../blitzkit/userError';
import { tankNamesTechTreeOnly } from '../discord/autocompleteTanks';
import { translator } from '../localization/translator';
import { SEARCH_KEYS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { go } from "fuzzysort";
import markdownEscape from "markdown-escape";
import { tankDefinitions, tankNames } from "../blitzkit/nonBlockingPromises";
import { UserError } from "../blitzkit/userError";
import { tankNamesTechTreeOnly } from "../discord/autocompleteTanks";
import { translator } from "../localization/translator";
export async function resolveTankId(
tank: string | number,
locale: Locale,
techTreeOnly = false,
techTreeOnly = false
) {
const { strings } = translator(locale);
const number = typeof tank === 'string' ? Number(tank) : tank;
const number = typeof tank === "string" ? Number(tank) : tank;
if (Number.isNaN(number)) {
const searchResult = go(
`${tank}`,
await (techTreeOnly ? tankNamesTechTreeOnly : tankNames),
{ keys: SEARCH_KEYS, limit: 1 },
{ keys: SEARCH_KEYS, limit: 1 }
);
if (searchResult.length === 0) {
throw new UserError(
literals(strings.bot.common.errors.tank_name_not_found, [
markdownEscape(`${tank}`),
]),
literals(strings.bot.common.errors.tank_name_not_found, {
search: markdownEscape(`${tank}`),
})
);
} else {
return searchResult[0].obj.id;
@@ -39,7 +39,9 @@ export async function resolveTankId(
return number;
} else {
throw new UserError(
literals(strings.bot.common.errors.tank_id_not_found, [`${number}`]),
literals(strings.bot.common.errors.tank_id_not_found, {
search: number,
})
);
}
}
@@ -1,12 +1,12 @@
import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { StatFilters } from '../blitzstars/filterStats';
import { translator } from '../localization/translator';
import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { StatFilters } from "../blitzstars/filterStats";
import { translator } from "../localization/translator";
export async function filtersToDescription(
{ nation, tier, type, class: tankClass, tank }: StatFilters,
locale: Locale,
locale: Locale
) {
const { strings, bkLocale } = translator(locale);
const awaitedTankDefinitions = await fetchTankDefinitions();
@@ -18,11 +18,13 @@ export async function filtersToDescription(
info.push((strings.common.nations as Record<string, string>)[nation]);
if (tier) {
info.push(
literals(strings.bot.common.filters.tier, [TIER_ROMAN_NUMERALS[tier]]),
literals(strings.bot.common.filters.tier, {
tier: TIER_ROMAN_NUMERALS[tier],
})
);
}
if (tankClass) info.push(strings.common.tank_class_short[tankClass]);
if (type) info.push(strings.common.tree_type[type]);
return info.length === 0 ? strings.bot.common.filters.none : info.join(', ');
return info.length === 0 ? strings.bot.common.filters.none : info.join(", ");
}
+17 -17
View File
@@ -1,13 +1,13 @@
import { numberToRomanNumeral } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { numberToRomanNumeral } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import {
APIApplicationCommandOptionChoice,
Locale,
SlashCommandStringOption,
} from 'discord.js';
import { range } from 'lodash-es';
import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject';
} from "discord.js";
import { range } from "lodash-es";
import { translator } from "../localization/translator";
import { localizationObject } from "./localizationObject";
export function addTierChoices(option: SlashCommandStringOption) {
const { strings } = translator(Locale.EnglishUS);
@@ -18,30 +18,30 @@ export function addTierChoices(option: SlashCommandStringOption) {
localizationObject(
(strings) => strings.bot.common.options.tier.name,
undefined,
true,
),
true
)
)
.setDescription(strings.bot.common.options.tier.description)
.setDescriptionLocalizations(
localizationObject(
(strings) => strings.bot.common.options.tier.description,
),
(strings) => strings.bot.common.options.tier.description
)
)
.setChoices(
...range(10, 0).map(
(tier) =>
({
name: literals(strings.bot.common.options.tier.choices.tier, [
`${tier}`,
numberToRomanNumeral(tier),
]),
name: literals(strings.bot.common.options.tier.choices.tier, {
tier_1: tier,
tier_2: numberToRomanNumeral(tier),
}),
value: `${tier}`,
name_localizations: localizationObject(
(strings) => strings.bot.common.options.tier.choices.tier,
[`${tier}`, numberToRomanNumeral(tier)],
{ tier_1: tier, tier_2: numberToRomanNumeral(tier) }
),
}) satisfies APIApplicationCommandOptionChoice,
),
}) satisfies APIApplicationCommandOptionChoice
)
)
.setRequired(true);
}
@@ -1,11 +1,11 @@
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { translator } from '../localization/translator';
import { PeriodSize } from './addPeriodSubCommands';
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { translator } from "../localization/translator";
import { PeriodSize } from "./addPeriodSubCommands";
export function getPeriodOptionName(period: PeriodSize, locale: Locale) {
const { strings } = translator(locale);
if (period === 'career') return strings.bot.common.periods.career;
if (period === 'today') return strings.bot.common.periods.today;
return literals(strings.bot.common.periods.days, [period]);
if (period === "career") return strings.bot.common.periods.career;
if (period === "today") return strings.bot.common.periods.today;
return literals(strings.bot.common.periods.days, { days: period });
}
@@ -1,35 +1,35 @@
import { Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { ChatInputCommandInteraction } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { searchClansAcrossRegionsBotWrapper } from '../blitz/searchClansAcrossRegionsBotWrapper';
import { UserError } from '../blitzkit/userError';
import { translator } from '../localization/translator';
import { Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { ChatInputCommandInteraction } from "discord.js";
import markdownEscape from "markdown-escape";
import { searchClansAcrossRegionsBotWrapper } from "../blitz/searchClansAcrossRegionsBotWrapper";
import { UserError } from "../blitzkit/userError";
import { translator } from "../localization/translator";
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export async function resolveClanFromCommand(
interaction: ChatInputCommandInteraction,
interaction: ChatInputCommandInteraction
) {
const { strings } = translator(interaction.locale);
const clan = interaction.options.getString('clan', true);
const clan = interaction.options.getString("clan", true);
if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split('/');
const [server, accountId] = clan.split("/");
return { region: server as Region, id: Number(accountId) };
} else {
const accounts = await searchClansAcrossRegionsBotWrapper(
clan,
interaction.locale,
interaction.locale
);
if (accounts[0]) {
return { region: accounts[0].region, id: accounts[0].clan_id };
} else {
throw new UserError(
literals(strings.bot.common.errors.clan_not_found, [
markdownEscape(clan),
]),
literals(strings.bot.common.errors.clan_not_found, {
search: markdownEscape(clan),
})
);
}
}
@@ -1,11 +1,11 @@
import { getTimeDaysAgo, Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { getPeriodNow } from '../blitzkit/getPeriodNow';
import { getPeriodStart } from '../blitzkit/getPeriodStart';
import { translator } from '../localization/translator';
import { PeriodType } from './addPeriodSubCommands';
import { getPeriodOptionName } from './getPeriodOptionName';
import { getTimeDaysAgo, Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { CacheType, ChatInputCommandInteraction } from "discord.js";
import { getPeriodNow } from "../blitzkit/getPeriodNow";
import { getPeriodStart } from "../blitzkit/getPeriodStart";
import { translator } from "../localization/translator";
import { PeriodType } from "./addPeriodSubCommands";
import { getPeriodOptionName } from "./getPeriodOptionName";
export interface ResolvedPeriod {
name: string;
@@ -16,7 +16,7 @@ export interface ResolvedPeriod {
export function resolvePeriodFromCommand(
region: Region,
interaction: ChatInputCommandInteraction<CacheType>,
forcedPeriod?: PeriodType,
forcedPeriod?: PeriodType
) {
const { strings } = translator(interaction.locale);
let name: string;
@@ -26,16 +26,16 @@ export function resolvePeriodFromCommand(
? forcedPeriod
: (interaction.options.getSubcommand() as PeriodType);
if (periodSubcommand === 'custom') {
const startOption = interaction.options.getInteger('start', true);
const endOption = interaction.options.getInteger('end', true);
if (periodSubcommand === "custom") {
const startOption = interaction.options.getInteger("start", true);
const endOption = interaction.options.getInteger("end", true);
const startDaysAgoMin = Math.min(startOption, endOption);
const endDaysAgoMax = Math.max(startOption, endOption);
name = literals(strings.bot.common.periods.custom, [
`${startDaysAgoMin}`,
`${endDaysAgoMax}`,
]);
name = literals(strings.bot.common.periods.custom, {
day_1: startDaysAgoMin,
day_2: endDaysAgoMax,
});
start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin);
} else {
@@ -1,11 +1,11 @@
import { idToRegion, Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { searchPlayersAcrossRegionsBotWrapper } from '../../blitz/searchPlayersAcrossRegionsBotWrapper';
import { getBlitzFromDiscord } from '../../blitzkit/getBlitzFromDiscord';
import { UserError } from '../../blitzkit/userError';
import { translator } from '../../localization/translator';
import { idToRegion, Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { CacheType, ChatInputCommandInteraction } from "discord.js";
import markdownEscape from "markdown-escape";
import { searchPlayersAcrossRegionsBotWrapper } from "../../blitz/searchPlayersAcrossRegionsBotWrapper";
import { getBlitzFromDiscord } from "../../blitzkit/getBlitzFromDiscord";
import { UserError } from "../../blitzkit/userError";
import { translator } from "../../localization/translator";
export interface ResolvedPlayer {
region: Region;
@@ -13,10 +13,10 @@ export interface ResolvedPlayer {
}
export async function resolvePlayerFromCommand(
interaction: ChatInputCommandInteraction<CacheType>,
interaction: ChatInputCommandInteraction<CacheType>
) {
const { strings } = translator(interaction.locale);
const commandUsername = interaction.options.getString('username');
const commandUsername = interaction.options.getString("username");
if (commandUsername !== null) {
const id = parseInt(commandUsername);
@@ -27,7 +27,7 @@ export async function resolvePlayerFromCommand(
} else {
const accounts = await searchPlayersAcrossRegionsBotWrapper(
commandUsername,
interaction.locale,
interaction.locale
);
if (accounts[0]) {
@@ -37,9 +37,9 @@ export async function resolvePlayerFromCommand(
} satisfies ResolvedPlayer;
} else {
throw new UserError(
literals(strings.bot.common.errors.player_not_found, [
markdownEscape(commandUsername),
]),
literals(strings.bot.common.errors.player_not_found, {
search: markdownEscape(commandUsername),
})
);
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
export function assertSecret(secret?: string) {
if (secret === undefined) throw new Error('Missing secret');
export function assertSecret<Type>(secret?: Type) {
if (secret === undefined) throw new Error("Missing secret");
return secret;
}
+12 -2
View File
@@ -1,9 +1,19 @@
import { assertSecret } from "@blitzkit/core";
const dev = assertSecret(import.meta.env.DEV);
export function literals(
string: string,
literals?: Record<string, string | number>
literals?: Record<string, string | number | undefined>
) {
for (const key in literals) {
string = string.replaceAll(`{${key}}`, `${literals[key]}`);
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`);
if (dev && mutated === string) {
console.warn(`Could not find literal {${key}} in string "${string}"`);
}
string = mutated;
}
return string;
+2 -2
View File
@@ -844,7 +844,7 @@
"other_projects": {
"title": "Other cool projects"
},
"help_translate": "Please help translate {name} to {language_2}!",
"help_translate": "Please help translate {name} to {language}!",
"seo_description": "Tools for everything World of Tanks Blitz"
},
@@ -1293,7 +1293,7 @@
"no_results": "No players found"
},
"controls": {
"title": "Tracking {count}",
"title": "Tracking {name}",
"subtitle": "Since {time}",
"reset": "Reset",
"change": "Change"
@@ -53,32 +53,38 @@ function LayerEntry({
<td>
{layer.type === null && (
<Text size="2" color={shotStatusColor}>
{literals(strings.common.units.mm, [
Math.round(layer.distance * 1000).toLocaleString(locale),
])}
{literals(strings.common.units.mm, {
value: Math.round(layer.distance * 1000).toLocaleString(locale),
})}
</Text>
)}
{layer.type === ArmorType.External && (
<Text size="2" color={shotStatusColor}>
{literals(strings.common.units.mm, [
Math.round(layer.thickness).toLocaleString(locale),
])}
{literals(strings.common.units.mm, {
value: Math.round(layer.thickness).toLocaleString(locale),
})}
</Text>
)}
{(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
<Text size="2" color={shotStatusColor}>
{literals(
angle === undefined
? strings.common.units.mm
: strings.website.tools.tankopedia.sandbox.dynamic.shot_card
{angle === undefined
? literals(strings.common.units.mm, {
value: Math.round(layer.thicknessAngled).toLocaleString(
locale
),
})
: literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.stats.thickness_and_angle,
[
Math.round(layer.thicknessAngled).toLocaleString(locale),
radToDeg(angle ?? 0).toFixed(0),
]
)}
{
thickness: Math.round(layer.thicknessAngled).toLocaleString(
locale
),
angle: radToDeg(angle ?? 0).toFixed(0),
}
)}
</Text>
)}
</td>
@@ -89,7 +95,7 @@ function LayerEntry({
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.ricochet_loss,
[Math.max(-100, -50 * layer.distance).toFixed(0)]
{ loss: Math.max(-100, -50 * layer.distance).toFixed(0) }
)}
</Text>
)}
@@ -100,7 +106,7 @@ function LayerEntry({
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.nominal,
[Math.round(layer.thickness).toLocaleString(locale)]
{ thickness: Math.round(layer.thickness).toLocaleString(locale) }
)}
</Text>
)}
@@ -132,7 +138,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
shot.in.status
],
[Math.round(shot.damage).toLocaleString(locale)]
{ damage: Math.round(shot.damage).toLocaleString(locale) }
)}
</Text>
@@ -202,7 +208,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.status[shot.out.status],
[Math.round(shot.damage).toLocaleString(locale)]
{ damage: Math.round(shot.damage).toLocaleString(locale) }
)}
</Text>
@@ -155,7 +155,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
{literals(
strings.website.tools.embed.configuration.export.generate
.success.info,
[`${copyUser?.nickname}`]
{ name: copyUser?.nickname }
)}
</Text>
</Flex>
@@ -50,9 +50,9 @@ export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
{skeleton ? (
<InlineSkeleton width="7rem" />
) : (
literals(strings.website.tools.gallery.search.results, [
filtered.length.toLocaleString(locale),
])
literals(strings.website.tools.gallery.search.results, {
count: filtered.length.toLocaleString(locale),
})
)}
</Text>
+4 -4
View File
@@ -139,10 +139,10 @@ export function Guesser() {
<Flex justify="center" style={{ userSelect: "none" }}>
<Text>
{literals(strings.website.tools.guess.stats, [
`${correctGuesses}`,
`${totalGuesses}`,
])}
{literals(strings.website.tools.guess.stats, {
correct: correctGuesses,
total: totalGuesses,
})}
</Text>
</Flex>
+18 -16
View File
@@ -1,8 +1,8 @@
---
import { Code, Flex, Heading, Text } from '@radix-ui/themes';
import { resolveBranchName } from '../core/blitzkit/branches';
import { getStrings } from '../core/i18n/getStrings';
import { literals } from '@blitzkit/i18n/src/literals';
import { Code, Flex, Heading, Text } from "@radix-ui/themes";
import { resolveBranchName } from "../core/blitzkit/branches";
import { getStrings } from "../core/i18n/getStrings";
import { literals } from "@blitzkit/i18n/src/literals";
const { locale } = Astro.params;
const branchName = resolveBranchName(locale);
@@ -45,7 +45,7 @@ const strings = getStrings(locale);
);
}
#home-hero[data-variant='pride'] {
#home-hero[data-variant="pride"] {
--g-1-dark: var(--crimson-3);
--g-1-light: var(--crimson-7);
--g-2-dark: var(--orange-3);
@@ -58,7 +58,7 @@ const strings = getStrings(locale);
--g-7-light: var(--purple-7);
}
#home-hero[data-variant='spooky'] {
#home-hero[data-variant="spooky"] {
--g-1-dark: var(--orange-3);
--g-1-light: var(--orange-7);
--g-2-dark: var(--tomato-3);
@@ -73,23 +73,23 @@ const strings = getStrings(locale);
</style>
<script>
import { clamp, times } from 'lodash-es';
import { clamp, times } from "lodash-es";
const n = 5;
const homeHero = document.getElementById('home-hero');
const homeHero = document.getElementById("home-hero");
if (homeHero) {
switch (new Date().getMonth()) {
case 5:
homeHero.dataset.variant = 'pride';
homeHero.dataset.variant = "pride";
break;
case 9:
homeHero.dataset.variant = 'spooky';
homeHero.dataset.variant = "spooky";
break;
}
window.addEventListener('pointermove', (event) => {
window.addEventListener("pointermove", (event) => {
const x = event.clientX / window.innerWidth;
const c = clamp(
(1 -
@@ -97,7 +97,7 @@ const strings = getStrings(locale);
window.innerHeight) **
2,
0,
1,
1
);
// https://www.desmos.com/calculator/ndz2poxk32
@@ -128,13 +128,13 @@ const strings = getStrings(locale);
width="100%"
>
<Flex direction="column" align="center" gap="1">
<Heading size={{ initial: '8', xs: '9' }} weight="bold" align="center">
<Flex align="center" gap={{ initial: '2', xs: '4' }}>
<Heading size={{ initial: "8", xs: "9" }} weight="bold" align="center">
<Flex align="center" gap={{ initial: "2", xs: "4" }}>
BlitzKit
{
isBranchNamed && (
<Code
size={{ initial: '7', xs: '8' }}
size={{ initial: "7", xs: "8" }}
color="gray"
variant="outline"
>
@@ -146,7 +146,9 @@ const strings = getStrings(locale);
</Heading>
<Text color="gray" align="center" size="4"
>{literals(strings.website.home.hero.credit, ['TrèsAbhi'])}</Text
>{
literals(strings.website.home.hero.credit, { name: "TrèsAbhi" })
}</Text
>
</Flex>
</Flex>
@@ -32,7 +32,7 @@ function Content() {
{strings.website.home.hot_tanks.title}
</Heading>
<Text color="gray" align="center" mb="5" size="2">
{literals(strings.website.home.hot_tanks.subtitle, ["7"])}
{literals(strings.website.home.hot_tanks.subtitle, { days: 7 })}
</Text>
<Flex justify="center" gap="4" wrap="wrap">
@@ -1,12 +1,12 @@
import { formatCompact } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n/src/literals';
import { InfoCircledIcon } from '@radix-ui/react-icons';
import { Callout, Flex } from '@radix-ui/themes';
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
import { awaitableDiscoveredIdsDefinitions } from '../../core/awaitables/discoveredIdsDefinitions';
import { useLocale } from '../../hooks/useLocale';
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
import { InlineSkeleton } from '../InlineSkeleton';
import { formatCompact } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import { InfoCircledIcon } from "@radix-ui/react-icons";
import { Callout, Flex } from "@radix-ui/themes";
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
import { awaitableDiscoveredIdsDefinitions } from "../../core/awaitables/discoveredIdsDefinitions";
import { useLocale } from "../../hooks/useLocale";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { InlineSkeleton } from "../InlineSkeleton";
const [discoveredIdsDefinitions, averageDefinitions] = await Promise.all([
awaitableDiscoveredIdsDefinitions,
@@ -17,22 +17,22 @@ export function PerformanceInfo({ skeleton }: MaybeSkeletonComponentProps) {
const { strings, locale } = useLocale();
const samples = formatCompact(
locale,
Math.round(discoveredIdsDefinitions.count),
Math.round(discoveredIdsDefinitions.count)
);
const minutesAgo = Math.floor(
(Date.now() - averageDefinitions.time) / (1000 * 60),
(Date.now() - averageDefinitions.time) / (1000 * 60)
);
const hoursAgo = Math.floor(minutesAgo / 60);
const timeAgo = literals(
strings.website.tools.performance.estimation[
hoursAgo === 0 ? 'minutes' : 'hours'
hoursAgo === 0 ? "minutes" : "hours"
],
[`${hoursAgo === 0 ? minutesAgo : hoursAgo}`],
{ count: hoursAgo === 0 ? minutesAgo : hoursAgo }
);
return (
<Flex justify="center">
<Callout.Root style={{ width: 'fit-content' }}>
<Callout.Root style={{ width: "fit-content" }}>
<Callout.Icon>
<InfoCircledIcon />
</Callout.Icon>
@@ -40,10 +40,10 @@ export function PerformanceInfo({ skeleton }: MaybeSkeletonComponentProps) {
{skeleton && <InlineSkeleton width="9rem" />}
{!skeleton &&
literals(strings.website.tools.performance.estimation.body, [
samples,
timeAgo,
])}
literals(strings.website.tools.performance.estimation.body, {
count: samples,
age: timeAgo,
})}
</Callout.Text>
</Callout.Root>
</Flex>
@@ -431,9 +431,9 @@ export const TankSearch = memo<TankSearchProps>(
<Text color="gray">
{tanksFiltered.length === 1
? strings.website.common.tank_search.count_singular
: literals(strings.website.common.tank_search.count_plural, [
`${tanksFiltered.length.toLocaleString(locale)}`,
])}
: literals(strings.website.common.tank_search.count_plural, {
count: tanksFiltered.length.toLocaleString(locale),
})}
</Text>
{onSelectAll && (
@@ -460,9 +460,11 @@ export const TankSearch = memo<TankSearchProps>(
{tankopediaSort.by !== "meta.none" && (
<Text color="gray">
{literals(strings.website.common.tank_search.sorting_by, [
strings.website.common.tank_search.sort[tankopediaSort.by],
])}
{literals(strings.website.common.tank_search.sorting_by, {
name: strings.website.common.tank_search.sort[
tankopediaSort.by
],
})}
{SORT_UNITS[tankopediaSort.by] === undefined
? ""
: ` (${SORT_UNITS[tankopediaSort.by]})`}
@@ -1,16 +1,16 @@
import type { AssaultRanges } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Box, Flex, Text } from '@radix-ui/themes';
import { clamp } from 'three/src/math/MathUtils.js';
import type { AssaultRanges } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Box, Flex, Text } from "@radix-ui/themes";
import { clamp } from "three/src/math/MathUtils.js";
import {
fakeLogSquishyHigh,
fakeLogSquishyLow,
} from '../../../../../../../core/blitzkit/fakeLog';
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
import { Var } from '../../../../../../../core/radix/var';
import { useLocale } from '../../../../../../../hooks/useLocale';
import { VisualizerCard } from '../VisualizerCard';
import { AssaultCursor } from './components/Cursor';
} from "../../../../../../../core/blitzkit/fakeLog";
import type { TankCharacteristics } from "../../../../../../../core/blitzkit/tankCharacteristics";
import { Var } from "../../../../../../../core/radix/var";
import { useLocale } from "../../../../../../../hooks/useLocale";
import { VisualizerCard } from "../VisualizerCard";
import { AssaultCursor } from "./components/Cursor";
interface Props {
ranges: AssaultRanges;
@@ -48,27 +48,29 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
p="3"
pt="8"
pr="5"
style={{ backgroundColor: Var('gray-1') }}
style={{ backgroundColor: Var("gray-1") }}
flexGrow="1"
>
<Flex flexGrow="1">
<Box position="relative" width={Var('space-8')}>
<Box position="relative" width={Var("space-8")}>
{ranges.map((range, index) => (
<Text
key={index}
style={{
position: 'absolute',
position: "absolute",
left: 0,
bottom: `${fakeLogSquishyLow(range.factor / maxFactor) * 100}%`,
transform: 'translateY(50%)',
transform: "translateY(50%)",
}}
color="gray"
size="1"
wrap="nowrap"
>
{literals(strings.common.units.hp, [
`${Math.round(stats.damageWithoutAssault * range.factor)}`,
])}
{literals(strings.common.units.hp, {
value: Math.round(
stats.damageWithoutAssault * range.factor
),
})}
</Text>
))}
</Box>
@@ -77,8 +79,8 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
{ranges.map((range, index) => {
const mix =
clamp(2 * (range.factor / maxFactor - 1) + 1, 0, 1) * 100;
const color0 = `color-mix(in hsl, ${Var('tomato-9')}, ${Var('jade-9')} ${mix}%)`;
const color1 = `color-mix(in hsl, ${Var('tomato-7')}, ${Var('jade-7')} ${mix}%)`;
const color0 = `color-mix(in hsl, ${Var("tomato-9")}, ${Var("jade-9")} ${mix}%)`;
const color1 = `color-mix(in hsl, ${Var("tomato-7")}, ${Var("jade-7")} ${mix}%)`;
const last = ranges[index - 1];
const lastFakeDistance = last?.fakeDistance ?? 0;
const fakeWidth = range.fakeDistance - lastFakeDistance;
@@ -105,8 +107,8 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
width="100%"
height="1pt"
style={{
transform: 'translateY(50%)',
backgroundColor: Var('gray-11'),
transform: "translateY(50%)",
backgroundColor: Var("gray-11"),
}}
/>
))}
@@ -118,7 +120,7 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
</Flex>
</Flex>
<Flex ml="8" height={Var('space-7')}>
<Flex ml="8" height={Var("space-7")}>
{ranges.map((range, index) => {
const last = ranges[index - 1];
const lastFakeDistance = last?.fakeDistance ?? 0;
@@ -137,15 +139,15 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
color="gray"
mt="2"
style={{
position: 'absolute',
position: "absolute",
right: 0,
transform: 'translateX(50%)',
writingMode: 'vertical-rl',
transform: "translateX(50%)",
writingMode: "vertical-rl",
}}
>
{literals(strings.common.units.m, [
`${Math.round(range.distance)}`,
])}
{literals(strings.common.units.m, {
value: Math.round(range.distance),
})}
</Text>
</Box>
);
@@ -38,16 +38,17 @@ export function CompareOptions(props: FlexProps) {
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Class}`}>
{literals(
strings.website.tools.tankopedia.characteristics.compare.class,
[
TIER_ROMAN_NUMERALS[tank.tier],
strings.common.tank_class_short_plural_lowercase[tank.class],
]
{
tier: TIER_ROMAN_NUMERALS[tank.tier],
class:
strings.common.tank_class_short_plural_lowercase[tank.class],
}
)}
</SegmentedControl.Item>
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
{literals(
strings.website.tools.tankopedia.characteristics.compare.tier,
[TIER_ROMAN_NUMERALS[tank.tier]]
{ tier: TIER_ROMAN_NUMERALS[tank.tier] }
)}
</SegmentedControl.Item>
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
@@ -1,15 +1,15 @@
import { CREW_MEMBER_NAMES, CrewType } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { AccessibilityIcon, InfoCircledIcon } from '@radix-ui/react-icons';
import { Flex, Heading, IconButton, Popover, Text } from '@radix-ui/themes';
import { Fragment } from 'react/jsx-runtime';
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useLocale } from '../../../../../../hooks/useLocale';
import { Duel } from '../../../../../../stores/duel';
import { InfoWithDelta } from './InfoWithDelta';
import { StatsTableWrapper } from './StatsTableWrapper';
import type { StatsAcceptorProps } from './TraverseVisualizer';
import { CREW_MEMBER_NAMES, CrewType } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { AccessibilityIcon, InfoCircledIcon } from "@radix-ui/react-icons";
import { Flex, Heading, IconButton, Popover, Text } from "@radix-ui/themes";
import { Fragment } from "react/jsx-runtime";
import { awaitableProvisionDefinitions } from "../../../../../../core/awaitables/provisionDefinitions";
import { useEquipment } from "../../../../../../hooks/useEquipment";
import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../stores/duel";
import { InfoWithDelta } from "./InfoWithDelta";
import { StatsTableWrapper } from "./StatsTableWrapper";
import type { StatsAcceptorProps } from "./TraverseVisualizer";
const provisionDefinitions = await awaitableProvisionDefinitions;
@@ -24,7 +24,7 @@ export function Crew({ stats }: StatsAcceptorProps) {
provisionDefinitions.provisions[provision].crew
? total + provisionDefinitions.provisions[provision].crew! / 100
: total,
0,
0
) + (hasImprovedVentilation ? 0.08 : 0);
const commanderMastery = 1 + provisionCrewBonus;
@@ -65,8 +65,8 @@ export function Crew({ stats }: StatsAcceptorProps) {
]
}${
member.count > 1
? ` ${literals(strings.common.units.x, [`${member.count}`])}`
: ''
? ` ${literals(strings.common.units.x, { value: member.count })}`
: ""
}`}
decimals={0}
noRanking
@@ -93,9 +93,9 @@ export function Crew({ stats }: StatsAcceptorProps) {
? strings.website.tools.tankopedia.crew[
CREW_MEMBER_NAMES[sub]
]
: sub,
: sub
)
.join(', ')}
.join(", ")}
</Flex>
</>
}
@@ -119,7 +119,7 @@ export function Firepower({
name={literals(
strings.website.tools.tankopedia.characteristics.values
.shell_index,
[`${index + 1}`]
{ index: index + 1 }
)}
decimals={2}
deltaType="lowerIsBetter"
@@ -191,7 +191,7 @@ export function Firepower({
name={literals(
strings.website.tools.tankopedia.characteristics.values
.dispersion_at_distance,
["100"]
{ distance: 100 }
)}
/>
<InfoWithDelta
@@ -359,31 +359,33 @@ export function GunFlexibilityVisualizer({
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.flexibility.max}
value={literals(strings.common.units.deg, [
radToDeg(maxPitch).toFixed(0),
])}
value={literals(strings.common.units.deg, {
value: radToDeg(maxPitch).toFixed(0),
})}
side="bottom-left"
/>
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.flexibility.min}
value={literals(strings.common.units.deg, [
radToDeg(minPitch).toFixed(0),
])}
value={literals(strings.common.units.deg, {
value: radToDeg(minPitch).toFixed(0),
})}
side="bottom-right"
/>
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
value={literals(strings.common.units.deg, [
radToDeg(pitch).toFixed(0),
])}
value={literals(strings.common.units.deg, {
value: radToDeg(pitch).toFixed(0),
})}
side="top-left"
/>
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.flexibility.yaw}
value={literals(strings.common.units.deg, [radToDeg(yaw).toFixed(0)])}
value={literals(strings.common.units.deg, {
value: radToDeg(yaw).toFixed(0),
})}
side="top-right"
/>
</VisualizerCard>
@@ -1,15 +1,15 @@
import { asset, isExplosive } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Box, Flex, Text } from '@radix-ui/themes';
import { useRef, useState } from 'react';
import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
import { Var } from '../../../../../../../core/radix/var';
import { useLocale } from '../../../../../../../hooks/useLocale';
import { Duel } from '../../../../../../../stores/duel';
import type { StatsAcceptorProps } from '../TraverseVisualizer';
import { VisualizerCard } from '../VisualizerCard';
import { VisualizerCornerStat } from '../VisualizerCornerStat';
import { ShellSwitcher } from './components/ShellSwitcher';
import { asset, isExplosive } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Box, Flex, Text } from "@radix-ui/themes";
import { useRef, useState } from "react";
import { clamp, degToRad, radToDeg } from "three/src/math/MathUtils.js";
import { Var } from "../../../../../../../core/radix/var";
import { useLocale } from "../../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../../stores/duel";
import type { StatsAcceptorProps } from "../TraverseVisualizer";
import { VisualizerCard } from "../VisualizerCard";
import { VisualizerCornerStat } from "../VisualizerCornerStat";
import { ShellSwitcher } from "./components/ShellSwitcher";
export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
const shell = Duel.use((state) => state.protagonist.shell);
@@ -30,7 +30,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
<Flex direction="column" mb="6" align="center">
<VisualizerCard
mb="0"
style={{ width: '100%' }}
style={{ width: "100%" }}
ref={container}
onPointerMove={(event) => {
if (!container.current || explodes) return;
@@ -50,7 +50,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
top="0"
left="0"
style={{
background: `linear-gradient(to right, transparent, ${Var('gray-a4')})`,
background: `linear-gradient(to right, transparent, ${Var("gray-a4")})`,
}}
>
<Box
@@ -73,20 +73,20 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
background: `conic-gradient(
at 0 50%,
${Var('ruby-a2')},
${Var('ruby-a1')} ${Math.PI / 2 - ricochet}rad,
${Var("ruby-a2")},
${Var("ruby-a1")} ${Math.PI / 2 - ricochet}rad,
${Var('jade-a2')} ${Math.PI / 2 - ricochet}rad,
${Var('jade-a4')} ${Math.PI / 2 - normalization}rad,
${Var("jade-a2")} ${Math.PI / 2 - ricochet}rad,
${Var("jade-a4")} ${Math.PI / 2 - normalization}rad,
${Var('cyan-a5')} ${Math.PI / 2 - normalization}rad,
${Var('cyan-a3')} ${Math.PI / 2 + normalization}rad,
${Var("cyan-a5")} ${Math.PI / 2 - normalization}rad,
${Var("cyan-a3")} ${Math.PI / 2 + normalization}rad,
${Var('jade-a2')} ${Math.PI / 2 + normalization}rad,
${Var('jade-a4')} ${Math.PI / 2 + ricochet}rad,
${Var("jade-a2")} ${Math.PI / 2 + normalization}rad,
${Var("jade-a4")} ${Math.PI / 2 + ricochet}rad,
${Var('ruby-a1')} ${Math.PI / 2 + ricochet}rad,
${Var('ruby-a2')} 180deg
${Var("ruby-a1")} ${Math.PI / 2 + ricochet}rad,
${Var("ruby-a2")} 180deg
`,
}}
/>
@@ -97,7 +97,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
top="50%"
height="18rem"
style={{
transformOrigin: 'top center',
transformOrigin: "top center",
transform: `translateX(-50%) rotate(${-Math.PI / 2 - angle}rad)`,
}}
>
@@ -107,8 +107,8 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
height="100%"
bottom="100%"
style={{
transform: 'translateX(-50%)',
backgroundColor: Var('gray-a8'),
transform: "translateX(-50%)",
backgroundColor: Var("gray-a8"),
}}
/>
<Box
@@ -116,22 +116,22 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
width="3px"
height="100%"
style={{
transform: 'translateX(-50%)',
background: `linear-gradient(${Var('gray-a11')}, ${Var('gray-a4')})`,
transform: "translateX(-50%)",
background: `linear-gradient(${Var("gray-a11")}, ${Var("gray-a4")})`,
}}
/>
<img
src={asset(`icons/shells/${shell.icon}.webp`)}
style={{
objectFit: 'contain',
transform: 'translate(-50%, -50%) rotate(-45deg)',
position: 'absolute',
width: '2rem',
height: '2rem',
top: '6rem',
objectFit: "contain",
transform: "translate(-50%, -50%) rotate(-45deg)",
position: "absolute",
width: "2rem",
height: "2rem",
top: "6rem",
left: 0,
filter: 'drop-shadow(0px 0px 0.25rem black)',
filter: "drop-shadow(0px 0px 0.25rem black)",
}}
/>
</Box>
@@ -142,7 +142,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
top="50%"
height="16rem"
style={{
transformOrigin: 'top center',
transformOrigin: "top center",
transform: `translateX(-50%) rotate(${-Math.PI / 2 - effectiveAngle}rad)`,
}}
>
@@ -152,17 +152,17 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
height="100%"
top="-100%"
style={{
transform: 'translateX(-50%)',
background: `linear-gradient(${Var('gray-a4')}, ${Var('gray-a11')})`,
transform: "translateX(-50%)",
background: `linear-gradient(${Var("gray-a4")}, ${Var("gray-a11")})`,
}}
/>
</Box>
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.ricochet.angle}
value={literals(strings.common.units.deg, [
radToDeg(Math.abs(angle)).toFixed(0),
])}
value={literals(strings.common.units.deg, {
value: radToDeg(Math.abs(angle)).toFixed(0),
})}
side="top-left"
/>
<VisualizerCornerStat
@@ -171,10 +171,12 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
}
value={
doesRicochet
? '-'
: literals(strings.common.units.deg, [
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(0),
])
? "-"
: literals(strings.common.units.deg, {
value: radToDeg(
Math.min(normalization, Math.abs(angle))
).toFixed(0),
})
}
side="top-right"
/>
@@ -185,10 +187,10 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
}
value={
doesRicochet
? '-'
: literals(strings.common.units.deg, [
radToDeg(Math.abs(effectiveAngle)).toFixed(0),
])
? "-"
: literals(strings.common.units.deg, {
value: radToDeg(Math.abs(effectiveAngle)).toFixed(0),
})
}
side="bottom-left"
/>
@@ -196,10 +198,10 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
label={strings.website.tools.tankopedia.visualizers.ricochet.armor}
value={
doesRicochet
? '-'
: literals(strings.common.units.percentage, [
armorPercentage.toFixed(0),
])
? "-"
: literals(strings.common.units.percentage, {
value: armorPercentage.toFixed(0),
})
}
side="bottom-right"
/>
@@ -215,7 +217,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
top="0"
left="0"
style={{
background: `radial-gradient(${Var('black-a7')}, ${Var('black-a11')})`,
background: `radial-gradient(${Var("black-a7")}, ${Var("black-a11")})`,
}}
>
<Text color="gray" align="center">
@@ -1,20 +1,20 @@
import { literals } from '@blitzkit/i18n';
import { Checkbox, Code, Flex, Tabs, Text } from '@radix-ui/themes';
import { useEffect, useRef, useState } from 'react';
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
import { useLocale } from '../../../../../../../hooks/useLocale';
import { VisualizerCard } from '../VisualizerCard';
import { literals } from "@blitzkit/i18n";
import { Checkbox, Code, Flex, Tabs, Text } from "@radix-ui/themes";
import { useEffect, useRef, useState } from "react";
import type { TankCharacteristics } from "../../../../../../../core/blitzkit/tankCharacteristics";
import { useLocale } from "../../../../../../../hooks/useLocale";
import { VisualizerCard } from "../VisualizerCard";
export interface StatsAcceptorProps {
stats: TankCharacteristics;
}
type Terrain = 'Hard' | 'Medium' | 'Soft';
type Terrain = "Hard" | "Medium" | "Soft";
export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
const hull = useRef<HTMLDivElement>(null);
const turret = useRef<HTMLImageElement>(null);
const [terrain, setTerrain] = useState<Terrain>('Hard');
const [terrain, setTerrain] = useState<Terrain>("Hard");
const [rotateHull, setRotateHull] = useState(true);
const [rotateTurret, setRotateTurret] = useState(true);
const { strings } = useLocale();
@@ -63,10 +63,10 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
<div
ref={hull}
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
animationDuration: `${
rotateHull ? 360 / stats[`hullTraverse${terrain}Terrain`] : 0
}s`,
@@ -75,12 +75,12 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
<img
draggable="false"
style={{
width: isTurretless ? '22rem' : '12rem',
height: isTurretless ? '22rem' : '12rem',
objectFit: 'contain',
filter: 'drop-shadow(0px 0px 4px black)',
width: isTurretless ? "22rem" : "12rem",
height: isTurretless ? "22rem" : "12rem",
objectFit: "contain",
filter: "drop-shadow(0px 0px 4px black)",
}}
src={`/assets/images/tankopedia/visualizers/hull-traverse/hull${isTurretless ? '-only' : ''}.png`}
src={`/assets/images/tankopedia/visualizers/hull-traverse/hull${isTurretless ? "-only" : ""}.png`}
/>
{!isTurretless && (
@@ -88,14 +88,14 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
draggable="false"
ref={turret}
style={{
width: '27rem',
height: '27rem',
objectFit: 'contain',
position: 'absolute',
top: '50%',
left: '50%',
filter: 'drop-shadow(0px 0px 4px black)',
transform: 'translate(-50%, -50%)',
width: "27rem",
height: "27rem",
objectFit: "contain",
position: "absolute",
top: "50%",
left: "50%",
filter: "drop-shadow(0px 0px 4px black)",
transform: "translate(-50%, -50%)",
animationDuration: `${
rotateTurret ? 360 / stats.turretTraverseSpeed : 0
}s`,
@@ -110,7 +110,7 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
bottom="0"
left="50%"
style={{
transform: 'translateX(-50%)',
transform: "translateX(-50%)",
}}
>
<Tabs.Root
@@ -137,16 +137,16 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
left="50%"
bottom="7"
style={{
transform: 'translateX(-50%)',
transform: "translateX(-50%)",
}}
>
<Text color="gray" size="1">
{strings.website.tools.tankopedia.visualizers.rotator.effective}
</Text>
<Code size="1" variant="ghost">
{literals(strings.common.units.deg_s, [
`${(turretSpeedDeg + hullSpeedDeg).toFixed(1)}`,
])}
{literals(strings.common.units.deg_s, {
value: (turretSpeedDeg + hullSpeedDeg).toFixed(1),
})}
</Code>
</Flex>
@@ -160,15 +160,15 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
gap="5"
justify="center"
style={{
transform: 'translateX(-50%)',
userSelect: 'none',
transform: "translateX(-50%)",
userSelect: "none",
}}
>
<Flex
align="center"
gap="2"
onClick={() => setRotateHull((state) => !state)}
style={{ cursor: 'pointer' }}
style={{ cursor: "pointer" }}
>
<Checkbox variant="classic" checked={rotateHull} />
<Text size="2">
@@ -180,7 +180,7 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
align="center"
gap="2"
onClick={() => setRotateTurret((state) => !state)}
style={{ cursor: 'pointer' }}
style={{ cursor: "pointer" }}
>
<Checkbox variant="classic" checked={rotateTurret} />
<Text size="2">
@@ -1,20 +1,20 @@
import { literals } from '@blitzkit/i18n';
import { EyeOpenIcon } from '@radix-ui/react-icons';
import { Box, Card, Code, Flex, Slider, Tabs, Text } from '@radix-ui/themes';
import { useRef, useState } from 'react';
import { Var } from '../../../../../../core/radix/var';
import { useLocale } from '../../../../../../hooks/useLocale';
import type { StatsAcceptorProps } from './TraverseVisualizer';
import { literals } from "@blitzkit/i18n";
import { EyeOpenIcon } from "@radix-ui/react-icons";
import { Box, Card, Code, Flex, Slider, Tabs, Text } from "@radix-ui/themes";
import { useRef, useState } from "react";
import { Var } from "../../../../../../core/radix/var";
import { useLocale } from "../../../../../../hooks/useLocale";
import type { StatsAcceptorProps } from "./TraverseVisualizer";
const maps = [
{
id: 'ghost-factory',
string: 'ghost_factory',
id: "ghost-factory",
string: "ghost_factory",
size: 512,
},
{
id: 'dead-rail',
string: 'dead_rail',
id: "dead-rail",
string: "dead_rail",
size: 512,
},
] as const;
@@ -32,7 +32,7 @@ export function ViewRangeVisualizer({ stats }: StatsAcceptorProps) {
return (
<Card
style={{ aspectRatio: '1 / 1', touchAction: 'none' }}
style={{ aspectRatio: "1 / 1", touchAction: "none" }}
mb="6"
variant="classic"
>
@@ -61,8 +61,8 @@ export function ViewRangeVisualizer({ stats }: StatsAcceptorProps) {
style={{
backgroundImage: `url(/assets/images/tankopedia/visualizers/range/${map.id}.png)`,
filter: `grayscale(0.5) brightness(0.8) contrast(1.2)`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
@@ -75,16 +75,18 @@ export function ViewRangeVisualizer({ stats }: StatsAcceptorProps) {
align="center"
justify="center"
style={{
aspectRatio: '1 / 1',
transform: 'translate(-50%, -50%)',
background: `radial-gradient(${Var('black-a6')}, ${Var('gray-a12')})`,
borderRadius: '50%',
aspectRatio: "1 / 1",
transform: "translate(-50%, -50%)",
background: `radial-gradient(${Var("black-a6")}, ${Var("gray-a12")})`,
borderRadius: "50%",
}}
>
<Text style={{ userSelect: 'none' }}>
<Text style={{ userSelect: "none" }}>
<Flex align="center" gap="1">
<EyeOpenIcon width="1.25rem" height="1.25rem" />
{literals(strings.common.units.m, [resolvedViewRange.toFixed(0)])}
{literals(strings.common.units.m, {
value: resolvedViewRange.toFixed(0),
})}
</Flex>
</Text>
</Flex>
@@ -100,8 +102,8 @@ export function ViewRangeVisualizer({ stats }: StatsAcceptorProps) {
gap="3"
align="center"
style={{
backgroundColor: Var('black-a7'),
backdropFilter: 'blur(4rem)',
backgroundColor: Var("black-a7"),
backdropFilter: "blur(4rem)",
}}
>
<Text size="2">
@@ -120,11 +122,9 @@ export function ViewRangeVisualizer({ stats }: StatsAcceptorProps) {
<Code size="1" color="gray" variant="ghost">
<Flex justify="center" width="2em">
{literals(
strings.website.tools.tankopedia.visualizers.view_range
.percentage,
[`${Math.round(camouflage * 100)}`.padStart(3, ' ')],
)}
{literals(strings.common.units.percentage, {
percentage: `${Math.round(camouflage * 100)}`.padStart(3, " "),
})}
</Flex>
</Code>
</Flex>
@@ -136,7 +136,7 @@ export function ViewRangeVisualizer({ stats }: StatsAcceptorProps) {
left="0"
width="100%"
style={{
background: `radial-gradient(at 50% 100%, ${Var('gray-1')}, transparent 70%)`,
background: `radial-gradient(at 50% 100%, ${Var("gray-1")}, transparent 70%)`,
}}
>
<Tabs.Root
@@ -151,7 +151,7 @@ export function CustomShellButton() {
}}
>
<TextField.Slot side="right">
{literals(strings.common.units.mm, [""])}
{literals(strings.common.units.mm, { value: "" })}
</TextField.Slot>
</TextField.Root>
</Flex>
@@ -188,7 +188,7 @@ export function CustomShellButton() {
}}
>
<TextField.Slot side="right">
{literals(strings.common.units.mm, [""])}
{literals(strings.common.units.mm, { value: "" })}
</TextField.Slot>
</TextField.Root>
</Flex>
@@ -212,9 +212,9 @@ export function CustomShellButton() {
minWidth="8rem"
>
<Text color="gray">
{literals(strings.common.units.deg, [
(customShell.normalization ?? 0).toFixed(0),
])}
{literals(strings.common.units.deg, {
value: (customShell.normalization ?? 0).toFixed(0),
})}
</Text>
<Slider
variant="classic"
@@ -243,9 +243,9 @@ export function CustomShellButton() {
<Flex align="center" gap="2" flexGrow="1" maxWidth="15rem">
<Text color="gray">
{literals(strings.common.units.deg, [
(customShell.ricochet ?? 90).toFixed(0),
])}
{literals(strings.common.units.deg, {
value: (customShell.ricochet ?? 90).toFixed(0),
})}
</Text>
<Slider
variant="classic"
@@ -59,15 +59,15 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
>
<Flex direction="column" align="end" justify="between">
<Text color="gray" size="1">
{literals(strings.common.units.mm, [
(thicknessRange.value * 1.5).toFixed(0),
])}
{literals(strings.common.units.mm, {
value: (thicknessRange.value * 1.5).toFixed(0),
})}
</Text>
<Text size="1">
{strings.website.tools.tankopedia.sandbox.static.primary}
</Text>
<Text color="gray" size="1">
{literals(strings.common.units.mm, ["0"])}
{literals(strings.common.units.mm, { value: 0 })}
</Text>
</Flex>
@@ -100,15 +100,15 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
>
<Flex direction="column" align="end" justify="between">
<Text color="gray" size="1">
{literals(strings.common.units.mm, [
thicknessRange.value.toFixed(0),
])}
{literals(strings.common.units.mm, {
value: thicknessRange.value.toFixed(0),
})}
</Text>
<Text size="1">
{strings.website.tools.tankopedia.sandbox.static.spaced}
</Text>
<Text color="gray" size="1">
{literals(strings.common.units.mm, ["0"])}
{literals(strings.common.units.mm, { value: 0 })}
</Text>
</Flex>
@@ -92,14 +92,14 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
>
{!hasCustomShell && (
<Text color="gray" size={{ initial: "1", sm: "2" }}>
{literals(strings.common.units.mm, [
(
{literals(strings.common.units.mm, {
value: (
resolvePenetrationCoefficient(
hasCalibratedShells,
antagonistShell.type
) * antagonistShell.penetration.near
).toFixed(0),
])}
})}
</Text>
)}
@@ -156,9 +156,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
secondaryDiscriminator={
<Text style={{ fontSize: "0.75em" }}>
{literals(strings.common.units.mm, [
gun.shells[0].caliber.toFixed(0),
])}
{literals(strings.common.units.mm, {
value: gun.shells[0].caliber.toFixed(0),
})}
</Text>
}
/>
@@ -135,7 +135,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
? strings.website.tools.tankopedia.tech_tree.battle
: literals(
strings.website.tools.tankopedia.tech_tree.battles,
[`${games}`]
{ count: games }
))}
</Text>
)}
@@ -158,7 +158,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
pl="1"
>
<Text size="2">
{literals(strings.common.units.x, [`${multiplier}`])}
{literals(strings.common.units.x, { value: multiplier })}
</Text>
</Flex>
);
@@ -218,10 +218,10 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
<CaretLeftIcon />
</IconButton>
<Text>
{literals(strings.website.tools.tankopedia.tech_tree.route, [
`${lineIndex + 1}`,
`${lines.length}`,
])}
{literals(strings.website.tools.tankopedia.tech_tree.route, {
index: lineIndex + 1,
count: lines.length,
})}
</Text>
<IconButton
size="2"
@@ -88,12 +88,13 @@ export const getStaticPaths = (() => {
target="_blank"
underline="always"
>
{literals(strings.website.home.help_translate, [
"BlitzKit",
strings.common.locales[
locale as keyof typeof strings.common.locales
],
])}
{literals(strings.website.home.help_translate, {
name: "BlitzKit",
language:
strings.common.locales[
locale as keyof typeof strings.common.locales
],
})}
</Link>
</Flex>
)
@@ -277,14 +277,14 @@ function Content() {
>
<Flex mt="2" mb="2" direction="column" gap="2">
<Heading size="5">
{literals(strings.website.tools.session.controls.title, [
`${accountInfo?.nickname}`,
])}
{literals(strings.website.tools.session.controls.title, {
name: accountInfo?.nickname,
})}
</Heading>
<Text color="gray">
{literals(strings.website.tools.session.controls.subtitle, [
new Date(session.player.since).toLocaleString(locale),
])}
{literals(strings.website.tools.session.controls.subtitle, {
time: new Date(session.player.since).toLocaleString(locale),
})}
</Text>
</Flex>
@@ -49,15 +49,16 @@ const tankModelDefinitions = await fetchModelDefinitions();
const tank = tankDefinitions.tanks[id];
const tankModelDefinition = tankModelDefinitions.models[tank.id];
const unwrap = unwrapper(locale);
const title = literals(strings.website.tools.tankopedia.tank_seo_title, [
unwrap(tank.name),
TIER_ROMAN_NUMERALS[tank.tier],
strings.common.nations_adjectives[
tank.nation as keyof typeof strings.common.nations_adjectives
],
strings.common.tank_class_short[tank.class],
"BlitzKit",
]);
const title = literals(strings.website.tools.tankopedia.tank_seo_title, {
name: unwrap(tank.name),
tier: TIER_ROMAN_NUMERALS[tank.tier],
nationality:
strings.common.nations_adjectives[
tank.nation as keyof typeof strings.common.nations_adjectives
],
class: strings.common.tank_class_short[tank.class],
website: "BlitzKit",
});
const url = import.meta.env.SITE + Astro.url.pathname;
const icon = asset(`icons/tanks/big/${id}.webp`);
@@ -90,14 +91,14 @@ const characteristics = tankCharacteristics(
);
const description = literals(
strings.website.tools.tankopedia.tank_seo_description,
[
unwrap(tank.name),
Math.round(characteristics.dpm).toLocaleString(locale),
Math.round(characteristics.damage).toLocaleString(locale),
Math.round(characteristics.penetration).toLocaleString(locale),
Math.round(characteristics.speedForwards).toLocaleString(locale),
Math.round(characteristics.health).toLocaleString(locale),
]
{
name: unwrap(tank.name),
dpm: Math.round(characteristics.dpm).toLocaleString(locale),
alpha: Math.round(characteristics.damage).toLocaleString(locale),
penetration: Math.round(characteristics.penetration).toLocaleString(locale),
speed: Math.round(characteristics.speedForwards).toLocaleString(locale),
health: Math.round(characteristics.health).toLocaleString(locale),
}
);
export interface TankGuide {