Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a2d4aaa9c | ||
|
|
d7914bb2fb | ||
|
|
8cb15d488d | ||
|
|
5c3ab2e10d | ||
|
|
28c30f210e | ||
|
|
5c92f9f7ee | ||
|
|
249b185836 |
No files matched your search
@@ -2,11 +2,6 @@ name: Website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- production
|
||||
|
||||
pull_request:
|
||||
|
||||
schedule:
|
||||
- cron: "0 4 * * *"
|
||||
@@ -98,7 +93,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
chunk: [default, api, models_tanks, models_legendary, models_maps]
|
||||
chunk: [default, api]
|
||||
|
||||
steps:
|
||||
- name: Free disk space
|
||||
@@ -107,8 +102,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# the PR's ref, or the manual input, or production (selected when cron)
|
||||
ref: ${{ github.event.pull_request.head.sha || inputs.branch || 'production' }}
|
||||
ref: ${{ inputs.branch || 'production' }}
|
||||
submodules: recursive
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
@@ -128,14 +122,9 @@ 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:
|
||||
@@ -150,15 +139,13 @@ jobs:
|
||||
|
||||
- name: Trigger deployment
|
||||
|
||||
# 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
|
||||
# Deploy if:
|
||||
# 1. Running cron from main using content from production to target production
|
||||
# 2. Running manually and targeting non-production
|
||||
if: >-
|
||||
${{
|
||||
(github.event_name == 'schedule' && github.ref == 'refs/heads/main')
|
||||
|| (github.event_name == 'push' && github.ref == 'refs/heads/production')
|
||||
|| (github.event_name == 'workflow_dispatch')
|
||||
github.ref == 'refs/heads/production' ||
|
||||
(inputs.environment != '' && inputs.environment != 'production')
|
||||
}}
|
||||
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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.
|
||||
@@ -1,14 +1,14 @@
|
||||
# BlitzKit 1.29.10
|
||||
|
||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
||||
Thank you to [Mark Greene](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 [HitJack]
|
||||
- Added tier filters to the Guess game [Mark Greene]
|
||||
- Changing tiers will break your streak
|
||||
|
||||
## Fixes
|
||||
|
||||
- module damage is now properly shown when sorting with it in the tank search [HitJack]
|
||||
- module damage is now properly shown when sorting with it in the tank search [Mark Greene]
|
||||
- Duplicate abilities in the tank filters are now removed
|
||||
@@ -1,6 +1,6 @@
|
||||
# BlitzKit 1.29.8
|
||||
|
||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
||||
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -15,5 +15,5 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
|
||||
## Fixes
|
||||
|
||||
- The Tankopedia now smoothly resizes when enlarging on interaction
|
||||
- Stock turrets now correctly mount the corresponding stock gun [HitJack]
|
||||
- Stock turrets now correctly mount the corresponding stock gun [Mark Greene]
|
||||
- The consumable and provision buttons in Compare are no longer tiny
|
||||
@@ -1,14 +1,14 @@
|
||||
# BlitzKit 1.29.9
|
||||
|
||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
||||
Thank you to [Mark Greene](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 [HitJack]
|
||||
- Added the ability to filter tanks by consumables [Mark Greene]
|
||||
- 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 [HitJack]
|
||||
- Provisions now properly affect terrain resistances [Mark Greene]
|
||||
@@ -1,6 +1,6 @@
|
||||
# BlitzKit 1.30.7
|
||||
|
||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
||||
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -8,13 +8,13 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
|
||||
|
||||
## Fixes
|
||||
|
||||
- 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]
|
||||
- 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]
|
||||
- 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 [HitJack]
|
||||
- Safari no longer crashes when unloading textures for unloaded tanks [Mark Greene]
|
||||
- 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 [HitJack]
|
||||
- Armor plate names in Static Armor are now properly capitalized in English and translated to other languages [Mark Greene]
|
||||
@@ -1,75 +1,75 @@
|
||||
# BlitzKit 1.32.0
|
||||
|
||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
||||
Thank you to [Mark Greene](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]
|
||||
- Added a spall liner button to the Tankopedia beside the other equipment [Mark Greene]
|
||||
- Listed track repair times in the Tankopedia [Mark Greene]
|
||||
- The penetration chance is now displayed when penetration testing armor [Mark Greene]
|
||||
- Added a new setting to disable automatically loading tanks in Tankopedia [Mark Greene]
|
||||
|
||||
### Tank Performance & Mechanics
|
||||
|
||||
- Added Peekaboo DPM [HitJack]
|
||||
- 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
|
||||
|
||||
### 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]
|
||||
- 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]
|
||||
|
||||
### Game Compatibility
|
||||
|
||||
- Added compatibility for tier XI tanks [HitJack]
|
||||
- Added compatibility for tier XI tanks [Mark Greene]
|
||||
|
||||
## 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]
|
||||
- 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 penetration chance is now displayed when penetration testing armor [Mark Greene]
|
||||
- Listed track repair times in the Tankopedia [Mark Greene]
|
||||
- Added a spall liner button to the Tankopedia beside the other equipment [Mark Greene]
|
||||
- The flags in the Tankopedia tech tree are no longer offset to the top left [Mark Greene]
|
||||
- The loading bar below the name of the tanks in Tankopedia is now sufficiently spaced below [Mark Greene]
|
||||
- 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]
|
||||
- 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 tank search now remains consistent in order even when an added filter does not add/remove tanks from the list [Mark Greene]
|
||||
- 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]
|
||||
|
||||
### Visuals & UI
|
||||
|
||||
- Removed the Beta badge from the Equalizer button [HitJack]
|
||||
- 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 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 contact shadows on the ground no longer grow & darken when switching between tabs [Mark Greene]
|
||||
- Additionally, the contact shadows stays visible at low angles [Mark Greene]
|
||||
- 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]
|
||||
- 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]
|
||||
- Plot controls for the Static armor tab no longer pokes in from the right on non-Static armor tabs in some languages [Mark Greene]
|
||||
- Mixer's controllers now scale better on smaller screens [Mark Greene]
|
||||
- The list of available tanks in the Playlist is no longer excessively wide [Mark Greene]
|
||||
|
||||
### 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]
|
||||
- The gun depression visualizer now properly handles the inherent tilt of tanks like the Minotauro [Mark Greene]
|
||||
- When moving the tracks of tanks, the textures no longer move at different rates [Mark Greene]
|
||||
|
||||
### Localization
|
||||
|
||||
- Users with a preference on a language are once again redirected to the correct localized pages [HitJack]
|
||||
- Users with a preference on a language are once again redirected to the correct localized pages [Mark Greene]
|
||||
|
||||
### Session & Game Compatibility
|
||||
|
||||
- BlitzKit Session now properly handles tanks removed from the game [HitJack]
|
||||
- BlitzKit Session now properly handles tanks removed from the game [Mark Greene]
|
||||
|
||||
## Technical Fixes
|
||||
|
||||
@@ -78,7 +78,7 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
|
||||
- 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]
|
||||
- The player discovery loop now persists for longer than 1 iteration [Mark Greene]
|
||||
- Bun is now forced to run scripts with the `--bun` flag
|
||||
|
||||
## Technical Features
|
||||
@@ -94,5 +94,5 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
|
||||
- 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]
|
||||
- The player discovery loop now persists for longer than 1 iteration [Mark Greene]
|
||||
- Bun is now forced to run scripts with the `--bun` flag
|
||||
@@ -4,33 +4,14 @@
|
||||
|
||||
"paths": {
|
||||
"default": null,
|
||||
|
||||
"api": null,
|
||||
|
||||
"models_tanks": null,
|
||||
"models_legendary": null,
|
||||
"models_maps": null
|
||||
"api": null
|
||||
},
|
||||
|
||||
"nodes": {
|
||||
"default": "blitzkit/deploy-node-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"
|
||||
"api": "blitzkit/deploy-node-api.beta.blitzkit.app"
|
||||
},
|
||||
|
||||
"aliases": {
|
||||
"default": "/",
|
||||
|
||||
"api": "https://api.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"
|
||||
"static": "https://static.beta.blitzkit.app"
|
||||
}
|
||||
}
|
||||
@@ -4,25 +4,11 @@
|
||||
|
||||
"paths": {
|
||||
"default": null,
|
||||
|
||||
"api": "api",
|
||||
|
||||
"models_tanks": "models/tanks",
|
||||
"models_legendary": "models/legendary",
|
||||
"models_maps": "models/maps"
|
||||
"api": "api"
|
||||
},
|
||||
|
||||
"nodes": {},
|
||||
|
||||
"aliases": {
|
||||
"default": "/",
|
||||
|
||||
"api": "/api",
|
||||
|
||||
"models_tanks": "/models/tanks",
|
||||
"models_legendary": "/models/legendary",
|
||||
"models_maps": "/models/maps",
|
||||
|
||||
"static": "https://static.blitzkit.app"
|
||||
}
|
||||
}
|
||||
@@ -4,33 +4,14 @@
|
||||
|
||||
"paths": {
|
||||
"default": null,
|
||||
|
||||
"api": null,
|
||||
|
||||
"models_tanks": null,
|
||||
"models_legendary": null,
|
||||
"models_maps": null
|
||||
"api": null
|
||||
},
|
||||
|
||||
"nodes": {
|
||||
"default": "blitzkit/deploy-node-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"
|
||||
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app"
|
||||
},
|
||||
|
||||
"aliases": {
|
||||
"default": "/",
|
||||
|
||||
"api": "https://api.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"
|
||||
"static": "https://static.opentest.blitzkit.app"
|
||||
}
|
||||
}
|
||||
@@ -4,33 +4,14 @@
|
||||
|
||||
"paths": {
|
||||
"default": null,
|
||||
|
||||
"api": null,
|
||||
|
||||
"models_tanks": null,
|
||||
"models_legendary": null,
|
||||
"models_maps": null
|
||||
"api": null
|
||||
},
|
||||
|
||||
"nodes": {
|
||||
"default": "blitzkit/deploy-node-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"
|
||||
"api": "blitzkit/deploy-node-api.preview.blitzkit.app"
|
||||
},
|
||||
|
||||
"aliases": {
|
||||
"default": "/",
|
||||
|
||||
"api": "https://api.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"
|
||||
"static": "https://static.preview.blitzkit.app"
|
||||
}
|
||||
}
|
||||
@@ -4,33 +4,14 @@
|
||||
|
||||
"paths": {
|
||||
"default": null,
|
||||
|
||||
"api": null,
|
||||
|
||||
"models_tanks": null,
|
||||
"models_legendary": null,
|
||||
"models_maps": null
|
||||
"api": null
|
||||
},
|
||||
|
||||
"nodes": {
|
||||
"default": "blitzkit/deploy-node-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"
|
||||
"api": "blitzkit/deploy-node-api.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"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
{
|
||||
"name": "blitzkit",
|
||||
"version": "1.33.0-beta.1",
|
||||
"version": "1.32.0",
|
||||
"license": "BlitzKit License",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cd packages/website && bun --bun run dev",
|
||||
"build": "cd packages/website && bun --bun run build",
|
||||
"build": "cd packages/website && 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",
|
||||
"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"
|
||||
"clean-astro": "rm -rf packages/website/.astro packages/website/node_modules/.astro packages/website/node_modules/.vite"
|
||||
},
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -246,27 +246,6 @@ 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<string, string> = {
|
||||
[locales.default]: this.stringsI18n[locales.default][name],
|
||||
@@ -285,8 +264,6 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
|
||||
collection[locale] = localizedString;
|
||||
}
|
||||
|
||||
collection[locales.default] = collection[locales.default] ?? name;
|
||||
|
||||
return { locales: collection } satisfies I18nString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,7 +387,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
||||
: TankType.TANK_TYPE_RESEARCHABLE,
|
||||
tier: tank.level,
|
||||
class: tankClass,
|
||||
testing: tankTags.includes("testTank") || tank.level === 11,
|
||||
testing: tankTags.includes("testTank"),
|
||||
deprecated: tankTags.includes("deprecated"),
|
||||
price: tankPrice,
|
||||
camouflage_still: tankDefinition.root.invisibility.still,
|
||||
@@ -595,25 +595,9 @@ 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 <shots> 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 = piercingPower
|
||||
const penetrationRaw = gunShellEntry.piercingPower
|
||||
.split(" ")
|
||||
.filter((penetrationString) => penetrationString !== "")
|
||||
.map(Number);
|
||||
@@ -623,7 +607,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
||||
].shells.push({
|
||||
id: shellId,
|
||||
name: shellName,
|
||||
velocity: speed,
|
||||
velocity: gunShellEntry.speed,
|
||||
armor_damage: shell.damage.armor,
|
||||
module_damage: shell.damage.devices,
|
||||
caliber: shell.caliber,
|
||||
@@ -639,7 +623,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
||||
near: penetrationRaw[0],
|
||||
far: penetrationRaw[1],
|
||||
},
|
||||
range: maxDistance,
|
||||
range: gunShellEntry.maxDistance,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +1,15 @@
|
||||
import {
|
||||
CamouflageDefinitions,
|
||||
Sc2ReadStream,
|
||||
type TankParameters,
|
||||
} from "@blitzkit/core";
|
||||
import { isEqual, times } from "lodash-es";
|
||||
import { CamouflageDefinitions } from "@blitzkit/core";
|
||||
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<TankParameters>(
|
||||
`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<string>();
|
||||
|
||||
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<string, { Name: string; Path: string }>
|
||||
>("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<string, Promise<Set<string>>>();
|
||||
|
||||
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)
|
||||
@@ -65,51 +20,12 @@ 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`))
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
let hasPreset = false;
|
||||
|
||||
if (camo.group === "legendary" && yamlEntry.preset !== undefined) {
|
||||
if (!skinPresets.has(vehicle)) {
|
||||
skinPresets.set(vehicle, this.skinPresets(vehicle));
|
||||
}
|
||||
|
||||
hasPreset = (await skinPresets.get(vehicle)!).has(yamlEntry.preset);
|
||||
}
|
||||
|
||||
if (resolvedParts.some(Boolean) || hasPreset) {
|
||||
tankId = this.tankStringIdMap[vehicle];
|
||||
}
|
||||
}
|
||||
|
||||
const name = this.getString(camo.userString);
|
||||
|
||||
camouflageDefinitions.camouflages[camo.id] = {
|
||||
id: camo.id,
|
||||
name,
|
||||
name: this.getString(camo.userString),
|
||||
tank_name: resolvedTankName,
|
||||
tank_name_full: resolvedTankNameFull,
|
||||
tank_id: tankId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,7 @@ import {
|
||||
ChassisDefinitionsList,
|
||||
Crew,
|
||||
GunDefinitionsList,
|
||||
I_HAT,
|
||||
J_HAT,
|
||||
K_HAT,
|
||||
ModelDefinitions,
|
||||
parseHierarchies,
|
||||
Sc2ReadStream,
|
||||
ScgReadStream,
|
||||
TankParameters,
|
||||
toUniqueId,
|
||||
TurretDefinitionsList,
|
||||
@@ -18,10 +12,9 @@ import {
|
||||
VehicleDefinitionArmor,
|
||||
VehicleDefinitionList,
|
||||
VehicleDefinitions,
|
||||
VertexAttribute,
|
||||
} from "@blitzkit/core";
|
||||
import { parse as parsePath } from "path";
|
||||
import { MathUtils, Vector3 as THREEVector3, Vector3Tuple } from "three";
|
||||
import { Vector3Tuple } from "three";
|
||||
import { Cache } from "./0_base";
|
||||
import { ServerBlitzKitAPI2 } from "./2_camouflageDefinitions";
|
||||
|
||||
@@ -30,87 +23,6 @@ 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,
|
||||
@@ -155,12 +67,6 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
||||
const tankParameters = await this.vfs.yaml<TankParameters>(
|
||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
const collisionBoundingBoxes = await this.collisionBoundingBoxes(
|
||||
nation,
|
||||
tankKey,
|
||||
tankDefinition.root.hull.turretInitialRotation,
|
||||
);
|
||||
const hullBoundingBox = collisionBoundingBoxes.hull;
|
||||
const turretOrigin = tankDefinition.root.hull.turretPositions.turret
|
||||
.split(" ")
|
||||
.map(Number) as Vector3Tuple;
|
||||
@@ -235,8 +141,12 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
||||
}
|
||||
: undefined,
|
||||
bounding_box: {
|
||||
min: this.vector3TupleToBlitzkit(hullBoundingBox.min),
|
||||
max: this.vector3TupleToBlitzkit(hullBoundingBox.max),
|
||||
min: this.vector3TupleToBlitzkit(
|
||||
tankParameters.collision.hull.bbox.min,
|
||||
),
|
||||
max: this.vector3TupleToBlitzkit(
|
||||
tankParameters.collision.hull.bbox.max,
|
||||
),
|
||||
},
|
||||
turrets: {},
|
||||
tracks: {},
|
||||
@@ -298,35 +208,26 @@ 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: isFullYaw
|
||||
? undefined
|
||||
: { min: turretYaw[0], max: turretYaw[1] },
|
||||
yaw:
|
||||
-turretYaw[0] + turretYaw[1] === 360
|
||||
? undefined
|
||||
: { min: turretYaw[0], max: turretYaw[1] },
|
||||
guns: {},
|
||||
bounding_box: {
|
||||
min: this.vector3TupleToBlitzkit(turretBoundingBox.min),
|
||||
max: this.vector3TupleToBlitzkit(turretBoundingBox.max),
|
||||
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,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 {
|
||||
filter_type: {
|
||||
$case: "ids",
|
||||
value: {
|
||||
ids: this.getTankFilterIds(includeRaw.name, key, "include"),
|
||||
ids: includeRaw.name.split(/ +/).map((key) => {
|
||||
return this.tankStringIdMap[key];
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -70,7 +72,9 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 {
|
||||
filter_type: {
|
||||
$case: "ids",
|
||||
value: {
|
||||
ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"),
|
||||
ids: excludeRaw.name.split(/ +/).map((key) => {
|
||||
return this.tankStringIdMap[key];
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
|
||||
async provisionDefinitions() {
|
||||
const provisionDefinitions = ProvisionDefinitions.create();
|
||||
|
||||
Object.entries(this.provisionsCommon).forEach(([key, provision]) => {
|
||||
Object.entries(this.provisionsCommon).forEach(([, provision]) => {
|
||||
const entry: Provision = {
|
||||
id: provision.id,
|
||||
exclude: [],
|
||||
@@ -42,7 +42,9 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
|
||||
filter_type: {
|
||||
$case: "ids",
|
||||
value: {
|
||||
ids: this.getTankFilterIds(includeRaw.name, key, "include"),
|
||||
ids: includeRaw.name.split(/ +/).map((key) => {
|
||||
return this.tankStringIdMap[key];
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -68,7 +70,9 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
|
||||
filter_type: {
|
||||
$case: "ids",
|
||||
value: {
|
||||
ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"),
|
||||
ids: excludeRaw.name
|
||||
.split(/ +/)
|
||||
.map((key) => this.tankStringIdMap[key]),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,7 +25,6 @@ export * from "./sluggify";
|
||||
export * from "./staticAsset";
|
||||
export * from "./sum";
|
||||
export * from "./tankDefinitions";
|
||||
export * from "./tankBoundingBox";
|
||||
export * from "./tankIcon";
|
||||
export * from "./tankNames";
|
||||
export * from "./unionBoundingBox";
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ModelDefinition, TurretModelDefinition } from "../protos";
|
||||
import { unionBoundingBox } from "./unionBoundingBox";
|
||||
|
||||
export function tankBoundingBox(
|
||||
tankModelDefinition: ModelDefinition,
|
||||
turretModelDefinition: TurretModelDefinition,
|
||||
) {
|
||||
const hull = tankModelDefinition.bounding_box!;
|
||||
const turret = turretModelDefinition.bounding_box!;
|
||||
const origin = tankModelDefinition.turret_origin!;
|
||||
|
||||
if (
|
||||
turret.min!.x === turret.max!.x &&
|
||||
turret.min!.y === turret.max!.y &&
|
||||
turret.min!.z === turret.max!.z
|
||||
) {
|
||||
return unionBoundingBox(hull, hull);
|
||||
}
|
||||
|
||||
return unionBoundingBox(hull, {
|
||||
min: {
|
||||
x: turret.min!.x + origin.x,
|
||||
y: turret.min!.y + origin.z,
|
||||
z: turret.min!.z + origin.y,
|
||||
},
|
||||
max: {
|
||||
x: turret.max!.x + origin.x,
|
||||
y: turret.max!.y + origin.z,
|
||||
z: turret.max!.z + origin.y,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { times } from "lodash-es";
|
||||
import { CrewType, ShellType } from "../../protos";
|
||||
import { CrewType, ShellType, TankClass, TankType } from "../../protos";
|
||||
|
||||
export const SHELL_NAMES: Record<ShellType, string> = {
|
||||
[ShellType.SHELL_TYPE_AP]: "AP",
|
||||
@@ -15,6 +15,33 @@ export const CREW_MEMBER_NAMES = {
|
||||
[CrewType.CREW_TYPE_RADIOMAN]: "radioman",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @deprecated use svg
|
||||
*/
|
||||
export const TANK_ICONS: Record<TankClass, string> = {
|
||||
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/BIHSEH0.png",
|
||||
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/CSNha5V.png",
|
||||
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/wvf3ltm.png",
|
||||
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/ECeqlZa.png",
|
||||
};
|
||||
/**
|
||||
* @deprecated use svg
|
||||
*/
|
||||
export const TANK_ICONS_PREMIUM: Record<TankClass, string> = {
|
||||
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/TCu3EdR.png",
|
||||
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/zdkpTRb.png",
|
||||
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/3z7eHX6.png",
|
||||
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/P3vbmyA.png",
|
||||
};
|
||||
/**
|
||||
* @deprecated use svg
|
||||
*/
|
||||
export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
|
||||
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/WTjeirB.png",
|
||||
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/EwhtKkU.png",
|
||||
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/u8YDMBh.png",
|
||||
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/8xRf3nc.png",
|
||||
};
|
||||
export const TIERS = times(10, (index) => index + 1);
|
||||
export const TIER_ROMAN_NUMERALS: Record<number, string> = {
|
||||
1: "I",
|
||||
@@ -41,3 +68,8 @@ export const flags: Record<string, string> = {
|
||||
european: "🇪🇺",
|
||||
other: "<:other:1218421572243558482>",
|
||||
};
|
||||
export const TREE_TYPE_ICONS: Record<TankType, Record<TankClass, string>> = {
|
||||
[TankType.TANK_TYPE_RESEARCHABLE]: TANK_ICONS,
|
||||
[TankType.TANK_TYPE_PREMIUM]: TANK_ICONS_PREMIUM,
|
||||
[TankType.TANK_TYPE_COLLECTOR]: TANK_ICONS_COLLECTOR,
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
export enum ImgurSize {
|
||||
/**
|
||||
* The default size
|
||||
*/
|
||||
Default = '',
|
||||
/**
|
||||
* Unknown size but really really small
|
||||
*/
|
||||
UltraSmall = 'ss',
|
||||
/**
|
||||
* 90x90
|
||||
*/
|
||||
SmallSquare = 's',
|
||||
/**
|
||||
* 160x160
|
||||
*/
|
||||
BigSquare = 'b',
|
||||
/**
|
||||
* 160x160
|
||||
*/
|
||||
SmallThumbnail = 't',
|
||||
/**
|
||||
* 320x320
|
||||
*/
|
||||
MediumThumbnail = 'm',
|
||||
/**
|
||||
* 640x640
|
||||
*/
|
||||
LargeThumbnail = 'l',
|
||||
/**
|
||||
* 1024x1024
|
||||
*/
|
||||
HugeThumbnail = 'h',
|
||||
}
|
||||
|
||||
interface ImgurOptions {
|
||||
format: 'jpeg' | 'png';
|
||||
size: ImgurSize;
|
||||
}
|
||||
|
||||
const defaultImgurOptions: ImgurOptions = {
|
||||
format: 'png',
|
||||
size: ImgurSize.Default,
|
||||
};
|
||||
|
||||
export function imgur(id: string, imgurOptions?: Partial<ImgurOptions>) {
|
||||
const options = { ...defaultImgurOptions, ...imgurOptions };
|
||||
|
||||
return `https://i.imgur.com/${id}${options.size}.${options.format}`;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './imgur';
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./blitz";
|
||||
export * from "./blitzkit";
|
||||
export * from "./imgur";
|
||||
export * from "./math";
|
||||
export * from "./patreon";
|
||||
export * from "./protobuf";
|
||||
|
||||
@@ -13,5 +13,4 @@ message Camouflage {
|
||||
required I18nString name = 2;
|
||||
optional I18nString tank_name = 3;
|
||||
optional I18nString tank_name_full = 4;
|
||||
optional uint32 tank_id = 5;
|
||||
}
|
||||
@@ -24,7 +24,6 @@ export interface Camouflage {
|
||||
name: I18nString | undefined;
|
||||
tank_name?: I18nString | undefined;
|
||||
tank_name_full?: I18nString | undefined;
|
||||
tank_id?: number | undefined;
|
||||
}
|
||||
|
||||
function createBaseCamouflageDefinitions(): CamouflageDefinitions {
|
||||
@@ -192,7 +191,7 @@ export const CamouflageDefinitions_CamouflagesEntry: MessageFns<CamouflageDefini
|
||||
};
|
||||
|
||||
function createBaseCamouflage(): Camouflage {
|
||||
return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined, tank_id: 0 };
|
||||
return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined };
|
||||
}
|
||||
|
||||
export const Camouflage: MessageFns<Camouflage> = {
|
||||
@@ -209,9 +208,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
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);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
@@ -254,14 +250,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
message.tank_name_full = I18nString.decode(reader, reader.uint32());
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.tank_id = reader.uint32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
@@ -277,7 +265,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
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,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -295,9 +282,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
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);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
@@ -316,7 +300,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
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;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -88,7 +88,6 @@ enum PvrChannelType {
|
||||
|
||||
enum ResolvedBitRatePixelFormat {
|
||||
R4G4B4A4,
|
||||
R8G8B8A8,
|
||||
R5G6B5A0,
|
||||
}
|
||||
|
||||
@@ -138,27 +137,6 @@ 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);
|
||||
|
||||
@@ -230,13 +208,10 @@ export class PvrReadStream extends ReadStream {
|
||||
if (pixelFormat.order === "rgba") {
|
||||
if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) {
|
||||
return ResolvedBitRatePixelFormat.R4G4B4A4;
|
||||
} else if (isEqual(pixelFormat.bitRates, [8, 8, 8, 8])) {
|
||||
return ResolvedBitRatePixelFormat.R8G8B8A8;
|
||||
} else {
|
||||
} 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;
|
||||
|
||||
@@ -90,28 +90,6 @@ export interface Hierarchy {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function parseHierarchies<Parent>(
|
||||
hierarchies: Hierarchy[],
|
||||
parent: Parent,
|
||||
parseHierarchy: (
|
||||
hierarchy: Hierarchy,
|
||||
components: Component[],
|
||||
parent: Parent,
|
||||
) => Parent | undefined,
|
||||
) {
|
||||
for (const hierarchy of hierarchies) {
|
||||
const components = Array.from(
|
||||
{ length: hierarchy.components.count },
|
||||
(_, index) => hierarchy.components[index.toString().padStart(4, "0")],
|
||||
);
|
||||
const child = parseHierarchy(hierarchy, components, parent);
|
||||
|
||||
if (child !== undefined && hierarchy["#hierarchy"]) {
|
||||
parseHierarchies(hierarchy["#hierarchy"], child, parseHierarchy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 0001, 0002, 0003...: Component
|
||||
type Components = {
|
||||
count: number;
|
||||
|
||||
@@ -3,7 +3,5 @@ export type CamouflagesYaml = Record<
|
||||
{
|
||||
userString?: string;
|
||||
shortUserString?: string;
|
||||
preset?: string;
|
||||
customEntities?: Record<string, { item: string }>;
|
||||
}
|
||||
>;
|
||||
@@ -20,13 +20,11 @@ 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,9 +10,6 @@ export type ShellDefinitionsList = Record<
|
||||
normalizationAngle?: number;
|
||||
ricochetAngle?: number;
|
||||
explosionRadius?: number;
|
||||
speed: number;
|
||||
maxDistance?: number;
|
||||
piercingPower: string;
|
||||
}
|
||||
> & {
|
||||
icons: Record<string, string>;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -38,7 +44,7 @@
|
||||
|
||||
"branches": {
|
||||
"beta": "Beta",
|
||||
"development": "Post Malone Edition",
|
||||
"development": "Development",
|
||||
"opentest": "OpenTest",
|
||||
"preview": "Preview",
|
||||
"production": "Production"
|
||||
@@ -332,13 +338,6 @@
|
||||
},
|
||||
|
||||
"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,10 +992,11 @@
|
||||
|
||||
"footer": {
|
||||
"motto": "Everything World of Tanks Blitz",
|
||||
"association": "An Open Collective Project",
|
||||
"copyright": "Unaffiliated with Wargaming or any service",
|
||||
|
||||
"about": {
|
||||
"title": "About",
|
||||
"credits": "Credits",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"patches": "Patches"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"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": {
|
||||
@@ -302,12 +307,6 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"credits": {
|
||||
"name": "Credits",
|
||||
"button": "View people",
|
||||
"description": "Contributors of BlitzKit",
|
||||
"seo_description": "BlitzKit is the work of many"
|
||||
},
|
||||
"playlist": {
|
||||
"reset": "Restablecer",
|
||||
"shuffle": "Shuffle",
|
||||
@@ -920,9 +919,10 @@
|
||||
},
|
||||
"footer": {
|
||||
"motto": "Todo sobre World of Tanks Blitz",
|
||||
"association": "An Open Collective Project",
|
||||
"copyright": "No afiliado con Wargaming u otros servicios",
|
||||
"about": {
|
||||
"title": "About",
|
||||
"credits": "Credits",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"patches": "Parches"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"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": {
|
||||
@@ -302,12 +307,6 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"credits": {
|
||||
"name": "Credits",
|
||||
"button": "View people",
|
||||
"description": "Contributors of BlitzKit",
|
||||
"seo_description": "BlitzKit is the work of many"
|
||||
},
|
||||
"playlist": {
|
||||
"reset": "Reset",
|
||||
"shuffle": "Shuffle",
|
||||
@@ -920,9 +919,10 @@
|
||||
},
|
||||
"footer": {
|
||||
"motto": "Everything World of Tanks Blitz",
|
||||
"association": "An Open Collective Project",
|
||||
"copyright": "Unaffiliated with Wargaming or any service",
|
||||
"about": {
|
||||
"title": "About",
|
||||
"credits": "Credits",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"patches": "Patches"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"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": {
|
||||
@@ -302,12 +307,6 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"credits": {
|
||||
"name": "Credits",
|
||||
"button": "View people",
|
||||
"description": "Contributors of BlitzKit",
|
||||
"seo_description": "BlitzKit is the work of many"
|
||||
},
|
||||
"playlist": {
|
||||
"reset": "リセット",
|
||||
"shuffle": "Shuffle",
|
||||
@@ -920,9 +919,10 @@
|
||||
},
|
||||
"footer": {
|
||||
"motto": "Everything World of Tanks Blitz",
|
||||
"association": "An Open Collective Project",
|
||||
"copyright": "Wargamingやその他のサービスとは関係ありません",
|
||||
"about": {
|
||||
"title": "概要",
|
||||
"credits": "クレジット",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"patches": "パッチノート"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"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": {
|
||||
@@ -14,7 +19,7 @@
|
||||
"gun_types": {
|
||||
"auto_loader": "Samo ładowacz",
|
||||
"auto_reloader": "Automatyczne przeładowanie",
|
||||
"regular": "Regularny"
|
||||
"regular": "Regular"
|
||||
},
|
||||
"shells": {
|
||||
"ap": "AP",
|
||||
@@ -33,10 +38,10 @@
|
||||
},
|
||||
"branches": {
|
||||
"beta": "Beta",
|
||||
"development": "Rozwój",
|
||||
"opentest": "OtwartyTest",
|
||||
"preview": "Przegląd",
|
||||
"production": "Produkcja"
|
||||
"development": "Development",
|
||||
"opentest": "OpenTest",
|
||||
"preview": "Preview",
|
||||
"production": "Production"
|
||||
},
|
||||
"locales": {
|
||||
"en": "Angielski",
|
||||
@@ -97,28 +102,28 @@
|
||||
"other": "Hybryda"
|
||||
},
|
||||
"tank_class_short": {
|
||||
"-1": "nierozpoznany",
|
||||
"-1": "unrecognized",
|
||||
"0": "Lekki",
|
||||
"1": "Średni",
|
||||
"2": "Ciężki",
|
||||
"3": "NC"
|
||||
"3": "TD"
|
||||
},
|
||||
"tank_class_short_plural_lowercase": {
|
||||
"0": "lekkie",
|
||||
"1": "średnie",
|
||||
"2": "ciężkie",
|
||||
"3": "NC-ów"
|
||||
"3": "TDs"
|
||||
},
|
||||
"tank_class_medium": {
|
||||
"-1": "nierozpoznany",
|
||||
"-1": "unrecognized",
|
||||
"0": "Lekki",
|
||||
"1": "Średni",
|
||||
"2": "Ciężki",
|
||||
"3": "Niszczyciel czołgów"
|
||||
},
|
||||
"tree_type": {
|
||||
"-1": "nierozpoznany",
|
||||
"0": "Drzewko technologiczne",
|
||||
"-1": "unrecognized",
|
||||
"0": "Tech tree",
|
||||
"1": "Premium",
|
||||
"2": "Kolekcjonerski"
|
||||
},
|
||||
@@ -182,229 +187,223 @@
|
||||
},
|
||||
"website": {
|
||||
"redirect": {
|
||||
"title": "301: przeniesiony na stałe",
|
||||
"body": "Ta strona została przeniesiona. Przekierowywanie..."
|
||||
"title": "301: moved permanently",
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ustawienia",
|
||||
"description": "Zmień ustawienia BlitzKit",
|
||||
"dev_mode": "Tryb deweloperski",
|
||||
"auto_load_models": "Automatycznie wczytaj modele",
|
||||
"language": "Język"
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "Development mode",
|
||||
"auto_load_models": "Auto-load models",
|
||||
"language": "Language"
|
||||
},
|
||||
"common": {
|
||||
"player_search": {
|
||||
"hint": "Szukaj graczy...",
|
||||
"no_results": "Brak wyników",
|
||||
"searching": "Wyszukiwanie..."
|
||||
"hint": "Search players...",
|
||||
"no_results": "No results",
|
||||
"searching": "Searching..."
|
||||
},
|
||||
"copy_button": {
|
||||
"copied": "Skopiowano!"
|
||||
"copied": "Copied!"
|
||||
},
|
||||
"tank_search": {
|
||||
"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)",
|
||||
"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)",
|
||||
"no_tanks_found": {
|
||||
"body": "Nie znaleziono czołgu.",
|
||||
"body": "No tanks found.",
|
||||
"search_again": "",
|
||||
"clear_filters": "Spróbuj wyczyścić filtry."
|
||||
"clear_filters": "Try clearing filters."
|
||||
},
|
||||
"sort": {
|
||||
"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",
|
||||
"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",
|
||||
"fire.tertiaryPenetration": "tertiary penetration",
|
||||
"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.damage": "damage",
|
||||
"fire.moduleDamage": "module damage",
|
||||
"fire.shellVelocity": "shell velocity",
|
||||
"fire.shellRange": "shell range",
|
||||
"fire.shellCapacity": "shell capacity",
|
||||
"fire.aimTime": "aim time",
|
||||
"fire.dispersionStill": "dispersion still",
|
||||
"fire.dispersionMoving": "dispersion moving",
|
||||
"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",
|
||||
"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",
|
||||
"maneuverability.traverseSpeed": "traverse speed"
|
||||
},
|
||||
"sort_dropdown": {
|
||||
"label": "Filtruj",
|
||||
"label": "Sort",
|
||||
"groups": {
|
||||
"sort": {
|
||||
"no_sort": "Brak filtrów",
|
||||
"label": "Filtruj według",
|
||||
"fire": "Siła ognia",
|
||||
"maneuverability": "Mobilność ",
|
||||
"survivability": "Przetrwanie"
|
||||
"no_sort": "No sort",
|
||||
"label": "Sort by",
|
||||
"fire": "Fire",
|
||||
"maneuverability": "Maneuverability",
|
||||
"survivability": "Survivability"
|
||||
},
|
||||
"direction": {
|
||||
"label": "Direction",
|
||||
"ascending": "Rosnąco",
|
||||
"descending": "Malejąco"
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"warnings": {
|
||||
"test": "Wartości statystyk czołgów w fazie testowej mogą ulec zmianie."
|
||||
"test": "Tanks in testing are subject to change and may not represent the final product."
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"discord": {
|
||||
"join_short": "Dołącz do elity!",
|
||||
"join_long": "Dołącz do topowej elity!",
|
||||
"join_short": "Join cool people!",
|
||||
"join_long": "Join super duper cool people!",
|
||||
"action": "Discord"
|
||||
},
|
||||
"hero": {
|
||||
"credit": "Według {name} ♥️"
|
||||
"credit": "By {name} ❤️"
|
||||
},
|
||||
"hot_tanks": {
|
||||
"title": "Popularne czołgi",
|
||||
"subtitle": "Ostatnie {days} dni"
|
||||
"title": "Popular tanks",
|
||||
"subtitle": "Last {days} days"
|
||||
},
|
||||
"other_projects": {
|
||||
"title": "Inne epickie projekty"
|
||||
"title": "Other cool projects"
|
||||
},
|
||||
"help_translate": "Proszę pomóż przetłumaczyć {name} na {language}!",
|
||||
"seo_description": "Narzędzia do wszystkiego World of Tanks Blitz"
|
||||
"help_translate": "Please help translate {name} to {language}!",
|
||||
"seo_description": "Tools for everything World of Tanks Blitz"
|
||||
},
|
||||
"docs": {
|
||||
"patches": {
|
||||
"name": "Zapisy zmian BlitzKit",
|
||||
"seo_description": "Zapisy zmian BlitzKit: Discord bot i strona internetowa"
|
||||
"name": "BlitzKit Changelogs",
|
||||
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"credits": {
|
||||
"name": "Credits",
|
||||
"button": "View people",
|
||||
"description": "Contributors of BlitzKit",
|
||||
"seo_description": "BlitzKit is the work of many"
|
||||
},
|
||||
"playlist": {
|
||||
"reset": "Resetuj",
|
||||
"shuffle": "Wymieszaj",
|
||||
"remove": "Usuń czołgi, którymi grałem",
|
||||
"tracking": "BlitzKit zapisuje twoje bitwy",
|
||||
"reset": "Reset",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "Filtruj według",
|
||||
"last_played": "Ostatnio używane czołgi",
|
||||
"least_played": "Najmniej używane czołgi"
|
||||
"button": "Sort by",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Nigdy",
|
||||
"tank": "Czołg",
|
||||
"last_played": "Ostatnio używane czołgi",
|
||||
"battles": "Wszystkie bitwy na koncie",
|
||||
"never": "Never",
|
||||
"tank": "Tank",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "Zwycięstwa"
|
||||
},
|
||||
"large_warning": {
|
||||
"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ź"
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"mixer": {
|
||||
"name": "Mikser",
|
||||
"name": "Mixer",
|
||||
"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": "Zrzut ekranu",
|
||||
"randomize": "Wymieszaj"
|
||||
"screenshot": "Screenshot",
|
||||
"randomize": "Randomize"
|
||||
},
|
||||
"guess": {
|
||||
"name": "Zgadnij",
|
||||
"name": "Guess",
|
||||
"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": "Wpisz odpowiedź",
|
||||
"skip": "Pomiń",
|
||||
"guess": "Zgaduję",
|
||||
"next": "Następny",
|
||||
"placeholder": "Type to guess...",
|
||||
"skip": "Skip",
|
||||
"guess": "Guess",
|
||||
"next": "Next",
|
||||
"no_results": "Brak wyników"
|
||||
},
|
||||
"stats": "{correct} / {total} correct, {streak} streak",
|
||||
"help": {
|
||||
"title": "Odsłonić część czołgu?",
|
||||
"title": "Partially reveal tank?",
|
||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||
"cancel": "Anuluj",
|
||||
"reveal": "Pokaż"
|
||||
"cancel": "Cancel",
|
||||
"reveal": "Reveal"
|
||||
},
|
||||
"clear": "Wyczyść",
|
||||
"select_all": "Zaznacz wszystko ",
|
||||
"tier": "Poziom czołgu {tier}"
|
||||
"clear": "Clear",
|
||||
"select_all": "Select all",
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
"tankopedia": {
|
||||
"search": "Wyszukaj statystykę...",
|
||||
"search": "Search a stat...",
|
||||
"name": "Tankopedia",
|
||||
"button": "Znajdź czołg",
|
||||
"description": "Encyklopedia czołgów w WoTb",
|
||||
"button": "Find tanks",
|
||||
"description": "Blitz tank encyclopedia",
|
||||
"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": "Pomiń"
|
||||
"skip": "Skip"
|
||||
},
|
||||
"assault": {
|
||||
"title": "Assault shells"
|
||||
},
|
||||
"rotator": {
|
||||
"hull": "Kadłub",
|
||||
"turret": "Wieża",
|
||||
"hard": "Utwardzony",
|
||||
"hull": "Hull",
|
||||
"turret": "Turret",
|
||||
"hard": "Hard",
|
||||
"medium": "Średni",
|
||||
"soft": "Miękki",
|
||||
"effective": "Rzeczywisty"
|
||||
"soft": "Soft",
|
||||
"effective": "Effective"
|
||||
},
|
||||
"ricochet": {
|
||||
"angle": "Kąt",
|
||||
"angle": "Angle",
|
||||
"normalization": "Normalization",
|
||||
"effective_angle": "Rzeczywisty kąt nachylenia pancerza",
|
||||
"armor": "Pancerz",
|
||||
"no_ricochet": "Pociski odłamkowe nie rykoszetują."
|
||||
"effective_angle": "Effective angle",
|
||||
"armor": "Armor",
|
||||
"no_ricochet": "Explosive shells don't ricochet"
|
||||
},
|
||||
"view_range": {
|
||||
"camo": "Kamuflaż",
|
||||
"ghost_factory": "Fabryka duchów",
|
||||
"dead_rail": "Martwy tor"
|
||||
"camo": "Camo",
|
||||
"ghost_factory": "Ghost Factory",
|
||||
"dead_rail": "Dead Rail"
|
||||
},
|
||||
"flexibility": {
|
||||
"yaw": "Yaw",
|
||||
@@ -444,71 +443,71 @@
|
||||
"ricochet": "Ricochet",
|
||||
"explosion_radius": "Explosion radius",
|
||||
"exit": "Exit",
|
||||
"reset": "Zresetuj"
|
||||
"reset": "Reset"
|
||||
},
|
||||
"advanced": "Zaawansowane podświetlanie",
|
||||
"advanced": "Advanced highlighting",
|
||||
"shot_card": {
|
||||
"element": {
|
||||
"primary": "Główny",
|
||||
"primary": "Primary",
|
||||
"spaced": "Spaced",
|
||||
"external": "Zewnętrzny",
|
||||
"gap": "Odstęp",
|
||||
"gun": "Działo",
|
||||
"track": "Gąsienice"
|
||||
"external": "External",
|
||||
"gap": "Gap",
|
||||
"gun": "Gun",
|
||||
"track": "Tracks"
|
||||
},
|
||||
"status": {
|
||||
"penetration": "{damage} penetracja punktów wytrzymałości",
|
||||
"blocked": "Zablokowany",
|
||||
"ricochet": "Rykoszet",
|
||||
"splash": "{damage} rozprysk punktów wytrzymałości"
|
||||
"penetration": "{damage}hp penetration",
|
||||
"blocked": "Blocked",
|
||||
"ricochet": "Ricochet",
|
||||
"splash": "{damage}hp splash"
|
||||
},
|
||||
"stats": {
|
||||
"thickness_and_angle": "{thickness}mm {angle}",
|
||||
"nominal": "{thickness}mm nominalny",
|
||||
"ricochet_loss": "{loss}% penetracji"
|
||||
"thickness_and_angle": "{thickness}mm {angle}°",
|
||||
"nominal": "{thickness}mm nominal",
|
||||
"ricochet_loss": "{loss}% penetration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"static": {
|
||||
"name": "Pancerz statyczny",
|
||||
"primary": "Główny Pancerz",
|
||||
"spaced": "Ekran przeciwkumulacyjny",
|
||||
"external": "Zewnętrzny",
|
||||
"name": "Static armor",
|
||||
"primary": "Primary",
|
||||
"spaced": "Spaced",
|
||||
"external": "External",
|
||||
"edit": {
|
||||
"label": "Edytuj",
|
||||
"reset": "Zresetuj"
|
||||
"label": "Edit",
|
||||
"reset": "Reset"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"back": "Powrót",
|
||||
"compare": "Porównaj",
|
||||
"back": "Back",
|
||||
"compare": "Compare",
|
||||
"swap": {
|
||||
"button": "Zamień",
|
||||
"title": "Zamień czołgi"
|
||||
"button": "Swap",
|
||||
"title": "Swap tanks"
|
||||
},
|
||||
"vanilla": "Vanilla",
|
||||
"opentest": "OpenTest",
|
||||
"full_name": "Pełna nazwa",
|
||||
"full_name": "Full name",
|
||||
"nation": "Naród",
|
||||
"class": "Klasa",
|
||||
"tier": "Poziom",
|
||||
"type": "Rodzaj",
|
||||
"dev_id": "DEV: ID",
|
||||
"dev_name": "DEV: NAZWA",
|
||||
"purchase": "Cena zakupu",
|
||||
"restoration": "Cena odzyskania",
|
||||
"dev_name": "DEV: NAME",
|
||||
"purchase": "Purchase price",
|
||||
"restoration": "Restoration price",
|
||||
"sale": "Sale price",
|
||||
"research": "Research XP"
|
||||
},
|
||||
"tech_tree": {
|
||||
"title": "Drzewko technologiczne",
|
||||
"battle": "1 bitwa",
|
||||
"battles": "{count} bitew",
|
||||
"route": "Ścieżka {index} z {count}"
|
||||
"title": "Tech tree",
|
||||
"battle": "1 battle",
|
||||
"battles": "{count} battles",
|
||||
"route": "Route {index} of {count}"
|
||||
},
|
||||
"game_modes": {
|
||||
"title": "Umiejętności trybu gry"
|
||||
"title": "Game mode abilities"
|
||||
},
|
||||
"review": {
|
||||
"title": "Review videos",
|
||||
@@ -521,26 +520,26 @@
|
||||
"equalize": "Equalize"
|
||||
},
|
||||
"modules": {
|
||||
"title": "Moduły",
|
||||
"stock": "Standardowy",
|
||||
"upgrade": "Ulepsz"
|
||||
"title": "Modules",
|
||||
"stock": "Stock",
|
||||
"upgrade": "Upgrade"
|
||||
},
|
||||
"equipment": {
|
||||
"title": "Wyposażenie",
|
||||
"clear": "Wyczyść"
|
||||
"title": "Equipment",
|
||||
"clear": "Clear"
|
||||
},
|
||||
"provisions": {
|
||||
"title": "Materiały eksploatacyjne",
|
||||
"clear": "Wyczyść"
|
||||
"title": "Provisions",
|
||||
"clear": "Clear"
|
||||
},
|
||||
"consumables": {
|
||||
"title": "Consumables",
|
||||
"clear": "Wyczyść",
|
||||
"clear": "Clear",
|
||||
"info": "Yellow consumables do not affect the statistics."
|
||||
},
|
||||
"skills": {
|
||||
"title": "Crew skills",
|
||||
"clear": "Wyczyść",
|
||||
"clear": "Clear",
|
||||
"maximize": "Maximize",
|
||||
"info": "Yellow skills only apply under special circumstances. Hold Space to quickly toggle between level 0 and 7."
|
||||
},
|
||||
@@ -558,49 +557,49 @@
|
||||
},
|
||||
"values": {
|
||||
"shell_index": "Shell {index}",
|
||||
"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",
|
||||
"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",
|
||||
"shellNormalization": "Normalization",
|
||||
"shellRicochet": "Ricochet",
|
||||
"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",
|
||||
"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",
|
||||
"caliber": "Caliber",
|
||||
"penetration": "Penetration",
|
||||
"damage": "Uszkodzenia",
|
||||
"damageWithoutAssault": "Uszkodzenia bez pocisków szturmowych",
|
||||
"clipDamage": "Potencjał magazynu",
|
||||
"moduleDamage": "Uszkodzenia modułów",
|
||||
"explosionRadius": "Promień eksplozji",
|
||||
"shellVelocity": "Prędkość pocisku",
|
||||
"aimTime": "Czas celowania",
|
||||
"damageWithoutAssault": "Damage without assault shells",
|
||||
"clipDamage": "Clipping potential",
|
||||
"moduleDamage": "Module damage",
|
||||
"explosionRadius": "Explosion radius",
|
||||
"shellVelocity": "Shell velocity",
|
||||
"aimTime": "Aim time",
|
||||
"dispersion": "Still",
|
||||
"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",
|
||||
"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",
|
||||
"gunRearElevation": "Rear elevation",
|
||||
"azimuthLeft": "Azimuth left",
|
||||
"azimuthRight": "Azimuth right",
|
||||
@@ -627,7 +626,7 @@
|
||||
"trackRepairTime": "Track repair time",
|
||||
"viewRange": "View range",
|
||||
"camouflageStill": "Still",
|
||||
"camouflageMoving": "Podczas ruchu",
|
||||
"camouflageMoving": "Moving",
|
||||
"camouflageShootingStill": "Shooting while still",
|
||||
"camouflageShootingMoving": "Shooting while moving",
|
||||
"camouflageCaughtOnFire": "Caught on fire",
|
||||
@@ -649,7 +648,7 @@
|
||||
"crew": {
|
||||
"title": "Crew training",
|
||||
"info": "represents the other roles a crew member can take.",
|
||||
"count": "Liczba członków załogi",
|
||||
"count": "Crew count",
|
||||
"commander": "Commander",
|
||||
"gunner": "Gunner",
|
||||
"driver": "Driver",
|
||||
@@ -657,14 +656,14 @@
|
||||
"loader": "Loader"
|
||||
},
|
||||
"maneuverability": {
|
||||
"title": "Mobilność "
|
||||
"title": "Maneuverability"
|
||||
},
|
||||
"survivability": {
|
||||
"title": "Przetrwanie"
|
||||
"title": "Survivability"
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"name": "Porównaj",
|
||||
"name": "Compare",
|
||||
"button": "Compare tanks",
|
||||
"description": "Compare tank statistics",
|
||||
"seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz",
|
||||
@@ -674,7 +673,7 @@
|
||||
"title": "Add tanks",
|
||||
"description": "Select tanks to add to comparison"
|
||||
},
|
||||
"clear": "Wyczyść",
|
||||
"clear": "Clear",
|
||||
"deltas": {
|
||||
"none": "No deltas",
|
||||
"percentage": "Percentage",
|
||||
@@ -687,17 +686,17 @@
|
||||
},
|
||||
"table": {
|
||||
"equipment": {
|
||||
"clear": "Wyczyść",
|
||||
"clear": "Clear",
|
||||
"apply_to_all": "Apply to all"
|
||||
},
|
||||
"skills": {
|
||||
"clear": "Wyczyść",
|
||||
"clear": "Clear",
|
||||
"maximize": "Maximize",
|
||||
"equalize": "Equalize"
|
||||
},
|
||||
"modules": {
|
||||
"stock": "Standardowy",
|
||||
"upgrade": "Ulepsz"
|
||||
"stock": "Stock",
|
||||
"upgrade": "Upgrade"
|
||||
},
|
||||
"provisions": {
|
||||
"clear": "Clear this tank",
|
||||
@@ -762,14 +761,14 @@
|
||||
"seo_description": "Track your stats in real time as you play World of Tanks Blitz",
|
||||
"search": {
|
||||
"lookup": "Look up a player to get started",
|
||||
"hint": "Szukaj graczy...",
|
||||
"searching": "Wyszukiwanie...",
|
||||
"hint": "Search players...",
|
||||
"searching": "Searching...",
|
||||
"no_results": "No players found"
|
||||
},
|
||||
"controls": {
|
||||
"title": "Tracking {name}",
|
||||
"subtitle": "Since {time}",
|
||||
"reset": "Zresetuj",
|
||||
"reset": "Reset",
|
||||
"change": "Change"
|
||||
},
|
||||
"no_cc": {
|
||||
@@ -793,7 +792,7 @@
|
||||
"description": "Rank Blitz tanks",
|
||||
"seo_description": "Explore tier lists ranking tanks from best to worst, based on player ratings and statistical performance in World of Tanks Blitz",
|
||||
"buttons": {
|
||||
"reset": "Zresetuj",
|
||||
"reset": "Reset",
|
||||
"share": "Share"
|
||||
}
|
||||
},
|
||||
@@ -829,7 +828,7 @@
|
||||
"success": {
|
||||
"title": "Copy URL",
|
||||
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
|
||||
"back": "Powrót",
|
||||
"back": "Back",
|
||||
"copy": "Copy URL"
|
||||
}
|
||||
},
|
||||
@@ -840,7 +839,7 @@
|
||||
},
|
||||
"customize": {
|
||||
"title": "Customize",
|
||||
"reset": "Zresetuj",
|
||||
"reset": "Reset",
|
||||
"description": "Bells and whistles of your embed"
|
||||
}
|
||||
},
|
||||
@@ -920,9 +919,10 @@
|
||||
},
|
||||
"footer": {
|
||||
"motto": "Everything World of Tanks Blitz",
|
||||
"association": "An Open Collective Project",
|
||||
"copyright": "Unaffiliated with Wargaming or any service",
|
||||
"about": {
|
||||
"title": "About",
|
||||
"credits": "Credits",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"patches": "Patches"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"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": {
|
||||
@@ -36,7 +41,7 @@
|
||||
"development": "Development",
|
||||
"opentest": "TesteAberto",
|
||||
"preview": "Preview",
|
||||
"production": "Produção"
|
||||
"production": "Production"
|
||||
},
|
||||
"locales": {
|
||||
"en": "Inglês",
|
||||
@@ -182,11 +187,11 @@
|
||||
},
|
||||
"website": {
|
||||
"redirect": {
|
||||
"title": "301: movido permanentemente",
|
||||
"title": "301: moved permanently",
|
||||
"body": "This page has moved. Redirecting..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
"title": "Settings",
|
||||
"description": "Change BlitzKit settings",
|
||||
"dev_mode": "Modo de desenvolvimento",
|
||||
"auto_load_models": "Auto-load models",
|
||||
@@ -202,9 +207,9 @@
|
||||
"copied": "Copiado!"
|
||||
},
|
||||
"tank_search": {
|
||||
"select_dialog_title": "Procura por tanque",
|
||||
"select_dialog_title": "Search for tanks",
|
||||
"swap_dialog_title": "Swap tank",
|
||||
"clear_filters": "Limpar filtros",
|
||||
"clear_filters": "Clear filters",
|
||||
"login": "Login to use this",
|
||||
"search_bar_hint": "Procurar tanques...",
|
||||
"recent": "Visto recentemente",
|
||||
@@ -302,12 +307,6 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"credits": {
|
||||
"name": "Credits",
|
||||
"button": "View people",
|
||||
"description": "Contributors of BlitzKit",
|
||||
"seo_description": "BlitzKit is the work of many"
|
||||
},
|
||||
"playlist": {
|
||||
"reset": "Restaurar",
|
||||
"shuffle": "Shuffle",
|
||||
@@ -920,9 +919,10 @@
|
||||
},
|
||||
"footer": {
|
||||
"motto": "Tudo World of Tanks Blitz",
|
||||
"association": "An Open Collective Project",
|
||||
"copyright": "Não afiliado à Wargaming ou qualquer serviço",
|
||||
"about": {
|
||||
"title": "Sobre",
|
||||
"credits": "Créditos",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"patches": "Atualizações"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"test": {
|
||||
"default": "тест",
|
||||
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт — {fruit}, потому что это цвет {color}. Он мне так нравится, так что я скажу его 3 раза {fruit}! {fruit}! {fruit}! И ещё раз, это {color}, а не {not_color}.",
|
||||
"fox": "Съешь же ещё этих мягких французских булок, да выпей чаю."
|
||||
},
|
||||
"common": {
|
||||
"name": "BlitzKit",
|
||||
"units": {
|
||||
@@ -33,10 +38,10 @@
|
||||
},
|
||||
"branches": {
|
||||
"beta": "Бета",
|
||||
"development": "Разработка",
|
||||
"development": "Development",
|
||||
"opentest": "Открытый тест",
|
||||
"preview": "Предпросмотр",
|
||||
"production": "Производство"
|
||||
"preview": "Preview",
|
||||
"production": "Production"
|
||||
},
|
||||
"locales": {
|
||||
"en": "Английский",
|
||||
@@ -189,7 +194,7 @@
|
||||
"title": "Настройки",
|
||||
"description": "Изменить настройки BlitzKit",
|
||||
"dev_mode": "Режим разработчика",
|
||||
"auto_load_models": "Автозагрузка моделей",
|
||||
"auto_load_models": "Auto-load models",
|
||||
"language": "Язык"
|
||||
},
|
||||
"common": {
|
||||
@@ -302,12 +307,6 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"credits": {
|
||||
"name": "Credits",
|
||||
"button": "View people",
|
||||
"description": "Contributors of BlitzKit",
|
||||
"seo_description": "BlitzKit is the work of many"
|
||||
},
|
||||
"playlist": {
|
||||
"reset": "Сброс",
|
||||
"shuffle": "Перемешать",
|
||||
@@ -414,7 +413,7 @@
|
||||
}
|
||||
},
|
||||
"sandbox": {
|
||||
"load_model": "Загрузить модель",
|
||||
"load_model": "Load model",
|
||||
"poses": {
|
||||
"hull_down": "За холмом",
|
||||
"face_hug": "В упор",
|
||||
@@ -575,7 +574,7 @@
|
||||
"dpm": "ДПМ",
|
||||
"dpmEffective": "Эффективность за 60с",
|
||||
"peekabooDpm": "Peekaboo DPM",
|
||||
"peekabooDpmInfo": "Урон в минуту при условии, если вы сбросите камуфляж(или перезарядка окончена, если она медленнее) - не менее 10 секунд между выстрелами.",
|
||||
"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": "Снарядов в барабане",
|
||||
"mostOptimalShellIndex": "Самый эффективный снаряд",
|
||||
"shellReloads": "Перезарядка",
|
||||
@@ -920,9 +919,10 @@
|
||||
},
|
||||
"footer": {
|
||||
"motto": "Всё о World of Tanks Blitz",
|
||||
"association": "An Open Collective Project",
|
||||
"copyright": "Не связан с Wargaming или иным сервисом",
|
||||
"about": {
|
||||
"title": "О сайте",
|
||||
"credits": "Авторы",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"patches": "Патчи"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"test": {
|
||||
"default": "тест",
|
||||
"long": "{color}{fruit}",
|
||||
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
|
||||
},
|
||||
"common": {
|
||||
"name": "BlitzKit",
|
||||
"units": {
|
||||
@@ -302,12 +307,6 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"credits": {
|
||||
"name": "Credits",
|
||||
"button": "View people",
|
||||
"description": "Contributors of BlitzKit",
|
||||
"seo_description": "BlitzKit is the work of many"
|
||||
},
|
||||
"playlist": {
|
||||
"reset": "Скинути",
|
||||
"shuffle": "Перемішати",
|
||||
@@ -365,7 +364,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": "Вибрати все",
|
||||
@@ -920,9 +919,10 @@
|
||||
},
|
||||
"footer": {
|
||||
"motto": "Everything World of Tanks Blitz",
|
||||
"association": "An Open Collective Project",
|
||||
"copyright": "Unaffiliated with Wargaming or any service",
|
||||
"about": {
|
||||
"title": "About",
|
||||
"credits": "Credits",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"patches": "Patches"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"test": {
|
||||
"default": "测试",
|
||||
"long": "这是一个测试用长字符串。你绝对不会在用户界面中看见这些。我最喜欢的水果是{fruit}因为它的颜色是{color}的。我如此喜欢它,以至于我会把它的名字念3次!{fruit}!{fruit}!{fruit}!重申一次,它是{color}的而不是{not_color}的。",
|
||||
"fox": "褐色的狐狸迅速地跳过那只懒狗"
|
||||
},
|
||||
"common": {
|
||||
"name": "BlitzKit",
|
||||
"units": {
|
||||
@@ -302,12 +307,6 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"credits": {
|
||||
"name": "Credits",
|
||||
"button": "View people",
|
||||
"description": "Contributors of BlitzKit",
|
||||
"seo_description": "BlitzKit is the work of many"
|
||||
},
|
||||
"playlist": {
|
||||
"reset": "重置",
|
||||
"shuffle": "换一批",
|
||||
@@ -920,9 +919,10 @@
|
||||
},
|
||||
"footer": {
|
||||
"motto": "《坦克世界:闪击战》全方位工具",
|
||||
"association": "An Open Collective Project",
|
||||
"copyright": "本网站与 Wargaming战游网 或任何服务无关",
|
||||
"about": {
|
||||
"title": "关于",
|
||||
"credits": "鸣谢",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"patches": "补丁"
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
"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-smart-dlc": "bun src/downloadSmartDLC/index.ts",
|
||||
"vfs": "bun src/vfs/index.ts"
|
||||
"download-client": "bun src/downloadClient/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blitzkit/closed": "workspace:*",
|
||||
|
||||
@@ -2,16 +2,9 @@ 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 { RegionsPublish } from "../types/regionsPublish";
|
||||
import { vfs } from "../vfs/vfs";
|
||||
|
||||
const CLIENT_DIR = assertSecret(import.meta.env.CLIENT_DIR);
|
||||
|
||||
const regionsPublish = await vfs.yaml<RegionsPublish>(
|
||||
"Data/regions_publish.yaml",
|
||||
);
|
||||
const [smartDlcUrl] = regionsPublish.Common.smartDlcUrls;
|
||||
import { PATCHES_ROOT, vfs } from "./buildAssets/constants";
|
||||
|
||||
const versionTextFile = await vfs.text(`Data/version.txt`);
|
||||
const currentVersion = versionTextFile
|
||||
@@ -25,7 +18,9 @@ console.log(`Installing patches for ${currentVersion}...`);
|
||||
let patchIndex = 1;
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${smartDlcUrl}/s${currentVersion}_${patchIndex}.yaml`,
|
||||
`${assertSecret(
|
||||
import.meta.env.WOTB_DLC_CDN,
|
||||
)}/dlc/s${currentVersion}_${patchIndex}.yaml`,
|
||||
);
|
||||
|
||||
console.log(patchIndex, response.status);
|
||||
@@ -35,34 +30,51 @@ while (true) {
|
||||
console.log(`Applying patch ${patchIndex}...`);
|
||||
|
||||
const data = parseYaml(await response.text());
|
||||
const dvpm = await fetch(`${smartDlcUrl}/${data.dx11}`).then((response) =>
|
||||
response.arrayBuffer(),
|
||||
);
|
||||
const dvpm = await fetch(
|
||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11}`,
|
||||
).then((response) => response.arrayBuffer());
|
||||
const dvpd = await fetch(
|
||||
`${smartDlcUrl}/${data.dx11.replace(".dvpm", ".dvpd")}`,
|
||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${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);
|
||||
|
||||
await mkdir(`${CLIENT_DIR}/Data/${dir}`, { recursive: true });
|
||||
try {
|
||||
await mkdir(`${PATCHES_ROOT}/Data/${dir}`, { recursive: true });
|
||||
} catch (error) {
|
||||
console.warn(`Failed to make directory "${dir}"`);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(data);
|
||||
|
||||
await writeFile(`${CLIENT_DIR}/Data/${path}`, new Uint8Array(buffer));
|
||||
await writeFile(`${PATCHES_ROOT}/Data/${path}`, new Uint8Array(buffer));
|
||||
|
||||
bar.tick();
|
||||
}
|
||||
|
||||
console.log("patches done for patch");
|
||||
|
||||
if ("dynamicContentLocalizationsDir" in data) {
|
||||
console.log(
|
||||
`${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`,
|
||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${
|
||||
data.dynamicContentLocalizationsDir
|
||||
}/en.yaml`,
|
||||
);
|
||||
const localizationsResponse = await fetch(
|
||||
`${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`,
|
||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${
|
||||
data.dynamicContentLocalizationsDir
|
||||
}/en.yaml`,
|
||||
);
|
||||
console.log("fetched");
|
||||
const newStrings = parseYaml(await localizationsResponse.text());
|
||||
@@ -74,10 +86,10 @@ while (true) {
|
||||
const patchedContent = stringifyYaml(patchedStrings);
|
||||
console.log("stringified");
|
||||
|
||||
await mkdir(`${CLIENT_DIR}/Data/Strings`, { recursive: true });
|
||||
await mkdir(`${PATCHES_ROOT}/Data/Strings`, { recursive: true });
|
||||
console.log("mkdir");
|
||||
await writeFile(
|
||||
`${CLIENT_DIR}/Data/Strings/en.yaml`,
|
||||
`${PATCHES_ROOT}/Data/Strings/en.yaml`,
|
||||
new Uint8Array(Buffer.from(patchedContent)),
|
||||
);
|
||||
console.log("file");
|
||||
@@ -9,8 +9,6 @@ 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);
|
||||
@@ -45,27 +43,11 @@ if (provider === "steam") {
|
||||
const username = assertSecret(import.meta.env.STEAM_USERNAME);
|
||||
const password = assertSecret(import.meta.env.STEAM_PASSWORD);
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
await $`${DEPOT_DOWNLOADER_EXECUTABLE} -os ${
|
||||
platform
|
||||
} -app ${id} -username ${username} -password ${
|
||||
password
|
||||
} -dir ${dir} -max-downloads ${DEPOT_DOWNLOADER_MAX_DOWNLOADS}`;
|
||||
} else if (provider === "zip") {
|
||||
const url = assertSecret(import.meta.env.CLIENT_SOURCE);
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export interface RegionsPublish {
|
||||
Common: Common;
|
||||
}
|
||||
|
||||
interface Common {
|
||||
smartDlcUrls: string[];
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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));
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
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));
|
||||
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import { tankIcon } from "@blitzkit/core";
|
||||
import { Flex, Heading, Link, Strong, Text } from "@radix-ui/themes";
|
||||
---
|
||||
|
||||
<Flex justify="center" px="4">
|
||||
<Link
|
||||
color="gray"
|
||||
highContrast
|
||||
style={{ display: "content" }}
|
||||
href="/docs/guides/christmas-2025"
|
||||
underline="none"
|
||||
>
|
||||
<Flex
|
||||
mt="3"
|
||||
px="5"
|
||||
py="4"
|
||||
style={{
|
||||
borderRadius: "var(--radius-3)",
|
||||
backgroundImage: "url(https://i.imgur.com/LRYmSkg.png)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
align="center"
|
||||
gap="5"
|
||||
>
|
||||
<img src={tankIcon(25169)} style={{ height: "min(15vw, 5rem)" }} />
|
||||
|
||||
<Flex direction="column">
|
||||
<Heading size={{ initial: "4", sm: "6" }}>Christmas 2025</Heading>
|
||||
<Text size={{ initial: "2", sm: "3" }}>
|
||||
Win the <Strong>GSOR the TANK</Strong> and more!
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Link>
|
||||
</Flex>
|
||||
@@ -1,36 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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 (
|
||||
<Flex align="center" gap="4" direction="column" className={styles.outer}>
|
||||
<Text size="3" className={styles.important}>
|
||||
<Flex gap="2" align="center">
|
||||
<img src="https://github.com/tresabhi.png" />
|
||||
<Em>We need your attention</Em>
|
||||
</Flex>
|
||||
</Text>
|
||||
|
||||
<Flex direction="column" className={styles.center} gap="4">
|
||||
<Flex direction="column" gap="3">
|
||||
<Text color="tomato">
|
||||
We can no longer keep BlitzKit sustainable for <Strong>free</Strong>
|
||||
.
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
We want to add tank modules, player statistics, rebalancing patch
|
||||
notes, bring back the Discord bot, and more.
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
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.
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3">
|
||||
{donationTiers.map(({ name, value, link }) => (
|
||||
<a target="_blank" key={name} href={link}>
|
||||
<Button size="3" color="crimson">
|
||||
{value === null && <HeartFilledIcon />}
|
||||
{value === null ? "Custom" : `$${value}`}
|
||||
</Button>
|
||||
</a>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
---
|
||||
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);
|
||||
---
|
||||
|
||||
<style>
|
||||
.credits {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
padding: var(--space-4);
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
|
||||
width: 100%;
|
||||
max-width: 80rem;
|
||||
|
||||
.list {
|
||||
gap: var(--space-5);
|
||||
|
||||
.link {
|
||||
display: contents;
|
||||
|
||||
.person {
|
||||
--profile-size: 5rem;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: var(--space-4);
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--profile-size) / 2);
|
||||
gap: var(--space-1);
|
||||
|
||||
box-shadow: var(--shadow-3);
|
||||
border-radius: var(--radius-3);
|
||||
|
||||
.contain {
|
||||
border-radius: inherit;
|
||||
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.hint {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
filter: saturate(20) brightness(0.125) blur(4rem);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
|
||||
position: relative;
|
||||
|
||||
margin-top: calc(var(--profile-size) / -2);
|
||||
|
||||
object-fit: cover;
|
||||
width: var(--profile-size);
|
||||
height: var(--profile-size);
|
||||
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="credits">
|
||||
<div class="inner">
|
||||
<Grid
|
||||
className="list"
|
||||
py="4"
|
||||
columns="repeat(auto-fill, minmax(16rem, 1fr))"
|
||||
flow="dense"
|
||||
gap="4"
|
||||
gapY="8"
|
||||
>
|
||||
{
|
||||
credits.map((person) => (
|
||||
<a target="_blank" href={person.url} class="link">
|
||||
<div class="person">
|
||||
<div class="contain">
|
||||
<div
|
||||
class="hint"
|
||||
style={`background-image: url(${person.image})`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<img src={person.image} />
|
||||
|
||||
<Text weight="medium" className="text" color="gray" highContrast>
|
||||
{person.name}
|
||||
</Text>
|
||||
<Text className="text" color="gray" wrap="pretty">
|
||||
{person.description}
|
||||
</Text>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,9 +43,9 @@ const strings = getStrings(locale);
|
||||
}}
|
||||
>
|
||||
<Heading>BlitzKit</Heading>
|
||||
<Text size="2" color="gray">{strings.website.footer.motto}</Text>
|
||||
<Text size="2" color="gray">
|
||||
{strings.website.footer.association}
|
||||
<Text color="gray">{strings.website.footer.motto}</Text>
|
||||
<Text size="1" color="gray">
|
||||
{strings.website.footer.copyright}
|
||||
</Text>
|
||||
|
||||
<Box mt="2">
|
||||
@@ -84,6 +84,9 @@ const strings = getStrings(locale);
|
||||
sm: "start",
|
||||
}}
|
||||
>
|
||||
<LinkI18n {locale} href="/docs/about/credits" color="gray">
|
||||
{strings.website.footer.about.credits}
|
||||
</LinkI18n>
|
||||
<Link
|
||||
href="https://discord.gg/nDt7AjGJQH"
|
||||
target="_blank"
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useEffect, useState, type ReactNode } from "react";
|
||||
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
|
||||
import { InlineSkeleton } from "./InlineSkeleton";
|
||||
|
||||
const TIME = 1792598400 * 1000;
|
||||
const TIME = 1764898200 * 1000;
|
||||
|
||||
export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,14 +31,14 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
|
||||
string = (
|
||||
<>
|
||||
revealed in{" "}
|
||||
in{" "}
|
||||
<Code highContrast>
|
||||
{hours}h {minutes}m {seconds}s
|
||||
</Code>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
string = "have been revealed.";
|
||||
string = "has ended. Come again later!";
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -47,7 +47,7 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
color="gray"
|
||||
highContrast
|
||||
style={{ display: "content" }}
|
||||
href="https://preview.blitzkit.app/tanks"
|
||||
href="https://discord.gg/njQdBhNS?event=1446208951136813117"
|
||||
underline="none"
|
||||
>
|
||||
<Flex
|
||||
@@ -56,17 +56,17 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
py="4"
|
||||
style={{
|
||||
borderRadius: "var(--radius-3)",
|
||||
backgroundImage: "url(https://i.imgur.com/xgngxoy.png)",
|
||||
backgroundImage: "url(https://i.imgur.com/LRYmSkg.png)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
boxSizing: "border-box",
|
||||
boxShadow: "0 0 var(--space-7) var(--white-a2)",
|
||||
boxShadow: "0 0 var(--space-7) var(--blue-a5)",
|
||||
}}
|
||||
align="center"
|
||||
gap="5"
|
||||
>
|
||||
<img
|
||||
src="https://i.imgur.com/kXV3r3P.png"
|
||||
src="https://i.imgur.com/mvmJQgB.png"
|
||||
style={{
|
||||
height: "min(15vw, 5rem)",
|
||||
filter: "drop-shadow(0.5rem 0.5rem 1rem #00000080)",
|
||||
@@ -75,11 +75,11 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
|
||||
<Flex direction="column">
|
||||
<Heading size={{ initial: "4", sm: "6" }}>
|
||||
Tier XIs {!skeleton && string}{" "}
|
||||
Music Box giveaway {!skeleton && string}{" "}
|
||||
{skeleton && <InlineSkeleton width="10rem" />}
|
||||
</Heading>
|
||||
<Text size={{ initial: "2", sm: "3" }}>
|
||||
Get ready to go to the Tankopedia!
|
||||
Join the BlitzKit Discord server event
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useModel } from "../hooks/useModel";
|
||||
import { Guess, GuessState } from "../stores/guess";
|
||||
import { SmartCanvas } from "./SmartCanvas";
|
||||
import { TankModel } from "./TankModel";
|
||||
import { Controls } from "./Tankopedia/HeroSection/components/TankSandbox/components/Control";
|
||||
import { Lighting } from "./Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||
import { TankModel } from "./Tankopedia/HeroSection/components/TankSandbox/components/TankModel";
|
||||
|
||||
const BRIGHTNESS = 2 ** -5.5;
|
||||
const CONTRAST = 2 ** 2;
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { Link, Table } from '@radix-ui/themes';
|
||||
import MarkdownToJSX from 'markdown-to-jsx';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { docImageUrl, isExternalImage } from '../core/blitzkit/docImages';
|
||||
|
||||
interface MarkdownProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
function Image({ src, ...props }: ComponentProps<'img'>) {
|
||||
return (
|
||||
<img
|
||||
style={{ maxWidth: '100%' }}
|
||||
src={src && isExternalImage(src) ? docImageUrl(src) : src}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
function Image(props: ComponentProps<'img'>) {
|
||||
return <img style={{ maxWidth: '100%' }} {...props} />;
|
||||
}
|
||||
|
||||
export function Markdown({ children }: MarkdownProps) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
GearIcon,
|
||||
HamburgerMenuIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Box, Code, Flex, Grid, IconButton, Link } from "@radix-ui/themes";
|
||||
import { Box, Code, Flex, 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);
|
||||
<Flex
|
||||
className="content"
|
||||
width="100%"
|
||||
p="3"
|
||||
p={{ initial: "4", sm: "3" }}
|
||||
direction="column"
|
||||
align="center"
|
||||
>
|
||||
@@ -133,6 +133,41 @@ const strings = getStrings(locale);
|
||||
|
||||
<Box flexGrow="1" />
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
gap={{ initial: "3", md: "6" }}
|
||||
display={{ initial: "none", sm: "flex" }}
|
||||
justify="center"
|
||||
className="tools"
|
||||
>
|
||||
{
|
||||
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 (
|
||||
<LinkI18n
|
||||
{locale}
|
||||
weight={selected ? "medium" : "regular"}
|
||||
color="gray"
|
||||
highContrast={selected}
|
||||
size={{ initial: "1", md: "2" }}
|
||||
href={`/${tool.id}`}
|
||||
underline="hover"
|
||||
>
|
||||
{toolStrings.name}
|
||||
</LinkI18n>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
|
||||
<Box flexGrow="1" />
|
||||
|
||||
<Flex align="center" gap="4">
|
||||
<Link
|
||||
style={{ display: "flex", alignContent: "center" }}
|
||||
@@ -156,7 +191,7 @@ const strings = getStrings(locale);
|
||||
|
||||
<NavbarAccount {locale} client:idle />
|
||||
|
||||
<Flex align="center">
|
||||
<Flex align="center" display={{ initial: "flex", sm: "none" }}>
|
||||
<IconButton variant="ghost" color="gray" className="hamburger">
|
||||
<Cross1Icon data-expanded />
|
||||
<HamburgerMenuIcon data-closed />
|
||||
@@ -165,10 +200,11 @@ const strings = getStrings(locale);
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Grid
|
||||
<Flex
|
||||
direction="column"
|
||||
data-expanded
|
||||
columns="repeat(auto-fill, minmax(30rem, 1fr))"
|
||||
gap="3"
|
||||
display={{ initial: "flex", sm: "none" }}
|
||||
>
|
||||
{
|
||||
toolsFiltered.map((tool) => {
|
||||
@@ -193,7 +229,7 @@ const strings = getStrings(locale);
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
p="6"
|
||||
py="4"
|
||||
justify="end"
|
||||
flexGrow="1"
|
||||
style={{
|
||||
@@ -207,7 +243,7 @@ const strings = getStrings(locale);
|
||||
);
|
||||
})
|
||||
}
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function StupidCat() {
|
||||
return <img src="https://i.imgur.com/6FBBzb9.png" />;
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
import {
|
||||
invalidate,
|
||||
useThree,
|
||||
type ObjectMap,
|
||||
type ThreeEvent,
|
||||
} from "@react-three/fiber";
|
||||
import { useRef } from "react";
|
||||
import type { Texture } from "three";
|
||||
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
|
||||
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
|
||||
import { hasEquipment } from "../core/blitzkit/hasEquipment";
|
||||
import { modelTransformEvent } from "../core/blitzkit/modelTransform";
|
||||
import { controlsEnabledEvent } from "../core/controlsEnabled";
|
||||
import { useBatched } from "../hooks/useBatched";
|
||||
import { useModel } from "../hooks/useModel";
|
||||
import { useTankModelDefinition } from "../hooks/useTankModelDefinition";
|
||||
import { useTankTransform } from "../hooks/useTankTransform";
|
||||
import { Duel } from "../stores/duel";
|
||||
import { Tankopedia } from "../stores/tankopedia";
|
||||
import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants";
|
||||
import { ModelTankWrapper } from "./Armor/components/ModelTankWrapper";
|
||||
|
||||
interface SectionProps {
|
||||
map: ObjectMap;
|
||||
}
|
||||
|
||||
function Tracks({ map }: SectionProps) {
|
||||
const Batched = useBatched(map, true);
|
||||
|
||||
const position = useRef(new Vector2());
|
||||
const delta = useRef(new Vector2());
|
||||
|
||||
const textures = useRef(new Set<Texture | null>());
|
||||
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
delta.current.set(event.clientX, event.clientY).sub(position.current);
|
||||
position.current.set(event.clientX, event.clientY);
|
||||
|
||||
const deltaX = delta.current.x / window.innerWidth;
|
||||
const deltaY = delta.current.y / window.innerHeight;
|
||||
|
||||
const offset = deltaX + deltaY;
|
||||
|
||||
for (const texture of textures.current) {
|
||||
if (!texture) continue;
|
||||
texture.offset.y += offset;
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
function handlePointerUp() {
|
||||
controlsEnabledEvent.dispatch(true);
|
||||
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
if (Tankopedia.state.display !== TankopediaDisplay.Model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const material = (event.object as Mesh).material as MeshStandardMaterial;
|
||||
|
||||
textures.current = new Set([
|
||||
material.map,
|
||||
material.aoMap,
|
||||
material.normalMap,
|
||||
material.roughnessMap,
|
||||
material.metalnessMap,
|
||||
]);
|
||||
|
||||
position.current.set(event.clientX, event.clientY);
|
||||
event.stopPropagation();
|
||||
|
||||
controlsEnabledEvent.dispatch(false);
|
||||
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return (
|
||||
<Batched
|
||||
onPointerDown={handlePointerDown}
|
||||
slicer={(node) => {
|
||||
return node.name.startsWith("chassis_track_");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Hull({ map }: SectionProps) {
|
||||
const Batched = useBatched(map, true);
|
||||
|
||||
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
return (
|
||||
<Batched
|
||||
onPointerDown={handlePointerDown}
|
||||
slicer={(node) => {
|
||||
const isHull = node.name === "hull";
|
||||
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||
|
||||
return isHull || isWheel;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Turret({ map }: SectionProps) {
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||
|
||||
const Batched = useBatched(map, true);
|
||||
|
||||
const position = useRef(new Vector2());
|
||||
const delta = useRef(new Vector2());
|
||||
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
position.current.set(event.clientX, event.clientY);
|
||||
|
||||
controlsEnabledEvent.dispatch(false);
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
|
||||
delta.current.set(event.clientX, event.clientY).sub(position.current);
|
||||
position.current.set(event.clientX, event.clientY);
|
||||
|
||||
const [pitch, yaw] = applyPitchYawLimits(
|
||||
modelTransformEvent.last!.pitch,
|
||||
modelTransformEvent.last!.yaw +
|
||||
delta.current.x * (Math.PI / boundingRect.width),
|
||||
gunModelDefinition.pitch!,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
controlsEnabledEvent.dispatch(true);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return (
|
||||
<Batched
|
||||
onPointerDown={onPointerDown}
|
||||
slicer={(node) => {
|
||||
const isCurrentTurret =
|
||||
node.name ===
|
||||
`turret_${turretModelDefinition.model_id
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
|
||||
return isCurrentTurret;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Gun({ map }: SectionProps) {
|
||||
const Batched = useBatched(map, true);
|
||||
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||
|
||||
const position = useRef(new Vector2());
|
||||
const delta = useRef(new Vector2());
|
||||
const pitch = useRef(0);
|
||||
const yaw = useRef(0);
|
||||
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
delta.current.set(event.clientX, event.clientY).sub(position.current);
|
||||
position.current.set(event.clientX, event.clientY);
|
||||
|
||||
[pitch.current, yaw.current] = applyPitchYawLimits(
|
||||
pitch.current - delta.current.y * (Math.PI / boundingRect.height),
|
||||
yaw.current + delta.current.x * (Math.PI / boundingRect.width),
|
||||
gunModelDefinition.pitch!,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.dispatch({ pitch: pitch.current, yaw: yaw.current });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
controlsEnabledEvent.dispatch(true);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
controlsEnabledEvent.dispatch(false);
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
|
||||
position.current.set(event.clientX, event.clientY);
|
||||
pitch.current = modelTransformEvent.last!.pitch;
|
||||
yaw.current = modelTransformEvent.last!.yaw;
|
||||
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return (
|
||||
<Batched
|
||||
onPointerDown={onPointerDown}
|
||||
slicer={(node) => {
|
||||
const isCurrentMantlet =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}_mask`;
|
||||
const isCurrentGun =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`;
|
||||
|
||||
return isCurrentGun || isCurrentMantlet;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TankModel() {
|
||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||
|
||||
const track = Duel.use((state) => state.protagonist.track);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
|
||||
const hullContainer = useRef<Group>(null);
|
||||
const turretContainer = useRef<Group>(null!);
|
||||
const gunContainer = useRef<Group>(null!);
|
||||
|
||||
const { gltf } = useModel(protagonist.tank.id);
|
||||
|
||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||
|
||||
return (
|
||||
<ModelTankWrapper ref={hullContainer}>
|
||||
<Tracks map={gltf} />
|
||||
<Hull map={gltf} />
|
||||
|
||||
<group ref={turretContainer}>
|
||||
<Turret map={gltf} />
|
||||
|
||||
<group ref={gunContainer}>
|
||||
<Gun map={gltf} />
|
||||
</group>
|
||||
</group>
|
||||
</ModelTankWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
normalizeBoundingBox,
|
||||
resolveDpm,
|
||||
tankBoundingBox,
|
||||
unionBoundingBox,
|
||||
type TankDefinition,
|
||||
} from "@blitzkit/core";
|
||||
import { useMemo } from "react";
|
||||
@@ -112,7 +112,10 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
|
||||
case "survivability.volume": {
|
||||
const dimensions = normalizeBoundingBox(
|
||||
tankBoundingBox(tankModelDefinition, turretModelDefinition),
|
||||
unionBoundingBox(
|
||||
tankModelDefinition.bounding_box!,
|
||||
turretModelDefinition.bounding_box!,
|
||||
),
|
||||
);
|
||||
|
||||
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
|
||||
@@ -120,7 +123,10 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
|
||||
case "survivability.length": {
|
||||
const bounds = normalizeBoundingBox(
|
||||
tankBoundingBox(tankModelDefinition, turretModelDefinition),
|
||||
unionBoundingBox(
|
||||
tankModelDefinition.bounding_box!,
|
||||
turretModelDefinition.bounding_box!,
|
||||
),
|
||||
);
|
||||
|
||||
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
normalizeBoundingBox,
|
||||
resolveDpm,
|
||||
SEARCH_KEYS,
|
||||
tankBoundingBox,
|
||||
unionBoundingBox,
|
||||
type TankDefinition,
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
@@ -137,10 +137,16 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const bTurretModelDefinition =
|
||||
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
|
||||
const aSize = normalizeBoundingBox(
|
||||
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition),
|
||||
unionBoundingBox(
|
||||
aTankModelDefinition.bounding_box!,
|
||||
aTurretModelDefinition.bounding_box!,
|
||||
),
|
||||
);
|
||||
const bSize = normalizeBoundingBox(
|
||||
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition),
|
||||
unionBoundingBox(
|
||||
bTankModelDefinition.bounding_box!,
|
||||
bTurretModelDefinition.bounding_box!,
|
||||
),
|
||||
);
|
||||
const aVolume = aSize.x * aSize.y * aSize.z;
|
||||
const bVolume = bSize.x * bSize.y * bSize.z;
|
||||
@@ -158,10 +164,16 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const bTurretModelDefinition =
|
||||
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
|
||||
const aSize = normalizeBoundingBox(
|
||||
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition),
|
||||
unionBoundingBox(
|
||||
aTankModelDefinition.bounding_box!,
|
||||
aTurretModelDefinition.bounding_box!,
|
||||
),
|
||||
);
|
||||
const bSize = normalizeBoundingBox(
|
||||
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition),
|
||||
unionBoundingBox(
|
||||
bTankModelDefinition.bounding_box!,
|
||||
bTurretModelDefinition.bounding_box!,
|
||||
),
|
||||
);
|
||||
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
|
||||
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Flex, IconButton, type FlexProps } from "@radix-ui/themes";
|
||||
import type { ComponentProps, CSSProperties } from "react";
|
||||
|
||||
type SidebarIconButtonProps = ComponentProps<typeof IconButton> & {
|
||||
selected: boolean;
|
||||
src: string;
|
||||
alt: string;
|
||||
imageStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
export function SidebarIconGroup({ style, ...props }: FlexProps) {
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)", ...style }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarIconButton({
|
||||
selected,
|
||||
src,
|
||||
alt,
|
||||
imageStyle,
|
||||
...props
|
||||
}: SidebarIconButtonProps) {
|
||||
return (
|
||||
<IconButton
|
||||
color={selected ? undefined : "gray"}
|
||||
variant="soft"
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
radius="none"
|
||||
{...props}
|
||||
>
|
||||
<img
|
||||
alt={alt}
|
||||
src={src}
|
||||
style={{ width: "50%", height: "50%", ...imageStyle }}
|
||||
/>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { alias } from "@blitzkit/core";
|
||||
import { Flex, Tooltip } from "@radix-ui/themes";
|
||||
import { uniqBy } from "lodash-es";
|
||||
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
|
||||
import { api } from "../../../../../../core/blitzkit/api";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
||||
import { SidebarIconButton } from "./SidebarIconButton";
|
||||
|
||||
const camouflageDefinitions = await api.camouflageDefinitions();
|
||||
|
||||
export function SkinSwitcher() {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const skin = Tankopedia.use((state) => state.requestedSkin);
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||
const { unwrap } = useLocale();
|
||||
const skins = uniqBy(
|
||||
Object.values(camouflageDefinitions.camouflages).filter(
|
||||
(camouflage) => camouflage.tank_id === tank.id,
|
||||
),
|
||||
(camouflage) => unwrap(camouflage.name!),
|
||||
);
|
||||
|
||||
if (skins.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
top="50%"
|
||||
right={
|
||||
disturbed && requestedDisplay === TankopediaDisplay.Model
|
||||
? "3"
|
||||
: "-4rem"
|
||||
}
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: "translateY(-50%)",
|
||||
transitionDuration: "200ms",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{skins.map((camouflage, index) => {
|
||||
const name = unwrap(camouflage.name!).replace(
|
||||
"%(camo_num)",
|
||||
SKIN_NUMBER,
|
||||
);
|
||||
const top = index === 0 ? "var(--radius-full)" : "0";
|
||||
const bottom = index === skins.length - 1 ? "var(--radius-full)" : "0";
|
||||
|
||||
return (
|
||||
<Tooltip key={camouflage.id} content={name} side="left">
|
||||
<SidebarIconButton
|
||||
selected={skin === camouflage.id}
|
||||
alt={name}
|
||||
src={alias("api", `/icons/camouflages/${camouflage.id}.webp`)}
|
||||
size={{ initial: "3", sm: "4" }}
|
||||
radius="full"
|
||||
style={{
|
||||
borderRadius: `${top} ${top} ${bottom} ${bottom}`,
|
||||
}}
|
||||
imageStyle={{
|
||||
width: "70%",
|
||||
height: "70%",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
onClick={() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.requestedSkin =
|
||||
draft.requestedSkin === camouflage.id
|
||||
? undefined
|
||||
: camouflage.id;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -46,11 +46,6 @@ import { TankSearch } from "../../../../TankSearch";
|
||||
import { CustomShellButton } from "./components/CustomShellButton";
|
||||
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
||||
import { QuickInputs } from "./components/QuickInputs";
|
||||
import {
|
||||
SidebarIconButton,
|
||||
SidebarIconGroup,
|
||||
} from "./components/SidebarIconButton";
|
||||
import { SkinSwitcher } from "./components/SkinSwitcher";
|
||||
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
|
||||
import { Thicknesses } from "./components/Thicknesses";
|
||||
|
||||
@@ -92,8 +87,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
|
||||
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
||||
|
||||
<SkinSwitcher />
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
direction="column"
|
||||
@@ -188,13 +181,24 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
</Popover.Root>
|
||||
)}
|
||||
|
||||
<SidebarIconGroup>
|
||||
<Flex
|
||||
direction="column"
|
||||
style={{
|
||||
borderRadius: "var(--radius-full)",
|
||||
}}
|
||||
overflow="hidden"
|
||||
>
|
||||
{antagonistGun.shells.map((thisShell) => (
|
||||
<SidebarIconButton
|
||||
<IconButton
|
||||
color={
|
||||
thisShell.id === antagonistShell.id && !hasCustomShell
|
||||
? undefined
|
||||
: "gray"
|
||||
}
|
||||
variant="soft"
|
||||
key={thisShell.id}
|
||||
selected={thisShell.id === antagonistShell.id && !hasCustomShell}
|
||||
alt={unwrap(thisShell.name!)}
|
||||
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
Duel.mutate((draft) => {
|
||||
draft.antagonist.shell = thisShell;
|
||||
@@ -204,10 +208,19 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
draft.customShell = undefined;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<img
|
||||
alt={unwrap(thisShell.name!)}
|
||||
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
|
||||
style={{
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
))}
|
||||
<CustomShellButton />
|
||||
</SidebarIconGroup>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
direction="column"
|
||||
|
||||
@@ -1,46 +1,30 @@
|
||||
import { UpdateIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Flex, Heading } from "@radix-ui/themes";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
|
||||
import { api } from "../../../../../../core/blitzkit/api";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
|
||||
import { Tracker } from "./components/Tracker";
|
||||
|
||||
const camouflageDefinitions = await api.camouflageDefinitions();
|
||||
|
||||
export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const { unwrap, strings } = useLocale();
|
||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||
const revealed = Tankopedia.use((state) => state.revealed);
|
||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||
const hasRevealed = useRef(false);
|
||||
const skin = Tankopedia.use((state) => state.requestedSkin);
|
||||
const camouflage =
|
||||
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
|
||||
const tankName = unwrap(protagonist.name!);
|
||||
const name = camouflage
|
||||
? `${tankName} ${unwrap(camouflage.name!).replace("%(camo_num)", SKIN_NUMBER)}`
|
||||
: tankName;
|
||||
const name = unwrap(protagonist.name!);
|
||||
const fontSize = revealed
|
||||
? disturbed
|
||||
? "1.5rem"
|
||||
: "2rem"
|
||||
: `min(12vh, ${75 / name.length}vw)`;
|
||||
|
||||
useEffect(() => {
|
||||
if (revealed) hasRevealed.current = true;
|
||||
}, [revealed]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!modelRequested || hasRevealed.current) return;
|
||||
if (!modelRequested) return;
|
||||
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent
|
||||
|
||||
const ANGLE = degToRad(15);
|
||||
const REVEAL_ANIMATION_TIME = 3;
|
||||
export const TRANSITION_ANIMATION_TIME = 0.5;
|
||||
const TRANSITION_ANIMATION_TIME = 0.5;
|
||||
|
||||
const LIGHTS_COUNT = 3;
|
||||
const THETA_OFFSET = degToRad(180 - 45);
|
||||
@@ -46,17 +46,11 @@ export function Lighting({ hasPbr }: Props) {
|
||||
|
||||
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const revealed = Tankopedia.use((state) => state.revealed);
|
||||
const skinPending = Tankopedia.use(
|
||||
(state) => state.loadedSkin !== state.skin,
|
||||
);
|
||||
|
||||
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
||||
const t0 = useRef(performance.now() / 1e3 - animationTime.current);
|
||||
const holding = useRef(false);
|
||||
|
||||
const isRevealing = useRef(true);
|
||||
const isFirstReveal = useRef(true);
|
||||
|
||||
const [animate, setAnimate] = useState(true);
|
||||
|
||||
@@ -77,37 +71,6 @@ export function Lighting({ hasPbr }: Props) {
|
||||
setAnimate(true);
|
||||
}, [requestedDisplay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstReveal.current) {
|
||||
isFirstReveal.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!revealed) {
|
||||
animationTime.current = TRANSITION_ANIMATION_TIME;
|
||||
t0.current = performance.now() / 1e3;
|
||||
holding.current = true;
|
||||
} else if (holding.current) {
|
||||
animationTime.current = REVEAL_ANIMATION_TIME;
|
||||
t0.current = performance.now() / 1e3 - animationTime.current;
|
||||
holding.current = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
setAnimate(true);
|
||||
invalidate();
|
||||
}, [revealed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!skinPending) return;
|
||||
|
||||
animationTime.current = TRANSITION_ANIMATION_TIME;
|
||||
t0.current = performance.now() / 1e3;
|
||||
|
||||
setAnimate(true);
|
||||
}, [skinPending]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContactShadows
|
||||
@@ -127,7 +90,6 @@ export function Lighting({ hasPbr }: Props) {
|
||||
hasPbr={hasPbr}
|
||||
t0={t0}
|
||||
animationTime={animationTime}
|
||||
holding={holding}
|
||||
wrapper={wrapper}
|
||||
stop={() => {
|
||||
setAnimate(false);
|
||||
@@ -163,7 +125,7 @@ export function Lighting({ hasPbr }: Props) {
|
||||
position={position}
|
||||
intensity={intensity}
|
||||
penumbra={1}
|
||||
castShadow={highGraphics}
|
||||
castShadow={highGraphics && index === 0}
|
||||
decay={1}
|
||||
color="#ffffff"
|
||||
angle={0}
|
||||
@@ -183,19 +145,11 @@ interface AnimatorProps {
|
||||
|
||||
t0: RefObject<number>;
|
||||
animationTime: RefObject<number>;
|
||||
holding: RefObject<boolean>;
|
||||
wrapper: RefObject<Group | null>;
|
||||
hasPbr: boolean;
|
||||
}
|
||||
|
||||
function Animator({
|
||||
stop,
|
||||
t0,
|
||||
animationTime,
|
||||
holding,
|
||||
wrapper,
|
||||
hasPbr,
|
||||
}: AnimatorProps) {
|
||||
function Animator({ stop, t0, animationTime, wrapper, hasPbr }: AnimatorProps) {
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
|
||||
@@ -222,7 +176,7 @@ function Animator({
|
||||
|
||||
useFrame(({ invalidate }) => {
|
||||
const dt = performance.now() / 1e3 - t0.current;
|
||||
const x = clamp(dt / animationTime.current, 0, holding.current ? 1 : 2);
|
||||
const x = clamp(dt / animationTime.current, 0, 2);
|
||||
|
||||
if (x === 2) {
|
||||
apply(1);
|
||||
@@ -234,9 +188,6 @@ function Animator({
|
||||
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
|
||||
|
||||
apply(t);
|
||||
|
||||
if (holding.current && x === 1) return;
|
||||
|
||||
invalidate();
|
||||
|
||||
if (x >= 1 && Tankopedia.state.display !== requestedDisplay) {
|
||||
@@ -244,12 +195,6 @@ function Animator({
|
||||
draft.display = requestedDisplay;
|
||||
});
|
||||
}
|
||||
|
||||
if (x >= 1 && Tankopedia.state.skin !== Tankopedia.state.loadedSkin) {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.skin = draft.loadedSkin;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
|
||||
import { useRef } from "react";
|
||||
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
|
||||
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
|
||||
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||
import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
|
||||
import { skinTree } from "../../../../../../core/blitzkit/skinTree";
|
||||
import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled";
|
||||
import { useModel } from "../../../../../../hooks/useModel";
|
||||
import { useSkin } from "../../../../../../hooks/useSkin";
|
||||
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
|
||||
import { useTankTransform } from "../../../../../../hooks/useTankTransform";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
||||
import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper";
|
||||
|
||||
export function TankModel() {
|
||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||
const track = Duel.use((state) => state.protagonist.track);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
const hullContainer = useRef<Group>(null);
|
||||
const turretContainer = useRef<Group>(null!);
|
||||
const gunContainer = useRef<Group>(null!);
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonist.turret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||
const { gltf } = useModel(protagonist.tank.id);
|
||||
const nodes = Object.values(gltf.nodes);
|
||||
const skinGltf = useSkin(protagonist.tank.id);
|
||||
|
||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||
|
||||
return (
|
||||
<ModelTankWrapper ref={hullContainer}>
|
||||
{nodes.map((node) => {
|
||||
const isHull = node.name === "hull";
|
||||
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||
const isTrack = node.name.startsWith("chassis_track_");
|
||||
const isVisible = isHull || isWheel || isTrack;
|
||||
const position = new Vector2();
|
||||
const delta = new Vector2();
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
function translateTexture(offset: number) {
|
||||
const mesh = node.children[0] as Mesh;
|
||||
const material = mesh?.material as MeshStandardMaterial;
|
||||
|
||||
if (!material) return;
|
||||
|
||||
invalidate();
|
||||
|
||||
const textures = new Set([
|
||||
material.map,
|
||||
material.aoMap,
|
||||
material.normalMap,
|
||||
material.roughnessMap,
|
||||
material.metalnessMap,
|
||||
]);
|
||||
|
||||
for (const texture of textures) {
|
||||
if (texture) texture.offset.y += offset;
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
if (isTrack && Tankopedia.state.display === TankopediaDisplay.Model) {
|
||||
position.set(event.clientX, event.clientY);
|
||||
event.stopPropagation();
|
||||
|
||||
controlsEnabledEvent.dispatch(false);
|
||||
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
position.set(event.clientX, event.clientY);
|
||||
const deltaX = delta.x / window.innerWidth;
|
||||
const deltaY = delta.y / window.innerHeight;
|
||||
|
||||
translateTexture(deltaX + deltaY);
|
||||
}
|
||||
function handlePointerUp() {
|
||||
controlsEnabledEvent.dispatch(true);
|
||||
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return skinTree(node, skinGltf, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={key}
|
||||
onPointerDown={onPointerDown}
|
||||
castShadow
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
})}
|
||||
|
||||
<group ref={turretContainer}>
|
||||
{nodes.map((node) => {
|
||||
const isTurret = node.name.startsWith("turret_");
|
||||
const isCurrentTurret =
|
||||
node.name ===
|
||||
`turret_${turretModelDefinition.model_id
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
const isVisible = isCurrentTurret;
|
||||
const position = new Vector2();
|
||||
const delta = new Vector2();
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!isTurret) return;
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
|
||||
controlsEnabledEvent.dispatch(false);
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
position.set(event.clientX, event.clientY);
|
||||
|
||||
const [pitch, yaw] = applyPitchYawLimits(
|
||||
modelTransformEvent.last!.pitch,
|
||||
modelTransformEvent.last!.yaw +
|
||||
delta.x * (Math.PI / boundingRect.width),
|
||||
gunModelDefinition.pitch!,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
controlsEnabledEvent.dispatch(true);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return skinTree(node, skinGltf, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={key}
|
||||
onPointerDown={onPointerDown}
|
||||
castShadow
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
})}
|
||||
|
||||
<group ref={gunContainer}>
|
||||
{nodes.map((node) => {
|
||||
const isCurrentMantlet =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model_id
|
||||
.toString()
|
||||
.padStart(2, "0")}_mask`;
|
||||
const isCurrentGun =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`;
|
||||
const isVisible = isCurrentGun || isCurrentMantlet;
|
||||
const position = new Vector2();
|
||||
const delta = new Vector2();
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
let pitch = 0;
|
||||
let yaw = 0;
|
||||
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
controlsEnabledEvent.dispatch(false);
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
pitch = modelTransformEvent.last!.pitch;
|
||||
yaw = modelTransformEvent.last!.yaw;
|
||||
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
Duel.state.protagonist.tank.equipment_preset,
|
||||
Duel.state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
position.set(event.clientX, event.clientY);
|
||||
|
||||
[pitch, yaw] = applyPitchYawLimits(
|
||||
pitch - delta.y * (Math.PI / boundingRect.height),
|
||||
yaw + delta.x * (Math.PI / boundingRect.width),
|
||||
gunModelDefinition.pitch!,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
controlsEnabledEvent.dispatch(true);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return skinTree(node, skinGltf, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
key={key}
|
||||
onPointerDown={onPointerDown}
|
||||
castShadow
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
})}
|
||||
</group>
|
||||
</group>
|
||||
</ModelTankWrapper>
|
||||
);
|
||||
}
|
||||
@@ -23,12 +23,12 @@ import {
|
||||
type ThicknessRange,
|
||||
} from "../../../../Armor/components/StaticArmor";
|
||||
import { SmartCanvas } from "../../../../SmartCanvas";
|
||||
import { TankModel } from "../../../../TankModel";
|
||||
import { AutoClear } from "./components/AutoClear";
|
||||
import { Controls } from "./components/Control";
|
||||
import { InitialAligner } from "./components/InitialAligner";
|
||||
import { Lighting } from "./components/Lighting";
|
||||
import { SceneProps } from "./components/SceneProps";
|
||||
import { TankModel } from "./components/TankModel";
|
||||
|
||||
interface TankSandboxProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
interface CreditPeople {
|
||||
name: string;
|
||||
description: string;
|
||||
url?: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export const credits: CreditPeople[] = [
|
||||
{
|
||||
name: "TrèsAbhi",
|
||||
description: "Founder and lead developer",
|
||||
url: "https://abhi-deep.com/",
|
||||
image: "https://github.com/tresabhi.png",
|
||||
},
|
||||
{
|
||||
name: "HitJack",
|
||||
description: "Developer on website",
|
||||
url: "https://marksstuff.com",
|
||||
image: "https://github.com/Hitjack007.png",
|
||||
},
|
||||
{
|
||||
name: "aeson000",
|
||||
description: "Advisor on game and website",
|
||||
url: "https://discord.gg/WHdER7ZPAD",
|
||||
image: "/media/users/aeson000.png",
|
||||
},
|
||||
{
|
||||
name: "GonnaHetzMe",
|
||||
description: "Developer and advisor on back end",
|
||||
url: "https://github.com/karelpak1",
|
||||
image: "https://github.com/karelpak1.png",
|
||||
},
|
||||
{
|
||||
name: "ChickenMan7777",
|
||||
description: "Moderator and former advisor on game mechanics",
|
||||
url: "https://discord.gg/rEsUbXCqD9",
|
||||
image: "/media/users/chickenman7777.webp",
|
||||
},
|
||||
{
|
||||
name: "Avenir",
|
||||
description: "Moderator and elite Advanced Recon Commando",
|
||||
image: "/media/users/avenir.webp",
|
||||
},
|
||||
{
|
||||
name: "Pyogenics",
|
||||
description: "Former advisor on game 3D format",
|
||||
url: "https://github.com/Pyogenics",
|
||||
image: "https://github.com/Pyogenics.png",
|
||||
},
|
||||
{
|
||||
name: "Prince_NA",
|
||||
description: "Former advisor on bot and website",
|
||||
image: "/media/users/prince-na.webp",
|
||||
},
|
||||
{
|
||||
name: "Vovko",
|
||||
description: "Former advisor on bot",
|
||||
url: "https://amth.one/",
|
||||
image: "https://github.com/cufee.png",
|
||||
},
|
||||
{
|
||||
name: "S0AP",
|
||||
description: "Former advisor on and advertiser for bot",
|
||||
url: "https://discord.gg/3KfH93um3Y",
|
||||
image: "/media/users/s0ap.webp",
|
||||
},
|
||||
{
|
||||
name: "Synx",
|
||||
description: "Former advisor on bot and website",
|
||||
url: "https://www.youtube.com/@Synx_Blitz",
|
||||
image: "/media/users/synx.webp",
|
||||
},
|
||||
{
|
||||
name: "34wotb",
|
||||
description: "Former advisor on bot",
|
||||
url: "https://www.twitch.tv/34wotb/",
|
||||
image: "/media/users/34wotb.jpg",
|
||||
},
|
||||
{
|
||||
name: "HisRoyalFatness",
|
||||
description: "Former advisor on website",
|
||||
url: "https://www.youtube.com/@hisroyalfatness",
|
||||
image: "/media/users/hisroyalfatness.jpg",
|
||||
},
|
||||
{
|
||||
name: "Minitelrose",
|
||||
description: "Former advisor on game mechanics",
|
||||
url: "https://wotinspector.com/",
|
||||
image: "/media/users/minitelrose.webp",
|
||||
},
|
||||
{
|
||||
name: "Maddox",
|
||||
description: "Game asset decompression",
|
||||
url: "https://github.com/Maddoxkkm",
|
||||
image: "https://github.com/Maddoxkkm.png",
|
||||
},
|
||||
{
|
||||
name: "TJ Duff",
|
||||
description: "Chief executive cheer leader",
|
||||
image: "/media/users/tj-duff.jpg",
|
||||
},
|
||||
];
|
||||
@@ -1,23 +0,0 @@
|
||||
// TODO: source this from the open collective api, if even possible
|
||||
export const donationTiers = [
|
||||
{
|
||||
name: "The Backer",
|
||||
value: 5,
|
||||
link: "https://opencollective.com/blitzkit/contribute/backers-106312/checkout",
|
||||
},
|
||||
{
|
||||
name: "The Engineer",
|
||||
value: 10,
|
||||
link: "https://opencollective.com/blitzkit/contribute/the-engineer-107057/checkout",
|
||||
},
|
||||
{
|
||||
name: "The Patron",
|
||||
value: 25,
|
||||
link: "https://opencollective.com/blitzkit/contribute/the-patron-107056/checkout",
|
||||
},
|
||||
{
|
||||
name: "The Sponsor",
|
||||
value: null,
|
||||
link: "https://opencollective.com/blitzkit/donate",
|
||||
},
|
||||
];
|
||||
@@ -1 +0,0 @@
|
||||
export const SKIN_NUMBER = "0417";
|
||||
@@ -74,8 +74,4 @@ export const tools: Record<string, Tool> = {
|
||||
href: "https://discord.gg/nDt7AjGJQH",
|
||||
button: "plum",
|
||||
},
|
||||
credits: {
|
||||
id: "credits",
|
||||
button: "bronze",
|
||||
},
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { alias } from "@blitzkit/core";
|
||||
|
||||
const EXTERNAL_IMAGE = /^https?:\/\//;
|
||||
const MARKDOWN_IMAGES = [
|
||||
/!\[[^\]]*\]\((\S+?)(?:\s+"[^"]*")?\)/g,
|
||||
/<img[^>]*\ssrc=["']([^"']+)["']/g,
|
||||
];
|
||||
|
||||
export function isExternalImage(src: string) {
|
||||
return EXTERNAL_IMAGE.test(src);
|
||||
}
|
||||
|
||||
export function docImageId(url: string) {
|
||||
let hash = 0x811c9dc5;
|
||||
|
||||
for (let index = 0; index < url.length; index++) {
|
||||
hash ^= url.charCodeAt(index);
|
||||
hash = Math.imul(hash, 0x01000193) >>> 0;
|
||||
}
|
||||
|
||||
return hash.toString(16).padStart(8, "0");
|
||||
}
|
||||
|
||||
export function docImageUrl(url: string) {
|
||||
return alias("api", `/docs/images/${docImageId(url)}.webp`);
|
||||
}
|
||||
|
||||
export function externalDocImages(markdown: string) {
|
||||
return MARKDOWN_IMAGES.flatMap((pattern) =>
|
||||
Array.from(markdown.matchAll(pattern), (match) => match[1]),
|
||||
).filter(isExternalImage);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { Material, Object3D } from "three";
|
||||
import type { Skin } from "../../hooks/useSkin";
|
||||
import { jsxTree } from "./jsxTree";
|
||||
|
||||
type Mergers = Parameters<typeof jsxTree>[1];
|
||||
|
||||
export function skinTree(
|
||||
node: Object3D,
|
||||
skin: Skin | undefined,
|
||||
mergers: Mergers,
|
||||
) {
|
||||
const skinNodes = new Map(
|
||||
skin?.scene.children.map((node) => [node.name, node]),
|
||||
);
|
||||
const skinMergers: Mergers = {
|
||||
mesh(mesh, props, key) {
|
||||
const material = skin?.materials.get((mesh.material as Material).name);
|
||||
|
||||
return mergers!.mesh!(
|
||||
mesh,
|
||||
material ? { ...props, material } : props,
|
||||
key,
|
||||
);
|
||||
},
|
||||
group(object3d, props, key) {
|
||||
const skinNode = skinNodes.get(object3d.name);
|
||||
const isSwitch = object3d.userData.state !== undefined;
|
||||
const state = skin
|
||||
? skin.scene.userData.states[object3d.name]
|
||||
: object3d.userData.state;
|
||||
|
||||
return (
|
||||
<group key={key} {...props}>
|
||||
{isSwitch
|
||||
? object3d.children
|
||||
.filter((child) => child.name === state)
|
||||
.map((child) => jsxTree(child, skinMergers))
|
||||
: props.children}
|
||||
{skinNode?.children.map((child) => jsxTree(child, mergers))}
|
||||
</group>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return jsxTree(node, skinMergers);
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
resolvePenetrationCoefficient,
|
||||
ShellType,
|
||||
sum,
|
||||
tankBoundingBox,
|
||||
TankClass,
|
||||
unionBoundingBox,
|
||||
type EngineDefinition,
|
||||
type EquipmentDefinitions,
|
||||
type GunDefinition,
|
||||
@@ -336,7 +336,10 @@ export function tankCharacteristics(
|
||||
);
|
||||
|
||||
const size = normalizeBoundingBox(
|
||||
tankBoundingBox(tankModelDefinition, turretModelDefinition),
|
||||
unionBoundingBox(
|
||||
tankModelDefinition.bounding_box!,
|
||||
turretModelDefinition.bounding_box!,
|
||||
),
|
||||
);
|
||||
const weightKg =
|
||||
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
|
||||
|
||||
@@ -7,23 +7,12 @@ import {
|
||||
import type { DuelMember } from "../../stores/duel";
|
||||
import { genericDefaultEquipmentMatrix } from "../../stores/duel/constants";
|
||||
|
||||
export function defaultTankWeapon(tank: TankDefinition) {
|
||||
for (const turret of [...tank.turrets].reverse()) {
|
||||
for (const gun of [...turret.guns].reverse()) {
|
||||
const shell = gun.shells[0];
|
||||
|
||||
if (shell) return { turret, gun, shell };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Tank ${tank.id} has no gun with shells`);
|
||||
}
|
||||
|
||||
export function tankToDuelMember(
|
||||
tank: TankDefinition,
|
||||
provisionDefinitions: ProvisionDefinitions,
|
||||
) {
|
||||
const { turret, gun, shell } = defaultTankWeapon(tank);
|
||||
const turret = tank.turrets.at(-1)!;
|
||||
const gun = turret.guns.at(-1)!;
|
||||
|
||||
return {
|
||||
camouflage: true,
|
||||
@@ -33,7 +22,7 @@ export function tankToDuelMember(
|
||||
provisions: createDefaultProvisions(tank, gun, provisionDefinitions),
|
||||
engine: tank.engines.at(-1)!,
|
||||
gun,
|
||||
shell,
|
||||
shell: tank.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||
tank,
|
||||
track: tank.tracks.at(-1)!,
|
||||
turret,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GLTFLoader } from "three-stdlib";
|
||||
import { useDispose } from "./useDispose";
|
||||
|
||||
export function useArmor(id: number) {
|
||||
const path = alias("models_tanks", `/${id}/collision.glb`);
|
||||
const path = alias("api", `/tanks/${id}/collision.glb`);
|
||||
const gltf = useLoader(GLTFLoader, path);
|
||||
|
||||
useDispose(gltf, path);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { ObjectMap } from "@react-three/fiber";
|
||||
import type { ComponentProps } from "react";
|
||||
import {
|
||||
BatchedMesh,
|
||||
Group,
|
||||
Mesh,
|
||||
MeshStandardMaterial,
|
||||
type Object3D,
|
||||
} from "three";
|
||||
|
||||
interface BatchedProps extends ComponentProps<"group"> {
|
||||
slicer: (node: Object3D) => boolean;
|
||||
mutator?: (mesh: Mesh) => void;
|
||||
}
|
||||
|
||||
export function useBatched(map: ObjectMap, shadows = false) {
|
||||
return function Batched({ slicer, mutator, ...props }: BatchedProps) {
|
||||
const groups = new Map<
|
||||
MeshStandardMaterial,
|
||||
{ clonedMaterial: MeshStandardMaterial; meshes: Mesh[] }
|
||||
>();
|
||||
|
||||
for (const node of Object.values(map.nodes)) {
|
||||
if (!slicer(node)) continue;
|
||||
|
||||
node.traverse((object) => {
|
||||
if (!(object instanceof Mesh)) return;
|
||||
|
||||
const originalMaterial = object.material;
|
||||
|
||||
if (!(originalMaterial instanceof MeshStandardMaterial)) return;
|
||||
|
||||
let group = groups.get(originalMaterial);
|
||||
|
||||
if (!group) {
|
||||
group = {
|
||||
clonedMaterial: originalMaterial.clone(),
|
||||
meshes: [],
|
||||
};
|
||||
groups.set(originalMaterial, group);
|
||||
}
|
||||
|
||||
group.meshes.push(object);
|
||||
});
|
||||
}
|
||||
|
||||
const wrapper = new Group();
|
||||
|
||||
for (const { clonedMaterial: material, meshes } of groups.values()) {
|
||||
let vertices = 0;
|
||||
let indices = 0;
|
||||
|
||||
for (const mesh of meshes) {
|
||||
vertices += mesh.geometry.attributes.position.count;
|
||||
indices += mesh.geometry.index!.count;
|
||||
}
|
||||
|
||||
const batched = new BatchedMesh(
|
||||
meshes.length,
|
||||
vertices,
|
||||
indices,
|
||||
material,
|
||||
);
|
||||
|
||||
for (const mesh of meshes) {
|
||||
const geometryId = batched.addGeometry(mesh.geometry);
|
||||
const instanceId = batched.addInstance(geometryId);
|
||||
|
||||
mesh.updateWorldMatrix(true, false);
|
||||
batched.setMatrixAt(instanceId, mesh.matrixWorld);
|
||||
|
||||
if (shadows) {
|
||||
batched.castShadow = true;
|
||||
batched.receiveShadow = true;
|
||||
}
|
||||
|
||||
mutator?.(mesh);
|
||||
}
|
||||
|
||||
wrapper.add(batched);
|
||||
}
|
||||
|
||||
return (
|
||||
<group {...props}>
|
||||
<primitive object={wrapper} />
|
||||
</group>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { GLTFLoader } from "three-stdlib";
|
||||
import { useDispose } from "./useDispose";
|
||||
|
||||
export function useModel(id: number) {
|
||||
const path = alias("models_tanks", `/${id}/model.glb`);
|
||||
const path = alias("api", `/tanks/${id}/model.glb`);
|
||||
const gltf = useLoader(GLTFLoader, path);
|
||||
|
||||
useDispose(gltf, path);
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { alias } from "@blitzkit/core";
|
||||
import { useEffect } from "react";
|
||||
import type { Group, Material } from "three";
|
||||
import { GLTFLoader } from "three-stdlib";
|
||||
import { TRANSITION_ANIMATION_TIME } from "../components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||
import { Tankopedia } from "../stores/tankopedia";
|
||||
|
||||
export interface Skin {
|
||||
scene: Group;
|
||||
materials: Map<string, Material>;
|
||||
}
|
||||
|
||||
const skins = new Map<string, Promise<Skin>>();
|
||||
const loadedSkins = new Map<string, Skin>();
|
||||
|
||||
async function loadSkin(path: string) {
|
||||
const gltf = await new GLTFLoader().loadAsync(path);
|
||||
const materials: Material[] = await gltf.parser.getDependencies("material");
|
||||
const skin = {
|
||||
scene: gltf.scene,
|
||||
materials: new Map(materials.map((material) => [material.name, material])),
|
||||
};
|
||||
|
||||
loadedSkins.set(path, skin);
|
||||
|
||||
return skin;
|
||||
}
|
||||
|
||||
function skinPath(id: number, skin: number) {
|
||||
return alias("models_legendary", `/${id}/skins/${skin}.glb`);
|
||||
}
|
||||
|
||||
export function useSkin(id: number) {
|
||||
const requestedSkin = Tankopedia.use((state) => state.requestedSkin);
|
||||
const skin = Tankopedia.use((state) => state.skin);
|
||||
|
||||
useEffect(() => {
|
||||
const path =
|
||||
requestedSkin === undefined ? undefined : skinPath(id, requestedSkin);
|
||||
let settled = false;
|
||||
|
||||
if (path === undefined || loadedSkins.has(path)) {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.loadedSkin = requestedSkin;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = false;
|
||||
});
|
||||
|
||||
if (!skins.has(path)) skins.set(path, loadSkin(path));
|
||||
|
||||
Promise.all([
|
||||
skins.get(path),
|
||||
new Promise((resolve) =>
|
||||
setTimeout(resolve, TRANSITION_ANIMATION_TIME * 1e3),
|
||||
),
|
||||
]).then(() => {
|
||||
if (settled) return;
|
||||
|
||||
settled = true;
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.loadedSkin = requestedSkin;
|
||||
draft.skin = requestedSkin;
|
||||
draft.revealed = true;
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (settled) return;
|
||||
|
||||
settled = true;
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
};
|
||||
}, [id, requestedSkin]);
|
||||
|
||||
return skin === undefined ? undefined : loadedSkins.get(skinPath(id, skin));
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { APIContext, GetStaticPathsItem } from "astro";
|
||||
import sharp from "sharp";
|
||||
import {
|
||||
docImageId,
|
||||
externalDocImages,
|
||||
} from "../../../../core/blitzkit/docImages";
|
||||
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
|
||||
import { getStaticPaths as _getStaticPaths } from "../../_index";
|
||||
|
||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
||||
const documents = import.meta.glob<string>(
|
||||
"../../../../../../../docs/**/*.md",
|
||||
{ eager: true, query: "?raw", import: "default" },
|
||||
);
|
||||
const urls = new Set(Object.values(documents).flatMap(externalDocImages));
|
||||
|
||||
return Array.from(
|
||||
urls,
|
||||
(url) =>
|
||||
({
|
||||
params: { id: docImageId(url) },
|
||||
props: { url },
|
||||
}) satisfies GetStaticPathsItem,
|
||||
);
|
||||
});
|
||||
|
||||
export async function GET({ props }: APIContext<{ url: string }>) {
|
||||
const response = await fetch(props.url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${props.url}: ${response.status}`);
|
||||
}
|
||||
|
||||
const image = await sharp(Buffer.from(await response.arrayBuffer()))
|
||||
.webp()
|
||||
.toBuffer();
|
||||
|
||||
return new Response(new Uint8Array(image));
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { CamouflagesXml } from "@blitzkit/core";
|
||||
import type { APIContext, GetStaticPathsItem } from "astro";
|
||||
import sharp from "sharp";
|
||||
import { extractPackedIcon } from "../../../../core/blitz/extractPackedIcon";
|
||||
import { parsePackedSpriteRect } from "../../../../core/blitz/parsePackedSpriteRect";
|
||||
import { api } from "../../../../core/blitzkit/api";
|
||||
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
|
||||
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||
import { getStaticPaths as _getStaticPaths } from "../../_index";
|
||||
|
||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
||||
const camouflageDefinitions = await api.camouflageDefinitions();
|
||||
const camouflagesXml = await vfs.xml<{ root: CamouflagesXml }>(
|
||||
"Data/XML/item_defs/vehicles/common/camouflages.xml",
|
||||
);
|
||||
const icons = new Map(
|
||||
Object.values(camouflagesXml.root.camouflages).map((camouflage) => [
|
||||
camouflage.id,
|
||||
camouflage.icon,
|
||||
]),
|
||||
);
|
||||
const paths: GetStaticPathsItem[] = [];
|
||||
|
||||
for (const camouflage of Object.values(camouflageDefinitions.camouflages)) {
|
||||
const icon = icons.get(camouflage.id);
|
||||
|
||||
if (camouflage.tank_id === undefined || !icon) continue;
|
||||
|
||||
paths.push({
|
||||
params: { id: camouflage.id },
|
||||
props: {
|
||||
path: icon.replace("~res:/", "Data/").replace(/\.txt$/, ""),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return paths;
|
||||
});
|
||||
|
||||
export async function GET({ props }: APIContext<{ path: string }>) {
|
||||
const path = (await vfs.resolve(`${props.path}@2x.packed.webp`))
|
||||
? `${props.path}@2x.packed.webp`
|
||||
: `${props.path}.packed.webp`;
|
||||
const packed = await vfs.file(path);
|
||||
const texture = sharp(packed);
|
||||
const sizes = parsePackedSpriteRect(packed);
|
||||
const image = sizes
|
||||
? sharp(await extractPackedIcon(texture, sizes))
|
||||
: texture;
|
||||
const bytes = await image.webp().toBuffer();
|
||||
|
||||
return new Response(new Uint8Array(bytes));
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import {
|
||||
parseHierarchies,
|
||||
Sc2ReadStream,
|
||||
ScgReadStream,
|
||||
toUniqueId,
|
||||
VertexAttribute,
|
||||
vertexAttributeVectorSizes,
|
||||
type Hierarchy,
|
||||
type VehicleDefinitionList,
|
||||
} from "@blitzkit/core";
|
||||
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||
import { Accessor, Document, Node, NodeIO, Scene } from "@gltf-transform/core";
|
||||
import type { APIContext } from "astro";
|
||||
import { vfs } from "../../../core/blitzkit/vfs";
|
||||
import { times } from "lodash-es";
|
||||
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||
|
||||
export { getStaticPaths } from "./_index";
|
||||
|
||||
@@ -55,10 +56,13 @@ async function extractArmor(vfs: AbstractVFS, fileName: string) {
|
||||
const scene = document.createScene();
|
||||
const buffer = document.createBuffer();
|
||||
|
||||
parseHierarchies<Scene | Node>(
|
||||
sc2["#hierarchy"],
|
||||
scene,
|
||||
(hierarchy, components, parent) => {
|
||||
function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) {
|
||||
hierarchies.forEach((hierarchy) => {
|
||||
const components = times(
|
||||
hierarchy.components.count,
|
||||
(index) => hierarchy.components[index.toString().padStart(4, "0")],
|
||||
);
|
||||
|
||||
components.forEach((component) => {
|
||||
switch (component["comp.typename"]) {
|
||||
case "TransformComponent":
|
||||
@@ -159,9 +163,13 @@ async function extractArmor(vfs: AbstractVFS, fileName: string) {
|
||||
}
|
||||
});
|
||||
|
||||
return parent;
|
||||
},
|
||||
);
|
||||
if (hierarchy["#hierarchy"]) {
|
||||
parseHierarchies(hierarchy["#hierarchy"], parent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseHierarchies(sc2["#hierarchy"], scene);
|
||||
|
||||
return document;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
bufferToBigInt,
|
||||
DdsReadStream,
|
||||
parseHierarchies,
|
||||
PvrReadStream,
|
||||
Sc2ReadStream,
|
||||
ScgReadStream,
|
||||
toUniqueId,
|
||||
VertexAttribute,
|
||||
type ConfigArchive,
|
||||
type Hierarchy,
|
||||
type TankParameters,
|
||||
type Textures,
|
||||
type VehicleDefinitionList,
|
||||
@@ -17,10 +17,10 @@ import { Document, Material, Node, NodeIO, Scene } from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { dedup, prune } from "@gltf-transform/functions";
|
||||
import type { APIContext } from "astro";
|
||||
import { isEqual, times } from "lodash-es";
|
||||
import { times } from "lodash-es";
|
||||
import { dirname } from "path";
|
||||
import sharp from "sharp";
|
||||
import { vfs } from "../../../core/blitzkit/vfs";
|
||||
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||
import {
|
||||
vertexAttributeGLTFName,
|
||||
vertexAttributeGltfVectorSizes,
|
||||
@@ -69,11 +69,7 @@ const omitMeshNames = {
|
||||
end: ["_POINT"],
|
||||
};
|
||||
|
||||
export async function extractModel(
|
||||
vfs: AbstractVFS,
|
||||
path: string,
|
||||
isPart = false,
|
||||
) {
|
||||
async function extractModel(vfs: AbstractVFS, path: string) {
|
||||
const sc2Path = `Data/3d/${path}.sc2`;
|
||||
const scgPath = `Data/3d/${path}.scg`;
|
||||
const sc2 = new Sc2ReadStream(
|
||||
@@ -85,153 +81,6 @@ export async function extractModel(
|
||||
const document = new Document();
|
||||
const scene = document.createScene();
|
||||
const buffer = document.createBuffer();
|
||||
const materials = await extractMaterials(document, sc2, path);
|
||||
|
||||
parseHierarchies<Scene | Node>(
|
||||
sc2["#hierarchy"],
|
||||
scene,
|
||||
(hierarchy, components, parent) => {
|
||||
if (
|
||||
omitMeshNames.start.some((omit) => hierarchy.name.startsWith(omit)) ||
|
||||
omitMeshNames.end.some((omit) => hierarchy.name.endsWith(omit))
|
||||
)
|
||||
return;
|
||||
|
||||
const node = document.createNode(hierarchy.name);
|
||||
|
||||
components.forEach((component) => {
|
||||
switch (component["comp.typename"]) {
|
||||
case "LodComponent":
|
||||
// found and used later by transform component
|
||||
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
||||
break;
|
||||
|
||||
case "TransformComponent": {
|
||||
const localTranslation = component["tc.localTranslation"];
|
||||
// The game resets top-level node translation to [0, 0, 0] on load.
|
||||
// Child nodes and customization parts must keep their authored
|
||||
// local offsets.
|
||||
node.setTranslation(
|
||||
parent instanceof Scene && !isPart ? [0, 0, 0] : localTranslation,
|
||||
);
|
||||
node.setRotation(component["tc.localRotation"]);
|
||||
node.setScale(component["tc.localScale"]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "StateSwitcherComponent":
|
||||
node.setExtras({
|
||||
state: component[`ssc.state${component["ssc.activeState"]}`],
|
||||
});
|
||||
break;
|
||||
|
||||
case "RenderComponent": {
|
||||
const renderObject = component["rc.renderObj"];
|
||||
|
||||
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
||||
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
||||
|
||||
if (lodIndex !== 0) return;
|
||||
|
||||
const batchKey = batchIndex.toString().padStart(4, "0");
|
||||
const batch = renderObject["ro.batches"][batchKey];
|
||||
const material = materials.get(batch["rb.nmatname"]);
|
||||
const polygonGroup = scg.get(batch["rb.datasource"]);
|
||||
|
||||
if (!(material instanceof Material)) {
|
||||
// probably shadow material
|
||||
return;
|
||||
}
|
||||
if (polygonGroup === undefined) {
|
||||
throw new Error(
|
||||
`Missing polygon group ${batch["rb.datasource"]}`,
|
||||
);
|
||||
}
|
||||
|
||||
const lodNode = document.createNode(batchKey);
|
||||
const indicesAccessor = document
|
||||
.createAccessor()
|
||||
.setType("SCALAR")
|
||||
.setArray(new Uint16Array(polygonGroup.indices))
|
||||
.setBuffer(buffer);
|
||||
const primitive = document
|
||||
.createPrimitive()
|
||||
.setIndices(indicesAccessor)
|
||||
.setMaterial(material)
|
||||
.setName(batchKey);
|
||||
|
||||
const attributes = new Map<VertexAttribute, number[][]>();
|
||||
|
||||
polygonGroup.vertices.forEach((vertex) => {
|
||||
vertex.forEach(({ attribute, value }) => {
|
||||
if (!attributes.has(attribute)) {
|
||||
attributes.set(attribute, []);
|
||||
}
|
||||
|
||||
attributes.get(attribute)!.push(value);
|
||||
});
|
||||
});
|
||||
|
||||
attributes.forEach((value, attribute) => {
|
||||
const name = vertexAttributeGLTFName[attribute];
|
||||
|
||||
if (
|
||||
name === undefined ||
|
||||
primitive.getAttribute(name) !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
||||
const attributeAccessor = document
|
||||
.createAccessor(name)
|
||||
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
||||
.setArray(new Float32Array(value.flat()))
|
||||
.setBuffer(buffer);
|
||||
|
||||
primitive.setAttribute(name, attributeAccessor);
|
||||
});
|
||||
|
||||
const mesh = document
|
||||
.createMesh(batch["##name"])
|
||||
.addPrimitive(primitive);
|
||||
lodNode.setMesh(mesh);
|
||||
node.addChild(lodNode);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
||||
throw new Error(
|
||||
`Unhandled component type: ${component["comp.typename"]}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
parent.addChild(node);
|
||||
|
||||
return node;
|
||||
},
|
||||
);
|
||||
|
||||
scene.addChild(document.createNode("test"));
|
||||
|
||||
await document.transform(prune({ keepAttributes: true }), dedup());
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
export async function extractMaterials(
|
||||
document: Document,
|
||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
||||
path: string,
|
||||
config?: string,
|
||||
) {
|
||||
const materials = new Map<bigint, Material | bigint | undefined>();
|
||||
|
||||
for (const node of sc2["#dataNodes"]) {
|
||||
@@ -246,32 +95,15 @@ export async function extractMaterials(
|
||||
continue;
|
||||
}
|
||||
|
||||
const material = document.createMaterial(node.materialName);
|
||||
let textures: Textures | undefined = undefined;
|
||||
|
||||
if (config !== undefined) {
|
||||
if (typeof node.configCount !== "number") continue;
|
||||
|
||||
const archive = times(
|
||||
node.configCount,
|
||||
(index) => node[`configArchive_${index}`],
|
||||
).find((archive) => archive.configName === config);
|
||||
|
||||
if (
|
||||
!archive ||
|
||||
isEqual(archive.textures, node.configArchive_0.textures)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
textures = archive.textures;
|
||||
} else if (node.textures) {
|
||||
if (node.textures) {
|
||||
textures = node.textures;
|
||||
} else if (typeof node.configCount === "number") {
|
||||
textures = node.configArchive_0.textures;
|
||||
}
|
||||
|
||||
const material = document.createMaterial(node.materialName);
|
||||
|
||||
if (textures) {
|
||||
const baseColor = await readBaseColor(
|
||||
`Data/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
|
||||
@@ -408,7 +240,142 @@ export async function extractMaterials(
|
||||
materials.set(id, resolvedMaterial);
|
||||
});
|
||||
|
||||
return materials;
|
||||
function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) {
|
||||
hierarchies.forEach((hierarchy) => {
|
||||
if (
|
||||
omitMeshNames.start.some((omit) => hierarchy.name.startsWith(omit)) ||
|
||||
omitMeshNames.end.some((omit) => hierarchy.name.endsWith(omit))
|
||||
)
|
||||
return;
|
||||
|
||||
const node = document.createNode(hierarchy.name);
|
||||
const components = times(
|
||||
hierarchy.components.count,
|
||||
(index) => hierarchy.components[index.toString().padStart(4, "0")],
|
||||
);
|
||||
|
||||
components.forEach((component) => {
|
||||
switch (component["comp.typename"]) {
|
||||
case "LodComponent":
|
||||
// found and used later by transform component
|
||||
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
||||
break;
|
||||
|
||||
case "TransformComponent": {
|
||||
const localTranslation = component["tc.localTranslation"];
|
||||
// The game resets top-level node translation to [0, 0, 0] on load.
|
||||
// Child nodes must keep their authored local offsets.
|
||||
node.setTranslation(
|
||||
parent instanceof Scene ? [0, 0, 0] : localTranslation,
|
||||
);
|
||||
node.setRotation(component["tc.localRotation"]);
|
||||
node.setScale(component["tc.localScale"]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "RenderComponent": {
|
||||
const renderObject = component["rc.renderObj"];
|
||||
|
||||
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
||||
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
||||
|
||||
if (lodIndex !== 0) return;
|
||||
|
||||
const batchKey = batchIndex.toString().padStart(4, "0");
|
||||
const batch = renderObject["ro.batches"][batchKey];
|
||||
const material = materials.get(batch["rb.nmatname"]);
|
||||
const polygonGroup = scg.get(batch["rb.datasource"]);
|
||||
|
||||
if (!(material instanceof Material)) {
|
||||
// probably shadow material
|
||||
return;
|
||||
}
|
||||
if (polygonGroup === undefined) {
|
||||
throw new Error(
|
||||
`Missing polygon group ${batch["rb.datasource"]}`,
|
||||
);
|
||||
}
|
||||
|
||||
const lodNode = document.createNode(batchKey);
|
||||
const indicesAccessor = document
|
||||
.createAccessor()
|
||||
.setType("SCALAR")
|
||||
.setArray(new Uint16Array(polygonGroup.indices))
|
||||
.setBuffer(buffer);
|
||||
const primitive = document
|
||||
.createPrimitive()
|
||||
.setIndices(indicesAccessor)
|
||||
.setMaterial(material)
|
||||
.setName(batchKey);
|
||||
|
||||
const attributes = new Map<VertexAttribute, number[][]>();
|
||||
|
||||
polygonGroup.vertices.forEach((vertex) => {
|
||||
vertex.forEach(({ attribute, value }) => {
|
||||
if (!attributes.has(attribute)) {
|
||||
attributes.set(attribute, []);
|
||||
}
|
||||
|
||||
attributes.get(attribute)!.push(value);
|
||||
});
|
||||
});
|
||||
|
||||
attributes.forEach((value, attribute) => {
|
||||
const name = vertexAttributeGLTFName[attribute];
|
||||
|
||||
if (
|
||||
name === undefined ||
|
||||
primitive.getAttribute(name) !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
||||
const attributeAccessor = document
|
||||
.createAccessor(name)
|
||||
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
||||
.setArray(new Float32Array(value.flat()))
|
||||
.setBuffer(buffer);
|
||||
|
||||
primitive.setAttribute(name, attributeAccessor);
|
||||
});
|
||||
|
||||
const mesh = document
|
||||
.createMesh(batch["##name"])
|
||||
.addPrimitive(primitive);
|
||||
lodNode.setMesh(mesh);
|
||||
node.addChild(lodNode);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
||||
throw new Error(
|
||||
`Unhandled component type: ${component["comp.typename"]}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hierarchy["#hierarchy"]) {
|
||||
parseHierarchies(hierarchy["#hierarchy"], node);
|
||||
}
|
||||
|
||||
parent.addChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
parseHierarchies(sc2["#hierarchy"], scene);
|
||||
|
||||
scene.addChild(document.createNode("test"));
|
||||
|
||||
await document.transform(prune({ keepAttributes: true }), dedup());
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
async function readBaseColor(path: string) {
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
import CreditsPromo from "../../../../components/CreditsPromo.astro";
|
||||
import { tools } from "../../../../constants/tools";
|
||||
import ToolLayout from "../../../../layouts/ToolLayout.astro";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolLayout tool={tools.credits} {locale}>
|
||||
<CreditsPromo {locale} />
|
||||
</ToolLayout>
|
||||
@@ -5,7 +5,6 @@ import locales from "@blitzkit/i18n/locales.json";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { Box, Flex, Link } from "@radix-ui/themes";
|
||||
import { AesonPlug } from "../../../components/AesonPlug";
|
||||
import { Beg } from "../../../components/Beg";
|
||||
import HomeHero from "../../../components/HomeHero.astro";
|
||||
import { Plugs } from "../../../components/Plugs";
|
||||
import ToolCard from "../../../components/ToolCard.astro";
|
||||
@@ -66,8 +65,6 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
|
||||
<BaseLayout {locale}>
|
||||
<HomeHero />
|
||||
|
||||
<Beg />
|
||||
|
||||
{
|
||||
locale !== locales.default && (
|
||||
<Flex justify="center" pt="4" my="4">
|
||||
|
||||
@@ -13,7 +13,6 @@ import { load } from "cheerio";
|
||||
import { api } from "../../../../../core/blitzkit/api";
|
||||
import { mixStaticPaths } from "../../../../../core/blitzkit/mixStaticPaths";
|
||||
import { tankCharacteristics } from "../../../../../core/blitzkit/tankCharacteristics";
|
||||
import { defaultTankWeapon } from "../../../../../core/blitzkit/tankToDuelMember";
|
||||
import { getStrings } from "../../../../../core/i18n/getStrings";
|
||||
import BaseLayout from "../../../../../layouts/BaseLayout.astro";
|
||||
import { genericDefaultEquipmentMatrix } from "../../../../../stores/duel/constants";
|
||||
@@ -54,7 +53,9 @@ const title = literals(strings.website.tools.tankopedia.tank_seo_title, {
|
||||
const url = import.meta.env.SITE + Astro.url.pathname;
|
||||
|
||||
const icon = alias("api", `/tanks/${id}/icons/big.webp`);
|
||||
const { turret, gun, shell } = defaultTankWeapon(tank);
|
||||
const turret = tank.turrets.at(-1)!;
|
||||
const gun = turret.guns.at(-1)!;
|
||||
const shell = gun.shells[0];
|
||||
const characteristics = tankCharacteristics(
|
||||
{
|
||||
tank,
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
import {
|
||||
parseHierarchies,
|
||||
Sc2ReadStream,
|
||||
type CamouflagesXml,
|
||||
type CamouflagesYaml,
|
||||
type TankParameters,
|
||||
} from "@blitzkit/core";
|
||||
import {
|
||||
Document,
|
||||
Node,
|
||||
NodeIO,
|
||||
PropertyType,
|
||||
Scene,
|
||||
} from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions";
|
||||
import type { APIContext } from "astro";
|
||||
import type { Vector3Tuple, Vector4Tuple } from "three";
|
||||
import { SKIN_NUMBER } from "../../../../constants/skinNumber";
|
||||
import { api } from "../../../../core/blitzkit/api";
|
||||
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
|
||||
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||
import {
|
||||
extractMaterials,
|
||||
extractModel,
|
||||
} from "../../../[...chunk_models_tanks]/[id]/model.glb";
|
||||
import { getStaticPaths as _getStaticPaths } from "../../_index";
|
||||
|
||||
const SKIN_NUMBER_SLOTS = 5;
|
||||
const DIGIT_COLUMNS = 5;
|
||||
const DIGIT_ROWS = 2;
|
||||
|
||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
||||
const camouflages = await api.camouflageDefinitions();
|
||||
|
||||
return Object.values(camouflages.camouflages)
|
||||
.filter((camouflage) => camouflage.tank_id)
|
||||
.map((camouflage) => ({
|
||||
params: { id: camouflage.tank_id!, skin: camouflage.id },
|
||||
props: { skin: camouflage.id },
|
||||
}));
|
||||
});
|
||||
|
||||
const camouflagesXml = await vfs.xml<{ root: CamouflagesXml }>(
|
||||
"Data/XML/item_defs/vehicles/common/camouflages.xml",
|
||||
);
|
||||
const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
|
||||
"Data/camouflages.yaml",
|
||||
);
|
||||
const customization = await vfs.yaml<
|
||||
Record<string, { Name: string; Path: string }>
|
||||
>("Data/3d/Customization.yaml");
|
||||
|
||||
export async function GET({ props }: APIContext<{ skin: number }>) {
|
||||
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||
|
||||
const items = new Map(
|
||||
Object.values(customization).map(({ Name, Path }) => [
|
||||
Name,
|
||||
Path.replace("~res:/3d/", "").replace(/\.sc2$/, ""),
|
||||
]),
|
||||
);
|
||||
const key = Object.keys(camouflagesXml.root.camouflages).find(
|
||||
(key) => camouflagesXml.root.camouflages[key].id === props.skin,
|
||||
)!;
|
||||
const include = camouflagesXml.root.camouflages[key].vehicleFilter.include;
|
||||
const [nation, tankKey] = (
|
||||
include as { vehicle: { name: string } }
|
||||
).vehicle.name.split(":");
|
||||
const { preset, customEntities } = camouflagesYaml[key];
|
||||
const skinNumber = Number(preset?.match(/(\d+)$/)?.[1] ?? 1);
|
||||
const parameters = await vfs.yaml<TankParameters>(
|
||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
const basePath = parameters.resourcesPath.blitzModelPath.replace(
|
||||
/\.sc2$/,
|
||||
"",
|
||||
);
|
||||
const sc2 = new Sc2ReadStream(
|
||||
(await vfs.file(`Data/3d/${basePath}.sc2`)).buffer as ArrayBuffer,
|
||||
).sc2();
|
||||
const slots: Record<
|
||||
string,
|
||||
{
|
||||
target: string;
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector4Tuple;
|
||||
scale: Vector3Tuple;
|
||||
}
|
||||
> = {};
|
||||
const states: Record<string, string | null> = {};
|
||||
|
||||
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
||||
for (const component of components) {
|
||||
if (component["comp.typename"] === "NewSlotComponent") {
|
||||
for (const slot of Object.values(component.slots)) {
|
||||
slots[slot.name] = {
|
||||
target: hierarchy.name,
|
||||
position: slot.transform.position,
|
||||
rotation: slot.transform.rotation,
|
||||
scale: slot.transform.scale,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (component["comp.typename"] === "SlotComponent") {
|
||||
slots[component["sc.slotName"]] = {
|
||||
target: hierarchy.name,
|
||||
position: component["sc.attachmentTranslation"] as Vector3Tuple,
|
||||
rotation: component["sc.attachmentRotation"] as Vector4Tuple,
|
||||
scale: component["sc.attachmentScale"] as Vector3Tuple,
|
||||
};
|
||||
}
|
||||
|
||||
if (component["comp.typename"] !== "StateSwitcherComponent") continue;
|
||||
|
||||
const active = component[`ssc.state${component["ssc.activeState"]}`];
|
||||
// camos without their own variant keep the default piece, but groups
|
||||
// of elements to hide stay hidden
|
||||
let state: string | null =
|
||||
active && !/skin\d*/.test(active) && !active.includes("hide")
|
||||
? active
|
||||
: null;
|
||||
|
||||
for (let index = 0; index < component["ssc.statesCount"]; index++) {
|
||||
const name = component[`ssc.state${index}`];
|
||||
const match = name.match(/skin(\d*)/);
|
||||
|
||||
if (
|
||||
match &&
|
||||
!name.includes("hide") &&
|
||||
Number(match[1] || 1) === skinNumber
|
||||
) {
|
||||
state = name;
|
||||
}
|
||||
}
|
||||
|
||||
states[hierarchy.name] = state;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const document = new Document();
|
||||
const scene = document.createScene().setExtras({ states });
|
||||
const targets = new Map<string, Node>();
|
||||
|
||||
document.createBuffer();
|
||||
|
||||
if (preset) await extractMaterials(document, sc2, basePath, preset);
|
||||
|
||||
for (const name in customEntities) {
|
||||
const slot = slots[name];
|
||||
const path = items.get(customEntities[name].item);
|
||||
|
||||
if (
|
||||
!slot ||
|
||||
!path ||
|
||||
!(await vfs.resolve(`Data/3d/${path}.sc2`)) ||
|
||||
!(await vfs.resolve(`Data/3d/${path}.scg`))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const part = await extractModel(vfs, path, true);
|
||||
const map = mergeDocuments(document, part);
|
||||
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
|
||||
const node = document
|
||||
.createNode(name)
|
||||
.setTranslation(slot.position)
|
||||
.setRotation(slot.rotation)
|
||||
.setScale(slot.scale);
|
||||
let target = targets.get(slot.target);
|
||||
|
||||
if (!target) {
|
||||
target = document.createNode(slot.target);
|
||||
targets.set(slot.target, target);
|
||||
scene.addChild(target);
|
||||
}
|
||||
|
||||
for (const child of partScene.listChildren()) {
|
||||
node.addChild(child as Node);
|
||||
}
|
||||
|
||||
partScene.dispose();
|
||||
target.addChild(node);
|
||||
}
|
||||
|
||||
applySkinNumber(document, SKIN_NUMBER);
|
||||
|
||||
await document.transform(
|
||||
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.TEXTURE] }),
|
||||
unpartition(),
|
||||
);
|
||||
|
||||
return new Response(await nodeIO.writeBinary(document));
|
||||
}
|
||||
|
||||
function applySkinNumber(document: Document, number: string) {
|
||||
const start = Math.ceil((SKIN_NUMBER_SLOTS - number.length) / 2) + 1;
|
||||
|
||||
for (const node of document.getRoot().listNodes()) {
|
||||
const match = node.getName().match(/_number_\d+_(\d+)$/);
|
||||
|
||||
if (!match) continue;
|
||||
|
||||
const digit = number[Number(match[1]) - start];
|
||||
|
||||
if (digit === undefined) {
|
||||
node.dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
const cell = (Number(digit) + 9) % 10;
|
||||
const column = cell % DIGIT_COLUMNS;
|
||||
const row = Math.floor(cell / DIGIT_COLUMNS);
|
||||
|
||||
node.traverse((child) => {
|
||||
for (const primitive of child.getMesh()?.listPrimitives() ?? []) {
|
||||
const uvs = primitive.getAttribute("TEXCOORD_0")?.clone();
|
||||
|
||||
if (!uvs) continue;
|
||||
|
||||
for (let index = 0; index < uvs.getCount(); index++) {
|
||||
const [u, v] = uvs.getElement(index, [0, 0]) as [number, number];
|
||||
|
||||
uvs.setElement(index, [
|
||||
(column + u) / DIGIT_COLUMNS,
|
||||
(row + v) / DIGIT_ROWS,
|
||||
]);
|
||||
}
|
||||
|
||||
primitive.setAttribute("TEXCOORD_0", uvs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||