Compare commits

..
29 Commits
Author SHA1 Message Date
TrèsAbhi 3daa56e100 New translations en.json (Polish)
[ci skip]
2026-10-23 09:57:28 -05:00
TrèsAbhi 37c257c8c4 New translations en.json (Russian)
[ci skip]
2026-10-22 17:04:11 -05:00
TrèsAbhi dfc6a2bada New translations en.json (Portuguese)
[ci skip]
2026-10-22 17:04:08 -05:00
TrèsAbhi 9b8b06ca70 New translations en.json (Polish)
[ci skip]
2026-10-22 17:04:06 -05:00
TrèsAbhi f7ffb2d34d New translations en.json (Japanese)
[ci skip]
2026-10-22 17:04:04 -05:00
TrèsAbhi 2b7e88e30c New translations en.json (Spanish)
[ci skip]
2026-10-22 17:04:00 -05:00
TrèsAbhi af172c95f2 New translations en.json (French)
[ci skip]
2026-10-22 17:03:59 -05:00
TrèsAbhi f8794de782 New translations en.json (Chinese Simplified)
[ci skip]
2026-10-22 17:03:57 -05:00
TrèsAbhi 5fe295b209 New translations en.json (Ukrainian)
[ci skip]
2026-10-22 17:03:55 -05:00
TrèsAbhi 3b75d7b523 New translations en.json (Russian)
[ci skip]
2026-10-22 15:56:13 -05:00
TrèsAbhi 0cbd67ff38 New translations en.json (Portuguese)
[ci skip]
2026-10-22 15:56:11 -05:00
TrèsAbhi 068f481b93 New translations en.json (Polish)
[ci skip]
2026-10-22 15:56:10 -05:00
TrèsAbhi 1430c32440 New translations en.json (Japanese)
[ci skip]
2026-10-22 15:56:08 -05:00
TrèsAbhi d14afc8b39 New translations en.json (Spanish)
[ci skip]
2026-10-22 15:56:07 -05:00
TrèsAbhi cba591b368 New translations en.json (French)
[ci skip]
2026-10-22 15:56:05 -05:00
TrèsAbhi 42b5ab6f02 New translations en.json (Chinese Simplified)
[ci skip]
2026-10-22 15:56:04 -05:00
TrèsAbhi c1363be70a New translations en.json (Ukrainian)
[ci skip]
2026-10-22 15:56:02 -05:00
TrèsAbhi 602bf37411 New translations en.json (Russian)
[ci skip]
2026-10-22 05:51:52 -05:00
TrèsAbhi 10b0e29a26 New translations en.json (Polish)
[ci skip]
2026-10-21 10:03:16 -05:00
TrèsAbhi ed36eedbfb New translations en.json (Polish)
[ci skip]
2026-10-21 07:42:08 -05:00
TrèsAbhi 05648ea040 Merge pull request #353 from Hitjack007/remove-imgur-references
Fix: migrate Imgur images to BlitzKit hosting
2026-10-18 14:12:50 -05:00
TrèsAbhi 73080fd60a Merge branch 'main' into remove-imgur-references 2026-10-18 14:03:19 -05:00
TrèsAbhi 14be326c41 Merge pull request #368 from Hitjack007/calculated-bounding-boxes 2026-10-18 14:02:34 -05:00
TrèsAbhi 8132f2d91e Merge branch 'main' into calculated-bounding-boxes 2026-10-18 13:59:49 -05:00
TrèsAbhi 1258cd1fa2 New Crowdin updates (#399) 2026-10-18 13:59:07 -05:00
tresabhi 7f7d0cc499 remove outdated docs 2026-10-10 16:41:57 -05:00
TrèsAbhi 0c91ee3600 New Crowdin updates (#389)
* New translations en.json (Ukrainian)

[ci skip]

* New translations en.json (Chinese Simplified)

[ci skip]

* New translations en.json (French)

[ci skip]

* New translations en.json (Spanish)

[ci skip]

* New translations en.json (Japanese)

[ci skip]

* New translations en.json (Polish)

[ci skip]

* New translations en.json (Portuguese)

[ci skip]

* New translations en.json (Russian)

[ci skip]
2026-10-09 23:33:06 -05:00
Mark Greene a365fa5e50 Fix: migrate Imgur images to BlitzKit hosting 2026-10-08 20:28:02 +13:00
Mark Greene 57d77bf2b4 Fix: calculate tank dimensions from collision models 2026-10-08 20:25:03 +13:00
32 changed files with 533 additions and 854 deletions

No files matched your search

-9
View File
@@ -1,9 +0,0 @@
# Timezones
What is considered a "day's worth of games" is determined by the reset time. The reset time is generally at midnight everyday after which newer games are counted towards the next day. Reset times vary from region to region:
- North America: `UTC-5:00` (Central North American Time midnight)
- Europe: `UTC+1:00` (Central European Time midnight)
- Asia: `UTC+7:00` (Central Asia Standard Time midnight)
**Reset times are different from Blitz Stars** (Blitz Stars resets at 5:00 am locally relative to the user opening their website). Inconsistencies with Blitz Stars only occur with sleep-deprived players and players who play in the wrong region.
-45
View File
@@ -1,45 +0,0 @@
# Blitzkit Rating Leaderboard
A straightforward way of storing the entire World of Tanks Blitz rating leaderboard to disk.
```cpp
primary BlitzkitRatingLeaderboard {
utf8"BKRL" magic;
Header header;
primary Body<header> body;
}
Header {
uint16 version; // always 1
Format format;
uint32 count;
}
Format enum<uint8> {
Base,
Superset1,
}
Body<Header header> match<header.format> {
Base {
primary {
uint32 id;
uint16 score;
}[header.count] entries;
};
Superset1 {
primary {
uint32 id;
uint16 score;
uint32 battles;
uint32 wins;
uint32 survived;
uint32 damage;
uint32 kills;
}[header.count] entries;
};
}
```
-18
View File
@@ -1,18 +0,0 @@
# Discovered Ids
```c
primary DiscoveredIds {
utf8"DIDS" magic;
Header header;
primary Body body;
}
Header {
uint16 version; // always 1
}
Body {
uint32 count;
uint32[count] ids;
}
```
+1 -170
View File
@@ -1,172 +1,3 @@
# KitScore
## Memo
KitScore is a performance metric that makes sense. This metric can be applied to any game, virtual or real. Specific pieces of information relating directly to World of Tanks Blitz will be provided within parentheses and with a "WoTB:" prefix.
## Consumption of Observations
KitScore is driven solely by the behavior of other players and finding meaning of a new observation within a pool of condensed data based on previously observed data. Generally, we will be provided with a number of statistics (the $x_i$ of an observation) per player (WoTB: $x_\text{frags}$, $x_\text{xp}$, $x_\text{spots}$, etc.) including a target statistic (the $x_j$ of an observation) that all other statistics try to maximize (WoTB: "winrate" which we will defined later as $x_j$).
If the statistics are cumulative, they will have to be normalized (WoTB: statistics fetched from Wargaming are indeed cumulative even for a single player; $x_\text{frags}=\frac{\Sigma x_\text{frags}}{\Sigma x_\text{battles}}$, $x_\text{spots}=\frac{\Sigma x_\text{spots}}{\Sigma x_\text{battles}}$, $x_j=\frac{\Sigma x_\text{wins}}{\Sigma x_\text{battles}}$, etc.). Each player will have such average statistics, giving us many points of data to work with. If the statistics are not cumulative, then you will be dealing with multiple observations per player which will only help improve accuracy.
## It's Impossible to "Find What Makes the Metric Tick"
WN8, a widely adopted metric in the World of Tanks and World of Tanks Blitz communities is plagued with problems. One of them being that people often find and exploit "what makes the metric tick." In other words, people easily exploit the rudimentary nature of WN8 to score undeservedly high.
In WN8, the gains of some rewarding categories overshadow the losses of others though that may not necessarily be in the best interest in maximizing winrate. KitScore makes this nearly impossible as it punishes a player for scoring well solely high in a certain category. You will never exploit KitScore. Period.
## Generating Reasonable Values
TODO: there's like no runaway cases
## Symbol Interpretation
| Symbol | Interpretation |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| $A_\text{human}$ | The human readable version of the raw score $A$. |
| $A$ | The raw KitScore. |
| $C$ | An arbitrary coefficient. |
| $I$ | The number of statistics including the target statistic. This means that there are $I-1$ entries for $x_i$ and $1$ entry for $y$ for each observation. |
| $i$ | The statistic index of where there are $I$ entries. |
| $j$ | A discriminator for the target statistic. There may never be more than $1$ entries for $j$. |
| $m$ | The degree of the polynomial regression. |
| $n$ | The number of observations. |
| $a$ | An "atomic" score which is a function of the $i$th statistic. |
| $w$ | The weight of the $i$th atomic statistic. |
## Computing the Score
The final metric $A_\text{human}$ is an arbitrarily scaled value of the raw score $A$ where $C$ is any coefficient of your liking. BlitzKit uses $C=10^3$.
$$
A_\text{human}=CA
$$
The final metric $A$ is the weighted average of individual scores $a$ which are functions of the $i$th statistic excluding the target statistic $j$ itself.
$$
A=\frac{\sum_{i \neq j} w_{i}a_{i}}{\sum_{i \neq j} w_{i}}
$$
The "atomic" score $a$ is the difference between the predicted target statistic $P_{i \leftrightarrow j}(x_i)$ acquired using a polynomial regression of degree $m$ and the actual newly observed $x_j$.
$$
a_i=x_j-P_{i \leftrightarrow j}(x_i)
$$
The polynomial regression of $m$th degree can be written as follows:
$$
P(x)=\beta_0x^0+\beta_1x^1+\beta_2x^2+\dots+\beta_mx^m
$$
Polynomial regression coefficients $\beta$ can be acquired through basic linear algebra where an $m \times m$ of constants is multiplied with $m \times 1$ of unknowns which equals another $m \times 1$ of constants.
```math
\begin{bmatrix}
\sum_{k=1}^n x_i^0 & \sum_{k=1}^n x_i^1 & \sum_{k=1}^n x_i^2 & \dots & \sum_{k=1}^n x_i^m \\
\sum_{k=1}^n x_i^1 & \sum_{k=1}^n x_i^2 & \sum_{k=1}^n x_i^3 & \dots & \sum_{k=1}^n x_i^{m+1} \\
\sum_{k=1}^n x_i^2 & \sum_{k=1}^n x_i^3 & \sum_{k=1}^n x_i^4 & \dots & \sum_{k=1}^n x_i^{m+2} \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
\sum_{k=1}^n x_i^m & \sum_{k=1}^n x_i^{m+1} & \sum_{k=1}^n x_i^{m+1} & \dots & \sum_{k=1}^n x_i^{2m} \\
\end{bmatrix}
\begin{bmatrix}
\beta_0 \\
\beta_1 \\
\beta_2 \\
\vdots \\
\beta_m
\end{bmatrix}
=
\begin{bmatrix}
\sum_{k=1}^n x_jx_i^0 \\
\sum_{k=1}^n x_jx_i^1 \\
\sum_{k=1}^n x_jx_i^2 \\
\vdots \\
\sum_{k=1}^n x_jx_i^m
\end{bmatrix}
```
The weight $w$ is a slice of the Pearson corelation matrix. The numerator is the corelation between the $i$th statistic and the target statistic $j$ while the denominator the sum of the corelations between the $i$th statistic and all statistics but the target statistic $j$.
$$
w_{i}=\frac{r_{i\leftrightarrow j}^2}{\sum_{i_{k}\neq j}r_{i\leftrightarrow i_{k}}^2}
$$
## Understanding the Corelation Matrix
Forming the corelation matrix will create an $I \times I$ matrix where. This matrix is best represented as a table. The example below illustrates a matrix/table for $I=4$ which includes $3$ entries for $i$ statistics and $1$ entry for the $j$ target statistic.
| | $x_1$ | $x_2$ | $x_3$ | $x_j$ |
| ----- | --------------------------- | --------------------------- | --------------------------- | --------------------------- |
| $x_1$ | $1$ | $r_{1 \leftrightarrow 2}^2$ | $r_{1 \leftrightarrow 3}^2$ | $r_{1 \leftrightarrow j}^2$ |
| $x_2$ | $r_{1 \leftrightarrow 2}^2$ | $1$ | $r_{2 \leftrightarrow 3}^2$ | $r_{2 \leftrightarrow j}^2$ |
| $x_3$ | $r_{1 \leftrightarrow 3}^2$ | $r_{2 \leftrightarrow 3}^2$ | $1$ | $r_{3 \leftrightarrow j}^2$ |
| $x_j$ | $r_{1 \leftrightarrow j}^2$ | $r_{2 \leftrightarrow j}^2$ | $r_{3 \leftrightarrow j}^2$ | $1$ |
It should not be a surprise to find $r_{i \leftrightarrow i}^2=1$ which forms a neat diagonal across the table. Note that there are many duplicates within the $I^2$ entries. The "bottom-right triangle" can be eliminated to remove redundancies leaving us with only $\frac{I(I+1)}{2}$ fields occupied.
| | $x_1$ | $x_2$ | $x_3$ | $x_j$ |
| ----- | ----- | --------------------------- | --------------------------- | --------------------------- |
| $x_1$ | $1$ | $r_{1 \leftrightarrow 2}^2$ | $r_{1 \leftrightarrow 3}^2$ | $r_{1 \leftrightarrow j}^2$ |
| $x_2$ | | $1$ | $r_{2 \leftrightarrow 3}^2$ | $r_{2 \leftrightarrow j}^2$ |
| $x_3$ | | | $1$ | $r_{3 \leftrightarrow j}^2$ |
| $x_j$ | | | | $1$ |
When programming this matrix into an array, $r_{i \leftrightarrow i}^2=1$ will be redundant. I recommend condensing the matrix into an array of size $\frac{I(I+1)}{2}+I=\frac{I(I-1)}{2}$ to avoid all redundant entries. For $I=4$, the array will look something like the one illustrated below (here, $j=4$).
$$
[r_{1 \leftrightarrow 2}^2, r_{1 \leftrightarrow 3}^2, r_{1 \leftrightarrow 4}^2, r_{2 \leftrightarrow 3}^2, r_{2 \leftrightarrow 4}^2, r_{3 \leftrightarrow 4}^2]
$$
## Understanding the Weights
Some may find the weight equation arbitrary; it evidently isn't. The goal of $w$ is to eliminate over-rewarding the same behavior represented in multiple statistics. It makes all atomic statistics concurrent by leveraging the Pearson corelation matrix.
A simplified example will clarify the idea. First, we compute a Pearson corelation matrix for $I=3$ statistics. We have $x_1$ and $x_2$, the statistics that are trying to maximize $x_j$. Reminder: a Pearson corelation matrix finds the $r$ value not only between $x_i$ and $x_j$ but also between all combinations of $x_i$ and themselves. Here, $r^2$ is calculated using polynomial regression corelation analysis to the $m$th degree.
| | $x_1$ | $x_2$ | $x_3$ | $x_j$ |
| ----- | ----- | ----------- | ----- | ----- |
| $x_1$ | $1$ | $\boxed{0}$ | $0$ | $1$ |
| $x_2$ | | $1$ | $0$ | $1$ |
| $x_3$ | | | $1$ | $1$ |
| $x_j$ | | | | $1$ |
In the case above, $r^2_{1 \leftrightarrow 2}=0$ meaning that both statistics have absolutely nothing to do with each other. Hence, the player will be awarded for optimizing both individually. And since $r^2_{1 \leftrightarrow j}=r^2_{2 \leftrightarrow j}$, they should both be rewarded equally; in other words, they should have equal weights.
$$
w_{1}=\frac{1}{1 + 0 + 0}=1
$$
$$
w_{2}=\frac{1}{0 + 1 + 0}=1
$$
$$
w_{3}=\frac{1}{0 + 0 + 1}=1
$$
Now consider a case where $x_1$ and $x_2$ are fully correlated. It would be a bad idea to reward them equally; hence, their weights suffer.
| | $x_1$ | $x_2$ | $x_3$ | $x_j$ |
| ----- | ----- | ----------- | ----- | ----- |
| $x_1$ | $1$ | $\boxed{1}$ | $0$ | $1$ |
| $x_2$ | | $1$ | $0$ | $1$ |
| $x_3$ | | | $1$ | $1$ |
| $x_j$ | | | | $1$ |
$$
w_{1}=\frac{1}{1 + 1 + 0}=0.5
$$
$$
w_{2}=\frac{1}{1 + 1 + 0}=0.5
$$
$$
w_{3}=\frac{1}{0 + 0 + 1}=1
$$
Remind me to write out the linearized model!
-71
View File
@@ -1,71 +0,0 @@
# BlitzKit Regular Tank Stats
A straightforward way of storing the entire World of Tanks Blitz tank statistics for players to disk.
## Manifest `manifest.rtsm`
```cpp
primary RegularTankStatisticsManifest {
utf8"RTSM" magic;
Header header;
primary Body body;
}
Header {
uint16 version; // always 1
}
Body {
uint32 time;
uint8 base;
}
```
## Chunk `*.rtsc`
```cpp
primary RegularTankStatisticsChunk {
utf8"RTSC" magic;
Header header;
primary Body body<header.playerCount>;
}
Header {
uint16 version; // always 1
uint32 playerCount;
}
Body<uint32 playerCount> {
Player[playerCount] players;
}
Player {
uint32 id;
uint16 tankCount;
Tank[tankCount] tanks;
}
Tank {
uint32 id;
uint32 battleLifeTime;
uint32 lastBattleTime;
uint8 markOfMastery;
uint32 battles;
uint32 capturePoints;
uint32 damageDealt;
uint32 damageReceived;
uint32 droppedCapturePoints;
uint32 frags;
uint32 frags8p;
uint32 hits;
uint32 losses;
uint8 maxFrags;
uint16 maxXp;
uint32 shots;
uint32 spotted;
uint32 survivedBattles;
uint32 winAndSurvived;
uint32 wins;
uint32 xp;
}
```
@@ -4,7 +4,13 @@ import {
ChassisDefinitionsList,
Crew,
GunDefinitionsList,
I_HAT,
J_HAT,
K_HAT,
ModelDefinitions,
parseHierarchies,
Sc2ReadStream,
ScgReadStream,
TankParameters,
toUniqueId,
TurretDefinitionsList,
@@ -12,9 +18,10 @@ import {
VehicleDefinitionArmor,
VehicleDefinitionList,
VehicleDefinitions,
VertexAttribute,
} from "@blitzkit/core";
import { parse as parsePath } from "path";
import { Vector3Tuple } from "three";
import { MathUtils, Vector3 as THREEVector3, Vector3Tuple } from "three";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI2 } from "./2_camouflageDefinitions";
@@ -23,6 +30,87 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3;
}
private async collisionBoundingBoxes(
nation: string,
tankKey: string,
turretRotation?: { yaw: number; pitch: number; roll: number },
) {
const boundingBoxes: Record<
string,
{ min: Vector3Tuple; max: Vector3Tuple }
> = {};
const path = `Data/3d/Tanks/CollisionMeshes/${nation}-${tankKey}`;
if (!(await this.vfs.resolve(`${path}.sc2`))) {
throw new Error(`Missing collision mesh for ${nation}-${tankKey}`);
}
const sc2 = new Sc2ReadStream(
(await this.vfs.file(`${path}.sc2`)).buffer as ArrayBuffer,
).sc2();
const scg = new ScgReadStream(
(await this.vfs.file(`${path}.scg`)).buffer as ArrayBuffer,
).scg();
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
for (const component of components) {
if (component["comp.typename"] !== "RenderComponent") continue;
const polygonGroup = scg.get(
component["rc.renderObj"]["ro.batches"]["0000"]["rb.datasource"],
);
if (!polygonGroup) continue;
const min: Vector3Tuple = [Infinity, Infinity, Infinity];
const max: Vector3Tuple = [-Infinity, -Infinity, -Infinity];
const rotate =
turretRotation && hierarchy.name.toLowerCase().startsWith("turret");
polygonGroup.vertices.forEach((vertex) => {
vertex.forEach(({ attribute, value }) => {
if (attribute !== VertexAttribute.VERTEX) return;
const position = new THREEVector3(...value);
if (rotate) {
position
.applyAxisAngle(
I_HAT,
-MathUtils.degToRad(turretRotation.pitch),
)
.applyAxisAngle(
J_HAT,
-MathUtils.degToRad(turretRotation.roll),
)
.applyAxisAngle(
K_HAT,
-MathUtils.degToRad(turretRotation.yaw),
);
}
const coordinates = position.toArray();
for (let axis = 0; axis < 3; axis++) {
min[axis] = Math.min(min[axis], coordinates[axis]);
max[axis] = Math.max(max[axis], coordinates[axis]);
}
});
});
boundingBoxes[hierarchy.name.toLowerCase()] = { min, max };
}
return null;
});
if (!boundingBoxes.hull) {
throw new Error(`Missing hull collision mesh for ${nation}-${tankKey}`);
}
return boundingBoxes;
}
private assignArmor(
raw: VehicleDefinitionArmor[string],
id: number,
@@ -67,6 +155,12 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
const tankParameters = await this.vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
);
const collisionBoundingBoxes = await this.collisionBoundingBoxes(
nation,
tankKey,
tankDefinition.root.hull.turretInitialRotation,
);
const hullBoundingBox = collisionBoundingBoxes.hull;
const turretOrigin = tankDefinition.root.hull.turretPositions.turret
.split(" ")
.map(Number) as Vector3Tuple;
@@ -141,12 +235,8 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
}
: undefined,
bounding_box: {
min: this.vector3TupleToBlitzkit(
tankParameters.collision.hull.bbox.min,
),
max: this.vector3TupleToBlitzkit(
tankParameters.collision.hull.bbox.max,
),
min: this.vector3TupleToBlitzkit(hullBoundingBox.min),
max: this.vector3TupleToBlitzkit(hullBoundingBox.max),
},
turrets: {},
tracks: {},
@@ -208,26 +298,35 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
turret.userString;
const turretCollisionName = parsePath(
turret.hitTester.collisionModel,
).name.toLowerCase();
const isFullYaw = -turretYaw[0] + turretYaw[1] === 360;
if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) {
throw new Error(
`Missing turret collision mesh for ${nation}-${tankKey} ${turretKey}`,
);
}
const turretBoundingBox = collisionBoundingBoxes[
turretCollisionName
] ?? {
min: [0, 0, 0] as Vector3Tuple,
max: [0, 0, 0] as Vector3Tuple,
};
modelDefinitions.models[tankId].turrets[turretId] = {
armor: turretArmor,
gun_origin: this.vector3TupleToBlitzkit(gunOrigin),
model_id: turretModel,
yaw:
-turretYaw[0] + turretYaw[1] === 360
? undefined
: { min: turretYaw[0], max: turretYaw[1] },
yaw: isFullYaw
? undefined
: { min: turretYaw[0], max: turretYaw[1] },
guns: {},
bounding_box: {
min: this.vector3TupleToBlitzkit(
tankParameters.collision[
parsePath(turret.hitTester.collisionModel).name.toLowerCase()
].bbox.min,
),
max: this.vector3TupleToBlitzkit(
tankParameters.collision[
parsePath(turret.hitTester.collisionModel).name.toLowerCase()
].bbox.max,
),
min: this.vector3TupleToBlitzkit(turretBoundingBox.min),
max: this.vector3TupleToBlitzkit(turretBoundingBox.max),
},
};
+1
View File
@@ -25,6 +25,7 @@ export * from "./sluggify";
export * from "./staticAsset";
export * from "./sum";
export * from "./tankDefinitions";
export * from "./tankBoundingBox";
export * from "./tankIcon";
export * from "./tankNames";
export * from "./unionBoundingBox";
@@ -0,0 +1,32 @@
import { ModelDefinition, TurretModelDefinition } from "../protos";
import { unionBoundingBox } from "./unionBoundingBox";
export function tankBoundingBox(
tankModelDefinition: ModelDefinition,
turretModelDefinition: TurretModelDefinition,
) {
const hull = tankModelDefinition.bounding_box!;
const turret = turretModelDefinition.bounding_box!;
const origin = tankModelDefinition.turret_origin!;
if (
turret.min!.x === turret.max!.x &&
turret.min!.y === turret.max!.y &&
turret.min!.z === turret.max!.z
) {
return unionBoundingBox(hull, hull);
}
return unionBoundingBox(hull, {
min: {
x: turret.min!.x + origin.x,
y: turret.min!.y + origin.z,
z: turret.min!.z + origin.y,
},
max: {
x: turret.max!.x + origin.x,
y: turret.max!.y + origin.z,
z: turret.max!.z + origin.y,
},
});
}
@@ -1,5 +1,5 @@
import { times } from "lodash-es";
import { CrewType, ShellType, TankClass, TankType } from "../../protos";
import { CrewType, ShellType } from "../../protos";
export const SHELL_NAMES: Record<ShellType, string> = {
[ShellType.SHELL_TYPE_AP]: "AP",
@@ -15,33 +15,6 @@ export const CREW_MEMBER_NAMES = {
[CrewType.CREW_TYPE_RADIOMAN]: "radioman",
} as const;
/**
* @deprecated use svg
*/
export const TANK_ICONS: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/BIHSEH0.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/CSNha5V.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/wvf3ltm.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/ECeqlZa.png",
};
/**
* @deprecated use svg
*/
export const TANK_ICONS_PREMIUM: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/TCu3EdR.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/zdkpTRb.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/3z7eHX6.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/P3vbmyA.png",
};
/**
* @deprecated use svg
*/
export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/WTjeirB.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/EwhtKkU.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/u8YDMBh.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/8xRf3nc.png",
};
export const TIERS = times(10, (index) => index + 1);
export const TIER_ROMAN_NUMERALS: Record<number, string> = {
1: "I",
@@ -68,8 +41,3 @@ export const flags: Record<string, string> = {
european: "🇪🇺",
other: "<:other:1218421572243558482>",
};
export const TREE_TYPE_ICONS: Record<TankType, Record<TankClass, string>> = {
[TankType.TANK_TYPE_RESEARCHABLE]: TANK_ICONS,
[TankType.TANK_TYPE_PREMIUM]: TANK_ICONS_PREMIUM,
[TankType.TANK_TYPE_COLLECTOR]: TANK_ICONS_COLLECTOR,
};
-50
View File
@@ -1,50 +0,0 @@
export enum ImgurSize {
/**
* The default size
*/
Default = '',
/**
* Unknown size but really really small
*/
UltraSmall = 'ss',
/**
* 90x90
*/
SmallSquare = 's',
/**
* 160x160
*/
BigSquare = 'b',
/**
* 160x160
*/
SmallThumbnail = 't',
/**
* 320x320
*/
MediumThumbnail = 'm',
/**
* 640x640
*/
LargeThumbnail = 'l',
/**
* 1024x1024
*/
HugeThumbnail = 'h',
}
interface ImgurOptions {
format: 'jpeg' | 'png';
size: ImgurSize;
}
const defaultImgurOptions: ImgurOptions = {
format: 'png',
size: ImgurSize.Default,
};
export function imgur(id: string, imgurOptions?: Partial<ImgurOptions>) {
const options = { ...defaultImgurOptions, ...imgurOptions };
return `https://i.imgur.com/${id}${options.size}.${options.format}`;
}
-1
View File
@@ -1 +0,0 @@
export * from './imgur';
-1
View File
@@ -1,6 +1,5 @@
export * from "./blitz";
export * from "./blitzkit";
export * from "./imgur";
export * from "./math";
export * from "./patreon";
export * from "./protobuf";
+22
View File
@@ -90,6 +90,28 @@ export interface Hierarchy {
name: string;
}
export function parseHierarchies<Parent>(
hierarchies: Hierarchy[],
parent: Parent,
parseHierarchy: (
hierarchy: Hierarchy,
components: Component[],
parent: Parent,
) => Parent | undefined,
) {
for (const hierarchy of hierarchies) {
const components = Array.from(
{ length: hierarchy.components.count },
(_, index) => hierarchy.components[index.toString().padStart(4, "0")],
);
const child = parseHierarchy(hierarchy, components, parent);
if (child !== undefined && hierarchy["#hierarchy"]) {
parseHierarchies(hierarchy["#hierarchy"], child, parseHierarchy);
}
}
}
// 0001, 0002, 0003...: Component
type Components = {
count: number;
+1
View File
@@ -623,6 +623,7 @@
"hullTraverseSoftTerrain": "Suave",
"health": "Salud",
"fireChance": "Probabilidad de incendio",
"trackRepairTime": "Track repair time",
"viewRange": "Rango de visión",
"camouflageStill": "Quieto",
"camouflageMoving": "En movimiento",
+1
View File
@@ -623,6 +623,7 @@
"hullTraverseSoftTerrain": "Soft",
"health": "Health",
"fireChance": "Fire chance",
"trackRepairTime": "Track repair time",
"viewRange": "View range",
"camouflageStill": "Still",
"camouflageMoving": "Moving",
+1
View File
@@ -623,6 +623,7 @@
"hullTraverseSoftTerrain": "軟弱地",
"health": "耐久値",
"fireChance": "火災発生率",
"trackRepairTime": "Track repair time",
"viewRange": "視認範囲",
"camouflageStill": "静止時",
"camouflageMoving": "移動時",
+232 -231
View File
@@ -1,7 +1,7 @@
{
"test": {
"default": "test",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": "To jest długi ciąg testowy. NIGDY NIE ZOBACZYSZ tego w interfejsie użytkownika. Mój ulubiony owoc to {fruit}, ponieważ ma kolor {color}. Tak bardzo go lubię, że powiem jego nazwę 3 razy! {fruit}! {fruit}! {fruit}! Po raz kolejny to {color}, a nie {not_color}.",
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
},
"common": {
@@ -19,7 +19,7 @@
"gun_types": {
"auto_loader": "Samo ładowacz",
"auto_reloader": "Automatyczne przeładowanie",
"regular": "Regular"
"regular": "Regularny"
},
"shells": {
"ap": "AP",
@@ -38,10 +38,10 @@
},
"branches": {
"beta": "Beta",
"development": "Development",
"opentest": "OpenTest",
"preview": "Preview",
"production": "Production"
"development": "Rozwój",
"opentest": "OtwartyTest",
"preview": "Przegląd",
"production": "Produkcja"
},
"locales": {
"en": "Angielski",
@@ -102,28 +102,28 @@
"other": "Hybryda"
},
"tank_class_short": {
"-1": "unrecognized",
"-1": "nierozpoznany",
"0": "Lekki",
"1": "Średni",
"2": "Ciężki",
"3": "TD"
"3": "NC"
},
"tank_class_short_plural_lowercase": {
"0": "lekkie",
"1": "średnie",
"2": "ciężkie",
"3": "TDs"
"3": "NC-ów"
},
"tank_class_medium": {
"-1": "unrecognized",
"-1": "nierozpoznany",
"0": "Lekki",
"1": "Średni",
"2": "Ciężki",
"3": "Niszczyciel czołgów"
},
"tree_type": {
"-1": "unrecognized",
"0": "Tech tree",
"-1": "nierozpoznany",
"0": "Drzewko technologiczne",
"1": "Premium",
"2": "Kolekcjonerski"
},
@@ -187,223 +187,223 @@
},
"website": {
"redirect": {
"title": "301: moved permanently",
"body": "This page has moved. Redirecting..."
"title": "301: przeniesiony na stałe",
"body": "Ta strona została przeniesiona. Przekierowywanie..."
},
"settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Development mode",
"auto_load_models": "Auto-load models",
"language": "Language"
"title": "Ustawienia",
"description": "Zmień ustawienia BlitzKit",
"dev_mode": "Tryb deweloperski",
"auto_load_models": "Automatycznie wczytaj modele",
"language": "Język"
},
"common": {
"player_search": {
"hint": "Search players...",
"no_results": "No results",
"searching": "Searching..."
"hint": "Szukaj graczy...",
"no_results": "Brak wyników",
"searching": "Wyszukiwanie..."
},
"copy_button": {
"copied": "Copied!"
"copied": "Skopiowano!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
"select_all": "Select all",
"count_singular": "1 tank",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"clear": "Clear",
"tier": "Tier {tier}",
"passive": "{name} (Passive)",
"active": "{name} (Active)",
"select_dialog_title": "Szukaj czołgów",
"swap_dialog_title": "Zamień czołg",
"clear_filters": "Wyczyść filtry",
"login": "Zaloguj się, aby tego użyć",
"search_bar_hint": "Szukaj czołgów...",
"recent": "Ostatnio oglądane",
"select_all": "Zaznacz wszystko",
"count_singular": "1 czołg",
"count_plural": "{count} czołgi",
"sorting_by": "Sortowanie według {name}",
"clear": "Wyczyść",
"tier": "Poziom czołgu {tier}",
"passive": "{name} (Pasywny)",
"active": "{name} (Aktywne)",
"no_tanks_found": {
"body": "No tanks found.",
"body": "Nie znaleziono czołgu.",
"search_again": "",
"clear_filters": "Try clearing filters."
"clear_filters": "Spróbuj wyczyścić filtry."
},
"sort": {
"meta.none": "none",
"survivability.health": "health",
"survivability.viewRange": "view range",
"survivability.camouflageStill": "camouflage still",
"survivability.camouflageMoving": "camouflage moving",
"survivability.camouflageShooting": "camouflage shooting",
"survivability.volume": "volume",
"survivability.length": "length",
"fire.dpm": "standard DPM",
"fire.dpmPremium": "premium DPM",
"fire.reload": "reload",
"fire.caliber": "caliber",
"fire.standardPenetration": "standard penetration",
"fire.premiumPenetration": "premium penetration",
"meta.none": "brak",
"survivability.health": "punkty.życia",
"survivability.viewRange": "zasięg widzenia",
"survivability.camouflageStill": "kamuflaż w bezruchu",
"survivability.camouflageMoving": "kamuflaż w trakcie ruchu",
"survivability.camouflageShooting": "kamuflaż podczas strzału",
"survivability.volume": "wartość",
"survivability.length": "długość",
"fire.dpm": "DPM pocisków podstawowych",
"fire.dpmPremium": "DPM pocisków premium",
"fire.reload": "przeładowanie",
"fire.caliber": "kaliber",
"fire.standardPenetration": "Penetracja pocisków standardowych",
"fire.premiumPenetration": "Penetracja pocisków premium",
"fire.tertiaryPenetration": "tertiary penetration",
"fire.damage": "damage",
"fire.moduleDamage": "module damage",
"fire.shellVelocity": "shell velocity",
"fire.shellRange": "shell range",
"fire.shellCapacity": "shell capacity",
"fire.aimTime": "aim time",
"fire.damage": "uszkodzenia",
"fire.moduleDamage": "Uszkodzenia modułów",
"fire.shellVelocity": "Prędkość pocisku",
"fire.shellRange": "Zasięg pocisku",
"fire.shellCapacity": "Maksymalna ilość pocisków",
"fire.aimTime": "Czas celowania",
"fire.dispersionStill": "dispersion still",
"fire.dispersionMoving": "dispersion moving",
"fire.gunDepression": "gun depression",
"fire.gunElevation": "gun elevation",
"maneuverability.forwardsSpeed": "forwards speed",
"maneuverability.backwardsSpeed": "backwards speed",
"maneuverability.power": "power",
"maneuverability.powerToWeight": "power to weight",
"maneuverability.weight": "weight",
"fire.gunDepression": "Depresja działa",
"fire.gunElevation": "Elewacja działa",
"maneuverability.forwardsSpeed": "Maksymalna prędkość do przodu",
"maneuverability.backwardsSpeed": "Maksymalna prędkość do tyłu",
"maneuverability.power": "Moc silnika",
"maneuverability.powerToWeight": "Proporcja mocy do masy",
"maneuverability.weight": "Masa czołgu",
"maneuverability.traverseSpeed": "traverse speed"
},
"sort_dropdown": {
"label": "Sort",
"label": "Filtruj",
"groups": {
"sort": {
"no_sort": "No sort",
"label": "Sort by",
"fire": "Fire",
"maneuverability": "Maneuverability",
"survivability": "Survivability"
"no_sort": "Brak filtrów",
"label": "Filtruj według",
"fire": "Siła ognia",
"maneuverability": "Mobilność ",
"survivability": "Przetrwanie"
},
"direction": {
"label": "Direction",
"ascending": "Ascending",
"descending": "Descending"
"ascending": "Rosnąco",
"descending": "Malejąco"
}
}
}
},
"warnings": {
"test": "Tanks in testing are subject to change and may not represent the final product."
"test": "Wartości statystyk czołgów w fazie testowej mogą ulec zmianie."
}
},
"home": {
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
"join_short": "Dołącz do elity!",
"join_long": "Dołącz do topowej elity!",
"action": "Discord"
},
"hero": {
"credit": "By {name} ❤️"
"credit": "Według {name} ♥️"
},
"hot_tanks": {
"title": "Popular tanks",
"subtitle": "Last {days} days"
"title": "Popularne czołgi",
"subtitle": "Ostatnie {days} dni"
},
"other_projects": {
"title": "Other cool projects"
"title": "Inne epickie projekty"
},
"help_translate": "Please help translate {name} to {language}!",
"seo_description": "Tools for everything World of Tanks Blitz"
"help_translate": "Proszę pomóż przetłumaczyć {name} na {language}!",
"seo_description": "Narzędzia do wszystkiego World of Tanks Blitz"
},
"docs": {
"patches": {
"name": "BlitzKit Changelogs",
"seo_description": "Changelogs for BlitzKit Discord bot and website"
"name": "Zapisy zmian BlitzKit",
"seo_description": "Zapisy zmian BlitzKit: Discord bot i strona internetowa"
}
},
"tools": {
"playlist": {
"reset": "Reset",
"shuffle": "Shuffle",
"remove": "Remove played",
"tracking": "BlitzKit is tracking your battles",
"reset": "Resetuj",
"shuffle": "Wymieszaj",
"remove": "Usuń czołgi, którymi grałem",
"tracking": "BlitzKit zapisuje twoje bitwy",
"sort": {
"button": "Sort by",
"last_played": "Last played",
"least_played": "Least played"
"button": "Filtruj według",
"last_played": "Ostatnio używane czołgi",
"least_played": "Najmniej używane czołgi"
},
"table": {
"index": "#",
"never": "Never",
"tank": "Tank",
"last_played": "Last played",
"battles": "Career battles",
"never": "Nigdy",
"tank": "Czołg",
"last_played": "Ostatnio używane czołgi",
"battles": "Wszystkie bitwy na koncie",
"unrecorded": "--",
"wins": "Zwycięstwa"
},
"large_warning": {
"title": "That's a lot of tanks!",
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
"cancel": "Cancel",
"proceed": "Proceed"
"title": "To jest mnóstwo czołgów!",
"description": "Czy na pewno chcesz stworzyć playlistę złożoną z {count} czołgów? To {time} godzin gry przy 50% zwycięstw!",
"cancel": "Anuluj",
"proceed": "Zatwierdź"
},
"generate": "Generate playlist",
"name": "Playlist",
"description": "Tank playlist generator",
"seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!",
"button": "Generate",
"pool_size": "{count} possible tanks"
"generate": "Wygeneruj playlistę",
"name": "Playlista",
"description": "Generator playlisty czołgów",
"seo_description": "Nie wiesz, którym czołgiem grać w World of Tanks Blitz? Użyj funkcji playlisty BlitzKit, i ułóż własną listę czołgów do gry!",
"button": "Wygeneruj",
"pool_size": "{count} możliwe czołgi"
},
"mixer": {
"name": "Mixer",
"name": "Mikser",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
"screenshot": "Zrzut ekranu",
"randomize": "Wymieszaj"
},
"guess": {
"name": "Guess",
"name": "Zgadnij",
"button": "What could it be?",
"description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": {
"placeholder": "Type to guess...",
"skip": "Skip",
"guess": "Guess",
"next": "Next",
"placeholder": "Wpisz odpowiedź",
"skip": "Pomiń",
"guess": "Zgaduję",
"next": "Następny",
"no_results": "Brak wyników"
},
"stats": "{correct} / {total} correct, {streak} streak",
"help": {
"title": "Partially reveal tank?",
"title": "Odsłonić część czołgu?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"reveal": "Reveal"
"cancel": "Anuluj",
"reveal": "Pokaż"
},
"clear": "Clear",
"select_all": "Select all",
"tier": "Tier {tier}"
"clear": "Wyczyść",
"select_all": "Zaznacz wszystko ",
"tier": "Poziom czołgu {tier}"
},
"tankopedia": {
"search": "Search a stat...",
"search": "Wyszukaj statystykę...",
"name": "Tankopedia",
"button": "Find tanks",
"description": "Blitz tank encyclopedia",
"button": "Znajdź czołg",
"description": "Encyklopedia czołgów w WoTb",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"visualizers": {
"reload": {
"skip": "Skip"
"skip": "Pomiń"
},
"assault": {
"title": "Assault shells"
},
"rotator": {
"hull": "Hull",
"turret": "Turret",
"hard": "Hard",
"hull": "Kadłub",
"turret": "Wieża",
"hard": "Utwardzony",
"medium": "Średni",
"soft": "Soft",
"effective": "Effective"
"soft": "Miękki",
"effective": "Rzeczywisty"
},
"ricochet": {
"angle": "Angle",
"angle": "Kąt",
"normalization": "Normalization",
"effective_angle": "Effective angle",
"armor": "Armor",
"no_ricochet": "Explosive shells don't ricochet"
"effective_angle": "Rzeczywisty kąt nachylenia pancerza",
"armor": "Pancerz",
"no_ricochet": "Pociski odłamkowe nie rykoszetują."
},
"view_range": {
"camo": "Camo",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
"camo": "Kamuflaż",
"ghost_factory": "Fabryka duchów",
"dead_rail": "Martwy tor"
},
"flexibility": {
"yaw": "Yaw",
@@ -443,71 +443,71 @@
"ricochet": "Ricochet",
"explosion_radius": "Explosion radius",
"exit": "Exit",
"reset": "Reset"
"reset": "Zresetuj"
},
"advanced": "Advanced highlighting",
"advanced": "Zaawansowane podświetlanie",
"shot_card": {
"element": {
"primary": "Primary",
"primary": "Główny",
"spaced": "Spaced",
"external": "External",
"gap": "Gap",
"gun": "Gun",
"track": "Tracks"
"external": "Zewnętrzny",
"gap": "Odstęp",
"gun": "Działo",
"track": "Gąsienice"
},
"status": {
"penetration": "{damage}hp penetration",
"blocked": "Blocked",
"ricochet": "Ricochet",
"splash": "{damage}hp splash"
"penetration": "{damage} penetracja punktów wytrzymałości",
"blocked": "Zablokowany",
"ricochet": "Rykoszet",
"splash": "{damage} rozprysk punktów wytrzymałości"
},
"stats": {
"thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetration"
"thickness_and_angle": "{thickness}mm {angle}",
"nominal": "{thickness}mm nominalny",
"ricochet_loss": "{loss}% penetracji"
}
}
},
"static": {
"name": "Static armor",
"primary": "Primary",
"spaced": "Spaced",
"external": "External",
"name": "Pancerz statyczny",
"primary": "Główny Pancerz",
"spaced": "Ekran przeciwkumulacyjny",
"external": "Zewnętrzny",
"edit": {
"label": "Edit",
"reset": "Reset"
"label": "Edytuj",
"reset": "Zresetuj"
}
}
},
"meta": {
"back": "Back",
"compare": "Compare",
"back": "Powrót",
"compare": "Porównaj",
"swap": {
"button": "Swap",
"title": "Swap tanks"
"button": "Zamień",
"title": "Zamień czołgi"
},
"vanilla": "Vanilla",
"opentest": "OpenTest",
"full_name": "Full name",
"full_name": "Pełna nazwa",
"nation": "Naród",
"class": "Klasa",
"tier": "Poziom",
"type": "Rodzaj",
"dev_id": "DEV: ID",
"dev_name": "DEV: NAME",
"purchase": "Purchase price",
"restoration": "Restoration price",
"dev_name": "DEV: NAZWA",
"purchase": "Cena zakupu",
"restoration": "Cena odzyskania",
"sale": "Sale price",
"research": "Research XP"
},
"tech_tree": {
"title": "Tech tree",
"battle": "1 battle",
"battles": "{count} battles",
"route": "Route {index} of {count}"
"title": "Drzewko technologiczne",
"battle": "1 bitwa",
"battles": "{count} bitew",
"route": "Ścieżka {index} z {count}"
},
"game_modes": {
"title": "Game mode abilities"
"title": "Umiejętności trybu gry"
},
"review": {
"title": "Review videos",
@@ -520,26 +520,26 @@
"equalize": "Equalize"
},
"modules": {
"title": "Modules",
"stock": "Stock",
"upgrade": "Upgrade"
"title": "Moduły",
"stock": "Standardowy",
"upgrade": "Ulepsz"
},
"equipment": {
"title": "Equipment",
"clear": "Clear"
"title": "Wyposażenie",
"clear": "Wyczyść"
},
"provisions": {
"title": "Provisions",
"clear": "Clear"
"title": "Materiały eksploatacyjne",
"clear": "Wyczyść"
},
"consumables": {
"title": "Consumables",
"clear": "Clear",
"clear": "Wyczyść",
"info": "Yellow consumables do not affect the statistics."
},
"skills": {
"title": "Crew skills",
"clear": "Clear",
"clear": "Wyczyść",
"maximize": "Maximize",
"info": "Yellow skills only apply under special circumstances. Hold Space to quickly toggle between level 0 and 7."
},
@@ -557,49 +557,49 @@
},
"values": {
"shell_index": "Shell {index}",
"at_distance": "At {distance}m",
"at_distance_slider_label": "Distance",
"dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "Gun flexibility",
"speed": "Speed",
"terrain_coefficients": "Terrain coefficients",
"raw_terrain_coefficients": "Raw terrain coefficients",
"power_to_weight_ratio": "Power to weight ratio",
"hull_traverse_speed": "Hull traverse speed",
"camouflage": "Camouflage",
"reload": "Reload",
"crewCount": "Crew count",
"at_distance": "Z odległości {distance}m",
"at_distance_slider_label": "Zasięg widzenia",
"dispersion_at_distance": "Rozrzut na {distance}m",
"gun_flexibility": "Elastyczność działa",
"speed": "Prędkość",
"terrain_coefficients": "Współczynniki terenu",
"raw_terrain_coefficients": "Surowe współczynniki terenu",
"power_to_weight_ratio": "Stosunek mocy do masy",
"hull_traverse_speed": "Prędkość obrotu kadłuba",
"camouflage": "Kamuflaż",
"reload": "Przeładowanie",
"crewCount": "Liczba członków załogi",
"shellNormalization": "Normalization",
"shellRicochet": "Ricochet",
"dpm": "DPM",
"dpmEffective": "Effective at 60s",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "Shells",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Reloads",
"shellReload": "Reload",
"intraClip": "Intra-clip",
"dpm": "Uszkodzenia na minutę",
"dpmEffective": "Efektywne w 60s",
"peekabooDpm": "Obrażenia na minutę w stylu „wychyl i schowaj”",
"peekabooDpmInfo": "Obrażenia na minutę przy założeniu, że strzelisz tylko po zresetowaniu kamuflażu (lub ponownym załadowaniu, jeśli jest wolniejszy) — co najmniej 10 sekund pomiędzy strzałami.",
"shells": "Pociski",
"mostOptimalShellIndex": "Optymalny pocisk",
"shellReloads": "Przeładowania",
"shellReload": "Przeładowanie",
"intraClip": "Czas przeładowania pocisku",
"caliber": "Caliber",
"penetration": "Penetration",
"damage": "Uszkodzenia",
"damageWithoutAssault": "Damage without assault shells",
"clipDamage": "Clipping potential",
"moduleDamage": "Module damage",
"explosionRadius": "Explosion radius",
"shellVelocity": "Shell velocity",
"aimTime": "Aim time",
"damageWithoutAssault": "Uszkodzenia bez pocisków szturmowych",
"clipDamage": "Potencjał magazynu",
"moduleDamage": "Uszkodzenia modułów",
"explosionRadius": "Promień eksplozji",
"shellVelocity": "Prędkość pocisku",
"aimTime": "Czas celowania",
"dispersion": "Still",
"dispersionMoving": "Moving",
"dispersionHullTraversing": "Hull traversing",
"dispersionTurretTraversing": "Turret traversing",
"dispersionShooting": "Shooting",
"dispersionGunDamaged": "Gun damaged",
"gunDepression": "Depression",
"gunElevation": "Elevation",
"gunFrontalDepression": "Frontal depression",
"gunFrontalElevation": "Frontal elevation",
"gunRearDepression": "Rear depression",
"dispersionMoving": "Podczas ruchu",
"dispersionHullTraversing": "Obrót kadłuba",
"dispersionTurretTraversing": "Obrót wieży",
"dispersionShooting": "Strzelanie",
"dispersionGunDamaged": "Uszkodzone Działo",
"gunDepression": "Limit obrotu działa w dół",
"gunElevation": "Limit obrotu działa w górę",
"gunFrontalDepression": "Przedni limit obrotu działa w dół",
"gunFrontalElevation": "Przedni limit obrotu działa w górę",
"gunRearDepression": "Tylne opuszczanie działa",
"gunRearElevation": "Rear elevation",
"azimuthLeft": "Azimuth left",
"azimuthRight": "Azimuth right",
@@ -623,9 +623,10 @@
"hullTraverseSoftTerrain": "Soft",
"health": "Health",
"fireChance": "Fire chance",
"trackRepairTime": "Track repair time",
"viewRange": "View range",
"camouflageStill": "Still",
"camouflageMoving": "Moving",
"camouflageMoving": "Podczas ruchu",
"camouflageShootingStill": "Shooting while still",
"camouflageShootingMoving": "Shooting while moving",
"camouflageCaughtOnFire": "Caught on fire",
@@ -647,7 +648,7 @@
"crew": {
"title": "Crew training",
"info": "represents the other roles a crew member can take.",
"count": "Crew count",
"count": "Liczba członków załogi",
"commander": "Commander",
"gunner": "Gunner",
"driver": "Driver",
@@ -655,14 +656,14 @@
"loader": "Loader"
},
"maneuverability": {
"title": "Maneuverability"
"title": "Mobilność "
},
"survivability": {
"title": "Survivability"
"title": "Przetrwanie"
}
},
"compare": {
"name": "Compare",
"name": "Porównaj",
"button": "Compare tanks",
"description": "Compare tank statistics",
"seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz",
@@ -672,7 +673,7 @@
"title": "Add tanks",
"description": "Select tanks to add to comparison"
},
"clear": "Clear",
"clear": "Wyczyść",
"deltas": {
"none": "No deltas",
"percentage": "Percentage",
@@ -685,17 +686,17 @@
},
"table": {
"equipment": {
"clear": "Clear",
"clear": "Wyczyść",
"apply_to_all": "Apply to all"
},
"skills": {
"clear": "Clear",
"clear": "Wyczyść",
"maximize": "Maximize",
"equalize": "Equalize"
},
"modules": {
"stock": "Stock",
"upgrade": "Upgrade"
"stock": "Standardowy",
"upgrade": "Ulepsz"
},
"provisions": {
"clear": "Clear this tank",
@@ -760,14 +761,14 @@
"seo_description": "Track your stats in real time as you play World of Tanks Blitz",
"search": {
"lookup": "Look up a player to get started",
"hint": "Search players...",
"searching": "Searching...",
"hint": "Szukaj graczy...",
"searching": "Wyszukiwanie...",
"no_results": "No players found"
},
"controls": {
"title": "Tracking {name}",
"subtitle": "Since {time}",
"reset": "Reset",
"reset": "Zresetuj",
"change": "Change"
},
"no_cc": {
@@ -791,7 +792,7 @@
"description": "Rank Blitz tanks",
"seo_description": "Explore tier lists ranking tanks from best to worst, based on player ratings and statistical performance in World of Tanks Blitz",
"buttons": {
"reset": "Reset",
"reset": "Zresetuj",
"share": "Share"
}
},
@@ -827,7 +828,7 @@
"success": {
"title": "Copy URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"back": "Back",
"back": "Powrót",
"copy": "Copy URL"
}
},
@@ -838,7 +839,7 @@
},
"customize": {
"title": "Customize",
"reset": "Reset",
"reset": "Zresetuj",
"description": "Bells and whistles of your embed"
}
},
+6 -5
View File
@@ -41,7 +41,7 @@
"development": "Development",
"opentest": "TesteAberto",
"preview": "Preview",
"production": "Production"
"production": "Produção"
},
"locales": {
"en": "Inglês",
@@ -187,11 +187,11 @@
},
"website": {
"redirect": {
"title": "301: moved permanently",
"title": "301: movido permanentemente",
"body": "This page has moved. Redirecting..."
},
"settings": {
"title": "Settings",
"title": "Configurações",
"description": "Change BlitzKit settings",
"dev_mode": "Modo de desenvolvimento",
"auto_load_models": "Auto-load models",
@@ -207,9 +207,9 @@
"copied": "Copiado!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"select_dialog_title": "Procura por tanque",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"clear_filters": "Limpar filtros",
"login": "Login to use this",
"search_bar_hint": "Procurar tanques...",
"recent": "Visto recentemente",
@@ -623,6 +623,7 @@
"hullTraverseSoftTerrain": "Macio",
"health": "Vida",
"fireChance": "Chance de incêndio",
"trackRepairTime": "Track repair time",
"viewRange": "Alcance de visão",
"camouflageStill": "Parado",
"camouflageMoving": "Em movimento",
+7 -6
View File
@@ -38,10 +38,10 @@
},
"branches": {
"beta": "Бета",
"development": "Development",
"development": "Разработка",
"opentest": "Открытый тест",
"preview": "Preview",
"production": "Production"
"preview": "Предпросмотр",
"production": "Производство"
},
"locales": {
"en": "Английский",
@@ -194,7 +194,7 @@
"title": "Настройки",
"description": "Изменить настройки BlitzKit",
"dev_mode": "Режим разработчика",
"auto_load_models": "Auto-load models",
"auto_load_models": "Автозагрузка моделей",
"language": "Язык"
},
"common": {
@@ -413,7 +413,7 @@
}
},
"sandbox": {
"load_model": "Load model",
"load_model": "Загрузить модель",
"poses": {
"hull_down": "За холмом",
"face_hug": "В упор",
@@ -574,7 +574,7 @@
"dpm": "ДПМ",
"dpmEffective": "Эффективность за 60с",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"peekabooDpmInfo": "Урон в минуту при условии, если вы сбросите камуфляж(или перезарядка окончена, если она медленнее) - не менее 10 секунд между выстрелами.",
"shells": "Снарядов в барабане",
"mostOptimalShellIndex": "Самый эффективный снаряд",
"shellReloads": "Перезарядка",
@@ -623,6 +623,7 @@
"hullTraverseSoftTerrain": "На мягком грунте",
"health": "Прочность",
"fireChance": "Вероятность возгорания",
"trackRepairTime": "Track repair time",
"viewRange": "Радиус обзора",
"camouflageStill": "Когда неподвижен",
"camouflageMoving": "В движении",
+1
View File
@@ -623,6 +623,7 @@
"hullTraverseSoftTerrain": "Soft",
"health": "Здоров'я",
"fireChance": "Fire chance",
"trackRepairTime": "Track repair time",
"viewRange": "View range",
"camouflageStill": "Нерухомо",
"camouflageMoving": "У русі",
+1
View File
@@ -623,6 +623,7 @@
"hullTraverseSoftTerrain": "水面上车转速率",
"health": "血量",
"fireChance": "发动机受击时着火概率",
"trackRepairTime": "Track repair time",
"viewRange": "视野范围",
"camouflageStill": "静止隐蔽",
"camouflageMoving": "移动时",
@@ -1,38 +0,0 @@
---
import { tankIcon } from "@blitzkit/core";
import { Flex, Heading, Link, Strong, Text } from "@radix-ui/themes";
---
<Flex justify="center" px="4">
<Link
color="gray"
highContrast
style={{ display: "content" }}
href="/docs/guides/christmas-2025"
underline="none"
>
<Flex
mt="3"
px="5"
py="4"
style={{
borderRadius: "var(--radius-3)",
backgroundImage: "url(https://i.imgur.com/LRYmSkg.png)",
backgroundSize: "cover",
backgroundPosition: "center",
boxSizing: "border-box",
}}
align="center"
gap="5"
>
<img src={tankIcon(25169)} style={{ height: "min(15vw, 5rem)" }} />
<Flex direction="column">
<Heading size={{ initial: "4", sm: "6" }}>Christmas 2025</Heading>
<Text size={{ initial: "2", sm: "3" }}>
Win the <Strong>GSOR the TANK</Strong> and more!
</Text>
</Flex>
</Flex>
</Link>
</Flex>
@@ -1,89 +0,0 @@
import { Code, Flex, Heading, Link, Text } from "@radix-ui/themes";
import { useEffect, useState, type ReactNode } from "react";
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
import { InlineSkeleton } from "./InlineSkeleton";
const TIME = 1764898200 * 1000;
export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) {
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
useEffect(() => {
const interval = setInterval(() => {
setTimeLeft(TIME - Date.now());
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
let string: ReactNode;
if (timeLeft > 0) {
const hours = Math.floor(timeLeft / (1000 * 60 * 60));
const minutes = Math.floor((timeLeft / (1000 * 60)) % 60)
.toString()
.padStart(2, "0");
const seconds = Math.floor((timeLeft / 1000) % 60)
.toString()
.padStart(2, "0");
string = (
<>
in{" "}
<Code highContrast>
{hours}h {minutes}m {seconds}s
</Code>
</>
);
} else {
string = "has ended. Come again later!";
}
return (
<Flex justify="center" px="4">
<Link
color="gray"
highContrast
style={{ display: "content" }}
href="https://discord.gg/njQdBhNS?event=1446208951136813117"
underline="none"
>
<Flex
mt="3"
px="5"
py="4"
style={{
borderRadius: "var(--radius-3)",
backgroundImage: "url(https://i.imgur.com/LRYmSkg.png)",
backgroundSize: "cover",
backgroundPosition: "center",
boxSizing: "border-box",
boxShadow: "0 0 var(--space-7) var(--blue-a5)",
}}
align="center"
gap="5"
>
<img
src="https://i.imgur.com/mvmJQgB.png"
style={{
height: "min(15vw, 5rem)",
filter: "drop-shadow(0.5rem 0.5rem 1rem #00000080)",
}}
/>
<Flex direction="column">
<Heading size={{ initial: "4", sm: "6" }}>
Music Box giveaway {!skeleton && string}{" "}
{skeleton && <InlineSkeleton width="10rem" />}
</Heading>
<Text size={{ initial: "2", sm: "3" }}>
Join the BlitzKit Discord server event
</Text>
</Flex>
</Flex>
</Link>
</Flex>
);
}
+9 -2
View File
@@ -1,13 +1,20 @@
import { Link, Table } from '@radix-ui/themes';
import MarkdownToJSX from 'markdown-to-jsx';
import type { ComponentProps } from 'react';
import { docImageUrl, isExternalImage } from '../core/blitzkit/docImages';
interface MarkdownProps {
children: string;
}
function Image(props: ComponentProps<'img'>) {
return <img style={{ maxWidth: '100%' }} {...props} />;
function Image({ src, ...props }: ComponentProps<'img'>) {
return (
<img
style={{ maxWidth: '100%' }}
src={src && isExternalImage(src) ? docImageUrl(src) : src}
{...props}
/>
);
}
export function Markdown({ children }: MarkdownProps) {
@@ -1,3 +0,0 @@
export function StupidCat() {
return <img src="https://i.imgur.com/6FBBzb9.png" />;
}
@@ -1,7 +1,7 @@
import {
normalizeBoundingBox,
resolveDpm,
unionBoundingBox,
tankBoundingBox,
type TankDefinition,
} from "@blitzkit/core";
import { useMemo } from "react";
@@ -112,10 +112,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
case "survivability.volume": {
const dimensions = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
tankBoundingBox(tankModelDefinition, turretModelDefinition),
);
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
@@ -123,10 +120,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case "survivability.length": {
const bounds = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
tankBoundingBox(tankModelDefinition, turretModelDefinition),
);
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
@@ -3,7 +3,7 @@ import {
normalizeBoundingBox,
resolveDpm,
SEARCH_KEYS,
unionBoundingBox,
tankBoundingBox,
type TankDefinition,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
@@ -137,16 +137,10 @@ export const TankSearch = memo<TankSearchProps>(
const bTurretModelDefinition =
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox(
unionBoundingBox(
aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box!,
),
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition),
);
const bSize = normalizeBoundingBox(
unionBoundingBox(
bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box!,
),
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition),
);
const aVolume = aSize.x * aSize.y * aSize.z;
const bVolume = bSize.x * bSize.y * bSize.z;
@@ -164,16 +158,10 @@ export const TankSearch = memo<TankSearchProps>(
const bTurretModelDefinition =
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox(
unionBoundingBox(
aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box!,
),
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition),
);
const bSize = normalizeBoundingBox(
unionBoundingBox(
bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box!,
),
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition),
);
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
@@ -0,0 +1,32 @@
import { alias } from "@blitzkit/core";
const EXTERNAL_IMAGE = /^https?:\/\//;
const MARKDOWN_IMAGES = [
/!\[[^\]]*\]\((\S+?)(?:\s+"[^"]*")?\)/g,
/<img[^>]*\ssrc=["']([^"']+)["']/g,
];
export function isExternalImage(src: string) {
return EXTERNAL_IMAGE.test(src);
}
export function docImageId(url: string) {
let hash = 0x811c9dc5;
for (let index = 0; index < url.length; index++) {
hash ^= url.charCodeAt(index);
hash = Math.imul(hash, 0x01000193) >>> 0;
}
return hash.toString(16).padStart(8, "0");
}
export function docImageUrl(url: string) {
return alias("api", `/docs/images/${docImageId(url)}.webp`);
}
export function externalDocImages(markdown: string) {
return MARKDOWN_IMAGES.flatMap((pattern) =>
Array.from(markdown.matchAll(pattern), (match) => match[1]),
).filter(isExternalImage);
}
@@ -8,8 +8,8 @@ import {
resolvePenetrationCoefficient,
ShellType,
sum,
tankBoundingBox,
TankClass,
unionBoundingBox,
type EngineDefinition,
type EquipmentDefinitions,
type GunDefinition,
@@ -336,10 +336,7 @@ export function tankCharacteristics(
);
const size = normalizeBoundingBox(
unionBoundingBox(
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
tankBoundingBox(tankModelDefinition, turretModelDefinition),
);
const weightKg =
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
@@ -0,0 +1,39 @@
import type { APIContext, GetStaticPathsItem } from "astro";
import sharp from "sharp";
import {
docImageId,
externalDocImages,
} from "../../../../core/blitzkit/docImages";
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
import { getStaticPaths as _getStaticPaths } from "../../_index";
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
const documents = import.meta.glob<string>(
"../../../../../../../docs/**/*.md",
{ eager: true, query: "?raw", import: "default" },
);
const urls = new Set(Object.values(documents).flatMap(externalDocImages));
return Array.from(
urls,
(url) =>
({
params: { id: docImageId(url) },
props: { url },
}) satisfies GetStaticPathsItem,
);
});
export async function GET({ props }: APIContext<{ url: string }>) {
const response = await fetch(props.url);
if (!response.ok) {
throw new Error(`Failed to fetch ${props.url}: ${response.status}`);
}
const image = await sharp(Buffer.from(await response.arrayBuffer()))
.webp()
.toBuffer();
return new Response(new Uint8Array(image));
}
@@ -1,16 +1,15 @@
import {
parseHierarchies,
Sc2ReadStream,
ScgReadStream,
toUniqueId,
VertexAttribute,
vertexAttributeVectorSizes,
type Hierarchy,
type VehicleDefinitionList,
} from "@blitzkit/core";
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
import { Accessor, Document, Node, NodeIO, Scene } from "@gltf-transform/core";
import type { APIContext } from "astro";
import { times } from "lodash-es";
import { vfs } from "../../../../core/blitzkit/vfs";
export { getStaticPaths } from "./_index";
@@ -56,13 +55,10 @@ async function extractArmor(vfs: AbstractVFS, fileName: string) {
const scene = document.createScene();
const buffer = document.createBuffer();
function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) {
hierarchies.forEach((hierarchy) => {
const components = times(
hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, "0")],
);
parseHierarchies<Scene | Node>(
sc2["#hierarchy"],
scene,
(hierarchy, components, parent) => {
components.forEach((component) => {
switch (component["comp.typename"]) {
case "TransformComponent":
@@ -163,13 +159,9 @@ async function extractArmor(vfs: AbstractVFS, fileName: string) {
}
});
if (hierarchy["#hierarchy"]) {
parseHierarchies(hierarchy["#hierarchy"], parent);
}
});
}
parseHierarchies(sc2["#hierarchy"], scene);
return parent;
},
);
return document;
}
@@ -1,13 +1,13 @@
import {
bufferToBigInt,
DdsReadStream,
parseHierarchies,
PvrReadStream,
Sc2ReadStream,
ScgReadStream,
toUniqueId,
VertexAttribute,
type ConfigArchive,
type Hierarchy,
type TankParameters,
type Textures,
type VehicleDefinitionList,
@@ -240,8 +240,10 @@ async function extractModel(vfs: AbstractVFS, path: string) {
materials.set(id, resolvedMaterial);
});
function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) {
hierarchies.forEach((hierarchy) => {
parseHierarchies<Scene | Node>(
sc2["#hierarchy"],
scene,
(hierarchy, components, parent) => {
if (
omitMeshNames.start.some((omit) => hierarchy.name.startsWith(omit)) ||
omitMeshNames.end.some((omit) => hierarchy.name.endsWith(omit))
@@ -249,10 +251,6 @@ async function extractModel(vfs: AbstractVFS, path: string) {
return;
const node = document.createNode(hierarchy.name);
const components = times(
hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, "0")],
);
components.forEach((component) => {
switch (component["comp.typename"]) {
@@ -361,15 +359,11 @@ async function extractModel(vfs: AbstractVFS, path: string) {
}
});
if (hierarchy["#hierarchy"]) {
parseHierarchies(hierarchy["#hierarchy"], node);
}
parent.addChild(node);
});
}
parseHierarchies(sc2["#hierarchy"], scene);
return node;
},
);
scene.addChild(document.createNode("test"));