Merge branch 'main' into ue

This commit is contained in:
TrèsAbhi committed 2026-02-18 19:19:21 -06:00
commit 5412c479c8
42 files changed
+1044 -934

No files matched your search

+3
View File
@@ -17,6 +17,9 @@
"astrojs",
"astrolib",
"atgm",
"autocrop",
"Autoloader",
"Autoreloader",
"AYUV",
"battleresults",
"bbox",
+524 -370
View File
File diff suppressed because it is too large. Load diff
+14
View File
@@ -0,0 +1,14 @@
# BlitzKit 1.29.12
## Fixes
- BlitzKit no longer uses BlitzStars for tank averages for some commands
- BlitzKit Performance was created to completely replace BlitzStars but BlitzStars was still used for some older commands
- But since BlitzStars keeps having downtime, it has been completely removed
- Upgraded all dependencies to the latest versions
- The replay command is now properly localized across all languages
- Special Chinese characters are now properly handled
- Commands are now registered synchronously, allowing errors to reveal their exact origins
- Error handling now properly exists the process after logging the error
- The bot now uses Discord's new `clientReady` instead of the ambiguous `ready` event
- Both the armor and model extractors now use the new BlitzKit `SteamVFS`
+20
View File
@@ -0,0 +1,20 @@
# BlitzKit 1.29.13
## Features
- The screenshot button now automatically crops your image so you don't have excessive blank space all around
- Increased the max zoom out distance in Tankopedia to 50m
## Fixes
- Turrets in Tankopedia now stay within a range of -180 and +180 degrees
- Tanks with transparency like the T34 and Herdbreaker are now properly rendered
- This will take a few days to roll out for all tanks (see screenshots below for the fixed models)
- High End Consumables now give a 33% boost instead of 70%
- Static Armor now properly deselects all irrelevant armor plates
- Removed a few debug loggers left behind
- Static Armor now properly rerenders armor plates when their thicknesses are changed
- The Ricochet Visualizer now stays in place when dragging around on mobile
- Reduced the text in the Tankopedia Compare segmented display to fit on smaller screens
- Sorting by tertiary shells no longer crashes for some tanks
- The Tank Filters are now left aligned on smaller screens for readability
+2 -1
View File
@@ -14,7 +14,7 @@
"build:catalogitemaccesssor": "cd packages/scripts && bun run build:catalogitemaccesssor",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
"build:app": "cd packages/app && bun run build",
"dev:bot": "bun run build:bot --dev && bun start:bot",
"dev:bot": "bun run build:bot --dev && bun start:bot --dev",
"start:bot": "bun dist/bot/main.js",
"dev:docs": "vitepress dev docs",
"dev:website": "cd packages/website-ue && bun run dev",
@@ -47,6 +47,7 @@
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@prisma/client": "^6.18.0",
"prisma": "^6.18.0",
"sharp": "^0.34.5",
"typescript": "^5.9.3"
}
}
+11 -11
View File
@@ -356,9 +356,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [
"serde",
]
@@ -2105,9 +2105,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "num-conv"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-traits"
@@ -3907,30 +3907,30 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.44"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.24"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
+10 -9
View File
@@ -4,25 +4,26 @@
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/themes": "^3.2.1",
"@radix-ui/themes": "^3.3.0",
"@resvg/resvg-js": "^2.6.2",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.3.9",
"@types/lodash-es": "^4.17.12",
"@types/markdown-escape": "^1.1.3",
"@types/react": "^19.2.2",
"@types/react": "^19.2.14",
"adm-zip": "^0.5.16",
"bepaint": "^0.1.0-beta.3",
"discord.js": "^14.22.1",
"dotenv": "^17.2.2",
"discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"fuzzysort": "^3.1.0",
"lodash-es": "^4.17.21",
"lodash-es": "^4.17.23",
"markdown-escape": "^2.0.0",
"pickleparser": "^0.2.1",
"prisma": "^6.17.1",
"prisma": "^6.19.2",
"protobufjs": "^7.5.4",
"react": "^19.2.0",
"satori": "^0.18.3",
"sharp": "^0.34.4"
"react": "^19.2.4",
"satori": "^0.18.4",
"sharp": "^0.34.5"
},
"private": true,
"scripts": {
+4 -4
View File
@@ -1,7 +1,7 @@
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import {
COMMANDS_RAW,
commandsArray,
type CommandRegistry,
} from "../events/interactionCreate";
@@ -35,7 +35,7 @@ export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
if (subcommand === "commands") {
return `${strings.bot.commands.about.subcommands.commands.body}${(
await Promise.all(COMMANDS_RAW)
await Promise.all(commandsArray)
)
.sort((a, b) =>
(a.command.name_localizations?.[interaction.locale] ??
@@ -43,7 +43,7 @@ export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
(b.command.name_localizations?.[interaction.locale] ??
b.command.name)
? -1
: 1
: 1,
)
.map(
(registry) =>
@@ -54,7 +54,7 @@ export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
registry.command.description_localizations?.[
interaction.locale
] ?? registry.command.description
}`
}`,
)
.join("\n")}`;
}
+32 -27
View File
@@ -15,11 +15,16 @@ import { CommandWrapper } from "../../components/CommandWrapper";
import { NoData } from "../../components/NoData";
import { TitleBar } from "../../components/TitleBar";
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
import { tankDefinitions } from "../../core/blitzkit/nonBlockingPromises";
import {
averageDefinitions,
tankDefinitions,
} from "../../core/blitzkit/nonBlockingPromises";
import { UserError } from "../../core/blitzkit/userError";
import { filterStats, type StatFilters } from "../../core/blitzstars/filterStats";
import {
filterStats,
type StatFilters,
} from "../../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
import { blitzStarsTankAverages } from "../../core/blitzstars/tankAverages";
import type { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
import type { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
import { translator } from "../../core/localization/translator";
@@ -31,11 +36,11 @@ export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale
locale: Locale,
) {
const { strings, unwrap } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await blitzStarsTankAverages;
const awaitedTankAverages = await averageDefinitions;
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id);
@@ -53,14 +58,14 @@ export async function renderBreakdown(
}
filteredOrder.forEach((id) => {
const tankAveragesEntry = awaitedTankAverages[id];
const tankAveragesEntry = awaitedTankAverages.averages[id];
const current = statsInPeriod.diff[id];
const career = tankStats.find((tank) => tank.tank_id === id)!.all;
const currentWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, current)
? calculateWN8(tankAveragesEntry.mu, current)
: undefined;
const careerWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, career)
? calculateWN8(tankAveragesEntry.mu, career)
: undefined;
orderedCurrentStats.push(current);
@@ -69,65 +74,65 @@ export async function renderBreakdown(
orderedCareerWN8.push(careerWN8);
});
tankStats.forEach((tank) => {
const tankAveragesEntry = awaitedTankAverages[tank.tank_id];
const tankAveragesEntry = awaitedTankAverages.averages[tank.tank_id];
orderedCareerWN8Full.push(
tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, tank.all)
: undefined
? calculateWN8(tankAveragesEntry.mu, tank.all)
: undefined,
);
});
const currentBattles = orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.battles,
0
0,
);
const careerBattles = tankStats.reduce(
(accumulator, current) => accumulator + current.all.battles,
0
0,
);
const currentWinrate =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.wins,
0
0,
) / currentBattles;
const careerWinrate =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.wins,
0
0,
) / careerBattles;
const currentWN8 =
orderedCurrentWN8.reduce<number>(
(accumulator, current, index) =>
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
0
0,
) /
orderedCurrentStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
0
0,
);
const careerWN8 =
orderedCareerWN8Full.reduce<number>(
(accumulator, current) => accumulator + (current ?? 0),
0
0,
) /
tankStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
0
0,
);
const currentDamage =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.damage_dealt,
0
0,
) / currentBattles;
const careerDamage =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.damage_dealt,
0
0,
) / careerBattles;
const children: React.JSX.Element[] = [];
@@ -172,7 +177,7 @@ export async function renderBreakdown(
delta: currentDamage - careerDamage,
},
]}
/>
/>,
);
filteredOrder.forEach((id, index) => {
@@ -206,7 +211,7 @@ export async function renderBreakdown(
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * (current.wins / current.battles)).toFixed(
2
2,
)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
@@ -230,24 +235,24 @@ export async function renderBreakdown(
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(
current.damage_dealt / current.battles
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>
/>,
);
});
}
const pages = chunk(
children,
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE)
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
);
if (filteredOrder.length > 0) {
+92 -77
View File
@@ -5,28 +5,29 @@ import {
idToRegion,
tankIcon,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import { Locale } from 'discord.js';
import { clamp } from 'lodash-es';
import { CommandWrapper } from '../components/CommandWrapper';
import { TitleBar } from '../components/TitleBar';
import { iconPng } from '../core/blitzkit/iconPng';
} from "@blitzkit/core";
import { Locale } from "discord.js";
import { clamp } from "lodash-es";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { iconPng } from "../core/blitzkit/iconPng";
import {
averageDefinitions,
mapDefinitions,
tankDefinitions,
} from '../core/blitzkit/nonBlockingPromises';
import { RenderConfiguration } from '../core/blitzkit/renderConfiguration';
import { blitzStarsTankAverages } from '../core/blitzstars/tankAverages';
import { buttonLink } from '../core/discord/buttonLink';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { type CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
} from "../core/blitzkit/nonBlockingPromises";
import { RenderConfiguration } from "../core/blitzkit/renderConfiguration";
import { buttonLink } from "../core/discord/buttonLink";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
type WotInspectorReplaySubmission =
| { status: 'error'; data: unknown; error: { message: string } }
| { status: "error"; data: unknown; error: { message: string } }
| {
status: 'ok';
status: "ok";
data: {
download_url: string;
view_url: string;
@@ -209,9 +210,9 @@ function stat(value: string | number, flex = 2) {
<div
style={{
flex,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 16 }}>{value}</span>
@@ -227,15 +228,15 @@ async function playerListing(
recordingPlayer = false,
) {
const { unwrap } = translator(locale);
const awaitedTankAverages = await blitzStarsTankAverages;
const awaitedTankAverages = await averageDefinitions;
const awaitedTankDefinitions = await tankDefinitions;
const tank = awaitedTankDefinitions.tanks[player.vehicle_descr];
const blockAccent = recordingPlayer ? '_amber' : '';
const blockAccent = recordingPlayer ? "_amber" : "";
const healthAccent = protagonistTeam
? recordingPlayer
? '_amber'
: '_green'
: '_red';
? "_amber"
: "_green"
: "_red";
// our turret ids are always off by 1 or 2 lol so imma just grab the closest
// TODO: 💀
@@ -245,11 +246,11 @@ async function playerListing(
)[0];
const totalHealth = turret.health + tank.health;
const healthLeft = clamp(player.hitpoints_left / totalHealth, 0, 1);
const tankAveragesEntry = awaitedTankAverages[player.vehicle_descr];
const tankAveragesEntry = awaitedTankAverages.averages[player.vehicle_descr];
const wn8 =
tankAveragesEntry === undefined
? -1
: calculateWN8(tankAveragesEntry.all, {
: calculateWN8(tankAveragesEntry.mu, {
battles: 1,
damage_dealt: player.damage_made,
dropped_capture_points: player.base_defend_points,
@@ -262,8 +263,8 @@ async function playerListing(
<div
key={player.entity_id}
style={{
display: 'flex',
alignItems: 'center',
display: "flex",
alignItems: "center",
gap: 8,
backgroundColor: theme.colors[`appBackground2${blockAccent}`],
padding: 8,
@@ -271,11 +272,11 @@ async function playerListing(
>
<div
style={{
display: 'flex',
display: "flex",
gap: 8,
flex: 1,
overflow: 'hidden',
alignItems: 'center',
overflow: "hidden",
alignItems: "center",
}}
>
<img
@@ -284,22 +285,22 @@ async function playerListing(
style={{
width: 32,
height: 32,
objectFit: 'contain',
objectFit: "contain",
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
// align
}}
>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<span
@@ -324,23 +325,23 @@ async function playerListing(
style={{
fontSize: 16,
color: theme.colors[`textLowContrast${blockAccent}`],
whiteSpace: 'nowrap',
whiteSpace: "nowrap",
}}
>
{player.name}
{player.clan_tag ? ` [${player.clan_tag}]` : ''}
{player.clan_tag ? ` [${player.clan_tag}]` : ""}
</span>
</div>
</div>
<div
style={{
display: 'flex',
display: "flex",
flex: 1,
}}
>
{stat(player.damage_made.toLocaleString())}
{stat(wn8 === -1 ? '--' : Math.round(wn8).toLocaleString())}
{stat(wn8 === -1 ? "--" : Math.round(wn8).toLocaleString())}
{stat(player.exp.toLocaleString())}
{stat(player.enemies_destroyed, 1)}
</div>
@@ -349,15 +350,15 @@ async function playerListing(
style={{
borderRadius: 2,
backgroundColor: theme.colors[`componentInteractive${healthAccent}`],
display: 'flex',
display: "flex",
width: 4,
height: '100%',
transform: 'scaleY(-1)',
height: "100%",
transform: "scaleY(-1)",
}}
>
<div
style={{
width: '100%',
width: "100%",
height: `${healthLeft * 100}%`,
borderRadius: 2,
backgroundColor: theme.colors[`solidBackground${healthAccent}`],
@@ -373,8 +374,8 @@ function Header() {
<div
style={{
padding: 8,
display: 'flex',
alignItems: 'center',
display: "flex",
alignItems: "center",
gap: 8,
}}
>
@@ -382,16 +383,16 @@ function Header() {
<div
style={{
display: 'flex',
display: "flex",
flex: 1,
}}
>
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -406,9 +407,9 @@ function Header() {
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -423,9 +424,9 @@ function Header() {
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -440,9 +441,9 @@ function Header() {
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -460,28 +461,42 @@ function Header() {
}
export const replayCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand('replay').addAttachmentOption((option) =>
option.setName('file').setDescription('Replay file').setRequired(true),
command: createLocalizedCommand("replay").addAttachmentOption((option) =>
option
.setName(strings.bot.commands.replay.options.file.name)
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.replay.options.file.name,
undefined,
true,
),
)
.setDescription(strings.bot.commands.replay.options.file.description)
.setDescriptionLocalizations(
localizationObject(
(strings) => strings.bot.commands.replay.options.file.description,
),
)
.setRequired(true),
),
async handler(interaction) {
const awaitedMapDefinitions = await mapDefinitions;
const file = interaction.options.getAttachment('file')!;
// const file = {
// url: 'https://cdn.discordapp.com/ephemeral-attachments/1232434891652861972/1232781496016834581/20240328_1530__TresAbhi_M60_1157057448155406708.wotbreplay?ex=662ab4bc&is=6629633c&hm=c784cd10e80232350ddaf291fa987b879c4d0a8083b78298c9eee4fa0d507f6c&',
// };
const file = interaction.options.getAttachment("file")!;
const submissionData = (await fetch(
`https://wotinspector.com/api/replay/upload/?url=${encodeURIComponent(
file.url,
)}`,
).then((response) => response.json())) as WotInspectorReplaySubmission;
if (submissionData.status === 'error') {
if (submissionData.status === "error") {
return `# Uh oh! WoT Inspector didn't like that.\n\nThere was an error processing your replay. Get help on [the official Discord server](https://discord.gg/nDt7AjGJQH).`;
}
const replayId = submissionData.data.download_url.split('/').at(-1)!;
const replayId = submissionData.data.download_url.split("/").at(-1)!;
const replayData = (await fetch(
`https://api.wotinspector.com/v2/blitz/replays/${replayId}/`,
).then((response) => response.json())) as WotInspectorReplay;
@@ -493,7 +508,7 @@ export const replayCommand = new Promise<CommandRegistry>((resolve) => {
await getClanAccountInfo(
idToRegion(replayData.protagonist),
replayData.protagonist,
['clan'],
["clan"],
)
)?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
@@ -506,22 +521,22 @@ export const replayCommand = new Promise<CommandRegistry>((resolve) => {
image={clanImage}
description={`${
replayData.protagonist_team === replayData.winner_team
? 'Victory'
: 'Defeat'
? "Victory"
: "Defeat"
} • ${awaitedMapDefinitions.maps[replayData.map_id].name}`}
/>
<div
style={{
display: 'flex',
display: "flex",
gap: 8,
}}
>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
@@ -547,8 +562,8 @@ export const replayCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
@@ -569,8 +584,8 @@ export const replayCommand = new Promise<CommandRegistry>((resolve) => {
</div>
</div>
</CommandWrapper>,
buttonLink(submissionData.data.view_url, 'WoT Inspector'),
buttonLink(submissionData.data.download_url, 'Download'),
buttonLink(submissionData.data.view_url, "WoT Inspector"),
buttonLink(submissionData.data.download_url, "Download"),
];
},
});
@@ -1,11 +1,13 @@
import {
fetchAverageDefinitions,
fetchDiscoveredIdsDefinitions,
fetchGameDefinitions,
fetchMapDefinitions,
fetchTankDefinitions,
fetchTankNames,
} from '@blitzkit/core';
} from "@blitzkit/core";
export const averageDefinitions = fetchAverageDefinitions();
export const tankDefinitions = fetchTankDefinitions();
export const gameDefinitions = fetchGameDefinitions();
export const tankNames = fetchTankNames();
+26 -18
View File
@@ -1,29 +1,37 @@
import { usersDatabase } from '../db/users';
import { client } from '../discord/client';
import { usersDatabase } from "../db/users";
import { client } from "../discord/client";
const EXIT_EVENTS = [
'beforeExit',
'exit',
'SIGINT',
'SIGTERM',
'SIGUSR1',
'SIGUSR2',
];
const EXIT_EVENTS = ["SIGINT", "SIGTERM", "SIGUSR1", "SIGUSR2"];
async function cleanup() {
console.log('Cleaning up...');
let cleaningUp = false;
await Promise.all([usersDatabase.$disconnect(), client.destroy()]);
async function cleanup(signal: string) {
if (cleaningUp) return;
cleaningUp = true;
console.log('Gracefully exiting...');
console.log(`Cleaning up... (${signal})`);
try {
await Promise.all([usersDatabase.$disconnect(), client.destroy()]);
} catch (err) {
console.error("Cleanup error:", err);
}
console.log("Gracefully exiting...");
EXIT_EVENTS.forEach((event) => process.off(event, cleanup));
process.exit(0);
}
export function registerProcesses() {
process.on('uncaughtException', console.error);
client.on('error', console.error);
process.on("uncaughtException", (err) => {
console.error("uncaughtException:", err);
});
EXIT_EVENTS.forEach((event) => process.on(event, cleanup));
process.on("unhandledRejection", (err) => {
console.error("unhandledRejection:", err);
});
client.on("error", console.error);
EXIT_EVENTS.forEach((event) => process.on(event, () => cleanup(event)));
}
+15 -11
View File
@@ -3,8 +3,10 @@ import type { SupplementaryStats } from "@blitzkit/core/src/blitz/getAccountInfo
import { calculateWN8 } from "@blitzkit/core/src/statistics/calculateWN8";
import type { BlitzStats } from "@blitzkit/core/src/statistics/compositeStats";
import { sumAllStats } from "@blitzkit/core/src/statistics/sumAllStats";
import { tankDefinitions } from "../blitzkit/nonBlockingPromises";
import { blitzStarsTankAverages } from "./tankAverages";
import {
averageDefinitions,
tankDefinitions,
} from "../blitzkit/nonBlockingPromises";
export interface StatFilters {
nation?: string;
@@ -16,13 +18,13 @@ export interface StatFilters {
export async function filterStats(
{ diff, order }: DiffedTankStats,
filters: StatFilters
filters: StatFilters,
) {
// tank, if provided, takes priority over all other filters
if (filters.tank) filters = { tank: filters.tank };
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await blitzStarsTankAverages;
const awaitedTankAverages = await averageDefinitions;
const filteredOrder = order.filter((id) => {
const entry = awaitedTankDefinitions.tanks[id];
@@ -44,26 +46,28 @@ export async function filterStats(
const stats = sumAllStats(filteredOrder.map((id) => diff[id]));
const battlesOfTanksWithAverages = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id] ? accumulator + diff[id].battles : accumulator,
0
awaitedTankAverages.averages[id]
? accumulator + diff[id].battles
: accumulator,
0,
);
const battlesOfTanksWithTankDefinition = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankDefinitions.tanks[id]
? accumulator + diff[id].battles
: accumulator,
0
0,
);
const supplementary = {
WN8:
filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id]
awaitedTankAverages.averages[id]
? accumulator +
calculateWN8(awaitedTankAverages[id].all, diff[id]) *
calculateWN8(awaitedTankAverages.averages[id].mu, diff[id]) *
diff[id].battles
: accumulator,
0
0,
) / battlesOfTanksWithAverages,
tier:
filteredOrder.reduce<number>(
@@ -72,7 +76,7 @@ export async function filterStats(
? accumulator +
awaitedTankDefinitions.tanks[id]!.tier * diff[id].battles
: accumulator,
0
0,
) / battlesOfTanksWithTankDefinition,
} satisfies SupplementaryStats;
@@ -1,101 +0,0 @@
import { BlitzStats } from '@blitzkit/core';
import { context } from '@blitzkit/core/src/blitzkit/context';
interface SpecialStats {
winrate: number;
damageRatio: number;
kdr: number;
damagePerBattle: number;
killsPerBattle: number;
hitsPerBattle: number;
spotsPerBattle: number;
wpm: number;
dpm: number;
kpm: number;
hitRate: number;
survivalRate: number;
}
export type Percentiles = [
number,
number,
number,
number,
number,
number,
number,
number,
number,
];
export interface SpecialPercentiles {
winRate: Percentiles;
damageRatio: Percentiles;
kdr: Percentiles;
damagePerBattle: Percentiles;
killsPerBattle: Percentiles;
hitsPerBattle: Percentiles;
spotsPerBattle: Percentiles;
wpm: Percentiles;
dpm: Percentiles;
kpm: Percentiles;
hitRate: Percentiles;
survivalRate: Percentiles;
}
export interface MeanSd {
winrateMean: number;
winrateSd: number;
hitRateMean: number;
hitRateSd: number;
survivalRateMean: number;
survivalRateSd: number;
damageRatioMean: number;
damageRatioSd: number;
kdrMean: number;
kdrSd: number;
dpbMean: number;
dpbSd: number;
kpbMean: number;
kpbSd: number;
hpbMean: number;
hpbSd: number;
spbMean: number;
spbSd: number;
}
export interface IndividualTankAverage {
_id: number;
battle_life_time: number;
total_battle_life_time: number;
number_of_players: number;
tank_id: number;
all: BlitzStats;
special: SpecialStats;
percentiles: SpecialPercentiles;
percentiles_player_count: number;
meanSd: MeanSd;
shotEff: number;
last_update: number;
}
export type BlitzStarsTankAverages = Record<number, IndividualTankAverage>;
export const blitzStarsTankAverages = fetch(
'https://www.blitzstars.com/api/tankaverages.json',
)
.then((response) => response.json())
.then((json) => {
if (context === 'server') {
const partialTankAverages: BlitzStarsTankAverages = {};
(json as IndividualTankAverage[]).forEach((individualTankAverage) => {
partialTankAverages[individualTankAverage.tank_id] =
individualTankAverage;
});
return partialTankAverages;
} else {
return json as BlitzStarsTankAverages;
}
});
+5 -5
View File
@@ -1,9 +1,9 @@
import { Client, GatewayIntentBits } from 'discord.js';
import { interactionCreate } from '../../events/interactionCreate';
import { ready } from '../../events/ready';
import { Client, GatewayIntentBits } from "discord.js";
import { interactionCreate } from "../../events/interactionCreate";
import { ready } from "../../events/ready";
export const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
.on('ready', ready)
.on('interactionCreate', interactionCreate);
.on("clientReady", ready)
.on("interactionCreate", interactionCreate);
@@ -22,11 +22,11 @@ export function createLocalizedCommand(
command: string,
sub?:
| CreateLocalizedCommandSubcommand[]
| CreateLocalizedCommandSubcommandGroup[]
| CreateLocalizedCommandSubcommandGroup[],
) {
const commandPathItem = command.replaceAll(
"-",
"_"
"_",
) as keyof BlitzKitStrings["bot"]["commands"];
const { strings } = translator(Locale.EnglishUS);
const slashCommand = new SlashCommandBuilder()
@@ -35,14 +35,14 @@ export function createLocalizedCommand(
localizationObject(
(strings) => strings.bot.commands[commandPathItem].name,
undefined,
true
)
true,
),
)
.setDescription(strings.bot.commands[commandPathItem].description)
.setDescriptionLocalizations(
localizationObject(
(strings) => strings.bot.commands[commandPathItem].description
)
(strings) => strings.bot.commands[commandPathItem].description,
),
);
if (sub) {
@@ -56,7 +56,7 @@ export function createLocalizedCommand(
}
).subcommands[subItem.subcommand].name,
undefined,
true
true,
);
const subcommandDescriptionLocalizations = localizationObject(
(strings) =>
@@ -64,7 +64,7 @@ export function createLocalizedCommand(
strings.bot.commands[commandPathItem] as unknown as {
subcommands: Record<string, { description: string }>;
}
).subcommands[subItem.subcommand].description
).subcommands[subItem.subcommand].description,
);
slashCommand.addSubcommand((option) => {
@@ -78,7 +78,7 @@ export function createLocalizedCommand(
strings.bot.commands[commandPathItem] as unknown as {
subcommands: Record<string, { description: string }>;
}
).subcommands[subItem.subcommand].description
).subcommands[subItem.subcommand].description,
)
.setDescriptionLocalizations(subcommandDescriptionLocalizations);
});
@@ -91,7 +91,7 @@ export function createLocalizedCommand(
}
).groups[subItem.group].name,
undefined,
true
true,
);
const subcommandDescriptionLocalizations = localizationObject(
(strings) =>
@@ -99,7 +99,7 @@ export function createLocalizedCommand(
strings.bot.commands[commandPathItem] as unknown as {
groups: Record<string, { description: string }>;
}
).groups[subItem.group].description
).groups[subItem.group].description,
);
slashCommand.addSubcommandGroup((option) => {
@@ -113,7 +113,7 @@ export function createLocalizedCommand(
strings.bot.commands[commandPathItem] as unknown as {
groups: Record<string, { description: string }>;
}
).groups[subItem.group].description
).groups[subItem.group].description,
)
.setDescriptionLocalizations(subcommandDescriptionLocalizations);
});
@@ -1,10 +1,10 @@
import { AutocompleteInteraction, CacheType } from 'discord.js';
import { commands } from '..';
import { AutocompleteInteraction, CacheType } from "discord.js";
import { commands } from "..";
export async function handleAutocomplete(
interaction: AutocompleteInteraction<CacheType>,
) {
const command = (await commands)[interaction.commandName];
const command = commands[interaction.commandName];
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
@@ -1,13 +1,13 @@
import { ButtonInteraction, CacheType } from 'discord.js';
import { commands } from '..';
import { normalizeInteractionReturnable } from '../../../core/discord/normalizeInteractionReturnable';
import { ButtonInteraction, CacheType } from "discord.js";
import { commands } from "..";
import { normalizeInteractionReturnable } from "../../../core/discord/normalizeInteractionReturnable";
export async function handleButton(interaction: ButtonInteraction<CacheType>) {
await interaction.deferUpdate();
const url = new URL(`https://example.com/${interaction.customId}`);
const commandName = url.pathname.split('/')[1];
const button = (await commands)[commandName]?.button;
const commandName = url.pathname.split("/")[1];
const button = commands[commandName]?.button;
if (!button) {
throw new Error(`Button handler not found for "${commandName}"`);
@@ -16,7 +16,7 @@ export async function handleButton(interaction: ButtonInteraction<CacheType>) {
if (interaction.message.interaction?.user.id !== interaction.user.id) {
interaction.followUp({
ephemeral: true,
content: 'You are not the author of this command.',
content: "You are not the author of this command.",
});
return;
}
@@ -17,12 +17,11 @@ import { translator } from "../../../core/localization/translator";
import type { Writeable } from "../../../types/writable";
export async function handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>
interaction: ChatInputCommandInteraction<CacheType>,
) {
await interaction.deferReply();
const awaitedCommands = await commands;
const registry = awaitedCommands[interaction.commandName];
const registry = commands[interaction.commandName];
try {
const returnable = await registry.handler(interaction);
@@ -36,7 +35,7 @@ export async function handleChatInputCommand(
if (typeof object === "string") {
chunks.push([object]);
} else chunks.at(-1)?.push(object!);
}
},
);
let index = 0;
@@ -59,7 +58,7 @@ export async function handleChatInputCommand(
} else if (psa.data.type === "image") {
if (!reply.files) reply.files = [];
(reply.files as Writeable<typeof reply.files>).push(
new AttachmentBuilder(psa.data.image)
new AttachmentBuilder(psa.data.image),
);
reply.content = reply.content ?? psa.data.title;
}
@@ -72,7 +71,7 @@ export async function handleChatInputCommand(
(
reply.components[0] as ActionRowBuilder<ButtonBuilder>
).addComponents(
psa.data.links.map(({ title, url }) => buttonLink(url, title))
psa.data.links.map(({ title, url }) => buttonLink(url, title)),
);
}
}
@@ -103,7 +102,7 @@ export async function handleChatInputCommand(
} else if (psa.data.type === "image") {
if (!followUp.files) followUp.files = [];
(followUp.files as Writeable<typeof followUp.files>).push(
new AttachmentBuilder(psa.data.image)
new AttachmentBuilder(psa.data.image),
);
followUp.content = followUp.content ?? psa.data.title;
}
@@ -116,7 +115,7 @@ export async function handleChatInputCommand(
(
followUp.components[0] as ActionRowBuilder<ButtonBuilder>
).addComponents(
psa.data.links.map(({ title, url }) => buttonLink(url, title))
psa.data.links.map(({ title, url }) => buttonLink(url, title)),
);
}
@@ -133,7 +132,7 @@ export async function handleChatInputCommand(
new ButtonBuilder()
.setLabel(strings.bot.common.errors.get_help)
.setURL("https://discord.gg/nDt7AjGJQH")
.setStyle(ButtonStyle.Link)
.setStyle(ButtonStyle.Link),
),
];
if (error instanceof UserError) {
@@ -72,13 +72,13 @@ interface CommandRegistryImplicit extends CommandRegistryBase {
interface CommandRegistryExplicit extends CommandRegistryBase {
handlesInteraction?: false;
handler: (
interaction: ChatInputCommandInteraction<CacheType>
interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable;
}
export type CommandRegistry = CommandRegistryImplicit | CommandRegistryExplicit;
export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
export const commandsArray: Promise<CommandRegistry>[] = [
// auctionCommand,
permissionsCommand,
debugCommand,
@@ -101,34 +101,28 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
replayCommand,
];
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
return rawCommands.reduce<Record<string, CommandRegistry>>(
(commands, registry, index) => {
if (registry.status === "rejected") {
console.warn(
`Command ${index} failed to load; skipping...`,
registry.reason
);
return commands;
}
return { ...commands, [registry.value.command.name]: registry.value };
},
{}
);
});
export const commands: Record<string, CommandRegistry> = {};
const commandsFulfilled: CommandRegistry[] = [];
let index = 0;
for (const registry of await Promise.allSettled(commandsArray)) {
if (registry.status === "rejected") {
console.warn(`Command ${index} rejected`, registry.reason);
continue;
}
commandsFulfilled.push(registry.value);
commands[registry.value.command.name] = registry.value;
index++;
}
const rest = new REST().setToken(assertSecret(import.meta.env.DISCORD_TOKEN));
const body = commandsFulfilled.map((registry) => registry.command.toJSON());
const applicationCommandsRoute = Routes.applicationCommands(
assertSecret(import.meta.env.DISCORD_CLIENT_ID),
);
commands.then((awaitedCommands) => {
const body = Object.values(awaitedCommands).map((registry) =>
registry.command.toJSON()
);
rest.put(
Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)),
{ body }
);
});
await rest.put(applicationCommandsRoute, { body });
export async function interactionCreate(interaction: Interaction<CacheType>) {
if (interaction.isAutocomplete()) {
+14 -14
View File
@@ -1,27 +1,27 @@
import { assertSecret } from '@blitzkit/core';
import { ActivityType, ShardingManager } from 'discord.js';
import { assertSecret } from "@blitzkit/core";
import { ActivityType, ShardingManager } from "discord.js";
import {
discoveredIdsDefinitions,
tankDefinitions,
} from './core/blitzkit/nonBlockingPromises';
} from "./core/blitzkit/nonBlockingPromises";
let manager: ShardingManager;
let iteration = 0;
const interval = setInterval(async () => {
iteration++;
console.log(`Attempting launch on iteration ${++iteration}`);
console.log(`Attempting launch on iteration ${iteration}`);
const manager = new ShardingManager('dist/bot/workers/bot.js', {
manager?.respawnAll();
manager = new ShardingManager("dist/bot/workers/bot.js", {
token: assertSecret(import.meta.env.DISCORD_TOKEN),
});
const shards = await manager
.on('shardCreate', (shard) => {
.on("shardCreate", (shard) => {
clearInterval(interval);
console.log(`🟡 Launching shard ${shard.id}`);
shard.on('ready', () => console.log(`🟢 Launched shard ${shard.id}`));
shard.on("ready", () => console.log(`🟢 Launched shard ${shard.id}`));
})
.spawn();
.spawn({ timeout: 60000 });
try {
let servers = 0;
@@ -29,7 +29,7 @@ const interval = setInterval(async () => {
let users = 0;
for (const [, shard] of shards) {
const guilds = (await shard.fetchClientValue('guilds.cache')) as any;
const guilds = (await shard.fetchClientValue("guilds.cache")) as any;
servers += guilds.length;
@@ -66,7 +66,7 @@ const interval = setInterval(async () => {
for (const guild of client.guilds.cache.values()) {
try {
const botMember = await guild.members.fetch(client.user.id);
await botMember.setNickname('BlitzKit');
await botMember.setNickname("BlitzKit");
} catch {}
}
},
@@ -74,8 +74,8 @@ const interval = setInterval(async () => {
);
} catch (error) {
console.warn(
'An error occurred when setting up presence, ignoring to keep the process alive',
"An error occurred when setting up presence, ignoring to keep the process alive",
error,
);
}
}, 5000);
}, 15000);
+3 -3
View File
@@ -1,6 +1,6 @@
import { assertSecret } from '@blitzkit/core';
import { registerProcesses } from '../core/blitzkit/processes';
import { client } from '../core/discord/client';
import { assertSecret } from "@blitzkit/core";
import { registerProcesses } from "../core/blitzkit/processes";
import { client } from "../core/discord/client";
console.log(`🟡 Launching bot ${client.shard?.ids[0]}`);
+4 -4
View File
@@ -493,7 +493,7 @@
"description": "Search clans",
"options": {
"limit": {
"name": "limit",
"name": "",
"description": "The max number of clan results (default: {limit})"
}
},
@@ -507,7 +507,7 @@
"description": "Search players",
"options": {
"limit": {
"name": "limit",
"name": "",
"description": "The max number of player results (default: {limit})"
}
},
@@ -531,7 +531,7 @@
},
"options": {
"limit": {
"name": "limit",
"name": "",
"description": "How many neighbors to display (default: {count})"
},
"season": {
@@ -675,7 +675,7 @@
"active": "{name} (Active)",
"no_tanks_found": {
"body": "No tanks found.",
"search_again": "Try searching again.",
"search_again": "",
"clear_filters": "Try clearing filters."
},
"sort": {
+2 -2
View File
@@ -24,7 +24,7 @@
"shells": {
"ap": "ББ",
"ap_cr": "БП",
"hc": "ОФ",
"hc": "КС",
"he": "ОФ"
},
"player_count_period": {
@@ -39,7 +39,7 @@
"branches": {
"local": "dev",
"dev": "бета",
"opentest": "открытый тест",
"opentest": "общий тест",
"preview": "превью"
},
"locales": {
+12 -19
View File
@@ -1,15 +1,10 @@
import { build } from "esbuild";
import { cp, readdir, rm } from "fs/promises";
import { argv } from "process";
const dev = argv.includes("--dev");
const projectDir = "../..";
const srcRoot = `${projectDir}/packages/bot`;
const distRoot = `${projectDir}/dist/bot`;
const modulesRoot = dev
? "../../packages/bot/node_modules"
: "../../node_modules";
const modulesRoot = "../../node_modules";
const workers = await readdir(`${srcRoot}/src/workers`);
await rm(distRoot, { recursive: true, force: true });
@@ -30,9 +25,9 @@ build({
},
external: ["sharp"],
minifyIdentifiers: false, // causes goofy ahh issues
minifySyntax: !dev,
minifyWhitespace: !dev,
sourcemap: !dev,
minifySyntax: true,
minifyWhitespace: true,
sourcemap: true,
jsxImportSource: "react",
jsx: "automatic",
});
@@ -44,18 +39,16 @@ for (const module of await readdir(`${modulesRoot}/@resvg`)) {
.map((file) =>
cp(
`${modulesRoot}/@resvg/${module}/${file}`,
`${distRoot}/workers/${file}`
)
)
`${distRoot}/workers/${file}`,
),
),
);
}
if (!dev) {
await cp(`${projectDir}/prisma`, `${distRoot}/prisma`, { recursive: true });
await cp(`${projectDir}/prisma`, `${distRoot}/prisma`, { recursive: true });
for (const file of await readdir(`${modulesRoot}/prisma`).then((files) =>
files.filter((file) => file.endsWith(".node"))
)) {
cp(`${modulesRoot}/prisma/${file}`, `${distRoot}/prisma/${file}`);
}
for (const file of await readdir(`${modulesRoot}/prisma`).then((files) =>
files.filter((file) => file.endsWith(".node")),
)) {
cp(`${modulesRoot}/prisma/${file}`, `${distRoot}/prisma/${file}`);
}
+11 -11
View File
@@ -6,7 +6,7 @@ import {
} from "@blitzkit/core";
import { Accessor, Document, Node, Scene } from "@gltf-transform/core";
import { times } from "lodash-es";
import { SteamVFS } from "../vfs/vfs";
import { SteamVFS } from "../vfs/steam";
import {
vertexAttributeGLTFName,
vertexAttributeGltfVectorSizes,
@@ -16,10 +16,10 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
const sc2Path = `Data/3d/Tanks/CollisionMeshes/${fileName}.sc2`;
const scgPath = `Data/3d/Tanks/CollisionMeshes/${fileName}.scg`;
const sc2 = new Sc2ReadStream(
(await vfs.file(sc2Path)).buffer as ArrayBuffer
(await vfs.file(sc2Path)).buffer as ArrayBuffer,
).sc2();
const scg = new ScgReadStream(
(await vfs.file(scgPath)).buffer as ArrayBuffer
(await vfs.file(scgPath)).buffer as ArrayBuffer,
).scg();
const document = new Document();
const scene = document.createScene();
@@ -29,7 +29,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
hierarchies.forEach((hierarchy) => {
const components = times(
hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, "0")]
(index) => hierarchy.components[index.toString().padStart(4, "0")],
);
components.forEach((component) => {
@@ -43,7 +43,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
if (!polygonGroup) {
console.warn(
`Missing polygon group ${batch["rb.datasource"]} (${hierarchy.name}); skipping...`
`Missing polygon group ${batch["rb.datasource"]} (${hierarchy.name}); skipping...`,
);
break;
@@ -73,7 +73,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
VertexAttribute.TANGENT,
attributes
.get(VertexAttribute.TANGENT)!
.map((tangent) => [...tangent, 1])
.map((tangent) => [...tangent, 1]),
);
}
@@ -94,7 +94,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
hardJointIndices.forEach((hardJointIndex) => {
const node = document.createNode(
`${hierarchy.name}_armor_${hardJointIndex}`
`${hierarchy.name}_armor_${hardJointIndex}`,
);
const mesh = document.createMesh(batch["##name"]);
const indicesAccessor = document
@@ -104,9 +104,9 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
new Uint16Array(
polygonGroup.indices.filter(
(index) =>
vertexHardJointIndices.get(index) === hardJointIndex
)
)
vertexHardJointIndices.get(index) === hardJointIndex,
),
),
)
.setBuffer(buffer);
const primitive = document
@@ -127,7 +127,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
default:
throw new TypeError(
`Unhandled component type: ${component["comp.typename"]}`
`Unhandled component type: ${component["comp.typename"]}`,
);
}
});
@@ -9,7 +9,7 @@ import { Document, Material, Node, Scene } from "@gltf-transform/core";
import { dedup, prune } from "@gltf-transform/functions";
import { times } from "lodash-es";
import { dirname } from "path";
import { SteamVFS } from "../../vfs/vfs";
import { SteamVFS } from "../../vfs/steam";
import { readBaseColor } from "../readBaseColor";
import { readNormal } from "../readNormal";
import { readRoughnessMetallic } from "../readRoughnessMetallic";
@@ -29,10 +29,10 @@ export async function extractModel(vfs: SteamVFS, path: string) {
const sc2Path = `Data/3d/${path}.sc2`;
const scgPath = `Data/3d/${path}.scg`;
const sc2 = new Sc2ReadStream(
(await vfs.file(sc2Path)).buffer as ArrayBuffer
(await vfs.file(sc2Path)).buffer as ArrayBuffer,
).sc2();
const scg = new ScgReadStream(
(await vfs.file(scgPath)).buffer as ArrayBuffer
(await vfs.file(scgPath)).buffer as ArrayBuffer,
).scg();
const document = new Document();
const scene = document.createScene();
@@ -61,20 +61,17 @@ export async function extractModel(vfs: SteamVFS, path: string) {
}
if (textures) {
const { image: baseColor, hasAlpha } = await readBaseColor(
`Data/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
textures.miscMap
? `Data/3d/${dirname(path)}/${textures.miscMap}`
: undefined,
);
material.setBaseColorTexture(
document
.createTexture(node.materialName)
.setMimeType("image/webp")
.setImage(
await readBaseColor(
`Data/3d/${dirname(path)}/${
textures.baseColorMap ?? textures.albedo
}`,
textures.miscMap
? `Data/3d/${dirname(path)}/${textures.miscMap}`
: undefined
)
)
.setImage(baseColor),
);
if (node.configCount) {
@@ -83,15 +80,7 @@ export async function extractModel(vfs: SteamVFS, path: string) {
configIndex < node.configCount;
configIndex++
) {
const archive = node[`configArchive_${configIndex}`];
if (
archive.configName !== "Default" ||
!archive.enabledPresets?.AlphaTest
) {
continue;
}
if (!hasAlpha) continue;
material.setAlphaMode("BLEND").setDoubleSided(true);
}
}
@@ -103,9 +92,9 @@ export async function extractModel(vfs: SteamVFS, path: string) {
.setMimeType("image/webp")
.setImage(
await readRoughnessMetallic(
`Data/3d/${dirname(path)}/${textures.baseRMMap}`
)
)
`Data/3d/${dirname(path)}/${textures.baseRMMap}`,
),
),
);
}
@@ -121,9 +110,9 @@ export async function extractModel(vfs: SteamVFS, path: string) {
`Data/3d/${dirname(path)}/${
textures.baseNormalMap ?? textures.normalmap
}`,
isBase
)
)
isBase,
),
),
);
}
@@ -157,7 +146,7 @@ export async function extractModel(vfs: SteamVFS, path: string) {
const node = document.createNode(hierarchy.name);
const components = times(
hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, "0")]
(index) => hierarchy.components[index.toString().padStart(4, "0")],
);
components.forEach((component) => {
@@ -194,7 +183,7 @@ export async function extractModel(vfs: SteamVFS, path: string) {
}
if (polygonGroup === undefined) {
throw new Error(
`Missing polygon group ${batch["rb.datasource"]}`
`Missing polygon group ${batch["rb.datasource"]}`,
);
}
@@ -255,7 +244,7 @@ export async function extractModel(vfs: SteamVFS, path: string) {
default: {
if (ERROR_ON_UNKNOWN_COMPONENT) {
throw new Error(
`Unhandled component type: ${component["comp.typename"]}`
`Unhandled component type: ${component["comp.typename"]}`,
);
}
}
@@ -2,6 +2,7 @@ import sharp from "sharp";
import { readTexture } from "./readTexture";
export async function readBaseColor(path: string, occlusionPath?: string) {
let hasAlpha = false;
const baseRaw = await readTexture(path);
const occlusionRaw = occlusionPath
? await readTexture(occlusionPath)
@@ -30,10 +31,13 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
c = occlusion![occlusionI] / 255;
}
const alpha = base[i * 4 + 3];
hasAlpha ||= alpha < 255;
combined[i * 4 + 0] = Math.round(base[i * 4 + 0] * c);
combined[i * 4 + 1] = Math.round(base[i * 4 + 1] * c);
combined[i * 4 + 2] = Math.round(base[i * 4 + 2] * c);
combined[i * 4 + 3] = base[i * 4 + 3];
combined[i * 4 + 3] = alpha;
}
const image = await sharp(combined, {
@@ -42,5 +46,5 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
.webp()
.toBuffer();
return image;
return { hasAlpha, image };
}
+3 -2
View File
@@ -32,12 +32,13 @@
"compare-versions": "^6.1.1",
"fuzzysort": "^3.1.0",
"immer": "^10.1.3",
"jimp": "^1.6.0",
"lodash-es": "^4.17.21",
"markdown-to-jsx": "^7.7.13",
"pwa-install-handler": "^2.6.3",
"quicklime": "^1.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-promise-suspense": "^0.3.4",
"three": "^0.180.0",
"three-stdlib": "^2.36.0",
@@ -1,5 +1,5 @@
import { I_HAT, J_HAT } from "@blitzkit/core";
import { useThree } from "@react-three/fiber";
import { invalidate, useThree } from "@react-three/fiber";
import { clamp } from "lodash-es";
import type { QuicklimeEvent } from "quicklime";
import { useEffect, useMemo, type ComponentProps } from "react";
@@ -91,7 +91,7 @@ export function StaticArmorSceneComponent({
color = new Color(
clamp(1 - (7 / 8) * xClamped, 0, 1),
0,
clamp(1 - (1 / 8) * xClamped, 0, 1)
clamp(1 - (1 / 8) * xClamped, 0, 1),
);
opacity = clamp(x + 1 / 2, 0, 1);
break;
@@ -115,7 +115,7 @@ export function StaticArmorSceneComponent({
opacity: 0,
userData: { opacity0: opacity },
}),
[thickness]
[thickness],
);
const outlineMaterial = useMemo(
() =>
@@ -126,7 +126,7 @@ export function StaticArmorSceneComponent({
opacity: 0,
transparent: true,
}),
[thickness]
[thickness],
);
useEffect(() => {
@@ -165,8 +165,11 @@ export function StaticArmorSceneComponent({
}
surfaceMaterial.needsUpdate = true;
invalidate();
}
handleHighlightArmor();
function handleTransitionEvent(event: QuicklimeEvent<number>) {
surfaceMaterial.opacity = surfaceMaterial.userData.opacity0 * event.data;
outlineMaterial.opacity = event.data;
@@ -180,7 +183,7 @@ export function StaticArmorSceneComponent({
const unsubscribes = [
Tankopedia.on(
(state) => state.highlightArmor?.name,
handleHighlightArmor
handleHighlightArmor,
),
() => transitionEvent.off(handleTransitionEvent),
];
@@ -194,14 +197,14 @@ export function StaticArmorSceneComponent({
const { clip } = props;
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
unrotateDavaVector(props.turretOrigin.clone())
unrotateDavaVector(props.turretOrigin.clone()),
);
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
const point = new Vector3();
function handleModelTransform(
event: QuicklimeEvent<ModelTransformEventData>
event: QuicklimeEvent<ModelTransformEventData>,
) {
clip.normal
.set(0, 0, -1)
@@ -266,7 +269,7 @@ export function StaticArmorSceneComponent({
const surfaceNormal = event
.normal!.clone()
.applyQuaternion(
event.object.getWorldQuaternion(new Quaternion())
event.object.getWorldQuaternion(new Quaternion()),
);
const angle = surfaceNormal.angleTo(cameraNormal);
const thicknessAngled =
@@ -38,7 +38,7 @@ export function ConsumablesManager({
timers
? consumable.duration === undefined
? undefined
: consumable.duration * (hasHighEndConsumables ? 1.7 : 1)
: consumable.duration * (hasHighEndConsumables ? 1.33 : 1)
: undefined
}
cooldown={
@@ -1,26 +1,3 @@
<script>
import locales from "@blitzkit/i18n/locales.json";
import { getBestLocale } from "../core/i18n/getBestLocale";
const isAPI = window.location.pathname.startsWith("/api");
const isI18nPath = locales.supported.some(({ locale }) =>
window.location.pathname.startsWith(`/${locale}/`)
);
const bestLocale = getBestLocale();
const currentPath = locales.supported.find(({ locale }) =>
window.location.pathname.startsWith(`/${locale}/`)
);
if (
!isAPI && // not an api, duh!
localStorage.getItem("preferred-locale") !== null && // no preference, so probably google's crawler
bestLocale !== currentPath?.locale // we need to redirect
) {
window.location.pathname = isI18nPath
? window.location.pathname.replace(
`/${currentPath?.locale}`,
bestLocale === undefined ? "" : `/${bestLocale}`
)
: `/${bestLocale}${window.location.pathname}`;
}
</script>
@@ -6,7 +6,8 @@ import {
Popover,
type ButtonProps,
} from "@radix-ui/themes";
import type { RefObject } from "react";
import { Jimp } from "jimp";
import { useCallback, type RefObject } from "react";
import { useLocale } from "../hooks/useLocale";
import { screenshotReadyEvent } from "./Tankopedia/HeroSection/components/TankSandbox/components/SceneProps";
@@ -18,6 +19,35 @@ interface Props extends ButtonProps {
export function ScreenshotButton({ canvas, name, children, ...props }: Props) {
const { strings } = useLocale();
const screenshot = useCallback(async () => {
if (!canvas.current) {
throw new Error("Canvas not found");
}
screenshotReadyEvent.dispatch(true);
const blob = await new Promise<Blob>((resolve, reject) => {
requestAnimationFrame(() => {
canvas.current.toBlob((blob) => {
screenshotReadyEvent.dispatch(false);
if (!blob) {
reject(new Error("Failed to capture screenshot"));
} else {
resolve(blob);
}
});
});
});
const buffer = await blob.arrayBuffer();
const image = await Jimp.fromBuffer(buffer);
image.autocrop();
return image;
}, [canvas]);
return (
<Popover.Root>
<Popover.Trigger>
@@ -37,23 +67,13 @@ export function ScreenshotButton({ canvas, name, children, ...props }: Props) {
<Flex direction="column" gap="2">
<Popover.Close>
<Button
onClick={() => {
screenshotReadyEvent.dispatch(true);
onClick={async () => {
const image = await screenshot();
const anchor = document.createElement("a");
requestAnimationFrame(() => {
if (!canvas.current) return;
const anchor = document.createElement("a");
anchor.setAttribute("download", `${name}.png`);
anchor.setAttribute(
"href",
canvas.current.toDataURL("image/png")
);
anchor.click();
screenshotReadyEvent.dispatch(false);
});
anchor.setAttribute("download", `${name}.png`);
anchor.setAttribute("href", await image.getBase64("image/png"));
anchor.click();
}}
>
<DownloadIcon />
@@ -63,22 +83,16 @@ export function ScreenshotButton({ canvas, name, children, ...props }: Props) {
<Popover.Close>
<Button
variant="outline"
onClick={() => {
screenshotReadyEvent.dispatch(true);
requestAnimationFrame(() => {
if (!canvas.current) return;
canvas.current.toBlob((blob) => {
if (!blob) return;
navigator.clipboard.write([
new ClipboardItem({ "image/png": blob }),
]);
});
screenshotReadyEvent.dispatch(false);
onClick={async () => {
const image = await screenshot();
const buffer = await image.getBuffer("image/png");
const blob = new Blob([buffer as unknown as ArrayBuffer], {
type: "image/png",
});
navigator.clipboard.write([
new ClipboardItem({ "image/png": blob }),
]);
}}
>
<CopyIcon />
@@ -3,12 +3,12 @@ import {
resolveDpm,
unionBoundingBox,
type TankDefinition,
} from '@blitzkit/core';
import { useMemo } from 'react';
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
import { resolveReload } from '../../../core/blitzkit/resolveReload';
import { TankSort } from '../../../stores/tankopediaSort';
import { TankCard } from '../../TankCard';
} from "@blitzkit/core";
import { useMemo } from "react";
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
import { resolveReload } from "../../../core/blitzkit/resolveReload";
import { TankSort } from "../../../stores/tankopediaSort";
import { TankCard } from "../../TankCard";
interface TankSearchCardProps {
tank: TankDefinition;
@@ -20,7 +20,7 @@ const modelDefinitions = await awaitableModelDefinitions;
export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
const by = TankSort.use((state) => state.by);
const discriminator = useMemo(() => {
if (by.startsWith('meta')) return undefined;
if (by.startsWith("meta")) return undefined;
const turret = tank.turrets.at(-1)!;
const gun = turret.guns.at(-1)!;
@@ -34,53 +34,53 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
const gunModelDefinition = turretModelDefinition.guns[gun.id];
switch (by) {
case 'fire.aimTime':
case "fire.aimTime":
return gun.aim_time.toFixed(2);
case 'fire.caliber':
case "fire.caliber":
return shell0.caliber.toFixed(0);
case 'fire.damage':
case "fire.damage":
return shell0.armor_damage.toFixed(0);
case 'fire.moduleDamage':
case "fire.moduleDamage":
return shell0.module_damage.toFixed(0);
case 'fire.dispersionMoving':
case "fire.dispersionMoving":
return tracks.dispersion_move.toFixed(3);
case 'fire.dispersionStill':
case "fire.dispersionStill":
return gun.dispersion_base.toFixed(3);
case 'fire.dpm':
case "fire.dpm":
return resolveDpm(gun, shell0).toFixed(0);
case 'fire.dpmPremium':
return shell1 ? resolveDpm(gun, shell1).toFixed(0) : '--';
case 'fire.reload':
case "fire.dpmPremium":
return shell1 ? resolveDpm(gun, shell1).toFixed(0) : "--";
case "fire.reload":
return resolveReload(gun).toFixed(2);
case 'fire.standardPenetration':
return shell0.penetration.near.toFixed(0);
case 'fire.premiumPenetration':
return shell1 ? shell1.penetration.near.toFixed(0) : '--';
case 'fire.tertiaryPenetration':
return shell1 ? shell2.penetration.near.toFixed(0) : '--';
case 'fire.shellVelocity':
case "fire.standardPenetration":
return shell0?.penetration.near.toFixed(0);
case "fire.premiumPenetration":
return shell1 ? shell1?.penetration.near.toFixed(0) : "--";
case "fire.tertiaryPenetration":
return shell1 ? shell2?.penetration.near.toFixed(0) : "--";
case "fire.shellVelocity":
return shell0.velocity.toFixed(0);
case 'fire.shellCapacity':
case "fire.shellCapacity":
return gun.shell_capacity.toFixed(0);
case 'fire.shellRange':
case "fire.shellRange":
return shell0.range.toFixed(0);
case 'fire.gunDepression':
case "fire.gunDepression":
return (
gunModelDefinition.pitch.max +
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
).toFixed(1);
case 'fire.gunElevation':
case "fire.gunElevation":
return (
-gunModelDefinition.pitch.min -
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
).toFixed(1);
case 'maneuverability.forwardsSpeed':
case "maneuverability.forwardsSpeed":
return tank.speed_forwards.toFixed(0);
case 'maneuverability.backwardsSpeed':
case "maneuverability.backwardsSpeed":
return tank.speed_backwards.toFixed(0);
case 'maneuverability.power':
case "maneuverability.power":
return engine.power.toFixed(0);
case 'maneuverability.powerToWeight':
case "maneuverability.powerToWeight":
return (
(1000 * engine.power) /
(tank.weight +
@@ -89,7 +89,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
turret.weight +
gun.weight)
).toFixed(1);
case 'maneuverability.weight':
case "maneuverability.weight":
return (
(tank.weight +
engine.weight +
@@ -98,19 +98,19 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
gun.weight) /
1000
).toFixed(1);
case 'maneuverability.traverseSpeed':
case "maneuverability.traverseSpeed":
return tracks.traverse_speed.toFixed(1);
case 'survivability.health':
case "survivability.health":
return (tank.health + turret.health).toFixed(0);
case 'survivability.viewRange':
case "survivability.viewRange":
return turret.view_range.toFixed(0);
case 'survivability.camouflageStill':
case "survivability.camouflageStill":
return (tank.camouflage_still * 100).toFixed(0);
case 'survivability.camouflageMoving':
case "survivability.camouflageMoving":
return (tank.camouflage_moving * 100).toFixed(0);
case 'survivability.camouflageShooting':
case "survivability.camouflageShooting":
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
case 'survivability.volume': {
case "survivability.volume": {
const dimensions = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box,
@@ -121,7 +121,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
}
case 'survivability.length': {
case "survivability.length": {
const bounds = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box,
@@ -129,7 +129,7 @@ const consumablesArray = Object.values(consumableDefinitions.consumables)
.filter(
(consumable) =>
!consumable.game_mode_exclusive &&
consumable.name.locales[locales.default]
consumable.name.locales[locales.default],
)
.map((consumable) => consumable.id);
const provisionsArray = Array.from(
@@ -137,7 +137,7 @@ const provisionsArray = Array.from(
.filter(
(provision) =>
!provision.game_mode_exclusive &&
provision.name.locales[locales.default]
provision.name.locales[locales.default],
)
.reduce((uniqueMap, consumable) => {
if (!uniqueMap.has(consumable.name.locales[locales.default])) {
@@ -146,7 +146,7 @@ const provisionsArray = Array.from(
return uniqueMap;
}, new Map<string, number>())
.values()
.values(),
);
const shellTypeIcons: Record<ShellType, string> = {
[ShellType.AP]: "ap",
@@ -171,7 +171,7 @@ const TANK_TYPE_COLORS: Record<TankType, string> = {
};
const GUN_TYPES = Object.keys(
GUN_TYPE_ICONS
GUN_TYPE_ICONS,
) as (keyof typeof GUN_TYPE_ICONS)[];
const MAX_ICONS = 4;
@@ -180,7 +180,12 @@ const TIERS = times(10, (i) => 10 - i);
export function FilterControl() {
return (
<Flex align="center" justify="center" gap="2" wrap="wrap">
<Flex
align="center"
justify={{ initial: "start", md: "center" }}
gap="2"
wrap="wrap"
>
<TiersFilter />
<ClassFilter />
<TypeFilter />
@@ -346,7 +351,7 @@ function NationsFilter() {
mr="-2"
style={{
backgroundImage: `url(${asset(
`flags/scratched/${nation}.webp`
`flags/scratched/${nation}.webp`,
)})`,
backgroundSize: "100%",
backgroundRepeat: "no-repeat",
@@ -437,7 +442,7 @@ function ClassFilter() {
TankFilters.mutate((draft) => {
if (selected) {
draft.classes = draft.classes.filter(
(c) => c !== tankClass
(c) => c !== tankClass,
);
} else {
draft.classes = [...draft.classes, tankClass];
@@ -683,7 +688,7 @@ function IndividualShellFilter({
src={asset(
`icons/shells/${shellTypeIcons[shells[index]]}${
premium ? "_premium" : ""
}.webp`
}.webp`,
)}
/>
)}
@@ -718,7 +723,7 @@ function IndividualShellFilter({
src={asset(
`icons/shells/${shellTypeIcons[shellType]}${
premium ? "_premium" : ""
}.webp`
}.webp`,
)}
style={{ width: "1.25em", height: "1.25em" }}
/>
@@ -917,7 +922,7 @@ function ConsumablesFilter() {
TankFilters.mutate((draft) => {
if (selected) {
draft.consumables = draft.consumables.filter(
(n) => n !== consumable
(n) => n !== consumable,
);
} else {
draft.consumables = [...draft.consumables, consumable];
@@ -1011,7 +1016,7 @@ function ProvisionsFilter() {
TankFilters.mutate((draft) => {
if (selected) {
draft.provisions = draft.provisions.filter(
(n) => n !== provision
(n) => n !== provision,
);
} else {
draft.provisions = [...draft.provisions, provision];
@@ -1140,7 +1145,7 @@ function GameModeAbilitiesFilter() {
TankFilters.mutate((draft) => {
if (selected) {
draft.abilities = draft.abilities.filter(
(n) => n !== consumable
(n) => n !== consumable,
);
} else {
draft.abilities = [...draft.abilities, consumable];
@@ -1189,7 +1194,7 @@ function GameModeAbilitiesFilter() {
TankFilters.mutate((draft) => {
if (selected) {
draft.powers = draft.powers.filter(
(n) => n !== provision
(n) => n !== provision,
);
} else {
draft.powers = [...draft.powers, provision];
@@ -30,9 +30,11 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
<Flex direction="column" mb="6" align="center">
<VisualizerCard
mb="0"
style={{ width: "100%" }}
style={{ width: "100%", touchAction: "none" }}
ref={container}
onPointerMove={(event) => {
event.preventDefault();
if (!container.current || explodes) return;
const rect = container.current.getBoundingClientRect();
@@ -174,7 +176,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
? "-"
: literals(strings.common.units.deg, {
value: radToDeg(
Math.min(normalization, Math.abs(angle))
Math.min(normalization, Math.abs(angle)),
).toFixed(0),
})
}
@@ -55,17 +55,17 @@ export function Controls({
const protagonistHullOrigin = new Vector3(
protagonistTrackModelDefinition.origin.x,
protagonistTrackModelDefinition.origin.y,
-protagonistTrackModelDefinition.origin.z
-protagonistTrackModelDefinition.origin.z,
);
const protagonistTurretOrigin = new Vector3(
protagonistModelDefinition.turret_origin.x,
protagonistModelDefinition.turret_origin.y,
-protagonistModelDefinition.turret_origin.z
-protagonistModelDefinition.turret_origin.z,
);
const protagonistGunOrigin = new Vector3(
protagonistTurretModelDefinition.gun_origin.x,
protagonistTurretModelDefinition.gun_origin.y,
-protagonistTurretModelDefinition.gun_origin.z
-protagonistTurretModelDefinition.gun_origin.z,
);
const antagonistGunHeight =
protagonistTrackModelDefinition.origin.y +
@@ -106,12 +106,12 @@ export function Controls({
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
Duel.state.protagonist.equipmentMatrix,
);
switch (event) {
@@ -122,7 +122,7 @@ export function Controls({
protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
camera.position
@@ -134,14 +134,14 @@ export function Controls({
new Vector3(
0,
poseDistances[event] * Math.sin(pitch),
poseDistances[event] * -Math.cos(pitch)
)
poseDistances[event] * -Math.cos(pitch),
),
);
camera.lookAt(
protagonistHullOrigin
.clone()
.add(protagonistTurretOrigin)
.add(protagonistGunOrigin)
.add(protagonistGunOrigin),
);
orbitControls.current?.target.set(0, antagonistGunHeight, 0);
@@ -155,7 +155,7 @@ export function Controls({
protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
camera.position
@@ -167,8 +167,8 @@ export function Controls({
new Vector3(
0,
poseDistances[event] * Math.sin(pitch),
poseDistances[event] * -Math.cos(pitch)
)
poseDistances[event] * -Math.cos(pitch),
),
);
orbitControls.current?.target
.set(0, 0, 0)
@@ -179,8 +179,8 @@ export function Controls({
new Vector3(
0,
0.5 * poseDistances[event] * Math.sin(pitch),
0.5 * poseDistances[event] * -Math.cos(pitch)
)
0.5 * poseDistances[event] * -Math.cos(pitch),
),
);
break;
@@ -251,7 +251,7 @@ export function Controls({
return (
<OrbitControls
enableRotate={enableRotate}
maxDistance={20}
maxDistance={40}
minDistance={5}
enableZoom={zoomable}
zoomSpeed={zoomable ? undefined : 0}
@@ -47,7 +47,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
const display = Tankopedia.use((state) => state.display);
const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor
(state) => state.hideTankModelUnderArmor,
);
useImperativeHandle(ref, () => canvas.current!, []);
@@ -76,7 +76,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
@@ -90,7 +90,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
0,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer
hasImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
@@ -104,7 +104,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
0,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer
hasImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
@@ -126,7 +126,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
modelTransformEvent.last!.yaw,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer
hasImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
@@ -146,8 +146,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
}
}, [display]);
console.log(TankopediaDisplay[display]);
return (
<SmartCanvas
resize={{ debounce: 0 }}
@@ -196,5 +194,5 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
<InitialAligner />
</SmartCanvas>
);
}
},
);
@@ -2,9 +2,9 @@ import {
normalizeAngleRad,
type PitchLimits,
type YawLimits,
} from '@blitzkit/core';
import { clamp } from 'lodash-es';
import { degToRad } from 'three/src/math/MathUtils.js';
} from "@blitzkit/core";
import { clamp } from "lodash-es";
import { degToRad } from "three/src/math/MathUtils.js";
export const DEFAULT_PITCH_TRANSITION = 15;
@@ -87,5 +87,5 @@ export function applyPitchYawLimits(
newPitch = clamp(newPitch, lowerPitch, upperPitch);
return [newPitch, newYaw] as const;
return [newPitch, normalizeAngleRad(newYaw)] as const;
}
@@ -1,5 +1,5 @@
import type { ComponentProps, ReactNode } from "react";
import { Mesh, Object3D } from "three";
import { Mesh, MeshStandardMaterial, Object3D } from "three";
export function jsxTree(
node: Object3D,
@@ -7,14 +7,14 @@ export function jsxTree(
mesh?: (
mesh: Mesh,
props: ComponentProps<"mesh">,
key: string
key: string,
) => ReactNode;
group?: (
object3d: Object3D,
props: ComponentProps<"group">,
key: string
key: string,
) => ReactNode;
}
},
): ReactNode {
if (node instanceof Mesh) {
const props = {
@@ -25,6 +25,10 @@ export function jsxTree(
scale: node.scale,
};
if (props.material instanceof MeshStandardMaterial) {
props.material.depthWrite = true;
}
return mergers?.mesh ? (
mergers.mesh(node, props, node.uuid)
) : (
@@ -5,11 +5,11 @@ export function getBestLocale() {
localStorage.getItem("preferred-locale") ??
navigator.language.split("-")[0];
const isSupported = locales.supported.some(
({ locale }) => locale === desiredLocale
({ locale }) => locale === desiredLocale,
);
const isDefault = desiredLocale === locales.default;
if (!isSupported || isDefault) return undefined;
if (!isSupported || isDefault) return locales.default;
return desiredLocale;
}
@@ -12,7 +12,8 @@ export const getStaticPaths = (async () => {
.map((tank) =>
parentPaths.map((parent) => ({
params: { ...parent.params, id: tank.id },
}))
props: parent.props,
})),
)
.flat();
}) satisfies GetStaticPaths;