fix: update all

This commit is contained in:
TrèsAbhi committed 2026-03-31 14:11:45 -05:00
1 parent bdea9fdab3
commit e2f50871ec
86 files changed
+1939 -1537

No files matched your search

+4 -4
View File
@@ -43,7 +43,7 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
tankStats.map(async (tankData) => ({ tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!, tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!,
id: tankData.tank_id, id: tankData.tank_id,
})) })),
) )
).filter((tank) => tank.tankDefinitions?.tier === tier); ).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {}; const groupedTanks: Record<string, TankDefinition[]> = {};
@@ -69,16 +69,16 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
.filter((tank) => tank.tier === tier) .filter((tank) => tank.tier === tier)
.sort( .sort(
(a, b) => (a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class) TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
) )
.sort( .sort(
(a, b) => (a, b) =>
awaitedGameDefinitions.nations.indexOf(a.nation) - awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation) awaitedGameDefinitions.nations.indexOf(b.nation),
) )
.map( .map(
(tank) => (tank) =>
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name))}](<https://blitzkit.app/tanks/${tank.slug}>)` `[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name!))}](<https://blitzkit.app/tanks/${tank.slug}>)`,
); );
const lines = [title, ...tankList]; const lines = [title, ...tankList];
+2 -2
View File
@@ -280,7 +280,7 @@ async function playerListing(
}} }}
> >
<img <img
alt={unwrap(tank.name)} alt={unwrap(tank.name!)}
src={await iconPng(tankIcon(tank.id))} src={await iconPng(tankIcon(tank.id))}
style={{ style={{
width: 32, width: 32,
@@ -317,7 +317,7 @@ async function playerListing(
fontSize: 16, fontSize: 16,
}} }}
> >
{unwrap(tank.name)} {unwrap(tank.name!)}
</span> </span>
</div> </div>
+47 -47
View File
@@ -35,49 +35,49 @@ export const researchCommand = new Promise<CommandRegistry>((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!)
.setNameLocalizations( .setNameLocalizations(
localizationObject( localizationObject(
(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
.setName(strings.bot.commands.research.options.starting_tank.name) .setName(strings.bot.commands.research.options.starting_tank.name!)
.setNameLocalizations( .setNameLocalizations(
localizationObject( localizationObject(
(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),
@@ -87,7 +87,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
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];
@@ -99,13 +99,13 @@ 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,
{ tank: escapeMarkdown(unwrap(targetTank.name)) } { tank: escapeMarkdown(unwrap(targettank.name!)) },
); );
} }
@@ -113,8 +113,8 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
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, {
tank_1: escapeMarkdown(unwrap(startingTank.name)), tank_1: escapeMarkdown(unwrap(startingtank.name!)),
tank_2: escapeMarkdown(unwrap(targetTank.name)), tank_2: escapeMarkdown(unwrap(targettank.name!)),
}); });
} }
@@ -122,21 +122,21 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
return literals( return literals(
strings.bot.commands.research.errors.tanks_not_on_line, strings.bot.commands.research.errors.tanks_not_on_line,
{ {
tank_1: escapeMarkdown(unwrap(targetTank.name)), tank_1: escapeMarkdown(unwrap(targettank.name!)),
tank_2: escapeMarkdown(unwrap(startingTank.name)), tank_2: escapeMarkdown(unwrap(startingtank.name!)),
} },
); );
} }
} else { } else {
if (targetTank.type !== TankType.RESEARCHABLE) { if (targetTank.type !== TankType.TANK_TYPE_RESEARCHABLE) {
return literals(strings.bot.commands.research.errors.non_tech_tree, { return literals(strings.bot.commands.research.errors.non_tech_tree, {
tank: 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, {
tank: escapeMarkdown(unwrap(targetTank.name)), tank: escapeMarkdown(unwrap(targettank.name!)),
}); });
} }
@@ -149,14 +149,14 @@ 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,
{ tank: 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) {
@@ -168,7 +168,7 @@ 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();
@@ -190,7 +190,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
) { ) {
turretXps.set( turretXps.set(
turret.id, turret.id,
turret.research_cost.research_cost_type!.value turret.research_cost.research_cost_type!.value,
); );
} }
@@ -211,7 +211,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
) { ) {
engineXps.set( engineXps.set(
engine.id, engine.id,
engine.research_cost.research_cost_type!.value engine.research_cost.research_cost_type!.value,
); );
} }
}); });
@@ -223,7 +223,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
) { ) {
trackXps.set( trackXps.set(
track.id, track.id,
track.research_cost.research_cost_type!.value track.research_cost.research_cost_type!.value,
); );
} }
}); });
@@ -245,13 +245,13 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
return { return {
research: tank.research_cost!.research_cost_type!.value as number, research: tank.research_cost!.research_cost_type!.value as number,
upgrades: moduleXp, upgrades: moduleXp,
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 = (
@@ -456,7 +456,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}} }}
> >
<img <img
alt={unwrap(tank.name)} alt={unwrap(tank.name!)}
src={TANK_ICONS[tank.class]} src={TANK_ICONS[tank.class]}
width={16} width={16}
height={16} height={16}
@@ -467,7 +467,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
}} }}
> >
{unwrap(tank.name)} {unwrap(tank.name!)}
</span> </span>
</div> </div>
</div> </div>
@@ -505,9 +505,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
strings.bot.commands.research.body.research, strings.bot.commands.research.body.research,
{ {
cost: costs[index].research?.toLocaleString( cost: costs[index].research?.toLocaleString(
interaction.locale interaction.locale,
), ),
} },
)} )}
</span> </span>
</div> </div>
@@ -523,7 +523,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<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}
@@ -536,7 +536,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
> >
{literals( {literals(
strings.bot.commands.research.body.upgrades, strings.bot.commands.research.body.upgrades,
{ cost: costs[index].upgrades.toLocaleString() } { cost: costs[index].upgrades.toLocaleString() },
)} )}
</span> </span>
</div> </div>
@@ -552,7 +552,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<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}
@@ -567,9 +567,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
strings.bot.commands.research.body.purchase, strings.bot.commands.research.body.purchase,
{ {
cost: costs[index].purchase.toLocaleString( cost: costs[index].purchase.toLocaleString(
interaction.locale interaction.locale,
), ),
} },
)} )}
</span> </span>
</div> </div>
@@ -584,7 +584,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<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}
@@ -599,16 +599,16 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
strings.bot.commands.research.body.equipment, strings.bot.commands.research.body.equipment,
{ {
cost: 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>
@@ -3,14 +3,14 @@ import {
TankClass, TankClass,
TankType, TankType,
TREE_TYPE_ICONS, TREE_TYPE_ICONS,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { createColors } from 'bepaint'; import { createColors } from "bepaint";
import { import {
type AccentColor, type AccentColor,
type GrayColor, type GrayColor,
PALETTES, PALETTES,
} from '../../../core/radix/radixColors'; } from "../../../core/radix/radixColors";
import { RowStat } from './RowStat'; import { RowStat } from "./RowStat";
export interface RowStatItem { export interface RowStatItem {
title: string; title: string;
@@ -21,7 +21,7 @@ export interface RowStatItem {
} }
interface RowProps { interface RowProps {
displayType?: 'tank' | 'summary'; displayType?: "tank" | "summary";
naked?: boolean; naked?: boolean;
title: string; title: string;
minimized?: boolean; minimized?: boolean;
@@ -32,7 +32,7 @@ interface RowProps {
} }
export function Row({ export function Row({
color = 'slate', color = "slate",
stats, stats,
title, title,
minimized, minimized,
@@ -49,20 +49,20 @@ export function Row({
className="session-tracker-row" className="session-tracker-row"
style={{ style={{
maxWidth: naked ? 600 : Infinity, maxWidth: naked ? 600 : Infinity,
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
borderRadius: 4, borderRadius: 4,
backgroundColor: naked backgroundColor: naked
? `${theme.appBackground2}c0` ? `${theme.appBackground2}c0`
: theme.appBackground2, : theme.appBackground2,
overflow: 'hidden', overflow: "hidden",
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
padding: 8, padding: 8,
gap: 4, gap: 4,
backgroundColor: naked backgroundColor: naked
@@ -70,10 +70,14 @@ export function Row({
: theme.componentInteractive, : theme.componentInteractive,
}} }}
> >
{displayType === 'tank' && tankClass !== undefined && ( {displayType === "tank" && tankClass !== undefined && (
<img <img
alt={TankClass[tankClass]} alt={TankClass[tankClass]}
src={TREE_TYPE_ICONS[type ?? TankType.RESEARCHABLE][tankClass]} src={
TREE_TYPE_ICONS[type ?? TankType.TANK_TYPE_RESEARCHABLE][
tankClass
]
}
style={{ width: 16, height: 16 }} style={{ width: 16, height: 16 }}
/> />
)} )}
@@ -81,10 +85,10 @@ export function Row({
<span <span
style={{ style={{
color: color:
displayType === 'tank' displayType === "tank"
? type === TankType.COLLECTOR ? type === TankType.TANK_TYPE_COLLECTOR
? theme.textLowContrast_blue ? theme.textLowContrast_blue
: type === TankType.PREMIUM : type === TankType.TANK_TYPE_PREMIUM
? theme.textLowContrast_amber ? theme.textLowContrast_amber
: theme.textHighContrast : theme.textHighContrast
: theme.textLowContrast, : theme.textLowContrast,
@@ -98,7 +102,7 @@ export function Row({
<div <div
style={{ style={{
display: 'flex', display: "flex",
paddingTop: 8, paddingTop: 8,
paddingBottom: 8, paddingBottom: 8,
}} }}
@@ -109,8 +113,8 @@ export function Row({
<RowStat <RowStat
minimized={minimized} minimized={minimized}
key={index} key={index}
name={`${row.title} • ${row.career ?? '--'}`} name={`${row.title} • ${row.career ?? "--"}`}
value={row.current ?? '--'} value={row.current ?? "--"}
delta={row.delta} delta={row.delta}
percentile={row.percentile} percentile={row.percentile}
/> />
@@ -1,5 +1,5 @@
import { TankClass, TankType, TREE_TYPE_ICONS } from '@blitzkit/core'; import { TankClass, TankType, TREE_TYPE_ICONS } from "@blitzkit/core";
import { theme } from '../../../../stitches.config'; import { theme } from "../../../../stitches.config";
export interface ItemProps { export interface ItemProps {
tankClass?: TankClass; tankClass?: TankClass;
@@ -8,9 +8,9 @@ export interface ItemProps {
} }
const TREE_TYPE_COLOR = { const TREE_TYPE_COLOR = {
[TankType.RESEARCHABLE]: '', [TankType.TANK_TYPE_RESEARCHABLE]: "",
[TankType.PREMIUM]: '_amber', [TankType.TANK_TYPE_PREMIUM]: "_amber",
[TankType.COLLECTOR]: '_blue', [TankType.TANK_TYPE_COLLECTOR]: "_blue",
} as const; } as const;
export function Item({ export function Item({
@@ -21,12 +21,12 @@ export function Item({
return ( return (
<div <div
style={{ style={{
display: 'flex', display: "flex",
height: 32, height: 32,
paddingLeft: 8, paddingLeft: 8,
paddingRight: 8, paddingRight: 8,
overflow: 'hidden', overflow: "hidden",
alignItems: 'center', alignItems: "center",
borderRadius: 4, borderRadius: 4,
backgroundColor: backgroundColor:
theme.colors[`componentInteractive${TREE_TYPE_COLOR[treeType]}`], theme.colors[`componentInteractive${TREE_TYPE_COLOR[treeType]}`],
@@ -34,12 +34,12 @@ export function Item({
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 4, gap: 4,
flex: 1, flex: 1,
alignItems: 'center', alignItems: "center",
justifyContent: 'flex-start', justifyContent: "flex-start",
overflow: 'hidden', overflow: "hidden",
}} }}
> >
{tankType && ( {tankType && (
@@ -52,7 +52,7 @@ export function Item({
<span <span
style={{ style={{
fontSize: 16, fontSize: 16,
whiteSpace: 'nowrap', whiteSpace: "nowrap",
color: theme.colors[`textLowContrast${TREE_TYPE_COLOR[treeType]}`], color: theme.colors[`textLowContrast${TREE_TYPE_COLOR[treeType]}`],
}} }}
> >
@@ -6,21 +6,21 @@ 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();
const info: string[] = []; const info: string[] = [];
// TODO: somehow localize tank names? // TODO: somehow localize tank names?
if (tank) return awaitedTankDefinitions.tanks[tank].name.locales[bkLocale]; if (tank) return awaitedTankDefinitions.tanks[tank].name!.locales[bkLocale];
if (nation) if (nation)
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, { literals(strings.bot.common.filters.tier, {
tier: TIER_ROMAN_NUMERALS[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]);
@@ -34,12 +34,12 @@ export async function filterStats(
(filters.tier === undefined || entry.tier === filters.tier) && (filters.tier === undefined || entry.tier === filters.tier) &&
(filters.class === undefined || entry.class === filters.class) && (filters.class === undefined || entry.class === filters.class) &&
(filters.type === undefined || (filters.type === undefined ||
(filters.type === TankType.COLLECTOR && (filters.type === TankType.TANK_TYPE_COLLECTOR &&
entry.type === TankType.COLLECTOR) || entry.type === TankType.TANK_TYPE_COLLECTOR) ||
(filters.type === TankType.PREMIUM && (filters.type === TankType.TANK_TYPE_PREMIUM &&
entry.type === TankType.PREMIUM) || entry.type === TankType.TANK_TYPE_PREMIUM) ||
(filters.type === TankType.RESEARCHABLE && (filters.type === TankType.TANK_TYPE_RESEARCHABLE &&
entry.type === TankType.RESEARCHABLE)) && entry.type === TankType.TANK_TYPE_RESEARCHABLE)) &&
(filters.tank === undefined || entry.id === filters.tank) (filters.tank === undefined || entry.id === filters.tank)
); );
}); });
@@ -1,12 +1,12 @@
import { Locale, SlashCommandStringOption } from 'discord.js'; import { Locale, SlashCommandStringOption } from "discord.js";
import { translator } from '../localization/translator'; import { translator } from "../localization/translator";
import { localizationObject } from './localizationObject'; import { localizationObject } from "./localizationObject";
export function addTankChoices(option: SlashCommandStringOption) { export function addTankChoices(option: SlashCommandStringOption) {
const { strings } = translator(Locale.EnglishUS); const { strings } = translator(Locale.EnglishUS);
return option return option
.setName(strings.bot.common.options.tank.name) .setName(strings.bot.common.options.tank.name!)
.setNameLocalizations( .setNameLocalizations(
localizationObject( localizationObject(
(strings) => strings.bot.common.options.tank.name, (strings) => strings.bot.common.options.tank.name,
@@ -13,13 +13,13 @@ import {
} from "./autocompleteClan/constants"; } from "./autocompleteClan/constants";
export const tankNamesTechTreeOnly = fetchTankNames().then((names) => export const tankNamesTechTreeOnly = fetchTankNames().then((names) =>
names.filter((tank) => tank.treeType === TankType.RESEARCHABLE) names.filter((tank) => tank.treeType === TankType.TANK_TYPE_RESEARCHABLE),
); );
export async function autocompleteTanks( export async function autocompleteTanks(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
techTreeOnly = false, techTreeOnly = false,
fields = ["tank"] fields = ["tank"],
) { ) {
const { unwrap } = translator(interaction.locale); const { unwrap } = translator(interaction.locale);
const focusedOption = interaction.options.getFocused(true); const focusedOption = interaction.options.getFocused(true);
@@ -31,7 +31,7 @@ export async function autocompleteTanks(
go( go(
focusedOption.value, focusedOption.value,
await (techTreeOnly ? tankNamesTechTreeOnly : tankNames), await (techTreeOnly ? tankNamesTechTreeOnly : tankNames),
{ keys: SEARCH_KEYS, limit: 10 } { keys: SEARCH_KEYS, limit: 10 },
).map(async (item) => { ).map(async (item) => {
let name = unwrap(item.obj.searchableName); let name = unwrap(item.obj.searchableName);
@@ -42,7 +42,7 @@ export async function autocompleteTanks(
0, 0,
unwrap(item.obj.searchableName).length - unwrap(item.obj.searchableName).length -
overSize - overSize -
OVERFLOW_SUFFIX.length OVERFLOW_SUFFIX.length,
)}${OVERFLOW_SUFFIX}`; )}${OVERFLOW_SUFFIX}`;
} }
@@ -50,8 +50,8 @@ export async function autocompleteTanks(
name, name,
value: `${item.obj.id}`, value: `${item.obj.id}`,
} satisfies ApplicationCommandOptionChoiceData<string>; } satisfies ApplicationCommandOptionChoiceData<string>;
}) }),
) )
: [] : [],
); );
} }
+4 -2
View File
@@ -1,5 +1,7 @@
import { ShellDefinition, ShellType } from "../protos"; import { ShellDefinition, ShellType } from "../protos";
export function canSplash(type: ShellDefinition["type"]): type is ShellType.HE { export function canSplash(
return type === ShellType.HE; type: ShellDefinition["type"],
): type is ShellType.SHELL_TYPE_HE {
return type === ShellType.SHELL_TYPE_HE;
} }
+10 -10
View File
@@ -2,22 +2,22 @@ import { TankClass, TankType } from "../protos";
import type { BlitzStats } from "../statistics"; import type { BlitzStats } from "../statistics";
export const TANK_TYPES = [ export const TANK_TYPES = [
TankType.RESEARCHABLE, TankType.TANK_TYPE_RESEARCHABLE,
TankType.PREMIUM, TankType.TANK_TYPE_PREMIUM,
TankType.COLLECTOR, TankType.TANK_TYPE_COLLECTOR,
]; ];
export const TANK_TYPE_COMMAND_NAMES = { export const TANK_TYPE_COMMAND_NAMES = {
[TankType.RESEARCHABLE]: "researchable", [TankType.TANK_TYPE_RESEARCHABLE]: "researchable",
[TankType.PREMIUM]: "premium", [TankType.TANK_TYPE_PREMIUM]: "premium",
[TankType.COLLECTOR]: "collector", [TankType.TANK_TYPE_COLLECTOR]: "collector",
}; };
export const TANK_CLASSES = [ export const TANK_CLASSES = [
TankClass.LIGHT, TankClass.TANK_CLASS_LIGHT,
TankClass.MEDIUM, TankClass.TANK_CLASS_MEDIUM,
TankClass.HEAVY, TankClass.TANK_CLASS_HEAVY,
TankClass.TANK_DESTROYER, TankClass.TANK_CLASS_TANK_DESTROYER,
]; ];
export const emptyAllStats: BlitzStats = { export const emptyAllStats: BlitzStats = {
@@ -9,7 +9,7 @@ import {
export function checkConsumableProvisionInclusivity( export function checkConsumableProvisionInclusivity(
consumableProvision: Consumable | Provision, consumableProvision: Consumable | Provision,
tank: TankDefinition, tank: TankDefinition,
gun: GunDefinition gun: GunDefinition,
) { ) {
const included = consumableProvision.include?.every((rule) => { const included = consumableProvision.include?.every((rule) => {
switch (rule.filter_type!.$case) { switch (rule.filter_type!.$case) {
@@ -46,7 +46,7 @@ export function checkConsumableProvisionInclusivity(
case "categories": case "categories":
return rule.filter_type!.value.categories.some((category) => { return rule.filter_type!.value.categories.some((category) => {
switch (category) { switch (category) {
case ConsumableTankCategoryFilterCategory.CLIP: case ConsumableTankCategoryFilterCategory.CONSUMABLE_TANK_CATEGORY_FILTER_CATEGORY_CLIP:
return gun.gun_type!.$case !== "regular"; return gun.gun_type!.$case !== "regular";
} }
}); });
+3 -3
View File
@@ -1,7 +1,7 @@
import { ShellDefinition, ShellType } from "../protos"; import { ShellDefinition, ShellType } from "../protos";
export function isExplosive( export function isExplosive(
type: ShellDefinition["type"] type: ShellDefinition["type"],
): type is ShellType.HEAT | ShellType.HE { ): type is ShellType.SHELL_TYPE_HEAT | ShellType.SHELL_TYPE_HE {
return type === ShellType.HEAT || type === ShellType.HE; return type === ShellType.SHELL_TYPE_HEAT || type === ShellType.SHELL_TYPE_HE;
} }
+7 -7
View File
@@ -2,27 +2,27 @@ import { TANK_CLASSES } from "../blitz/constants";
import { GameDefinitions, TankDefinition, TankType } from "../protos"; import { GameDefinitions, TankDefinition, TankType } from "../protos";
const treeTypeOrder = [ const treeTypeOrder = [
TankType.RESEARCHABLE, TankType.TANK_TYPE_RESEARCHABLE,
TankType.PREMIUM, TankType.TANK_TYPE_PREMIUM,
TankType.COLLECTOR, TankType.TANK_TYPE_COLLECTOR,
]; ];
export function metaSortTank( export function metaSortTank(
tanks: TankDefinition[], tanks: TankDefinition[],
gameDefinitions: GameDefinitions gameDefinitions: GameDefinitions,
) { ) {
return tanks return tanks
.sort((a, b) => b.tier - a.tier) .sort((a, b) => b.tier - a.tier)
.sort( .sort(
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type) (a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type),
) )
.sort( .sort(
(a, b) => TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class) (a, b) => TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class),
) )
.sort( .sort(
(a, b) => (a, b) =>
gameDefinitions.nations.indexOf(b.nation) - gameDefinitions.nations.indexOf(b.nation) -
gameDefinitions.nations.indexOf(a.nation) gameDefinitions.nations.indexOf(a.nation),
) )
.sort((a, b) => a.tier - b.tier); .sort((a, b) => a.tier - b.tier);
} }
@@ -2,8 +2,8 @@ import { BoundingBox, Vector3 } from "../protos";
export function normalizeBoundingBox(boundingBox: BoundingBox) { export function normalizeBoundingBox(boundingBox: BoundingBox) {
return { return {
x: boundingBox.max.x - boundingBox.min.x, x: boundingBox.max!.x - boundingBox.min!.x,
y: boundingBox.max.y - boundingBox.min.y, y: boundingBox.max!.y - boundingBox.min!.y,
z: boundingBox.max.z - boundingBox.min.z, z: boundingBox.max!.z - boundingBox.min!.z,
} satisfies Vector3; } satisfies Vector3;
} }
@@ -1,4 +1,4 @@
import { ShellType } from '../protos'; import { ShellType } from "../protos";
export function resolvePenetrationCoefficient( export function resolvePenetrationCoefficient(
calibratedShells: boolean, calibratedShells: boolean,
@@ -6,11 +6,11 @@ export function resolvePenetrationCoefficient(
) { ) {
if (!calibratedShells) return 1; if (!calibratedShells) return 1;
return type === ShellType.AP return type === ShellType.SHELL_TYPE_AP
? 1.08 ? 1.08
: type === ShellType.APCR : type === ShellType.SHELL_TYPE_APCR
? 1.05 ? 1.05
: type === ShellType.HEAT : type === ShellType.SHELL_TYPE_HEAT
? 1.13 ? 1.13
: 1.08; : 1.08;
} }
@@ -1,74 +1,74 @@
import { times } from 'lodash-es'; import { times } from "lodash-es";
import { CrewType, ShellType, TankClass, TankType } from '../../protos'; import { CrewType, ShellType, TankClass, TankType } from "../../protos";
export const SHELL_NAMES: Record<ShellType, string> = { export const SHELL_NAMES: Record<ShellType, string> = {
[ShellType.AP]: 'AP', [ShellType.SHELL_TYPE_AP]: "AP",
[ShellType.APCR]: 'APCR', [ShellType.SHELL_TYPE_APCR]: "APCR",
[ShellType.HEAT]: 'HEAT', [ShellType.SHELL_TYPE_HEAT]: "HEAT",
[ShellType.HE]: 'HE', [ShellType.SHELL_TYPE_HE]: "HE",
}; };
export const CREW_MEMBER_NAMES = { export const CREW_MEMBER_NAMES = {
[CrewType.COMMANDER]: 'commander', [CrewType.CREW_TYPE_COMMANDER]: "commander",
[CrewType.DRIVER]: 'driver', [CrewType.CREW_TYPE_DRIVER]: "driver",
[CrewType.GUNNER]: 'gunner', [CrewType.CREW_TYPE_GUNNER]: "gunner",
[CrewType.LOADER]: 'loader', [CrewType.CREW_TYPE_LOADER]: "loader",
[CrewType.RADIOMAN]: 'radioman', [CrewType.CREW_TYPE_RADIOMAN]: "radioman",
} as const; } as const;
/** /**
* @deprecated use svg * @deprecated use svg
*/ */
export const TANK_ICONS: Record<TankClass, string> = { export const TANK_ICONS: Record<TankClass, string> = {
[TankClass.TANK_DESTROYER]: 'https://i.imgur.com/BIHSEH0.png', [TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/BIHSEH0.png",
[TankClass.LIGHT]: 'https://i.imgur.com/CSNha5V.png', [TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/CSNha5V.png",
[TankClass.MEDIUM]: 'https://i.imgur.com/wvf3ltm.png', [TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/wvf3ltm.png",
[TankClass.HEAVY]: 'https://i.imgur.com/ECeqlZa.png', [TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/ECeqlZa.png",
}; };
/** /**
* @deprecated use svg * @deprecated use svg
*/ */
export const TANK_ICONS_PREMIUM: Record<TankClass, string> = { export const TANK_ICONS_PREMIUM: Record<TankClass, string> = {
[TankClass.TANK_DESTROYER]: 'https://i.imgur.com/TCu3EdR.png', [TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/TCu3EdR.png",
[TankClass.LIGHT]: 'https://i.imgur.com/zdkpTRb.png', [TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/zdkpTRb.png",
[TankClass.MEDIUM]: 'https://i.imgur.com/3z7eHX6.png', [TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/3z7eHX6.png",
[TankClass.HEAVY]: 'https://i.imgur.com/P3vbmyA.png', [TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/P3vbmyA.png",
}; };
/** /**
* @deprecated use svg * @deprecated use svg
*/ */
export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = { export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
[TankClass.TANK_DESTROYER]: 'https://i.imgur.com/WTjeirB.png', [TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/WTjeirB.png",
[TankClass.LIGHT]: 'https://i.imgur.com/EwhtKkU.png', [TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/EwhtKkU.png",
[TankClass.MEDIUM]: 'https://i.imgur.com/u8YDMBh.png', [TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/u8YDMBh.png",
[TankClass.HEAVY]: 'https://i.imgur.com/8xRf3nc.png', [TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/8xRf3nc.png",
}; };
export const TIERS = times(10, (index) => index + 1); export const TIERS = times(10, (index) => index + 1);
export const TIER_ROMAN_NUMERALS: Record<number, string> = { export const TIER_ROMAN_NUMERALS: Record<number, string> = {
1: 'I', 1: "I",
2: 'II', 2: "II",
3: 'III', 3: "III",
4: 'IV', 4: "IV",
5: 'V', 5: "V",
6: 'VI', 6: "VI",
7: 'VII', 7: "VII",
8: 'VIII', 8: "VIII",
9: 'IX', 9: "IX",
10: 'X', 10: "X",
}; };
export const flags: Record<string, string> = { export const flags: Record<string, string> = {
ussr: '<:ussr:1218421042033197197>', ussr: "<:ussr:1218421042033197197>",
germany: '🇩🇪', germany: "🇩🇪",
usa: '🇺🇸', usa: "🇺🇸",
china: '🇨🇳', china: "🇨🇳",
uk: '🇬🇧', uk: "🇬🇧",
france: '🇫🇷', france: "🇫🇷",
japan: '🇯🇵', japan: "🇯🇵",
european: '🇪🇺', european: "🇪🇺",
other: '<:other:1218421572243558482>', other: "<:other:1218421572243558482>",
}; };
export const TREE_TYPE_ICONS: Record<TankType, Record<TankClass, string>> = { export const TREE_TYPE_ICONS: Record<TankType, Record<TankClass, string>> = {
[TankType.RESEARCHABLE]: TANK_ICONS, [TankType.TANK_TYPE_RESEARCHABLE]: TANK_ICONS,
[TankType.PREMIUM]: TANK_ICONS_PREMIUM, [TankType.TANK_TYPE_PREMIUM]: TANK_ICONS_PREMIUM,
[TankType.COLLECTOR]: TANK_ICONS_COLLECTOR, [TankType.TANK_TYPE_COLLECTOR]: TANK_ICONS_COLLECTOR,
}; };
+4 -4
View File
@@ -15,7 +15,7 @@ export async function fetchTankNames() {
tankDefinitionsArray.map(async (tank) => { tankDefinitionsArray.map(async (tank) => {
const searchableNameDeburr: I18nString = { locales: {} }; const searchableNameDeburr: I18nString = { locales: {} };
Object.entries(tank.name).forEach(([key, value]) => { Object.entries(tank.name!).forEach(([key, value]) => {
searchableNameDeburr.locales[key] = deburr(value); searchableNameDeburr.locales[key] = deburr(value);
}); });
@@ -28,8 +28,8 @@ export async function fetchTankNames() {
?.map((id) => ?.map((id) =>
locales.supported.map( locales.supported.map(
({ locale }) => ({ locale }) =>
camouflageDefinitions.camouflages[id]?.name.locales[locale] camouflageDefinitions.camouflages[id]?.name!.locales[locale],
) ),
) )
.flat() .flat()
.filter(Boolean) .filter(Boolean)
@@ -37,7 +37,7 @@ export async function fetchTankNames() {
.join(" "), .join(" "),
treeType: tank.type, treeType: tank.type,
}; };
}) }),
); );
} }
@@ -3,14 +3,14 @@ import { BoundingBox } from "../protos";
export function unionBoundingBox(a: BoundingBox, b: BoundingBox) { export function unionBoundingBox(a: BoundingBox, b: BoundingBox) {
return { return {
min: { min: {
x: Math.min(a.min.x, b.min.x), x: Math.min(a.min!.x, b.min!.x),
y: Math.min(a.min.y, b.min.y), y: Math.min(a.min!.y, b.min!.y),
z: Math.min(a.min.z, b.min.z), z: Math.min(a.min!.z, b.min!.z),
}, },
max: { max: {
x: Math.max(a.max.x, b.max.x), x: Math.max(a.max!.x, b.max!.x),
y: Math.max(a.max.y, b.max.y), y: Math.max(a.max!.y, b.max!.y),
z: Math.max(a.max.z, b.max.z), z: Math.max(a.max!.z, b.max!.z),
}, },
} satisfies BoundingBox; } satisfies BoundingBox;
} }
File diff suppressed because it is too large. Load diff
+23 -20
View File
@@ -1,5 +1,6 @@
import { import {
AverageDefinitions, AverageDefinitions,
AverageDefinitionsAllStats,
AverageDefinitionsEntry, AverageDefinitionsEntry,
type AverageDefinitionsManifest, type AverageDefinitionsManifest,
type IndividualTankStats, type IndividualTankStats,
@@ -30,9 +31,6 @@ type OptionalSecondLevel<T> = {
}; };
}; };
type AverageDefinitionsEntrySubPartial =
OptionalSecondLevel<AverageDefinitionsEntry>;
const MINUTE = 60 * 1000; const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE; const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR; const DAY = 24 * HOUR;
@@ -47,15 +45,15 @@ const preDiscoveredIds = await fetchPreDiscoveredIds();
const playerIds: Record<Region, number[][]> = { const playerIds: Record<Region, number[][]> = {
asia: chunk( asia: chunk(
preDiscoveredIds.filter((id) => idToRegion(id) === "asia"), preDiscoveredIds.filter((id) => idToRegion(id) === "asia"),
PLAYER_IDS_PER_CALL PLAYER_IDS_PER_CALL,
), ),
com: chunk( com: chunk(
preDiscoveredIds.filter((id) => idToRegion(id) === "com"), preDiscoveredIds.filter((id) => idToRegion(id) === "com"),
PLAYER_IDS_PER_CALL PLAYER_IDS_PER_CALL,
), ),
eu: chunk( eu: chunk(
preDiscoveredIds.filter((id) => idToRegion(id) === "eu"), preDiscoveredIds.filter((id) => idToRegion(id) === "eu"),
PLAYER_IDS_PER_CALL PLAYER_IDS_PER_CALL,
), ),
}; };
let regionIndex = 0; let regionIndex = 0;
@@ -71,7 +69,7 @@ times(THREADS, async () => {
const regionIds = playerIds[region]; const regionIds = playerIds[region];
const [ids] = regionIds.splice( const [ids] = regionIds.splice(
Math.floor(Math.random() * regionIds.length), Math.floor(Math.random() * regionIds.length),
1 1,
); );
const accountInfo = await getAccountInfo(region, ids, undefined, { const accountInfo = await getAccountInfo(region, ids, undefined, {
fields: "last_battle_time,statistics.all.battles", fields: "last_battle_time,statistics.all.battles",
@@ -104,8 +102,8 @@ times(THREADS, async () => {
filteredIds.map((id) => filteredIds.map((id) =>
getTankStats(region, id, { getTankStats(region, id, {
fields: "last_battle_time,battle_life_time,all,tank_id", fields: "last_battle_time,battle_life_time,all,tank_id",
}) }),
) ),
); );
players.forEach((tanks) => { players.forEach((tanks) => {
@@ -114,7 +112,7 @@ times(THREADS, async () => {
if (!tankIds.includes(tank.tank_id)) { if (!tankIds.includes(tank.tank_id)) {
console.log( console.log(
`Found new tank: ${tank.tank_id} (${tankIds.length + 1})` `Found new tank: ${tank.tank_id} (${tankIds.length + 1})`,
); );
tankIds.push(tank.tank_id); tankIds.push(tank.tank_id);
tanksSorted[tank.tank_id] = []; tanksSorted[tank.tank_id] = [];
@@ -140,7 +138,7 @@ async function postWork() {
console.log( console.log(
`Generating statistics based on ${samples.d_120.toLocaleString()} players (${samples.total.toLocaleString()} checked in total) and ${ `Generating statistics based on ${samples.d_120.toLocaleString()} players (${samples.total.toLocaleString()} checked in total) and ${
tankIds.length tankIds.length
} tanks...` } tanks...`,
); );
using uploader = new AssetUploader("averages"); using uploader = new AssetUploader("averages");
@@ -152,7 +150,12 @@ async function postWork() {
// (tank) => Date.now() - tank.last_battle_time * 1000 <= MAX_ACTIVITY_TIME, // (tank) => Date.now() - tank.last_battle_time * 1000 <= MAX_ACTIVITY_TIME,
// ); // );
const tanks = tanksSortedEntry; const tanks = tanksSortedEntry;
const entry: AverageDefinitionsEntrySubPartial = { const entry: {
mu: Partial<Record<keyof AverageDefinitionsAllStats, number>>;
sigma: Partial<Record<keyof AverageDefinitionsAllStats, number>>;
r: Partial<Record<keyof AverageDefinitionsAllStats, number>>;
samples: Samples;
} = {
mu: {}, mu: {},
sigma: {}, sigma: {},
r: {}, r: {},
@@ -162,13 +165,13 @@ async function postWork() {
tanks.forEach((tank) => { tanks.forEach((tank) => {
const timeSinceLastActivity = Date.now() - tank.last_battle_time * 1000; const timeSinceLastActivity = Date.now() - tank.last_battle_time * 1000;
entry.samples.total++; entry.samples!.total++;
if (timeSinceLastActivity <= 120 * DAY) entry.samples.d_120++; if (timeSinceLastActivity <= 120 * DAY) entry.samples!.d_120++;
if (timeSinceLastActivity <= 90 * DAY) entry.samples.d_90++; if (timeSinceLastActivity <= 90 * DAY) entry.samples!.d_90++;
if (timeSinceLastActivity <= 60 * DAY) entry.samples.d_60++; if (timeSinceLastActivity <= 60 * DAY) entry.samples!.d_60++;
if (timeSinceLastActivity <= 30 * DAY) entry.samples.d_30++; if (timeSinceLastActivity <= 30 * DAY) entry.samples!.d_30++;
if (timeSinceLastActivity <= 7 * DAY) entry.samples.d_7++; if (timeSinceLastActivity <= 7 * DAY) entry.samples!.d_7++;
if (timeSinceLastActivity <= 1 * DAY) entry.samples.d_1++; if (timeSinceLastActivity <= 1 * DAY) entry.samples!.d_1++;
}); });
const dataWY = tanks.map((tank) => ({ const dataWY = tanks.map((tank) => ({
@@ -191,7 +194,7 @@ async function postWork() {
function sum(slicer: (data: DataPoint) => number) { function sum(slicer: (data: DataPoint) => number) {
return data.reduce( return data.reduce(
(accumulator, data) => accumulator + slicer(data), (accumulator, data) => accumulator + slicer(data),
0 0,
); );
} }
+99 -98
View File
@@ -81,16 +81,16 @@ const blitzTankFilterDefinitionCategoryToBlitzkit: Record<
BlitzTankFilterDefinitionCategory, BlitzTankFilterDefinitionCategory,
ConsumableTankCategoryFilterCategory ConsumableTankCategoryFilterCategory
> = { > = {
clip: ConsumableTankCategoryFilterCategory.CLIP, clip: ConsumableTankCategoryFilterCategory.CONSUMABLE_TANK_CATEGORY_FILTER_CATEGORY_CLIP,
}; };
function vector3TupleToBlitzkit(tuple: Vector3Tuple) { function vector3TupleToBlitzkit(tuple: Vector3Tuple) {
return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3; return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3;
} }
const blitzTankClassToBlitzkit: Record<BlitzTankClass, TankClass> = { const blitzTankClassToBlitzkit: Record<BlitzTankClass, TankClass> = {
lightTank: TankClass.LIGHT, lightTank: TankClass.TANK_CLASS_LIGHT,
"AT-SPG": TankClass.TANK_DESTROYER, "AT-SPG": TankClass.TANK_CLASS_TANK_DESTROYER,
heavyTank: TankClass.HEAVY, heavyTank: TankClass.TANK_CLASS_HEAVY,
mediumTank: TankClass.MEDIUM, mediumTank: TankClass.TANK_CLASS_MEDIUM,
}; };
export interface BlitzStrings { export interface BlitzStrings {
[key: string]: string; [key: string]: string;
@@ -149,18 +149,18 @@ type BlitzModuleType = {
type UnlocksListing = BlitzModuleType | undefined; type UnlocksListing = BlitzModuleType | undefined;
type BlitzCrewType = "commander" | "radioman" | "gunner" | "driver" | "loader"; type BlitzCrewType = "commander" | "radioman" | "gunner" | "driver" | "loader";
const blitzCrewTypeToBlitzkit: Record<BlitzCrewType, CrewType> = { const blitzCrewTypeToBlitzkit: Record<BlitzCrewType, CrewType> = {
commander: CrewType.COMMANDER, commander: CrewType.CREW_TYPE_COMMANDER,
driver: CrewType.DRIVER, driver: CrewType.CREW_TYPE_DRIVER,
gunner: CrewType.GUNNER, gunner: CrewType.CREW_TYPE_GUNNER,
loader: CrewType.LOADER, loader: CrewType.CREW_TYPE_LOADER,
radioman: CrewType.RADIOMAN, radioman: CrewType.CREW_TYPE_RADIOMAN,
}; };
const blitzkitCrewTypeToBlitz: Record<CrewType, BlitzCrewType> = { const blitzkitCrewTypeToBlitz: Record<CrewType, BlitzCrewType> = {
[CrewType.COMMANDER]: "commander", [CrewType.CREW_TYPE_COMMANDER]: "commander",
[CrewType.DRIVER]: "driver", [CrewType.CREW_TYPE_DRIVER]: "driver",
[CrewType.GUNNER]: "gunner", [CrewType.CREW_TYPE_GUNNER]: "gunner",
[CrewType.LOADER]: "loader", [CrewType.CREW_TYPE_LOADER]: "loader",
[CrewType.RADIOMAN]: "radioman", [CrewType.CREW_TYPE_RADIOMAN]: "radioman",
}; };
interface VehicleDefinitions { interface VehicleDefinitions {
invisibility: { invisibility: {
@@ -298,7 +298,7 @@ interface VehicleDefinitions {
type: "default"; type: "default";
enabled: false; enabled: false;
factorsModifiers: ""; factorsModifiers: "";
} },
]; ];
}; };
}; };
@@ -517,18 +517,18 @@ type CombatRolesYaml = Record<
>; >;
const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = { const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
ARMOR_PIERCING: ShellType.AP, ARMOR_PIERCING: ShellType.SHELL_TYPE_AP,
ARMOR_PIERCING_CR: ShellType.APCR, ARMOR_PIERCING_CR: ShellType.SHELL_TYPE_APCR,
HIGH_EXPLOSIVE: ShellType.HE, HIGH_EXPLOSIVE: ShellType.SHELL_TYPE_HE,
HOLLOW_CHARGE: ShellType.HEAT, HOLLOW_CHARGE: ShellType.SHELL_TYPE_HEAT,
}; };
export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/; export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/;
const blitzModuleTypeToBlitzkit: Record<keyof BlitzModuleType, ModuleType> = { const blitzModuleTypeToBlitzkit: Record<keyof BlitzModuleType, ModuleType> = {
chassis: ModuleType.TRACKS, chassis: ModuleType.MODULE_TYPE_TRACKS,
engine: ModuleType.ENGINE, engine: ModuleType.MODULE_TYPE_ENGINE,
gun: ModuleType.GUN, gun: ModuleType.MODULE_TYPE_GUN,
turret: ModuleType.TURRET, turret: ModuleType.MODULE_TYPE_TURRET,
vehicle: ModuleType.VEHICLE, vehicle: ModuleType.MODULE_TYPE_VEHICLE,
}; };
export async function definitions() { export async function definitions() {
@@ -555,10 +555,10 @@ export async function definitions() {
const provisionDefinitions: ProvisionDefinitions = { provisions: {} }; const provisionDefinitions: ProvisionDefinitions = { provisions: {} };
const skillDefinitions: SkillDefinitions = { const skillDefinitions: SkillDefinitions = {
classes: { classes: {
[TankClass.LIGHT]: { skills: [] }, [TankClass.TANK_CLASS_LIGHT]: { skills: [] },
[TankClass.MEDIUM]: { skills: [] }, [TankClass.TANK_CLASS_MEDIUM]: { skills: [] },
[TankClass.HEAVY]: { skills: [] }, [TankClass.TANK_CLASS_HEAVY]: { skills: [] },
[TankClass.TANK_DESTROYER]: { skills: [] }, [TankClass.TANK_CLASS_TANK_DESTROYER]: { skills: [] },
}, },
}; };
const nations = vfs const nations = vfs
@@ -566,7 +566,7 @@ export async function definitions() {
.filter((nation) => nation !== "common"); .filter((nation) => nation !== "common");
const tankStringIdMap: Record<string, number> = {}; const tankStringIdMap: Record<string, number> = {};
const optionalDevices = await vfs.xml<{ root: OptionalDevices }>( const optionalDevices = await vfs.xml<{ root: OptionalDevices }>(
"Data/XML/item_defs/vehicles/common/optional_devices.xml" "Data/XML/item_defs/vehicles/common/optional_devices.xml",
); );
const stringsI18n: Record<string, Record<string, string>> = {}; const stringsI18n: Record<string, Record<string, string>> = {};
@@ -574,12 +574,12 @@ export async function definitions() {
locales.supported.map(async ({ locale }) => { locales.supported.map(async ({ locale }) => {
const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale]; const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale];
const cache = await fetch( const cache = await fetch(
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml` `https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`,
) )
.then((response) => response.text()) .then((response) => response.text())
.then((string) => parseYaml(string) as BlitzStrings); .then((string) => parseYaml(string) as BlitzStrings);
const preInstalled = await vfs.yaml<BlitzStrings>( const preInstalled = await vfs.yaml<BlitzStrings>(
`Data/Strings/${blitzLocale}.yaml` `Data/Strings/${blitzLocale}.yaml`,
); );
stringsI18n[locale] = { stringsI18n[locale] = {
@@ -588,7 +588,7 @@ export async function definitions() {
}; };
console.log(`Loaded strings for ${locale}`); console.log(`Loaded strings for ${locale}`);
}) }),
); );
function getString(name: string) { function getString(name: string) {
@@ -627,9 +627,9 @@ export async function definitions() {
consumables, consumables,
( (
await vfs.xml<{ root: ConsumablesCommon }>( await vfs.xml<{ root: ConsumablesCommon }>(
`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml` `Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`,
) )
).root ).root,
); );
} }
@@ -642,39 +642,39 @@ export async function definitions() {
provisions, provisions,
( (
await vfs.xml<{ root: ConsumablesCommon }>( await vfs.xml<{ root: ConsumablesCommon }>(
`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml` `Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`,
) )
).root ).root,
); );
} }
const avatar = await vfs.xml<{ root: Avatar }>( const avatar = await vfs.xml<{ root: Avatar }>(
`Data/XML/item_defs/tankmen/avatar.xml` `Data/XML/item_defs/tankmen/avatar.xml`,
); );
const maps = await vfs.yaml<Maps>(`Data/maps.yaml`); const maps = await vfs.yaml<Maps>(`Data/maps.yaml`);
const tankXps = new Map<number, ResearchCost>(); const tankXps = new Map<number, ResearchCost>();
const camouflagesXml = await vfs.xml<{ root: CamouflagesXML }>( const camouflagesXml = await vfs.xml<{ root: CamouflagesXML }>(
`Data/XML/item_defs/vehicles/common/camouflages.xml` `Data/XML/item_defs/vehicles/common/camouflages.xml`,
); );
const camouflagesYaml = await vfs.yaml<CamouflagesYaml>( const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
`Data/camouflages.yaml` `Data/camouflages.yaml`,
); );
const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages); const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages);
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>( const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml` `Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`,
); );
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>( const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml` `Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`,
); );
const gameModeNativeNames: Record<string, number> = {}; const gameModeNativeNames: Record<string, number> = {};
const combatRoles = await vfs.yaml<CombatRolesYaml>( const combatRoles = await vfs.yaml<CombatRolesYaml>(
`Data/XML/item_defs/vehicles/common/combat_roles.yaml` `Data/XML/item_defs/vehicles/common/combat_roles.yaml`,
); );
const consumableNativeNames: Record<string, number> = {}; const consumableNativeNames: Record<string, number> = {};
const provisionNativeNames: Record<string, number> = {}; const provisionNativeNames: Record<string, number> = {};
for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g /"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g,
)) { )) {
const id = Number(match[1]); const id = Number(match[1]);
const name = getString(match[2]); const name = getString(match[2]);
@@ -686,7 +686,7 @@ export async function definitions() {
} }
for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/eGameMode\.([a-zA-Z]+) -> "~res:\/Gfx\/UI\/Hangar\/GameTypes\/battle-type_([^"]+)"/g /eGameMode\.([a-zA-Z]+) -> "~res:\/Gfx\/UI\/Hangar\/GameTypes\/battle-type_([^"]+)"/g,
)) { )) {
Object.entries(gameModeNativeNames).forEach(([key, value]) => { Object.entries(gameModeNativeNames).forEach(([key, value]) => {
if (key.toLowerCase() === match[2].toLowerCase()) { if (key.toLowerCase() === match[2].toLowerCase()) {
@@ -720,7 +720,7 @@ export async function definitions() {
for (const nation of nations) { for (const nation of nations) {
const tankList = await vfs.xml<{ root: VehicleDefinitionList }>( const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml` `Data/XML/item_defs/vehicles/${nation}/list.xml`,
); );
for (const tankKey in tankList.root) { for (const tankKey in tankList.root) {
@@ -761,7 +761,7 @@ export async function definitions() {
console.warn( console.warn(
`Multiple tanks share slug ${slug}: ${requesters `Multiple tanks share slug ${slug}: ${requesters
.map(({ key }) => key) .map(({ key }) => key)
.join(", ")}` .join(", ")}`,
); );
if (requesters.length !== 2) { if (requesters.length !== 2) {
@@ -791,7 +791,7 @@ export async function definitions() {
if (canonical === undefined) { if (canonical === undefined) {
throw new Error( throw new Error(
"Two tutorial bots share the same slug? The world is truly broken." "Two tutorial bots share the same slug? The world is truly broken.",
); );
} }
@@ -804,7 +804,7 @@ export async function definitions() {
for (const nation of nations) { for (const nation of nations) {
const tankList = await vfs.xml<{ root: VehicleDefinitionList }>( const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml` `Data/XML/item_defs/vehicles/${nation}/list.xml`,
); );
const turretList = await vfs.xml<{ const turretList = await vfs.xml<{
root: TurretDefinitionsList; root: TurretDefinitionsList;
@@ -857,7 +857,7 @@ export async function definitions() {
: Number(item.cost.split(":")[1]), : Number(item.cost.split(":")[1]),
}, },
} satisfies Unlock; } satisfies Unlock;
}) }),
) )
.flat(); .flat();
} }
@@ -872,10 +872,10 @@ export async function definitions() {
const tank = tankList.root[tankKey]; const tank = tankList.root[tankKey];
let tankPrice: TankPrice; let tankPrice: TankPrice;
const tankDefinition = await vfs.xml<{ root: VehicleDefinitions }>( const tankDefinition = await vfs.xml<{ root: VehicleDefinitions }>(
`Data/XML/item_defs/vehicles/${nation}/${tankKey}.xml` `Data/XML/item_defs/vehicles/${nation}/${tankKey}.xml`,
); );
const tankParameters = await vfs.yaml<TankParameters>( const tankParameters = await vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml` `Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
); );
const turretOrigin = tankDefinition.root.hull.turretPositions.turret const turretOrigin = tankDefinition.root.hull.turretPositions.turret
.split(" ") .split(" ")
@@ -890,23 +890,23 @@ export async function definitions() {
if (slug === undefined) { if (slug === undefined) {
throw new Error( throw new Error(
`Could not find slug for ${nation}/${tankKey} (${tankId})` `Could not find slug for ${nation}/${tankKey} (${tankId})`,
); );
} }
if (tank.sellPrice) { if (tank.sellPrice) {
tankPrice = { tankPrice = {
type: TankPriceType.GOLD, type: TankPriceType.TANK_PRICE_TYPE_GOLD,
value: tank.sellPrice["#text"] * 2, value: tank.sellPrice["#text"] * 2,
}; };
} else if (typeof tank.price === "number") { } else if (typeof tank.price === "number") {
tankPrice = { tankPrice = {
type: TankPriceType.CREDITS, type: TankPriceType.TANK_PRICE_TYPE_CREDITS,
value: tank.price, value: tank.price,
}; };
} else { } else {
tankPrice = { tankPrice = {
type: TankPriceType.CREDITS, type: TankPriceType.TANK_PRICE_TYPE_CREDITS,
value: tank.price["#text"] * 400, value: tank.price["#text"] * 400,
}; };
} }
@@ -938,7 +938,7 @@ export async function definitions() {
Object.entries(tankDefinition.root.crew).forEach(([crewKey, value]) => { Object.entries(tankDefinition.root.crew).forEach(([crewKey, value]) => {
let entry: Crew; let entry: Crew;
const index = crew.findIndex( const index = crew.findIndex(
({ type }) => blitzkitCrewTypeToBlitz[type] === crewKey ({ type }) => blitzkitCrewTypeToBlitz[type] === crewKey,
); );
if (index === -1) { if (index === -1) {
if (crewKey === "#text") return; if (crewKey === "#text") return;
@@ -992,6 +992,7 @@ export async function definitions() {
ancestors: [], ancestors: [],
successors: [], successors: [],
id: tankId, id: tankId,
dev_name: tankKey,
roles: {}, roles: {},
camouflages: camouflages, camouflages: camouflages,
fixed_camouflage: fixedCamouflage, fixed_camouflage: fixedCamouflage,
@@ -1011,10 +1012,10 @@ export async function definitions() {
slug, slug,
nation, nation,
type: tankTags.includes("collectible") type: tankTags.includes("collectible")
? TankType.COLLECTOR ? TankType.TANK_TYPE_COLLECTOR
: (typeof tank.price === "number" ? false : "gold" in tank.price) : (typeof tank.price === "number" ? false : "gold" in tank.price)
? TankType.PREMIUM ? TankType.TANK_TYPE_PREMIUM
: TankType.RESEARCHABLE, : TankType.TANK_TYPE_RESEARCHABLE,
tier: tank.level, tier: tank.level,
class: blitzTankClassToBlitzkit[tankTags[0] as BlitzTankClass], class: blitzTankClassToBlitzkit[tankTags[0] as BlitzTankClass],
testing: tankTags.includes("testTank"), testing: tankTags.includes("testTank"),
@@ -1022,7 +1023,7 @@ export async function definitions() {
price: tankPrice, price: tankPrice,
camouflage_still: tankDefinition.root.invisibility.still, camouflage_still: tankDefinition.root.invisibility.still,
camouflage_moving: tankDefinition.root.invisibility.moving, camouflage_moving: tankDefinition.root.invisibility.moving,
camouflage_onFire: tankDefinition.root.invisibility.firePenalty, camouflage_on_fire: tankDefinition.root.invisibility.firePenalty,
turrets: [], turrets: [],
engines: [], engines: [],
tracks: [], tracks: [],
@@ -1031,12 +1032,12 @@ export async function definitions() {
if (tank.combatRole) { if (tank.combatRole) {
Object.entries(tank.combatRole).forEach(([gameMode, role]) => { Object.entries(tank.combatRole).forEach(([gameMode, role]) => {
const id = Object.entries(gameModeNativeNames).find( const id = Object.entries(gameModeNativeNames).find(
([key]) => key.toLowerCase() === gameMode.toLowerCase() ([key]) => key.toLowerCase() === gameMode.toLowerCase(),
)?.[1]; )?.[1];
if (id === undefined) { if (id === undefined) {
throw new Error( throw new Error(
`Unknown game mode in tank ${tankKey}: ${gameMode}` `Unknown game mode in tank ${tankKey}: ${gameMode}`,
); );
} }
@@ -1118,7 +1119,7 @@ export async function definitions() {
(turretKey, turretIndex) => { (turretKey, turretIndex) => {
const turret = tankDefinition.root.turrets0[turretKey]; const turret = tankDefinition.root.turrets0[turretKey];
const turretModel = Number( const turretModel = Number(
parsePath(turret.models.undamaged).name.split("_")[1] parsePath(turret.models.undamaged).name.split("_")[1],
); );
const turretId = toUniqueId(nation, turretList.root.ids[turretKey]); const turretId = toUniqueId(nation, turretList.root.ids[turretKey]);
const turretYaw = ( const turretYaw = (
@@ -1186,12 +1187,12 @@ export async function definitions() {
min: vector3TupleToBlitzkit( min: vector3TupleToBlitzkit(
tankParameters.collision[ tankParameters.collision[
parsePath(turret.hitTester.collisionModel).name.toLowerCase() parsePath(turret.hitTester.collisionModel).name.toLowerCase()
].bbox.min ].bbox.min,
), ),
max: vector3TupleToBlitzkit( max: vector3TupleToBlitzkit(
tankParameters.collision[ tankParameters.collision[
parsePath(turret.hitTester.collisionModel).name.toLowerCase() parsePath(turret.hitTester.collisionModel).name.toLowerCase()
].bbox.max ].bbox.max,
), ),
}, },
}; };
@@ -1209,7 +1210,7 @@ export async function definitions() {
.split(" ") .split(" ")
.map(Number) as [number, number]; .map(Number) as [number, number];
const gunModel = Number( const gunModel = Number(
parsePath(gun.models.undamaged).name.split("_")[1] parsePath(gun.models.undamaged).name.split("_")[1],
); );
const gunName = getString(gunListEntry.userString); const gunName = getString(gunListEntry.userString);
const gunType = const gunType =
@@ -1332,24 +1333,24 @@ export async function definitions() {
}, },
} }
: gunType === "autoReloader" : gunType === "autoReloader"
? { ? {
$case: "auto_reloader", $case: "auto_reloader",
value: { value: {
intra_clip: 60 / gun.clip!.rate, intra_clip: 60 / gun.clip!.rate,
shell_count: gunClipCount, shell_count: gunClipCount,
shell_reloads: gun shell_reloads: gun
.pumpGunReloadTimes!.split(" ") .pumpGunReloadTimes!.split(" ")
.map(Number), .map(Number),
},
}
: {
$case: "auto_loader",
value: {
intra_clip: 60 / gun.clip!.rate,
clip_reload: gun.reloadTime,
shell_count: gunClipCount,
},
}, },
}
: {
$case: "auto_loader",
value: {
intra_clip: 60 / gun.clip!.rate,
clip_reload: gun.reloadTime,
shell_count: gunClipCount,
},
},
} satisfies GunDefinition); } satisfies GunDefinition);
modelDefinitions.models[tankId].turrets[turretId].guns[gunId] = { modelDefinitions.models[tankId].turrets[turretId].guns[gunId] = {
@@ -1360,8 +1361,8 @@ export async function definitions() {
gun.armor.gun === undefined gun.armor.gun === undefined
? 0 ? 0
: typeof gun.armor.gun === "number" : typeof gun.armor.gun === "number"
? gun.armor.gun ? gun.armor.gun
: gun.armor.gun["#text"], : gun.armor.gun["#text"],
pitch: { pitch: {
min: gunPitch[0], min: gunPitch[0],
max: gunPitch[1], max: gunPitch[1],
@@ -1408,7 +1409,7 @@ export async function definitions() {
type: blitzShellKindToBlitzkit[shell.kind], type: blitzShellKindToBlitzkit[shell.kind],
explosion_radius: explosion_radius:
shell.kind === "HIGH_EXPLOSIVE" shell.kind === "HIGH_EXPLOSIVE"
? shell.explosionRadius ?? 0 ? (shell.explosionRadius ?? 0)
: undefined, : undefined,
icon: shell.icon, icon: shell.icon,
penetration: { penetration: {
@@ -1419,7 +1420,7 @@ export async function definitions() {
}); });
}); });
}); });
} },
); );
totalUnlocks.forEach((unlocks) => { totalUnlocks.forEach((unlocks) => {
@@ -1447,7 +1448,7 @@ export async function definitions() {
case "gun": { case "gun": {
gunXps.set( gunXps.set(
toUniqueId(nation, gunList.root.ids[vehicle["#text"]]), toUniqueId(nation, gunList.root.ids[vehicle["#text"]]),
parseResearchCost(vehicle.cost) parseResearchCost(vehicle.cost),
); );
break; break;
} }
@@ -1455,7 +1456,7 @@ export async function definitions() {
case "turret": { case "turret": {
turretXps.set( turretXps.set(
toUniqueId(nation, turretList.root.ids[vehicle["#text"]]), toUniqueId(nation, turretList.root.ids[vehicle["#text"]]),
parseResearchCost(vehicle.cost) parseResearchCost(vehicle.cost),
); );
break; break;
} }
@@ -1463,7 +1464,7 @@ export async function definitions() {
case "engine": { case "engine": {
engineXps.set( engineXps.set(
toUniqueId(nation, enginesList.root.ids[vehicle["#text"]]), toUniqueId(nation, enginesList.root.ids[vehicle["#text"]]),
parseResearchCost(vehicle.cost) parseResearchCost(vehicle.cost),
); );
break; break;
} }
@@ -1471,7 +1472,7 @@ export async function definitions() {
case "chassis": { case "chassis": {
trackXps.set( trackXps.set(
toUniqueId(nation, chassisList.root.ids[vehicle["#text"]]), toUniqueId(nation, chassisList.root.ids[vehicle["#text"]]),
parseResearchCost(vehicle.cost) parseResearchCost(vehicle.cost),
); );
break; break;
} }
@@ -1518,7 +1519,7 @@ export async function definitions() {
name: getString(optionalDeviceEntry.userString), name: getString(optionalDeviceEntry.userString),
description: getString(optionalDeviceEntry.description), description: getString(optionalDeviceEntry.description),
}; };
} },
); );
Object.entries(optionalDeviceSlots.root.presets).forEach( Object.entries(optionalDeviceSlots.root.presets).forEach(
@@ -1537,7 +1538,7 @@ export async function definitions() {
}) })
.flat(), .flat(),
}; };
} },
); );
Object.entries(consumables).forEach(([key, consumable]) => { Object.entries(consumables).forEach(([key, consumable]) => {
@@ -1620,7 +1621,7 @@ export async function definitions() {
(item) => (item) =>
blitzTankFilterDefinitionCategoryToBlitzkit[ blitzTankFilterDefinitionCategoryToBlitzkit[
item as BlitzTankFilterDefinitionCategory item as BlitzTankFilterDefinitionCategory
] ],
), ),
}, },
}, },
@@ -1718,7 +1719,7 @@ export async function definitions() {
(item) => (item) =>
blitzTankFilterDefinitionCategoryToBlitzkit[ blitzTankFilterDefinitionCategoryToBlitzkit[
item as BlitzTankFilterDefinitionCategory item as BlitzTankFilterDefinitionCategory
] ],
), ),
}, },
}, },
@@ -1749,11 +1750,11 @@ export async function definitions() {
value.default_abilities.forEach((ability) => { value.default_abilities.forEach((ability) => {
if (ability in consumableNativeNames) { if (ability in consumableNativeNames) {
gameDefinitions.roles[value.id].consumables.push( gameDefinitions.roles[value.id].consumables.push(
consumableNativeNames[ability] consumableNativeNames[ability],
); );
} else if (ability in provisionNativeNames) { } else if (ability in provisionNativeNames) {
gameDefinitions.roles[value.id].provisions.push( gameDefinitions.roles[value.id].provisions.push(
provisionNativeNames[ability] provisionNativeNames[ability],
); );
} else throw new Error(`Unknown ability ${ability}`); } else throw new Error(`Unknown ability ${ability}`);
}); });
+6 -6
View File
@@ -15,7 +15,7 @@ export async function glossary() {
const glossary = await fetchGlossary(supported.blitz ?? supported.locale); const glossary = await fetchGlossary(supported.blitz ?? supported.locale);
console.log( console.log(
`Found ${Object.keys(glossary).length} things for ${supported.locale}` `Found ${Object.keys(glossary).length} things for ${supported.locale}`,
); );
for (const key in glossary) { for (const key in glossary) {
@@ -30,7 +30,7 @@ export async function glossary() {
} }
if (key in avatars) { if (key in avatars) {
avatars[key].avatar.name.locales[supported.locale] = avatars[key].avatar.name!.locales[supported.locale] =
glossaryEntry.title; glossaryEntry.title;
} else { } else {
const extension = extname(glossaryEntry.image_url); const extension = extname(glossaryEntry.image_url);
@@ -45,7 +45,7 @@ export async function glossary() {
}; };
} }
} }
}) }),
); );
const gallery: Gallery = { avatars: [] }; const gallery: Gallery = { avatars: [] };
@@ -53,12 +53,12 @@ export async function glossary() {
const bar = new ProgressBar( const bar = new ProgressBar(
`Fetching avatars of ${totalCount} things :bar`, `Fetching avatars of ${totalCount} things :bar`,
totalCount totalCount,
); );
await Promise.all( await Promise.all(
Object.entries(avatars).map(async ([key, { url, avatar }]) => { Object.entries(avatars).map(async ([key, { url, avatar }]) => {
if (!(locales.default in avatars[key].avatar.name.locales)) { if (!(locales.default in avatars[key].avatar.name!.locales)) {
console.warn(`Avatar ${key} has no ${locales.default} name`); console.warn(`Avatar ${key} has no ${locales.default} name`);
bar.tick(); bar.tick();
@@ -77,7 +77,7 @@ export async function glossary() {
}); });
bar.tick(); bar.tick();
}) }),
); );
await uploader.add({ await uploader.add({
+4 -4
View File
@@ -1,5 +1,5 @@
import { crc32 } from 'crc'; import { crc32 } from "crc";
import { compressBlock } from 'lz4js'; import { compressBlock } from "lz4js";
/** /**
* Thanks Maddoxkkm! Modernized heavily. * Thanks Maddoxkkm! Modernized heavily.
@@ -14,7 +14,7 @@ export function writeDVPL(buffer: Buffer) {
destination, destination,
0, 0,
source.length, source.length,
0, [0],
); );
let output = Buffer.from(destination); let output = Buffer.from(destination);
@@ -58,7 +58,7 @@ function toDVPLFooter(
result.writeUInt32LE(compressedSize, 4); result.writeUInt32LE(compressedSize, 4);
result.writeUInt32LE(crc32, 8); result.writeUInt32LE(crc32, 8);
result.writeUInt32LE(type, 12); result.writeUInt32LE(type, 12);
result.write('DVPL', 16, 4, 'utf8'); result.write("DVPL", 16, 4, "utf8");
return result; return result;
} }
+3 -2
View File
@@ -38,7 +38,7 @@ export class SteamVFS extends AbstractVFS {
private username: string, private username: string,
private password: string, private password: string,
private app: number, private app: number,
private depot: number private depot: number,
) { ) {
super(); super();
} }
@@ -57,13 +57,14 @@ export class SteamVFS extends AbstractVFS {
this.steam.getManifest( this.steam.getManifest(
this.app, this.app,
this.depot, this.depot,
// @ts-expect-error
productInfo.apps[this.app].appinfo.depots[this.depot].manifests.public productInfo.apps[this.app].appinfo.depots[this.depot].manifests.public
.gid, .gid,
"public", "public",
// @ts-expect-error // @ts-expect-error
(_, response) => { (_, response) => {
resolve(response); resolve(response);
} },
); );
}); });
+32 -30
View File
@@ -53,7 +53,7 @@ if (newTanks.length > 0) {
notes += "## New Tanks\n\n"; notes += "## New Tanks\n\n";
for (const tank of newTanks) { for (const tank of newTanks) {
notes += `### [${tank.name.locales.en}](${domain}/tanks/${tank.slug})\n`; notes += `### [${tank.name!.locales.en}](${domain}/tanks/${tank.slug})\n`;
notes += `![](${tankIcon(tank.id)})\n\n`; notes += `![](${tankIcon(tank.id)})\n\n`;
} }
@@ -96,49 +96,49 @@ if (changedTanks.length > 0) {
const previewTankModel = previewModelDefinitions.models[preview.id]; const previewTankModel = previewModelDefinitions.models[preview.id];
const currentConsumables = Object.values( const currentConsumables = Object.values(
currentConsumableDefinitions.consumables currentConsumableDefinitions.consumables,
).filter((consumable) => ).filter((consumable) =>
checkConsumableProvisionInclusivity(consumable, current, currentGun) checkConsumableProvisionInclusivity(consumable, current, currentGun),
); );
const previewConsumables = Object.values( const previewConsumables = Object.values(
previewConsumableDefinitions.consumables previewConsumableDefinitions.consumables,
).filter((consumable) => ).filter((consumable) =>
checkConsumableProvisionInclusivity(consumable, preview, previewGun) checkConsumableProvisionInclusivity(consumable, preview, previewGun),
); );
const addedConsumables = previewConsumables.filter( const addedConsumables = previewConsumables.filter(
(previewConsumable) => (previewConsumable) =>
!currentConsumables.some( !currentConsumables.some(
(currentConsumable) => currentConsumable.id === previewConsumable.id (currentConsumable) => currentConsumable.id === previewConsumable.id,
) ),
); );
const removedConsumables = currentConsumables.filter( const removedConsumables = currentConsumables.filter(
(currentConsumable) => (currentConsumable) =>
!previewConsumables.some( !previewConsumables.some(
(previewConsumable) => previewConsumable.id === currentConsumable.id (previewConsumable) => previewConsumable.id === currentConsumable.id,
) ),
); );
const currentProvisions = availableProvisions( const currentProvisions = availableProvisions(
current, current,
currentGun, currentGun,
currentProvisionDefinitions currentProvisionDefinitions,
); );
const previewProvisions = availableProvisions( const previewProvisions = availableProvisions(
preview, preview,
previewGun, previewGun,
previewProvisionDefinitions previewProvisionDefinitions,
); );
const addedProvisions = previewProvisions.filter( const addedProvisions = previewProvisions.filter(
(previewProvision) => (previewProvision) =>
!currentProvisions.some( !currentProvisions.some(
(currentProvision) => currentProvision.id === previewProvision.id (currentProvision) => currentProvision.id === previewProvision.id,
) ),
); );
const removedProvisions = currentProvisions.filter( const removedProvisions = currentProvisions.filter(
(currentProvision) => (currentProvision) =>
!previewProvisions.some( !previewProvisions.some(
(previewProvision) => previewProvision.id === currentProvision.id (previewProvision) => previewProvision.id === currentProvision.id,
) ),
); );
const currentEquipment = currentEquipmentDefinitions.presets[ const currentEquipment = currentEquipmentDefinitions.presets[
@@ -155,16 +155,16 @@ if (changedTanks.length > 0) {
.filter( .filter(
(previewEquipment) => (previewEquipment) =>
!currentEquipment.some( !currentEquipment.some(
(currentEquipment) => currentEquipment === previewEquipment (currentEquipment) => currentEquipment === previewEquipment,
) ),
) )
.map((id) => previewEquipmentDefinitions.equipments[id]); .map((id) => previewEquipmentDefinitions.equipments[id]);
const removedEquipment = currentEquipment const removedEquipment = currentEquipment
.filter( .filter(
(currentEquipment) => (currentEquipment) =>
!previewEquipment.some( !previewEquipment.some(
(previewEquipment) => previewEquipment === currentEquipment (previewEquipment) => previewEquipment === currentEquipment,
) ),
) )
.map((id) => currentEquipmentDefinitions.equipments[id]); .map((id) => currentEquipmentDefinitions.equipments[id]);
@@ -184,7 +184,7 @@ if (changedTanks.length > 0) {
provisions: createDefaultProvisions( provisions: createDefaultProvisions(
current, current,
currentGun, currentGun,
currentProvisionDefinitions currentProvisionDefinitions,
), ),
equipmentMatrix: genericDefaultEquipmentMatrix, equipmentMatrix: genericDefaultEquipmentMatrix,
stockEngine: current.engines[0], stockEngine: current.engines[0],
@@ -198,7 +198,7 @@ if (changedTanks.length > 0) {
equipmentDefinitions: currentEquipmentDefinitions, equipmentDefinitions: currentEquipmentDefinitions,
provisionDefinitions: currentProvisionDefinitions, provisionDefinitions: currentProvisionDefinitions,
tankModelDefinition: currentTankModel, tankModelDefinition: currentTankModel,
} },
); );
const previewCharacteristics = tankCharacteristics( const previewCharacteristics = tankCharacteristics(
{ {
@@ -216,7 +216,7 @@ if (changedTanks.length > 0) {
provisions: createDefaultProvisions( provisions: createDefaultProvisions(
preview, preview,
previewGun, previewGun,
previewProvisionDefinitions previewProvisionDefinitions,
), ),
equipmentMatrix: genericDefaultEquipmentMatrix, equipmentMatrix: genericDefaultEquipmentMatrix,
stockEngine: preview.engines[0], stockEngine: preview.engines[0],
@@ -230,7 +230,7 @@ if (changedTanks.length > 0) {
equipmentDefinitions: previewEquipmentDefinitions, equipmentDefinitions: previewEquipmentDefinitions,
provisionDefinitions: previewProvisionDefinitions, provisionDefinitions: previewProvisionDefinitions,
tankModelDefinition: previewTankModel, tankModelDefinition: previewTankModel,
} },
); );
const changedCharacteristics: string[] = []; const changedCharacteristics: string[] = [];
@@ -244,17 +244,17 @@ if (changedTanks.length > 0) {
} }
} }
notes += `### [${preview.name.locales.en}](${domain}/tanks/${preview.slug})\n`; notes += `### [${preview.name!.locales.en}](${domain}/tanks/${preview.slug})\n`;
notes += `![](${tankIcon(preview.id)})\n\n`; notes += `![](${tankIcon(preview.id)})\n\n`;
if (addedConsumables.length + removedConsumables.length > 0) { if (addedConsumables.length + removedConsumables.length > 0) {
notes += `Consumables: `; notes += `Consumables: `;
notes += [ notes += [
...addedConsumables.map( ...addedConsumables.map(
(consumable) => `🟢 ${consumable.name.locales.en}` (consumable) => `🟢 ${consumable.name!.locales.en}`,
), ),
...removedConsumables.map( ...removedConsumables.map(
(consumable) => `🔴 ${consumable.name.locales.en}` (consumable) => `🔴 ${consumable.name!.locales.en}`,
), ),
].join(", "); ].join(", ");
notes += `\n`; notes += `\n`;
@@ -264,10 +264,10 @@ if (changedTanks.length > 0) {
notes += `Provisions: `; notes += `Provisions: `;
notes += [ notes += [
...addedProvisions.map( ...addedProvisions.map(
(provision) => `🟢 ${provision.name.locales.en}` (provision) => `🟢 ${provision.name!.locales.en}`,
), ),
...removedProvisions.map( ...removedProvisions.map(
(provision) => `🔴 ${provision.name.locales.en}` (provision) => `🔴 ${provision.name!.locales.en}`,
), ),
].join(", "); ].join(", ");
notes += `\n`; notes += `\n`;
@@ -276,9 +276,11 @@ if (changedTanks.length > 0) {
if (addedEquipment.length + removedEquipment.length > 0) { if (addedEquipment.length + removedEquipment.length > 0) {
notes += `-Equipment: `; notes += `-Equipment: `;
notes += [ notes += [
...addedEquipment.map((equipment) => `🟢 ${equipment.name.locales.en}`), ...addedEquipment.map(
(equipment) => `🟢 ${equipment.name!.locales.en}`,
),
...removedEquipment.map( ...removedEquipment.map(
(equipment) => `🔴 ${equipment.name.locales.en}` (equipment) => `🔴 ${equipment.name!.locales.en}`,
), ),
].join(", "); ].join(", ");
notes += `\n`; notes += `\n`;
+2 -2
View File
@@ -43,7 +43,7 @@ for (const tank of tanksSanitized) {
} }
try { try {
const q = `World of Tanks Blitz ${tank.name.locales[locales.default]}`; const q = `World of Tanks Blitz ${tank.name!.locales[locales.default]}`;
const results = await youtube.search.list({ const results = await youtube.search.list({
part: ["snippet"], part: ["snippet"],
@@ -74,7 +74,7 @@ for (const tank of tanksSanitized) {
}; };
console.log( console.log(
`${++done} done; found ${items.length} for ${tank.name.locales.en}`, `${++done} done; found ${items.length} for ${tank.name!.locales.en}`,
); );
await new Promise((resolve) => setTimeout(resolve, 1000 / 5)); await new Promise((resolve) => setTimeout(resolve, 1000 / 5));
@@ -1,11 +1,5 @@
import { ReloadIcon } from "@radix-ui/react-icons"; import { ReloadIcon } from "@radix-ui/react-icons";
import { import { Card, Flex, IconButton, Text, TextField } from "@radix-ui/themes";
Card,
Flex,
IconButton,
Text,
TextField,
} from "@radix-ui/themes";
import { Html } from "@react-three/drei"; import { Html } from "@react-three/drei";
import { useRef } from "react"; import { useRef } from "react";
import { radToDeg } from "three/src/math/MathUtils.js"; import { radToDeg } from "three/src/math/MathUtils.js";
@@ -107,12 +101,12 @@ export function ArmorPlateDisplay() {
const track = tank.tracks[protagonist.track.id]; const track = tank.tracks[protagonist.track.id];
if (highlightArmor.name.startsWith("hull_")) { if (highlightArmor.name.startsWith("hull_")) {
tank.armor.thickness[index] = thickness; tank.armor!.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith("turret_")) { } else if (highlightArmor.name.startsWith("turret_")) {
turret.armor.thickness[index] = thickness; turret.armor!.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith("gun_")) { } else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) { if (highlightArmor.name.includes("_armor_")) {
gun.armor.thickness[index] = thickness; gun.armor!.thickness[index] = thickness;
} else { } else {
gun.thickness = thickness; gun.thickness = thickness;
} }
@@ -162,15 +156,15 @@ export function ArmorPlateDisplay() {
initialTank.tracks[protagonist.track.id]; initialTank.tracks[protagonist.track.id];
if (highlightArmor.name.startsWith("hull_")) { if (highlightArmor.name.startsWith("hull_")) {
tank.armor.thickness[index] = tank.armor!.thickness[index] =
initialTank.armor.thickness[index]; initialTank.armor!.thickness[index];
} else if (highlightArmor.name.startsWith("turret_")) { } else if (highlightArmor.name.startsWith("turret_")) {
turret.armor.thickness[index] = turret.armor!.thickness[index] =
initialTurret.armor.thickness[index]; initialTurret.armor!.thickness[index];
} else if (highlightArmor.name.startsWith("gun_")) { } else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) { if (highlightArmor.name.includes("_armor_")) {
gun.armor.thickness[index] = gun.armor!.thickness[index] =
initialGun.armor.thickness[index]; initialGun.armor!.thickness[index];
} else { } else {
gun.thickness = initialGun.thickness; gun.thickness = initialGun.thickness;
} }
@@ -24,11 +24,11 @@ export const PrimaryArmorScene = memo(() => {
const trackModelDefinition = tankModelDefinition.tracks[track.id]; const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id]; const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id]; const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin); const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin); const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin); const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const isDynamicArmorActive = Duel.use((state) => const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73) state.protagonist.consumables.includes(73),
); );
useTankTransform(track, turret, turretContainer, gunContainer); useTankTransform(track, turret, turretContainer, gunContainer);
@@ -41,8 +41,8 @@ export const PrimaryArmorScene = memo(() => {
const isVisible = isHull; const isVisible = isHull;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor, tankModelDefinition.armor!,
armorId armorId,
); );
if ( if (
@@ -69,13 +69,13 @@ export const PrimaryArmorScene = memo(() => {
const isCurrentTurret = node.name.startsWith( const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id `turret_${turretModelDefinition.model_id
.toString() .toString()
.padStart(2, "0")}` .padStart(2, "0")}`,
); );
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor, turretModelDefinition.armor!,
armorId armorId,
); );
if ( if (
@@ -103,13 +103,13 @@ export const PrimaryArmorScene = memo(() => {
<group ref={gunContainer}> <group ref={gunContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith( const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}` `gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`,
); );
const isVisible = isCurrentGun; const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor, gunModelDefinition.armor!,
armorId armorId,
); );
if ( if (
@@ -88,10 +88,10 @@ export function PrimaryArmorSceneComponent({
material.uniforms.caliber.value = shell.caliber; material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad( material.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet! isExplosive(shell.type) ? 90 : shell.ricochet!,
); );
material.uniforms.normalization.value = degToRad( material.uniforms.normalization.value = degToRad(
shell.normalization ?? 0 shell.normalization ?? 0,
); );
material.uniforms.isExplosive.value = isExplosive(shell.type); material.uniforms.isExplosive.value = isExplosive(shell.type);
material.uniforms.canSplash.value = canSplash(shell.type); material.uniforms.canSplash.value = canSplash(shell.type);
@@ -100,11 +100,11 @@ export function PrimaryArmorSceneComponent({
handleProtagonistEquipmentChange( handleProtagonistEquipmentChange(
Duel.state.protagonist.equipmentMatrix, Duel.state.protagonist.equipmentMatrix,
true true,
); );
handleAntagonistEquipmentChange( handleAntagonistEquipmentChange(
Duel.state.antagonist.equipmentMatrix, Duel.state.antagonist.equipmentMatrix,
true true,
); );
invalidate(); invalidate();
@@ -124,12 +124,12 @@ export function PrimaryArmorSceneComponent({
} }
function handleProtagonistEquipmentChange( function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix, equipment: EquipmentMatrix,
noInvalidate = false noInvalidate = false,
) { ) {
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
equipment equipment,
); );
material.uniforms.thickness.value = hasEnhancedArmor material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03 ? thickness * 1.03
@@ -139,16 +139,16 @@ export function PrimaryArmorSceneComponent({
} }
function handleAntagonistEquipmentChange( function handleAntagonistEquipmentChange(
equipment: EquipmentMatrix, equipment: EquipmentMatrix,
noInvalidate = false noInvalidate = false,
) { ) {
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell; tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration!.near;
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
Duel.state.antagonist.tank.equipment_preset, Duel.state.antagonist.tank.equipment_preset,
equipment equipment,
); );
material.uniforms.penetration.value = material.uniforms.penetration.value =
@@ -165,7 +165,7 @@ export function PrimaryArmorSceneComponent({
handleShellChange(); handleShellChange();
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration); handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
handleAdvancedHighlightingChange( handleAdvancedHighlightingChange(
TankopediaPersistent.state.advancedHighlighting TankopediaPersistent.state.advancedHighlighting,
); );
handleOpaqueChange(TankopediaPersistent.state.opaque); handleOpaqueChange(TankopediaPersistent.state.opaque);
handleWireframeChange(TankopediaPersistent.state.wireframe); handleWireframeChange(TankopediaPersistent.state.wireframe);
@@ -179,20 +179,20 @@ export function PrimaryArmorSceneComponent({
Tankopedia.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
TankopediaPersistent.on( TankopediaPersistent.on(
(state) => state.greenPenetration, (state) => state.greenPenetration,
handleGreenPenetrationChange handleGreenPenetrationChange,
), ),
TankopediaPersistent.on( TankopediaPersistent.on(
(state) => state.advancedHighlighting, (state) => state.advancedHighlighting,
handleAdvancedHighlightingChange handleAdvancedHighlightingChange,
), ),
TankopediaPersistent.on((state) => state.opaque, handleOpaqueChange), TankopediaPersistent.on((state) => state.opaque, handleOpaqueChange),
Duel.on( Duel.on(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
(equipment) => handleProtagonistEquipmentChange(equipment) (equipment) => handleProtagonistEquipmentChange(equipment),
), ),
Duel.on( Duel.on(
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
(equipment) => handleAntagonistEquipmentChange(equipment) (equipment) => handleAntagonistEquipmentChange(equipment),
), ),
() => transitionEvent.off(handleTransitionEvent), () => transitionEvent.off(handleTransitionEvent),
]; ];
@@ -204,7 +204,7 @@ export function PrimaryArmorSceneComponent({
useFrame(({ gl, camera }) => { useFrame(({ gl, camera }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar( gl.getSize(material.uniforms.resolution.value).multiplyScalar(
gl.getPixelRatio() gl.getPixelRatio(),
); );
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture; material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
material.uniforms.spacedArmorDepth.value = material.uniforms.spacedArmorDepth.value =
@@ -37,15 +37,15 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
const trackModelDefinition = tankModelDefinition.tracks[track.id]; const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id]; const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id]; const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin); const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin); const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin); const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const maskOrigin = const maskOrigin =
gunModelDefinition.mask === undefined gunModelDefinition.mask === undefined
? undefined ? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y; : gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
const isDynamicArmorActive = Duel.use((state) => const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73) state.protagonist.consumables.includes(73),
); );
useTankTransform(track, turret, turretContainer, gunContainer); useTankTransform(track, turret, turretContainer, gunContainer);
@@ -58,8 +58,8 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
const isVisible = isHull; const isVisible = isHull;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor, tankModelDefinition.armor!,
armorId armorId,
); );
if ( if (
@@ -104,13 +104,13 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
<group ref={turretContainer}> <group ref={turretContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith( const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}` `turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`,
); );
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor, turretModelDefinition.armor!,
armorId armorId,
); );
if ( if (
@@ -139,13 +139,13 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
<group ref={gunContainer}> <group ref={gunContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith( const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}` `gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`,
); );
const isVisible = isCurrentGun; const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor, gunModelDefinition.armor!,
armorId armorId,
); );
if ( if (
@@ -50,13 +50,13 @@ export function SpacedArmorSubExternal({
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell; tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near; material.uniforms.penetration.value = shell.penetration!.near;
} }
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) { function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
equipment equipment,
); );
material.uniforms.thickness.value = hasEnhancedArmor material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03 ? thickness * 1.03
@@ -66,11 +66,11 @@ export function SpacedArmorSubExternal({
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell; tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration!.near;
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
Duel.state.antagonist.tank.equipment_preset, Duel.state.antagonist.tank.equipment_preset,
equipment equipment,
); );
material.uniforms.penetration.value = material.uniforms.penetration.value =
@@ -87,11 +87,11 @@ export function SpacedArmorSubExternal({
Tankopedia.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
Duel.on( Duel.on(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange handleProtagonistEquipmentChange,
), ),
Duel.on( Duel.on(
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange handleAntagonistEquipmentChange,
), ),
]; ];
@@ -53,20 +53,20 @@ export function SpacedArmorSubSpaced({
const shell = const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell; tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near; material.uniforms.penetration.value = shell.penetration!.near;
material.uniforms.caliber.value = shell.caliber; material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad( material.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet! isExplosive(shell.type) ? 90 : shell.ricochet!,
); );
material.uniforms.normalization.value = degToRad( material.uniforms.normalization.value = degToRad(
shell.normalization ?? 0 shell.normalization ?? 0,
); );
} }
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) { function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
equipment equipment,
); );
material.uniforms.thickness.value = hasEnhancedArmor material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03 ? thickness * 1.03
@@ -76,11 +76,11 @@ export function SpacedArmorSubSpaced({
const tankopediaEphemeral = Tankopedia.state; const tankopediaEphemeral = Tankopedia.state;
const shell = const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell; tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration!.near;
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
Duel.state.antagonist.tank.equipment_preset, Duel.state.antagonist.tank.equipment_preset,
equipment equipment,
); );
material.uniforms.penetration.value = material.uniforms.penetration.value =
@@ -97,11 +97,11 @@ export function SpacedArmorSubSpaced({
Tankopedia.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
Duel.on( Duel.on(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange handleProtagonistEquipmentChange,
), ),
Duel.on( Duel.on(
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange handleAntagonistEquipmentChange,
), ),
]; ];
@@ -83,16 +83,18 @@ export function SpacedArmorSceneComponent({
point: Vector3, point: Vector3,
intersections: Intersection[], intersections: Intersection[],
allowRicochet: boolean, allowRicochet: boolean,
remainingPenetrationInput?: number remainingPenetrationInput?: number,
) => { ) => {
const { customShell } = Tankopedia.state; const { customShell } = Tankopedia.state;
const shell = customShell ?? Duel.state.antagonist.shell; const shell = customShell ?? Duel.state.antagonist.shell;
const cameraNormal = camera.position.clone().sub(point).normalize(); const cameraNormal = camera.position.clone().sub(point).normalize();
const shot: Shot = { const shot: Shot = {
splashRadius: splashRadius:
shell.type === ShellType.HE ? shell.explosion_radius : undefined, shell.type === ShellType.SHELL_TYPE_HE
? shell.explosion_radius
: undefined,
damage: -1, damage: -1,
containsGaps: shell.type === ShellType.HEAT, containsGaps: shell.type === ShellType.SHELL_TYPE_HEAT,
in: { in: {
surfaceNormal: cameraNormal, surfaceNormal: cameraNormal,
layers: [], layers: [],
@@ -103,21 +105,21 @@ export function SpacedArmorSceneComponent({
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
Duel.state.antagonist.tank.equipment_preset, Duel.state.antagonist.tank.equipment_preset,
Duel.state.antagonist.equipmentMatrix Duel.state.antagonist.equipmentMatrix,
); );
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const penetration = const penetration =
shell.penetration.near * shell.penetration!.near *
resolvePenetrationCoefficient(hasCalibratedShells, shell.type); resolvePenetrationCoefficient(hasCalibratedShells, shell.type);
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1; const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
const filteredIntersections = intersections.filter( const filteredIntersections = intersections.filter(
(intersection) => (intersection) =>
"type" in intersection.object.userData && "type" in intersection.object.userData &&
!discardClippingPlane(intersection.object, intersection.point) !discardClippingPlane(intersection.object, intersection.point),
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[]; ) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
const encounteredExternalModuleVariants: ExternalModuleVariant[] = []; const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
const noDuplicateIntersections: typeof filteredIntersections = []; const noDuplicateIntersections: typeof filteredIntersections = [];
@@ -126,12 +128,12 @@ export function SpacedArmorSceneComponent({
if (intersection.object.userData.type === ArmorType.External) { if (intersection.object.userData.type === ArmorType.External) {
if ( if (
!encounteredExternalModuleVariants.includes( !encounteredExternalModuleVariants.includes(
intersection.object.userData.variant intersection.object.userData.variant,
) )
) { ) {
noDuplicateIntersections.push(intersection); noDuplicateIntersections.push(intersection);
encounteredExternalModuleVariants.push( encounteredExternalModuleVariants.push(
intersection.object.userData.variant intersection.object.userData.variant,
); );
} }
} else { } else {
@@ -159,10 +161,10 @@ export function SpacedArmorSceneComponent({
const surfaceNormal = intersection const surfaceNormal = intersection
.normal!.clone() .normal!.clone()
.applyQuaternion( .applyQuaternion(
intersection.object.getWorldQuaternion(new Quaternion()) intersection.object.getWorldQuaternion(new Quaternion()),
); );
if (shell.type === ShellType.HEAT && index > 0) { if (shell.type === ShellType.SHELL_TYPE_HEAT && index > 0) {
const lastLayer = shot.in.layers.at(-1) as ShotLayerBase; const lastLayer = shot.in.layers.at(-1) as ShotLayerBase;
const distance = lastLayer.point.distanceTo(intersection.point); const distance = lastLayer.point.distanceTo(intersection.point);
remainingPenetration -= 0.5 * remainingPenetration * distance; remainingPenetration -= 0.5 * remainingPenetration * distance;
@@ -181,7 +183,7 @@ export function SpacedArmorSceneComponent({
const thickness = layer.thickness * thicknessCoefficient; const thickness = layer.thickness * thicknessCoefficient;
remainingPenetration -= thickness; remainingPenetration -= thickness;
const blocked = const blocked =
remainingPenetration <= 0 || shell.type === ShellType.HE; remainingPenetration <= 0 || shell.type === ShellType.SHELL_TYPE_HE;
shot.in.layers.push({ shot.in.layers.push({
type: ArmorType.External, type: ArmorType.External,
@@ -190,7 +192,7 @@ export function SpacedArmorSceneComponent({
point: intersection.point, point: intersection.point,
surfaceNormal, surfaceNormal,
status: status:
shell.type === ShellType.HE shell.type === ShellType.SHELL_TYPE_HE
? "blocked" ? "blocked"
: blocked : blocked
? "blocked" ? "blocked"
@@ -200,7 +202,7 @@ export function SpacedArmorSceneComponent({
} else { } else {
const thickness = layer.thickness * thicknessCoefficient; const thickness = layer.thickness * thicknessCoefficient;
const ricochet = degToRad( const ricochet = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet! isExplosive(shell.type) ? 90 : shell.ricochet!,
); );
const angle = surfaceNormal.angleTo(cameraNormal); const angle = surfaceNormal.angleTo(cameraNormal);
@@ -241,7 +243,8 @@ export function SpacedArmorSceneComponent({
point: intersection.point, point: intersection.point,
surfaceNormal: intersection.face!.normal, surfaceNormal: intersection.face!.normal,
status: status:
shell.type === ShellType.HE && layer.type !== ArmorType.Primary shell.type === ShellType.SHELL_TYPE_HE &&
layer.type !== ArmorType.Primary
? "blocked" ? "blocked"
: blocked : blocked
? "blocked" ? "blocked"
@@ -251,7 +254,10 @@ export function SpacedArmorSceneComponent({
} }
} }
if (shell.type !== ShellType.HE && remainingPenetration <= 0) { if (
shell.type !== ShellType.SHELL_TYPE_HE &&
remainingPenetration <= 0
) {
break; break;
} }
@@ -266,7 +272,7 @@ export function SpacedArmorSceneComponent({
if (!lastLayer || !firstLayer) { if (!lastLayer || !firstLayer) {
return null; return null;
} else { } else {
if (shell.type === ShellType.HE) { if (shell.type === ShellType.SHELL_TYPE_HE) {
const totalSpacedArmorThickness = shot.in.layers.reduce( const totalSpacedArmorThickness = shot.in.layers.reduce(
(acc, layer) => { (acc, layer) => {
if (layer.type === null || layer.type === ArmorType.Primary) { if (layer.type === null || layer.type === ArmorType.Primary) {
@@ -279,10 +285,10 @@ export function SpacedArmorSceneComponent({
return acc + layer.thicknessAngled; return acc + layer.thicknessAngled;
}, },
0 0,
); );
const distanceFromSpacedArmor = firstLayer.point.distanceTo( const distanceFromSpacedArmor = firstLayer.point.distanceTo(
lastLayer.point lastLayer.point,
); );
const finalDamage = Math.max( const finalDamage = Math.max(
0, 0,
@@ -291,7 +297,7 @@ export function SpacedArmorSceneComponent({
(1 - distanceFromSpacedArmor / shell.explosion_radius!) - (1 - distanceFromSpacedArmor / shell.explosion_radius!) -
1.1 * 1.1 *
(lastLayer.thicknessAngled + (lastLayer.thicknessAngled +
Math.min(penetration, totalSpacedArmorThickness)) Math.min(penetration, totalSpacedArmorThickness)),
); );
/** /**
@@ -328,20 +334,20 @@ export function SpacedArmorSceneComponent({
.sub( .sub(
lastLayer.surfaceNormal lastLayer.surfaceNormal
.clone() .clone()
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)) .multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)),
); );
caster.set(lastLayer.point, ricochetNormal); caster.set(lastLayer.point, ricochetNormal);
const ricochetIntersections = caster.intersectObjects( const ricochetIntersections = caster.intersectObjects(
props.scene.children, props.scene.children,
true true,
); );
const outShot = shoot( const outShot = shoot(
lastLayer.point, lastLayer.point,
ricochetIntersections, ricochetIntersections,
false, false,
remainingPenetration remainingPenetration,
); );
shot.in.status = "ricochet"; shot.in.status = "ricochet";
@@ -363,7 +369,7 @@ export function SpacedArmorSceneComponent({
return shot; return shot;
}, },
[camera] [camera],
); );
return ( return (
@@ -43,9 +43,9 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const trackModelDefinition = tankModelDefinition.tracks[track.id]; const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id]; const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id]; const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin); const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin); const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin); const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const canvas = useThree((state) => state.gl.domElement); const canvas = useThree((state) => state.gl.domElement);
const maskOrigin = const maskOrigin =
@@ -53,16 +53,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
? undefined ? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y; : gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
const showPrimaryArmor = TankopediaPersistent.use( const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.showPrimaryArmor (state) => state.showPrimaryArmor,
); );
const showSpacedArmor = TankopediaPersistent.use( const showSpacedArmor = TankopediaPersistent.use(
(state) => state.showSpacedArmor (state) => state.showSpacedArmor,
); );
const showExternalModules = TankopediaPersistent.use( const showExternalModules = TankopediaPersistent.use(
(state) => state.showExternalModules (state) => state.showExternalModules,
); );
const isDynamicArmorActive = Duel.use((state) => const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73) state.protagonist.consumables.includes(73),
); );
useTankTransform(track, turret, turretContainer, gunContainer); useTankTransform(track, turret, turretContainer, gunContainer);
@@ -75,8 +75,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const isVisible = isHull; const isVisible = isHull;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor, tankModelDefinition.armor!,
armorId armorId,
); );
if ( if (
@@ -127,13 +127,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<group ref={turretContainer}> <group ref={turretContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith( const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}` `turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`,
); );
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor, turretModelDefinition.armor!,
armorId armorId,
); );
if ( if (
@@ -168,12 +168,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
@@ -184,10 +184,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
modelTransformEvent.last!.pitch, modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw + modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width), delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
@@ -216,13 +216,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<group ref={gunContainer}> <group ref={gunContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith( const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}` `gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`,
); );
const isVisible = isCurrentGun; const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor, gunModelDefinition.armor!,
armorId armorId,
); );
if ( if (
@@ -256,12 +256,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position); delta.set(event.clientX, event.clientY).sub(position);
@@ -272,10 +272,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
delta.y * (Math.PI / boundingRect.height), delta.y * (Math.PI / boundingRect.height),
modelTransformEvent.last!.yaw + modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width), delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
@@ -332,12 +332,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position); delta.set(event.clientX, event.clientY).sub(position);
@@ -348,10 +348,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
delta.y * (Math.PI / boundingRect.height), delta.y * (Math.PI / boundingRect.height),
modelTransformEvent.last!.yaw + modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width), delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
@@ -1,16 +1,16 @@
import { TankClass } from '@blitzkit/core'; import { TankClass } from "@blitzkit/core";
import type { ComponentProps, ReactNode } from 'react'; import type { ComponentProps, ReactNode } from "react";
import { ClassHeavy } from './components/ClassHeavy'; import { ClassHeavy } from "./components/ClassHeavy";
import { ClassLight } from './components/ClassLight'; import { ClassLight } from "./components/ClassLight";
import { ClassMedium } from './components/ClassMedium'; import { ClassMedium } from "./components/ClassMedium";
import { ClassTankDestroyer } from './components/ClassTankDestroyer'; import { ClassTankDestroyer } from "./components/ClassTankDestroyer";
export const classIcons: Record< export const classIcons: Record<
TankClass, TankClass,
(props: ComponentProps<'svg'>) => ReactNode (props: ComponentProps<"svg">) => ReactNode
> = { > = {
[TankClass.TANK_DESTROYER]: ClassTankDestroyer, [TankClass.TANK_CLASS_TANK_DESTROYER]: ClassTankDestroyer,
[TankClass.HEAVY]: ClassHeavy, [TankClass.TANK_CLASS_HEAVY]: ClassHeavy,
[TankClass.LIGHT]: ClassLight, [TankClass.TANK_CLASS_LIGHT]: ClassLight,
[TankClass.MEDIUM]: ClassMedium, [TankClass.TANK_CLASS_MEDIUM]: ClassMedium,
}; };
@@ -15,7 +15,7 @@ interface BodyProps {
export function Body({ stats }: BodyProps) { export function Body({ stats }: BodyProps) {
const members = CompareEphemeral.use((state) => state.members); const members = CompareEphemeral.use((state) => state.members);
const hasNonRegularGun = members.some( const hasNonRegularGun = members.some(
({ gun }) => gun.gun_type!.$case !== "regular" ({ gun }) => gun.gun_type!.$case !== "regular",
); );
const { unwrap, strings } = useLocale(); const { unwrap, strings } = useLocale();
@@ -55,7 +55,7 @@ export function Body({ stats }: BodyProps) {
}} }}
> >
<img <img
alt={unwrap(thisShell.name)} alt={unwrap(thisShell.name!)}
width={16} width={16}
height={16} height={16}
src={asset(`icons/shells/${thisShell.icon}.webp`)} src={asset(`icons/shells/${thisShell.icon}.webp`)}
@@ -294,7 +294,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<img <img
alt={unwrap( alt={unwrap(
provisionDefinitions.provisions[provision] provisionDefinitions.provisions[provision]
.name, .name!,
)} )}
key={provision} key={provision}
src={asset( src={asset(
@@ -501,7 +501,7 @@ export function CompareTable({ stats }: CompareTableProps) {
alt={unwrap( alt={unwrap(
consumableDefinitions.consumables[ consumableDefinitions.consumables[
consumable consumable
].name, ].name!,
)} )}
key={consumable} key={consumable}
src={asset( src={asset(
@@ -119,7 +119,7 @@ export function TankCard({ index, tank }: TankCardProps) {
}} }}
> >
<img <img
alt={unwrap(tank.name)} alt={unwrap(tank.name!)}
src={tankIcon(tank.id)} src={tankIcon(tank.id)}
width={64} width={64}
height={64} height={64}
@@ -137,7 +137,7 @@ export function TankCard({ index, tank }: TankCardProps) {
textOverflow: "ellipsis", textOverflow: "ellipsis",
}} }}
> >
{unwrap(tank.name)} {unwrap(tank.name!)}
</Text> </Text>
</Flex> </Flex>
</Flex> </Flex>
@@ -55,7 +55,7 @@ export const fakeCompositeStats = compositeStats(
wins: 30, wins: 30,
xp: 1230 * 50, xp: 1230 * 50,
}, },
averageDefinitions.averages[7297].mu averageDefinitions.averages[7297].mu!,
); );
export function BreakdownPreview() { export function BreakdownPreview() {
@@ -88,9 +88,9 @@ export function BreakdownRenderer() {
const diff = useMemo( const diff = useMemo(
() => () =>
deltaTankStats(tankStatsA, tankStatsB).sort( deltaTankStats(tankStatsA, tankStatsB).sort(
(a, b) => b.last_battle_time - a.last_battle_time (a, b) => b.last_battle_time - a.last_battle_time,
), ),
[tankStatsB] [tankStatsB],
); );
const compositeStats = diff const compositeStats = diff
.map((diff) => { .map((diff) => {
@@ -100,14 +100,14 @@ export function BreakdownRenderer() {
const compositeStats = calculateCompositeStats( const compositeStats = calculateCompositeStats(
{ battle_life_time: diff.battle_life_time, ...diff.all }, { battle_life_time: diff.battle_life_time, ...diff.all },
average.mu average.mu!,
); );
return { id: diff.tank_id, compositeStats }; return { id: diff.tank_id, compositeStats };
}) })
.filter((stats) => stats !== null); .filter((stats) => stats !== null);
const sum = sumCompositeStats( const sum = sumCompositeStats(
compositeStats.map(({ compositeStats }) => compositeStats) compositeStats.map(({ compositeStats }) => compositeStats),
); );
useEffect(() => { useEffect(() => {
@@ -19,7 +19,7 @@ type GalleryCardProps = MaybeSkeletonComponentProps<{
export function GalleryCard(props: GalleryCardProps) { export function GalleryCard(props: GalleryCardProps) {
const src = asset( const src = asset(
`gallery/avatars/${!props.skeleton && `${props.avatar.id}${props.avatar.extension}`}` `gallery/avatars/${!props.skeleton && `${props.avatar.id}${props.avatar.extension}`}`,
); );
const card = useRef<HTMLDivElement>(null!); const card = useRef<HTMLDivElement>(null!);
const { unwrap } = useLocale(); const { unwrap } = useLocale();
@@ -54,11 +54,11 @@ export function GalleryCard(props: GalleryCardProps) {
objectFit: "cover", objectFit: "cover",
}} }}
src={src} src={src}
alt={unwrap(props.avatar.name)} alt={unwrap(props.avatar.name!)}
/> />
<Text size="1" align="center" style={{ maxWidth: "100%" }}> <Text size="1" align="center" style={{ maxWidth: "100%" }}>
{unwrap(props.avatar.name)} {unwrap(props.avatar.name!)}
</Text> </Text>
</> </>
)} )}
@@ -90,12 +90,12 @@ export function GalleryCard(props: GalleryCardProps) {
</Flex> </Flex>
<Flex px="4" justify="center" mt="3"> <Flex px="4" justify="center" mt="3">
<img src={src} alt={unwrap(props.avatar.name)} /> <img src={src} alt={unwrap(props.avatar.name!)} />
</Flex> </Flex>
<Flex justify="center" mt="4"> <Flex justify="center" mt="4">
<Dialog.Title align="center" style={{ maxWidth: "10rem" }}> <Dialog.Title align="center" style={{ maxWidth: "10rem" }}>
{unwrap(props.avatar.name)} {unwrap(props.avatar.name!)}
</Dialog.Title> </Dialog.Title>
</Flex> </Flex>
</Dialog.Content> </Dialog.Content>
+5 -5
View File
@@ -82,10 +82,10 @@ export function Guesser() {
}); });
setResults( setResults(
searchResults.map((result) => tankDefinitions.tanks[result.obj.id]) searchResults.map((result) => tankDefinitions.tanks[result.obj.id]),
); );
}, 500), }, 500),
[] [],
); );
useEffect(() => { useEffect(() => {
@@ -126,11 +126,11 @@ export function Guesser() {
onClick={() => { onClick={() => {
if (!input.current) return; if (!input.current) return;
input.current.value = unwrap(result.name); input.current.value = unwrap(result.name!);
setSelected(result); setSelected(result);
setResults(null); setResults(null);
}} }}
text={unwrap(result.name)} text={unwrap(result.name!)}
prefix={ prefix={
<img <img
style={{ width: "1em", height: "1em" }} style={{ width: "1em", height: "1em" }}
@@ -321,7 +321,7 @@ export function Guesser() {
return tiers.includes(tank.tier); return tiers.includes(tank.tier);
}); });
const id = Number( const id = Number(
filteredIds[Math.floor(Math.random() * filteredIds.length)] filteredIds[Math.floor(Math.random() * filteredIds.length)],
); );
const tank = tankDefinitions.tanks[id]; const tank = tankDefinitions.tanks[id];
@@ -118,16 +118,16 @@ export function Model() {
ref={turretGroup} ref={turretGroup}
rotation={[0, 0, modelTransformEvent.last!.yaw]} rotation={[0, 0, modelTransformEvent.last!.yaw]}
position={[ position={[
tankModel.turret_origin.x + trackModel.origin.x, tankModel.turret_origin!.x + trackModel.origin!.x,
tankModel.turret_origin.z + trackModel.origin.z, tankModel.turret_origin!.z + trackModel.origin!.z,
tankModel.turret_origin.y + trackModel.origin.y, tankModel.turret_origin!.y + trackModel.origin!.y,
]} ]}
> >
<group <group
position={[ position={[
-turretTankModel.turret_origin.x - turretTrackModel.origin.x, -turretTankModel.turret_origin!.x - turretTrackModel.origin!.x,
-turretTankModel.turret_origin.z - turretTrackModel.origin.z, -turretTankModel.turret_origin!.z - turretTrackModel.origin!.z,
-turretTankModel.turret_origin.y - turretTrackModel.origin.y, -turretTankModel.turret_origin!.y - turretTrackModel.origin!.y,
]} ]}
> >
{turretNodes.map((node) => { {turretNodes.map((node) => {
@@ -160,28 +160,28 @@ export function Model() {
ref={gunGroup} ref={gunGroup}
rotation={[modelTransformEvent.last!.pitch, 0, 0]} rotation={[modelTransformEvent.last!.pitch, 0, 0]}
position={[ position={[
turretTankModel.turret_origin.x + turretTankModel.turret_origin!.x +
turretTrackModel.origin.x + turretTrackModel.origin!.x +
turretModel.gun_origin.x, turretModel.gun_origin!.x,
turretTankModel.turret_origin.z + turretTankModel.turret_origin!.z +
turretTrackModel.origin.z + turretTrackModel.origin!.z +
turretModel.gun_origin.z, turretModel.gun_origin!.z,
turretTankModel.turret_origin.y + turretTankModel.turret_origin!.y +
turretTrackModel.origin.y + turretTrackModel.origin!.y +
turretModel.gun_origin.y, turretModel.gun_origin!.y,
]} ]}
> >
<group <group
position={[ position={[
-turretTankModel.turret_origin.x - -turretTankModel.turret_origin!.x -
turretTrackModel.origin.x - turretTrackModel.origin!.x -
turretModel.gun_origin.x, turretModel.gun_origin!.x,
-turretTankModel.turret_origin.z - -turretTankModel.turret_origin!.z -
turretTrackModel.origin.z - turretTrackModel.origin!.z -
turretModel.gun_origin.z, turretModel.gun_origin!.z,
-turretTankModel.turret_origin.y - -turretTankModel.turret_origin!.y -
turretTrackModel.origin.y - turretTrackModel.origin!.y -
turretModel.gun_origin.y, turretModel.gun_origin!.y,
]} ]}
> >
{turretNodes.map((node) => { {turretNodes.map((node) => {
@@ -216,15 +216,15 @@ export function Model() {
<group <group
position={[ position={[
-gunTankModel.turret_origin.x - -gunTankModel.turret_origin!.x -
gunTrackModel.origin.x - gunTrackModel.origin!.x -
gunTurretModel.gun_origin.x, gunTurretModel.gun_origin!.x,
-gunTankModel.turret_origin.z - -gunTankModel.turret_origin!.z -
gunTrackModel.origin.z - gunTrackModel.origin!.z -
gunTurretModel.gun_origin.z, gunTurretModel.gun_origin!.z,
-gunTankModel.turret_origin.y - -gunTankModel.turret_origin!.y -
gunTrackModel.origin.y - gunTrackModel.origin!.y -
gunTurretModel.gun_origin.y, gunTurretModel.gun_origin!.y,
]} ]}
> >
{gunNodes.map((node) => { {gunNodes.map((node) => {
@@ -1,11 +1,11 @@
import { asset } from '@blitzkit/core'; import { asset } from "@blitzkit/core";
import { ClockIcon, ReloadIcon } from '@radix-ui/react-icons'; import { ClockIcon, ReloadIcon } from "@radix-ui/react-icons";
import { Flex, Text } from '@radix-ui/themes'; import { Flex, Text } from "@radix-ui/themes";
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions'; import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
import { useDelta } from '../../hooks/useDelta'; import { useDelta } from "../../hooks/useDelta";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import { GenericTankComponentButton } from './GenericTankComponentButton'; import { GenericTankComponentButton } from "./GenericTankComponentButton";
import type { TankComponentButtonProps } from './TankComponentButton'; import type { TankComponentButtonProps } from "./TankComponentButton";
interface ConsumableButtonProps extends TankComponentButtonProps { interface ConsumableButtonProps extends TankComponentButtonProps {
consumable: number; consumable: number;
@@ -27,8 +27,8 @@ export function ConsumableButton({
return ( return (
<GenericTankComponentButton <GenericTankComponentButton
style={{ width: '6rem' }} style={{ width: "6rem" }}
tooltip={unwrap(consumableDefinitions.consumables[consumable].name)} tooltip={unwrap(consumableDefinitions.consumables[consumable].name!)}
icon={asset(`icons/consumables/${consumable}.webp`)} icon={asset(`icons/consumables/${consumable}.webp`)}
{...props} {...props}
> >
@@ -39,9 +39,9 @@ export function ConsumableButton({
size="1" size="1"
color={ color={
durationDelta > 0 durationDelta > 0
? 'green' ? "green"
: durationDelta < 0 : durationDelta < 0
? 'tomato' ? "tomato"
: undefined : undefined
} }
> >
@@ -56,9 +56,9 @@ export function ConsumableButton({
size="1" size="1"
color={ color={
cooldownDelta < 0 cooldownDelta < 0
? 'green' ? "green"
: cooldownDelta > 0 : cooldownDelta > 0
? 'tomato' ? "tomato"
: undefined : undefined
} }
> >
@@ -1,8 +1,8 @@
import { asset } from '@blitzkit/core'; import { asset } from "@blitzkit/core";
import { awaitableEquipmentDefinitions } from '../../core/awaitables/equipmentDefinitions'; import { awaitableEquipmentDefinitions } from "../../core/awaitables/equipmentDefinitions";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import { GenericTankComponentButton } from './GenericTankComponentButton'; import { GenericTankComponentButton } from "./GenericTankComponentButton";
import type { TankComponentButtonProps } from './TankComponentButton'; import type { TankComponentButtonProps } from "./TankComponentButton";
interface EquipmentButtonProps extends TankComponentButtonProps { interface EquipmentButtonProps extends TankComponentButtonProps {
equipment: number; equipment: number;
@@ -15,7 +15,7 @@ export function EquipmentButton({ equipment, ...props }: EquipmentButtonProps) {
return ( return (
<GenericTankComponentButton <GenericTankComponentButton
tooltip={unwrap(equipmentDefinitions.equipments[equipment].name)} tooltip={unwrap(equipmentDefinitions.equipments[equipment].name!)}
icon={asset(`icons/equipment/${equipment}.webp`)} icon={asset(`icons/equipment/${equipment}.webp`)}
{...props} {...props}
/> />
@@ -1,8 +1,8 @@
import { asset } from '@blitzkit/core'; import { asset } from "@blitzkit/core";
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import { GenericTankComponentButton } from './GenericTankComponentButton'; import { GenericTankComponentButton } from "./GenericTankComponentButton";
import type { TankComponentButtonProps } from './TankComponentButton'; import type { TankComponentButtonProps } from "./TankComponentButton";
interface ProvisionButtonProps extends TankComponentButtonProps { interface ProvisionButtonProps extends TankComponentButtonProps {
provision: number; provision: number;
@@ -15,7 +15,7 @@ export function ProvisionButton({ provision, ...props }: ProvisionButtonProps) {
return ( return (
<GenericTankComponentButton <GenericTankComponentButton
tooltip={unwrap(provisionDefinitions.provisions[provision].name)} tooltip={unwrap(provisionDefinitions.provisions[provision].name!)}
icon={asset(`icons/provisions/${provision}.webp`)} icon={asset(`icons/provisions/${provision}.webp`)}
{...props} {...props}
/> />
@@ -6,11 +6,11 @@ import {
type TankDefinition, type TankDefinition,
type TrackDefinition, type TrackDefinition,
type TurretDefinition, type TurretDefinition,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { DropdownMenu, Flex } from '@radix-ui/themes'; import { DropdownMenu, Flex } from "@radix-ui/themes";
import { useState } from 'react'; import { useState } from "react";
import { useLocale } from '../hooks/useLocale'; import { useLocale } from "../hooks/useLocale";
import { ModuleButton } from './ModuleButtons/ModuleButton'; import { ModuleButton } from "./ModuleButtons/ModuleButton";
interface ModuleManagerProps { interface ModuleManagerProps {
tank: TankDefinition; tank: TankDefinition;
@@ -72,8 +72,8 @@ export function ModuleManager({
variant="ghost" variant="ghost"
module="turret" module="turret"
discriminator={TIER_ROMAN_NUMERALS[turret.tier]} discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
/>{' '} />{" "}
{unwrap(turret.name)} {unwrap(turret.name!)}
</DropdownMenu.RadioItem> </DropdownMenu.RadioItem>
); );
})} })}
@@ -112,8 +112,8 @@ export function ModuleManager({
variant="ghost" variant="ghost"
module="gun" module="gun"
discriminator={TIER_ROMAN_NUMERALS[gun.tier]} discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
/>{' '} />{" "}
{unwrap(gun.name)} {unwrap(gun.name!)}
</DropdownMenu.RadioItem> </DropdownMenu.RadioItem>
); );
})} })}
@@ -151,8 +151,8 @@ export function ModuleManager({
variant="ghost" variant="ghost"
module="engine" module="engine"
discriminator={TIER_ROMAN_NUMERALS[engine.tier]} discriminator={TIER_ROMAN_NUMERALS[engine.tier]}
/>{' '} />{" "}
{unwrap(engine.name)} {unwrap(engine.name!)}
</DropdownMenu.RadioItem> </DropdownMenu.RadioItem>
); );
})} })}
@@ -190,8 +190,8 @@ export function ModuleManager({
variant="ghost" variant="ghost"
module="chassis" module="chassis"
discriminator={TIER_ROMAN_NUMERALS[track.tier]} discriminator={TIER_ROMAN_NUMERALS[track.tier]}
/>{' '} />{" "}
{unwrap(track.name)} {unwrap(track.name!)}
</DropdownMenu.RadioItem> </DropdownMenu.RadioItem>
); );
})} })}
@@ -20,7 +20,7 @@ export const TankRow = memo<TankRowProps>(
const averages = averageDefinitions.averages[tank.id]; const averages = averageDefinitions.averages[tank.id];
const ratio = useAveragesExclusionRatio(); const ratio = useAveragesExclusionRatio();
const playerCountPeriod = Performance.use( const playerCountPeriod = Performance.use(
(state) => state.playerCountPeriod (state) => state.playerCountPeriod,
); );
const { locale } = useLocale(); const { locale } = useLocale();
@@ -29,107 +29,109 @@ export const TankRow = memo<TankRowProps>(
<TankRowHeaderCell tank={tank} /> <TankRowHeaderCell tank={tank} />
<CompareCell <CompareCell
value={averages.mu.wins / averages.mu.battles} value={averages.mu!.wins / averages.mu!.battles}
truth={average.winrate} truth={average.winrate}
align="center" align="center"
> >
{((averages.mu.wins / averages.mu.battles) * 100).toFixed(1)}% {((averages.mu!.wins / averages.mu!.battles) * 100).toFixed(1)}%
</CompareCell> </CompareCell>
<Table.Cell align="center"> <Table.Cell align="center">
{formatCompact( {formatCompact(
locale, locale,
Math.round(ratio * averages.samples[playerCountPeriod]) Math.round(ratio * averages.samples![playerCountPeriod]),
)} )}
</Table.Cell> </Table.Cell>
<CompareCell <CompareCell
value={averages.mu.damage_dealt / averages.mu.battles} value={averages.mu!.damage_dealt / averages.mu!.battles}
truth={average.damage} truth={average.damage}
align="center" align="center"
> >
{Math.round( {Math.round(
averages.mu.damage_dealt / averages.mu.battles averages.mu!.damage_dealt / averages.mu!.battles,
).toLocaleString(locale)} ).toLocaleString(locale)}
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.survived_battles / averages.mu.battles} value={averages.mu!.survived_battles / averages.mu!.battles}
truth={average.survival} truth={average.survival}
align="center" align="center"
> >
{Math.round( {Math.round(
(averages.mu.survived_battles / averages.mu.battles) * 100 (averages.mu!.survived_battles / averages.mu!.battles) * 100,
)} )}
% %
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.xp / averages.mu.battles} value={averages.mu!.xp / averages.mu!.battles}
truth={average.xp} truth={average.xp}
align="center" align="center"
> >
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString( {Math.round(averages.mu!.xp / averages.mu!.battles).toLocaleString(
locale locale,
)} )}
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.frags / averages.mu.battles} value={averages.mu!.frags / averages.mu!.battles}
truth={average.kills} truth={average.kills}
align="center" align="center"
> >
{(averages.mu.frags / averages.mu.battles).toFixed(2)} {(averages.mu!.frags / averages.mu!.battles).toFixed(2)}
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.spotted / averages.mu.battles} value={averages.mu!.spotted / averages.mu!.battles}
truth={average.spots} truth={average.spots}
align="center" align="center"
> >
{(averages.mu.spotted / averages.mu.battles).toFixed(2)} {(averages.mu!.spotted / averages.mu!.battles).toFixed(2)}
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.hits / averages.mu.shots} value={averages.mu!.hits / averages.mu!.shots}
truth={average.accuracy} truth={average.accuracy}
align="center" align="center"
> >
{Math.round((averages.mu.hits / averages.mu.shots) * 100)}% {Math.round((averages.mu!.hits / averages.mu!.shots) * 100)}%
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.shots / averages.mu.battles} value={averages.mu!.shots / averages.mu!.battles}
truth={average.shots} truth={average.shots}
align="center" align="center"
> >
{(averages.mu.shots / averages.mu.battles).toFixed(1)} {(averages.mu!.shots / averages.mu!.battles).toFixed(1)}
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.hits / averages.mu.battles} value={averages.mu!.hits / averages.mu!.battles}
truth={average.hits} truth={average.hits}
align="center" align="center"
> >
{(averages.mu.hits / averages.mu.battles).toFixed(1)} {(averages.mu!.hits / averages.mu!.battles).toFixed(1)}
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.damage_dealt / averages.mu.damage_received} value={averages.mu!.damage_dealt / averages.mu!.damage_received}
truth={average.damageRatio} truth={average.damageRatio}
align="center" align="center"
> >
{(averages.mu.damage_dealt / averages.mu.damage_received).toFixed(2)} {(averages.mu!.damage_dealt / averages.mu!.damage_received).toFixed(
2,
)}
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.damage_received / averages.mu.battles} value={averages.mu!.damage_received / averages.mu!.battles}
truth={average.damageReceived} truth={average.damageReceived}
direction={CompareCellDirection.LOWER_IS_BETTER} direction={CompareCellDirection.LOWER_IS_BETTER}
align="center" align="center"
> >
{Math.round( {Math.round(
averages.mu.damage_received / averages.mu.battles averages.mu!.damage_received / averages.mu!.battles,
).toLocaleString(locale)} ).toLocaleString(locale)}
</CompareCell> </CompareCell>
<CompareCell <CompareCell
value={averages.mu.capture_points / averages.mu.battles} value={averages.mu!.capture_points / averages.mu!.battles}
truth={average.capturePoints} truth={average.capturePoints}
align="center" align="center"
> >
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)} {(averages.mu!.capture_points / averages.mu!.battles).toFixed(2)}
</CompareCell> </CompareCell>
</Table.Row> </Table.Row>
); );
}, },
(prev, next) => prev.tank.id === next.tank.id (prev, next) => prev.tank.id === next.tank.id,
); );
@@ -49,10 +49,10 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
const filteredTanks = usePromise( const filteredTanks = usePromise(
() => () =>
filterTanks(filters, tankList).then((tanks) => filterTanks(filters, tankList).then((tanks) =>
tanks.filter(({ id }) => id in averageDefinitions.averages) tanks.filter(({ id }) => id in averageDefinitions.averages),
), ),
// react-promise-suspense has awful type annotations // react-promise-suspense has awful type annotations
[filters] as any [filters] as any,
); );
const tanksMapped = useMemo( const tanksMapped = useMemo(
() => () =>
@@ -60,7 +60,7 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
id, id,
...averageDefinitions.averages[id], ...averageDefinitions.averages[id],
})), })),
[filters] [filters],
); );
const tanksSorted = useMemo(() => { const tanksSorted = useMemo(() => {
const { playerCountPeriod } = Performance.state; const { playerCountPeriod } = Performance.state;
@@ -69,83 +69,85 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
case "accuracy": case "accuracy":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots) sort.direction *
(a.mu!.hits / a.mu!.shots - b.mu!.hits / b.mu!.shots),
); );
case "capture_points": case "capture_points":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.capture_points / a.mu.battles - (a.mu!.capture_points / a.mu!.battles -
b.mu.capture_points / b.mu.battles) b.mu!.capture_points / b.mu!.battles),
); );
case "damage": case "damage":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.damage_dealt / a.mu.battles - (a.mu!.damage_dealt / a.mu!.battles -
b.mu.damage_dealt / b.mu.battles) b.mu!.damage_dealt / b.mu!.battles),
); );
case "damage_ratio": case "damage_ratio":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.damage_dealt / a.mu.damage_received - (a.mu!.damage_dealt / a.mu!.damage_received -
b.mu.damage_dealt / b.mu.damage_received) b.mu!.damage_dealt / b.mu!.damage_received),
); );
case "damage_taken": case "damage_taken":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.damage_received / a.mu.battles - (a.mu!.damage_received / a.mu!.battles -
b.mu.damage_received / b.mu.battles) b.mu!.damage_received / b.mu!.battles),
); );
case "hits": case "hits":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles) (a.mu!.hits / a.mu!.battles - b.mu!.hits / b.mu!.battles),
); );
case "kills": case "kills":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles) (a.mu!.frags / a.mu!.battles - b.mu!.frags / b.mu!.battles),
); );
case "shots": case "shots":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles) (a.mu!.shots / a.mu!.battles - b.mu!.shots / b.mu!.battles),
); );
case "players": case "players":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod]) (a.samples![playerCountPeriod] - b.samples![playerCountPeriod]),
); );
case "spots": case "spots":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles) (a.mu!.spotted / a.mu!.battles - b.mu!.spotted / b.mu!.battles),
); );
case "survival": case "survival":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.survived_battles / a.mu.battles - (a.mu!.survived_battles / a.mu!.battles -
b.mu.survived_battles / b.mu.battles) b.mu!.survived_battles / b.mu!.battles),
); );
case "winrate": case "winrate":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * sort.direction *
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles) (a.mu!.wins / a.mu!.battles - b.mu!.wins / b.mu!.battles),
); );
case "xp": case "xp":
return tanksMapped.sort( return tanksMapped.sort(
(a, b) => (a, b) =>
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles) sort.direction *
(a.mu!.xp / a.mu!.battles - b.mu!.xp / b.mu!.battles),
); );
} }
}, [filters, sort]); }, [filters, sort]);
@@ -161,11 +163,11 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
const sum = useCallback( const sum = useCallback(
(slice: (tank: BlitzkitStats) => number) => { (slice: (tank: BlitzkitStats) => number) => {
return tanksSorted.reduce( return tanksSorted.reduce(
(acc, tank) => acc + tank.samples[playerCountPeriod] * slice(tank.mu), (acc, tank) => acc + tank.samples![playerCountPeriod] * slice(tank.mu!),
0 0,
); );
}, },
[tanksSorted] [tanksSorted],
); );
const battles = sum(({ battles }) => battles); const battles = sum(({ battles }) => battles);
@@ -209,7 +211,7 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
<Table.Cell align="center"> <Table.Cell align="center">
{formatCompact( {formatCompact(
locale, locale,
ratio * averageDefinitions.samples[playerCountPeriod] ratio * averageDefinitions.samples![playerCountPeriod],
)} )}
</Table.Cell> </Table.Cell>
<Table.Cell align="center"> <Table.Cell align="center">
@@ -255,12 +257,12 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
key={index} key={index}
onIntersection={() => { onIntersection={() => {
setLoadedRows((state) => setLoadedRows((state) =>
Math.min(state + 2, tanksSorted.length) Math.min(state + 2, tanksSorted.length),
); );
}} }}
/> />
); );
} },
)} )}
</Table.Body> </Table.Body>
); );
@@ -33,7 +33,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
style, style,
...props ...props
}: TankCardProps, }: TankCardProps,
ref ref,
) => { ) => {
const { unwrap, locale } = useLocale(); const { unwrap, locale } = useLocale();
const provideLink = !noLink && onSelect === undefined; const provideLink = !noLink && onSelect === undefined;
@@ -46,13 +46,13 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
tabIndex={onSelect ? 0 : undefined} tabIndex={onSelect ? 0 : undefined}
size="2" size="2"
color={ color={
tank.type === TankType.COLLECTOR tank.type === TankType.TANK_TYPE_COLLECTOR
? "blue" ? "blue"
: tank.type === TankType.PREMIUM : tank.type === TankType.TANK_TYPE_PREMIUM
? "amber" ? "amber"
: "gray" : "gray"
} }
highContrast={tank.type === TankType.RESEARCHABLE} highContrast={tank.type === TankType.TANK_TYPE_RESEARCHABLE}
onClick={() => { onClick={() => {
onSelect?.(tank); onSelect?.(tank);
TankopediaPersistent.mutate((draft) => { TankopediaPersistent.mutate((draft) => {
@@ -79,7 +79,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
}} }}
> >
<img <img
alt={unwrap(tank.name)} alt={unwrap(tank.name!)}
src={asset(`icons/tanks/big/${tank.id}.webp`)} src={asset(`icons/tanks/big/${tank.id}.webp`)}
className="image" className="image"
draggable={false} draggable={false}
@@ -95,7 +95,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
> >
<Icon className="class-icon" /> <Icon className="class-icon" />
<Text align="center" className="name"> <Text align="center" className="name">
{unwrap(tank.name)} {unwrap(tank.name!)}
</Text> </Text>
</Flex> </Flex>
@@ -115,5 +115,5 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
</LinkI18n> </LinkI18n>
</Text> </Text>
); );
} },
); );
@@ -19,13 +19,13 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
<LinkI18n <LinkI18n
locale={locale} locale={locale}
color={ color={
tank.type === TankType.COLLECTOR tank.type === TankType.TANK_TYPE_COLLECTOR
? "blue" ? "blue"
: tank.type === TankType.PREMIUM : tank.type === TankType.TANK_TYPE_PREMIUM
? "amber" ? "amber"
: "gray" : "gray"
} }
highContrast={tank.type === TankType.RESEARCHABLE} highContrast={tank.type === TankType.TANK_TYPE_RESEARCHABLE}
underline="hover" underline="hover"
wrap="nowrap" wrap="nowrap"
href={`/tanks/${tank.slug}`} href={`/tanks/${tank.slug}`}
@@ -55,7 +55,7 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
<ExperimentIcon style={{ width: "1em", height: "1em" }} /> <ExperimentIcon style={{ width: "1em", height: "1em" }} />
)} )}
{unwrap(tank.name)} {unwrap(tank.name!)}
</Flex> </Flex>
</LinkI18n> </LinkI18n>
</StickyRowHeaderCell> </StickyRowHeaderCell>
@@ -53,11 +53,11 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case "fire.reload": case "fire.reload":
return resolveReload(gun).toFixed(2); return resolveReload(gun).toFixed(2);
case "fire.standardPenetration": case "fire.standardPenetration":
return shell0?.penetration.near.toFixed(0); return shell0?.penetration!.near.toFixed(0);
case "fire.premiumPenetration": case "fire.premiumPenetration":
return shell1 ? shell1?.penetration.near.toFixed(0) : "--"; return shell1 ? shell1?.penetration!.near.toFixed(0) : "--";
case "fire.tertiaryPenetration": case "fire.tertiaryPenetration":
return shell1 ? shell2?.penetration.near.toFixed(0) : "--"; return shell1 ? shell2?.penetration!.near.toFixed(0) : "--";
case "fire.shellVelocity": case "fire.shellVelocity":
return shell0.velocity.toFixed(0); return shell0.velocity.toFixed(0);
case "fire.shellCapacity": case "fire.shellCapacity":
@@ -66,12 +66,12 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
return shell0.range.toFixed(0); return shell0.range.toFixed(0);
case "fire.gunDepression": case "fire.gunDepression":
return ( return (
gunModelDefinition.pitch.max + gunModelDefinition.pitch!.max +
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0) (tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
).toFixed(1); ).toFixed(1);
case "fire.gunElevation": case "fire.gunElevation":
return ( return (
-gunModelDefinition.pitch.min - -gunModelDefinition.pitch!.min -
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0) (tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
).toFixed(1); ).toFixed(1);
case "maneuverability.forwardsSpeed": case "maneuverability.forwardsSpeed":
@@ -113,8 +113,8 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case "survivability.volume": { case "survivability.volume": {
const dimensions = normalizeBoundingBox( const dimensions = normalizeBoundingBox(
unionBoundingBox( unionBoundingBox(
tankModelDefinition.bounding_box, tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box, turretModelDefinition.bounding_box!,
), ),
); );
@@ -124,8 +124,8 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case "survivability.length": { case "survivability.length": {
const bounds = normalizeBoundingBox( const bounds = normalizeBoundingBox(
unionBoundingBox( unionBoundingBox(
tankModelDefinition.bounding_box, tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box, turretModelDefinition.bounding_box!,
), ),
); );
@@ -85,11 +85,11 @@ for (const gameModeIdString in gameModeRoleSets) {
outerLoop: for (const id of role.consumables) { outerLoop: for (const id of role.consumables) {
const name = const name =
consumableDefinitions.consumables[id].name.locales[locales.default]; consumableDefinitions.consumables[id].name!.locales[locales.default];
for (const otherConsumable of consumables.values()) { for (const otherConsumable of consumables.values()) {
const otherName = const otherName =
consumableDefinitions.consumables[otherConsumable].name.locales[ consumableDefinitions.consumables[otherConsumable].name!.locales[
locales.default locales.default
]; ];
@@ -102,11 +102,11 @@ for (const gameModeIdString in gameModeRoleSets) {
outerLoop: for (const id of role.provisions) { outerLoop: for (const id of role.provisions) {
const name = const name =
provisionDefinitions.provisions[id].name.locales[locales.default]; provisionDefinitions.provisions[id].name!.locales[locales.default];
for (const otherProvision of provisions.values()) { for (const otherProvision of provisions.values()) {
const otherName = const otherName =
provisionDefinitions.provisions[otherProvision].name.locales[ provisionDefinitions.provisions[otherProvision].name!.locales[
locales.default locales.default
]; ];
@@ -129,7 +129,7 @@ const consumablesArray = Object.values(consumableDefinitions.consumables)
.filter( .filter(
(consumable) => (consumable) =>
!consumable.game_mode_exclusive && !consumable.game_mode_exclusive &&
consumable.name.locales[locales.default], consumable.name!.locales[locales.default],
) )
.map((consumable) => consumable.id); .map((consumable) => consumable.id);
const provisionsArray = Array.from( const provisionsArray = Array.from(
@@ -137,11 +137,11 @@ const provisionsArray = Array.from(
.filter( .filter(
(provision) => (provision) =>
!provision.game_mode_exclusive && !provision.game_mode_exclusive &&
provision.name.locales[locales.default], provision.name!.locales[locales.default],
) )
.reduce((uniqueMap, consumable) => { .reduce((uniqueMap, consumable) => {
if (!uniqueMap.has(consumable.name.locales[locales.default])) { if (!uniqueMap.has(consumable.name!.locales[locales.default])) {
uniqueMap.set(consumable.name.locales[locales.default], consumable.id); uniqueMap.set(consumable.name!.locales[locales.default], consumable.id);
} }
return uniqueMap; return uniqueMap;
@@ -149,10 +149,10 @@ const provisionsArray = Array.from(
.values(), .values(),
); );
const shellTypeIcons: Record<ShellType, string> = { const shellTypeIcons: Record<ShellType, string> = {
[ShellType.AP]: "ap", [ShellType.SHELL_TYPE_AP]: "ap",
[ShellType.APCR]: "ap_cr", [ShellType.SHELL_TYPE_APCR]: "ap_cr",
[ShellType.HE]: "he", [ShellType.SHELL_TYPE_HE]: "he",
[ShellType.HEAT]: "hc", [ShellType.SHELL_TYPE_HEAT]: "hc",
}; };
const GUN_TYPE_ICONS: Record< const GUN_TYPE_ICONS: Record<
@@ -165,9 +165,9 @@ const GUN_TYPE_ICONS: Record<
}; };
const TANK_TYPE_COLORS: Record<TankType, string> = { const TANK_TYPE_COLORS: Record<TankType, string> = {
[TankType.RESEARCHABLE]: "gray", [TankType.TANK_TYPE_RESEARCHABLE]: "gray",
[TankType.PREMIUM]: "amber", [TankType.TANK_TYPE_PREMIUM]: "amber",
[TankType.COLLECTOR]: "blue", [TankType.TANK_TYPE_COLLECTOR]: "blue",
}; };
const GUN_TYPES = Object.keys( const GUN_TYPES = Object.keys(
@@ -566,12 +566,12 @@ function TypeFilter() {
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<Button color="gray" variant="surface"> <Button color="gray" variant="surface">
<Flex gap="1"> <Flex gap="1">
{types.map((tankType, index) => { {types.map((tankType) => {
return ( return (
<ResearchedIcon <ResearchedIcon
style={{ style={{
color: `var(--${TANK_TYPE_COLORS[tankType]}-${ color: `var(--${TANK_TYPE_COLORS[tankType]}-${
tankType === TankType.RESEARCHABLE ? "12" : "11" tankType === TankType.TANK_TYPE_RESEARCHABLE ? "12" : "11"
})`, })`,
filter: "drop-shadow(0 0 var(--space-1) var(--black-a11))", filter: "drop-shadow(0 0 var(--space-1) var(--black-a11))",
opacity: 1, opacity: 1,
@@ -609,7 +609,7 @@ function TypeFilter() {
<ResearchedIcon <ResearchedIcon
style={{ style={{
color: `var(--${TANK_TYPE_COLORS[tankType]}-${ color: `var(--${TANK_TYPE_COLORS[tankType]}-${
tankType === TankType.RESEARCHABLE ? "12" : "11" tankType === TankType.TANK_TYPE_RESEARCHABLE ? "12" : "11"
})`, })`,
opacity: 1, opacity: 1,
width: "1.25em", width: "1.25em",
@@ -941,7 +941,7 @@ function ConsumablesFilter() {
src={asset(`icons/consumables/${consumable}.webp`)} src={asset(`icons/consumables/${consumable}.webp`)}
/> />
{unwrap(consumableDefinition.name)} {unwrap(consumableDefinition.name!)}
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
); );
})} })}
@@ -1035,7 +1035,7 @@ function ProvisionsFilter() {
src={asset(`icons/provisions/${provision}.webp`)} src={asset(`icons/provisions/${provision}.webp`)}
/> />
{unwrap(provisionDefinition.name)} {unwrap(provisionDefinition.name!)}
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
); );
})} })}
@@ -1125,9 +1125,9 @@ function GameModeAbilitiesFilter() {
<DropdownMenu.Label> <DropdownMenu.Label>
{hasBoth {hasBoth
? literals(strings.website.common.tank_search.active, { ? literals(strings.website.common.tank_search.active, {
name: unwrap(gameMode.name), name: unwrap(gameMode.name!),
}) })
: unwrap(gameMode.name)} : unwrap(gameMode.name!)}
</DropdownMenu.Label> </DropdownMenu.Label>
)} )}
@@ -1164,7 +1164,7 @@ function GameModeAbilitiesFilter() {
src={asset(`icons/consumables/${consumable}.webp`)} src={asset(`icons/consumables/${consumable}.webp`)}
/> />
{unwrap(abilityDefinition.name)} {unwrap(abilityDefinition.name!)}
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
); );
} }
@@ -1174,9 +1174,9 @@ function GameModeAbilitiesFilter() {
<DropdownMenu.Label> <DropdownMenu.Label>
{hasBoth {hasBoth
? literals(strings.website.common.tank_search.passive, { ? literals(strings.website.common.tank_search.passive, {
name: unwrap(gameMode.name), name: unwrap(gameMode.name!),
}) })
: unwrap(gameMode.name)} : unwrap(gameMode.name!)}
</DropdownMenu.Label> </DropdownMenu.Label>
)} )}
@@ -1213,7 +1213,7 @@ function GameModeAbilitiesFilter() {
src={asset(`icons/provisions/${provision}.webp`)} src={asset(`icons/provisions/${provision}.webp`)}
/> />
{unwrap(powerDefinition.name)} {unwrap(powerDefinition.name!)}
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
); );
} }
@@ -20,7 +20,7 @@ export function QuickLink({ topResult }: QuickLinkProps) {
<TextField.Slot> <TextField.Slot>
<LinkI18n locale={locale} href={`/tanks/${topResult.slug}`}> <LinkI18n locale={locale} href={`/tanks/${topResult.slug}`}>
<Button variant="ghost"> <Button variant="ghost">
{unwrap(topResult.name)} <CaretRightIcon /> {unwrap(topResult.name!)} <CaretRightIcon />
</Button> </Button>
</LinkI18n> </LinkI18n>
</TextField.Slot> </TextField.Slot>
@@ -67,7 +67,7 @@ export const TankSearch = memo<TankSearchProps>(
return filterTanks( return filterTanks(
tankFilters, tankFilters,
awaitedTanksDefinitionsArray, awaitedTanksDefinitionsArray,
wargaming?.id wargaming?.id,
); );
} else { } else {
return Promise.resolve( return Promise.resolve(
@@ -75,12 +75,12 @@ export const TankSearch = memo<TankSearchProps>(
.go(tankFilters.search, tankNames, { .go(tankFilters.search, tankNames, {
keys: SEARCH_KEYS, keys: SEARCH_KEYS,
}) })
.map((result) => tankDefinitions.tanks[result.obj.id]) .map((result) => tankDefinitions.tanks[result.obj.id]),
); );
} }
}, },
// react-promise-suspense has awful type annotations // react-promise-suspense has awful type annotations
[tankFilters] as any [tankFilters] as any,
); );
const sorted = useMemo(() => { const sorted = useMemo(() => {
if (tankFilters.search === null) { if (tankFilters.search === null) {
@@ -104,19 +104,19 @@ export const TankSearch = memo<TankSearchProps>(
case "survivability.viewRange": case "survivability.viewRange":
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range,
); );
break; break;
case "survivability.camouflageStill": case "survivability.camouflageStill":
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => a.camouflage_still - b.camouflage_still (a, b) => a.camouflage_still - b.camouflage_still,
); );
break; break;
case "survivability.camouflageMoving": case "survivability.camouflageMoving":
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => a.camouflage_moving - b.camouflage_moving (a, b) => a.camouflage_moving - b.camouflage_moving,
); );
break; break;
@@ -126,7 +126,7 @@ export const TankSearch = memo<TankSearchProps>(
a.camouflage_still * a.camouflage_still *
a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss - a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss -
b.camouflage_still * b.camouflage_still *
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss,
); );
break; break;
@@ -140,15 +140,15 @@ export const TankSearch = memo<TankSearchProps>(
bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox( const aSize = normalizeBoundingBox(
unionBoundingBox( unionBoundingBox(
aTankModelDefinition.bounding_box, aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box aTurretModelDefinition.bounding_box!,
) ),
); );
const bSize = normalizeBoundingBox( const bSize = normalizeBoundingBox(
unionBoundingBox( unionBoundingBox(
bTankModelDefinition.bounding_box, bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box bTurretModelDefinition.bounding_box!,
) ),
); );
const aVolume = aSize.x * aSize.y * aSize.z; const aVolume = aSize.x * aSize.y * aSize.z;
const bVolume = bSize.x * bSize.y * bSize.z; const bVolume = bSize.x * bSize.y * bSize.z;
@@ -167,15 +167,15 @@ export const TankSearch = memo<TankSearchProps>(
bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox( const aSize = normalizeBoundingBox(
unionBoundingBox( unionBoundingBox(
aTankModelDefinition.bounding_box, aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box aTurretModelDefinition.bounding_box!,
) ),
); );
const bSize = normalizeBoundingBox( const bSize = normalizeBoundingBox(
unionBoundingBox( unionBoundingBox(
bTankModelDefinition.bounding_box, bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box bTurretModelDefinition.bounding_box!,
) ),
); );
const aLength = Math.max(aSize.x, aSize.y, aSize.z); const aLength = Math.max(aSize.x, aSize.y, aSize.z);
const bLength = Math.max(bSize.x, bSize.y, bSize.z); const bLength = Math.max(bSize.x, bSize.y, bSize.z);
@@ -189,12 +189,12 @@ export const TankSearch = memo<TankSearchProps>(
(a, b) => (a, b) =>
resolveDpm( resolveDpm(
a.turrets.at(-1)!.guns.at(-1)!, a.turrets.at(-1)!.guns.at(-1)!,
a.turrets.at(-1)!.guns.at(-1)!.shells[0] a.turrets.at(-1)!.guns.at(-1)!.shells[0],
) - ) -
resolveDpm( resolveDpm(
b.turrets.at(-1)!.guns.at(-1)!, b.turrets.at(-1)!.guns.at(-1)!,
b.turrets.at(-1)!.guns.at(-1)!.shells[0] b.turrets.at(-1)!.guns.at(-1)!.shells[0],
) ),
); );
break; break;
@@ -204,15 +204,15 @@ export const TankSearch = memo<TankSearchProps>(
(a.turrets.at(-1)!.guns.at(-1)!.shells[1] (a.turrets.at(-1)!.guns.at(-1)!.shells[1]
? resolveDpm( ? resolveDpm(
a.turrets.at(-1)!.guns.at(-1)!, a.turrets.at(-1)!.guns.at(-1)!,
a.turrets.at(-1)!.guns.at(-1)!.shells[1] a.turrets.at(-1)!.guns.at(-1)!.shells[1],
) )
: 0) - : 0) -
(b.turrets.at(-1)!.guns.at(-1)!.shells[1] (b.turrets.at(-1)!.guns.at(-1)!.shells[1]
? resolveDpm( ? resolveDpm(
b.turrets.at(-1)!.guns.at(-1)!, b.turrets.at(-1)!.guns.at(-1)!,
b.turrets.at(-1)!.guns.at(-1)!.shells[1] b.turrets.at(-1)!.guns.at(-1)!.shells[1],
) )
: 0) : 0),
); );
break; break;
@@ -220,7 +220,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
resolveReload(a.turrets.at(-1)!.guns.at(-1)!) - resolveReload(a.turrets.at(-1)!.guns.at(-1)!) -
resolveReload(b.turrets.at(-1)!.guns.at(-1)!) resolveReload(b.turrets.at(-1)!.guns.at(-1)!),
); );
break; break;
@@ -228,35 +228,35 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber - a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber,
); );
break; break;
case "fire.standardPenetration": case "fire.standardPenetration":
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near - a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near,
); );
break; break;
case "fire.premiumPenetration": case "fire.premiumPenetration":
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ?? (a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration!.near ??
0) - 0) -
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ?? (b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration!.near ??
0) 0),
); );
break; break;
case "fire.tertiaryPenetration": case "fire.tertiaryPenetration":
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ?? (a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration!.near ??
0) - 0) -
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ?? (b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration!.near ??
0) 0),
); );
break; break;
@@ -264,7 +264,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage - a.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage,
); );
break; break;
@@ -272,7 +272,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage - a.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage,
); );
break; break;
@@ -280,7 +280,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity - a.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity,
); );
break; break;
@@ -288,7 +288,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].range - a.turrets.at(-1)!.guns.at(-1)!.shells[0].range -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].range b.turrets.at(-1)!.guns.at(-1)!.shells[0].range,
); );
break; break;
@@ -296,7 +296,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shell_capacity - a.turrets.at(-1)!.guns.at(-1)!.shell_capacity -
b.turrets.at(-1)!.guns.at(-1)!.shell_capacity b.turrets.at(-1)!.guns.at(-1)!.shell_capacity,
); );
break; break;
@@ -304,7 +304,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.aim_time - a.turrets.at(-1)!.guns.at(-1)!.aim_time -
b.turrets.at(-1)!.guns.at(-1)!.aim_time b.turrets.at(-1)!.guns.at(-1)!.aim_time,
); );
break; break;
@@ -312,7 +312,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.dispersion_base - a.turrets.at(-1)!.guns.at(-1)!.dispersion_base -
b.turrets.at(-1)!.guns.at(-1)!.dispersion_base b.turrets.at(-1)!.guns.at(-1)!.dispersion_base,
); );
break; break;
@@ -320,7 +320,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.tracks.at(-1)!.dispersion_move - a.tracks.at(-1)!.dispersion_move -
b.tracks.at(-1)!.dispersion_move b.tracks.at(-1)!.dispersion_move,
); );
break; break;
@@ -328,13 +328,13 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id] modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.max + .guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch!.max +
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ?? (modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
0) - 0) -
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id] modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.max - .guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch!.max -
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ?? (modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
0) 0),
); );
break; break;
@@ -342,31 +342,31 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id] modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.min + .guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch!.min +
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ?? (modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
0) - 0) -
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id] modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.min - .guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch!.min -
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ?? (modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
0) 0),
); );
break; break;
case "maneuverability.forwardsSpeed": case "maneuverability.forwardsSpeed":
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => a.speed_forwards - b.speed_forwards (a, b) => a.speed_forwards - b.speed_forwards,
); );
break; break;
case "maneuverability.backwardsSpeed": case "maneuverability.backwardsSpeed":
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => a.speed_backwards - b.speed_backwards (a, b) => a.speed_backwards - b.speed_backwards,
); );
break; break;
case "maneuverability.power": case "maneuverability.power":
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power (a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power,
); );
break; break;
@@ -384,7 +384,7 @@ export const TankSearch = memo<TankSearchProps>(
b.engines.at(-1)!.weight + b.engines.at(-1)!.weight +
b.tracks.at(-1)!.weight + b.tracks.at(-1)!.weight +
b.turrets.at(-1)!.weight + b.turrets.at(-1)!.weight +
b.turrets.at(-1)!.guns.at(-1)!.weight) b.turrets.at(-1)!.guns.at(-1)!.weight),
); );
break; break;
@@ -400,7 +400,7 @@ export const TankSearch = memo<TankSearchProps>(
b.engines.at(-1)!.weight + b.engines.at(-1)!.weight +
b.tracks.at(-1)!.weight + b.tracks.at(-1)!.weight +
b.turrets.at(-1)!.weight + b.turrets.at(-1)!.weight +
b.turrets.at(-1)!.guns.at(-1)!.weight) b.turrets.at(-1)!.guns.at(-1)!.weight),
); );
break; break;
@@ -408,7 +408,7 @@ export const TankSearch = memo<TankSearchProps>(
sorted = filtered.sort( sorted = filtered.sort(
(a, b) => (a, b) =>
a.tracks.at(-1)!.traverse_speed - a.tracks.at(-1)!.traverse_speed -
b.tracks.at(-1)!.traverse_speed b.tracks.at(-1)!.traverse_speed,
); );
break; break;
} }
@@ -520,12 +520,12 @@ export const TankSearch = memo<TankSearchProps>(
key={index} key={index}
onIntersection={() => { onIntersection={() => {
setLoadedCards((state) => setLoadedCards((state) =>
Math.min(state + 2, sorted.length) Math.min(state + 2, sorted.length),
); );
}} }}
/> />
); );
} },
)} )}
</TankCardWrapper> </TankCardWrapper>
)} )}
@@ -538,15 +538,15 @@ export const TankSearch = memo<TankSearchProps>(
<TankCardWrapper> <TankCardWrapper>
{times( {times(
Math.round( Math.round(
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random() skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random(),
), ),
(index) => ( (index) => (
<SkeletonTankCard key={index} /> <SkeletonTankCard key={index} />
) ),
)} )}
</TankCardWrapper> </TankCardWrapper>
)} )}
</Flex> </Flex>
); );
} },
); );
@@ -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;
@@ -71,7 +71,7 @@ export function Crew({ stats }: StatsAcceptorProps) {
decimals={0} decimals={0}
noRanking noRanking
value={() => value={() =>
(member.type === CrewType.COMMANDER (member.type === CrewType.CREW_TYPE_COMMANDER
? commanderMastery ? commanderMastery
: commanderMastery * 1.1) * 100 : commanderMastery * 1.1) * 100
} }
@@ -93,7 +93,7 @@ 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>
@@ -38,7 +38,7 @@ export function Firepower({
const selected = thisShell.id === shell.id; const selected = thisShell.id === shell.id;
return ( return (
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}> <Tooltip content={unwrap(thisShell.name!)} key={thisShell.id}>
<IconButton <IconButton
color={selected ? undefined : "gray"} color={selected ? undefined : "gray"}
variant={selected ? "solid" : "soft"} variant={selected ? "solid" : "soft"}
@@ -59,7 +59,7 @@ export function Firepower({
}} }}
> >
<img <img
alt={unwrap(thisShell.name)} alt={unwrap(thisShell.name!)}
width={16} width={16}
height={16} height={16}
src={asset(`icons/shells/${thisShell.icon}.webp`)} src={asset(`icons/shells/${thisShell.icon}.webp`)}
@@ -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: 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,
{ distance: 100 } { distance: 100 },
)} )}
/> />
<InfoWithDelta <InfoWithDelta
@@ -249,7 +249,7 @@ export function Firepower({
/> />
<InfoWithDelta value="gunDepression" stats={stats} decimals={1} indent /> <InfoWithDelta value="gunDepression" stats={stats} decimals={1} indent />
<InfoWithDelta stats={stats} decimals={1} indent value="gunElevation" /> <InfoWithDelta stats={stats} decimals={1} indent value="gunElevation" />
{gunModelDefinition.pitch.front && ( {gunModelDefinition.pitch!.front && (
<> <>
<InfoWithDelta <InfoWithDelta
stats={stats} stats={stats}
@@ -265,7 +265,7 @@ export function Firepower({
/> />
</> </>
)} )}
{gunModelDefinition.pitch.back && ( {gunModelDefinition.pitch!.back && (
<> <>
<InfoWithDelta <InfoWithDelta
stats={stats} stats={stats}
@@ -1,13 +1,13 @@
import type { QuicklimeEvent } from 'quicklime'; import type { QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from "react";
import type { Group } from 'three'; import type { Group } from "three";
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from "../../../../../../../../core/awaitables/modelDefinitions";
import { import {
modelTransformEvent, modelTransformEvent,
type ModelTransformEventData, type ModelTransformEventData,
} from '../../../../../../../../core/blitzkit/modelTransform'; } from "../../../../../../../../core/blitzkit/modelTransform";
import { Duel } from '../../../../../../../../stores/duel'; import { Duel } from "../../../../../../../../stores/duel";
import { ModelChunk } from './ModelChunk'; import { ModelChunk } from "./ModelChunk";
const modelDefinitions = await awaitableModelDefinitions; const modelDefinitions = await awaitableModelDefinitions;
@@ -39,8 +39,8 @@ export function CanvasContent() {
}, []); }, []);
return ( return (
<> <>
<group ref={hullWrapper} position={[0, 0, turretModel.gun_origin.z]}> <group ref={hullWrapper} position={[0, 0, turretModel.gun_origin!.z]}>
<group position={[0, 0, -turretModel.gun_origin.z]}> <group position={[0, 0, -turretModel.gun_origin!.z]}>
<ModelChunk only="body" /> <ModelChunk only="body" />
</group> </group>
</group> </group>
@@ -1,13 +1,13 @@
import { mauveDark } from '@radix-ui/colors'; import { mauveDark } from "@radix-ui/colors";
import { LineBasicMaterial, MeshBasicMaterial } from 'three'; import { LineBasicMaterial, MeshBasicMaterial } from "three";
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from "../../../../../../../../core/awaitables/modelDefinitions";
import { jsxTree } from '../../../../../../../../core/blitzkit/jsxTree'; import { jsxTree } from "../../../../../../../../core/blitzkit/jsxTree";
import { useModel } from '../../../../../../../../hooks/useModel'; import { useModel } from "../../../../../../../../hooks/useModel";
import { Duel } from '../../../../../../../../stores/duel'; import { Duel } from "../../../../../../../../stores/duel";
const modelDefinitions = await awaitableModelDefinitions; const modelDefinitions = await awaitableModelDefinitions;
const surfaceMaterial: Record<Props['only'], MeshBasicMaterial> = { const surfaceMaterial: Record<Props["only"], MeshBasicMaterial> = {
gun: new MeshBasicMaterial({ gun: new MeshBasicMaterial({
color: mauveDark.mauve8, color: mauveDark.mauve8,
toneMapped: false, toneMapped: false,
@@ -23,7 +23,7 @@ const surfaceMaterial: Record<Props['only'], MeshBasicMaterial> = {
opacity: 0.5, opacity: 0.5,
}), }),
}; };
const outlineMaterial: Record<Props['only'], LineBasicMaterial> = { const outlineMaterial: Record<Props["only"], LineBasicMaterial> = {
gun: new LineBasicMaterial({ gun: new LineBasicMaterial({
color: mauveDark.mauve11, color: mauveDark.mauve11,
toneMapped: false, toneMapped: false,
@@ -41,7 +41,7 @@ const outlineMaterial: Record<Props['only'], LineBasicMaterial> = {
}; };
interface Props { interface Props {
only: 'gun' | 'turret' | 'body'; only: "gun" | "turret" | "body";
} }
export function ModelChunk({ only }: Props) { export function ModelChunk({ only }: Props) {
@@ -53,8 +53,8 @@ export function ModelChunk({ only }: Props) {
const turretModel = tankModel.turrets[turret.id]; const turretModel = tankModel.turrets[turret.id];
const gunModel = turretModel.guns[gun.id]; const gunModel = turretModel.guns[gun.id];
const originZ = tankModel.turret_origin.z + turretModel.gun_origin.z; const originZ = tankModel.turret_origin!.z + turretModel.gun_origin!.z;
const originY = tankModel.turret_origin.y + turretModel.gun_origin.y; const originY = tankModel.turret_origin!.y + turretModel.gun_origin!.y;
const { gltf } = useModel(tank.id); const { gltf } = useModel(tank.id);
const nodes = Object.values(gltf.nodes); const nodes = Object.values(gltf.nodes);
@@ -64,25 +64,25 @@ export function ModelChunk({ only }: Props) {
{nodes.map((node) => { {nodes.map((node) => {
const isCurrentMantlet = const isCurrentMantlet =
node.name === node.name ===
`gun_${gunModel.model_id.toString().padStart(2, '0')}_mask`; `gun_${gunModel.model_id.toString().padStart(2, "0")}_mask`;
const isCurrentGun = const isCurrentGun =
node.name === `gun_${gunModel.model_id.toString().padStart(2, '0')}`; node.name === `gun_${gunModel.model_id.toString().padStart(2, "0")}`;
const isGun = isCurrentGun || isCurrentMantlet; const isGun = isCurrentGun || isCurrentMantlet;
const isCurrentTurret = const isCurrentTurret =
node.name === node.name ===
`turret_${turretModel.model_id.toString().padStart(2, '0')}`; `turret_${turretModel.model_id.toString().padStart(2, "0")}`;
const isTurret = isCurrentTurret; const isTurret = isCurrentTurret;
const isHull = node.name === 'hull'; const isHull = node.name === "hull";
const isWheel = node.name.startsWith('chassis_wheel_'); const isWheel = node.name.startsWith("chassis_wheel_");
const isTrack = node.name.startsWith('chassis_track_'); const isTrack = node.name.startsWith("chassis_track_");
const isBody = isHull || isWheel || isTrack; const isBody = isHull || isWheel || isTrack;
if ( if (
(only === 'gun' && !isGun) || (only === "gun" && !isGun) ||
(only === 'turret' && !isTurret) || (only === "turret" && !isTurret) ||
(only === 'body' && !isBody) (only === "body" && !isBody)
) { ) {
return null; return null;
} }
@@ -51,21 +51,21 @@ export function GunFlexibilityVisualizer({
const [yaw, setYaw] = useState(0); const [yaw, setYaw] = useState(0);
const [pitch, setPitch] = useState(0); const [pitch, setPitch] = useState(0);
const [minPitch, setMinPitch] = useState( const [minPitch, setMinPitch] = useState(
-degToRad(gunModel.pitch.front?.max ?? gunModel.pitch.max) -degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max),
); );
const [maxPitch, setMaxPitch] = useState( const [maxPitch, setMaxPitch] = useState(
-degToRad(gunModel.pitch.front?.min ?? gunModel.pitch.min) -degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min),
); );
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
const highlighter = useRef<HTMLDivElement>(null); const highlighter = useRef<HTMLDivElement>(null);
const pointer = useRef<HTMLDivElement>(null); const pointer = useRef<HTMLDivElement>(null);
const p = gunModel.pitch; const p = gunModel.pitch!;
const y = turretModel.yaw; const y = turretModel.yaw;
const f = p.front; const f = p.front;
const b = p.back; const b = p.back;
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION; const t = gunModel.pitch!.transition ?? DEFAULT_PITCH_TRANSITION;
let d = ""; let d = "";
@@ -223,12 +223,12 @@ export function GunFlexibilityVisualizer({
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const min = applyPitchYawLimits( const min = applyPitchYawLimits(
@@ -237,7 +237,7 @@ export function GunFlexibilityVisualizer({
p, p,
y, y,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
const max = applyPitchYawLimits( const max = applyPitchYawLimits(
Math.PI, Math.PI,
@@ -245,7 +245,7 @@ export function GunFlexibilityVisualizer({
p, p,
y, y,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
yaw = min[1]; yaw = min[1];
@@ -15,7 +15,7 @@ export function ShellSwitcher() {
const selected = thisShell.id === shell.id; const selected = thisShell.id === shell.id;
return ( return (
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}> <Tooltip content={unwrap(thisShell.name!)} key={thisShell.id}>
<IconButton <IconButton
size="3" size="3"
color={selected ? undefined : "gray"} color={selected ? undefined : "gray"}
@@ -36,7 +36,7 @@ export function ShellSwitcher() {
}} }}
> >
<img <img
alt={unwrap(thisShell.name)} alt={unwrap(thisShell.name!)}
style={{ width: "1.125rem", height: "1.125rem" }} style={{ width: "1.125rem", height: "1.125rem" }}
src={asset(`icons/shells/${thisShell.icon}.webp`)} src={asset(`icons/shells/${thisShell.icon}.webp`)}
/> />
@@ -26,13 +26,13 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
const consumables = Duel.use((state) => state.protagonist.consumables); const consumables = Duel.use((state) => state.protagonist.consumables);
const camouflage = Duel.use((state) => state.protagonist.camouflage); const camouflage = Duel.use((state) => state.protagonist.camouflage);
const assaultDistance = Duel.use( const assaultDistance = Duel.use(
(state) => state.protagonist.assaultDistance (state) => state.protagonist.assaultDistance,
); );
const equipmentMatrix = Duel.use( const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix (state) => state.protagonist.equipmentMatrix,
); );
const { tank, turret, gun, engine, track, shell } = Duel.use( const { tank, turret, gun, engine, track, shell } = Duel.use(
(state) => state.protagonist (state) => state.protagonist,
); );
const stockEngine = tank.engines[0]; const stockEngine = tank.engines[0];
const stockTrack = tank.tracks[0]; const stockTrack = tank.tracks[0];
@@ -67,7 +67,7 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
tankModelDefinition, tankModelDefinition,
equipmentDefinitions: equipmentDefinitions, equipmentDefinitions: equipmentDefinitions,
provisionDefinitions: provisionDefinitions, provisionDefinitions: provisionDefinitions,
} },
); );
const hasImprovedVerticalStabilizer = useEquipment(122); const hasImprovedVerticalStabilizer = useEquipment(122);
@@ -77,10 +77,10 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch, modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw, modelTransformEvent.last!.yaw,
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
@@ -43,11 +43,11 @@ interface ModuleWithUnlocks {
} }
const moduleTypeTypeIconName: Record<ModuleType, string> = { const moduleTypeTypeIconName: Record<ModuleType, string> = {
[ModuleType.VEHICLE]: "", [ModuleType.MODULE_TYPE_VEHICLE]: "",
[ModuleType.ENGINE]: "engine", [ModuleType.MODULE_TYPE_ENGINE]: "engine",
[ModuleType.TRACKS]: "chassis", [ModuleType.MODULE_TYPE_TRACKS]: "chassis",
[ModuleType.TURRET]: "turret", [ModuleType.MODULE_TYPE_TURRET]: "turret",
[ModuleType.GUN]: "gun", [ModuleType.MODULE_TYPE_GUN]: "gun",
}; };
function ModuleButton({ function ModuleButton({
@@ -63,7 +63,7 @@ function ModuleButton({
top: boolean; top: boolean;
onClick: () => void; onClick: () => void;
}) { }) {
const isTank = unlock.type === ModuleType.VEHICLE; const isTank = unlock.type === ModuleType.MODULE_TYPE_VEHICLE;
const { locale } = useLocale(); const { locale } = useLocale();
const button = ( const button = (
@@ -91,7 +91,7 @@ function ModuleButton({
{TIER_ROMAN_NUMERALS[tier]} {TIER_ROMAN_NUMERALS[tier]}
</Text> </Text>
{unlock.cost.value > 0 && ( {unlock.cost!.value > 0 && (
<Flex <Flex
style={{ style={{
position: "absolute", position: "absolute",
@@ -103,12 +103,12 @@ function ModuleButton({
gap={isTank ? "1" : undefined} gap={isTank ? "1" : undefined}
> >
<Text size="1" color="gray"> <Text size="1" color="gray">
{formatCompact(locale, unlock.cost.value)} {formatCompact(locale, unlock.cost!.value)}
</Text> </Text>
<img <img
alt={unlock.cost.type} alt={unlock.cost!.type}
src={asset(`icons/currencies/${unlock.cost.type}.webp`)} src={asset(`icons/currencies/${unlock.cost!.type}.webp`)}
style={{ style={{
width: 12, width: 12,
height: 12, height: 12,
@@ -171,16 +171,16 @@ export function Modules() {
function setByUnlock(unlock: Unlock) { function setByUnlock(unlock: Unlock) {
Duel.mutate((draft) => { Duel.mutate((draft) => {
if (unlock.type === ModuleType.TURRET) { if (unlock.type === ModuleType.MODULE_TYPE_TURRET) {
draft.protagonist.turret = draft.protagonist.tank.turrets.find( draft.protagonist.turret = draft.protagonist.tank.turrets.find(
(turret) => turret.id === unlock.id (turret) => turret.id === unlock.id,
)!; )!;
// Check if current gun exists in new turret // Check if current gun exists in new turret
const gunInNewTurret = draft.protagonist.turret.guns.find( const gunInNewTurret = draft.protagonist.turret.guns.find(
(gun) => gun.id === draft.protagonist.gun.id (gun) => gun.id === draft.protagonist.gun.id,
); );
if (gunInNewTurret) { if (gunInNewTurret) {
// Gun exists - get it FROM this turret // Gun exists - get it FROM this turret
draft.protagonist.gun = gunInNewTurret; draft.protagonist.gun = gunInNewTurret;
@@ -190,9 +190,9 @@ export function Modules() {
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!; draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
draft.protagonist.shell = draft.protagonist.gun.shells[0]; draft.protagonist.shell = draft.protagonist.gun.shells[0];
} }
} else if (unlock.type === ModuleType.GUN) { } else if (unlock.type === ModuleType.MODULE_TYPE_GUN) {
const gunInTurret = draft.protagonist.turret.guns.find( const gunInTurret = draft.protagonist.turret.guns.find(
(gun) => gun.id === unlock.id (gun) => gun.id === unlock.id,
); );
if (gunInTurret) { if (gunInTurret) {
draft.protagonist.gun = gunInTurret; draft.protagonist.gun = gunInTurret;
@@ -200,23 +200,23 @@ export function Modules() {
} else { } else {
// TODO: warn somehow? // TODO: warn somehow?
const suitableTurret = draft.protagonist.tank.turrets.find((turret) => const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
turret.guns.some((gun) => gun.id === unlock.id) turret.guns.some((gun) => gun.id === unlock.id),
)!; )!;
const gunInSuitableTurret = suitableTurret.guns.find( const gunInSuitableTurret = suitableTurret.guns.find(
(gun) => gun.id === unlock.id (gun) => gun.id === unlock.id,
)!; )!;
draft.protagonist.turret = suitableTurret; draft.protagonist.turret = suitableTurret;
draft.protagonist.gun = gunInSuitableTurret; draft.protagonist.gun = gunInSuitableTurret;
draft.protagonist.shell = gunInSuitableTurret.shells[0]; draft.protagonist.shell = gunInSuitableTurret.shells[0];
} }
} else if (unlock.type === ModuleType.ENGINE) { } else if (unlock.type === ModuleType.MODULE_TYPE_ENGINE) {
draft.protagonist.engine = draft.protagonist.tank.engines.find( draft.protagonist.engine = draft.protagonist.tank.engines.find(
(engine) => engine.id === unlock.id (engine) => engine.id === unlock.id,
)!; )!;
} else if (unlock.type === ModuleType.TRACKS) { } else if (unlock.type === ModuleType.MODULE_TYPE_TRACKS) {
draft.protagonist.track = draft.protagonist.tank.tracks.find( draft.protagonist.track = draft.protagonist.tank.tracks.find(
(track) => track.id === unlock.id (track) => track.id === unlock.id,
)!; )!;
} }
}); });
@@ -231,19 +231,19 @@ export function Modules() {
const central = !first && !last; const central = !first && !last;
let module: ModuleDefinition | TankDefinition | undefined = undefined; let module: ModuleDefinition | TankDefinition | undefined = undefined;
if (unlock.type === ModuleType.TRACKS) { if (unlock.type === ModuleType.MODULE_TYPE_TRACKS) {
module = tank.tracks.find((track) => track.id === unlock.id)!; module = tank.tracks.find((track) => track.id === unlock.id)!;
} else if (unlock.type === ModuleType.ENGINE) { } else if (unlock.type === ModuleType.MODULE_TYPE_ENGINE) {
module = tank.engines.find((engine) => engine.id === unlock.id)!; module = tank.engines.find((engine) => engine.id === unlock.id)!;
} else if (unlock.type === ModuleType.TURRET) { } else if (unlock.type === ModuleType.MODULE_TYPE_TURRET) {
module = tank.turrets.find((turret) => turret.id === unlock.id)!; module = tank.turrets.find((turret) => turret.id === unlock.id)!;
} else if (unlock.type === ModuleType.GUN) { } else if (unlock.type === ModuleType.MODULE_TYPE_GUN) {
module = tank.turrets module = tank.turrets
.find((turret) => .find((turret) =>
turret.guns.some((gun) => gun.id === unlock.id) turret.guns.some((gun) => gun.id === unlock.id),
)! )!
.guns.find((gun) => gun.id === unlock.id)!; .guns.find((gun) => gun.id === unlock.id)!;
} else if (unlock.type === ModuleType.VEHICLE) { } else if (unlock.type === ModuleType.MODULE_TYPE_VEHICLE) {
module = tankDefinitions.tanks[unlock.id]; module = tankDefinitions.tanks[unlock.id];
} }
@@ -257,7 +257,7 @@ export function Modules() {
gap="2" gap="2"
position="relative" position="relative"
> >
{unlock.cost.value > 0 && ( {unlock.cost!.value > 0 && (
<> <>
{unlocks.length > 1 && ( {unlocks.length > 1 && (
<div <div
@@ -302,24 +302,24 @@ export function Modules() {
unlock={unlock} unlock={unlock}
tier={extractModule(module).tier} tier={extractModule(module).tier}
selected={ selected={
(unlock.type === ModuleType.TURRET (unlock.type === ModuleType.MODULE_TYPE_TURRET
? turret.id ? turret.id
: unlock.type === ModuleType.GUN : unlock.type === ModuleType.MODULE_TYPE_GUN
? gun.id ? gun.id
: unlock.type === ModuleType.ENGINE : unlock.type === ModuleType.MODULE_TYPE_ENGINE
? engine.id ? engine.id
: unlock.type === ModuleType.TRACKS : unlock.type === ModuleType.MODULE_TYPE_TRACKS
? track.id ? track.id
: -1) === unlock.id : -1) === unlock.id
} }
top={ top={
unlock.type === ModuleType.TURRET unlock.type === ModuleType.MODULE_TYPE_TURRET
? extractModule(module).id == topTurret.id ? extractModule(module).id == topTurret.id
: unlock.type === ModuleType.GUN : unlock.type === ModuleType.MODULE_TYPE_GUN
? extractModule(module).id === topGun.id ? extractModule(module).id === topGun.id
: unlock.type === ModuleType.ENGINE : unlock.type === ModuleType.MODULE_TYPE_ENGINE
? extractModule(module).id === topEngine.id ? extractModule(module).id === topEngine.id
: unlock.type === ModuleType.TRACKS : unlock.type === ModuleType.MODULE_TYPE_TRACKS
? extractModule(module).id === topTrack.id ? extractModule(module).id === topTrack.id
: false : false
} }
@@ -342,7 +342,7 @@ export function Modules() {
{(extractModule(module) as ModuleWithUnlocks).unlocks && {(extractModule(module) as ModuleWithUnlocks).unlocks &&
tree( tree(
type, type,
(extractModule(module) as ModuleWithUnlocks).unlocks! (extractModule(module) as ModuleWithUnlocks).unlocks!,
)} )}
</Flex> </Flex>
); );
@@ -397,32 +397,32 @@ export function Modules() {
</Flex> </Flex>
<Flex gap="2" wrap="wrap" gapY="6"> <Flex gap="2" wrap="wrap" gapY="6">
{tree(ModuleType.ENGINE, [ {tree(ModuleType.MODULE_TYPE_ENGINE, [
{ {
cost: { type: "xp", value: 0 }, cost: { type: "xp", value: 0 },
id: engine0.id, id: engine0.id,
type: ModuleType.ENGINE, type: ModuleType.MODULE_TYPE_ENGINE,
}, },
])} ])}
{tree(ModuleType.TRACKS, [ {tree(ModuleType.MODULE_TYPE_TRACKS, [
{ {
cost: { type: "xp", value: 0 }, cost: { type: "xp", value: 0 },
id: track0.id, id: track0.id,
type: ModuleType.TRACKS, type: ModuleType.MODULE_TYPE_TRACKS,
}, },
])} ])}
{tree(ModuleType.TURRET, [ {tree(ModuleType.MODULE_TYPE_TURRET, [
{ {
cost: { type: "xp", value: 0 }, cost: { type: "xp", value: 0 },
id: turret0.id, id: turret0.id,
type: ModuleType.TURRET, type: ModuleType.MODULE_TYPE_TURRET,
}, },
])} ])}
{tree(ModuleType.GUN, [ {tree(ModuleType.MODULE_TYPE_GUN, [
{ {
cost: { type: "xp", value: 0 }, cost: { type: "xp", value: 0 },
id: gun0.id, id: gun0.id,
type: ModuleType.GUN, type: ModuleType.MODULE_TYPE_GUN,
}, },
])} ])}
</Flex> </Flex>
@@ -41,7 +41,7 @@ export function GameModeSection() {
style={{ style={{
borderRadius: "var(--radius-3)", borderRadius: "var(--radius-3)",
background: `url(${asset( background: `url(${asset(
`icons/game_mode_banners/${id}.webp` `icons/game_mode_banners/${id}.webp`,
)})`, )})`,
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
@@ -51,7 +51,7 @@ export function GameModeSection() {
flexGrow="1" flexGrow="1"
style={{ style={{
background: `url(${asset( background: `url(${asset(
`icons/game_mode_banners/${id}.webp` `icons/game_mode_banners/${id}.webp`,
)})`, )})`,
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
@@ -71,7 +71,7 @@ export function GameModeSection() {
justify="center" justify="center"
> >
<Text size="4" weight="bold"> <Text size="4" weight="bold">
{unwrap(gameMode.name)} {unwrap(gameMode.name!)}
</Text> </Text>
<Flex direction="column"> <Flex direction="column">
@@ -88,9 +88,9 @@ export function GameModeSection() {
objectFit: "contain", objectFit: "contain",
}} }}
src={asset(`icons/provisions/${id}.webp`)} src={asset(`icons/provisions/${id}.webp`)}
alt={unwrap(provisions.name)} alt={unwrap(provisions.name!)}
/> />
{unwrap(provisions.name)} {unwrap(provisions.name!)}
</Flex> </Flex>
</Text> </Text>
); );
@@ -109,9 +109,9 @@ export function GameModeSection() {
objectFit: "contain", objectFit: "contain",
}} }}
src={asset(`icons/consumables/${id}.webp`)} src={asset(`icons/consumables/${id}.webp`)}
alt={unwrap(consumable.name)} alt={unwrap(consumable.name!)}
/> />
{unwrap(consumable.name)} {unwrap(consumable.name!)}
</Flex> </Flex>
</Text> </Text>
); );
@@ -68,29 +68,29 @@ export function CustomShellButton() {
const modified = { ...customShell, type }; const modified = { ...customShell, type };
switch (type) { switch (type) {
case ShellType.AP: { case ShellType.SHELL_TYPE_AP: {
modified.normalization = 5; modified.normalization = 5;
modified.ricochet = 70; modified.ricochet = 70;
modified.explosion_radius = undefined; modified.explosion_radius = undefined;
break; break;
} }
case ShellType.APCR: { case ShellType.SHELL_TYPE_APCR: {
modified.normalization = 2; modified.normalization = 2;
modified.ricochet = 70; modified.ricochet = 70;
modified.explosion_radius = undefined; modified.explosion_radius = undefined;
break; break;
} }
case ShellType.HEAT: { case ShellType.SHELL_TYPE_HEAT: {
modified.normalization = undefined; modified.normalization = undefined;
modified.ricochet = undefined; modified.ricochet = undefined;
modified.explosion_radius = undefined; modified.explosion_radius = undefined;
} }
case ShellType.HE: { case ShellType.SHELL_TYPE_HE: {
modified.normalization = undefined; modified.normalization = undefined;
modified.ricochet = undefined; modified.ricochet = undefined;
modified.explosion_radius = modified.explosion_radius =
gun.shells.find( gun.shells.find(
(shell) => shell.type === ShellType.HE (shell) => shell.type === ShellType.SHELL_TYPE_HE,
)?.explosion_radius ?? 1; )?.explosion_radius ?? 1;
break; break;
} }
@@ -104,16 +104,16 @@ export function CustomShellButton() {
<Select.Trigger variant="classic" /> <Select.Trigger variant="classic" />
<Select.Content> <Select.Content>
<Select.Item value={`${ShellType.AP}`}> <Select.Item value={`${ShellType.SHELL_TYPE_AP}`}>
{strings.common.shells.ap} {strings.common.shells.ap}
</Select.Item> </Select.Item>
<Select.Item value={`${ShellType.APCR}`}> <Select.Item value={`${ShellType.SHELL_TYPE_APCR}`}>
{strings.common.shells.ap_cr} {strings.common.shells.ap_cr}
</Select.Item> </Select.Item>
<Select.Item value={`${ShellType.HEAT}`}> <Select.Item value={`${ShellType.SHELL_TYPE_HEAT}`}>
{strings.common.shells.hc} {strings.common.shells.hc}
</Select.Item> </Select.Item>
<Select.Item value={`${ShellType.HE}`}> <Select.Item value={`${ShellType.SHELL_TYPE_HE}`}>
{strings.common.shells.he} {strings.common.shells.he}
</Select.Item> </Select.Item>
</Select.Content> </Select.Content>
@@ -166,11 +166,11 @@ export function CustomShellButton() {
<TextField.Root <TextField.Root
variant="classic" variant="classic"
style={{ maxWidth: "6rem" }} style={{ maxWidth: "6rem" }}
defaultValue={customShell.penetration.near} defaultValue={customShell.penetration!.near}
type="number" type="number"
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell!.penetration.near = draft.customShell!.penetration!.near =
event.target.valueAsNumber; event.target.valueAsNumber;
}); });
}} }}
@@ -182,7 +182,7 @@ export function CustomShellButton() {
(event.target as HTMLInputElement).blur(); (event.target as HTMLInputElement).blur();
} else if (event.key === "Escape") { } else if (event.key === "Escape") {
(event.target as HTMLInputElement).valueAsNumber = (event.target as HTMLInputElement).valueAsNumber =
customShell.penetration.near; customShell.penetration!.near;
(event.target as HTMLInputElement).blur(); (event.target as HTMLInputElement).blur();
} }
}} }}
@@ -193,8 +193,8 @@ export function CustomShellButton() {
</TextField.Root> </TextField.Root>
</Flex> </Flex>
{(customShell.type === ShellType.AP || {(customShell.type === ShellType.SHELL_TYPE_AP ||
customShell.type === ShellType.APCR) && ( customShell.type === ShellType.SHELL_TYPE_APCR) && (
<> <>
<Flex align="center" justify="between" gap="4" py="1"> <Flex align="center" justify="between" gap="4" py="1">
<Text> <Text>
@@ -264,7 +264,7 @@ export function CustomShellButton() {
</> </>
)} )}
{customShell.type === ShellType.HE && ( {customShell.type === ShellType.SHELL_TYPE_HE && (
<Flex align="center" justify="between" gap="4"> <Flex align="center" justify="between" gap="4">
<Text>Explosion radius</Text> <Text>Explosion radius</Text>
<TextField.Root <TextField.Root
@@ -38,7 +38,7 @@ export function QuickInputs() {
} }
function handleTransformEvent( function handleTransformEvent(
event: QuicklimeEvent<ModelTransformEventData> event: QuicklimeEvent<ModelTransformEventData>,
) { ) {
syncInputValues(event.data); syncInputValues(event.data);
} }
@@ -77,7 +77,7 @@ export function QuickInputs() {
if (isNaN(value)) { if (isNaN(value)) {
yawInput.current!.value = radToDeg( yawInput.current!.value = radToDeg(
modelTransformEvent.last!.yaw modelTransformEvent.last!.yaw,
).toFixed(1); ).toFixed(1);
return; return;
} }
@@ -85,15 +85,15 @@ export function QuickInputs() {
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch, modelTransformEvent.last!.pitch,
degToRad(value), degToRad(value),
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
yawInput.current!.value = radToDeg( yawInput.current!.value = radToDeg(
modelTransformEvent.last!.yaw modelTransformEvent.last!.yaw,
).toFixed(1); ).toFixed(1);
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
@@ -129,10 +129,10 @@ export function QuickInputs() {
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
degToRad(value + initialGunPitch), degToRad(value + initialGunPitch),
modelTransformEvent.last!.yaw, modelTransformEvent.last!.yaw,
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
pitchInput.current!.value = ( pitchInput.current!.value = (
@@ -54,12 +54,12 @@ type OptionsProps = MaybeSkeletonComponentProps & {
export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) { export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const hasCustomShell = Tankopedia.use( const hasCustomShell = Tankopedia.use(
(state) => state.customShell !== undefined (state) => state.customShell !== undefined,
); );
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay); const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const advancedHighlighting = TankopediaPersistent.use( const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting (state) => state.advancedHighlighting,
); );
const fullScreenAvailable = useFullscreenAvailability(true); const fullScreenAvailable = useFullscreenAvailability(true);
const protagonistTank = Duel.use((state) => state.protagonist.tank); const protagonistTank = Duel.use((state) => state.protagonist.tank);
@@ -102,8 +102,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
value: ( value: (
resolvePenetrationCoefficient( resolvePenetrationCoefficient(
hasCalibratedShells, hasCalibratedShells,
antagonistShell.type antagonistShell.type,
) * antagonistShell.penetration.near ) * antagonistShell.penetration!.near
).toFixed(0), ).toFixed(0),
})} })}
</Text> </Text>
@@ -203,7 +203,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
}} }}
> >
<img <img
alt={unwrap(thisShell.name)} alt={unwrap(thisShell.name!)}
src={asset(`icons/shells/${thisShell.icon}.webp`)} src={asset(`icons/shells/${thisShell.icon}.webp`)}
style={{ style={{
width: "50%", width: "50%",
@@ -456,7 +456,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Dialog.Trigger> <Dialog.Trigger>
<Button variant="solid" highContrast> <Button variant="solid" highContrast>
<Crosshair1Icon /> <Crosshair1Icon />
{unwrap(antagonistTank.name)} {unwrap(antagonistTank.name!)}
<SmallTankIcon id={antagonistTank.id} size={16} /> <SmallTankIcon id={antagonistTank.id} size={16} />
</Button> </Button>
</Dialog.Trigger> </Dialog.Trigger>
@@ -9,7 +9,7 @@ export function Title() {
const protagonist = Duel.use((state) => state.protagonist.tank); const protagonist = Duel.use((state) => state.protagonist.tank);
const revealed = Tankopedia.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const name = unwrap(protagonist.name); const name = unwrap(protagonist.name!);
const fontSize = revealed const fontSize = revealed
? disturbed ? disturbed
? "1.5rem" ? "1.5rem"
@@ -53,24 +53,24 @@ export function Controls({
const protagonistGunModelDefinition = const protagonistGunModelDefinition =
protagonistTurretModelDefinition.guns[protagonistGun.id]; protagonistTurretModelDefinition.guns[protagonistGun.id];
const protagonistHullOrigin = new Vector3( const protagonistHullOrigin = new Vector3(
protagonistTrackModelDefinition.origin.x, protagonistTrackModelDefinition.origin!.x,
protagonistTrackModelDefinition.origin.y, protagonistTrackModelDefinition.origin!.y,
-protagonistTrackModelDefinition.origin.z, -protagonistTrackModelDefinition.origin!.z,
); );
const protagonistTurretOrigin = new Vector3( const protagonistTurretOrigin = new Vector3(
protagonistModelDefinition.turret_origin.x, protagonistModelDefinition.turret_origin!.x,
protagonistModelDefinition.turret_origin.y, protagonistModelDefinition.turret_origin!.y,
-protagonistModelDefinition.turret_origin.z, -protagonistModelDefinition.turret_origin!.z,
); );
const protagonistGunOrigin = new Vector3( const protagonistGunOrigin = new Vector3(
protagonistTurretModelDefinition.gun_origin.x, protagonistTurretModelDefinition.gun_origin!.x,
protagonistTurretModelDefinition.gun_origin.y, protagonistTurretModelDefinition.gun_origin!.y,
-protagonistTurretModelDefinition.gun_origin.z, -protagonistTurretModelDefinition.gun_origin!.z,
); );
const antagonistGunHeight = const antagonistGunHeight =
protagonistTrackModelDefinition.origin.y + protagonistTrackModelDefinition.origin!.y +
antagonistModelDefinition.turret_origin.y + antagonistModelDefinition.turret_origin!.y +
antagonistTurretModelDefinition.gun_origin.y; antagonistTurretModelDefinition.gun_origin!.y;
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const doAutoRotate = autoRotate && !disturbed; const doAutoRotate = autoRotate && !disturbed;
const gunHeight = const gunHeight =
@@ -119,7 +119,7 @@ export function Controls({
const [pitch] = applyPitchYawLimits( const [pitch] = applyPitchYawLimits(
-Infinity, -Infinity,
0, 0,
protagonistGunModelDefinition.pitch, protagonistGunModelDefinition.pitch!,
protagonistTurretModelDefinition.yaw, protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer,
@@ -152,7 +152,7 @@ export function Controls({
const [pitch] = applyPitchYawLimits( const [pitch] = applyPitchYawLimits(
0, 0,
0, 0,
protagonistGunModelDefinition.pitch, protagonistGunModelDefinition.pitch!,
protagonistTurretModelDefinition.yaw, protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer,
@@ -1,9 +1,9 @@
import { useEffect } from 'react'; import { useEffect } from "react";
import { degToRad } from 'three/src/math/MathUtils.js'; import { degToRad } from "three/src/math/MathUtils.js";
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from "../../../../../../core/awaitables/modelDefinitions";
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform'; import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
const modelDefinitions = await awaitableModelDefinitions; const modelDefinitions = await awaitableModelDefinitions;
@@ -20,7 +20,7 @@ export function InitialAligner() {
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
degToRad(8), degToRad(8),
degToRad(25), degToRad(25),
gunModel.pitch, gunModel.pitch!,
turretModel.yaw, turretModel.yaw,
); );
@@ -134,12 +134,12 @@ export function TankModel() {
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
@@ -150,10 +150,10 @@ export function TankModel() {
modelTransformEvent.last!.pitch, modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw + modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width), delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
@@ -217,12 +217,12 @@ export function TankModel() {
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
Duel.state.protagonist.tank.equipment_preset, Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix Duel.state.protagonist.equipmentMatrix,
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position); delta.set(event.clientX, event.clientY).sub(position);
@@ -231,10 +231,10 @@ export function TankModel() {
[pitch, yaw] = applyPitchYawLimits( [pitch, yaw] = applyPitchYawLimits(
pitch - delta.y * (Math.PI / boundingRect.height), pitch - delta.y * (Math.PI / boundingRect.height),
yaw + delta.x * (Math.PI / boundingRect.width), yaw + delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer hasDownImprovedVerticalStabilizer,
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
@@ -37,9 +37,9 @@ export function TransitionSkeleton() {
const gunModelDefinition = turretModelDefinition.guns[gun.id]; const gunModelDefinition = turretModelDefinition.guns[gun.id];
const trackModelDefinition = tankModelDefinition.tracks[track.id]; const trackModelDefinition = tankModelDefinition.tracks[track.id];
const wrapper = useRef<Group>(null); const wrapper = useRef<Group>(null);
const hullOrigin = correctZYTuple(trackModelDefinition.origin); const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin); const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin); const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
useTankTransform(track, turret, turretContainer, gunContainer); useTankTransform(track, turret, turretContainer, gunContainer);
@@ -77,7 +77,7 @@ export function TransitionSkeleton() {
<group ref={turretContainer}> <group ref={turretContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith( const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}` `turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`,
); );
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
if (!isVisible) { if (!isVisible) {
@@ -102,7 +102,7 @@ export function TransitionSkeleton() {
<group ref={gunContainer}> <group ref={gunContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith( const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}` `gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`,
); );
const isVisible = isCurrentGun; const isVisible = isCurrentGun;
@@ -73,7 +73,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
-Infinity, -Infinity,
0, 0,
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer,
@@ -88,7 +88,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
Infinity, Infinity,
0, 0,
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
); );
@@ -102,7 +102,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
0, 0,
0, 0,
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
); );
@@ -124,7 +124,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch, modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw, modelTransformEvent.last!.yaw,
gunModelDefinition.pitch, gunModelDefinition.pitch!,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
); );
@@ -22,13 +22,13 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
const thicknessRange = useMemo(() => { const thicknessRange = useMemo(() => {
const entries = Object.values(tankDefinitions.tanks); const entries = Object.values(tankDefinitions.tanks);
const filtered = entries.filter( const filtered = entries.filter(
(thisTank) => thisTank.tier === protagonist.tier (thisTank) => thisTank.tier === protagonist.tier,
); );
const value = const value =
(filtered.reduce((accumulator, thisTank) => { (filtered.reduce((accumulator, thisTank) => {
return ( return (
accumulator + accumulator +
thisTank.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near thisTank.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near
); );
}, 0) / }, 0) /
filtered.length) * filtered.length) *
@@ -99,11 +99,11 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
isFullScreen isFullScreen
? "100vh" ? "100vh"
: disturbed : disturbed
? "calc(100svh - 8rem)" ? "calc(100svh - 8rem)"
: { : {
initial: "28rem", initial: "28rem",
md: "calc(100svh - 14rem)", md: "calc(100svh - 14rem)",
} }
} }
maxWidth={isFullScreen ? undefined : "120rem"} maxWidth={isFullScreen ? undefined : "120rem"}
flexGrow="1" flexGrow="1"
@@ -36,7 +36,7 @@ export function VoteCaster() {
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content maxWidth="23rem"> <Dialog.Content maxWidth="23rem">
<Dialog.Title>Voting for {unwrap(tank.name)}</Dialog.Title> <Dialog.Title>Voting for {unwrap(tank.name!)}</Dialog.Title>
<Flex direction="column" mt="4"> <Flex direction="column" mt="4">
<Flex align="center" justify="between"> <Flex align="center" justify="between">
@@ -118,10 +118,10 @@ export function VoteCaster() {
survivability === null survivability === null
? "" ? ""
: `survivability=${survivability}&` : `survivability=${survivability}&`
}player=${wargaming.id}&token=${wargaming.token}` }player=${wargaming.id}&token=${wargaming.token}`,
) )
.then( .then(
(response) => response.json() as Promise<BlitzkitResponse> (response) => response.json() as Promise<BlitzkitResponse>,
) )
.then((json) => { .then((json) => {
setShowDialogue(false); setShowDialogue(false);
@@ -3,20 +3,20 @@ import {
TankPriceType, TankPriceType,
TankType, TankType,
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { Box, Flex } from '@radix-ui/themes'; import { Box, Flex } from "@radix-ui/themes";
import { Var } from '../../../core/radix/var'; import { Var } from "../../../core/radix/var";
import { useLocale } from '../../../hooks/useLocale'; import { useLocale } from "../../../hooks/useLocale";
import { App } from '../../../stores/app'; import { App } from "../../../stores/app";
import { Duel } from '../../../stores/duel'; import { Duel } from "../../../stores/duel";
import { classIcons } from '../../ClassIcon'; import { classIcons } from "../../ClassIcon";
import { CopyableCode } from '../../CopyableCode'; import { CopyableCode } from "../../CopyableCode";
import { Listing } from './components/Listing'; import { Listing } from "./components/Listing";
const NATIONAL_BANNER_POSITION_OVERRIDES: Record<string, string> = { const NATIONAL_BANNER_POSITION_OVERRIDES: Record<string, string> = {
germany: '50% 35%', germany: "50% 35%",
france: '50% 35%', france: "50% 35%",
other: '50% 35%', other: "50% 35%",
}; };
export function MetaSection() { export function MetaSection() {
@@ -30,10 +30,10 @@ export function MetaSection() {
<Box <Box
style={{ style={{
background: `url(/assets/images/national-wallpapers/${protagonist.nation}.jpg)`, background: `url(/assets/images/national-wallpapers/${protagonist.nation}.jpg)`,
backgroundRepeat: 'no-repeat', backgroundRepeat: "no-repeat",
backgroundSize: 'cover', backgroundSize: "cover",
backgroundPosition: backgroundPosition:
NATIONAL_BANNER_POSITION_OVERRIDES[protagonist.nation] ?? 'center', NATIONAL_BANNER_POSITION_OVERRIDES[protagonist.nation] ?? "center",
}} }}
flexGrow="1" flexGrow="1"
maxWidth="120rem" maxWidth="120rem"
@@ -41,9 +41,9 @@ export function MetaSection() {
<Box <Box
style={{ style={{
background: `url(${asset(`flags/scratched/${protagonist.nation}.webp`)})`, background: `url(${asset(`flags/scratched/${protagonist.nation}.webp`)})`,
backgroundRepeat: 'no-repeat', backgroundRepeat: "no-repeat",
backgroundSize: 'min(53rem, 80vw)', backgroundSize: "min(53rem, 80vw)",
backgroundPosition: '-8rem 50%', backgroundPosition: "-8rem 50%",
}} }}
> >
<Flex <Flex
@@ -51,19 +51,19 @@ export function MetaSection() {
align="center" align="center"
py="6rem" py="6rem"
style={{ style={{
background: `linear-gradient(${Var('black-a8')}, ${Var('black-a8')}, ${Var('gray-1')})`, background: `linear-gradient(${Var("black-a8")}, ${Var("black-a8")}, ${Var("gray-1")})`,
}} }}
> >
<Flex direction="column" align="center" gap="6"> <Flex direction="column" align="center" gap="6">
<Flex <Flex
gap={{ initial: '2', sm: '8' }} gap={{ initial: "2", sm: "8" }}
direction={{ initial: 'column', sm: 'row' }} direction={{ initial: "column", sm: "row" }}
> >
<Flex direction="column" width="100%" gap="2"> <Flex direction="column" width="100%" gap="2">
<Listing label={strings.website.tools.tankopedia.meta.nation}> <Listing label={strings.website.tools.tankopedia.meta.nation}>
<Flex align="center" gap="1"> <Flex align="center" gap="1">
<img <img
style={{ width: '1em', height: '1em' }} style={{ width: "1em", height: "1em" }}
alt={protagonist.nation} alt={protagonist.nation}
src={asset(`flags/circle/${protagonist.nation}.webp`)} src={asset(`flags/circle/${protagonist.nation}.webp`)}
/> />
@@ -86,10 +86,10 @@ export function MetaSection() {
<Listing <Listing
label={strings.website.tools.tankopedia.meta.type} label={strings.website.tools.tankopedia.meta.type}
color={ color={
protagonist.type === TankType.COLLECTOR protagonist.type === TankType.TANK_TYPE_COLLECTOR
? 'blue' ? "blue"
: protagonist.type === TankType.PREMIUM : protagonist.type === TankType.TANK_TYPE_PREMIUM
? 'amber' ? "amber"
: undefined : undefined
} }
> >
@@ -117,7 +117,7 @@ export function MetaSection() {
label={strings.website.tools.tankopedia.meta.dev_name} label={strings.website.tools.tankopedia.meta.dev_name}
> >
<CopyableCode <CopyableCode
copy={devName} copy={protagonist.dev_name}
variant="soft" variant="soft"
highContrast highContrast
color="gray" color="gray"
@@ -126,37 +126,38 @@ export function MetaSection() {
</CopyableCode> </CopyableCode>
</Listing> </Listing>
)} )}
{protagonist.type === TankType.PREMIUM && ( {protagonist.type === TankType.TANK_TYPE_PREMIUM && (
<Listing <Listing
label={strings.website.tools.tankopedia.meta.purchase} label={strings.website.tools.tankopedia.meta.purchase}
> >
<Flex align="center" gap="1"> <Flex align="center" gap="1">
{protagonist.price.value / 400} {protagonist.price!.value / 400}
<img <img
style={{ width: '1em', height: '1em' }} style={{ width: "1em", height: "1em" }}
alt="gold" alt="gold"
src={asset('icons/currencies/gold.webp')} src={asset("icons/currencies/gold.webp")}
/> />
</Flex> </Flex>
</Listing> </Listing>
)} )}
<Listing <Listing
label={ label={
protagonist.type === TankType.RESEARCHABLE protagonist.type === TankType.TANK_TYPE_RESEARCHABLE
? strings.website.tools.tankopedia.meta.purchase ? strings.website.tools.tankopedia.meta.purchase
: strings.website.tools.tankopedia.meta.restoration : strings.website.tools.tankopedia.meta.restoration
} }
> >
<Flex align="center" gap="1"> <Flex align="center" gap="1">
{protagonist.price.value.toLocaleString(locale)} {protagonist.price!.value.toLocaleString(locale)}
<img <img
style={{ width: '1em', height: '1em' }} style={{ width: "1em", height: "1em" }}
alt={TankPriceType[protagonist.price.type]} alt={TankPriceType[protagonist.price!.type]}
src={asset( src={asset(
`icons/currencies/${ `icons/currencies/${
protagonist.price.type === TankPriceType.GOLD protagonist.price!.type ===
? 'gold' TankPriceType.TANK_PRICE_TYPE_GOLD
: 'silver' ? "gold"
: "silver"
}.webp`, }.webp`,
)} )}
/> />
@@ -164,15 +165,16 @@ export function MetaSection() {
</Listing> </Listing>
<Listing label={strings.website.tools.tankopedia.meta.sale}> <Listing label={strings.website.tools.tankopedia.meta.sale}>
<Flex align="center" gap="1"> <Flex align="center" gap="1">
{(protagonist.price.value / 2).toLocaleString(locale)} {(protagonist.price!.value / 2).toLocaleString(locale)}
<img <img
style={{ width: '1em', height: '1em' }} style={{ width: "1em", height: "1em" }}
alt={TankPriceType[protagonist.price.type]} alt={TankPriceType[protagonist.price!.type]}
src={asset( src={asset(
`icons/currencies/${ `icons/currencies/${
protagonist.price.type === TankPriceType.GOLD protagonist.price!.type ===
? 'gold' TankPriceType.TANK_PRICE_TYPE_GOLD
: 'silver' ? "gold"
: "silver"
}.webp`, }.webp`,
)} )}
/> />
@@ -188,9 +190,9 @@ export function MetaSection() {
.value as number .value as number
).toLocaleString(locale)} ).toLocaleString(locale)}
<img <img
style={{ width: '1em', height: '1em' }} style={{ width: "1em", height: "1em" }}
alt="xp" alt="xp"
src={asset('icons/currencies/xp.webp')} src={asset("icons/currencies/xp.webp")}
/> />
</Flex> </Flex>
</Listing> </Listing>
@@ -36,7 +36,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
? 0 ? 0
: (tank.research_cost.research_cost_type!.value as number) / : (tank.research_cost.research_cost_type!.value as number) /
xpMultiplier), xpMultiplier),
0 0,
); );
const averages = averageDefinitions.averages[tank.id]?.mu; const averages = averageDefinitions.averages[tank.id]?.mu;
const averageXp = averages ? averages.xp / averages.battles : undefined; const averageXp = averages ? averages.xp / averages.battles : undefined;
@@ -78,7 +78,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
}} }}
> >
<img <img
alt={unwrap(tank.name)} alt={unwrap(tank.name!)}
src={asset(`icons/tanks/big/${id}.webp`)} src={asset(`icons/tanks/big/${id}.webp`)}
width={64} width={64}
height={64} height={64}
@@ -92,7 +92,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
<Text color="gray" size="1"> <Text color="gray" size="1">
{TIER_ROMAN_NUMERALS[tank.tier]} {TIER_ROMAN_NUMERALS[tank.tier]}
</Text> </Text>
<Text wrap="nowrap">{unwrap(tank.name)}</Text> <Text wrap="nowrap">{unwrap(tank.name!)}</Text>
</Flex> </Flex>
<Flex gap="2" align="center"> <Flex gap="2" align="center">
<Text color="gray" size="1" wrap="nowrap"> <Text color="gray" size="1" wrap="nowrap">
@@ -122,7 +122,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
objectPosition: "center", objectPosition: "center",
}} }}
/> />
{formatCompact(locale, tank.price.value)} {formatCompact(locale, tank.price!.value)}
</Flex> </Flex>
</Text> </Text>
</Flex> </Flex>
@@ -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,
{ count: games } { count: games },
))} ))}
</Text> </Text>
)} )}
@@ -26,14 +26,18 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
function extend(line: Line): Line[] { function extend(line: Line): Line[] {
const root = tankDefinitions.tanks[line.at(-1)!]; const root = tankDefinitions.tanks[line.at(-1)!];
if (root.ancestors === undefined || root.ancestors.length === 0 || root.tier === 1) { if (
root.ancestors === undefined ||
root.ancestors.length === 0 ||
root.tier === 1
) {
return [line]; return [line];
} else { } else {
if (root.ancestors.length === 1 || root.tier === 2) { if (root.ancestors.length === 1 || root.tier === 2) {
line.push( line.push(
root.ancestors.find( root.ancestors.find(
(ancestor) => !tankDefinitions.tanks[ancestor].deprecated (ancestor) => !tankDefinitions.tanks[ancestor].deprecated,
) ?? root.ancestors[0] ) ?? root.ancestors[0],
); );
return extend(line); return extend(line);
} else { } else {
@@ -52,7 +56,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
const [lineIndex, setLineIndex] = useState(0); const [lineIndex, setLineIndex] = useState(0);
const line = useMemo( const line = useMemo(
() => (lines.length === 0 ? [master.id] : [...lines[lineIndex]].reverse()), () => (lines.length === 0 ? [master.id] : [...lines[lineIndex]].reverse()),
[master, lineIndex] [master, lineIndex],
); );
const totalXp = line.reduce( const totalXp = line.reduce(
(xp, id) => (xp, id) =>
@@ -62,11 +66,11 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
? 0 ? 0
: (tankDefinitions.tanks[id].research_cost.research_cost_type! : (tankDefinitions.tanks[id].research_cost.research_cost_type!
.value as number)), .value as number)),
0 0,
); );
const totalCredits = line.reduce( const totalCredits = line.reduce(
(credits, id) => credits + tankDefinitions.tanks[id].price.value, (credits, id) => credits + tankDefinitions.tanks[id].price!.value,
0 0,
); );
useEffect(() => { useEffect(() => {
@@ -75,7 +79,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
}); });
if ( if (
master.type !== TankType.RESEARCHABLE || master.type !== TankType.TANK_TYPE_RESEARCHABLE ||
master.ancestors === undefined || master.ancestors === undefined ||
master.ancestors.length === 0 master.ancestors.length === 0
) { ) {
+36 -36
View File
@@ -5,20 +5,20 @@ import {
TankType, TankType,
type CompositeStats, type CompositeStats,
type CompositeStatsKey, type CompositeStatsKey,
} from '@blitzkit/core'; } from "@blitzkit/core";
import strings from '@blitzkit/i18n/strings/en.json'; import strings from "@blitzkit/i18n/strings/en.json";
import { amberDark, blueDark } from '@radix-ui/colors'; import { amberDark, blueDark } from "@radix-ui/colors";
import { Flex, Text, type FlexProps } from '@radix-ui/themes'; import { Flex, Text, type FlexProps } from "@radix-ui/themes";
import { times } from 'lodash-es'; import { times } from "lodash-es";
import type { ReactNode } from 'react'; import type { ReactNode } from "react";
import { breakdownConfig } from '../constants/embeds'; import { breakdownConfig } from "../constants/embeds";
import { toRadiusVar } from '../core/radix/utils'; import { toRadiusVar } from "../core/radix/utils";
import { useLocale } from '../hooks/useLocale'; import { useLocale } from "../hooks/useLocale";
import { useEmbedStateCurry } from '../stores/embedState/utilities'; import { useEmbedStateCurry } from "../stores/embedState/utilities";
import { ClassHeavy } from './ClassIcon/components/ClassHeavy'; import { ClassHeavy } from "./ClassIcon/components/ClassHeavy";
import { ClassLight } from './ClassIcon/components/ClassLight'; import { ClassLight } from "./ClassIcon/components/ClassLight";
import { ClassMedium } from './ClassIcon/components/ClassMedium'; import { ClassMedium } from "./ClassIcon/components/ClassMedium";
import { ClassTankDestroyer } from './ClassIcon/components/ClassTankDestroyer'; import { ClassTankDestroyer } from "./ClassIcon/components/ClassTankDestroyer";
type BreakdownEmbedWrapperProps = FlexProps & { type BreakdownEmbedWrapperProps = FlexProps & {
children: ReactNode; children: ReactNode;
@@ -33,7 +33,7 @@ export function BreakdownEmbedWrapper({
return ( return (
<Flex <Flex
direction="column" direction="column"
gap={useEmbedState('listGap')} gap={useEmbedState("listGap")}
height="100%" height="100%"
{...props} {...props}
> >
@@ -60,65 +60,65 @@ export function BreakdownEmbedCard({
flexShrink="0" flexShrink="0"
direction="column" direction="column"
style={{ style={{
borderRadius: toRadiusVar(useEmbedState('cardRadius')), borderRadius: toRadiusVar(useEmbedState("cardRadius")),
overflow: 'hidden', overflow: "hidden",
backgroundColor: useEmbedState('cardBodyBackgroundColor'), backgroundColor: useEmbedState("cardBodyBackgroundColor"),
}} }}
> >
<Flex <Flex
justify="center" justify="center"
style={{ style={{
backgroundColor: useEmbedState('cardHeaderBackgroundColor'), backgroundColor: useEmbedState("cardHeaderBackgroundColor"),
}} }}
p={useEmbedState('cardHeaderPadding')} p={useEmbedState("cardHeaderPadding")}
> >
<Text <Text
{...{ {...{
...useRichText('cardTitle'), ...useRichText("cardTitle"),
style: style:
tank === null tank === null
? undefined ? undefined
: tank === undefined || : tank === undefined ||
tank.type === TankType.RESEARCHABLE || tank.type === TankType.TANK_TYPE_RESEARCHABLE ||
!useEmbedState('cardTitleTypeColor') !useEmbedState("cardTitleTypeColor")
? undefined ? undefined
: { : {
color: color:
tank.type === TankType.PREMIUM tank.type === TankType.TANK_TYPE_PREMIUM
? amberDark.amber11 ? amberDark.amber11
: blueDark.blue11, : blueDark.blue11,
}, },
}} }}
> >
<Flex align="center" gap="1"> <Flex align="center" gap="1">
{useEmbedState('cardTitleClassIcon') && {useEmbedState("cardTitleClassIcon") &&
tank !== null && tank !== null &&
tank !== undefined && ( tank !== undefined && (
<> <>
{tank.class === TankClass.LIGHT && ( {tank.class === TankClass.TANK_CLASS_LIGHT && (
<ClassLight width="1em" height="1em" /> <ClassLight width="1em" height="1em" />
)} )}
{tank.class === TankClass.MEDIUM && ( {tank.class === TankClass.TANK_CLASS_MEDIUM && (
<ClassMedium width="1em" height="1em" /> <ClassMedium width="1em" height="1em" />
)} )}
{tank.class === TankClass.HEAVY && ( {tank.class === TankClass.TANK_CLASS_HEAVY && (
<ClassHeavy width="1em" height="1em" /> <ClassHeavy width="1em" height="1em" />
)} )}
{tank.class === TankClass.TANK_DESTROYER && ( {tank.class === TankClass.TANK_CLASS_TANK_DESTROYER && (
<ClassTankDestroyer width="1em" height="1em" /> <ClassTankDestroyer width="1em" height="1em" />
)} )}
</> </>
)}{' '} )}{" "}
{tank === null {tank === null
? 'Total' ? "Total"
: tank === undefined : tank === undefined
? `Unknown tank` ? `Unknown tank`
: unwrap(tank.name)} : unwrap(tank.name!)}
</Flex> </Flex>
</Text> </Text>
</Flex> </Flex>
<Flex p={useEmbedState('cardBodyPadding')}> <Flex p={useEmbedState("cardBodyPadding")}>
{times(4, (index) => { {times(4, (index) => {
const column = (index + 1) as 1 | 2 | 3 | 4; const column = (index + 1) as 1 | 2 | 3 | 4;
const statKey = useEmbedState(`column${column}`) as CompositeStatsKey; const statKey = useEmbedState(`column${column}`) as CompositeStatsKey;
@@ -129,13 +129,13 @@ export function BreakdownEmbedCard({
key={column} key={column}
direction="column" direction="column"
align="center" align="center"
gap={useEmbedState('columnGap')} gap={useEmbedState("columnGap")}
flexGrow="1" flexGrow="1"
> >
<Text {...useRichText('columnValue')}> <Text {...useRichText("columnValue")}>
{formatCompositeStat(composite[statKey], statKey, composite)} {formatCompositeStat(composite[statKey], statKey, composite)}
</Text> </Text>
<Text {...useRichText('columnLabel')}> <Text {...useRichText("columnLabel")}>
{customLabel.length === 0 {customLabel.length === 0
? strings.common.composite_stats[statKey] ? strings.common.composite_stats[statKey]
: customLabel} : customLabel}
@@ -143,22 +143,28 @@ export function tankCharacteristics(
const commanderMastery = 1 + provisionCrewBonus; const commanderMastery = 1 + provisionCrewBonus;
const loaderMastery = const loaderMastery =
commanderMastery * commanderMastery *
(tank.crew.some(({ type }) => type === CrewType.LOADER) ? 1.1 : 1.05); (tank.crew.some(({ type }) => type === CrewType.CREW_TYPE_LOADER)
? 1.1
: 1.05);
const gunnerMastery = const gunnerMastery =
commanderMastery * commanderMastery *
(tank.crew.some(({ type }) => type === CrewType.GUNNER) ? 1.1 : 1.05); (tank.crew.some(({ type }) => type === CrewType.CREW_TYPE_GUNNER)
? 1.1
: 1.05);
const driverMastery = const driverMastery =
commanderMastery * commanderMastery *
(tank.crew.some(({ type }) => type === CrewType.DRIVER) ? 1.1 : 1.05); (tank.crew.some(({ type }) => type === CrewType.CREW_TYPE_DRIVER)
? 1.1
: 1.05);
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]); const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
const burstShells = gun.burst?.count ?? 1; const burstShells = gun.burst?.count ?? 1;
const burstInterShell = gun.burst?.interval; const burstInterShell = gun.burst?.interval;
const armorDamageCoefficient = const armorDamageCoefficient =
coefficient([hasTungsten, 0.15]) * coefficient([hasTungsten, 0.15]) *
coefficient( coefficient(
[applyReactiveArmor && shell.type !== ShellType.HE, -0.27], [applyReactiveArmor && shell.type !== ShellType.SHELL_TYPE_HE, -0.27],
[applyDynamicArmor, -0.1], [applyDynamicArmor, -0.1],
[applySpallLiner && shell.type === ShellType.HE, -0.2], [applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE, -0.2],
); );
const assaultDamageCoefficient = const assaultDamageCoefficient =
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type) gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
@@ -215,7 +221,8 @@ export function tankCharacteristics(
const enginePowerCoefficient = coefficient( const enginePowerCoefficient = coefficient(
[ [
hasEngineAccelerator, hasEngineAccelerator,
tank.class === TankClass.HEAVY || tank.class === TankClass.TANK_DESTROYER tank.class === TankClass.TANK_CLASS_HEAVY ||
tank.class === TankClass.TANK_CLASS_TANK_DESTROYER
? 0.05 ? 0.05
: 0.04, : 0.04,
], ],
@@ -250,11 +257,11 @@ export function tankCharacteristics(
const viewRangeCoefficient = const viewRangeCoefficient =
coefficient([ coefficient([
hasImprovedOptics, hasImprovedOptics,
tank.class === TankClass.HEAVY tank.class === TankClass.TANK_CLASS_HEAVY
? 0.06 ? 0.06
: tank.class === TankClass.MEDIUM : tank.class === TankClass.TANK_CLASS_MEDIUM
? 0.08 ? 0.08
: tank.class === TankClass.LIGHT : tank.class === TankClass.TANK_CLASS_LIGHT
? 0.11 ? 0.11
: 0.04, : 0.04,
]) * coefficient([true, progressiveStat(commanderMastery)]); ]) * coefficient([true, progressiveStat(commanderMastery)]);
@@ -275,20 +282,20 @@ export function tankCharacteristics(
true, true,
coefficient([ coefficient([
camouflage, camouflage,
tank.class === TankClass.TANK_DESTROYER tank.class === TankClass.TANK_CLASS_TANK_DESTROYER
? 0.04 ? 0.04
: tank.class === TankClass.HEAVY : tank.class === TankClass.TANK_CLASS_HEAVY
? 0.03 ? 0.03
: 0.02, : 0.02,
]), ]),
], ],
[ [
hasCamouflageNet, hasCamouflageNet,
tank.class === TankClass.HEAVY tank.class === TankClass.TANK_CLASS_HEAVY
? 0.02 ? 0.02
: tank.class === TankClass.MEDIUM : tank.class === TankClass.TANK_CLASS_MEDIUM
? 0.04 ? 0.04
: tank.class === TankClass.LIGHT : tank.class === TankClass.TANK_CLASS_LIGHT
? 0.07 ? 0.07
: 0.1, : 0.1,
], ],
@@ -299,9 +306,9 @@ export function tankCharacteristics(
coefficient( coefficient(
[ [
camouflage, camouflage,
tank.class === TankClass.TANK_DESTROYER tank.class === TankClass.TANK_CLASS_TANK_DESTROYER
? 0.04 ? 0.04
: tank.class === TankClass.HEAVY : tank.class === TankClass.TANK_CLASS_HEAVY
? 0.03 ? 0.03
: 0.02, : 0.02,
], ],
@@ -310,11 +317,11 @@ export function tankCharacteristics(
], ],
[ [
hasCamouflageNet, hasCamouflageNet,
tank.class === TankClass.HEAVY tank.class === TankClass.TANK_CLASS_HEAVY
? 0.04 ? 0.04
: tank.class === TankClass.MEDIUM : tank.class === TankClass.TANK_CLASS_MEDIUM
? 0.08 ? 0.08
: tank.class === TankClass.LIGHT : tank.class === TankClass.TANK_CLASS_LIGHT
? 0.14 ? 0.14
: 0.2, : 0.2,
], ],
@@ -322,8 +329,8 @@ export function tankCharacteristics(
const size = normalizeBoundingBox( const size = normalizeBoundingBox(
unionBoundingBox( unionBoundingBox(
tankModelDefinition.bounding_box, tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box, turretModelDefinition.bounding_box!,
), ),
); );
const weightKg = const weightKg =
@@ -408,7 +415,7 @@ export function tankCharacteristics(
? gun.gun_type!.value.reload ? gun.gun_type!.value.reload
: gun.gun_type!.value.clip_reload) * reloadCoefficient; : gun.gun_type!.value.clip_reload) * reloadCoefficient;
const caliber = shell.caliber; const caliber = shell.caliber;
const penetration = shell.penetration.near * penetrationCoefficient; const penetration = shell.penetration!.near * penetrationCoefficient;
const clipDamage = const clipDamage =
gun.gun_type!.$case === "regular" gun.gun_type!.$case === "regular"
? undefined ? undefined
@@ -428,21 +435,21 @@ export function tankCharacteristics(
const dispersionGunDamaged = const dispersionGunDamaged =
gun.dispersion_damaged * dispersionDamagedCoefficient; gun.dispersion_damaged * dispersionDamagedCoefficient;
const gunDepression = const gunDepression =
gunModelDefinition.pitch.max + gunModelDefinition.pitch!.max +
gunDefaultPitch + gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0) + (hasImprovedVerticalStabilizer ? 3 : 0) +
(hasDownImprovedVerticalStabilizer ? 3 : 0); (hasDownImprovedVerticalStabilizer ? 3 : 0);
const gunElevation = const gunElevation =
-gunModelDefinition.pitch.min - -gunModelDefinition.pitch!.min -
gunDefaultPitch + gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 4 : 0); (hasImprovedVerticalStabilizer ? 4 : 0);
const gunFrontalDepression = gunModelDefinition.pitch.front?.max; const gunFrontalDepression = gunModelDefinition.pitch!.front?.max;
const gunFrontalElevation = gunModelDefinition.pitch.front const gunFrontalElevation = gunModelDefinition.pitch!.front
? -gunModelDefinition.pitch.front.min ? -gunModelDefinition.pitch!.front.min
: undefined; : undefined;
const gunRearDepression = gunModelDefinition.pitch.back?.max; const gunRearDepression = gunModelDefinition.pitch!.back?.max;
const gunRearElevation = gunModelDefinition.pitch.back const gunRearElevation = gunModelDefinition.pitch!.back
? -gunModelDefinition.pitch.back.min ? -gunModelDefinition.pitch!.back.min
: undefined; : undefined;
const azimuthLeft = turretModelDefinition.yaw const azimuthLeft = turretModelDefinition.yaw
? -turretModelDefinition.yaw.min ? -turretModelDefinition.yaw.min
@@ -496,18 +503,20 @@ export function tankCharacteristics(
const viewRange = turret.view_range * viewRangeCoefficient; const viewRange = turret.view_range * viewRangeCoefficient;
const camouflageStill = tank.camouflage_still * camouflageCoefficientStill; const camouflageStill = tank.camouflage_still * camouflageCoefficientStill;
const camouflageMoving = const camouflageMoving =
tank.class === TankClass.LIGHT tank.class === TankClass.TANK_CLASS_LIGHT
? tank.camouflage_still * camouflageCoefficientStill ? tank.camouflage_still * camouflageCoefficientStill
: tank.camouflage_moving * camouflageCoefficientMoving; : tank.camouflage_moving * camouflageCoefficientMoving;
const camouflageShootingStill = const camouflageShootingStill =
tank.camouflage_still * gun.camouflage_loss * camouflageCoefficientStill; tank.camouflage_still * gun.camouflage_loss * camouflageCoefficientStill;
const camouflageShootingMoving = const camouflageShootingMoving =
(tank.class === TankClass.LIGHT (tank.class === TankClass.TANK_CLASS_LIGHT
? tank.camouflage_still * camouflageCoefficientStill ? tank.camouflage_still * camouflageCoefficientStill
: tank.camouflage_moving * camouflageCoefficientMoving) * : tank.camouflage_moving * camouflageCoefficientMoving) *
gun.camouflage_loss; gun.camouflage_loss;
const camouflageCaughtOnFire = const camouflageCaughtOnFire =
tank.camouflage_onFire * tank.camouflage_still * camouflageCoefficientStill; tank.camouflage_on_fire *
tank.camouflage_still *
camouflageCoefficientStill;
const width = size.z; const width = size.z;
const height = size.x; const height = size.x;
const length = size.y; const length = size.y;
@@ -518,7 +527,7 @@ export function tankCharacteristics(
const shellRange = shell.range; const shellRange = shell.range;
const shellCapacity = gun.shell_capacity; const shellCapacity = gun.shell_capacity;
const gunType = gun.gun_type!.$case; const gunType = gun.gun_type!.$case;
const penetrationAt250m = shell.penetration.far; const penetrationAt250m = shell.penetration!.far;
return { return {
crewCount, crewCount,
@@ -1,5 +1,5 @@
import { awaitableAverageDefinitions } from '../core/awaitables/averageDefinitions'; import { awaitableAverageDefinitions } from "../core/awaitables/averageDefinitions";
import { awaitableDiscoveredIdsDefinitions } from '../core/awaitables/discoveredIdsDefinitions'; import { awaitableDiscoveredIdsDefinitions } from "../core/awaitables/discoveredIdsDefinitions";
const [discoveredIdsDefinitions, averageDefinitions] = await Promise.all([ const [discoveredIdsDefinitions, averageDefinitions] = await Promise.all([
awaitableDiscoveredIdsDefinitions, awaitableDiscoveredIdsDefinitions,
@@ -7,5 +7,5 @@ const [discoveredIdsDefinitions, averageDefinitions] = await Promise.all([
]); ]);
export function useAveragesExclusionRatio() { export function useAveragesExclusionRatio() {
return discoveredIdsDefinitions.count / averageDefinitions.samples.total; return discoveredIdsDefinitions.count / averageDefinitions.samples!.total;
} }
@@ -21,23 +21,23 @@ export function useTankTransform(
track: TrackDefinition, track: TrackDefinition,
turret: TurretDefinition, turret: TurretDefinition,
turretContainer: RefObject<Group>, turretContainer: RefObject<Group>,
gunContainer: RefObject<Group> gunContainer: RefObject<Group>,
) { ) {
const tankModelDefinition = useTankModelDefinition(); const tankModelDefinition = useTankModelDefinition();
useEffect(() => { useEffect(() => {
const trackModelDefinition = tankModelDefinition.tracks[track.id]; const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id]; const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin); const hullOrigin = correctZYTuple(trackModelDefinition.origin!);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin); const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin!);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin); const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin!);
const turretPosition = new Vector3(); const turretPosition = new Vector3();
const turretRotation = new Euler(); const turretRotation = new Euler();
const gunPosition = new Vector3(); const gunPosition = new Vector3();
const gunRotation = new Euler(); const gunRotation = new Euler();
function handleModelTransform( function handleModelTransform(
event: QuicklimeEvent<ModelTransformEventData> event: QuicklimeEvent<ModelTransformEventData>,
) { ) {
handleModelTransformInner(event.data); handleModelTransformInner(event.data);
} }
@@ -70,13 +70,13 @@ export function useTankTransform(
if (tankModelDefinition.initial_turret_rotation) { if (tankModelDefinition.initial_turret_rotation) {
const initialPitch = -degToRad( const initialPitch = -degToRad(
tankModelDefinition.initial_turret_rotation.pitch tankModelDefinition.initial_turret_rotation.pitch,
); );
const initialYaw = -degToRad( const initialYaw = -degToRad(
tankModelDefinition.initial_turret_rotation.yaw tankModelDefinition.initial_turret_rotation.yaw,
); );
const initialRoll = -degToRad( const initialRoll = -degToRad(
tankModelDefinition.initial_turret_rotation.roll tankModelDefinition.initial_turret_rotation.roll,
); );
turretPosition turretPosition
@@ -59,7 +59,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
const tank = Guess.use((state) => state.tank); const tank = Guess.use((state) => state.tank);
const guessState = Guess.use((state) => state.guessState); const guessState = Guess.use((state) => state.guessState);
const isRevealed = guessState !== GuessState.NotGuessed; const isRevealed = guessState !== GuessState.NotGuessed;
const name = unwrap(tank.name); const name = unwrap(tank.name!);
const fontSize = `min(48vh, ${55 / name.length}vw)`; const fontSize = `min(48vh, ${55 / name.length}vw)`;
const transitionDuration = isRevealed ? "2s" : undefined; const transitionDuration = isRevealed ? "2s" : undefined;
const tiers = Guess.use((state) => state.tiers); const tiers = Guess.use((state) => state.tiers);
@@ -77,7 +77,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
if (filteredIds.length === 0) return; if (filteredIds.length === 0) return;
const id = Number( const id = Number(
filteredIds[Math.floor(Math.random() * filteredIds.length)] filteredIds[Math.floor(Math.random() * filteredIds.length)],
); );
const newTank = tankDefinitions.tanks[id]; const newTank = tankDefinitions.tanks[id];
@@ -69,7 +69,7 @@ function Content() {
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
const session = Session.use(); const session = Session.use();
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[] | null>( const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[] | null>(
null null,
); );
const [accountInfo, setAccountInfo] = useState< const [accountInfo, setAccountInfo] = useState<
IndividualAccountInfo | null | undefined IndividualAccountInfo | null | undefined
@@ -81,7 +81,7 @@ function Content() {
if (!id) return setAccountInfo(null); if (!id) return setAccountInfo(null);
setAccountInfo( setAccountInfo(
session.tracking ? await getAccountInfo(idToRegion(id), id) : null session.tracking ? await getAccountInfo(idToRegion(id), id) : null,
); );
})(); })();
}, [session.tracking && session.player.id]); }, [session.tracking && session.player.id]);
@@ -95,20 +95,20 @@ function Content() {
const average = averageDefinitions.averages[tank.id]; const average = averageDefinitions.averages[tank.id];
const composite = compositeStats( const composite = compositeStats(
{ ...entry.all, battle_life_time: entry.battle_life_time }, { ...entry.all, battle_life_time: entry.battle_life_time },
average?.mu average?.mu!,
); );
return { tank, composite }; return { tank, composite };
}) })
: undefined, : undefined,
[session.tracking && session.player, tankStatsB] [session.tracking && session.player, tankStatsB],
); );
const total = useMemo( const total = useMemo(
() => () =>
delta delta
? sumCompositeStats(delta.map(({ composite }) => composite)) ? sumCompositeStats(delta.map(({ composite }) => composite))
: undefined, : undefined,
[delta] [delta],
); );
const search = debounce(async () => { const search = debounce(async () => {
if (!input.current) return; if (!input.current) return;
@@ -150,7 +150,7 @@ function Content() {
const tankStats = await getTankStats( const tankStats = await getTankStats(
idToRegion(session.player.id), idToRegion(session.player.id),
session.player.id session.player.id,
); );
setTankStatsB(tankStats); setTankStatsB(tankStats);
} }
@@ -294,7 +294,7 @@ function Content() {
onClick={async () => { onClick={async () => {
const stats = await getTankStats( const stats = await getTankStats(
session.player.region, session.player.region,
session.player.id session.player.id,
); );
if (stats === null) { if (stats === null) {
@@ -365,7 +365,7 @@ function Content() {
onClick={() => { onClick={() => {
Session.mutate((draft) => { Session.mutate((draft) => {
(draft as SessionTracking).columns.unshift( (draft as SessionTracking).columns.unshift(
key key,
); );
}); });
}} }}
@@ -402,7 +402,8 @@ function Content() {
{compositeStatsKeys {compositeStatsKeys
.filter( .filter(
(item) => (item) =>
!session.columns.includes(item) || item === column !session.columns.includes(item) ||
item === column,
) )
.map((key) => ( .map((key) => (
<Select.Item key={key} value={key}> <Select.Item key={key} value={key}>
@@ -452,7 +453,7 @@ function Content() {
{formatCompositeStat( {formatCompositeStat(
composite[column], composite[column],
column, column,
composite composite,
)} )}
</Table.Cell> </Table.Cell>
))} ))}
@@ -43,7 +43,7 @@ export function Page({ id, skeleton, locale, guide }: PageProps) {
<HeroSection skeleton={skeleton} /> <HeroSection skeleton={skeleton} />
<MetaSection /> <MetaSection />
<CalloutsSection /> <CalloutsSection />
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && ( {tank.type === TankType.TANK_TYPE_RESEARCHABLE && !tank.deprecated && (
<TechTreeSection skeleton={skeleton} /> <TechTreeSection skeleton={skeleton} />
)} )}
<CharacteristicsSection skeleton={skeleton} /> <CharacteristicsSection skeleton={skeleton} />
@@ -50,7 +50,7 @@ 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, {
name: unwrap(tank.name), name: unwrap(tank.name!),
tier: TIER_ROMAN_NUMERALS[tank.tier], tier: TIER_ROMAN_NUMERALS[tank.tier],
nationality: nationality:
strings.common.nations_adjectives[ strings.common.nations_adjectives[
@@ -92,7 +92,7 @@ const characteristics = tankCharacteristics(
const description = literals( const description = literals(
strings.website.tools.tankopedia.tank_seo_description, strings.website.tools.tankopedia.tank_seo_description,
{ {
name: unwrap(tank.name), name: unwrap(tank.name!),
dpm: Math.round(characteristics.dpm).toLocaleString(locale), dpm: Math.round(characteristics.dpm).toLocaleString(locale),
alpha: Math.round(characteristics.damage).toLocaleString(locale), alpha: Math.round(characteristics.damage).toLocaleString(locale),
penetration: Math.round(characteristics.penetration).toLocaleString(locale), penetration: Math.round(characteristics.penetration).toLocaleString(locale),