diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 58ff6f92c..e3675e93a 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -26,6 +26,7 @@ body: description: What branch of BlitzKit were you using? Please see the "Branch" section of the footer at the very bottom of any page. options: - Production + - Beta - Preview - OpenTest default: 0 diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml index 5ada3d32f..1cb3ea769 100644 --- a/.github/workflows/website.yaml +++ b/.github/workflows/website.yaml @@ -2,6 +2,11 @@ name: Website on: push: + branches: + - main + - production + + pull_request: schedule: - cron: "0 4 * * *" @@ -93,7 +98,7 @@ jobs: strategy: fail-fast: true matrix: - chunk: [default, api] + chunk: [default, api, models_tanks, models_legendary, models_maps] steps: - name: Free disk space @@ -102,9 +107,10 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: - ref: ${{ inputs.branch || 'production' }} + # the PR's ref, or the manual input, or production (selected when cron) + ref: ${{ github.event.pull_request.head.sha || inputs.branch || 'production' }} submodules: recursive - token: ${{ secrets.GH_TOKEN }} + token: ${{ secrets.GH_TOKEN || github.token }} - name: Setup bun uses: oven-sh/setup-bun@v2 @@ -122,9 +128,14 @@ jobs: - name: Install dependencies run: bun install - - name: Download Client + - name: Download client run: bun run download-client + - name: Download SmartDLC + if: ${{ env.CLIENT_PROVIDER == 'steam' }} + + run: bun run download-smart-dlc + - name: Build run: bun run --bun build env: @@ -139,13 +150,15 @@ jobs: - name: Trigger deployment - # Deploy if: - # 1. Running cron from main using content from production to target production - # 2. Running manually and targeting non-production + # workflow should trigger if: + # - is by cron and is on main: triggering to production + # - is by push and is on production: triggering to production + # - is by manual and is on anything: triggering to anything if: >- ${{ - github.ref == 'refs/heads/production' || - (inputs.environment != '' && inputs.environment != 'production') + (github.event_name == 'schedule' && github.ref == 'refs/heads/main') + || (github.event_name == 'push' && github.ref == 'refs/heads/production') + || (github.event_name == 'workflow_dispatch') }} env: diff --git a/cspell.json b/cspell.json index a67dd4960..17d054ca4 100644 --- a/cspell.json +++ b/cspell.json @@ -1,5 +1,5 @@ { - "words": [ + "words": [ "gamemodes", "Tankopedia" ] diff --git a/docs/about/credits.md b/docs/about/credits.md deleted file mode 100644 index ae2ac076a..000000000 --- a/docs/about/credits.md +++ /dev/null @@ -1,47 +0,0 @@ -# BlitzKit Credits - -BlitzKit is a one man band **TrèsAbhi** but I am far from solo. Helpers, in the order of contribution over time: - -### S0AP - -BlitzKit Discord bot verification, testing, advertisement, and the initial encouragement to create BlitzKit. - -### Prince_NA - -BlitzKit Discord bot and website testing and general advise. - -### Vovko - -BlitzKit Discord bot and website rendering pipeline. - -### Maddox - -Game file decompression. - -### Pyogenics - -Game 3D model parsing. - -### Aeson - -Game file layout, general advise, advertisement, and Tankopedia pre-release testing. - -### Minitelrose, ChickenMan7777 - -3-caliber rule edge cases. - -### HisRoyalFatness, Synx - -Embed UX. - -### [OPTML] clan, \_34, xkdkw, Droodles Blitz - -Tankopedia pre-release testing. - -### GonnaHetzMe - -Redacted by request. - -### Mark Greene - -GitHub contributor on bug fixes and feature additions. diff --git a/docs/changelogs/1.29.10.md b/docs/changelogs/1.29.10.md index d96d78fd8..8ba65ad99 100644 --- a/docs/changelogs/1.29.10.md +++ b/docs/changelogs/1.29.10.md @@ -1,14 +1,14 @@ # BlitzKit 1.29.10 -Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update. +Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. ## Features - Combined game mode passive and active abilities into a single dropdown -- Added tier filters to the Guess game [Mark Greene] +- Added tier filters to the Guess game [HitJack] - Changing tiers will break your streak ## Fixes -- module damage is now properly shown when sorting with it in the tank search [Mark Greene] +- module damage is now properly shown when sorting with it in the tank search [HitJack] - Duplicate abilities in the tank filters are now removed diff --git a/docs/changelogs/1.29.8.md b/docs/changelogs/1.29.8.md index a32e7d558..8f18b3474 100644 --- a/docs/changelogs/1.29.8.md +++ b/docs/changelogs/1.29.8.md @@ -1,6 +1,6 @@ # BlitzKit 1.29.8 -Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update. +Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. ## Features @@ -15,5 +15,5 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions ## Fixes - The Tankopedia now smoothly resizes when enlarging on interaction -- Stock turrets now correctly mount the corresponding stock gun [Mark Greene] +- Stock turrets now correctly mount the corresponding stock gun [HitJack] - The consumable and provision buttons in Compare are no longer tiny diff --git a/docs/changelogs/1.29.9.md b/docs/changelogs/1.29.9.md index c426194e1..3ec9b1333 100644 --- a/docs/changelogs/1.29.9.md +++ b/docs/changelogs/1.29.9.md @@ -1,14 +1,14 @@ # BlitzKit 1.29.9 -Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update. +Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. ## Features - Reworked tank search filters to be far more compact to make way for new filters -- Added the ability to filter tanks by consumables [Mark Greene] +- Added the ability to filter tanks by consumables [HitJack] - Added the ability to filter tanks by provisions and game-mode abilities and powers ## Fixes - Consumable and provision icons in Compare are now properly sized -- Provisions now properly affect terrain resistances [Mark Greene] +- Provisions now properly affect terrain resistances [HitJack] diff --git a/docs/changelogs/1.30.7.md b/docs/changelogs/1.30.7.md index c42589bff..c045cf5a5 100644 --- a/docs/changelogs/1.30.7.md +++ b/docs/changelogs/1.30.7.md @@ -1,6 +1,6 @@ # BlitzKit 1.30.7 -Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update. +Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. ## Features @@ -8,13 +8,13 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions ## Fixes -- Clicking on the armor multiple times with HE no longer crashes BlitzKit [Mark Greene] -- Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration [Mark Greene] +- Clicking on the armor multiple times with HE no longer crashes BlitzKit [HitJack] +- Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration [HitJack] - The lighting animation no longer stops prematurely when BlitzKit is unfocused - The spot light angle has been increased to more evenly light up larger tanks - BlitzKit once again uses soft shadows, reducing the pixelated shadows - Fixed the weird patterns that formed on the tracks of tanks under self-shadows - Increased the floor size and light decay to create the illusion that the floor is infinite -- Safari no longer crashes when unloading textures for unloaded tanks [Mark Greene] +- Safari no longer crashes when unloading textures for unloaded tanks [HitJack] - The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0 -- Armor plate names in Static Armor are now properly capitalized in English and translated to other languages [Mark Greene] +- Armor plate names in Static Armor are now properly capitalized in English and translated to other languages [HitJack] diff --git a/docs/changelogs/1.31.4.md b/docs/changelogs/1.31.4.md deleted file mode 100644 index 462d732f7..000000000 --- a/docs/changelogs/1.31.4.md +++ /dev/null @@ -1,41 +0,0 @@ -# BlitzKit 1.31.4 - -Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update. - -## Features - -- Added Peekaboo DPM [Mark Greene] - - Your Peekaboo DPM is lower than your normal DPM if your reload is faster than 10s as you'll have to wait for the remaining 10s for you to get unspotted before you can poke and shoot -- The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [Mark Greene] -- Added a spall liner button to the Tankopedia beside the other equipment [Mark Greene] - -## Fixes - -- BlitzKit Session now properly handles tanks removed from the game [Mark Greene] -- Removed the Beta badge from the Equalizer button [Mark Greene] -- Updated the background image for the Equalizer button to the actual banner of the new game mode -- The armor no longer disappears when you turn on Dynamic Armor System [Mark Greene] -- Camouflage values in Tankopedia are now more descriptive when shooting [Mark Greene] -- The contact shadows on the ground no longer grow & darken when switching between tabs [Mark Greene] -- The "Help Translate BlitzKit" text on the home page is now properly centered vertically -- The footer now says "Branch" instead of "CDN" [Mark Greene] -- The game version in the footer now displays a shorthand version instead of the entire version number [Mark Greene] -- The reload visualizer now displays just 1 decimal place [Mark Greene] -- The search bar no longer redirects to a seemingly random tank when clicking enter when searching [Mark Greene] -- Tank search now properly handles the full names of tanks like the Super Pershing [Mark Greene] -- The loading bar below the name of the tanks in Tankopedia is now sufficiently spaced below [Mark Greene] -- The gun depression visualizer now properly handles the inherent tilt of tanks like the Minotauro [Mark Greene] - -## Technical Features - -- Added documentation to the README on setting up and developing BlitzKit locally -- Added bug report and feature request forms to GitHub -- Switched to the GNU AGPLv3 license - -## Technical Fixes - -- BlitzKit now handles the new ID system for game-modes in WoTB -- The directory which preview versions of the game are downloaded to are now created properly - - This was the last bug standing in the way of BlitzKit Previews - - Welcome back, BlitzKit Previews! -- Removed some dead code from the BlitzKit model mangler diff --git a/docs/changelogs/1.32.0.md b/docs/changelogs/1.32.0.md new file mode 100644 index 000000000..9ffbce4ef --- /dev/null +++ b/docs/changelogs/1.32.0.md @@ -0,0 +1,98 @@ +# BlitzKit 1.32.0 + +Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. + +## Features + +### Tankopedia & Armor + +- Added a spall liner button to the Tankopedia beside the other equipment [HitJack] +- Listed track repair times in the Tankopedia [HitJack] +- The penetration chance is now displayed when penetration testing armor [HitJack] +- Added a new setting to disable automatically loading tanks in Tankopedia [HitJack] + +### Tank Performance & Mechanics + +- Added Peekaboo DPM [HitJack] + - Your Peekaboo DPM is lower than your normal DPM if your reload is faster than 10s as you'll have to wait for the remaining 10s for you to get unspotted before you can poke and shoot + +### Tank Filters + +- The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [HitJack] + +### Game Compatibility + +- Added compatibility for tier XI tanks [HitJack] + +## Fixes + +### Tankopedia & Armor + +- The armor no longer disappears when you turn on Dynamic Armor System [HitJack] +- Camouflage values in Tankopedia are now more descriptive when shooting [HitJack] +- The penetration chance is now displayed when penetration testing armor [HitJack] +- Listed track repair times in the Tankopedia [HitJack] +- Added a spall liner button to the Tankopedia beside the other equipment [HitJack] +- The flags in the Tankopedia tech tree are no longer offset to the top left [HitJack] +- The loading bar below the name of the tanks in Tankopedia is now sufficiently spaced below [HitJack] +- Removed the vertical sliver of pixels found to the right of the Equalizer toggle in Tankopedia + +### Tank Search & Filters + +- The search bar no longer redirects to a seemingly random tank when clicking enter when searching [HitJack] +- Tank search now properly handles the full names of tanks like the Super Pershing [HitJack] +- The tank search now remains consistent in order even when an added filter does not add/remove tanks from the list [HitJack] +- The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [HitJack] + +### Visuals & UI + +- Removed the Beta badge from the Equalizer button [HitJack] +- Updated the background image for the Equalizer button to the actual banner of the new game mode +- The contact shadows on the ground no longer grow & darken when switching between tabs [HitJack] +- Additionally, the contact shadows stays visible at low angles [HitJack] +- The "Help Translate BlitzKit" text on the home page is now properly centered vertically +- The footer now says "Branch" instead of "CDN" [HitJack] +- The game version in the footer now displays a shorthand version instead of the entire version number [HitJack] +- The reload visualizer now displays just 1 decimal place [HitJack] +- Plot controls for the Static armor tab no longer pokes in from the right on non-Static armor tabs in some languages [HitJack] +- Mixer's controllers now scale better on smaller screens [HitJack] +- The list of available tanks in the Playlist is no longer excessively wide [HitJack] + +### Tank Mechanics & Models + +- The gun depression visualizer now properly handles the inherent tilt of tanks like the Minotauro [HitJack] +- When moving the tracks of tanks, the textures no longer move at different rates [HitJack] + +### Localization + +- Users with a preference on a language are once again redirected to the correct localized pages [HitJack] + +### Session & Game Compatibility + +- BlitzKit Session now properly handles tanks removed from the game [HitJack] + +## Technical Fixes + +- BlitzKit now handles the new ID system for game-modes in WoTB +- The directory which preview versions of the game are downloaded to are now created properly + - This was the last bug standing in the way of BlitzKit Previews + - Welcome back, BlitzKit Previews! +- Removed some dead code from the BlitzKit model mangler +- The player discovery loop now persists for longer than 1 iteration [HitJack] +- Bun is now forced to run scripts with the `--bun` flag + +## Technical Features + +- Added documentation to the README on setting up and developing BlitzKit locally +- Added bug report and feature request forms to GitHub +- Switched to the GNU AGPLv3 license + +## Technical Fixes + +- BlitzKit now handles the new ID system for game-modes in WoTB +- The directory which preview versions of the game are downloaded to are now created properly + - This was the last bug standing in the way of BlitzKit Previews + - Welcome back, BlitzKit Previews! +- Removed some dead code from the BlitzKit model mangler +- The player discovery loop now persists for longer than 1 iteration [HitJack] +- Bun is now forced to run scripts with the `--bun` flag diff --git a/docs/guides/timezones.md b/docs/guides/timezones.md deleted file mode 100644 index 631450aaf..000000000 --- a/docs/guides/timezones.md +++ /dev/null @@ -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. diff --git a/docs/technical/bkrl.md b/docs/technical/bkrl.md deleted file mode 100644 index b008e11a8..000000000 --- a/docs/technical/bkrl.md +++ /dev/null @@ -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
body; -} - -Header { - uint16 version; // always 1 - Format format; - uint32 count; -} - -Format enum { - Base, - Superset1, -} - -Body
match { - 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; - }; -} -``` diff --git a/docs/technical/dids.md b/docs/technical/dids.md deleted file mode 100644 index 6a02cabbf..000000000 --- a/docs/technical/dids.md +++ /dev/null @@ -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; -} -``` diff --git a/docs/technical/kitscore.md b/docs/technical/kitscore.md index fe8e9dc5a..ab2168005 100644 --- a/docs/technical/kitscore.md +++ b/docs/technical/kitscore.md @@ -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! diff --git a/docs/technical/rts.md b/docs/technical/rts.md deleted file mode 100644 index 18b814b2e..000000000 --- a/docs/technical/rts.md +++ /dev/null @@ -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 { - uint16 version; // always 1 - uint32 playerCount; -} - -Body { - 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; -} -``` diff --git a/environments/beta.json b/environments/beta.json index d5ff3f5d5..c9cb9adfd 100644 --- a/environments/beta.json +++ b/environments/beta.json @@ -4,14 +4,33 @@ "paths": { "default": null, - "api": null + + "api": null, + + "models_tanks": null, + "models_legendary": null, + "models_maps": null }, + "nodes": { "default": "blitzkit/deploy-node-beta.blitzkit.app", - "api": "blitzkit/deploy-node-api.beta.blitzkit.app" + + "api": "blitzkit/deploy-node-api.beta.blitzkit.app", + + "models_tanks": "blitzkit/deploy-node-tanks.models.beta.blitzkit.app", + "models_legendary": "blitzkit/deploy-node-legendary.models.beta.blitzkit.app", + "models_maps": "blitzkit/deploy-node-maps.models.beta.blitzkit.app" }, + "aliases": { + "default": "/", + "api": "https://api.beta.blitzkit.app", - "static": "https://static.beta.blitzkit.app" + + "models_tanks": "https://tanks.models.beta.blitzkit.app", + "models_legendary": "https://legendary.models.beta.blitzkit.app", + "models_maps": "https://maps.models.beta.blitzkit.app", + + "static": "https://static.blitzkit.app" } } diff --git a/environments/development.json b/environments/development.json index 742768ff4..fbeeab959 100644 --- a/environments/development.json +++ b/environments/development.json @@ -4,11 +4,25 @@ "paths": { "default": null, - "api": "api" + + "api": "api", + + "models_tanks": "models/tanks", + "models_legendary": "models/legendary", + "models_maps": "models/maps" }, + "nodes": {}, + "aliases": { + "default": "/", + "api": "/api", + + "models_tanks": "/models/tanks", + "models_legendary": "/models/legendary", + "models_maps": "/models/maps", + "static": "https://static.blitzkit.app" } } diff --git a/environments/opentest.json b/environments/opentest.json index 0c29b792f..8aa51ea16 100644 --- a/environments/opentest.json +++ b/environments/opentest.json @@ -4,14 +4,33 @@ "paths": { "default": null, - "api": null + + "api": null, + + "models_tanks": null, + "models_legendary": null, + "models_maps": null }, + "nodes": { "default": "blitzkit/deploy-node-opentest.blitzkit.app", - "api": "blitzkit/deploy-node-api.opentest.blitzkit.app" + + "api": "blitzkit/deploy-node-api.opentest.blitzkit.app", + + "models_tanks": "blitzkit/deploy-node-tanks.models.opentest.blitzkit.app", + "models_legendary": "blitzkit/deploy-node-legendary.models.opentest.blitzkit.app", + "models_maps": "blitzkit/deploy-node-maps.models.opentest.blitzkit.app" }, + "aliases": { + "default": "/", + "api": "https://api.opentest.blitzkit.app", - "static": "https://static.opentest.blitzkit.app" + + "models_tanks": "https://tanks.models.opentest.blitzkit.app", + "models_legendary": "https://legendary.models.opentest.blitzkit.app", + "models_maps": "https://maps.models.opentest.blitzkit.app", + + "static": "https://static.blitzkit.app" } } diff --git a/environments/preview.json b/environments/preview.json index a3d980559..bcc9b2644 100644 --- a/environments/preview.json +++ b/environments/preview.json @@ -4,14 +4,33 @@ "paths": { "default": null, - "api": null + + "api": null, + + "models_tanks": null, + "models_legendary": null, + "models_maps": null }, + "nodes": { "default": "blitzkit/deploy-node-preview.blitzkit.app", - "api": "blitzkit/deploy-node-api.preview.blitzkit.app" + + "api": "blitzkit/deploy-node-api.preview.blitzkit.app", + + "models_tanks": "blitzkit/deploy-node-tanks.models.preview.blitzkit.app", + "models_legendary": "blitzkit/deploy-node-legendary.models.preview.blitzkit.app", + "models_maps": "blitzkit/deploy-node-maps.models.preview.blitzkit.app" }, + "aliases": { + "default": "/", + "api": "https://api.preview.blitzkit.app", - "static": "https://static.preview.blitzkit.app" + + "models_tanks": "https://tanks.models.preview.blitzkit.app", + "models_legendary": "https://legendary.models.preview.blitzkit.app", + "models_maps": "https://maps.models.preview.blitzkit.app", + + "static": "https://static.blitzkit.app" } } diff --git a/environments/production.json b/environments/production.json index 89d8e9a5e..d66b4c7ad 100644 --- a/environments/production.json +++ b/environments/production.json @@ -4,14 +4,33 @@ "paths": { "default": null, - "api": null + + "api": null, + + "models_tanks": null, + "models_legendary": null, + "models_maps": null }, + "nodes": { "default": "blitzkit/deploy-node-blitzkit.app", - "api": "blitzkit/deploy-node-api.blitzkit.app" + + "api": "blitzkit/deploy-node-api.blitzkit.app", + + "models_tanks": "blitzkit/deploy-node-tanks.models.blitzkit.app", + "models_legendary": "blitzkit/deploy-node-legendary.models.blitzkit.app", + "models_maps": "blitzkit/deploy-node-maps.models.blitzkit.app" }, + "aliases": { + "default": "/", + "api": "https://api.blitzkit.app", + + "models_tanks": "https://tanks.models.blitzkit.app", + "models_legendary": "https://legendary.models.blitzkit.app", + "models_maps": "https://maps.models.blitzkit.app", + "static": "https://static.blitzkit.app" } } diff --git a/package.json b/package.json index d9bc165ba..399d17fd9 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,18 @@ { "name": "blitzkit", - "version": "1.31.4", + "version": "1.33.0-beta.1", "license": "BlitzKit License", "private": true, "scripts": { "dev": "cd packages/website && bun --bun run dev", - "build": "cd packages/website && bun run build", + "build": "cd packages/website && bun --bun run build", "discover-ids": "cd packages/scripts && bun run discover-ids", "build-protos": "cd packages/scripts && bun run build-protos", "record-performance": "cd packages/scripts && bun run record-performance", "download-client": "cd packages/scripts && bun run download-client", - "clean-astro": "rm -rf packages/website/.astro packages/website/node_modules/.astro packages/website/node_modules/.vite" + "download-smart-dlc": "cd packages/scripts && bun run download-smart-dlc", + "clean-astro": "rm -rf packages/website/.astro packages/website/node_modules/.astro packages/website/node_modules/.vite", + "vfs": "cd packages/scripts && bun run vfs" }, "type": "module", "workspaces": [ diff --git a/packages/core/src/blitzkit/api/server/0_base.ts b/packages/core/src/blitzkit/api/server/0_base.ts index 2efff35ec..034122e00 100644 --- a/packages/core/src/blitzkit/api/server/0_base.ts +++ b/packages/core/src/blitzkit/api/server/0_base.ts @@ -246,6 +246,27 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI { return this; } + protected getTankFilterIds( + name: string, + definitionKey: string, + filter: "include" | "exclude", + ) { + const keys = name.trim().split(/\s+/).filter(Boolean); + + return keys.flatMap((key) => { + const id = this.tankStringIdMap[key]; + + if (id === undefined) { + console.warn( + `Skipping unavailable tank "${key}" in ${filter} vehicle filter for "${definitionKey}"`, + ); + return []; + } + + return [id]; + }); + } + protected getString(name: string) { const collection: Record = { [locales.default]: this.stringsI18n[locales.default][name], @@ -264,6 +285,8 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI { collection[locale] = localizedString; } + collection[locales.default] = collection[locales.default] ?? name; + return { locales: collection } satisfies I18nString; } } diff --git a/packages/core/src/blitzkit/api/server/1_tankDefinitions.ts b/packages/core/src/blitzkit/api/server/1_tankDefinitions.ts index 9e211001b..298e84df4 100644 --- a/packages/core/src/blitzkit/api/server/1_tankDefinitions.ts +++ b/packages/core/src/blitzkit/api/server/1_tankDefinitions.ts @@ -387,7 +387,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 { : TankType.TANK_TYPE_RESEARCHABLE, tier: tank.level, class: tankClass, - testing: tankTags.includes("testTank"), + testing: tankTags.includes("testTank") || tank.level === 11, deprecated: tankTags.includes("deprecated"), price: tankPrice, camouflage_still: tankDefinition.root.invisibility.still, @@ -595,9 +595,25 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 { for (const shellKey in gunListEntry.shots) { const gunShellEntry = gunListEntry.shots[shellKey]; const shell = shellList.root[shellKey]; + + // Newer client data stores shot properties on the shell rather + // than repeating them under every gun's entry. + const speed = + typeof gunShellEntry === "string" + ? shell.speed + : gunShellEntry.speed; + const maxDistance = + typeof gunShellEntry === "string" + ? shell.maxDistance + : gunShellEntry.maxDistance; + const piercingPower = + typeof gunShellEntry === "string" + ? shell.piercingPower + : gunShellEntry.piercingPower; + const shellId = toUniqueId(nation, shell.id); const shellName = this.getString(shell.userString); - const penetrationRaw = gunShellEntry.piercingPower + const penetrationRaw = piercingPower .split(" ") .filter((penetrationString) => penetrationString !== "") .map(Number); @@ -607,7 +623,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 { ].shells.push({ id: shellId, name: shellName, - velocity: gunShellEntry.speed, + velocity: speed, armor_damage: shell.damage.armor, module_damage: shell.damage.devices, caliber: shell.caliber, @@ -623,7 +639,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 { near: penetrationRaw[0], far: penetrationRaw[1], }, - range: gunShellEntry.maxDistance, + range: maxDistance, }); } diff --git a/packages/core/src/blitzkit/api/server/2_camouflageDefinitions.ts b/packages/core/src/blitzkit/api/server/2_camouflageDefinitions.ts index 30ce31d6a..4462f1010 100644 --- a/packages/core/src/blitzkit/api/server/2_camouflageDefinitions.ts +++ b/packages/core/src/blitzkit/api/server/2_camouflageDefinitions.ts @@ -1,15 +1,60 @@ -import { CamouflageDefinitions } from "@blitzkit/core"; +import { + CamouflageDefinitions, + Sc2ReadStream, + type TankParameters, +} from "@blitzkit/core"; +import { isEqual, times } from "lodash-es"; import { Cache } from "./0_base"; import { ServerBlitzKitAPI1 } from "./1_tankDefinitions"; export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 { + private async skinPresets(vehicle: string) { + const [nation, tankKey] = vehicle.split(":"); + const parameters = await this.vfs.yaml( + `Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`, + ); + const sc2 = new Sc2ReadStream( + ( + await this.vfs.file( + `Data/3d/${parameters.resourcesPath.blitzModelPath}`, + ) + ).buffer as ArrayBuffer, + ).sc2(); + const presets = new Set(); + + for (const node of sc2["#dataNodes"]) { + if (typeof node.configCount !== "number") continue; + + times(node.configCount, (index) => node[`configArchive_${index}`]) + .filter( + (archive) => + !isEqual(archive.textures, node.configArchive_0.textures), + ) + .forEach((archive) => presets.add(archive.configName)); + } + + return presets; + } + @Cache() async camouflageDefinitions() { const camouflageDefinitions = CamouflageDefinitions.create(); + const customization = await this.vfs.yaml< + Record + >("Data/3d/Customization.yaml"); + const items = new Map( + Object.values(customization).map(({ Name, Path }) => [ + Name, + Path.replace("~res:/", "Data/").replace(/\.sc2$/, ""), + ]), + ); + const skinPresets = new Map>>(); for (const camoKey in this.camouflagesXml!.root.camouflages) { const camo = this.camouflagesXml!.root.camouflages[camoKey]; + if (!(camoKey in this.camouflagesYaml!)) continue; + const yamlEntry = this.camouflagesYaml![camoKey]; const fullName = yamlEntry.userString ? this.getString(yamlEntry.userString) @@ -20,12 +65,57 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 { const resolvedTankName = shortName ?? fullName; const resolvedTankNameFull = resolvedTankName === fullName ? undefined : fullName; + const include = camo.vehicleFilter.include; + const vehicle = + include && !Array.isArray(include) && "vehicle" in include + ? include.vehicle?.name + : undefined; + let tankId: number | undefined = undefined; + + if (vehicle && vehicle in this.tankStringIdMap) { + const resolvedParts = await Promise.all( + Object.values(yamlEntry.customEntities ?? {}).map( + async ({ item }) => { + const path = items.get(item); + + return ( + path !== undefined && + (await this.vfs.resolve(`${path}.sc2`)) && + (await this.vfs.resolve(`${path}.scg`)) + ); + }, + ), + ); + + const [nation, tankKey] = vehicle.split(":"); + // some unique camos only set the preset for their one tank + const preset = + yamlEntry.preset ?? + yamlEntry.override?.[nation]?.override?.[tankKey]?.preset; + let hasPreset = false; + + if (preset !== undefined) { + if (!skinPresets.has(vehicle)) { + skinPresets.set(vehicle, this.skinPresets(vehicle)); + } + + hasPreset = (await skinPresets.get(vehicle)!).has(preset); + } + + if (resolvedParts.some(Boolean) || hasPreset) { + tankId = this.tankStringIdMap[vehicle]; + } + } + + const name = this.getString(camo.userString); camouflageDefinitions.camouflages[camo.id] = { id: camo.id, - name: this.getString(camo.userString), + name, tank_name: resolvedTankName, tank_name_full: resolvedTankNameFull, + tank_id: tankId, + attachment: camo.kind === "customization", }; } diff --git a/packages/core/src/blitzkit/api/server/3_modelDefinitions.ts b/packages/core/src/blitzkit/api/server/3_modelDefinitions.ts index 3e6f7f590..df5d3b48a 100644 --- a/packages/core/src/blitzkit/api/server/3_modelDefinitions.ts +++ b/packages/core/src/blitzkit/api/server/3_modelDefinitions.ts @@ -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( `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), }, }; diff --git a/packages/core/src/blitzkit/api/server/6_consumableDefinitions.ts b/packages/core/src/blitzkit/api/server/6_consumableDefinitions.ts index f4b333f61..d12b7612d 100644 --- a/packages/core/src/blitzkit/api/server/6_consumableDefinitions.ts +++ b/packages/core/src/blitzkit/api/server/6_consumableDefinitions.ts @@ -44,9 +44,7 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 { filter_type: { $case: "ids", value: { - ids: includeRaw.name.split(/ +/).map((key) => { - return this.tankStringIdMap[key]; - }), + ids: this.getTankFilterIds(includeRaw.name, key, "include"), }, }, }); @@ -72,9 +70,7 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 { filter_type: { $case: "ids", value: { - ids: excludeRaw.name.split(/ +/).map((key) => { - return this.tankStringIdMap[key]; - }), + ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"), }, }, }); diff --git a/packages/core/src/blitzkit/api/server/7_provisionDefinitions.ts b/packages/core/src/blitzkit/api/server/7_provisionDefinitions.ts index 32db2bcc1..d7baf6135 100644 --- a/packages/core/src/blitzkit/api/server/7_provisionDefinitions.ts +++ b/packages/core/src/blitzkit/api/server/7_provisionDefinitions.ts @@ -11,7 +11,7 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 { async provisionDefinitions() { const provisionDefinitions = ProvisionDefinitions.create(); - Object.entries(this.provisionsCommon).forEach(([, provision]) => { + Object.entries(this.provisionsCommon).forEach(([key, provision]) => { const entry: Provision = { id: provision.id, exclude: [], @@ -42,9 +42,7 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 { filter_type: { $case: "ids", value: { - ids: includeRaw.name.split(/ +/).map((key) => { - return this.tankStringIdMap[key]; - }), + ids: this.getTankFilterIds(includeRaw.name, key, "include"), }, }, }); @@ -70,9 +68,7 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 { filter_type: { $case: "ids", value: { - ids: excludeRaw.name - .split(/ +/) - .map((key) => this.tankStringIdMap[key]), + ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"), }, }, }); diff --git a/packages/core/src/blitzkit/index.ts b/packages/core/src/blitzkit/index.ts index 700e57018..20298bcee 100644 --- a/packages/core/src/blitzkit/index.ts +++ b/packages/core/src/blitzkit/index.ts @@ -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"; diff --git a/packages/core/src/blitzkit/tankBoundingBox.ts b/packages/core/src/blitzkit/tankBoundingBox.ts new file mode 100644 index 000000000..9e3408d74 --- /dev/null +++ b/packages/core/src/blitzkit/tankBoundingBox.ts @@ -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, + }, + }); +} diff --git a/packages/core/src/blitzkit/tankDefinitions/constants.ts b/packages/core/src/blitzkit/tankDefinitions/constants.ts index 9671b938a..a92e56743 100644 --- a/packages/core/src/blitzkit/tankDefinitions/constants.ts +++ b/packages/core/src/blitzkit/tankDefinitions/constants.ts @@ -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.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.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.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.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 = { 1: "I", @@ -68,8 +41,3 @@ export const flags: Record = { european: "🇪🇺", other: "<:other:1218421572243558482>", }; -export const TREE_TYPE_ICONS: Record> = { - [TankType.TANK_TYPE_RESEARCHABLE]: TANK_ICONS, - [TankType.TANK_TYPE_PREMIUM]: TANK_ICONS_PREMIUM, - [TankType.TANK_TYPE_COLLECTOR]: TANK_ICONS_COLLECTOR, -}; diff --git a/packages/core/src/imgur/imgur.ts b/packages/core/src/imgur/imgur.ts deleted file mode 100644 index 8135d678c..000000000 --- a/packages/core/src/imgur/imgur.ts +++ /dev/null @@ -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) { - const options = { ...defaultImgurOptions, ...imgurOptions }; - - return `https://i.imgur.com/${id}${options.size}.${options.format}`; -} diff --git a/packages/core/src/imgur/index.ts b/packages/core/src/imgur/index.ts deleted file mode 100644 index 6daa175fb..000000000 --- a/packages/core/src/imgur/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './imgur'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5550ecc1a..cb8712d9a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,5 @@ export * from "./blitz"; export * from "./blitzkit"; -export * from "./imgur"; export * from "./math"; export * from "./patreon"; export * from "./protobuf"; diff --git a/packages/core/src/protos/camouflage_definitions.proto b/packages/core/src/protos/camouflage_definitions.proto index b3b9bbbac..2bbf5e2c3 100644 --- a/packages/core/src/protos/camouflage_definitions.proto +++ b/packages/core/src/protos/camouflage_definitions.proto @@ -13,4 +13,6 @@ message Camouflage { required I18nString name = 2; optional I18nString tank_name = 3; optional I18nString tank_name_full = 4; + optional uint32 tank_id = 5; + optional bool attachment = 6; } \ No newline at end of file diff --git a/packages/core/src/protos/camouflage_definitions.ts b/packages/core/src/protos/camouflage_definitions.ts index ca6c2f9fa..14466cb1c 100644 --- a/packages/core/src/protos/camouflage_definitions.ts +++ b/packages/core/src/protos/camouflage_definitions.ts @@ -24,6 +24,8 @@ export interface Camouflage { name: I18nString | undefined; tank_name?: I18nString | undefined; tank_name_full?: I18nString | undefined; + tank_id?: number | undefined; + attachment?: boolean | undefined; } function createBaseCamouflageDefinitions(): CamouflageDefinitions { @@ -191,7 +193,7 @@ export const CamouflageDefinitions_CamouflagesEntry: MessageFns = { @@ -208,6 +210,12 @@ export const Camouflage: MessageFns = { if (message.tank_name_full !== undefined) { I18nString.encode(message.tank_name_full, writer.uint32(34).fork()).join(); } + if (message.tank_id !== undefined && message.tank_id !== 0) { + writer.uint32(40).uint32(message.tank_id); + } + if (message.attachment !== undefined && message.attachment !== false) { + writer.uint32(48).bool(message.attachment); + } return writer; }, @@ -250,6 +258,22 @@ export const Camouflage: MessageFns = { message.tank_name_full = I18nString.decode(reader, reader.uint32()); continue; } + case 5: { + if (tag !== 40) { + break; + } + + message.tank_id = reader.uint32(); + continue; + } + case 6: { + if (tag !== 48) { + break; + } + + message.attachment = reader.bool(); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -265,6 +289,8 @@ export const Camouflage: MessageFns = { name: isSet(object.name) ? I18nString.fromJSON(object.name) : undefined, tank_name: isSet(object.tank_name) ? I18nString.fromJSON(object.tank_name) : undefined, tank_name_full: isSet(object.tank_name_full) ? I18nString.fromJSON(object.tank_name_full) : undefined, + tank_id: isSet(object.tank_id) ? globalThis.Number(object.tank_id) : 0, + attachment: isSet(object.attachment) ? globalThis.Boolean(object.attachment) : false, }; }, @@ -282,6 +308,12 @@ export const Camouflage: MessageFns = { if (message.tank_name_full !== undefined) { obj.tank_name_full = I18nString.toJSON(message.tank_name_full); } + if (message.tank_id !== undefined && message.tank_id !== 0) { + obj.tank_id = Math.round(message.tank_id); + } + if (message.attachment !== undefined && message.attachment !== false) { + obj.attachment = message.attachment; + } return obj; }, @@ -300,6 +332,8 @@ export const Camouflage: MessageFns = { message.tank_name_full = (object.tank_name_full !== undefined && object.tank_name_full !== null) ? I18nString.fromPartial(object.tank_name_full) : undefined; + message.tank_id = object.tank_id ?? 0; + message.attachment = object.attachment ?? false; return message; }, }; diff --git a/packages/core/src/streams/pvr.ts b/packages/core/src/streams/pvr.ts index 8703f5cd7..d55e8aaad 100644 --- a/packages/core/src/streams/pvr.ts +++ b/packages/core/src/streams/pvr.ts @@ -88,6 +88,7 @@ enum PvrChannelType { enum ResolvedBitRatePixelFormat { R4G4B4A4, + R8G8B8A8, R5G6B5A0, } @@ -137,6 +138,27 @@ export class PvrReadStream extends ReadStream { }; } + case ResolvedBitRatePixelFormat.R8G8B8A8: { + const data = Buffer.alloc(pixelCount * 4); + + times(pixelCount, (index) => { + const buffer = this.consumeUint8Array(4); + const bufferIndex = index * 4; + + data[bufferIndex] = buffer[0]; // R + data[bufferIndex + 1] = buffer[1]; // G + data[bufferIndex + 2] = buffer[2]; // B + data[bufferIndex + 3] = buffer[3]; // A + }); + + return { + data, + width: header.width, + height: header.height, + channels: 4 as const, + }; + } + case ResolvedBitRatePixelFormat.R5G6B5A0: { const data = Buffer.alloc(pixelCount * 3); @@ -208,10 +230,13 @@ export class PvrReadStream extends ReadStream { if (pixelFormat.order === "rgba") { if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) { return ResolvedBitRatePixelFormat.R4G4B4A4; - } else + } else if (isEqual(pixelFormat.bitRates, [8, 8, 8, 8])) { + return ResolvedBitRatePixelFormat.R8G8B8A8; + } else { throw new TypeError( `Unhandled rgba bit rate ${pixelFormat.bitRates.join(", ")}`, ); + } } else if (pixelFormat.order === "rgb\0") { if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) { return ResolvedBitRatePixelFormat.R5G6B5A0; diff --git a/packages/core/src/streams/sc2.ts b/packages/core/src/streams/sc2.ts index 8b4837bab..db490d602 100644 --- a/packages/core/src/streams/sc2.ts +++ b/packages/core/src/streams/sc2.ts @@ -40,6 +40,8 @@ export interface ConfigArchive { interface EnabledPresets { AlphaTest: boolean; + AlphaBlend?: boolean; + AlphaBlendAdditive?: boolean; } interface Flags { @@ -90,6 +92,28 @@ export interface Hierarchy { name: string; } +export function parseHierarchies( + 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; diff --git a/packages/core/src/types/camouflagesYaml.ts b/packages/core/src/types/camouflagesYaml.ts index 7671432fe..35d1531ca 100644 --- a/packages/core/src/types/camouflagesYaml.ts +++ b/packages/core/src/types/camouflagesYaml.ts @@ -3,5 +3,12 @@ export type CamouflagesYaml = Record< { userString?: string; shortUserString?: string; + preset?: string; + customEntities?: Record; + // nation -> tank + override?: Record< + string, + { override?: Record } + >; } >; diff --git a/packages/core/src/types/gunDefinitionsList.ts b/packages/core/src/types/gunDefinitionsList.ts index 005238d10..51b2c939d 100644 --- a/packages/core/src/types/gunDefinitionsList.ts +++ b/packages/core/src/types/gunDefinitionsList.ts @@ -20,11 +20,13 @@ export interface GunDefinitionsList { burst?: { count: number; rate: number }; clip?: { count: number; rate: number }; shots: { - [key: string]: { - speed: number; - piercingPower: string; - maxDistance: number; - }; + [key: string]: + | "" + | { + speed: number; + piercingPower: string; + maxDistance: number; + }; }; }; }; diff --git a/packages/core/src/types/shellDefinitionsList.ts b/packages/core/src/types/shellDefinitionsList.ts index d9e5f3e6e..e8ec812c1 100644 --- a/packages/core/src/types/shellDefinitionsList.ts +++ b/packages/core/src/types/shellDefinitionsList.ts @@ -10,6 +10,9 @@ export type ShellDefinitionsList = Record< normalizationAngle?: number; ricochetAngle?: number; explosionRadius?: number; + speed: number; + maxDistance?: number; + piercingPower: string; } > & { icons: Record; diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 9a5c5dea1..c4c2967c7 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -1,10 +1,4 @@ { - "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}.", - "fox": "The quick brown fox jumps over the lazy dog." - }, - "common": { "name": "BlitzKit", @@ -44,7 +38,7 @@ "branches": { "beta": "Beta", - "development": "Development", + "development": "Post Malone Edition", "opentest": "OpenTest", "preview": "Preview", "production": "Production" @@ -338,6 +332,13 @@ }, "tools": { + "credits": { + "name": "Credits", + "button": "View people", + "description": "Contributors of BlitzKit", + "seo_description": "BlitzKit is the work of many" + }, + "playlist": { "reset": "Reset", "shuffle": "Shuffle", @@ -993,11 +994,10 @@ "footer": { "motto": "Everything World of Tanks Blitz", - "copyright": "Unaffiliated with Wargaming or any service", + "association": "An Open Collective Project", "about": { "title": "About", - "credits": "Credits", "discord": "Discord", "github": "GitHub", "patches": "Patches" diff --git a/packages/i18n/strings/es.json b/packages/i18n/strings/es.json index e877464d5..305f67ba3 100644 --- a/packages/i18n/strings/es.json +++ b/packages/i18n/strings/es.json @@ -1,9 +1,4 @@ { - "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}.", - "fox": "El rápido zorro marrón salta sobre el perro perezoso." - }, "common": { "name": "BlitzKit", "units": { @@ -307,6 +302,12 @@ } }, "tools": { + "credits": { + "name": "Credits", + "button": "View people", + "description": "Contributors of BlitzKit", + "seo_description": "BlitzKit is the work of many" + }, "playlist": { "reset": "Restablecer", "shuffle": "Shuffle", @@ -623,6 +624,7 @@ "hullTraverseSoftTerrain": "Suave", "health": "Salud", "fireChance": "Probabilidad de incendio", + "trackRepairTime": "Track repair time", "viewRange": "Rango de visión", "camouflageStill": "Quieto", "camouflageMoving": "En movimiento", @@ -921,7 +923,6 @@ "copyright": "No afiliado con Wargaming u otros servicios", "about": { "title": "About", - "credits": "Credits", "discord": "Discord", "github": "GitHub", "patches": "Parches" diff --git a/packages/i18n/strings/fr.json b/packages/i18n/strings/fr.json index ba01e0146..b0d2919aa 100644 --- a/packages/i18n/strings/fr.json +++ b/packages/i18n/strings/fr.json @@ -1,9 +1,4 @@ { - "test": { - "default": "teste", - "long": "Ceci est une longue chaîne de test. Vous ne DEVEZ JAMAIS le voir dans l'interface utilisateur. Mon fruit préféré est {fruit}, car il a la couleur {color}. Je l'aime tellement que je vais dire son nom 3 fois ! {fruit}! {fruit}! {fruit} ! Encore une fois, c'est {color}, pas {not_color}.", - "fox": "Le renard brun et rapide saute par-dessus le chien paresseux." - }, "common": { "name": "BlitzKit", "units": { @@ -307,6 +302,12 @@ } }, "tools": { + "credits": { + "name": "Credits", + "button": "View people", + "description": "Contributors of BlitzKit", + "seo_description": "BlitzKit is the work of many" + }, "playlist": { "reset": "Reset", "shuffle": "Shuffle", @@ -623,6 +624,7 @@ "hullTraverseSoftTerrain": "Soft", "health": "Health", "fireChance": "Fire chance", + "trackRepairTime": "Track repair time", "viewRange": "View range", "camouflageStill": "Still", "camouflageMoving": "Moving", @@ -921,7 +923,6 @@ "copyright": "Unaffiliated with Wargaming or any service", "about": { "title": "About", - "credits": "Credits", "discord": "Discord", "github": "GitHub", "patches": "Patches" diff --git a/packages/i18n/strings/ja.json b/packages/i18n/strings/ja.json index f56d1986f..b98faba62 100644 --- a/packages/i18n/strings/ja.json +++ b/packages/i18n/strings/ja.json @@ -1,9 +1,4 @@ { - "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}.", - "fox": "すばしっこい茶色のキツネが怠け者の犬を飛び越した。" - }, "common": { "name": "BlitzKit", "units": { @@ -307,6 +302,12 @@ } }, "tools": { + "credits": { + "name": "Credits", + "button": "View people", + "description": "Contributors of BlitzKit", + "seo_description": "BlitzKit is the work of many" + }, "playlist": { "reset": "リセット", "shuffle": "Shuffle", @@ -623,6 +624,7 @@ "hullTraverseSoftTerrain": "軟弱地", "health": "耐久値", "fireChance": "火災発生率", + "trackRepairTime": "Track repair time", "viewRange": "視認範囲", "camouflageStill": "静止時", "camouflageMoving": "移動時", @@ -921,7 +923,6 @@ "copyright": "Wargamingやその他のサービスとは関係ありません", "about": { "title": "概要", - "credits": "クレジット", "discord": "Discord", "github": "GitHub", "patches": "パッチノート" diff --git a/packages/i18n/strings/pl.json b/packages/i18n/strings/pl.json index 447d389b3..f3aecaf32 100644 --- a/packages/i18n/strings/pl.json +++ b/packages/i18n/strings/pl.json @@ -1,9 +1,4 @@ { - "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}.", - "fox": "Szybki brązowy lis przeskoczył nad leniwym psem." - }, "common": { "name": "BlitzKit", "units": { @@ -19,7 +14,7 @@ "gun_types": { "auto_loader": "Samo ładowacz", "auto_reloader": "Automatyczne przeładowanie", - "regular": "Regular" + "regular": "Regularny" }, "shells": { "ap": "AP", @@ -38,10 +33,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 +97,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 +182,229 @@ }, "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": { + "credits": { + "name": "Credits", + "button": "View people", + "description": "Contributors of BlitzKit", + "seo_description": "BlitzKit is the work of many" + }, "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 +444,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 +521,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 +558,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 +624,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 +649,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 +657,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 +674,7 @@ "title": "Add tanks", "description": "Select tanks to add to comparison" }, - "clear": "Clear", + "clear": "Wyczyść", "deltas": { "none": "No deltas", "percentage": "Percentage", @@ -685,17 +687,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 +762,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 +793,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 +829,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 +840,7 @@ }, "customize": { "title": "Customize", - "reset": "Reset", + "reset": "Zresetuj", "description": "Bells and whistles of your embed" } }, @@ -921,7 +923,6 @@ "copyright": "Unaffiliated with Wargaming or any service", "about": { "title": "About", - "credits": "Credits", "discord": "Discord", "github": "GitHub", "patches": "Patches" diff --git a/packages/i18n/strings/pt.json b/packages/i18n/strings/pt.json index 757530039..734898152 100644 --- a/packages/i18n/strings/pt.json +++ b/packages/i18n/strings/pt.json @@ -1,9 +1,4 @@ { - "test": { - "default": "teste", - "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}.", - "fox": "A ágil raposa castanha salta por cima do cão preguiçoso." - }, "common": { "name": "BlitzKit", "units": { @@ -41,7 +36,7 @@ "development": "Development", "opentest": "TesteAberto", "preview": "Preview", - "production": "Production" + "production": "Produção" }, "locales": { "en": "Inglês", @@ -187,11 +182,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 +202,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", @@ -307,6 +302,12 @@ } }, "tools": { + "credits": { + "name": "Credits", + "button": "View people", + "description": "Contributors of BlitzKit", + "seo_description": "BlitzKit is the work of many" + }, "playlist": { "reset": "Restaurar", "shuffle": "Shuffle", @@ -623,6 +624,7 @@ "hullTraverseSoftTerrain": "Macio", "health": "Vida", "fireChance": "Chance de incêndio", + "trackRepairTime": "Track repair time", "viewRange": "Alcance de visão", "camouflageStill": "Parado", "camouflageMoving": "Em movimento", @@ -921,7 +923,6 @@ "copyright": "Não afiliado à Wargaming ou qualquer serviço", "about": { "title": "Sobre", - "credits": "Créditos", "discord": "Discord", "github": "GitHub", "patches": "Atualizações" diff --git a/packages/i18n/strings/ru.json b/packages/i18n/strings/ru.json index c887147c7..d0c4afa3b 100644 --- a/packages/i18n/strings/ru.json +++ b/packages/i18n/strings/ru.json @@ -1,9 +1,4 @@ { - "test": { - "default": "тест", - "long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт — {fruit}, потому что это цвет {color}. Он мне так нравится, так что я скажу его 3 раза {fruit}! {fruit}! {fruit}! И ещё раз, это {color}, а не {not_color}.", - "fox": "Съешь же ещё этих мягких французских булок, да выпей чаю." - }, "common": { "name": "BlitzKit", "units": { @@ -38,10 +33,10 @@ }, "branches": { "beta": "Бета", - "development": "Development", + "development": "Разработка", "opentest": "Открытый тест", - "preview": "Preview", - "production": "Production" + "preview": "Предпросмотр", + "production": "Производство" }, "locales": { "en": "Английский", @@ -194,7 +189,7 @@ "title": "Настройки", "description": "Изменить настройки BlitzKit", "dev_mode": "Режим разработчика", - "auto_load_models": "Auto-load models", + "auto_load_models": "Автозагрузка моделей", "language": "Язык" }, "common": { @@ -307,6 +302,12 @@ } }, "tools": { + "credits": { + "name": "Credits", + "button": "View people", + "description": "Contributors of BlitzKit", + "seo_description": "BlitzKit is the work of many" + }, "playlist": { "reset": "Сброс", "shuffle": "Перемешать", @@ -413,7 +414,7 @@ } }, "sandbox": { - "load_model": "Load model", + "load_model": "Загрузить модель", "poses": { "hull_down": "За холмом", "face_hug": "В упор", @@ -574,7 +575,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 +624,7 @@ "hullTraverseSoftTerrain": "На мягком грунте", "health": "Прочность", "fireChance": "Вероятность возгорания", + "trackRepairTime": "Track repair time", "viewRange": "Радиус обзора", "camouflageStill": "Когда неподвижен", "camouflageMoving": "В движении", @@ -921,7 +923,6 @@ "copyright": "Не связан с Wargaming или иным сервисом", "about": { "title": "О сайте", - "credits": "Авторы", "discord": "Discord", "github": "GitHub", "patches": "Патчи" diff --git a/packages/i18n/strings/uk.json b/packages/i18n/strings/uk.json index 1cf6305fe..a5d65f9f2 100644 --- a/packages/i18n/strings/uk.json +++ b/packages/i18n/strings/uk.json @@ -1,9 +1,4 @@ { - "test": { - "default": "тест", - "long": "{color}{fruit}", - "fox": "Швидка бура лисиця перестрибує через лінивого собаку." - }, "common": { "name": "BlitzKit", "units": { @@ -307,6 +302,12 @@ } }, "tools": { + "credits": { + "name": "Credits", + "button": "View people", + "description": "Contributors of BlitzKit", + "seo_description": "BlitzKit is the work of many" + }, "playlist": { "reset": "Скинути", "shuffle": "Перемішати", @@ -364,7 +365,7 @@ "title": "Partially reveal tank?", "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", "cancel": "", - "reveal": "Reveal" + "reveal": "Рівень" }, "clear": "Clear", "select_all": "Вибрати все", @@ -623,6 +624,7 @@ "hullTraverseSoftTerrain": "Soft", "health": "Здоров'я", "fireChance": "Fire chance", + "trackRepairTime": "Track repair time", "viewRange": "View range", "camouflageStill": "Нерухомо", "camouflageMoving": "У русі", @@ -921,7 +923,6 @@ "copyright": "Unaffiliated with Wargaming or any service", "about": { "title": "About", - "credits": "Credits", "discord": "Discord", "github": "GitHub", "patches": "Patches" diff --git a/packages/i18n/strings/zh.json b/packages/i18n/strings/zh.json index 3ab253b45..2e9b4be5d 100644 --- a/packages/i18n/strings/zh.json +++ b/packages/i18n/strings/zh.json @@ -1,9 +1,4 @@ { - "test": { - "default": "测试", - "long": "这是一个测试用长字符串。你绝对不会在用户界面中看见这些。我最喜欢的水果是{fruit}因为它的颜色是{color}的。我如此喜欢它,以至于我会把它的名字念3次!{fruit}!{fruit}!{fruit}!重申一次,它是{color}的而不是{not_color}的。", - "fox": "褐色的狐狸迅速地跳过那只懒狗" - }, "common": { "name": "BlitzKit", "units": { @@ -307,6 +302,12 @@ } }, "tools": { + "credits": { + "name": "Credits", + "button": "View people", + "description": "Contributors of BlitzKit", + "seo_description": "BlitzKit is the work of many" + }, "playlist": { "reset": "重置", "shuffle": "换一批", @@ -623,6 +624,7 @@ "hullTraverseSoftTerrain": "水面上车转速率", "health": "血量", "fireChance": "发动机受击时着火概率", + "trackRepairTime": "Track repair time", "viewRange": "视野范围", "camouflageStill": "静止隐蔽", "camouflageMoving": "移动时", @@ -921,7 +923,6 @@ "copyright": "本网站与 Wargaming战游网 或任何服务无关", "about": { "title": "关于", - "credits": "鸣谢", "discord": "Discord", "github": "GitHub", "patches": "补丁" diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 70765e5f0..4afa98947 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -5,7 +5,9 @@ "discover-ids": "bun src/discoverIds/index.ts", "build-protos": "bun src/protos.ts", "record-performance": "bun src/recordPerformance/index.ts", - "download-client": "bun src/downloadClient/index.ts" + "download-client": "bun src/downloadClient/index.ts", + "download-smart-dlc": "bun src/downloadSmartDLC/index.ts", + "vfs": "bun src/vfs/index.ts" }, "dependencies": { "@blitzkit/closed": "workspace:*", diff --git a/packages/scripts/src/downloadClient/index.ts b/packages/scripts/src/downloadClient/index.ts index e3412bffe..c9eb68b61 100644 --- a/packages/scripts/src/downloadClient/index.ts +++ b/packages/scripts/src/downloadClient/index.ts @@ -9,6 +9,8 @@ import { } from "./constants"; import { QuickGitHubRelease } from "./types"; +const DEPOT_DOWNLOADER_RETRY_TIME = 30_000; + const provider = assertSecret(import.meta.env.CLIENT_PROVIDER); const source = assertSecret(import.meta.env.CLIENT_SOURCE); const dir = assertSecret(import.meta.env.CLIENT_DIR); @@ -43,11 +45,27 @@ if (provider === "steam") { const username = assertSecret(import.meta.env.STEAM_USERNAME); const password = assertSecret(import.meta.env.STEAM_PASSWORD); - await $`${DEPOT_DOWNLOADER_EXECUTABLE} -os ${ - platform - } -app ${id} -username ${username} -password ${ - password - } -dir ${dir} -max-downloads ${DEPOT_DOWNLOADER_MAX_DOWNLOADS}`; + while (true) { + const result = await $`${ + DEPOT_DOWNLOADER_EXECUTABLE + } -os ${platform} -app ${id} -username ${ + username + } -password ${password} -dir ${dir} -max-downloads ${ + DEPOT_DOWNLOADER_MAX_DOWNLOADS + }`.nothrow(); + + if (result.exitCode === 0) { + console.log("Client downloaded successfully!"); + break; + } + + console.error( + `DepotDownloader failed (exit ${result.exitCode}). Retrying in ${DEPOT_DOWNLOADER_RETRY_TIME / 1000} seconds...`, + ); + await new Promise((resolve) => + setTimeout(resolve, DEPOT_DOWNLOADER_RETRY_TIME), + ); + } } else if (provider === "zip") { const url = assertSecret(import.meta.env.CLIENT_SOURCE); diff --git a/packages/scripts/src/applyPatches.ts b/packages/scripts/src/downloadSmartDLC/index.ts similarity index 60% rename from packages/scripts/src/applyPatches.ts rename to packages/scripts/src/downloadSmartDLC/index.ts index 18aaf6ba3..24b13f068 100644 --- a/packages/scripts/src/applyPatches.ts +++ b/packages/scripts/src/downloadSmartDLC/index.ts @@ -2,9 +2,16 @@ import { dvp } from "@blitzkit/closed"; import { assertSecret } from "@blitzkit/core"; import { mkdir, writeFile } from "fs/promises"; import { parse as parsePath } from "path"; -import ProgressBar from "progress"; import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; -import { PATCHES_ROOT, vfs } from "./buildAssets/constants"; +import { RegionsPublish } from "../types/regionsPublish"; +import { vfs } from "../vfs/vfs"; + +const CLIENT_DIR = assertSecret(import.meta.env.CLIENT_DIR); + +const regionsPublish = await vfs.yaml( + "Data/regions_publish.yaml", +); +const [smartDlcUrl] = regionsPublish.Common.smartDlcUrls; const versionTextFile = await vfs.text(`Data/version.txt`); const currentVersion = versionTextFile @@ -18,9 +25,7 @@ console.log(`Installing patches for ${currentVersion}...`); let patchIndex = 1; while (true) { const response = await fetch( - `${assertSecret( - import.meta.env.WOTB_DLC_CDN, - )}/dlc/s${currentVersion}_${patchIndex}.yaml`, + `${smartDlcUrl}/s${currentVersion}_${patchIndex}.yaml`, ); console.log(patchIndex, response.status); @@ -30,51 +35,34 @@ while (true) { console.log(`Applying patch ${patchIndex}...`); const data = parseYaml(await response.text()); - const dvpm = await fetch( - `${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11}`, - ).then((response) => response.arrayBuffer()); + const dvpm = await fetch(`${smartDlcUrl}/${data.dx11}`).then((response) => + response.arrayBuffer(), + ); const dvpd = await fetch( - `${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11.replace( - ".dvpm", - ".dvpd", - )}`, + `${smartDlcUrl}/${data.dx11.replace(".dvpm", ".dvpd")}`, ).then((response) => response.arrayBuffer()); const files = await dvp(dvpm, dvpd); - const bar = new ProgressBar( - `Patching ${files.length} files :bar`, - files.length, - ); for (const { path, data } of files) { console.log(`Patching "${path}"...`); const { dir } = parsePath(path); - try { - await mkdir(`${PATCHES_ROOT}/Data/${dir}`, { recursive: true }); - } catch (error) { - console.warn(`Failed to make directory "${dir}"`); - } + await mkdir(`${CLIENT_DIR}/Data/${dir}`, { recursive: true }); const buffer = Buffer.from(data); - await writeFile(`${PATCHES_ROOT}/Data/${path}`, new Uint8Array(buffer)); - - bar.tick(); + await writeFile(`${CLIENT_DIR}/Data/${path}`, new Uint8Array(buffer)); } console.log("patches done for patch"); if ("dynamicContentLocalizationsDir" in data) { console.log( - `${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${ - data.dynamicContentLocalizationsDir - }/en.yaml`, + `${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`, ); const localizationsResponse = await fetch( - `${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${ - data.dynamicContentLocalizationsDir - }/en.yaml`, + `${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`, ); console.log("fetched"); const newStrings = parseYaml(await localizationsResponse.text()); @@ -86,10 +74,10 @@ while (true) { const patchedContent = stringifyYaml(patchedStrings); console.log("stringified"); - await mkdir(`${PATCHES_ROOT}/Data/Strings`, { recursive: true }); + await mkdir(`${CLIENT_DIR}/Data/Strings`, { recursive: true }); console.log("mkdir"); await writeFile( - `${PATCHES_ROOT}/Data/Strings/en.yaml`, + `${CLIENT_DIR}/Data/Strings/en.yaml`, new Uint8Array(Buffer.from(patchedContent)), ); console.log("file"); diff --git a/packages/scripts/src/types/regionsPublish.ts b/packages/scripts/src/types/regionsPublish.ts new file mode 100644 index 000000000..728de0f14 --- /dev/null +++ b/packages/scripts/src/types/regionsPublish.ts @@ -0,0 +1,7 @@ +export interface RegionsPublish { + Common: Common; +} + +interface Common { + smartDlcUrls: string[]; +} diff --git a/packages/scripts/src/vfs/index.ts b/packages/scripts/src/vfs/index.ts new file mode 100644 index 000000000..cdf8b1fe8 --- /dev/null +++ b/packages/scripts/src/vfs/index.ts @@ -0,0 +1,23 @@ +import { vfs } from "./vfs"; + +const command = Bun.argv[2]; +const path = Bun.argv[3]; + +if (!path) { + console.error("Gimme a path to assess dumb ass"); + process.exit(1); +} + +if (command === "cat") { + const content = await vfs.file(path); + await Bun.write(Bun.stdout, content); +} else if (command === "ls") { + const files = await vfs.dir(path); + + for (const file of files) { + console.log(file); + } +} else if (command === "yaml") { + const content = await vfs.yaml(path); + await Bun.write(Bun.stdout, JSON.stringify(content, null, 2)); +} diff --git a/packages/scripts/src/vfs/vfs.ts b/packages/scripts/src/vfs/vfs.ts new file mode 100644 index 000000000..a94b39bae --- /dev/null +++ b/packages/scripts/src/vfs/vfs.ts @@ -0,0 +1,4 @@ +import { assertSecret } from "@blitzkit/core"; +import { LocalVFS } from "@blitzkit/core/src/blitzkit/vfs/local"; + +export const vfs = new LocalVFS(assertSecret(import.meta.env.CLIENT_DIR)); diff --git a/packages/website/public/assets/images/tools/credits.webp b/packages/website/public/assets/images/tools/credits.webp new file mode 100644 index 000000000..59a61c284 Binary files /dev/null and b/packages/website/public/assets/images/tools/credits.webp differ diff --git a/packages/website/public/media/users/34wotb.jpg b/packages/website/public/media/users/34wotb.jpg new file mode 100644 index 000000000..216cad339 Binary files /dev/null and b/packages/website/public/media/users/34wotb.jpg differ diff --git a/packages/website/public/media/users/aeson000.png b/packages/website/public/media/users/aeson000.png new file mode 100644 index 000000000..a003dc074 Binary files /dev/null and b/packages/website/public/media/users/aeson000.png differ diff --git a/packages/website/public/media/users/avenir.webp b/packages/website/public/media/users/avenir.webp new file mode 100644 index 000000000..146130324 Binary files /dev/null and b/packages/website/public/media/users/avenir.webp differ diff --git a/packages/website/public/media/users/chickenman7777.webp b/packages/website/public/media/users/chickenman7777.webp new file mode 100644 index 000000000..0cbf54d78 Binary files /dev/null and b/packages/website/public/media/users/chickenman7777.webp differ diff --git a/packages/website/public/media/users/droodles-blitz.jpg b/packages/website/public/media/users/droodles-blitz.jpg new file mode 100644 index 000000000..f5793cb80 Binary files /dev/null and b/packages/website/public/media/users/droodles-blitz.jpg differ diff --git a/packages/website/public/media/users/hisroyalfatness.jpg b/packages/website/public/media/users/hisroyalfatness.jpg new file mode 100644 index 000000000..411a5808c Binary files /dev/null and b/packages/website/public/media/users/hisroyalfatness.jpg differ diff --git a/packages/website/public/media/users/minitelrose.webp b/packages/website/public/media/users/minitelrose.webp new file mode 100644 index 000000000..208a82c15 Binary files /dev/null and b/packages/website/public/media/users/minitelrose.webp differ diff --git a/packages/website/public/media/users/prince-na.webp b/packages/website/public/media/users/prince-na.webp new file mode 100644 index 000000000..2f515e76f Binary files /dev/null and b/packages/website/public/media/users/prince-na.webp differ diff --git a/packages/website/public/media/users/s0ap.webp b/packages/website/public/media/users/s0ap.webp new file mode 100644 index 000000000..4717c29ef Binary files /dev/null and b/packages/website/public/media/users/s0ap.webp differ diff --git a/packages/website/public/media/users/synx.webp b/packages/website/public/media/users/synx.webp new file mode 100644 index 000000000..5b6a506df Binary files /dev/null and b/packages/website/public/media/users/synx.webp differ diff --git a/packages/website/public/media/users/tj-duff.jpg b/packages/website/public/media/users/tj-duff.jpg new file mode 100644 index 000000000..9dbf4fb5c Binary files /dev/null and b/packages/website/public/media/users/tj-duff.jpg differ diff --git a/packages/website/public/media/users/xkdkw.webp b/packages/website/public/media/users/xkdkw.webp new file mode 100644 index 000000000..7d7998c59 Binary files /dev/null and b/packages/website/public/media/users/xkdkw.webp differ diff --git a/packages/website/src/components/ArticlePlug.astro b/packages/website/src/components/ArticlePlug.astro deleted file mode 100644 index 4ef838462..000000000 --- a/packages/website/src/components/ArticlePlug.astro +++ /dev/null @@ -1,38 +0,0 @@ ---- -import { tankIcon } from "@blitzkit/core"; -import { Flex, Heading, Link, Strong, Text } from "@radix-ui/themes"; ---- - - - - - - - - Christmas 2025 - - Win the GSOR the TANK and more! - - - - - diff --git a/packages/website/src/components/Beg/index.module.css b/packages/website/src/components/Beg/index.module.css new file mode 100644 index 000000000..5bcfacd77 --- /dev/null +++ b/packages/website/src/components/Beg/index.module.css @@ -0,0 +1,36 @@ +.outer { + padding: 0 var(--space-4); + margin-top: var(--space-3); + + img { + width: 1.25em; + height: 1.25em; + + border-radius: 50%; + } + + .center { + position: relative; + + background: linear-gradient(var(--pink-3), var(--purple-2)); + box-shadow: var(--shadow-3); + + max-width: 40rem; + + padding: var(--space-5); + border-radius: var(--radius-4); + + .important { + position: absolute; + + top: 0; + left: 50%; + + transform: translate(-50%, -50%); + + background-color: inherit; + + padding: var(--space-3); + } + } +} diff --git a/packages/website/src/components/Beg/index.tsx b/packages/website/src/components/Beg/index.tsx new file mode 100644 index 000000000..7f905c616 --- /dev/null +++ b/packages/website/src/components/Beg/index.tsx @@ -0,0 +1,48 @@ +import { HeartFilledIcon } from "@radix-ui/react-icons"; +import { Button, Em, Flex, Strong, Text } from "@radix-ui/themes"; +import { donationTiers } from "../../config/donations"; +import styles from "./index.module.css"; + +export function Beg() { + return ( + + + + + We need your attention + + + + + + + We can no longer keep BlitzKit sustainable for free + . + + + + We want to add tank modules, player statistics, rebalancing patch + notes, bring back the Discord bot, and more. + + + + But paying out of pocket makes that impossible. We are an earnest + non-profit, so consider donating to make the above possible. 100% of + your money goes back into BlitzKit. + + + + + {donationTiers.map(({ name, value, link }) => ( + + + + ))} + + + + ); +} diff --git a/packages/website/src/components/Giveaway.tsx b/packages/website/src/components/Countdown.tsx similarity index 79% rename from packages/website/src/components/Giveaway.tsx rename to packages/website/src/components/Countdown.tsx index edd64c67a..77468ccc8 100644 --- a/packages/website/src/components/Giveaway.tsx +++ b/packages/website/src/components/Countdown.tsx @@ -3,9 +3,9 @@ import { useEffect, useState, type ReactNode } from "react"; import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps"; import { InlineSkeleton } from "./InlineSkeleton"; -const TIME = 1764898200 * 1000; +const TIME = 1792598400 * 1000; -export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) { +export function Countdown({ skeleton }: MaybeSkeletonComponentProps) { const [timeLeft, setTimeLeft] = useState(TIME - Date.now()); useEffect(() => { @@ -31,14 +31,14 @@ export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) { string = ( <> - in{" "} + revealed in{" "} {hours}h {minutes}m {seconds}s ); } else { - string = "has ended. Come again later!"; + string = "have been revealed."; } return ( @@ -47,7 +47,7 @@ export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) { color="gray" highContrast style={{ display: "content" }} - href="https://discord.gg/njQdBhNS?event=1446208951136813117" + href="https://preview.blitzkit.app/tanks" underline="none" > - Music Box giveaway {!skeleton && string}{" "} + Tier XIs {!skeleton && string}{" "} {skeleton && } - Join the BlitzKit Discord server event + Get ready to go to the Tankopedia! diff --git a/packages/website/src/components/CreditsPromo.astro b/packages/website/src/components/CreditsPromo.astro new file mode 100644 index 000000000..65f2186d3 --- /dev/null +++ b/packages/website/src/components/CreditsPromo.astro @@ -0,0 +1,135 @@ +--- +import { Grid, Text } from "@radix-ui/themes"; +import { credits } from "../config/credits"; +import { getStrings } from "../core/i18n/getStrings"; + +interface Props { + locale: string; +} + +const { locale } = Astro.props; + +const strings = getStrings(locale); +--- + + + +
+
+ + { + credits.map((person) => ( + + +
diff --git a/packages/website/src/components/Footer.astro b/packages/website/src/components/Footer.astro index ff2f490d3..fb8d1d170 100644 --- a/packages/website/src/components/Footer.astro +++ b/packages/website/src/components/Footer.astro @@ -43,9 +43,9 @@ const strings = getStrings(locale); }} > BlitzKit - {strings.website.footer.motto} - - {strings.website.footer.copyright} + {strings.website.footer.motto} + + {strings.website.footer.association} @@ -84,9 +84,6 @@ const strings = getStrings(locale); sm: "start", }} > - - {strings.website.footer.about.credits} - ) { - return ; +function Image({ src, ...props }: ComponentProps<'img'>) { + return ( + + ); } export function Markdown({ children }: MarkdownProps) { diff --git a/packages/website/src/components/Navbar.astro b/packages/website/src/components/Navbar.astro index b67d5d10a..06eebdc2e 100644 --- a/packages/website/src/components/Navbar.astro +++ b/packages/website/src/components/Navbar.astro @@ -5,7 +5,7 @@ import { GearIcon, HamburgerMenuIcon, } from "@radix-ui/react-icons"; -import { Box, Code, Flex, IconButton, Link } from "@radix-ui/themes"; +import { Box, Code, Flex, Grid, IconButton, Link } from "@radix-ui/themes"; import { resolveBranchName } from "../core/blitzkit/branches"; import { getStrings } from "../core/i18n/getStrings"; import { Var } from "../core/radix/var"; @@ -20,7 +20,7 @@ export interface Props { const { integrated = false } = Astro.props; const toolsFiltered = Object.values(tools).filter( - (tool) => !tool.href && !tool.disabled + (tool) => !tool.href && !tool.disabled, ); const { locale } = Astro.props; const branchName = resolveBranchName(locale); @@ -105,7 +105,7 @@ const strings = getStrings(locale); @@ -133,41 +133,6 @@ const strings = getStrings(locale); - - { - toolsFiltered.map((tool) => { - const selected = Astro.url.pathname.includes(`/${tool.id}`); - const toolStrings = - strings.website.tools[ - (tool.strings ?? - tool.id) as keyof typeof strings.website.tools - ]; - - return ( - - {toolStrings.name} - - ); - }) - } - - - - - + @@ -200,11 +165,10 @@ const strings = getStrings(locale); - { toolsFiltered.map((tool) => { @@ -229,7 +193,7 @@ const strings = getStrings(locale); }} > + diff --git a/packages/website/src/components/StupidCat.tsx b/packages/website/src/components/StupidCat.tsx deleted file mode 100644 index 86c5be34d..000000000 --- a/packages/website/src/components/StupidCat.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function StupidCat() { - return ; -} diff --git a/packages/website/src/components/TankSearch/components/Card.tsx b/packages/website/src/components/TankSearch/components/Card.tsx index 97182487a..b210ef1c6 100644 --- a/packages/website/src/components/TankSearch/components/Card.tsx +++ b/packages/website/src/components/TankSearch/components/Card.tsx @@ -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); diff --git a/packages/website/src/components/TankSearch/index.tsx b/packages/website/src/components/TankSearch/index.tsx index 35321dc2c..eb8ac1ed9 100644 --- a/packages/website/src/components/TankSearch/index.tsx +++ b/packages/website/src/components/TankSearch/index.tsx @@ -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( 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( 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); diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx index f28050abc..cb975b796 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx @@ -44,7 +44,9 @@ export function GunFlexibilityVisualizer({ const tank = Duel.use((state) => state.protagonist.tank); const turret = Duel.use((state) => state.protagonist.turret); const gun = Duel.use((state) => state.protagonist.gun); - const modelRequested = Tankopedia.use((state) => state.modelRequested); + const modelRequested = Tankopedia.use( + (state) => state.loaders.model.requested, + ); const tankModel = modelDefinition.models[tank.id]; const turretModel = tankModel.turrets[turret.id]; diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx index 7d910738d..5e0fe27f5 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx @@ -1,5 +1,5 @@ import { createDefaultSkills } from "@blitzkit/core"; -import { Flex, Progress, Text } from "@radix-ui/themes"; +import { Box, Flex, Progress, Text } from "@radix-ui/themes"; import { clamp } from "lodash-es"; import { memo, useMemo, type ComponentProps, type ReactNode } from "react"; import { api } from "../../../../../../core/blitzkit/api"; @@ -16,6 +16,7 @@ import { TankopediaRelativeAgainst, } from "../../../../../../stores/tankopedia"; import { Info, type InfoProps } from "./Info"; +import { StatScale } from "./StatScale"; type InfoWithDeltaProps = Omit & { stats: TankCharacteristics; @@ -122,6 +123,15 @@ export const InfoWithDelta = memo( return othersValue !== undefined; }) as TankCharacteristics[]; }, [relativeAgainst, shellIndex, equipmentMatrix, equalize]); + const values = useMemo( + () => + others.map((tank) => + typeof props.value === "function" + ? props.value(tank)! + : (tank[props.value] as number), + ), + [others], + ); const betterTanks = others.filter((tank) => { const othersValue = typeof props.value === "function" @@ -161,13 +171,25 @@ export const InfoWithDelta = memo( {!noRanking && ( - + + + + + {clamp(betterTanks.length + 1, 1, others.length)} /{" "} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx new file mode 100644 index 000000000..a032c46d2 --- /dev/null +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx @@ -0,0 +1,91 @@ +import { memo, useEffect, useMemo, useRef } from "react"; + +const RESOLUTION = 1000; +const HEIGHT = 8; +const DENSE_TICKS = 100; + +interface StatScaleProps { + values: number[]; + value: number; + lowerIsBetter?: boolean; +} + +export const StatScale = memo( + ({ values, value, lowerIsBetter }) => { + const canvasRef = useRef(null); + const ticks = useMemo(() => { + const min = Math.min(value, ...values); + const max = Math.max(value, ...values); + const range = max - min; + const positions = new Set(); + + for (const other of values) { + const raw = range === 0 ? 0.5 : (other - min) / range; + const x = Math.round((lowerIsBetter ? 1 - raw : raw) * RESOLUTION); + + if (x === 0 || x === RESOLUTION) continue; + + positions.add(x); + } + + return Array.from(positions, (x) => ({ x })); + }, [values, value, lowerIsBetter]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const draw = () => { + const { width } = canvas.getBoundingClientRect(); + const pixelRatio = window.devicePixelRatio || 1; + const canvasWidth = Math.ceil(width * pixelRatio); + const canvasHeight = Math.ceil(HEIGHT * pixelRatio); + + if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) { + canvas.width = canvasWidth; + canvas.height = canvasHeight; + } + + const context = canvas.getContext("2d"); + if (!context) return; + + context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + context.clearRect(0, 0, width, HEIGHT); + + const styles = getComputedStyle(canvas); + for (const { x } of ticks) { + context.fillStyle = styles.getPropertyValue("--gray-a6").trim(); + context.fillRect((x / RESOLUTION) * width, 0, 1, HEIGHT); + } + }; + + const observer = new ResizeObserver(draw); + observer.observe(canvas); + window.addEventListener("resize", draw); + draw(); + + return () => { + observer.disconnect(); + window.removeEventListener("resize", draw); + }; + }, [ticks]); + + return ( +