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
+624 -592

No files matched your search

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