Merge branch 'main' into playlist-filters-layout

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-10-09 16:53:35 -05:00
commit 37fae7477d
65 files changed
+661 -154

No files matched your search

+25 -16
View File
@@ -25,8 +25,8 @@ import {
VehicleDefinitionList,
} from "@blitzkit/core";
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 { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n/src/strings";
import { parse as parseYaml } from "yaml";
import { BlitzKitAPI } from "../base";
@@ -113,6 +113,9 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
}
async init() {
const { existsSync } = await import("fs");
const { readFile, writeFile, mkdir } = await import("fs/promises");
console.log("Initializing virtual file system...");
await this.vfs.init();
@@ -155,31 +158,37 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
console.log("Fetching game localizations...");
if (import.meta.env.DEV) {
locales.supported = [locales.supported[0]];
}
let fetchedLocalizations = 0;
await Promise.all(
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 cache = await fetch(
const networkStrings = (await fetch(
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`,
)
.then((response) => response.text())
.then((string) => parseYaml(string) as BlitzStrings);
const preInstalled = await this.vfs.yaml<BlitzStrings>(
.then(parseYaml)) as BlitzStrings;
const localStrings = await this.vfs.yaml<BlitzStrings>(
`Data/Strings/${blitzLocale}.yaml`,
);
const combined = { ...networkStrings, ...localStrings };
this.stringsI18n[locale] = {
...cache,
...preInstalled,
};
await mkdir(root, { recursive: true });
await writeFile(cachePath, JSON.stringify(combined));
console.log(
`Fetched localizations for ${locale} (${++fetchedLocalizations}/${locales.supported.length})`,
);
this.stringsI18n[locale] = combined;
}),
);
@@ -350,6 +350,15 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
const tankClass =
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] = {
ancestors: [],
successors: [],
@@ -367,10 +376,8 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
typeof equipment === "string" ? equipment : equipment.at(-1)!,
max_consumables: tankDefinition.root.consumableSlots,
max_provisions: tankDefinition.root.provisionSlots,
name:
(tank.shortUserString
? this.getString(tank.shortUserString)
: undefined) ?? this.getString(tank.userString),
name,
name_full,
slug,
nation,
type: tankTags.includes("collectible")
@@ -12,6 +12,7 @@ export function metaSortTank(
gameDefinitions: GameDefinitions,
) {
return tanks
.sort((a, b) => a.id - b.id)
.sort((a, b) => b.tier - a.tier)
.sort(
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type),
@@ -54,6 +54,7 @@ export const TIER_ROMAN_NUMERALS: Record<number, string> = {
8: "VIII",
9: "IX",
10: "X",
11: "XI",
};
export const flags: Record<string, string> = {
+13
View File
@@ -18,11 +18,22 @@ export async function fetchTankNames(api: BlitzKitAPI) {
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 {
id: tank.id,
name: tank.name,
searchableName: tank.name,
searchableNameDeburr,
searchableNameFull,
searchableNameFullDeburr,
camouflages: tank.camouflages
?.map((id) =>
locales.supported.map(
@@ -45,6 +56,8 @@ export const SEARCH_KEYS = [
.map(({ locale }) => [
`searchableName.locales.${locale}`,
`searchableNameDeburr.locales.${locale}`,
`searchableNameFull.locales.${locale}`,
`searchableNameFullDeburr.locales.${locale}`,
])
.flat(),
"camouflages",
@@ -41,6 +41,7 @@ message TankDefinition {
required uint32 weight = 31;
required string dev_name = 32;
optional Equalizer equalizer = 33;
optional I18nString name_full = 34;
}
message Equalizer {
@@ -292,6 +292,7 @@ export interface TankDefinition {
weight: number;
dev_name: string;
equalizer?: Equalizer | undefined;
name_full?: I18nString | undefined;
}
export interface TankDefinition_RolesEntry {
@@ -651,6 +652,7 @@ function createBaseTankDefinition(): TankDefinition {
weight: 0,
dev_name: "",
equalizer: undefined,
name_full: undefined,
};
}
@@ -752,6 +754,9 @@ export const TankDefinition: MessageFns<TankDefinition> = {
if (message.equalizer !== undefined) {
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;
},
@@ -1051,6 +1056,14 @@ export const TankDefinition: MessageFns<TankDefinition> = {
message.equalizer = Equalizer.decode(reader, reader.uint32());
continue;
}
case 34: {
if (tag !== 274) {
break;
}
message.name_full = I18nString.decode(reader, reader.uint32());
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@@ -1111,6 +1124,7 @@ export const TankDefinition: MessageFns<TankDefinition> = {
weight: isSet(object.weight) ? globalThis.Number(object.weight) : 0,
dev_name: isSet(object.dev_name) ? globalThis.String(object.dev_name) : "",
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) {
obj.equalizer = Equalizer.toJSON(message.equalizer);
}
if (message.name_full !== undefined) {
obj.name_full = I18nString.toJSON(message.name_full);
}
return obj;
},
@@ -1271,6 +1288,9 @@ export const TankDefinition: MessageFns<TankDefinition> = {
message.equalizer = (object.equalizer !== undefined && object.equalizer !== null)
? Equalizer.fromPartial(object.equalizer)
: undefined;
message.name_full = (object.name_full !== undefined && object.name_full !== null)
? I18nString.fromPartial(object.name_full)
: undefined;
return message;
},
};
+3
View File
@@ -215,6 +215,7 @@
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Development mode",
"auto_load_models": "Auto-load models",
"language": "Language"
},
@@ -458,6 +459,8 @@
},
"sandbox": {
"load_model": "Load model",
"poses": {
"hull_down": "Hull down",
"face_hug": "Face hug",
+2
View File
@@ -194,6 +194,7 @@
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Modo de desarrollador",
"auto_load_models": "Auto-load models",
"language": "Idioma"
},
"common": {
@@ -412,6 +413,7 @@
}
},
"sandbox": {
"load_model": "Load model",
"poses": {
"hull_down": "Hull down",
"face_hug": "Cara a cara",
+2
View File
@@ -194,6 +194,7 @@
"title": "Paramètres",
"description": "Modifier les paramètres de BlitzKit",
"dev_mode": "Mode développement",
"auto_load_models": "Auto-load models",
"language": "Langue"
},
"common": {
@@ -412,6 +413,7 @@
}
},
"sandbox": {
"load_model": "Load model",
"poses": {
"hull_down": "Hull down",
"face_hug": "Face hug",
+2
View File
@@ -194,6 +194,7 @@
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "開発者モード",
"auto_load_models": "Auto-load models",
"language": "言語 / Language"
},
"common": {
@@ -412,6 +413,7 @@
}
},
"sandbox": {
"load_model": "Load model",
"poses": {
"hull_down": "ハルダウン",
"face_hug": "密着",
+2
View File
@@ -194,6 +194,7 @@
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Development mode",
"auto_load_models": "Auto-load models",
"language": "Language"
},
"common": {
@@ -412,6 +413,7 @@
}
},
"sandbox": {
"load_model": "Load model",
"poses": {
"hull_down": "Hull down",
"face_hug": "Face hug",
+2
View File
@@ -194,6 +194,7 @@
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Modo de desenvolvimento",
"auto_load_models": "Auto-load models",
"language": "Idioma"
},
"common": {
@@ -412,6 +413,7 @@
}
},
"sandbox": {
"load_model": "Load model",
"poses": {
"hull_down": "Casco para baixo",
"face_hug": "Abraço facial",
+2
View File
@@ -194,6 +194,7 @@
"title": "Настройки",
"description": "Изменить настройки BlitzKit",
"dev_mode": "Режим разработчика",
"auto_load_models": "Auto-load models",
"language": "Язык"
},
"common": {
@@ -412,6 +413,7 @@
}
},
"sandbox": {
"load_model": "Load model",
"poses": {
"hull_down": "За холмом",
"face_hug": "В упор",
+2
View File
@@ -194,6 +194,7 @@
"title": "Налаштування",
"description": "Змінити налаштування BlitzKit ",
"dev_mode": "Режим розробника",
"auto_load_models": "Auto-load models",
"language": "Мова"
},
"common": {
@@ -412,6 +413,7 @@
}
},
"sandbox": {
"load_model": "Load model",
"poses": {
"hull_down": "Від башти",
"face_hug": "Клінч",
+2
View File
@@ -194,6 +194,7 @@
"title": "设置",
"description": "更改 BlitzKit 设置",
"dev_mode": "开发者模式",
"auto_load_models": "Auto-load models",
"language": "语言"
},
"common": {
@@ -412,6 +413,7 @@
}
},
"sandbox": {
"load_model": "Load model",
"poses": {
"hull_down": "俯角视角",
"face_hug": "顶牛视角",
+12 -11
View File
@@ -135,7 +135,7 @@ let regionI = 0;
console.log("Starting discovery loop...");
atMaxRate(async ({ _break: break1 }) => {
await atMaxRate(async ({ _break: break1 }) => {
if (regions.length === 0) {
console.log("All regions exhausted, exiting discovery loop...");
return break1();
@@ -166,18 +166,19 @@ atMaxRate(async ({ _break: break1 }) => {
fields
}&account_id=${accountIds}`;
atMaxRate(async ({ _break: break2 }) => {
if (idRange.length < MAX_IDS_PER_CALL) {
console.log(
`Removing region ${region.domain} removed due to ID range exhaustion...`,
);
regions.splice(regionI, 1);
}
if (idRange.length < MAX_IDS_PER_CALL) {
console.log(
`Removing region ${region.domain} removed due to ID range exhaustion...`,
);
regions.splice(regionI, 1);
}
const response = await fetch(url);
const data = (await response.json()) as QuickInfo;
await atMaxRate(async ({ _break: break2 }) => {
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...");
return;
}
+4 -2
View File
@@ -20,7 +20,9 @@ interface MethodContext {
let lastT = 0;
const maxDelta = 1000 / API_RATE;
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;
const context: MethodContext = {
@@ -39,7 +41,7 @@ export async function atMaxRate(method: (context: MethodContext) => void) {
await sleep(maxDelta - delta + DELTA_BUFFER);
}
method(context);
await method(context);
lastT = t;
}
}
+2
View File
@@ -6,7 +6,9 @@ import { promisify } from "util";
const MAX_COMMAND_LENGTH = 2 ** 11;
const TS_PROTO_EXECUTABLE_LOCATIONS = [
"./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.exe",
];
const roots = [
@@ -18,6 +18,10 @@ import {
import { degToRad } from "three/src/math/MathUtils.js";
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
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 { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
import { Duel } from "../../../../stores/duel";
@@ -384,6 +388,13 @@ export function SpacedArmorSceneComponent({
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;
},
[camera],
+5 -4
View File
@@ -12,6 +12,7 @@ export interface Props {
}
const gameDefinitions = await api.gameDefinitions();
const gameVersion = gameDefinitions.version.split("_")[0];
const environment = assertSecret(import.meta.env.PUBLIC_ENVIRONMENT);
const { locale } = Astro.props;
const strings = getStrings(locale);
@@ -141,7 +142,7 @@ const strings = getStrings(locale);
</Text>
)
}
<Text size="2" color="gray">CDN</Text>
<Text size="2" color="gray">Branch</Text>
</Flex>
<Flex direction="column" gap="1" flexGrow="1" flexBasis="0" align="start">
@@ -151,7 +152,7 @@ const strings = getStrings(locale);
{
environment !== "preview" && (
<Code size="2" color="gray">
{gameDefinitions.version}
{gameVersion}
</Code>
)
}
@@ -174,13 +175,13 @@ const strings = getStrings(locale);
WoTB
</Text>
<Code size="2" color="gray">
{gameDefinitions.version}
{gameVersion}
</Code>
</>
)
}
<Text size="2" color="gray">CDN</Text>
<Text size="2" color="gray">Branch</Text>
<Code size="2" color="gray">
{environment}
</Code>
+5 -4
View File
@@ -27,9 +27,10 @@ import {
TextField,
} from "@radix-ui/themes";
import fuzzysort from "fuzzysort";
import { debounce, times } from "lodash-es";
import { debounce } from "lodash-es";
import { useCallback, useEffect, useRef, useState } from "react";
import { awaitableTankNames } from "../core/awaitables/tankNames";
import { awaitableTiers } from "../core/awaitables/tiers";
import { api } from "../core/blitzkit/api";
import { useLocale } from "../hooks/useLocale";
import { Guess, GuessState } from "../stores/guess";
@@ -38,9 +39,10 @@ import { SearchResults } from "./SearchResults";
const { go } = fuzzysort;
const [tankNames, tankDefinitions] = await Promise.all([
const [tankNames, tankDefinitions, TIERS] = await Promise.all([
awaitableTankNames,
api.tankDefinitions(),
awaitableTiers,
]);
const ids = Object.keys(tankDefinitions.tanks);
@@ -193,8 +195,7 @@ export function Guesser() {
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{times(10, (index) => {
const tier = 10 - index;
{TIERS.map((tier) => {
const isSelected = tiers.includes(tier);
return (
@@ -1,3 +1,27 @@
<script>
import locales from "@blitzkit/i18n/locales.json";
import { getBestLocale } from "../core/i18n/getBestLocale";
const pathname = window.location.pathname;
const hasPreference = localStorage.getItem("preferred-locale") !== null;
const bestLocale = getBestLocale();
const currentLocale =
locales.supported.find(
({ locale }) =>
pathname === `/${locale}` || pathname.startsWith(`/${locale}/`),
)?.locale ?? locales.default;
if (
bestLocale !== currentLocale &&
(hasPreference || currentLocale === locales.default)
) {
const rawPath =
currentLocale === locales.default
? pathname
: pathname.slice(`/${currentLocale}`.length) || "/";
window.location.pathname = `${
bestLocale === locales.default ? "" : `/${bestLocale}`
}${rawPath}`;
}
</script>
@@ -25,8 +25,8 @@ import {
Tooltip,
type FlexProps,
} from "@radix-ui/themes";
import { times } from "lodash-es";
import { Fragment, type ComponentProps, type ReactNode } from "react";
import { awaitableTiers } from "../../../core/awaitables/tiers";
import { api } from "../../../core/blitzkit/api";
import { useLocale } from "../../../hooks/useLocale";
import { App } from "../../../stores/app";
@@ -171,7 +171,7 @@ const GUN_TYPES = Object.keys(
const MAX_ICONS = 4;
const TIERS = times(10, (i) => 10 - i);
const TIERS = await awaitableTiers;
export function FilterControl() {
return (
@@ -789,6 +789,7 @@ function OwnershipFilterInternal(props: FlexProps) {
onClick={() => {
TankFilters.mutate((draft) => {
draft.showOwned = !draft.showOwned;
if (draft.showOwned) draft.showUnowned = false;
});
}}
>
@@ -808,6 +809,7 @@ function OwnershipFilterInternal(props: FlexProps) {
onClick={() => {
TankFilters.mutate((draft) => {
draft.showUnowned = !draft.showUnowned;
if (draft.showUnowned) draft.showOwned = false;
});
}}
>
@@ -834,6 +836,7 @@ function TestFilter() {
onClick={() => {
TankFilters.mutate((draft) => {
draft.showNonTesting = !draft.showNonTesting;
if (draft.showNonTesting) draft.showTesting = false;
});
}}
>
@@ -854,6 +857,7 @@ function TestFilter() {
onClick={() => {
TankFilters.mutate((draft) => {
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;
});
}, 500),
[]
[],
);
const handleChange = useCallback(() => {
if (!searching) {
@@ -46,7 +46,14 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
}, [searching]);
const handleKeyDown = useCallback<KeyboardEventHandler>(
(event) => {
if (event.key !== "Enter" || !topResult || searching) return;
if (
event.key !== "Enter" ||
!topResult ||
!TankFilters.state.search ||
TankFilters.state.searching
) {
return;
}
event.preventDefault();
@@ -56,7 +63,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
window.location.href = `/tanks/${topResult.slug}`;
}
},
[topResult]
[topResult],
);
return (
@@ -1,10 +1,12 @@
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { Flex, IconButton, Text } from "@radix-ui/themes";
import { times } from "lodash-es";
import { memo } from "react";
import { awaitableTiers } from "../../../core/awaitables/tiers";
import { TankFilters } from "../../../stores/tankFilters";
import { TankSort } from "../../../stores/tankopediaSort";
const TIERS = await awaitableTiers;
export const TierFilter = memo(() => {
const by = TankSort.use((state) => state.by);
const search = TankFilters.use((state) => state.search);
@@ -19,8 +21,7 @@ export const TierFilter = memo(() => {
overflow="hidden"
style={{ borderRadius: "var(--radius-full)" }}
>
{times(10, (index) => {
const tier = 10 - index;
{TIERS.map((tier) => {
const selected = tiers.includes(tier);
return (
@@ -17,7 +17,7 @@ import { AssaultRangesVisualizer } from "./AssaultRangesVisualizer";
import { GunFlexibilityVisualizer } from "./GunFlexibilityVisualizer";
import { Info } from "./Info";
import { InfoWithDelta } from "./InfoWithDelta";
import { ReloadVisualizer } from "./ReloadVisualizer";
import { RELOAD_PRECISION, ReloadVisualizer } from "./ReloadVisualizer";
import { RicochetVisualizer } from "./RicochetVisualizer";
import { StatsTableWrapper } from "./StatsTableWrapper";
import type { StatsAcceptorProps } from "./TraverseVisualizer";
@@ -163,7 +163,7 @@ export function Firepower({
.shell_index,
{ index: index + 1 },
)}
decimals={2}
decimals={RELOAD_PRECISION}
deltaType="lowerIsBetter"
noRanking
value={() => reload}
@@ -173,7 +173,7 @@ export function Firepower({
) : (
<InfoWithDelta
stats={stats}
decimals={2}
decimals={RELOAD_PRECISION}
deltaType="lowerIsBetter"
value="shellReload"
/>
@@ -183,7 +183,7 @@ export function Firepower({
<InfoWithDelta
stats={stats}
indent
decimals={2}
decimals={RELOAD_PRECISION}
deltaType="lowerIsBetter"
value="intraClip"
/>
@@ -1,6 +1,7 @@
import type { QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import type { Group } from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import { api } from "../../../../../../../../core/blitzkit/api";
import {
modelTransformEvent,
@@ -21,6 +22,13 @@ export function CanvasContent() {
const tankModel = modelDefinitions.models[tank.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(() => {
function handleModelTransformEvent(
event: QuicklimeEvent<ModelTransformEventData>,
@@ -45,10 +53,23 @@ export function CanvasContent() {
</group>
</group>
<ModelChunk only="turret" />
<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" />
<group ref={gunWrapper}>
<ModelChunk only="gun" />
<group ref={gunWrapper}>
<ModelChunk only="gun" />
</group>
</group>
</group>
</>
);
@@ -13,6 +13,7 @@ import { modelTransformEvent } from "../../../../../../../core/blitzkit/modelTra
import { Var } from "../../../../../../../core/radix/var";
import { useLocale } from "../../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps";
import { VisualizerCard } from "../VisualizerCard";
import { VisualizerCornerStat } from "../VisualizerCornerStat";
@@ -43,18 +44,33 @@ export function GunFlexibilityVisualizer({
const tank = Duel.use((state) => state.protagonist.tank);
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const tankModel = modelDefinition.models[tank.id];
const turretModel = tankModel.turrets[turret.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 [pitch, setPitch] = useState(0);
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(
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min),
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min) -
initialTurretPitch,
);
const container = useRef<HTMLDivElement>(null);
@@ -70,7 +86,7 @@ export function GunFlexibilityVisualizer({
let d = "";
// right
const mo = mag(p.max);
const mo = magTilted(p.max);
if (y) {
d += `M 0 0`;
d += `L ${c(90 - y.max, mo)}`;
@@ -87,7 +103,7 @@ export function GunFlexibilityVisualizer({
// front
if (f) {
const m = mag(f.max);
const m = magTilted(f.max);
d += `L ${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
if (!y && b) {
const m = mag(b.max);
const m = magTilted(b.max);
d += `L ${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`;
// right
const mi = mag(p.min);
const mi = magTilted(p.min);
if (y) {
d += `M 0 0`;
d += `L ${c(90 - y.max, mi)}`;
@@ -133,7 +149,7 @@ export function GunFlexibilityVisualizer({
// front
if (f) {
const m = mag(f.min);
const m = magTilted(f.min);
d += `L ${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
if (!y && b) {
const m = mag(b.min);
const m = magTilted(b.min);
d += `L ${c(-90 - b.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
}
d += `Z`;
const r = mag(radToDeg(-pitch));
const r = mag(radToDeg(-(pitch - initialTurretPitch)));
const dotX = r * Math.sin(yaw);
const dotY = r * Math.cos(yaw);
@@ -179,7 +195,7 @@ export function GunFlexibilityVisualizer({
top="0"
left="0"
>
{!skeleton && <FlexibilityCanvas />}
{!skeleton && modelRequested && <FlexibilityCanvas />}
<Box
position="absolute"
@@ -251,13 +267,13 @@ export function GunFlexibilityVisualizer({
yaw = min[1];
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
t = clamp(t, 0, 1);
let pitch = degToRad(magInverse(-t));
let pitch = degToRad(magInverse(-t)) + initialTurretPitch;
pitch = clamp(pitch, min[0], max[0]);
setPitch(pitch);
setYaw(yaw);
setMinPitch(min[0]);
setMaxPitch(max[0]);
setMinPitch(min[0] - initialTurretPitch);
setMaxPitch(max[0] - initialTurretPitch);
modelTransformEvent.dispatch({ pitch, yaw });
}}
@@ -376,7 +392,7 @@ export function GunFlexibilityVisualizer({
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
value={literals(strings.common.units.deg, {
value: radToDeg(pitch).toFixed(0),
value: radToDeg(pitch - initialTurretPitch).toFixed(0),
})}
side="top-left"
/>
@@ -11,7 +11,7 @@ import { Shell } from './components/Shell';
import { Target } from './components/Target';
import './index.css';
const PRECISION = 2;
export const RELOAD_PRECISION = 1;
export interface ReloadUpdateData {
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(
PRECISION,
RELOAD_PRECISION,
);
if (state.current >= reloadThreshold) shoot();
@@ -117,9 +117,9 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
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(
PRECISION,
RELOAD_PRECISION,
);
if (lastState % 1 > state.current % 1) shoot();
@@ -262,7 +262,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
transform: 'translate(50%, -50%)',
}}
>
0.00
{(0).toFixed(RELOAD_PRECISION)}
</Code>
<Code
ref={totalTime}
@@ -277,7 +277,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
transform: 'translate(-50%, -50%)',
}}
>
0.00
{(0).toFixed(RELOAD_PRECISION)}
</Code>
{stats.shells > 1 &&
@@ -28,23 +28,17 @@ export function Equalizer() {
width="100%"
style={{
cursor: "pointer",
borderRadius: "var(--radius-3)",
backgroundImage: `url(${alias("api", `/gamemodes/Insanity/banner.webp`)})`,
backgroundSize: "cover",
borderRadius: "var(--radius-3) 0 0 var(--radius-3)",
backgroundImage: `
linear-gradient(90deg, var(--black-a3), var(--gray-1)),
url(${alias("api", `/gamemodes/Insanity/banner.webp`)})
`,
backgroundSize: "auto, fit",
backgroundPosition: "50% 25%",
overflow: "hidden",
}}
>
<Flex
align="center"
width="100%"
justify="between"
p="4"
style={{
background:
"linear-gradient(90deg, var(--black-a3), var(--gray-1))",
}}
>
<Flex align="center" width="100%" justify="between" p="4" style={{}}>
<Text
style={{
textShadow: "0 0 var(--space-1) var(--black-a12)",
@@ -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 { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
import { QuickInputs } from "./components/QuickInputs";
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
import { Thicknesses } from "./components/Thicknesses";
type OptionsProps = MaybeSkeletonComponentProps & {
@@ -76,6 +77,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const equalize = Duel.use((state) => state.equalize);
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
@@ -278,9 +280,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
}}
/>
</IconButton>
<SpallLinerSwitcher />
</Flex>
{!skeleton && (
{!skeleton && modelRequested && (
<Suspense>
<DynamicArmorSwitcher />
</Suspense>
@@ -15,7 +15,7 @@ export function Tracker({ fontSize }: Props) {
top="50%"
width="min(100%, 16rem)"
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} />
@@ -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 { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
import { Tracker } from "./components/Tracker";
export function Title() {
const { unwrap } = useLocale();
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);
@@ -21,6 +24,8 @@ export function Title() {
onPointerDown={(event) => {
event.preventDefault();
if (!modelRequested) return;
Tankopedia.mutate((draft) => {
draft.revealed = true;
});
@@ -60,7 +65,36 @@ export function Title() {
{name}
</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>
);
}
@@ -82,6 +82,7 @@ export function Lighting({ hasPbr }: Props) {
blur={2 ** 1}
resolution={2 ** 9}
depthWrite={false}
renderOrder={1}
/>
{animate && (
@@ -51,11 +51,17 @@ export function TankModel() {
invalidate();
if (material.map) material.map.offset.y += offset;
if (material.aoMap) material.aoMap.offset.y += offset;
if (material.normalMap) material.normalMap.offset.y += offset;
if (material.roughnessMap) material.roughnessMap.offset.y += offset;
if (material.metalnessMap) material.metalnessMap.offset.y += offset;
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>) {
@@ -1,6 +1,6 @@
import { Box, Flex } from "@radix-ui/themes";
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 { defaultEqualizer } from "../../../core/blitzkit/tankToDuelMember";
import { useFullScreen } from "../../../hooks/useFullScreen";
@@ -20,6 +20,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
const canvas = useRef<HTMLCanvasElement>(null!);
const isFullScreen = useFullScreen();
const protagonist = Duel.use((state) => state.protagonist.tank);
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const thicknessRange = useMemo(() => {
const entries = Object.values(tankDefinitions.tanks);
const filtered = entries.filter(
@@ -131,13 +132,15 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
top="0"
left="0"
>
{!skeleton && (
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
{!skeleton && modelRequested && (
<Suspense>
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
</Suspense>
)}
</Box>
</Box>
<Title />
<Title skeleton={skeleton} />
<Options
skeleton={skeleton}
@@ -127,7 +127,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
</Text>
</Flex>
{averages && nextTanks && tank.tier !== 10 && (
{averages && nextTanks && (
<Text color="gray" size="1" mt="2">
{skeleton && <Skeleton height="1em" width="4em" />}
{!skeleton &&
@@ -191,7 +191,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
);
})}
{master.tier < 10 && (
{master.successors.length > 0 && (
<>
<Arrow />
{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[] = [],
) {
return (
((filters.showOwned && owned.includes(tank.id)) ||
((!filters.showOwned && !filters.showUnowned) ||
(filters.showOwned && owned.includes(tank.id)) ||
(filters.showUnowned && !owned.includes(tank.id))) &&
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
(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.gunType.length === 0 ||
(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";
import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient";
import type { EquipmentMatrix } from "../../stores/duel";
import { SPALL_LINER_HE_DAMAGE_DELTA } from "./spallLiner";
import { defaultEqualizer } from "./tankToDuelMember";
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
@@ -170,7 +171,10 @@ export function tankCharacteristics(
coefficient(
[applyReactiveArmor && shell.type !== ShellType.SHELL_TYPE_HE, -0.27],
[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 =
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
@@ -3,11 +3,13 @@ import { useRef } from "react";
export function useStableProgress() {
const { loaded, total } = useProgress();
const baseline = useRef(loaded);
const progress = useRef(0);
const pending = total - baseline.current;
progress.current = Math.max(
progress.current,
total === 0 ? 0 : (loaded / total) * 100
pending <= 0 ? 0 : ((loaded - baseline.current) / pending) * 100
);
return progress.current;
@@ -67,7 +67,7 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
{
locale !== locales.default && (
<Flex justify="center" mb="4">
<Flex justify="center" pt="4" my="4">
<Link
href="https://crowdin.com/project/blitzkrieg"
target="_blank"
@@ -17,7 +17,10 @@ export function Page({
}: Props & MaybeSkeletonComponentProps) {
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 (
<LocaleProvider locale={locale}>
@@ -18,6 +18,10 @@ export function Page({ locale }: LocaleAcceptorProps) {
function Content({ locale }: LocaleAcceptorProps) {
const developerMode = App.useDeferred((state) => state.developerMode, false);
const autoLoadModels = App.useDeferred(
(state) => state.autoLoadModels,
true,
);
const { strings } = useLocale();
return (
@@ -36,6 +40,19 @@ function Content({ locale }: LocaleAcceptorProps) {
/>
</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">
<Text>{strings.website.settings.language}</Text>
<LocaleSwitcher locale={locale} />
+2
View File
@@ -8,6 +8,7 @@ export interface WargamingLogin {
export interface App {
developerMode: boolean;
autoLoadModels: boolean;
policiesAgreementIndex: number;
logins: {
wargaming?: WargamingLogin;
@@ -17,6 +18,7 @@ export interface App {
export const App = new Varuna<App>(
{
developerMode: false,
autoLoadModels: true,
policiesAgreementIndex: -1,
logins: {},
},
+4 -2
View File
@@ -1,6 +1,8 @@
import type { TankDefinition } from "@blitzkit/core";
import { times } from "lodash-es";
import { Varuna } from "varuna";
import { awaitableTiers } from "../core/awaitables/tiers";
const tiers = await awaitableTiers;
export enum GuessState {
Correct,
@@ -25,5 +27,5 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
correctGuesses: 0,
streak: 0,
helpingReveal: false,
tiers: times(10, (index) => index + 1),
tiers,
}));
+4 -4
View File
@@ -46,8 +46,8 @@ export const TankFilters = new Varuna<TankFilters>({
abilities: [],
powers: [],
showTesting: true,
showNonTesting: true,
showOwned: true,
showUnowned: true,
showTesting: false,
showNonTesting: false,
showOwned: false,
showUnowned: false,
});
@@ -9,6 +9,7 @@ import type { ArmorType } from "../components/Armor/components/SpacedArmorScene"
import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent";
import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection";
import { api } from "../core/blitzkit/api";
import { App } from "./app";
import { TankopediaDisplay } from "./tankopediaPersistent/constants";
export interface ShotLayerBase {
@@ -79,6 +80,7 @@ export enum TankopediaRelativeAgainst {
interface Tankopedia {
disturbed: boolean;
revealed: boolean;
modelRequested: boolean;
shot?: Shot;
skills: Record<string, number>;
relativeAgainst: TankopediaRelativeAgainst;
@@ -110,6 +112,7 @@ const skillDefinitions = await api.skillDefinitions();
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
disturbed: false,
revealed: false,
modelRequested: App.state.autoLoadModels,
relativeAgainst: TankopediaRelativeAgainst.Class,
editStatic: false,
skills: createDefaultSkills(skillDefinitions),
+35
View File
@@ -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=