mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Merge branch 'main' into restore-locale-redirect
This commit is contained in:
54 files changed
+586
-109
No files matched your search
@@ -1,7 +1,75 @@
|
|||||||
# BlitzKit
|
# BlitzKit
|
||||||
|
|
||||||
Tools for everything and anything, World of Tanks Blitz.
|
Your Blitz experience, elevated. https://blitzkit.app/.
|
||||||
|
|
||||||
This GitHub repository contains the code for BlitzKit and all its services. If you are here, looking to use BlitzKit, go to https://blitzkit.app/.
|
## Developing: Prerequisites
|
||||||
|
|
||||||
If you intend to contribute to the code, note that you may need to check out sensitive, closed-source submodules to locally develop certain aspects of BlitzKit. Communicate with `@tresabhi` on Discord to get started or discuss sensitive content, such as environment variables or submodules.
|
There's a few things you'll need to get started. If this is your first time working with BlitzKit, it's completely safe to ignore all optional prerequisites below to get things running as quickly as possible.
|
||||||
|
|
||||||
|
### Bun
|
||||||
|
|
||||||
|
[Bun](https://bun.sh/) is the package manager and runtime that BlitzKit uses.
|
||||||
|
|
||||||
|
Ideally, you should also uninstall Node.js if you have it installed to avoid any cases where Node.js is used to run code instead of Bun which happens in some cases. But, this is not required and is unlikely to cause issues.
|
||||||
|
|
||||||
|
### Protocol Buffer
|
||||||
|
|
||||||
|
BlitzKit uses [Protocol Buffer's protoc CLI](https://protobuf.dev/installation/) to generate efficiently packed data structures to serve data to clients in place of JSON.
|
||||||
|
|
||||||
|
### World of Tanks Blitz
|
||||||
|
|
||||||
|
You will need a local installation of [World of Tanks Blitz](https://wotblitz.com/) for BlitzKit to build off.
|
||||||
|
|
||||||
|
### aria2 (Optional)
|
||||||
|
|
||||||
|
If you plan to use BlitzKit's client-management CLI for BlitzKit Previews, you will need to [get aria2](https://aria2.github.io/) to speed up your downloads.
|
||||||
|
|
||||||
|
### libarchive (Optional)
|
||||||
|
|
||||||
|
If you plan to use BlitzKit's client-management CLI for BlitzKit OpenTests, you will need to [get libarchive](https://www.libarchive.org/) to keep your downloads completely in-memory.
|
||||||
|
|
||||||
|
### Linux (Optional)
|
||||||
|
|
||||||
|
All work on BlitzKit is done on Linux and macOS. It is theoretically possible to work on Windows; please feel free to try and share your discoveries with us!
|
||||||
|
|
||||||
|
## Developing: Setting Up
|
||||||
|
|
||||||
|
Start by cloning the repository and changing directories.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/blitzkit/blitzkit.git
|
||||||
|
cd blitzkit
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> There is a closed source submodule under `packages/closed` that you can safely ignore. This sole purpose of this submodule is to provide `dvp.ts`, a proprietary Wargaming game asset tree compression tool. You will not need this at any point.
|
||||||
|
|
||||||
|
Then, download the dependencies with Bun.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, compile the proto files into TypeScript.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build build-protos
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing: Environment
|
||||||
|
|
||||||
|
Go through the different packages in the `packages` directory and clone their respective `template.env`'s to `.env` files. Follow the instructions in the `.env` files to fill them out as much as you can.
|
||||||
|
|
||||||
|
## Developing: Running
|
||||||
|
|
||||||
|
Launching the website is as easy as running the `dev` script.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
If you ever make a change to any proto file, make sure to rebuild them.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build build-protos
|
||||||
|
```
|
||||||
@@ -6,6 +6,8 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions
|
|||||||
|
|
||||||
- [Mark Greene] Added Peekaboo DPM
|
- [Mark Greene] Added Peekaboo DPM
|
||||||
- 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
|
- 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
|
||||||
|
- [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] Added a spall liner button to the Tankopedia beside the other equipment
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
@@ -15,6 +17,20 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions
|
|||||||
- [Mark Greene] The armor no longer disappears when you turn on Dynamic Armor System
|
- [Mark Greene] 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] Camouflage values in Tankopedia are now more descriptive when shooting
|
||||||
- [Mark Greene] The contact shadows on the ground no longer grow & darken when switching between tabs
|
- [Mark Greene] The contact shadows on the ground no longer grow & darken when switching between tabs
|
||||||
|
- The "Help Translate BlitzKit" text on the home page is now properly centered vertically
|
||||||
|
- [Mark Greene] The footer now says "Branch" instead of "CDN"
|
||||||
|
- [Mark Greene] The game version in the footer now displays a shorthand version instead of the entire version number
|
||||||
|
- [Mark Greene] The reload visualizer now displays just 1 decimal place
|
||||||
|
- [Mark Greene] The search bar no longer redirects to a seemingly random tank when clicking enter when searching
|
||||||
|
- [Mark Greene] Tank search now properly handles the full names of tanks like the Super Pershing
|
||||||
|
- [Mark Greene] The loading bar below the name of the tanks in Tankopedia is now sufficiently spaced below
|
||||||
|
- [Mark Greene] The gun depression visualizer now properly handles the inherent tilt of tanks like the Minotauro
|
||||||
|
|
||||||
|
## Technical Features
|
||||||
|
|
||||||
|
- Added documentation to the README on setting up and developing BlitzKit locally
|
||||||
|
- Added bug report and feature request forms to GitHub
|
||||||
|
- Switched to the GNU AGPLv3 license
|
||||||
|
|
||||||
## Technical Fixes
|
## Technical Fixes
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -4,12 +4,13 @@
|
|||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cd packages/website && bun run dev",
|
"dev": "cd packages/website && bun --bun run dev",
|
||||||
"build": "cd packages/website && bun run build",
|
"build": "cd packages/website && bun run build",
|
||||||
"discover-ids": "cd packages/scripts && bun run discover-ids",
|
"discover-ids": "cd packages/scripts && bun run discover-ids",
|
||||||
"build-protos": "cd packages/scripts && bun run build-protos",
|
"build-protos": "cd packages/scripts && bun run build-protos",
|
||||||
"record-performance": "cd packages/scripts && bun run record-performance",
|
"record-performance": "cd packages/scripts && bun run record-performance",
|
||||||
"download-client": "cd packages/scripts && bun run download-client"
|
"download-client": "cd packages/scripts && bun run download-client",
|
||||||
|
"clean-astro": "rm -rf packages/website/.astro packages/website/node_modules/.astro packages/website/node_modules/.vite"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import {
|
|||||||
VehicleDefinitionList,
|
VehicleDefinitionList,
|
||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
import { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||||
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n";
|
|
||||||
import locales from "@blitzkit/i18n/locales.json";
|
import locales from "@blitzkit/i18n/locales.json";
|
||||||
|
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n/src/strings";
|
||||||
import { parse as parseYaml } from "yaml";
|
import { parse as parseYaml } from "yaml";
|
||||||
import { BlitzKitAPI } from "../base";
|
import { BlitzKitAPI } from "../base";
|
||||||
|
|
||||||
@@ -113,6 +113,9 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
const { existsSync } = await import("fs");
|
||||||
|
const { readFile, writeFile, mkdir } = await import("fs/promises");
|
||||||
|
|
||||||
console.log("Initializing virtual file system...");
|
console.log("Initializing virtual file system...");
|
||||||
await this.vfs.init();
|
await this.vfs.init();
|
||||||
|
|
||||||
@@ -155,31 +158,37 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
|
|||||||
|
|
||||||
console.log("Fetching game localizations...");
|
console.log("Fetching game localizations...");
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
locales.supported = [locales.supported[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
let fetchedLocalizations = 0;
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
locales.supported.map(async ({ locale }) => {
|
locales.supported.map(async ({ locale }) => {
|
||||||
|
const root = "../../temp/game-strings";
|
||||||
|
const cachePath = `${root}/${locale}.json`;
|
||||||
|
|
||||||
|
if (existsSync(cachePath)) {
|
||||||
|
console.log(` Cache hit for ${locale}`);
|
||||||
|
|
||||||
|
const content = await readFile(cachePath, "utf-8");
|
||||||
|
this.stringsI18n[locale] = JSON.parse(content) as BlitzStrings;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Cache miss for ${locale} (this will take a while...)`);
|
||||||
|
|
||||||
const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale];
|
const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale];
|
||||||
const cache = await fetch(
|
const networkStrings = (await fetch(
|
||||||
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`,
|
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`,
|
||||||
)
|
)
|
||||||
.then((response) => response.text())
|
.then((response) => response.text())
|
||||||
.then((string) => parseYaml(string) as BlitzStrings);
|
.then(parseYaml)) as BlitzStrings;
|
||||||
const preInstalled = await this.vfs.yaml<BlitzStrings>(
|
const localStrings = await this.vfs.yaml<BlitzStrings>(
|
||||||
`Data/Strings/${blitzLocale}.yaml`,
|
`Data/Strings/${blitzLocale}.yaml`,
|
||||||
);
|
);
|
||||||
|
const combined = { ...networkStrings, ...localStrings };
|
||||||
|
|
||||||
this.stringsI18n[locale] = {
|
await mkdir(root, { recursive: true });
|
||||||
...cache,
|
await writeFile(cachePath, JSON.stringify(combined));
|
||||||
...preInstalled,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(
|
this.stringsI18n[locale] = combined;
|
||||||
`Fetched localizations for ${locale} (${++fetchedLocalizations}/${locales.supported.length})`,
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -350,6 +350,15 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
|||||||
const tankClass =
|
const tankClass =
|
||||||
this.blitzTankClassToBlitzkit[blitzTankClass as BlitzTankClass];
|
this.blitzTankClassToBlitzkit[blitzTankClass as BlitzTankClass];
|
||||||
|
|
||||||
|
const canonicalFullName = this.getString(tank.userString);
|
||||||
|
const name =
|
||||||
|
(tank.shortUserString
|
||||||
|
? this.getString(tank.shortUserString)
|
||||||
|
: undefined) ?? canonicalFullName;
|
||||||
|
// only supply full name if it's different from the displayed short name to save some bytes
|
||||||
|
const name_full =
|
||||||
|
name === canonicalFullName ? undefined : canonicalFullName;
|
||||||
|
|
||||||
tankDefinitions.tanks[tankId] = {
|
tankDefinitions.tanks[tankId] = {
|
||||||
ancestors: [],
|
ancestors: [],
|
||||||
successors: [],
|
successors: [],
|
||||||
@@ -367,10 +376,8 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
|||||||
typeof equipment === "string" ? equipment : equipment.at(-1)!,
|
typeof equipment === "string" ? equipment : equipment.at(-1)!,
|
||||||
max_consumables: tankDefinition.root.consumableSlots,
|
max_consumables: tankDefinition.root.consumableSlots,
|
||||||
max_provisions: tankDefinition.root.provisionSlots,
|
max_provisions: tankDefinition.root.provisionSlots,
|
||||||
name:
|
name,
|
||||||
(tank.shortUserString
|
name_full,
|
||||||
? this.getString(tank.shortUserString)
|
|
||||||
: undefined) ?? this.getString(tank.userString),
|
|
||||||
slug,
|
slug,
|
||||||
nation,
|
nation,
|
||||||
type: tankTags.includes("collectible")
|
type: tankTags.includes("collectible")
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export function metaSortTank(
|
|||||||
gameDefinitions: GameDefinitions,
|
gameDefinitions: GameDefinitions,
|
||||||
) {
|
) {
|
||||||
return tanks
|
return tanks
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
.sort((a, b) => b.tier - a.tier)
|
.sort((a, b) => b.tier - a.tier)
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type),
|
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type),
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export const TIER_ROMAN_NUMERALS: Record<number, string> = {
|
|||||||
8: "VIII",
|
8: "VIII",
|
||||||
9: "IX",
|
9: "IX",
|
||||||
10: "X",
|
10: "X",
|
||||||
|
11: "XI",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const flags: Record<string, string> = {
|
export const flags: Record<string, string> = {
|
||||||
|
|||||||
@@ -18,11 +18,22 @@ export async function fetchTankNames(api: BlitzKitAPI) {
|
|||||||
searchableNameDeburr.locales[key] = deburr(value);
|
searchableNameDeburr.locales[key] = deburr(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const searchableNameFull: I18nString = {
|
||||||
|
locales: { ...tank.name_full?.locales },
|
||||||
|
};
|
||||||
|
const searchableNameFullDeburr: I18nString = { locales: {} };
|
||||||
|
|
||||||
|
Object.entries(searchableNameFull.locales).forEach(([key, value]) => {
|
||||||
|
searchableNameFullDeburr.locales[key] = deburr(value);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: tank.id,
|
id: tank.id,
|
||||||
name: tank.name,
|
name: tank.name,
|
||||||
searchableName: tank.name,
|
searchableName: tank.name,
|
||||||
searchableNameDeburr,
|
searchableNameDeburr,
|
||||||
|
searchableNameFull,
|
||||||
|
searchableNameFullDeburr,
|
||||||
camouflages: tank.camouflages
|
camouflages: tank.camouflages
|
||||||
?.map((id) =>
|
?.map((id) =>
|
||||||
locales.supported.map(
|
locales.supported.map(
|
||||||
@@ -45,6 +56,8 @@ export const SEARCH_KEYS = [
|
|||||||
.map(({ locale }) => [
|
.map(({ locale }) => [
|
||||||
`searchableName.locales.${locale}`,
|
`searchableName.locales.${locale}`,
|
||||||
`searchableNameDeburr.locales.${locale}`,
|
`searchableNameDeburr.locales.${locale}`,
|
||||||
|
`searchableNameFull.locales.${locale}`,
|
||||||
|
`searchableNameFullDeburr.locales.${locale}`,
|
||||||
])
|
])
|
||||||
.flat(),
|
.flat(),
|
||||||
"camouflages",
|
"camouflages",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ message TankDefinition {
|
|||||||
required uint32 weight = 31;
|
required uint32 weight = 31;
|
||||||
required string dev_name = 32;
|
required string dev_name = 32;
|
||||||
optional Equalizer equalizer = 33;
|
optional Equalizer equalizer = 33;
|
||||||
|
optional I18nString name_full = 34;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Equalizer {
|
message Equalizer {
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ export interface TankDefinition {
|
|||||||
weight: number;
|
weight: number;
|
||||||
dev_name: string;
|
dev_name: string;
|
||||||
equalizer?: Equalizer | undefined;
|
equalizer?: Equalizer | undefined;
|
||||||
|
name_full?: I18nString | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TankDefinition_RolesEntry {
|
export interface TankDefinition_RolesEntry {
|
||||||
@@ -651,6 +652,7 @@ function createBaseTankDefinition(): TankDefinition {
|
|||||||
weight: 0,
|
weight: 0,
|
||||||
dev_name: "",
|
dev_name: "",
|
||||||
equalizer: undefined,
|
equalizer: undefined,
|
||||||
|
name_full: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,6 +754,9 @@ export const TankDefinition: MessageFns<TankDefinition> = {
|
|||||||
if (message.equalizer !== undefined) {
|
if (message.equalizer !== undefined) {
|
||||||
Equalizer.encode(message.equalizer, writer.uint32(266).fork()).join();
|
Equalizer.encode(message.equalizer, writer.uint32(266).fork()).join();
|
||||||
}
|
}
|
||||||
|
if (message.name_full !== undefined) {
|
||||||
|
I18nString.encode(message.name_full, writer.uint32(274).fork()).join();
|
||||||
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1051,6 +1056,14 @@ export const TankDefinition: MessageFns<TankDefinition> = {
|
|||||||
message.equalizer = Equalizer.decode(reader, reader.uint32());
|
message.equalizer = Equalizer.decode(reader, reader.uint32());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
case 34: {
|
||||||
|
if (tag !== 274) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.name_full = I18nString.decode(reader, reader.uint32());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
@@ -1111,6 +1124,7 @@ export const TankDefinition: MessageFns<TankDefinition> = {
|
|||||||
weight: isSet(object.weight) ? globalThis.Number(object.weight) : 0,
|
weight: isSet(object.weight) ? globalThis.Number(object.weight) : 0,
|
||||||
dev_name: isSet(object.dev_name) ? globalThis.String(object.dev_name) : "",
|
dev_name: isSet(object.dev_name) ? globalThis.String(object.dev_name) : "",
|
||||||
equalizer: isSet(object.equalizer) ? Equalizer.fromJSON(object.equalizer) : undefined,
|
equalizer: isSet(object.equalizer) ? Equalizer.fromJSON(object.equalizer) : undefined,
|
||||||
|
name_full: isSet(object.name_full) ? I18nString.fromJSON(object.name_full) : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1218,6 +1232,9 @@ export const TankDefinition: MessageFns<TankDefinition> = {
|
|||||||
if (message.equalizer !== undefined) {
|
if (message.equalizer !== undefined) {
|
||||||
obj.equalizer = Equalizer.toJSON(message.equalizer);
|
obj.equalizer = Equalizer.toJSON(message.equalizer);
|
||||||
}
|
}
|
||||||
|
if (message.name_full !== undefined) {
|
||||||
|
obj.name_full = I18nString.toJSON(message.name_full);
|
||||||
|
}
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1271,6 +1288,9 @@ export const TankDefinition: MessageFns<TankDefinition> = {
|
|||||||
message.equalizer = (object.equalizer !== undefined && object.equalizer !== null)
|
message.equalizer = (object.equalizer !== undefined && object.equalizer !== null)
|
||||||
? Equalizer.fromPartial(object.equalizer)
|
? Equalizer.fromPartial(object.equalizer)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
message.name_full = (object.name_full !== undefined && object.name_full !== null)
|
||||||
|
? I18nString.fromPartial(object.name_full)
|
||||||
|
: undefined;
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -215,6 +215,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "Development mode",
|
"dev_mode": "Development mode",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Language"
|
"language": "Language"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -458,6 +459,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
|
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Hull down",
|
"hull_down": "Hull down",
|
||||||
"face_hug": "Face hug",
|
"face_hug": "Face hug",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "Modo de desarrollador",
|
"dev_mode": "Modo de desarrollador",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Idioma"
|
"language": "Idioma"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Hull down",
|
"hull_down": "Hull down",
|
||||||
"face_hug": "Cara a cara",
|
"face_hug": "Cara a cara",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Paramètres",
|
"title": "Paramètres",
|
||||||
"description": "Modifier les paramètres de BlitzKit",
|
"description": "Modifier les paramètres de BlitzKit",
|
||||||
"dev_mode": "Mode développement",
|
"dev_mode": "Mode développement",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Langue"
|
"language": "Langue"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Hull down",
|
"hull_down": "Hull down",
|
||||||
"face_hug": "Face hug",
|
"face_hug": "Face hug",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "開発者モード",
|
"dev_mode": "開発者モード",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "言語 / Language"
|
"language": "言語 / Language"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "ハルダウン",
|
"hull_down": "ハルダウン",
|
||||||
"face_hug": "密着",
|
"face_hug": "密着",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "Development mode",
|
"dev_mode": "Development mode",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Language"
|
"language": "Language"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Hull down",
|
"hull_down": "Hull down",
|
||||||
"face_hug": "Face hug",
|
"face_hug": "Face hug",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "Modo de desenvolvimento",
|
"dev_mode": "Modo de desenvolvimento",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Idioma"
|
"language": "Idioma"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Casco para baixo",
|
"hull_down": "Casco para baixo",
|
||||||
"face_hug": "Abraço facial",
|
"face_hug": "Abraço facial",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Настройки",
|
"title": "Настройки",
|
||||||
"description": "Изменить настройки BlitzKit",
|
"description": "Изменить настройки BlitzKit",
|
||||||
"dev_mode": "Режим разработчика",
|
"dev_mode": "Режим разработчика",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Язык"
|
"language": "Язык"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "За холмом",
|
"hull_down": "За холмом",
|
||||||
"face_hug": "В упор",
|
"face_hug": "В упор",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Налаштування",
|
"title": "Налаштування",
|
||||||
"description": "Змінити налаштування BlitzKit ",
|
"description": "Змінити налаштування BlitzKit ",
|
||||||
"dev_mode": "Режим розробника",
|
"dev_mode": "Режим розробника",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Мова"
|
"language": "Мова"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Від башти",
|
"hull_down": "Від башти",
|
||||||
"face_hug": "Клінч",
|
"face_hug": "Клінч",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "设置",
|
"title": "设置",
|
||||||
"description": "更改 BlitzKit 设置",
|
"description": "更改 BlitzKit 设置",
|
||||||
"dev_mode": "开发者模式",
|
"dev_mode": "开发者模式",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "语言"
|
"language": "语言"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "俯角视角",
|
"hull_down": "俯角视角",
|
||||||
"face_hug": "顶牛视角",
|
"face_hug": "顶牛视角",
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ let regionI = 0;
|
|||||||
|
|
||||||
console.log("Starting discovery loop...");
|
console.log("Starting discovery loop...");
|
||||||
|
|
||||||
atMaxRate(async ({ _break: break1 }) => {
|
await atMaxRate(async ({ _break: break1 }) => {
|
||||||
if (regions.length === 0) {
|
if (regions.length === 0) {
|
||||||
console.log("All regions exhausted, exiting discovery loop...");
|
console.log("All regions exhausted, exiting discovery loop...");
|
||||||
return break1();
|
return break1();
|
||||||
@@ -166,7 +166,6 @@ atMaxRate(async ({ _break: break1 }) => {
|
|||||||
fields
|
fields
|
||||||
}&account_id=${accountIds}`;
|
}&account_id=${accountIds}`;
|
||||||
|
|
||||||
atMaxRate(async ({ _break: break2 }) => {
|
|
||||||
if (idRange.length < MAX_IDS_PER_CALL) {
|
if (idRange.length < MAX_IDS_PER_CALL) {
|
||||||
console.log(
|
console.log(
|
||||||
`Removing region ${region.domain} removed due to ID range exhaustion...`,
|
`Removing region ${region.domain} removed due to ID range exhaustion...`,
|
||||||
@@ -174,10 +173,12 @@ atMaxRate(async ({ _break: break1 }) => {
|
|||||||
regions.splice(regionI, 1);
|
regions.splice(regionI, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url);
|
await atMaxRate(async ({ _break: break2 }) => {
|
||||||
const data = (await response.json()) as QuickInfo;
|
const data = await fetch(url)
|
||||||
|
.then((response) => response.json() as Promise<QuickInfo>)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
if (data.status !== "ok") {
|
if (data === null || data.status !== "ok") {
|
||||||
console.warn("Failed API call; trying again...");
|
console.warn("Failed API call; trying again...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ interface MethodContext {
|
|||||||
let lastT = 0;
|
let lastT = 0;
|
||||||
const maxDelta = 1000 / API_RATE;
|
const maxDelta = 1000 / API_RATE;
|
||||||
const DELTA_BUFFER = 0;
|
const DELTA_BUFFER = 0;
|
||||||
export async function atMaxRate(method: (context: MethodContext) => void) {
|
export async function atMaxRate(
|
||||||
|
method: (context: MethodContext) => void | Promise<void>,
|
||||||
|
) {
|
||||||
let isBroken = false;
|
let isBroken = false;
|
||||||
|
|
||||||
const context: MethodContext = {
|
const context: MethodContext = {
|
||||||
@@ -39,7 +41,7 @@ export async function atMaxRate(method: (context: MethodContext) => void) {
|
|||||||
await sleep(maxDelta - delta + DELTA_BUFFER);
|
await sleep(maxDelta - delta + DELTA_BUFFER);
|
||||||
}
|
}
|
||||||
|
|
||||||
method(context);
|
await method(context);
|
||||||
lastT = t;
|
lastT = t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,9 @@ import { promisify } from "util";
|
|||||||
const MAX_COMMAND_LENGTH = 2 ** 11;
|
const MAX_COMMAND_LENGTH = 2 ** 11;
|
||||||
const TS_PROTO_EXECUTABLE_LOCATIONS = [
|
const TS_PROTO_EXECUTABLE_LOCATIONS = [
|
||||||
"./node_modules/.bin/protoc-gen-ts_proto",
|
"./node_modules/.bin/protoc-gen-ts_proto",
|
||||||
|
"./node_modules/.bin/protoc-gen-ts_proto.exe",
|
||||||
"../../node_modules/.bin/protoc-gen-ts_proto",
|
"../../node_modules/.bin/protoc-gen-ts_proto",
|
||||||
|
"../../node_modules/.bin/protoc-gen-ts_proto.exe",
|
||||||
];
|
];
|
||||||
|
|
||||||
const roots = [
|
const roots = [
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ import {
|
|||||||
import { degToRad } from "three/src/math/MathUtils.js";
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||||
|
import {
|
||||||
|
SPALL_LINER_HE_DAMAGE_DELTA,
|
||||||
|
SPALL_LINER_PROVISION_ID,
|
||||||
|
} from "../../../../core/blitzkit/spallLiner";
|
||||||
import { defaultEqualizer } from "../../../../core/blitzkit/tankToDuelMember";
|
import { defaultEqualizer } from "../../../../core/blitzkit/tankToDuelMember";
|
||||||
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
|
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
|
||||||
import { Duel } from "../../../../stores/duel";
|
import { Duel } from "../../../../stores/duel";
|
||||||
@@ -384,6 +388,13 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
shot.damage *= antagonistEqualizer.damage;
|
shot.damage *= antagonistEqualizer.damage;
|
||||||
|
|
||||||
|
const hasSpallLiner = Duel.state.protagonist.provisions.includes(
|
||||||
|
SPALL_LINER_PROVISION_ID,
|
||||||
|
);
|
||||||
|
if (hasSpallLiner && shell.type === ShellType.SHELL_TYPE_HE) {
|
||||||
|
shot.damage *= 1 + SPALL_LINER_HE_DAMAGE_DELTA;
|
||||||
|
}
|
||||||
|
|
||||||
return shot;
|
return shot;
|
||||||
},
|
},
|
||||||
[camera],
|
[camera],
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gameDefinitions = await api.gameDefinitions();
|
const gameDefinitions = await api.gameDefinitions();
|
||||||
|
const gameVersion = gameDefinitions.version.split("_")[0];
|
||||||
const environment = assertSecret(import.meta.env.PUBLIC_ENVIRONMENT);
|
const environment = assertSecret(import.meta.env.PUBLIC_ENVIRONMENT);
|
||||||
const { locale } = Astro.props;
|
const { locale } = Astro.props;
|
||||||
const strings = getStrings(locale);
|
const strings = getStrings(locale);
|
||||||
@@ -141,7 +142,7 @@ const strings = getStrings(locale);
|
|||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Text size="2" color="gray">CDN</Text>
|
<Text size="2" color="gray">Branch</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex direction="column" gap="1" flexGrow="1" flexBasis="0" align="start">
|
<Flex direction="column" gap="1" flexGrow="1" flexBasis="0" align="start">
|
||||||
@@ -151,7 +152,7 @@ const strings = getStrings(locale);
|
|||||||
{
|
{
|
||||||
environment !== "preview" && (
|
environment !== "preview" && (
|
||||||
<Code size="2" color="gray">
|
<Code size="2" color="gray">
|
||||||
{gameDefinitions.version}
|
{gameVersion}
|
||||||
</Code>
|
</Code>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,13 +175,13 @@ const strings = getStrings(locale);
|
|||||||
WoTB
|
WoTB
|
||||||
</Text>
|
</Text>
|
||||||
<Code size="2" color="gray">
|
<Code size="2" color="gray">
|
||||||
{gameDefinitions.version}
|
{gameVersion}
|
||||||
</Code>
|
</Code>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<Text size="2" color="gray">CDN</Text>
|
<Text size="2" color="gray">Branch</Text>
|
||||||
<Code size="2" color="gray">
|
<Code size="2" color="gray">
|
||||||
{environment}
|
{environment}
|
||||||
</Code>
|
</Code>
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import fuzzysort from "fuzzysort";
|
import fuzzysort from "fuzzysort";
|
||||||
import { debounce, times } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
||||||
|
import { awaitableTiers } from "../core/awaitables/tiers";
|
||||||
import { api } from "../core/blitzkit/api";
|
import { api } from "../core/blitzkit/api";
|
||||||
import { useLocale } from "../hooks/useLocale";
|
import { useLocale } from "../hooks/useLocale";
|
||||||
import { Guess, GuessState } from "../stores/guess";
|
import { Guess, GuessState } from "../stores/guess";
|
||||||
@@ -38,9 +39,10 @@ import { SearchResults } from "./SearchResults";
|
|||||||
|
|
||||||
const { go } = fuzzysort;
|
const { go } = fuzzysort;
|
||||||
|
|
||||||
const [tankNames, tankDefinitions] = await Promise.all([
|
const [tankNames, tankDefinitions, TIERS] = await Promise.all([
|
||||||
awaitableTankNames,
|
awaitableTankNames,
|
||||||
api.tankDefinitions(),
|
api.tankDefinitions(),
|
||||||
|
awaitableTiers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ids = Object.keys(tankDefinitions.tanks);
|
const ids = Object.keys(tankDefinitions.tanks);
|
||||||
@@ -193,8 +195,7 @@ export function Guesser() {
|
|||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
{times(10, (index) => {
|
{TIERS.map((tier) => {
|
||||||
const tier = 10 - index;
|
|
||||||
const isSelected = tiers.includes(tier);
|
const isSelected = tiers.includes(tier);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
type FlexProps,
|
type FlexProps,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import { times } from "lodash-es";
|
|
||||||
import { Fragment, type ComponentProps, type ReactNode } from "react";
|
import { Fragment, type ComponentProps, type ReactNode } from "react";
|
||||||
|
import { awaitableTiers } from "../../../core/awaitables/tiers";
|
||||||
import { api } from "../../../core/blitzkit/api";
|
import { api } from "../../../core/blitzkit/api";
|
||||||
import { useLocale } from "../../../hooks/useLocale";
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import { App } from "../../../stores/app";
|
import { App } from "../../../stores/app";
|
||||||
@@ -171,7 +171,7 @@ const GUN_TYPES = Object.keys(
|
|||||||
|
|
||||||
const MAX_ICONS = 4;
|
const MAX_ICONS = 4;
|
||||||
|
|
||||||
const TIERS = times(10, (i) => 10 - i);
|
const TIERS = await awaitableTiers;
|
||||||
|
|
||||||
export function FilterControl() {
|
export function FilterControl() {
|
||||||
return (
|
return (
|
||||||
@@ -789,6 +789,7 @@ function OwnershipFilterInternal(props: FlexProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.showOwned = !draft.showOwned;
|
draft.showOwned = !draft.showOwned;
|
||||||
|
if (draft.showOwned) draft.showUnowned = false;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -808,6 +809,7 @@ function OwnershipFilterInternal(props: FlexProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.showUnowned = !draft.showUnowned;
|
draft.showUnowned = !draft.showUnowned;
|
||||||
|
if (draft.showUnowned) draft.showOwned = false;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -834,6 +836,7 @@ function TestFilter() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.showNonTesting = !draft.showNonTesting;
|
draft.showNonTesting = !draft.showNonTesting;
|
||||||
|
if (draft.showNonTesting) draft.showTesting = false;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -854,6 +857,7 @@ function TestFilter() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.showTesting = !draft.showTesting;
|
draft.showTesting = !draft.showTesting;
|
||||||
|
if (draft.showTesting) draft.showNonTesting = false;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
draft.search = sanitized.length === 0 ? null : sanitized;
|
draft.search = sanitized.length === 0 ? null : sanitized;
|
||||||
});
|
});
|
||||||
}, 500),
|
}, 500),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
const handleChange = useCallback(() => {
|
const handleChange = useCallback(() => {
|
||||||
if (!searching) {
|
if (!searching) {
|
||||||
@@ -46,7 +46,14 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
}, [searching]);
|
}, [searching]);
|
||||||
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
||||||
(event) => {
|
(event) => {
|
||||||
if (event.key !== "Enter" || !topResult || searching) return;
|
if (
|
||||||
|
event.key !== "Enter" ||
|
||||||
|
!topResult ||
|
||||||
|
!TankFilters.state.search ||
|
||||||
|
TankFilters.state.searching
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -56,7 +63,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
window.location.href = `/tanks/${topResult.slug}`;
|
window.location.href = `/tanks/${topResult.slug}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[topResult]
|
[topResult],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||||
import { Flex, IconButton, Text } from "@radix-ui/themes";
|
import { Flex, IconButton, Text } from "@radix-ui/themes";
|
||||||
import { times } from "lodash-es";
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
import { awaitableTiers } from "../../../core/awaitables/tiers";
|
||||||
import { TankFilters } from "../../../stores/tankFilters";
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { TankSort } from "../../../stores/tankopediaSort";
|
import { TankSort } from "../../../stores/tankopediaSort";
|
||||||
|
|
||||||
|
const TIERS = await awaitableTiers;
|
||||||
|
|
||||||
export const TierFilter = memo(() => {
|
export const TierFilter = memo(() => {
|
||||||
const by = TankSort.use((state) => state.by);
|
const by = TankSort.use((state) => state.by);
|
||||||
const search = TankFilters.use((state) => state.search);
|
const search = TankFilters.use((state) => state.search);
|
||||||
@@ -19,8 +21,7 @@ export const TierFilter = memo(() => {
|
|||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: "var(--radius-full)" }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
>
|
>
|
||||||
{times(10, (index) => {
|
{TIERS.map((tier) => {
|
||||||
const tier = 10 - index;
|
|
||||||
const selected = tiers.includes(tier);
|
const selected = tiers.includes(tier);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+4
-4
@@ -17,7 +17,7 @@ import { AssaultRangesVisualizer } from "./AssaultRangesVisualizer";
|
|||||||
import { GunFlexibilityVisualizer } from "./GunFlexibilityVisualizer";
|
import { GunFlexibilityVisualizer } from "./GunFlexibilityVisualizer";
|
||||||
import { Info } from "./Info";
|
import { Info } from "./Info";
|
||||||
import { InfoWithDelta } from "./InfoWithDelta";
|
import { InfoWithDelta } from "./InfoWithDelta";
|
||||||
import { ReloadVisualizer } from "./ReloadVisualizer";
|
import { RELOAD_PRECISION, ReloadVisualizer } from "./ReloadVisualizer";
|
||||||
import { RicochetVisualizer } from "./RicochetVisualizer";
|
import { RicochetVisualizer } from "./RicochetVisualizer";
|
||||||
import { StatsTableWrapper } from "./StatsTableWrapper";
|
import { StatsTableWrapper } from "./StatsTableWrapper";
|
||||||
import type { StatsAcceptorProps } from "./TraverseVisualizer";
|
import type { StatsAcceptorProps } from "./TraverseVisualizer";
|
||||||
@@ -163,7 +163,7 @@ export function Firepower({
|
|||||||
.shell_index,
|
.shell_index,
|
||||||
{ index: index + 1 },
|
{ index: index + 1 },
|
||||||
)}
|
)}
|
||||||
decimals={2}
|
decimals={RELOAD_PRECISION}
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
noRanking
|
noRanking
|
||||||
value={() => reload}
|
value={() => reload}
|
||||||
@@ -173,7 +173,7 @@ export function Firepower({
|
|||||||
) : (
|
) : (
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
stats={stats}
|
stats={stats}
|
||||||
decimals={2}
|
decimals={RELOAD_PRECISION}
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
value="shellReload"
|
value="shellReload"
|
||||||
/>
|
/>
|
||||||
@@ -183,7 +183,7 @@ export function Firepower({
|
|||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
decimals={2}
|
decimals={RELOAD_PRECISION}
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
value="intraClip"
|
value="intraClip"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+21
@@ -1,6 +1,7 @@
|
|||||||
import type { QuicklimeEvent } from "quicklime";
|
import type { QuicklimeEvent } from "quicklime";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import type { Group } from "three";
|
import type { Group } from "three";
|
||||||
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import { api } from "../../../../../../../../core/blitzkit/api";
|
import { api } from "../../../../../../../../core/blitzkit/api";
|
||||||
import {
|
import {
|
||||||
modelTransformEvent,
|
modelTransformEvent,
|
||||||
@@ -21,6 +22,13 @@ export function CanvasContent() {
|
|||||||
const tankModel = modelDefinitions.models[tank.id];
|
const tankModel = modelDefinitions.models[tank.id];
|
||||||
const turretModel = tankModel.turrets[turret.id];
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
|
||||||
|
// baked-in turret mount tilt (e.g. Minotauro sits pitched 3deg forward);
|
||||||
|
// applied as a static base rotation so the live gun pitch composes on top
|
||||||
|
// of it, mirroring useTankTransform.ts's turretContainer/gunContainer nesting
|
||||||
|
const initialTurretPitch = -degToRad(
|
||||||
|
tankModel.initial_turret_rotation?.pitch ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleModelTransformEvent(
|
function handleModelTransformEvent(
|
||||||
event: QuicklimeEvent<ModelTransformEventData>,
|
event: QuicklimeEvent<ModelTransformEventData>,
|
||||||
@@ -45,11 +53,24 @@ export function CanvasContent() {
|
|||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
<group
|
||||||
|
position={[0, turretModel.gun_origin!.y, turretModel.gun_origin!.z]}
|
||||||
|
rotation={[initialTurretPitch, 0, 0]}
|
||||||
|
>
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
0,
|
||||||
|
-turretModel.gun_origin!.y,
|
||||||
|
-turretModel.gun_origin!.z,
|
||||||
|
]}
|
||||||
|
>
|
||||||
<ModelChunk only="turret" />
|
<ModelChunk only="turret" />
|
||||||
|
|
||||||
<group ref={gunWrapper}>
|
<group ref={gunWrapper}>
|
||||||
<ModelChunk only="gun" />
|
<ModelChunk only="gun" />
|
||||||
</group>
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+30
-14
@@ -13,6 +13,7 @@ import { modelTransformEvent } from "../../../../../../../core/blitzkit/modelTra
|
|||||||
import { Var } from "../../../../../../../core/radix/var";
|
import { Var } from "../../../../../../../core/radix/var";
|
||||||
import { useLocale } from "../../../../../../../hooks/useLocale";
|
import { useLocale } from "../../../../../../../hooks/useLocale";
|
||||||
import { Duel } from "../../../../../../../stores/duel";
|
import { Duel } from "../../../../../../../stores/duel";
|
||||||
|
import { Tankopedia } from "../../../../../../../stores/tankopedia";
|
||||||
import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps";
|
import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps";
|
||||||
import { VisualizerCard } from "../VisualizerCard";
|
import { VisualizerCard } from "../VisualizerCard";
|
||||||
import { VisualizerCornerStat } from "../VisualizerCornerStat";
|
import { VisualizerCornerStat } from "../VisualizerCornerStat";
|
||||||
@@ -43,18 +44,33 @@ export function GunFlexibilityVisualizer({
|
|||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||||
|
|
||||||
const tankModel = modelDefinition.models[tank.id];
|
const tankModel = modelDefinition.models[tank.id];
|
||||||
const turretModel = tankModel.turrets[turret.id];
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
const gunModel = turretModel.guns[gun.id];
|
const gunModel = turretModel.guns[gun.id];
|
||||||
|
|
||||||
|
// turret's own baked-in mount tilt (e.g. Minotauro's turret sits pitched
|
||||||
|
// 3deg forward on the hull); must be folded into displayed pitch values
|
||||||
|
// the same way tankCharacteristics.ts and QuickInputs do
|
||||||
|
const initialTurretPitch = degToRad(
|
||||||
|
tankModel.initial_turret_rotation?.pitch ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// radial map is in the tilt-compensated frame so the neutrality circle sits
|
||||||
|
// at the displayed 0deg
|
||||||
|
const tiltDeg = radToDeg(initialTurretPitch);
|
||||||
|
const magTilted = (deg: number) => mag(deg + tiltDeg);
|
||||||
|
|
||||||
const [yaw, setYaw] = useState(0);
|
const [yaw, setYaw] = useState(0);
|
||||||
const [pitch, setPitch] = useState(0);
|
const [pitch, setPitch] = useState(0);
|
||||||
const [minPitch, setMinPitch] = useState(
|
const [minPitch, setMinPitch] = useState(
|
||||||
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max),
|
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max) -
|
||||||
|
initialTurretPitch,
|
||||||
);
|
);
|
||||||
const [maxPitch, setMaxPitch] = useState(
|
const [maxPitch, setMaxPitch] = useState(
|
||||||
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min),
|
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min) -
|
||||||
|
initialTurretPitch,
|
||||||
);
|
);
|
||||||
|
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
@@ -70,7 +86,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
let d = "";
|
let d = "";
|
||||||
|
|
||||||
// right
|
// right
|
||||||
const mo = mag(p.max);
|
const mo = magTilted(p.max);
|
||||||
if (y) {
|
if (y) {
|
||||||
d += `M 0 0`;
|
d += `M 0 0`;
|
||||||
d += `L ${c(90 - y.max, mo)}`;
|
d += `L ${c(90 - y.max, mo)}`;
|
||||||
@@ -87,7 +103,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
|
|
||||||
// front
|
// front
|
||||||
if (f) {
|
if (f) {
|
||||||
const m = mag(f.max);
|
const m = magTilted(f.max);
|
||||||
d += `L ${c(90 - f.range / 2, m)}`;
|
d += `L ${c(90 - f.range / 2, m)}`;
|
||||||
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||||
}
|
}
|
||||||
@@ -108,7 +124,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
|
|
||||||
// back
|
// back
|
||||||
if (!y && b) {
|
if (!y && b) {
|
||||||
const m = mag(b.max);
|
const m = magTilted(b.max);
|
||||||
d += `L ${c(-90 - b.range / 2, m)}`;
|
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||||
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||||
}
|
}
|
||||||
@@ -116,7 +132,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
d += `Z`;
|
d += `Z`;
|
||||||
|
|
||||||
// right
|
// right
|
||||||
const mi = mag(p.min);
|
const mi = magTilted(p.min);
|
||||||
if (y) {
|
if (y) {
|
||||||
d += `M 0 0`;
|
d += `M 0 0`;
|
||||||
d += `L ${c(90 - y.max, mi)}`;
|
d += `L ${c(90 - y.max, mi)}`;
|
||||||
@@ -133,7 +149,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
|
|
||||||
// front
|
// front
|
||||||
if (f) {
|
if (f) {
|
||||||
const m = mag(f.min);
|
const m = magTilted(f.min);
|
||||||
d += `L ${c(90 - f.range / 2, m)}`;
|
d += `L ${c(90 - f.range / 2, m)}`;
|
||||||
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||||
}
|
}
|
||||||
@@ -154,14 +170,14 @@ export function GunFlexibilityVisualizer({
|
|||||||
|
|
||||||
// back
|
// back
|
||||||
if (!y && b) {
|
if (!y && b) {
|
||||||
const m = mag(b.min);
|
const m = magTilted(b.min);
|
||||||
d += `L ${c(-90 - b.range / 2, m)}`;
|
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||||
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
d += `Z`;
|
d += `Z`;
|
||||||
|
|
||||||
const r = mag(radToDeg(-pitch));
|
const r = mag(radToDeg(-(pitch - initialTurretPitch)));
|
||||||
const dotX = r * Math.sin(yaw);
|
const dotX = r * Math.sin(yaw);
|
||||||
const dotY = r * Math.cos(yaw);
|
const dotY = r * Math.cos(yaw);
|
||||||
|
|
||||||
@@ -179,7 +195,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
top="0"
|
top="0"
|
||||||
left="0"
|
left="0"
|
||||||
>
|
>
|
||||||
{!skeleton && <FlexibilityCanvas />}
|
{!skeleton && modelRequested && <FlexibilityCanvas />}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
@@ -251,13 +267,13 @@ export function GunFlexibilityVisualizer({
|
|||||||
yaw = min[1];
|
yaw = min[1];
|
||||||
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
|
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
|
||||||
t = clamp(t, 0, 1);
|
t = clamp(t, 0, 1);
|
||||||
let pitch = degToRad(magInverse(-t));
|
let pitch = degToRad(magInverse(-t)) + initialTurretPitch;
|
||||||
pitch = clamp(pitch, min[0], max[0]);
|
pitch = clamp(pitch, min[0], max[0]);
|
||||||
|
|
||||||
setPitch(pitch);
|
setPitch(pitch);
|
||||||
setYaw(yaw);
|
setYaw(yaw);
|
||||||
setMinPitch(min[0]);
|
setMinPitch(min[0] - initialTurretPitch);
|
||||||
setMaxPitch(max[0]);
|
setMaxPitch(max[0] - initialTurretPitch);
|
||||||
|
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}}
|
}}
|
||||||
@@ -376,7 +392,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
<VisualizerCornerStat
|
<VisualizerCornerStat
|
||||||
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
|
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
|
||||||
value={literals(strings.common.units.deg, {
|
value={literals(strings.common.units.deg, {
|
||||||
value: radToDeg(pitch).toFixed(0),
|
value: radToDeg(pitch - initialTurretPitch).toFixed(0),
|
||||||
})}
|
})}
|
||||||
side="top-left"
|
side="top-left"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+7
-7
@@ -11,7 +11,7 @@ import { Shell } from './components/Shell';
|
|||||||
import { Target } from './components/Target';
|
import { Target } from './components/Target';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const PRECISION = 2;
|
export const RELOAD_PRECISION = 1;
|
||||||
|
|
||||||
export interface ReloadUpdateData {
|
export interface ReloadUpdateData {
|
||||||
reload: number;
|
reload: number;
|
||||||
@@ -82,9 +82,9 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTime.current.innerHTML = shellReload.toFixed(PRECISION);
|
totalTime.current.innerHTML = shellReload.toFixed(RELOAD_PRECISION);
|
||||||
progressTime.current.innerHTML = ((1 - reload) * shellReload).toFixed(
|
progressTime.current.innerHTML = ((1 - reload) * shellReload).toFixed(
|
||||||
PRECISION,
|
RELOAD_PRECISION,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (state.current >= reloadThreshold) shoot();
|
if (state.current >= reloadThreshold) shoot();
|
||||||
@@ -117,9 +117,9 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
shells = times(stats.shells, (index) => index > state.current - 1);
|
shells = times(stats.shells, (index) => index > state.current - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTime.current.innerHTML = interval.toFixed(PRECISION);
|
totalTime.current.innerHTML = interval.toFixed(RELOAD_PRECISION);
|
||||||
progressTime.current.innerHTML = ((1 - reload) * interval).toFixed(
|
progressTime.current.innerHTML = ((1 - reload) * interval).toFixed(
|
||||||
PRECISION,
|
RELOAD_PRECISION,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (lastState % 1 > state.current % 1) shoot();
|
if (lastState % 1 > state.current % 1) shoot();
|
||||||
@@ -262,7 +262,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
transform: 'translate(50%, -50%)',
|
transform: 'translate(50%, -50%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
0.00
|
{(0).toFixed(RELOAD_PRECISION)}
|
||||||
</Code>
|
</Code>
|
||||||
<Code
|
<Code
|
||||||
ref={totalTime}
|
ref={totalTime}
|
||||||
@@ -277,7 +277,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
0.00
|
{(0).toFixed(RELOAD_PRECISION)}
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
{stats.shells > 1 &&
|
{stats.shells > 1 &&
|
||||||
|
|||||||
+7
-13
@@ -28,23 +28,17 @@ export function Equalizer() {
|
|||||||
width="100%"
|
width="100%"
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
borderRadius: "var(--radius-3)",
|
borderRadius: "var(--radius-3) 0 0 var(--radius-3)",
|
||||||
backgroundImage: `url(${alias("api", `/gamemodes/Insanity/banner.webp`)})`,
|
backgroundImage: `
|
||||||
backgroundSize: "cover",
|
linear-gradient(90deg, var(--black-a3), var(--gray-1)),
|
||||||
|
url(${alias("api", `/gamemodes/Insanity/banner.webp`)})
|
||||||
|
`,
|
||||||
|
backgroundSize: "auto, fit",
|
||||||
backgroundPosition: "50% 25%",
|
backgroundPosition: "50% 25%",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex align="center" width="100%" justify="between" p="4" style={{}}>
|
||||||
align="center"
|
|
||||||
width="100%"
|
|
||||||
justify="between"
|
|
||||||
p="4"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
"linear-gradient(90deg, var(--black-a3), var(--gray-1))",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
textShadow: "0 0 var(--space-1) var(--black-a12)",
|
textShadow: "0 0 var(--space-1) var(--black-a12)",
|
||||||
|
|||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
import { alias, availableProvisions } from "@blitzkit/core";
|
||||||
|
import { ShuffleIcon } from "@radix-ui/react-icons";
|
||||||
|
import { Flex, IconButton, Popover } from "@radix-ui/themes";
|
||||||
|
import { api } from "../../../../../../core/blitzkit/api";
|
||||||
|
import { SPALL_LINER_PROVISION_ID } from "../../../../../../core/blitzkit/spallLiner";
|
||||||
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
|
|
||||||
|
const provisionDefinitions = await api.provisionDefinitions();
|
||||||
|
|
||||||
|
export function SpallLinerSwitcher() {
|
||||||
|
const { tank, gun, provisions } = Duel.use((state) => state.protagonist);
|
||||||
|
const { unwrap } = useLocale();
|
||||||
|
const isSpallLinerActive = provisions.includes(SPALL_LINER_PROVISION_ID);
|
||||||
|
const hasSlotAvailable = provisions.length < tank.max_provisions;
|
||||||
|
|
||||||
|
const canEquipSpallLiner = availableProvisions(
|
||||||
|
tank,
|
||||||
|
gun,
|
||||||
|
provisionDefinitions,
|
||||||
|
).some((provision) => provision.id === SPALL_LINER_PROVISION_ID);
|
||||||
|
|
||||||
|
if (!canEquipSpallLiner) return null;
|
||||||
|
|
||||||
|
function equip(replacing?: number) {
|
||||||
|
Duel.mutate((draft) => {
|
||||||
|
if (replacing !== undefined) {
|
||||||
|
const provisions = draft.protagonist.provisions;
|
||||||
|
provisions[provisions.indexOf(replacing)] = SPALL_LINER_PROVISION_ID;
|
||||||
|
} else {
|
||||||
|
draft.protagonist.provisions.push(SPALL_LINER_PROVISION_ID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unequip() {
|
||||||
|
Duel.mutate((draft) => {
|
||||||
|
draft.protagonist.provisions = draft.protagonist.provisions.filter(
|
||||||
|
(id) => id !== SPALL_LINER_PROVISION_ID,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<IconButton
|
||||||
|
color={isSpallLinerActive ? undefined : "gray"}
|
||||||
|
variant="soft"
|
||||||
|
size={{ initial: "2", sm: "3" }}
|
||||||
|
radius="medium"
|
||||||
|
onClick={() => {
|
||||||
|
if (isSpallLinerActive) {
|
||||||
|
unequip();
|
||||||
|
} else if (hasSlotAvailable) {
|
||||||
|
equip();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={unwrap(
|
||||||
|
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
|
||||||
|
)}
|
||||||
|
src={alias("api", `/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`)}
|
||||||
|
style={{
|
||||||
|
width: "50%",
|
||||||
|
height: "50%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSpallLinerActive || hasSlotAvailable) return button;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>{button}</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Content>
|
||||||
|
<Flex gap="2" align="center">
|
||||||
|
<img
|
||||||
|
alt={unwrap(
|
||||||
|
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
|
||||||
|
)}
|
||||||
|
src={alias(
|
||||||
|
"api",
|
||||||
|
`/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: "1.25em",
|
||||||
|
height: "1.25em",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShuffleIcon color="var(--gray-9)" />
|
||||||
|
|
||||||
|
<Flex gap="2">
|
||||||
|
{provisions.map((id) => (
|
||||||
|
<Popover.Close key={id}>
|
||||||
|
<IconButton
|
||||||
|
color="gray"
|
||||||
|
variant="soft"
|
||||||
|
size="3"
|
||||||
|
radius="medium"
|
||||||
|
onClick={() => equip(id)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={unwrap(provisionDefinitions.provisions[id].name!)}
|
||||||
|
src={alias("api", `/icons/provisions/${id}.webp`)}
|
||||||
|
style={{
|
||||||
|
width: "50%",
|
||||||
|
height: "50%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Popover.Close>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ import { TankSearch } from "../../../../TankSearch";
|
|||||||
import { CustomShellButton } from "./components/CustomShellButton";
|
import { CustomShellButton } from "./components/CustomShellButton";
|
||||||
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
||||||
import { QuickInputs } from "./components/QuickInputs";
|
import { QuickInputs } from "./components/QuickInputs";
|
||||||
|
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
|
||||||
import { Thicknesses } from "./components/Thicknesses";
|
import { Thicknesses } from "./components/Thicknesses";
|
||||||
|
|
||||||
type OptionsProps = MaybeSkeletonComponentProps & {
|
type OptionsProps = MaybeSkeletonComponentProps & {
|
||||||
@@ -76,6 +77,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||||
|
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||||
const equalize = Duel.use((state) => state.equalize);
|
const equalize = Duel.use((state) => state.equalize);
|
||||||
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
|
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
|
||||||
|
|
||||||
@@ -278,9 +280,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
|
<SpallLinerSwitcher />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{!skeleton && (
|
{!skeleton && modelRequested && (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<DynamicArmorSwitcher />
|
<DynamicArmorSwitcher />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ export function Tracker({ fontSize }: Props) {
|
|||||||
top="50%"
|
top="50%"
|
||||||
width="min(100%, 16rem)"
|
width="min(100%, 16rem)"
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(-50%, calc(${fontSize} / 2 + var(--space-4)))`,
|
transform: `translate(-50%, calc(${fontSize} * 0.75 + var(--space-4)))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Progress color="gray" value={progress} />
|
<Progress color="gray" value={progress} />
|
||||||
|
|||||||
+38
-4
@@ -1,11 +1,14 @@
|
|||||||
import { Flex, Heading } from "@radix-ui/themes";
|
import { UpdateIcon } from "@radix-ui/react-icons";
|
||||||
|
import { Button, Flex, Heading } from "@radix-ui/themes";
|
||||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
|
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
|
||||||
import { Tracker } from "./components/Tracker";
|
import { Tracker } from "./components/Tracker";
|
||||||
|
|
||||||
export function Title() {
|
export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const { unwrap } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
|
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
@@ -21,6 +24,8 @@ export function Title() {
|
|||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!modelRequested) return;
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.revealed = true;
|
draft.revealed = true;
|
||||||
});
|
});
|
||||||
@@ -60,7 +65,36 @@ export function Title() {
|
|||||||
{name}
|
{name}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
{!revealed && <Tracker fontSize={fontSize} />}
|
{!skeleton && !revealed && modelRequested && (
|
||||||
|
<Tracker fontSize={fontSize} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!skeleton && !modelRequested && (
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
left="50%"
|
||||||
|
top="50%"
|
||||||
|
width="min(100%, 16rem)"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
transform: `translate(-50%, calc(${fontSize} * 0.75 + var(--space-4) - 50%))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
highContrast
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
onClick={() => {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.modelRequested = true;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UpdateIcon />
|
||||||
|
{strings.website.tools.tankopedia.sandbox.load_model}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+1
@@ -82,6 +82,7 @@ export function Lighting({ hasPbr }: Props) {
|
|||||||
blur={2 ** 1}
|
blur={2 ** 1}
|
||||||
resolution={2 ** 9}
|
resolution={2 ** 9}
|
||||||
depthWrite={false}
|
depthWrite={false}
|
||||||
|
renderOrder={1}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{animate && (
|
{animate && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, Flex } from "@radix-ui/themes";
|
import { Box, Flex } from "@radix-ui/themes";
|
||||||
import { times } from "lodash-es";
|
import { times } from "lodash-es";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { Suspense, useEffect, useMemo, useRef } from "react";
|
||||||
import { api } from "../../../core/blitzkit/api";
|
import { api } from "../../../core/blitzkit/api";
|
||||||
import { defaultEqualizer } from "../../../core/blitzkit/tankToDuelMember";
|
import { defaultEqualizer } from "../../../core/blitzkit/tankToDuelMember";
|
||||||
import { useFullScreen } from "../../../hooks/useFullScreen";
|
import { useFullScreen } from "../../../hooks/useFullScreen";
|
||||||
@@ -20,6 +20,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
const canvas = useRef<HTMLCanvasElement>(null!);
|
const canvas = useRef<HTMLCanvasElement>(null!);
|
||||||
const isFullScreen = useFullScreen();
|
const isFullScreen = useFullScreen();
|
||||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||||
const thicknessRange = useMemo(() => {
|
const thicknessRange = useMemo(() => {
|
||||||
const entries = Object.values(tankDefinitions.tanks);
|
const entries = Object.values(tankDefinitions.tanks);
|
||||||
const filtered = entries.filter(
|
const filtered = entries.filter(
|
||||||
@@ -131,13 +132,15 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
top="0"
|
top="0"
|
||||||
left="0"
|
left="0"
|
||||||
>
|
>
|
||||||
{!skeleton && (
|
{!skeleton && modelRequested && (
|
||||||
|
<Suspense>
|
||||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||||
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Title />
|
<Title skeleton={skeleton} />
|
||||||
|
|
||||||
<Options
|
<Options
|
||||||
skeleton={skeleton}
|
skeleton={skeleton}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{averages && nextTanks && tank.tier !== 10 && (
|
{averages && nextTanks && (
|
||||||
<Text color="gray" size="1" mt="2">
|
<Text color="gray" size="1" mt="2">
|
||||||
{skeleton && <Skeleton height="1em" width="4em" />}
|
{skeleton && <Skeleton height="1em" width="4em" />}
|
||||||
{!skeleton &&
|
{!skeleton &&
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{master.tier < 10 && (
|
{master.successors.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Arrow />
|
<Arrow />
|
||||||
{master.successors!.map((id, index) => (
|
{master.successors!.map((id, index) => (
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { times } from "lodash-es";
|
||||||
|
import { api } from "../blitzkit/api";
|
||||||
|
|
||||||
|
export const awaitableTiers = api.tankDefinitions().then((tankDefinitions) => {
|
||||||
|
let max = 0;
|
||||||
|
|
||||||
|
for (const tank of Object.values(tankDefinitions.tanks)) {
|
||||||
|
if (tank.tier > max) max = tank.tier;
|
||||||
|
}
|
||||||
|
|
||||||
|
return times(max, (index) => max - index);
|
||||||
|
});
|
||||||
@@ -16,13 +16,15 @@ export async function filterTank(
|
|||||||
owned: number[] = [],
|
owned: number[] = [],
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
((filters.showOwned && owned.includes(tank.id)) ||
|
((!filters.showOwned && !filters.showUnowned) ||
|
||||||
|
(filters.showOwned && owned.includes(tank.id)) ||
|
||||||
(filters.showUnowned && !owned.includes(tank.id))) &&
|
(filters.showUnowned && !owned.includes(tank.id))) &&
|
||||||
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
|
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
|
||||||
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
|
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
|
||||||
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
|
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
|
||||||
(filters.types.length === 0 || filters.types.includes(tank.type)) &&
|
(filters.types.length === 0 || filters.types.includes(tank.type)) &&
|
||||||
((filters.showTesting && tank.testing) ||
|
((!filters.showTesting && !filters.showNonTesting) ||
|
||||||
|
(filters.showTesting && tank.testing) ||
|
||||||
(filters.showNonTesting && !tank.testing)) &&
|
(filters.showNonTesting && !tank.testing)) &&
|
||||||
(filters.gunType.length === 0 ||
|
(filters.gunType.length === 0 ||
|
||||||
(filters.gunType.includes("regular") &&
|
(filters.gunType.includes("regular") &&
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export const SPALL_LINER_PROVISION_ID = 101;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the in-game reduction Spall Liner grants against HE splash
|
||||||
|
* damage. Shared between the characteristics panel and the Dynamic
|
||||||
|
* Armor 3D viewer's damage readout so the figure isn't duplicated.
|
||||||
|
*/
|
||||||
|
export const SPALL_LINER_HE_DAMAGE_DELTA = -0.2;
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient";
|
import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient";
|
||||||
import type { EquipmentMatrix } from "../../stores/duel";
|
import type { EquipmentMatrix } from "../../stores/duel";
|
||||||
|
import { SPALL_LINER_HE_DAMAGE_DELTA } from "./spallLiner";
|
||||||
import { defaultEqualizer } from "./tankToDuelMember";
|
import { defaultEqualizer } from "./tankToDuelMember";
|
||||||
|
|
||||||
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
|
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
|
||||||
@@ -170,7 +171,10 @@ export function tankCharacteristics(
|
|||||||
coefficient(
|
coefficient(
|
||||||
[applyReactiveArmor && shell.type !== ShellType.SHELL_TYPE_HE, -0.27],
|
[applyReactiveArmor && shell.type !== ShellType.SHELL_TYPE_HE, -0.27],
|
||||||
[applyDynamicArmor, -0.1],
|
[applyDynamicArmor, -0.1],
|
||||||
[applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE, -0.2],
|
[
|
||||||
|
applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE,
|
||||||
|
SPALL_LINER_HE_DAMAGE_DELTA,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
const assaultDamageCoefficient =
|
const assaultDamageCoefficient =
|
||||||
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
|
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { useRef } from "react";
|
|||||||
|
|
||||||
export function useStableProgress() {
|
export function useStableProgress() {
|
||||||
const { loaded, total } = useProgress();
|
const { loaded, total } = useProgress();
|
||||||
|
const baseline = useRef(loaded);
|
||||||
const progress = useRef(0);
|
const progress = useRef(0);
|
||||||
|
const pending = total - baseline.current;
|
||||||
|
|
||||||
progress.current = Math.max(
|
progress.current = Math.max(
|
||||||
progress.current,
|
progress.current,
|
||||||
total === 0 ? 0 : (loaded / total) * 100
|
pending <= 0 ? 0 : ((loaded - baseline.current) / pending) * 100
|
||||||
);
|
);
|
||||||
|
|
||||||
return progress.current;
|
return progress.current;
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
locale !== locales.default && (
|
locale !== locales.default && (
|
||||||
<Flex justify="center" mb="4">
|
<Flex justify="center" pt="4" my="4">
|
||||||
<Link
|
<Link
|
||||||
href="https://crowdin.com/project/blitzkrieg"
|
href="https://crowdin.com/project/blitzkrieg"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ export function Page({
|
|||||||
}: Props & MaybeSkeletonComponentProps) {
|
}: Props & MaybeSkeletonComponentProps) {
|
||||||
const generated = Playlist.use((state) => state.list !== undefined);
|
const generated = Playlist.use((state) => state.list !== undefined);
|
||||||
|
|
||||||
if (App.state.logins.wargaming) TankFilters.state.showUnowned = false;
|
if (App.state.logins.wargaming) {
|
||||||
|
TankFilters.state.showOwned = true;
|
||||||
|
TankFilters.state.showUnowned = false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export function Page({ locale }: LocaleAcceptorProps) {
|
|||||||
|
|
||||||
function Content({ locale }: LocaleAcceptorProps) {
|
function Content({ locale }: LocaleAcceptorProps) {
|
||||||
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
||||||
|
const autoLoadModels = App.useDeferred(
|
||||||
|
(state) => state.autoLoadModels,
|
||||||
|
true,
|
||||||
|
);
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,6 +40,19 @@ function Content({ locale }: LocaleAcceptorProps) {
|
|||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
<Flex align="center" gap="2" justify="between">
|
||||||
|
<Text>{strings.website.settings.auto_load_models}</Text>
|
||||||
|
<Switch
|
||||||
|
variant="classic"
|
||||||
|
checked={autoLoadModels}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
App.mutate((draft) => {
|
||||||
|
draft.autoLoadModels = checked;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Flex align="center" gap="2" justify="between">
|
<Flex align="center" gap="2" justify="between">
|
||||||
<Text>{strings.website.settings.language}</Text>
|
<Text>{strings.website.settings.language}</Text>
|
||||||
<LocaleSwitcher locale={locale} />
|
<LocaleSwitcher locale={locale} />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface WargamingLogin {
|
|||||||
|
|
||||||
export interface App {
|
export interface App {
|
||||||
developerMode: boolean;
|
developerMode: boolean;
|
||||||
|
autoLoadModels: boolean;
|
||||||
policiesAgreementIndex: number;
|
policiesAgreementIndex: number;
|
||||||
logins: {
|
logins: {
|
||||||
wargaming?: WargamingLogin;
|
wargaming?: WargamingLogin;
|
||||||
@@ -17,6 +18,7 @@ export interface App {
|
|||||||
export const App = new Varuna<App>(
|
export const App = new Varuna<App>(
|
||||||
{
|
{
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
|
autoLoadModels: true,
|
||||||
policiesAgreementIndex: -1,
|
policiesAgreementIndex: -1,
|
||||||
logins: {},
|
logins: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { TankDefinition } from "@blitzkit/core";
|
import type { TankDefinition } from "@blitzkit/core";
|
||||||
import { times } from "lodash-es";
|
|
||||||
import { Varuna } from "varuna";
|
import { Varuna } from "varuna";
|
||||||
|
import { awaitableTiers } from "../core/awaitables/tiers";
|
||||||
|
|
||||||
|
const tiers = await awaitableTiers;
|
||||||
|
|
||||||
export enum GuessState {
|
export enum GuessState {
|
||||||
Correct,
|
Correct,
|
||||||
@@ -25,5 +27,5 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
|
|||||||
correctGuesses: 0,
|
correctGuesses: 0,
|
||||||
streak: 0,
|
streak: 0,
|
||||||
helpingReveal: false,
|
helpingReveal: false,
|
||||||
tiers: times(10, (index) => index + 1),
|
tiers,
|
||||||
}));
|
}));
|
||||||
@@ -46,8 +46,8 @@ export const TankFilters = new Varuna<TankFilters>({
|
|||||||
abilities: [],
|
abilities: [],
|
||||||
powers: [],
|
powers: [],
|
||||||
|
|
||||||
showTesting: true,
|
showTesting: false,
|
||||||
showNonTesting: true,
|
showNonTesting: false,
|
||||||
showOwned: true,
|
showOwned: false,
|
||||||
showUnowned: true,
|
showUnowned: false,
|
||||||
});
|
});
|
||||||
@@ -9,6 +9,7 @@ import type { ArmorType } from "../components/Armor/components/SpacedArmorScene"
|
|||||||
import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent";
|
import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent";
|
||||||
import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection";
|
import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection";
|
||||||
import { api } from "../core/blitzkit/api";
|
import { api } from "../core/blitzkit/api";
|
||||||
|
import { App } from "./app";
|
||||||
import { TankopediaDisplay } from "./tankopediaPersistent/constants";
|
import { TankopediaDisplay } from "./tankopediaPersistent/constants";
|
||||||
|
|
||||||
export interface ShotLayerBase {
|
export interface ShotLayerBase {
|
||||||
@@ -79,6 +80,7 @@ export enum TankopediaRelativeAgainst {
|
|||||||
interface Tankopedia {
|
interface Tankopedia {
|
||||||
disturbed: boolean;
|
disturbed: boolean;
|
||||||
revealed: boolean;
|
revealed: boolean;
|
||||||
|
modelRequested: boolean;
|
||||||
shot?: Shot;
|
shot?: Shot;
|
||||||
skills: Record<string, number>;
|
skills: Record<string, number>;
|
||||||
relativeAgainst: TankopediaRelativeAgainst;
|
relativeAgainst: TankopediaRelativeAgainst;
|
||||||
@@ -110,6 +112,7 @@ const skillDefinitions = await api.skillDefinitions();
|
|||||||
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
||||||
disturbed: false,
|
disturbed: false,
|
||||||
revealed: false,
|
revealed: false,
|
||||||
|
modelRequested: App.state.autoLoadModels,
|
||||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||||
editStatic: false,
|
editStatic: false,
|
||||||
skills: createDefaultSkills(skillDefinitions),
|
skills: createDefaultSkills(skillDefinitions),
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Point this to your local installation of the game (make sure you see the Data
|
||||||
|
# folder within the directory you're putting here); don't forget to use
|
||||||
|
# double quotation marks if your path has spaces
|
||||||
|
CLIENT_DIR=
|
||||||
|
|
||||||
|
# Should be set to * for local development to build all chunks at once
|
||||||
|
CHUNK=*
|
||||||
|
|
||||||
|
# Should always be development when using CHUNKS=*
|
||||||
|
PUBLIC_ENVIRONMENT=development
|
||||||
|
|
||||||
|
# GuidesBlitz is the official textual partner of BlitzKit, but, really any
|
||||||
|
# website that supports BlitzKit tank slugs like example.com/tank-slug works
|
||||||
|
# too
|
||||||
|
PUBLIC_GUIDE_WEBSITE=https://guidesblitz.com
|
||||||
|
|
||||||
|
# The tag manager doesn't do anything in localhost, but here's what BlitzKit
|
||||||
|
# uses for completeness sake
|
||||||
|
PUBLIC_GOOGLE_TAG_MANAGER_ID=G-GL2JVHCGPQ
|
||||||
|
|
||||||
|
# A very rare, but required variable, enabling the advertising of BlitzKit
|
||||||
|
# OpenTest; TODO: make this a string field to allow for cross-advertising for
|
||||||
|
# any branch of BlitzKit
|
||||||
|
PUBLIC_PROMOTE_OPENTEST=false
|
||||||
|
|
||||||
|
# Legacy variable; this will be removed soon
|
||||||
|
PUBLIC_STATIC_ASSET_BASE=https://assets.blitzkit.app
|
||||||
|
|
||||||
|
# Proprietary Wargaming fields; you shouldn't need these for 99% of the tasks
|
||||||
|
WOTB_GLOSSARY=
|
||||||
|
WOTB_GLOSSARY_DOMAIN=
|
||||||
|
WOTB_GLOSSARY_PATH=
|
||||||
|
|
||||||
|
# Generate your own application id: https://developers.wargaming.net
|
||||||
|
PUBLIC_WARGAMING_APPLICATION_ID=
|
||||||
Reference in new issue
Block a user