mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
Merge branch 'main' into ue
This commit is contained in:
commit
c0376182f8
164 files changed
+5145
-4079
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AverageDefinitions,
|
||||
AverageDefinitionsAllStats,
|
||||
AverageDefinitionsEntry,
|
||||
type AverageDefinitionsManifest,
|
||||
type IndividualTankStats,
|
||||
@@ -30,9 +31,6 @@ type OptionalSecondLevel<T> = {
|
||||
};
|
||||
};
|
||||
|
||||
type AverageDefinitionsEntrySubPartial =
|
||||
OptionalSecondLevel<AverageDefinitionsEntry>;
|
||||
|
||||
const MINUTE = 60 * 1000;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
@@ -47,15 +45,15 @@ const preDiscoveredIds = await fetchPreDiscoveredIds();
|
||||
const playerIds: Record<Region, number[][]> = {
|
||||
asia: chunk(
|
||||
preDiscoveredIds.filter((id) => idToRegion(id) === "asia"),
|
||||
PLAYER_IDS_PER_CALL
|
||||
PLAYER_IDS_PER_CALL,
|
||||
),
|
||||
com: chunk(
|
||||
preDiscoveredIds.filter((id) => idToRegion(id) === "com"),
|
||||
PLAYER_IDS_PER_CALL
|
||||
PLAYER_IDS_PER_CALL,
|
||||
),
|
||||
eu: chunk(
|
||||
preDiscoveredIds.filter((id) => idToRegion(id) === "eu"),
|
||||
PLAYER_IDS_PER_CALL
|
||||
PLAYER_IDS_PER_CALL,
|
||||
),
|
||||
};
|
||||
let regionIndex = 0;
|
||||
@@ -71,7 +69,7 @@ times(THREADS, async () => {
|
||||
const regionIds = playerIds[region];
|
||||
const [ids] = regionIds.splice(
|
||||
Math.floor(Math.random() * regionIds.length),
|
||||
1
|
||||
1,
|
||||
);
|
||||
const accountInfo = await getAccountInfo(region, ids, undefined, {
|
||||
fields: "last_battle_time,statistics.all.battles",
|
||||
@@ -104,8 +102,8 @@ times(THREADS, async () => {
|
||||
filteredIds.map((id) =>
|
||||
getTankStats(region, id, {
|
||||
fields: "last_battle_time,battle_life_time,all,tank_id",
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
players.forEach((tanks) => {
|
||||
@@ -114,7 +112,7 @@ times(THREADS, async () => {
|
||||
|
||||
if (!tankIds.includes(tank.tank_id)) {
|
||||
console.log(
|
||||
`Found new tank: ${tank.tank_id} (${tankIds.length + 1})`
|
||||
`Found new tank: ${tank.tank_id} (${tankIds.length + 1})`,
|
||||
);
|
||||
tankIds.push(tank.tank_id);
|
||||
tanksSorted[tank.tank_id] = [];
|
||||
@@ -140,7 +138,7 @@ async function postWork() {
|
||||
console.log(
|
||||
`Generating statistics based on ${samples.d_120.toLocaleString()} players (${samples.total.toLocaleString()} checked in total) and ${
|
||||
tankIds.length
|
||||
} tanks...`
|
||||
} tanks...`,
|
||||
);
|
||||
|
||||
using uploader = new AssetUploader("averages");
|
||||
@@ -152,7 +150,12 @@ async function postWork() {
|
||||
// (tank) => Date.now() - tank.last_battle_time * 1000 <= MAX_ACTIVITY_TIME,
|
||||
// );
|
||||
const tanks = tanksSortedEntry;
|
||||
const entry: AverageDefinitionsEntrySubPartial = {
|
||||
const entry: {
|
||||
mu: Partial<Record<keyof AverageDefinitionsAllStats, number>>;
|
||||
sigma: Partial<Record<keyof AverageDefinitionsAllStats, number>>;
|
||||
r: Partial<Record<keyof AverageDefinitionsAllStats, number>>;
|
||||
samples: Samples;
|
||||
} = {
|
||||
mu: {},
|
||||
sigma: {},
|
||||
r: {},
|
||||
@@ -162,13 +165,13 @@ async function postWork() {
|
||||
tanks.forEach((tank) => {
|
||||
const timeSinceLastActivity = Date.now() - tank.last_battle_time * 1000;
|
||||
|
||||
entry.samples.total++;
|
||||
if (timeSinceLastActivity <= 120 * DAY) entry.samples.d_120++;
|
||||
if (timeSinceLastActivity <= 90 * DAY) entry.samples.d_90++;
|
||||
if (timeSinceLastActivity <= 60 * DAY) entry.samples.d_60++;
|
||||
if (timeSinceLastActivity <= 30 * DAY) entry.samples.d_30++;
|
||||
if (timeSinceLastActivity <= 7 * DAY) entry.samples.d_7++;
|
||||
if (timeSinceLastActivity <= 1 * DAY) entry.samples.d_1++;
|
||||
entry.samples!.total++;
|
||||
if (timeSinceLastActivity <= 120 * DAY) entry.samples!.d_120++;
|
||||
if (timeSinceLastActivity <= 90 * DAY) entry.samples!.d_90++;
|
||||
if (timeSinceLastActivity <= 60 * DAY) entry.samples!.d_60++;
|
||||
if (timeSinceLastActivity <= 30 * DAY) entry.samples!.d_30++;
|
||||
if (timeSinceLastActivity <= 7 * DAY) entry.samples!.d_7++;
|
||||
if (timeSinceLastActivity <= 1 * DAY) entry.samples!.d_1++;
|
||||
});
|
||||
|
||||
const dataWY = tanks.map((tank) => ({
|
||||
@@ -191,7 +194,7 @@ async function postWork() {
|
||||
function sum(slicer: (data: DataPoint) => number) {
|
||||
return data.reduce(
|
||||
(accumulator, data) => accumulator + slicer(data),
|
||||
0
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@ export async function boosterIcons() {
|
||||
console.log("Building booster icons...");
|
||||
|
||||
const uploader = new AssetUploader("booster icons");
|
||||
const boosterFiles = vfs
|
||||
const boosterFiles = await vfs
|
||||
.dir("Data/Gfx/Shared/boosters")
|
||||
.filter(
|
||||
(file) => !file.includes("@2x.txt") && !file.startsWith("texture0")
|
||||
.then((files) =>
|
||||
files.filter(
|
||||
(file) => !file.includes("@2x.txt") && !file.startsWith("texture0"),
|
||||
),
|
||||
);
|
||||
const image = sharp(
|
||||
await vfs.file(`Data/Gfx/Shared/boosters/texture0.packed.webp`)
|
||||
await vfs.file(`Data/Gfx/Shared/boosters/texture0.packed.webp`),
|
||||
);
|
||||
|
||||
for (const file of boosterFiles) {
|
||||
|
||||
@@ -6,7 +6,7 @@ export async function camouflageIcons() {
|
||||
|
||||
using uploader = new AssetUploader("camo icons");
|
||||
const content = await vfs.file(
|
||||
`Data/Gfx/UI/Hangar/IconCamouflage.packed.webp`
|
||||
`Data/Gfx/UI/Hangar/IconCamouflage.packed.webp`,
|
||||
);
|
||||
|
||||
await uploader.add({ content, path: "icons/camo.webp" });
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { assertSecret } from "@blitzkit/core";
|
||||
import { AbstractVFS } from "../core/vfs/abstract";
|
||||
import { LocalVFS } from "../core/vfs/local";
|
||||
import { SteamVFS } from "../core/vfs/steam";
|
||||
import { ZipVFS } from "../core/vfs/zip";
|
||||
|
||||
const steamPattern = /steam:(\d+)\/(\d+)/;
|
||||
const zipPattern = /zip:(.+)/;
|
||||
const localePattern = /local:"?(.+)"?/;
|
||||
|
||||
const source = Bun.argv
|
||||
.find((arg) => arg.startsWith("--source="))
|
||||
@@ -21,11 +23,14 @@ if (!source) {
|
||||
assertSecret(import.meta.env.STEAM_USERNAME),
|
||||
assertSecret(import.meta.env.STEAM_PASSWORD),
|
||||
app,
|
||||
depot
|
||||
depot,
|
||||
).init();
|
||||
} else if (zipPattern.test(source)) {
|
||||
const [, url] = source.match(zipPattern)!;
|
||||
resolved = await new ZipVFS(url).init();
|
||||
} else if (localePattern.test(source)) {
|
||||
const [, path] = source.match(localePattern)!;
|
||||
resolved = await new LocalVFS(path).init();
|
||||
} else {
|
||||
throw new Error("No valid source specified");
|
||||
}
|
||||
|
||||
@@ -18,26 +18,20 @@ export async function consumableProvisionIcons() {
|
||||
|
||||
using uploader = new AssetUploader("consumable and provision icons");
|
||||
const styles = [
|
||||
"UI/Styles/Lobby/Inventory/InventoryNormalStyles.yaml",
|
||||
"UI/Styles/Lobby/Inventory/Event/InventoryBigStyles.yaml",
|
||||
"UI/Styles/Lobby/Inventory/Event/InventoryNormalStyles.yaml",
|
||||
"UI/Styles/Lobby/Inventory/InventoryNormalStyles.yaml",
|
||||
"UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml",
|
||||
"UI/Screens/Battle/Styles/BattleEquipmentStyles.yaml",
|
||||
];
|
||||
const styleSheets = await Promise.all(
|
||||
styles.map(async (path) =>
|
||||
Object.values(
|
||||
(
|
||||
await vfs.yaml<Mappings>(`Data/${path}`)
|
||||
).StyleSheets
|
||||
)
|
||||
)
|
||||
Object.values((await vfs.yaml<Mappings>(`Data/${path}`)).StyleSheets),
|
||||
),
|
||||
).then((array) => array.flat());
|
||||
|
||||
for (const match of (
|
||||
await vfs.text(
|
||||
`Data/XML/item_defs/vehicles/common/consumables/list.xml`
|
||||
)
|
||||
await vfs.text(`Data/XML/item_defs/vehicles/common/consumables/list.xml`)
|
||||
).matchAll(listItemsPattern)) {
|
||||
const consumablesCommon = await vfs.xml<{
|
||||
root: ConsumablesCommon;
|
||||
@@ -46,13 +40,13 @@ export async function consumableProvisionIcons() {
|
||||
for (const consumable of Object.values(consumablesCommon.root)) {
|
||||
const styleSheet = styleSheets.find((styleSheet) =>
|
||||
styleSheet.selectors.some((selector) =>
|
||||
selector.includes(`${consumable.icon} `)
|
||||
)
|
||||
selector.includes(`${consumable.icon} `),
|
||||
),
|
||||
);
|
||||
|
||||
if (!styleSheet) {
|
||||
console.warn(
|
||||
`No style sheet found for consumable ${consumable.icon}; skipping...`
|
||||
`No style sheet found for consumable ${consumable.icon}; skipping...`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -62,10 +56,8 @@ export async function consumableProvisionIcons() {
|
||||
.replace(".psd", "")
|
||||
.replace(".txt", "");
|
||||
|
||||
if (vfs.resolve(`Data/${configPath}.packed.webp`)) {
|
||||
const image = sharp(
|
||||
await vfs.file(`Data/${configPath}.packed.webp`)
|
||||
);
|
||||
if (await vfs.resolve(`Data/${configPath}.packed.webp`)) {
|
||||
const image = sharp(await vfs.file(`Data/${configPath}.packed.webp`));
|
||||
const content = await image.trim({ threshold: 100 }).toBuffer();
|
||||
|
||||
await uploader.add({
|
||||
@@ -78,8 +70,8 @@ export async function consumableProvisionIcons() {
|
||||
`${`Data/${configPath}`
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/")}/texture0.packed.webp`
|
||||
)
|
||||
.join("/")}/texture0.packed.webp`,
|
||||
),
|
||||
);
|
||||
const sizes = (await vfs.text(`Data/${configPath}.txt`))
|
||||
.split("\n")[4]
|
||||
@@ -104,9 +96,7 @@ export async function consumableProvisionIcons() {
|
||||
}
|
||||
|
||||
for (const match of (
|
||||
await vfs.text(
|
||||
`Data/XML/item_defs/vehicles/common/provisions/list.xml`
|
||||
)
|
||||
await vfs.text(`Data/XML/item_defs/vehicles/common/provisions/list.xml`)
|
||||
).matchAll(listItemsPattern)) {
|
||||
const provisionsCommon = await vfs.xml<{
|
||||
root: ProvisionsCommon;
|
||||
@@ -115,13 +105,13 @@ export async function consumableProvisionIcons() {
|
||||
for (const provision of Object.values(provisionsCommon.root)) {
|
||||
const styleSheet = styleSheets.find((styleSheet) =>
|
||||
styleSheet.selectors.some((selector) =>
|
||||
selector.includes(`${provision.icon} `)
|
||||
)
|
||||
selector.includes(`${provision.icon} `),
|
||||
),
|
||||
);
|
||||
|
||||
if (!styleSheet) {
|
||||
console.warn(
|
||||
`No style sheet found for provision ${provision.icon}; skipping...`
|
||||
`No style sheet found for provision ${provision.icon}; skipping...`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -131,10 +121,8 @@ export async function consumableProvisionIcons() {
|
||||
.replace(".psd", "")
|
||||
.replace(".txt", "");
|
||||
|
||||
if (vfs.resolve(`Data/${configPath}.packed.webp`)) {
|
||||
const image = sharp(
|
||||
await vfs.file(`Data/${configPath}.packed.webp`)
|
||||
);
|
||||
if (await vfs.resolve(`Data/${configPath}.packed.webp`)) {
|
||||
const image = sharp(await vfs.file(`Data/${configPath}.packed.webp`));
|
||||
const content = await image.trim({ threshold: 100 }).toBuffer();
|
||||
|
||||
await uploader.add({
|
||||
@@ -147,8 +135,8 @@ export async function consumableProvisionIcons() {
|
||||
`${`Data/${configPath}`
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/")}/texture0.packed.webp`
|
||||
)
|
||||
.join("/")}/texture0.packed.webp`,
|
||||
),
|
||||
);
|
||||
const sizes = (await vfs.text(`Data/${configPath}.txt`))
|
||||
.split("\n")[4]
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function currencies() {
|
||||
|
||||
for (const [file, name] of ICONS) {
|
||||
const content = await sharp(
|
||||
await vfs.file(`Data/Gfx/Lobby/currency/${file}`)
|
||||
await vfs.file(`Data/Gfx/Lobby/currency/${file}`),
|
||||
)
|
||||
.trim()
|
||||
.toBuffer();
|
||||
@@ -36,14 +36,14 @@ export async function currencies() {
|
||||
.then((glossary) =>
|
||||
Object.values(glossary)
|
||||
.entries()
|
||||
.filter(([key]) => /^prx_season_\d+$/.test(`${key}`))
|
||||
.filter(([key]) => /^prx_season_\d+$/.test(`${key}`)),
|
||||
);
|
||||
|
||||
for (const [key, item] of glossary) {
|
||||
if (item.image_url === null) throw new Error(`No image_url for ${key}`);
|
||||
|
||||
const imageRaw = await fetch(item.image_url).then((response) =>
|
||||
response.arrayBuffer()
|
||||
response.arrayBuffer(),
|
||||
);
|
||||
const content = await sharp(imageRaw)
|
||||
.trim({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ConsumableTankCategoryFilterCategory,
|
||||
Crew,
|
||||
CrewType,
|
||||
Equalizer,
|
||||
EquipmentDefinitions,
|
||||
EquipmentSlot,
|
||||
GameDefinitions,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
} from "@blitzkit/core";
|
||||
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n";
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import { readFile } from "fs/promises";
|
||||
import { deburr } from "lodash-es";
|
||||
import { parse as parsePath } from "path";
|
||||
import type { Vector3Tuple } from "three";
|
||||
@@ -81,16 +83,16 @@ const blitzTankFilterDefinitionCategoryToBlitzkit: Record<
|
||||
BlitzTankFilterDefinitionCategory,
|
||||
ConsumableTankCategoryFilterCategory
|
||||
> = {
|
||||
clip: ConsumableTankCategoryFilterCategory.CLIP,
|
||||
clip: ConsumableTankCategoryFilterCategory.CONSUMABLE_TANK_CATEGORY_FILTER_CATEGORY_CLIP,
|
||||
};
|
||||
function vector3TupleToBlitzkit(tuple: Vector3Tuple) {
|
||||
return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3;
|
||||
}
|
||||
const blitzTankClassToBlitzkit: Record<BlitzTankClass, TankClass> = {
|
||||
lightTank: TankClass.LIGHT,
|
||||
"AT-SPG": TankClass.TANK_DESTROYER,
|
||||
heavyTank: TankClass.HEAVY,
|
||||
mediumTank: TankClass.MEDIUM,
|
||||
lightTank: TankClass.TANK_CLASS_LIGHT,
|
||||
"AT-SPG": TankClass.TANK_CLASS_TANK_DESTROYER,
|
||||
heavyTank: TankClass.TANK_CLASS_HEAVY,
|
||||
mediumTank: TankClass.TANK_CLASS_MEDIUM,
|
||||
};
|
||||
export interface BlitzStrings {
|
||||
[key: string]: string;
|
||||
@@ -135,7 +137,7 @@ export interface ChassisDefinitionsList {
|
||||
}
|
||||
type VehicleDefinitionArmor = Record<
|
||||
string,
|
||||
number | { vehicleDamageFactor: 0; "#text": number }
|
||||
number | { vehicleDamageFactor: 0; "#text": number } | number[]
|
||||
>;
|
||||
interface UnlocksInner {
|
||||
cost: number | string;
|
||||
@@ -149,18 +151,18 @@ type BlitzModuleType = {
|
||||
type UnlocksListing = BlitzModuleType | undefined;
|
||||
type BlitzCrewType = "commander" | "radioman" | "gunner" | "driver" | "loader";
|
||||
const blitzCrewTypeToBlitzkit: Record<BlitzCrewType, CrewType> = {
|
||||
commander: CrewType.COMMANDER,
|
||||
driver: CrewType.DRIVER,
|
||||
gunner: CrewType.GUNNER,
|
||||
loader: CrewType.LOADER,
|
||||
radioman: CrewType.RADIOMAN,
|
||||
commander: CrewType.CREW_TYPE_COMMANDER,
|
||||
driver: CrewType.CREW_TYPE_DRIVER,
|
||||
gunner: CrewType.CREW_TYPE_GUNNER,
|
||||
loader: CrewType.CREW_TYPE_LOADER,
|
||||
radioman: CrewType.CREW_TYPE_RADIOMAN,
|
||||
};
|
||||
const blitzkitCrewTypeToBlitz: Record<CrewType, BlitzCrewType> = {
|
||||
[CrewType.COMMANDER]: "commander",
|
||||
[CrewType.DRIVER]: "driver",
|
||||
[CrewType.GUNNER]: "gunner",
|
||||
[CrewType.LOADER]: "loader",
|
||||
[CrewType.RADIOMAN]: "radioman",
|
||||
[CrewType.CREW_TYPE_COMMANDER]: "commander",
|
||||
[CrewType.CREW_TYPE_DRIVER]: "driver",
|
||||
[CrewType.CREW_TYPE_GUNNER]: "gunner",
|
||||
[CrewType.CREW_TYPE_LOADER]: "loader",
|
||||
[CrewType.CREW_TYPE_RADIOMAN]: "radioman",
|
||||
};
|
||||
interface VehicleDefinitions {
|
||||
invisibility: {
|
||||
@@ -298,7 +300,7 @@ interface VehicleDefinitions {
|
||||
type: "default";
|
||||
enabled: false;
|
||||
factorsModifiers: "";
|
||||
}
|
||||
},
|
||||
];
|
||||
};
|
||||
};
|
||||
@@ -517,20 +519,36 @@ type CombatRolesYaml = Record<
|
||||
>;
|
||||
|
||||
const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
|
||||
ARMOR_PIERCING: ShellType.AP,
|
||||
ARMOR_PIERCING_CR: ShellType.APCR,
|
||||
HIGH_EXPLOSIVE: ShellType.HE,
|
||||
HOLLOW_CHARGE: ShellType.HEAT,
|
||||
ARMOR_PIERCING: ShellType.SHELL_TYPE_AP,
|
||||
ARMOR_PIERCING_CR: ShellType.SHELL_TYPE_APCR,
|
||||
HIGH_EXPLOSIVE: ShellType.SHELL_TYPE_HE,
|
||||
HOLLOW_CHARGE: ShellType.SHELL_TYPE_HEAT,
|
||||
};
|
||||
export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/;
|
||||
const blitzModuleTypeToBlitzkit: Record<keyof BlitzModuleType, ModuleType> = {
|
||||
chassis: ModuleType.TRACKS,
|
||||
engine: ModuleType.ENGINE,
|
||||
gun: ModuleType.GUN,
|
||||
turret: ModuleType.TURRET,
|
||||
vehicle: ModuleType.VEHICLE,
|
||||
chassis: ModuleType.MODULE_TYPE_TRACKS,
|
||||
engine: ModuleType.MODULE_TYPE_ENGINE,
|
||||
gun: ModuleType.MODULE_TYPE_GUN,
|
||||
turret: ModuleType.MODULE_TYPE_TURRET,
|
||||
vehicle: ModuleType.MODULE_TYPE_VEHICLE,
|
||||
};
|
||||
|
||||
function assignArmor(
|
||||
raw: VehicleDefinitionArmor[string],
|
||||
id: number,
|
||||
armor: Armor,
|
||||
) {
|
||||
if (typeof raw === "number") {
|
||||
armor.thickness[id] = raw;
|
||||
} else if (Array.isArray(raw)) {
|
||||
armor.thickness[id] = raw.at(-1)!;
|
||||
} else {
|
||||
if (!armor.spaced) armor.spaced = [];
|
||||
armor.thickness[id] = raw["#text"];
|
||||
armor.spaced.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
export async function definitions() {
|
||||
console.log("Building definitions...");
|
||||
|
||||
@@ -555,18 +573,18 @@ export async function definitions() {
|
||||
const provisionDefinitions: ProvisionDefinitions = { provisions: {} };
|
||||
const skillDefinitions: SkillDefinitions = {
|
||||
classes: {
|
||||
[TankClass.LIGHT]: { skills: [] },
|
||||
[TankClass.MEDIUM]: { skills: [] },
|
||||
[TankClass.HEAVY]: { skills: [] },
|
||||
[TankClass.TANK_DESTROYER]: { skills: [] },
|
||||
[TankClass.TANK_CLASS_LIGHT]: { skills: [] },
|
||||
[TankClass.TANK_CLASS_MEDIUM]: { skills: [] },
|
||||
[TankClass.TANK_CLASS_HEAVY]: { skills: [] },
|
||||
[TankClass.TANK_CLASS_TANK_DESTROYER]: { skills: [] },
|
||||
},
|
||||
};
|
||||
const nations = vfs
|
||||
const nations = await vfs
|
||||
.dir("Data/XML/item_defs/vehicles")
|
||||
.filter((nation) => nation !== "common");
|
||||
.then((files) => files.filter((nation) => nation !== "common"));
|
||||
const tankStringIdMap: Record<string, number> = {};
|
||||
const optionalDevices = await vfs.xml<{ root: OptionalDevices }>(
|
||||
"Data/XML/item_defs/vehicles/common/optional_devices.xml"
|
||||
"Data/XML/item_defs/vehicles/common/optional_devices.xml",
|
||||
);
|
||||
const stringsI18n: Record<string, Record<string, string>> = {};
|
||||
|
||||
@@ -574,12 +592,12 @@ export async function definitions() {
|
||||
locales.supported.map(async ({ locale }) => {
|
||||
const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale];
|
||||
const cache = await fetch(
|
||||
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`
|
||||
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`,
|
||||
)
|
||||
.then((response) => response.text())
|
||||
.then((string) => parseYaml(string) as BlitzStrings);
|
||||
const preInstalled = await vfs.yaml<BlitzStrings>(
|
||||
`Data/Strings/${blitzLocale}.yaml`
|
||||
`Data/Strings/${blitzLocale}.yaml`,
|
||||
);
|
||||
|
||||
stringsI18n[locale] = {
|
||||
@@ -588,7 +606,7 @@ export async function definitions() {
|
||||
};
|
||||
|
||||
console.log(`Loaded strings for ${locale}`);
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
function getString(name: string) {
|
||||
@@ -627,9 +645,9 @@ export async function definitions() {
|
||||
consumables,
|
||||
(
|
||||
await vfs.xml<{ root: ConsumablesCommon }>(
|
||||
`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`
|
||||
`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`,
|
||||
)
|
||||
).root
|
||||
).root,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -642,39 +660,57 @@ export async function definitions() {
|
||||
provisions,
|
||||
(
|
||||
await vfs.xml<{ root: ConsumablesCommon }>(
|
||||
`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`
|
||||
`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`,
|
||||
)
|
||||
).root
|
||||
).root,
|
||||
);
|
||||
}
|
||||
|
||||
const avatar = await vfs.xml<{ root: Avatar }>(
|
||||
`Data/XML/item_defs/tankmen/avatar.xml`
|
||||
`Data/XML/item_defs/tankmen/avatar.xml`,
|
||||
);
|
||||
const maps = await vfs.yaml<Maps>(`Data/maps.yaml`);
|
||||
const tankXps = new Map<number, ResearchCost>();
|
||||
const camouflagesXml = await vfs.xml<{ root: CamouflagesXML }>(
|
||||
`Data/XML/item_defs/vehicles/common/camouflages.xml`
|
||||
`Data/XML/item_defs/vehicles/common/camouflages.xml`,
|
||||
);
|
||||
const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
|
||||
`Data/camouflages.yaml`
|
||||
`Data/camouflages.yaml`,
|
||||
);
|
||||
const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages);
|
||||
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
|
||||
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`
|
||||
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`,
|
||||
);
|
||||
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
|
||||
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`
|
||||
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`,
|
||||
);
|
||||
const gameModeNativeNames: Record<string, number> = {};
|
||||
const combatRoles = await vfs.yaml<CombatRolesYaml>(
|
||||
`Data/XML/item_defs/vehicles/common/combat_roles.yaml`
|
||||
`Data/XML/item_defs/vehicles/common/combat_roles.yaml`,
|
||||
);
|
||||
const consumableNativeNames: Record<string, number> = {};
|
||||
const provisionNativeNames: Record<string, number> = {};
|
||||
let equalizerDefinitions: string[][];
|
||||
|
||||
if (
|
||||
await vfs.resolve("Data/XML/item_defs/vehicles/common/tier_equializer.csv")
|
||||
) {
|
||||
equalizerDefinitions = await vfs.csv(
|
||||
`Data/XML/item_defs/vehicles/common/tier_equializer.csv`,
|
||||
{ delimiter: ";" },
|
||||
);
|
||||
} else {
|
||||
const stubEqualizerDefinitions = await readFile("stub/equalizer.csv").then(
|
||||
(buffer) => buffer.toString(),
|
||||
);
|
||||
|
||||
equalizerDefinitions = await vfs._csv(stubEqualizerDefinitions, {
|
||||
delimiter: ";",
|
||||
});
|
||||
}
|
||||
|
||||
for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
|
||||
/"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g
|
||||
/"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g,
|
||||
)) {
|
||||
const id = Number(match[1]);
|
||||
const name = getString(match[2]);
|
||||
@@ -686,7 +722,7 @@ export async function definitions() {
|
||||
}
|
||||
|
||||
for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
|
||||
/eGameMode\.([a-zA-Z]+) -> "~res:\/Gfx\/UI\/Hangar\/GameTypes\/battle-type_([^"]+)"/g
|
||||
/eGameMode\.([a-zA-Z]+) -> "~res:\/Gfx\/UI\/Hangar\/GameTypes\/battle-type_([^"]+)"/g,
|
||||
)) {
|
||||
Object.entries(gameModeNativeNames).forEach(([key, value]) => {
|
||||
if (key.toLowerCase() === match[2].toLowerCase()) {
|
||||
@@ -720,7 +756,7 @@ export async function definitions() {
|
||||
|
||||
for (const nation of nations) {
|
||||
const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
|
||||
for (const tankKey in tankList.root) {
|
||||
@@ -761,7 +797,7 @@ export async function definitions() {
|
||||
console.warn(
|
||||
`Multiple tanks share slug ${slug}: ${requesters
|
||||
.map(({ key }) => key)
|
||||
.join(", ")}`
|
||||
.join(", ")}`,
|
||||
);
|
||||
|
||||
if (requesters.length !== 2) {
|
||||
@@ -791,7 +827,7 @@ export async function definitions() {
|
||||
|
||||
if (canonical === undefined) {
|
||||
throw new Error(
|
||||
"Two tutorial bots share the same slug? The world is truly broken."
|
||||
"Two tutorial bots share the same slug? The world is truly broken.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -804,7 +840,7 @@ export async function definitions() {
|
||||
|
||||
for (const nation of nations) {
|
||||
const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
const turretList = await vfs.xml<{
|
||||
root: TurretDefinitionsList;
|
||||
@@ -857,7 +893,7 @@ export async function definitions() {
|
||||
: Number(item.cost.split(":")[1]),
|
||||
},
|
||||
} satisfies Unlock;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.flat();
|
||||
}
|
||||
@@ -872,10 +908,10 @@ export async function definitions() {
|
||||
const tank = tankList.root[tankKey];
|
||||
let tankPrice: TankPrice;
|
||||
const tankDefinition = await vfs.xml<{ root: VehicleDefinitions }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/${tankKey}.xml`
|
||||
`Data/XML/item_defs/vehicles/${nation}/${tankKey}.xml`,
|
||||
);
|
||||
const tankParameters = await vfs.yaml<TankParameters>(
|
||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
|
||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
const turretOrigin = tankDefinition.root.hull.turretPositions.turret
|
||||
.split(" ")
|
||||
@@ -890,23 +926,23 @@ export async function definitions() {
|
||||
|
||||
if (slug === undefined) {
|
||||
throw new Error(
|
||||
`Could not find slug for ${nation}/${tankKey} (${tankId})`
|
||||
`Could not find slug for ${nation}/${tankKey} (${tankId})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (tank.sellPrice) {
|
||||
tankPrice = {
|
||||
type: TankPriceType.GOLD,
|
||||
type: TankPriceType.TANK_PRICE_TYPE_GOLD,
|
||||
value: tank.sellPrice["#text"] * 2,
|
||||
};
|
||||
} else if (typeof tank.price === "number") {
|
||||
tankPrice = {
|
||||
type: TankPriceType.CREDITS,
|
||||
type: TankPriceType.TANK_PRICE_TYPE_CREDITS,
|
||||
value: tank.price,
|
||||
};
|
||||
} else {
|
||||
tankPrice = {
|
||||
type: TankPriceType.CREDITS,
|
||||
type: TankPriceType.TANK_PRICE_TYPE_CREDITS,
|
||||
value: tank.price["#text"] * 400,
|
||||
};
|
||||
}
|
||||
@@ -923,13 +959,7 @@ export async function definitions() {
|
||||
const armorId = parseInt(armorIdString);
|
||||
const armorRaw = tankDefinition.root.hull.armor[name];
|
||||
|
||||
if (typeof armorRaw === "number") {
|
||||
hullArmor.thickness[armorId] = armorRaw;
|
||||
} else {
|
||||
if (!hullArmor.spaced) hullArmor.spaced = [];
|
||||
hullArmor.thickness[armorId] = armorRaw["#text"];
|
||||
hullArmor.spaced.push(armorId);
|
||||
}
|
||||
assignArmor(armorRaw, armorId, hullArmor);
|
||||
});
|
||||
const crew: Crew[] = [];
|
||||
const fixedCamouflage = tankTags.includes("eventCamouflage_user");
|
||||
@@ -938,7 +968,7 @@ export async function definitions() {
|
||||
Object.entries(tankDefinition.root.crew).forEach(([crewKey, value]) => {
|
||||
let entry: Crew;
|
||||
const index = crew.findIndex(
|
||||
({ type }) => blitzkitCrewTypeToBlitz[type] === crewKey
|
||||
({ type }) => blitzkitCrewTypeToBlitz[type] === crewKey,
|
||||
);
|
||||
if (index === -1) {
|
||||
if (crewKey === "#text") return;
|
||||
@@ -988,10 +1018,23 @@ export async function definitions() {
|
||||
})
|
||||
.map(([, camo]) => camo.id);
|
||||
|
||||
let equalizerEntry = equalizerDefinitions.find(
|
||||
(line) => line[0] === `${nation}:${tankKey}`,
|
||||
);
|
||||
|
||||
let equalizer: Equalizer | undefined;
|
||||
|
||||
if (equalizerEntry) {
|
||||
const [health, penetration, module_health, damage, armor] =
|
||||
equalizerEntry?.slice(1).map(Number);
|
||||
equalizer = { health, penetration, module_health, damage, armor };
|
||||
}
|
||||
|
||||
tankDefinitions.tanks[tankId] = {
|
||||
ancestors: [],
|
||||
successors: [],
|
||||
id: tankId,
|
||||
dev_name: tankKey,
|
||||
roles: {},
|
||||
camouflages: camouflages,
|
||||
fixed_camouflage: fixedCamouflage,
|
||||
@@ -1011,10 +1054,10 @@ export async function definitions() {
|
||||
slug,
|
||||
nation,
|
||||
type: tankTags.includes("collectible")
|
||||
? TankType.COLLECTOR
|
||||
? TankType.TANK_TYPE_COLLECTOR
|
||||
: (typeof tank.price === "number" ? false : "gold" in tank.price)
|
||||
? TankType.PREMIUM
|
||||
: TankType.RESEARCHABLE,
|
||||
? TankType.TANK_TYPE_PREMIUM
|
||||
: TankType.TANK_TYPE_RESEARCHABLE,
|
||||
tier: tank.level,
|
||||
class: blitzTankClassToBlitzkit[tankTags[0] as BlitzTankClass],
|
||||
testing: tankTags.includes("testTank"),
|
||||
@@ -1022,21 +1065,22 @@ export async function definitions() {
|
||||
price: tankPrice,
|
||||
camouflage_still: tankDefinition.root.invisibility.still,
|
||||
camouflage_moving: tankDefinition.root.invisibility.moving,
|
||||
camouflage_onFire: tankDefinition.root.invisibility.firePenalty,
|
||||
camouflage_on_fire: tankDefinition.root.invisibility.firePenalty,
|
||||
turrets: [],
|
||||
engines: [],
|
||||
tracks: [],
|
||||
equalizer,
|
||||
};
|
||||
|
||||
if (tank.combatRole) {
|
||||
Object.entries(tank.combatRole).forEach(([gameMode, role]) => {
|
||||
const id = Object.entries(gameModeNativeNames).find(
|
||||
([key]) => key.toLowerCase() === gameMode.toLowerCase()
|
||||
([key]) => key.toLowerCase() === gameMode.toLowerCase(),
|
||||
)?.[1];
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error(
|
||||
`Unknown game mode in tank ${tankKey}: ${gameMode}`
|
||||
`Unknown game mode in tank ${tankKey}: ${gameMode}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1118,7 +1162,7 @@ export async function definitions() {
|
||||
(turretKey, turretIndex) => {
|
||||
const turret = tankDefinition.root.turrets0[turretKey];
|
||||
const turretModel = Number(
|
||||
parsePath(turret.models.undamaged).name.split("_")[1]
|
||||
parsePath(turret.models.undamaged).name.split("_")[1],
|
||||
);
|
||||
const turretId = toUniqueId(nation, turretList.root.ids[turretKey]);
|
||||
const turretYaw = (
|
||||
@@ -1150,13 +1194,7 @@ export async function definitions() {
|
||||
const armorId = parseInt(armorIdString);
|
||||
const armorRaw = turret.armor[name];
|
||||
|
||||
if (typeof armorRaw === "number") {
|
||||
turretArmor.thickness[armorId] = armorRaw;
|
||||
} else {
|
||||
if (!turretArmor.spaced) turretArmor.spaced = [];
|
||||
turretArmor.thickness[armorId] = armorRaw["#text"];
|
||||
turretArmor.spaced.push(armorId);
|
||||
}
|
||||
assignArmor(armorRaw, armorId, turretArmor);
|
||||
});
|
||||
|
||||
turret.userString;
|
||||
@@ -1186,12 +1224,12 @@ export async function definitions() {
|
||||
min: vector3TupleToBlitzkit(
|
||||
tankParameters.collision[
|
||||
parsePath(turret.hitTester.collisionModel).name.toLowerCase()
|
||||
].bbox.min
|
||||
].bbox.min,
|
||||
),
|
||||
max: vector3TupleToBlitzkit(
|
||||
tankParameters.collision[
|
||||
parsePath(turret.hitTester.collisionModel).name.toLowerCase()
|
||||
].bbox.max
|
||||
].bbox.max,
|
||||
),
|
||||
},
|
||||
};
|
||||
@@ -1209,7 +1247,7 @@ export async function definitions() {
|
||||
.split(" ")
|
||||
.map(Number) as [number, number];
|
||||
const gunModel = Number(
|
||||
parsePath(gun.models.undamaged).name.split("_")[1]
|
||||
parsePath(gun.models.undamaged).name.split("_")[1],
|
||||
);
|
||||
const gunName = getString(gunListEntry.userString);
|
||||
const gunType =
|
||||
@@ -1287,13 +1325,8 @@ export async function definitions() {
|
||||
}
|
||||
const armorId = parseInt(armorIdString);
|
||||
const armorRaw = gun.armor[name];
|
||||
if (typeof armorRaw === "number") {
|
||||
gunArmor.thickness[armorId] = armorRaw;
|
||||
} else {
|
||||
if (!gunArmor.spaced) gunArmor.spaced = [];
|
||||
gunArmor.thickness[armorId] = armorRaw["#text"];
|
||||
gunArmor.spaced.push(armorId);
|
||||
}
|
||||
|
||||
assignArmor(armorRaw, armorId, gunArmor);
|
||||
});
|
||||
|
||||
tankDefinitions.tanks[tankId].turrets[turretIndex].guns.push({
|
||||
@@ -1332,24 +1365,24 @@ export async function definitions() {
|
||||
},
|
||||
}
|
||||
: gunType === "autoReloader"
|
||||
? {
|
||||
$case: "auto_reloader",
|
||||
value: {
|
||||
intra_clip: 60 / gun.clip!.rate,
|
||||
shell_count: gunClipCount,
|
||||
shell_reloads: gun
|
||||
.pumpGunReloadTimes!.split(" ")
|
||||
.map(Number),
|
||||
? {
|
||||
$case: "auto_reloader",
|
||||
value: {
|
||||
intra_clip: 60 / gun.clip!.rate,
|
||||
shell_count: gunClipCount,
|
||||
shell_reloads: gun
|
||||
.pumpGunReloadTimes!.split(" ")
|
||||
.map(Number),
|
||||
},
|
||||
}
|
||||
: {
|
||||
$case: "auto_loader",
|
||||
value: {
|
||||
intra_clip: 60 / gun.clip!.rate,
|
||||
clip_reload: gun.reloadTime,
|
||||
shell_count: gunClipCount,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
$case: "auto_loader",
|
||||
value: {
|
||||
intra_clip: 60 / gun.clip!.rate,
|
||||
clip_reload: gun.reloadTime,
|
||||
shell_count: gunClipCount,
|
||||
},
|
||||
},
|
||||
} satisfies GunDefinition);
|
||||
|
||||
modelDefinitions.models[tankId].turrets[turretId].guns[gunId] = {
|
||||
@@ -1360,8 +1393,8 @@ export async function definitions() {
|
||||
gun.armor.gun === undefined
|
||||
? 0
|
||||
: typeof gun.armor.gun === "number"
|
||||
? gun.armor.gun
|
||||
: gun.armor.gun["#text"],
|
||||
? gun.armor.gun
|
||||
: gun.armor.gun["#text"],
|
||||
pitch: {
|
||||
min: gunPitch[0],
|
||||
max: gunPitch[1],
|
||||
@@ -1408,7 +1441,7 @@ export async function definitions() {
|
||||
type: blitzShellKindToBlitzkit[shell.kind],
|
||||
explosion_radius:
|
||||
shell.kind === "HIGH_EXPLOSIVE"
|
||||
? shell.explosionRadius ?? 0
|
||||
? (shell.explosionRadius ?? 0)
|
||||
: undefined,
|
||||
icon: shell.icon,
|
||||
penetration: {
|
||||
@@ -1419,7 +1452,7 @@ export async function definitions() {
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
totalUnlocks.forEach((unlocks) => {
|
||||
@@ -1447,7 +1480,7 @@ export async function definitions() {
|
||||
case "gun": {
|
||||
gunXps.set(
|
||||
toUniqueId(nation, gunList.root.ids[vehicle["#text"]]),
|
||||
parseResearchCost(vehicle.cost)
|
||||
parseResearchCost(vehicle.cost),
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1455,7 +1488,7 @@ export async function definitions() {
|
||||
case "turret": {
|
||||
turretXps.set(
|
||||
toUniqueId(nation, turretList.root.ids[vehicle["#text"]]),
|
||||
parseResearchCost(vehicle.cost)
|
||||
parseResearchCost(vehicle.cost),
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1463,7 +1496,7 @@ export async function definitions() {
|
||||
case "engine": {
|
||||
engineXps.set(
|
||||
toUniqueId(nation, enginesList.root.ids[vehicle["#text"]]),
|
||||
parseResearchCost(vehicle.cost)
|
||||
parseResearchCost(vehicle.cost),
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1471,7 +1504,7 @@ export async function definitions() {
|
||||
case "chassis": {
|
||||
trackXps.set(
|
||||
toUniqueId(nation, chassisList.root.ids[vehicle["#text"]]),
|
||||
parseResearchCost(vehicle.cost)
|
||||
parseResearchCost(vehicle.cost),
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1518,7 +1551,7 @@ export async function definitions() {
|
||||
name: getString(optionalDeviceEntry.userString),
|
||||
description: getString(optionalDeviceEntry.description),
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Object.entries(optionalDeviceSlots.root.presets).forEach(
|
||||
@@ -1537,7 +1570,7 @@ export async function definitions() {
|
||||
})
|
||||
.flat(),
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Object.entries(consumables).forEach(([key, consumable]) => {
|
||||
@@ -1620,7 +1653,7 @@ export async function definitions() {
|
||||
(item) =>
|
||||
blitzTankFilterDefinitionCategoryToBlitzkit[
|
||||
item as BlitzTankFilterDefinitionCategory
|
||||
]
|
||||
],
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -1718,7 +1751,7 @@ export async function definitions() {
|
||||
(item) =>
|
||||
blitzTankFilterDefinitionCategoryToBlitzkit[
|
||||
item as BlitzTankFilterDefinitionCategory
|
||||
]
|
||||
],
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -1749,11 +1782,11 @@ export async function definitions() {
|
||||
value.default_abilities.forEach((ability) => {
|
||||
if (ability in consumableNativeNames) {
|
||||
gameDefinitions.roles[value.id].consumables.push(
|
||||
consumableNativeNames[ability]
|
||||
consumableNativeNames[ability],
|
||||
);
|
||||
} else if (ability in provisionNativeNames) {
|
||||
gameDefinitions.roles[value.id].provisions.push(
|
||||
provisionNativeNames[ability]
|
||||
provisionNativeNames[ability],
|
||||
);
|
||||
} else throw new Error(`Unknown ability ${ability}`);
|
||||
});
|
||||
|
||||
@@ -17,15 +17,15 @@ export async function equipmentIcons() {
|
||||
|
||||
using uploader = new AssetUploader("equipment icons");
|
||||
const optionalDevices = await vfs.xml<{ root: OptionalDevices }>(
|
||||
`Data/XML/item_defs/vehicles/common/optional_devices.xml`
|
||||
`Data/XML/item_defs/vehicles/common/optional_devices.xml`,
|
||||
);
|
||||
const mappings = await vfs.yaml<Mappings>(
|
||||
`Data/UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml`
|
||||
`Data/UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml`,
|
||||
);
|
||||
const image = sharp(
|
||||
await vfs.file(
|
||||
`Data/Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp`
|
||||
)
|
||||
`Data/Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp`,
|
||||
),
|
||||
);
|
||||
|
||||
for (const optionalDeviceKey in optionalDevices.root) {
|
||||
@@ -35,8 +35,8 @@ export async function equipmentIcons() {
|
||||
|
||||
const mapping = mappings.StyleSheets.find((mapping) =>
|
||||
mapping.selectors.includes(
|
||||
`.optional_device_item.${optionalDevice.icon} #Img`
|
||||
)
|
||||
`.optional_device_item.${optionalDevice.icon} #Img`,
|
||||
),
|
||||
);
|
||||
|
||||
if (!mapping) {
|
||||
@@ -49,9 +49,7 @@ export async function equipmentIcons() {
|
||||
|
||||
if (configPath.startsWith("Gfx/Lobby")) {
|
||||
const configPathWebp = configPath.replace(".txt", "");
|
||||
const image = sharp(
|
||||
await vfs.file(`Data/${configPathWebp}.packed.webp`)
|
||||
);
|
||||
const image = sharp(await vfs.file(`Data/${configPathWebp}.packed.webp`));
|
||||
const content = await image.trim().toBuffer();
|
||||
|
||||
await uploader.add({
|
||||
|
||||
@@ -6,20 +6,32 @@ export async function flags() {
|
||||
console.log("Building flags...");
|
||||
|
||||
using uploader = new AssetUploader("flags");
|
||||
const circleFiles = vfs.dir(`Data/Gfx/Lobby/flags`).filter(
|
||||
(flag) =>
|
||||
flag.startsWith("flag_profile-stat_") &&
|
||||
!flag.endsWith("@2x.packed.webp")
|
||||
const circleFiles = await vfs
|
||||
.dir(`Data/Gfx/Lobby/flags`)
|
||||
.then((files) =>
|
||||
files.filter(
|
||||
(flag) =>
|
||||
flag.startsWith("flag_profile-stat_") &&
|
||||
!flag.endsWith("@2x.packed.webp"),
|
||||
),
|
||||
);
|
||||
const scratchedFiles = vfs.dir(`Data/Gfx/Lobby/flags`).filter(
|
||||
const scratchedFiles = await vfs
|
||||
.dir(`Data/Gfx/Lobby/flags`)
|
||||
.then((files) =>
|
||||
files.filter(
|
||||
(flag) =>
|
||||
flag.startsWith("flag_tutor-tank_") &&
|
||||
!flag.endsWith("@2x.packed.webp")
|
||||
)
|
||||
const fadedFiles = vfs.dir(`Data/Gfx/Lobby/flags`).filter(
|
||||
(flag) =>
|
||||
flag.startsWith("flag_filter_") && flag.endsWith("@2x.packed.webp")
|
||||
)
|
||||
!flag.endsWith("@2x.packed.webp"),
|
||||
),
|
||||
);
|
||||
const fadedFiles = await vfs
|
||||
.dir(`Data/Gfx/Lobby/flags`)
|
||||
.then((files) =>
|
||||
files.filter(
|
||||
(flag) =>
|
||||
flag.startsWith("flag_filter_") && flag.endsWith("@2x.packed.webp"),
|
||||
),
|
||||
);
|
||||
|
||||
for (const flag of circleFiles) {
|
||||
const image = sharp(await vfs.file(`Data/Gfx/Lobby/flags/${flag}`));
|
||||
@@ -43,9 +55,7 @@ export async function flags() {
|
||||
}
|
||||
|
||||
for (const flag of fadedFiles) {
|
||||
const content = await sharp(
|
||||
await vfs.file(`Data/Gfx/Lobby/flags/${flag}`)
|
||||
)
|
||||
const content = await sharp(await vfs.file(`Data/Gfx/Lobby/flags/${flag}`))
|
||||
.trim({
|
||||
threshold: 100,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
|
||||
@@ -8,35 +8,33 @@ export async function gameModeBanners() {
|
||||
|
||||
using uploader = new AssetUploader("game mode banners");
|
||||
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
|
||||
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`
|
||||
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`,
|
||||
);
|
||||
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
|
||||
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`
|
||||
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`,
|
||||
);
|
||||
const bannerMatches: { name: string; path: string }[] = [];
|
||||
|
||||
for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
|
||||
/eGameMode\.([a-zA-Z]+) -> "~res:([^"]+)"/g
|
||||
/eGameMode\.([a-zA-Z]+) -> "~res:([^"]+)"/g,
|
||||
)) {
|
||||
bannerMatches.push({ name: match[1], path: match[2] });
|
||||
}
|
||||
|
||||
for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
|
||||
/"(\d+)" -> "battleType\/([a-zA-Z]+)"/g
|
||||
/"(\d+)" -> "battleType\/([a-zA-Z]+)"/g,
|
||||
)) {
|
||||
const id = Number(match[1]);
|
||||
const name = match[2];
|
||||
let path = bannerMatches.find(
|
||||
(bannerMatch) => bannerMatch.name.toLowerCase() === name.toLowerCase()
|
||||
(bannerMatch) => bannerMatch.name.toLowerCase() === name.toLowerCase(),
|
||||
)?.path;
|
||||
|
||||
if (path === undefined) {
|
||||
path = `/Gfx/UI/Hangar/GameTypes/battle-type_${name.toLowerCase()}`;
|
||||
}
|
||||
|
||||
const content = await sharp(
|
||||
await vfs.file(`Data${path}.packed.webp`)
|
||||
)
|
||||
const content = await sharp(await vfs.file(`Data${path}.packed.webp`))
|
||||
.trim({ background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.toBuffer();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function glossary() {
|
||||
const glossary = await fetchGlossary(supported.blitz ?? supported.locale);
|
||||
|
||||
console.log(
|
||||
`Found ${Object.keys(glossary).length} things for ${supported.locale}`
|
||||
`Found ${Object.keys(glossary).length} things for ${supported.locale}`,
|
||||
);
|
||||
|
||||
for (const key in glossary) {
|
||||
@@ -30,7 +30,7 @@ export async function glossary() {
|
||||
}
|
||||
|
||||
if (key in avatars) {
|
||||
avatars[key].avatar.name.locales[supported.locale] =
|
||||
avatars[key].avatar.name!.locales[supported.locale] =
|
||||
glossaryEntry.title;
|
||||
} else {
|
||||
const extension = extname(glossaryEntry.image_url);
|
||||
@@ -45,7 +45,7 @@ export async function glossary() {
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const gallery: Gallery = { avatars: [] };
|
||||
@@ -53,12 +53,12 @@ export async function glossary() {
|
||||
|
||||
const bar = new ProgressBar(
|
||||
`Fetching avatars of ${totalCount} things :bar`,
|
||||
totalCount
|
||||
totalCount,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(avatars).map(async ([key, { url, avatar }]) => {
|
||||
if (!(locales.default in avatars[key].avatar.name.locales)) {
|
||||
if (!(locales.default in avatars[key].avatar.name!.locales)) {
|
||||
console.warn(`Avatar ${key} has no ${locales.default} name`);
|
||||
bar.tick();
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function glossary() {
|
||||
});
|
||||
|
||||
bar.tick();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await uploader.add({
|
||||
|
||||
@@ -8,11 +8,13 @@ export async function migration() {
|
||||
|
||||
using uploader = new AssetUploader("migration map");
|
||||
const map: Record<string, number> = {};
|
||||
const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common");
|
||||
const nations = await vfs
|
||||
.dir(`Data/XML/item_defs/vehicles`)
|
||||
.then((files) => files.filter((nation) => nation !== "common"));
|
||||
|
||||
for (const nation of nations) {
|
||||
const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
|
||||
for (const tankKey in tankList.root) {
|
||||
|
||||
@@ -6,15 +6,20 @@ export async function moduleIcons() {
|
||||
console.log("Building module icons...");
|
||||
|
||||
using uploader = new AssetUploader("module icons");
|
||||
const files = vfs.dir(`Data/Gfx/UI/ModulesTechTree`).filter(
|
||||
(file) => !file.endsWith("@2x.packed.webp") && file.startsWith("vehicle")
|
||||
)
|
||||
const files = await vfs
|
||||
.dir(`Data/Gfx/UI/ModulesTechTree`)
|
||||
.then((files) =>
|
||||
files.filter(
|
||||
(file) =>
|
||||
!file.endsWith("@2x.packed.webp") && file.startsWith("vehicle"),
|
||||
),
|
||||
);
|
||||
|
||||
for (const file of files) {
|
||||
const nameRaw = file.match(/vehicle(.+)\.packed\.webp/)![1];
|
||||
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
|
||||
const content = await sharp(
|
||||
await vfs.file(`Data/Gfx/UI/ModulesTechTree/${file}`)
|
||||
await vfs.file(`Data/Gfx/UI/ModulesTechTree/${file}`),
|
||||
)
|
||||
.trim()
|
||||
.toBuffer();
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function popularTanks() {
|
||||
const analytics = google.analyticsdata({ version: "v1beta", auth });
|
||||
const report = await analytics.properties.runReport({
|
||||
property: `properties/${assertSecret(
|
||||
import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
|
||||
import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID,
|
||||
)}`,
|
||||
requestBody: {
|
||||
dateRanges: [{ startDate: "7daysAgo", endDate: "today" }],
|
||||
@@ -47,13 +47,13 @@ export async function popularTanks() {
|
||||
row.dimensionValues[0].value &&
|
||||
row.dimensionValues[0].value !== "/tanks/" &&
|
||||
row.metricValues &&
|
||||
row.metricValues[0].value
|
||||
row.metricValues[0].value,
|
||||
)
|
||||
.map((row) => ({
|
||||
id: Number(
|
||||
row.dimensionValues![0].value!.match(
|
||||
/\/tools\/tankopedia\/(\d+)\/?/
|
||||
)?.[1]
|
||||
/\/tools\/tankopedia\/(\d+)\/?/,
|
||||
)?.[1],
|
||||
),
|
||||
views: Number(row.metricValues![0].value!),
|
||||
}))
|
||||
|
||||
@@ -8,19 +8,17 @@ export async function shellIcons() {
|
||||
using uploader = new AssetUploader("shell icons");
|
||||
const image = sharp(
|
||||
await vfs.file(
|
||||
`Data/Gfx/Shared/tank-supply/ammunition/big/texture0.packed.webp`
|
||||
)
|
||||
`Data/Gfx/Shared/tank-supply/ammunition/big/texture0.packed.webp`,
|
||||
),
|
||||
);
|
||||
const files = vfs.dir(
|
||||
`Data/Gfx/Shared/tank-supply/ammunition/big`
|
||||
).filter((file) => file.endsWith("_l.txt"))
|
||||
const files = await vfs
|
||||
.dir(`Data/Gfx/Shared/tank-supply/ammunition/big`)
|
||||
.then((files) => files.filter((file) => file.endsWith("_l.txt")));
|
||||
|
||||
for (const file of files) {
|
||||
const name = file.match(/(.+)_l\.txt/)![1];
|
||||
const sizes = (
|
||||
await vfs.text(
|
||||
`Data/Gfx/Shared/tank-supply/ammunition/big/${file}`
|
||||
)
|
||||
await vfs.text(`Data/Gfx/Shared/tank-supply/ammunition/big/${file}`)
|
||||
)
|
||||
.split("\n")[4]
|
||||
.split(" ")
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function skillIcons() {
|
||||
console.log("Building skill icons...");
|
||||
|
||||
const avatar = await vfs.xml<{ root: Avatar }>(
|
||||
`Data/XML/item_defs/tankmen/avatar.xml`
|
||||
`Data/XML/item_defs/tankmen/avatar.xml`,
|
||||
);
|
||||
const uploader = new AssetUploader("skill icons");
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ export async function tankArmors() {
|
||||
|
||||
using uploader = new AssetUploader("tank armors");
|
||||
const nodeIO = new NodeIO();
|
||||
const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common");
|
||||
const nations = await vfs
|
||||
.dir(`Data/XML/item_defs/vehicles`)
|
||||
.then((files) => files.filter((nation) => nation !== "common"));
|
||||
|
||||
for (const nation of nations) {
|
||||
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
|
||||
for (const tankKey in tanks.root) {
|
||||
|
||||
@@ -41,11 +41,13 @@ export async function tankIcons() {
|
||||
console.log("Building tank icons...");
|
||||
|
||||
using uploader = new AssetUploader("tank icons");
|
||||
const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common")
|
||||
const nations = await vfs
|
||||
.dir(`Data/XML/item_defs/vehicles`)
|
||||
.then((files) => files.filter((nation) => nation !== "common"));
|
||||
|
||||
for (const nation of nations) {
|
||||
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
|
||||
for (const tankKey in tanks.root) {
|
||||
@@ -57,7 +59,7 @@ export async function tankIcons() {
|
||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||
|
||||
const parameters = await vfs.yaml<TankParameters>(
|
||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
|
||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
const smallPath = `Data/${parameters.resourcesPath.smallIconPath
|
||||
.replace(/~res:\//, "")
|
||||
|
||||
@@ -12,9 +12,9 @@ export async function tankModels() {
|
||||
|
||||
using uploader = new AssetUploader("tank models");
|
||||
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||
const nations = vfs
|
||||
const nations = await vfs
|
||||
.dir(`Data/XML/item_defs/vehicles`)
|
||||
.filter((nation) => nation !== "common");
|
||||
.then((files) => files.filter((nation) => nation !== "common"));
|
||||
|
||||
for (const nationIndex in nations) {
|
||||
const nation = nations[nationIndex];
|
||||
@@ -41,4 +41,4 @@ export async function tankModels() {
|
||||
}
|
||||
|
||||
await uploader.flush();
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,7 @@ export async function extractModel(vfs: AbstractVFS, path: string) {
|
||||
break;
|
||||
|
||||
case "TransformComponent": {
|
||||
component["tc.localTranslation"] = [0, 0, 0]; // Game when is loading model resets position to [0, 0, 0], so any set value is ignored.
|
||||
node.setTranslation(component["tc.localTranslation"]);
|
||||
node.setRotation(component["tc.localRotation"]);
|
||||
node.setScale(component["tc.localScale"]);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { vfs } from "../../../buildAssets/constants";
|
||||
|
||||
export async function readTexture(path: string) {
|
||||
const ddsTexturePath = path.replace(".tex", ".dx11.dds");
|
||||
const isDds = vfs.resolve(ddsTexturePath);
|
||||
const isDds = await vfs.resolve(ddsTexturePath);
|
||||
const resolvedTexturePath = isDds
|
||||
? ddsTexturePath
|
||||
: ddsTexturePath.replace(".dds", ".pvr");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { crc32 } from 'crc';
|
||||
import { compressBlock } from 'lz4js';
|
||||
import { crc32 } from "crc";
|
||||
import { compressBlock } from "lz4js";
|
||||
|
||||
/**
|
||||
* Thanks Maddoxkkm! Modernized heavily.
|
||||
@@ -14,7 +14,7 @@ export function writeDVPL(buffer: Buffer) {
|
||||
destination,
|
||||
0,
|
||||
source.length,
|
||||
0,
|
||||
[0],
|
||||
);
|
||||
let output = Buffer.from(destination);
|
||||
|
||||
@@ -58,7 +58,7 @@ function toDVPLFooter(
|
||||
result.writeUInt32LE(compressedSize, 4);
|
||||
result.writeUInt32LE(crc32, 8);
|
||||
result.writeUInt32LE(type, 12);
|
||||
result.write('DVPL', 16, 4, 'utf8');
|
||||
result.write("DVPL", 16, 4, "utf8");
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export class AssetUploader {
|
||||
|
||||
constructor(public message: string) {
|
||||
[this.owner, this.repo] = assertSecret(
|
||||
import.meta.env.PUBLIC_ASSET_REPO
|
||||
import.meta.env.PUBLIC_ASSET_REPO,
|
||||
).split("/");
|
||||
this.branch = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export class AssetUploader {
|
||||
const blobPath = `${this.owner}/${this.repo}/${this.branch}/${change.path}`;
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/${blobPath}`,
|
||||
{ headers: { "Accept-Encoding": "identity" } }
|
||||
{ headers: { "Accept-Encoding": "identity" } },
|
||||
);
|
||||
let diff: { change: number; status: DiffStatus };
|
||||
|
||||
@@ -71,7 +71,7 @@ export class AssetUploader {
|
||||
} else if (response.status === 200) {
|
||||
if (
|
||||
this.heuristicFormats.some((format) =>
|
||||
change.path.endsWith(`.${format}`)
|
||||
change.path.endsWith(`.${format}`),
|
||||
)
|
||||
) {
|
||||
/**
|
||||
@@ -94,7 +94,7 @@ export class AssetUploader {
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unexpected status code ${response.status} for ${change.path}`
|
||||
`Unexpected status code ${response.status} for ${change.path}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export class AssetUploader {
|
||||
console.log(
|
||||
`${diff.status === DiffStatus.New ? "🟢" : "🟡"} (${
|
||||
diff.change > 0 ? "+" : ""
|
||||
}${diff.change.toLocaleString()}B) ${change.path}`
|
||||
}${diff.change.toLocaleString()}B) ${change.path}`,
|
||||
);
|
||||
|
||||
// flush BEFORE going over limit
|
||||
@@ -160,8 +160,8 @@ export class AssetUploader {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(
|
||||
resolve,
|
||||
Math.max(0, this.time_between_blobs + (start - end))
|
||||
)
|
||||
Math.max(0, this.time_between_blobs + (start - end)),
|
||||
),
|
||||
);
|
||||
|
||||
blobs.push({ sha, path: change.path, mode: "100644", type: "blob" });
|
||||
@@ -170,11 +170,11 @@ export class AssetUploader {
|
||||
break;
|
||||
} catch {
|
||||
console.warn(
|
||||
` failed ${change.path}; retrying in ${this.time_per_blob}ms...`
|
||||
` failed ${change.path}; retrying in ${this.time_per_blob}ms...`,
|
||||
);
|
||||
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, this.time_per_blob)
|
||||
setTimeout(resolve, this.time_per_blob),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -209,7 +209,7 @@ export class AssetUploader {
|
||||
[Symbol.dispose]() {
|
||||
if (this.changes.length > 0) {
|
||||
throw new Error(
|
||||
`${this.changes.length} uncommitted changes. Did you forget to flush?`
|
||||
`${this.changes.length} uncommitted changes. Did you forget to flush?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { readDVPL } from "@blitzkit/core";
|
||||
import { Options, parse as parseCSV } from "csv-parse/sync";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
import { normalize } from "path/posix";
|
||||
import { parse } from "yaml";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
export abstract class AbstractVFS {
|
||||
textDecoder = new TextDecoder();
|
||||
@@ -11,24 +12,22 @@ export abstract class AbstractVFS {
|
||||
|
||||
abstract dispose(): void;
|
||||
|
||||
abstract has(path: string): boolean;
|
||||
abstract has(path: string): Promise<boolean>;
|
||||
|
||||
abstract raw(path: string): Promise<Uint8Array>;
|
||||
|
||||
abstract paths(): string[];
|
||||
|
||||
resolve(path: string) {
|
||||
async resolve(path: string) {
|
||||
const normalized = normalize(path);
|
||||
if (this.has(normalized)) return normalized;
|
||||
if (await this.has(normalized)) return normalized;
|
||||
|
||||
const dvplPath = `${normalized}.dvpl`;
|
||||
if (this.has(dvplPath)) return dvplPath;
|
||||
if (await this.has(dvplPath)) return dvplPath;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
assert(path: string) {
|
||||
const resolved = this.resolve(path);
|
||||
async assert(path: string) {
|
||||
const resolved = await this.resolve(path);
|
||||
|
||||
if (resolved === null) throw new Error(`File not found: ${path}`);
|
||||
|
||||
@@ -36,7 +35,7 @@ export abstract class AbstractVFS {
|
||||
}
|
||||
|
||||
async file(file: string) {
|
||||
const resolved = this.assert(file);
|
||||
const resolved = await this.assert(file);
|
||||
const raw = await this.raw(resolved);
|
||||
let buffer = raw;
|
||||
|
||||
@@ -47,21 +46,7 @@ export abstract class AbstractVFS {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
dir(path: string) {
|
||||
const parentSegments = path.split("/").length;
|
||||
const children = new Set<string>();
|
||||
|
||||
for (const child of this.paths()) {
|
||||
if (!child.startsWith(path) || child === path) continue;
|
||||
|
||||
const childSegments = child.split("/");
|
||||
const nextSegment = childSegments[parentSegments];
|
||||
|
||||
children.add(nextSegment);
|
||||
}
|
||||
|
||||
return Array.from(children);
|
||||
}
|
||||
abstract dir(path: string): Promise<string[]>;
|
||||
|
||||
async text(path: string) {
|
||||
const file = await this.file(path);
|
||||
@@ -70,7 +55,7 @@ export abstract class AbstractVFS {
|
||||
|
||||
async yaml<Type>(path: string) {
|
||||
const file = await this.text(path);
|
||||
return parse(file) as Type;
|
||||
return parseYaml(file) as Type;
|
||||
}
|
||||
|
||||
async xml<Type>(path: string) {
|
||||
@@ -78,6 +63,16 @@ export abstract class AbstractVFS {
|
||||
return this.xmlParser.parse(file) as Type;
|
||||
}
|
||||
|
||||
async _csv(text: string, options: Options = {}) {
|
||||
const parsed = parseCSV(text, options);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async csv(path: string, options: Options = {}) {
|
||||
const text = await this.text(path);
|
||||
return await this._csv(text, options);
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { AbstractVFS } from "./abstract";
|
||||
|
||||
export class LocalVFS extends AbstractVFS {
|
||||
constructor(private base: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
async init() {
|
||||
return this;
|
||||
}
|
||||
|
||||
async has(path: string) {
|
||||
const file = Bun.file(`${this.base}/${path}`);
|
||||
return await file.exists();
|
||||
}
|
||||
|
||||
async raw(path: string) {
|
||||
const file = Bun.file(`${this.base}/${path}`);
|
||||
const buffer = await file.arrayBuffer();
|
||||
const array = new Uint8Array(buffer);
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
async dir(path: string) {
|
||||
return await readdir(`${this.base}/${path}`);
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export class SteamVFS extends AbstractVFS {
|
||||
private username: string,
|
||||
private password: string,
|
||||
private app: number,
|
||||
private depot: number
|
||||
private depot: number,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -57,13 +57,14 @@ export class SteamVFS extends AbstractVFS {
|
||||
this.steam.getManifest(
|
||||
this.app,
|
||||
this.depot,
|
||||
// @ts-expect-error
|
||||
productInfo.apps[this.app].appinfo.depots[this.depot].manifests.public
|
||||
.gid,
|
||||
"public",
|
||||
// @ts-expect-error
|
||||
(_, response) => {
|
||||
resolve(response);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -75,7 +76,7 @@ export class SteamVFS extends AbstractVFS {
|
||||
return this;
|
||||
}
|
||||
|
||||
has(path: string) {
|
||||
async has(path: string) {
|
||||
return this.manifest.has(path);
|
||||
}
|
||||
|
||||
@@ -88,8 +89,20 @@ export class SteamVFS extends AbstractVFS {
|
||||
return new Uint8Array(downloaded.file);
|
||||
}
|
||||
|
||||
paths() {
|
||||
return Array.from(this.manifest.keys());
|
||||
async dir(path: string) {
|
||||
const parentSegments = path.split("/").length;
|
||||
const children = new Set<string>();
|
||||
|
||||
for (const child of this.manifest.keys()) {
|
||||
if (!child.startsWith(path) || child === path) continue;
|
||||
|
||||
const childSegments = child.split("/");
|
||||
const nextSegment = childSegments[parentSegments];
|
||||
|
||||
children.add(nextSegment);
|
||||
}
|
||||
|
||||
return Array.from(children);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
@@ -5,7 +5,10 @@ import { AbstractVFS } from "./abstract";
|
||||
export class ZipVFS extends AbstractVFS {
|
||||
entries = new Map<string, ZipEntry>();
|
||||
|
||||
constructor(private url: string, private root = "app/") {
|
||||
constructor(
|
||||
private url: string,
|
||||
private root = "app/",
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -21,7 +24,7 @@ export class ZipVFS extends AbstractVFS {
|
||||
return this;
|
||||
}
|
||||
|
||||
has(path: string) {
|
||||
async has(path: string) {
|
||||
return this.entries.has(path);
|
||||
}
|
||||
|
||||
@@ -30,8 +33,20 @@ export class ZipVFS extends AbstractVFS {
|
||||
return new Uint8Array(arrayBuffer);
|
||||
}
|
||||
|
||||
paths() {
|
||||
return Array.from(this.entries.keys());
|
||||
async dir(path: string) {
|
||||
const parentSegments = path.split("/").length;
|
||||
const children = new Set<string>();
|
||||
|
||||
for (const child of this.entries.keys()) {
|
||||
if (!child.startsWith(path) || child === path) continue;
|
||||
|
||||
const childSegments = child.split("/");
|
||||
const nextSegment = childSegments[parentSegments];
|
||||
|
||||
children.add(nextSegment);
|
||||
}
|
||||
|
||||
return Array.from(children);
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
|
||||
@@ -53,7 +53,7 @@ if (newTanks.length > 0) {
|
||||
notes += "## New Tanks\n\n";
|
||||
|
||||
for (const tank of newTanks) {
|
||||
notes += `### [${tank.name.locales.en}](${domain}/tanks/${tank.slug})\n`;
|
||||
notes += `### [${tank.name!.locales.en}](${domain}/tanks/${tank.slug})\n`;
|
||||
notes += `})\n\n`;
|
||||
}
|
||||
|
||||
@@ -96,49 +96,49 @@ if (changedTanks.length > 0) {
|
||||
const previewTankModel = previewModelDefinitions.models[preview.id];
|
||||
|
||||
const currentConsumables = Object.values(
|
||||
currentConsumableDefinitions.consumables
|
||||
currentConsumableDefinitions.consumables,
|
||||
).filter((consumable) =>
|
||||
checkConsumableProvisionInclusivity(consumable, current, currentGun)
|
||||
checkConsumableProvisionInclusivity(consumable, current, currentGun),
|
||||
);
|
||||
const previewConsumables = Object.values(
|
||||
previewConsumableDefinitions.consumables
|
||||
previewConsumableDefinitions.consumables,
|
||||
).filter((consumable) =>
|
||||
checkConsumableProvisionInclusivity(consumable, preview, previewGun)
|
||||
checkConsumableProvisionInclusivity(consumable, preview, previewGun),
|
||||
);
|
||||
const addedConsumables = previewConsumables.filter(
|
||||
(previewConsumable) =>
|
||||
!currentConsumables.some(
|
||||
(currentConsumable) => currentConsumable.id === previewConsumable.id
|
||||
)
|
||||
(currentConsumable) => currentConsumable.id === previewConsumable.id,
|
||||
),
|
||||
);
|
||||
const removedConsumables = currentConsumables.filter(
|
||||
(currentConsumable) =>
|
||||
!previewConsumables.some(
|
||||
(previewConsumable) => previewConsumable.id === currentConsumable.id
|
||||
)
|
||||
(previewConsumable) => previewConsumable.id === currentConsumable.id,
|
||||
),
|
||||
);
|
||||
|
||||
const currentProvisions = availableProvisions(
|
||||
current,
|
||||
currentGun,
|
||||
currentProvisionDefinitions
|
||||
currentProvisionDefinitions,
|
||||
);
|
||||
const previewProvisions = availableProvisions(
|
||||
preview,
|
||||
previewGun,
|
||||
previewProvisionDefinitions
|
||||
previewProvisionDefinitions,
|
||||
);
|
||||
const addedProvisions = previewProvisions.filter(
|
||||
(previewProvision) =>
|
||||
!currentProvisions.some(
|
||||
(currentProvision) => currentProvision.id === previewProvision.id
|
||||
)
|
||||
(currentProvision) => currentProvision.id === previewProvision.id,
|
||||
),
|
||||
);
|
||||
const removedProvisions = currentProvisions.filter(
|
||||
(currentProvision) =>
|
||||
!previewProvisions.some(
|
||||
(previewProvision) => previewProvision.id === currentProvision.id
|
||||
)
|
||||
(previewProvision) => previewProvision.id === currentProvision.id,
|
||||
),
|
||||
);
|
||||
|
||||
const currentEquipment = currentEquipmentDefinitions.presets[
|
||||
@@ -155,19 +155,20 @@ if (changedTanks.length > 0) {
|
||||
.filter(
|
||||
(previewEquipment) =>
|
||||
!currentEquipment.some(
|
||||
(currentEquipment) => currentEquipment === previewEquipment
|
||||
)
|
||||
(currentEquipment) => currentEquipment === previewEquipment,
|
||||
),
|
||||
)
|
||||
.map((id) => previewEquipmentDefinitions.equipments[id]);
|
||||
const removedEquipment = currentEquipment
|
||||
.filter(
|
||||
(currentEquipment) =>
|
||||
!previewEquipment.some(
|
||||
(previewEquipment) => previewEquipment === currentEquipment
|
||||
)
|
||||
(previewEquipment) => previewEquipment === currentEquipment,
|
||||
),
|
||||
)
|
||||
.map((id) => currentEquipmentDefinitions.equipments[id]);
|
||||
|
||||
const equalize = false;
|
||||
const currentCharacteristics = tankCharacteristics(
|
||||
{
|
||||
tank: current,
|
||||
@@ -184,7 +185,7 @@ if (changedTanks.length > 0) {
|
||||
provisions: createDefaultProvisions(
|
||||
current,
|
||||
currentGun,
|
||||
currentProvisionDefinitions
|
||||
currentProvisionDefinitions,
|
||||
),
|
||||
equipmentMatrix: genericDefaultEquipmentMatrix,
|
||||
stockEngine: current.engines[0],
|
||||
@@ -193,12 +194,13 @@ if (changedTanks.length > 0) {
|
||||
stockTurret: current.turrets[0],
|
||||
track: currentTracks,
|
||||
turret: currentTurret,
|
||||
equalize,
|
||||
},
|
||||
{
|
||||
equipmentDefinitions: currentEquipmentDefinitions,
|
||||
provisionDefinitions: currentProvisionDefinitions,
|
||||
tankModelDefinition: currentTankModel,
|
||||
}
|
||||
},
|
||||
);
|
||||
const previewCharacteristics = tankCharacteristics(
|
||||
{
|
||||
@@ -216,7 +218,7 @@ if (changedTanks.length > 0) {
|
||||
provisions: createDefaultProvisions(
|
||||
preview,
|
||||
previewGun,
|
||||
previewProvisionDefinitions
|
||||
previewProvisionDefinitions,
|
||||
),
|
||||
equipmentMatrix: genericDefaultEquipmentMatrix,
|
||||
stockEngine: preview.engines[0],
|
||||
@@ -225,12 +227,13 @@ if (changedTanks.length > 0) {
|
||||
stockTurret: preview.turrets[0],
|
||||
track: previewTracks,
|
||||
turret: previewTurret,
|
||||
equalize,
|
||||
},
|
||||
{
|
||||
equipmentDefinitions: previewEquipmentDefinitions,
|
||||
provisionDefinitions: previewProvisionDefinitions,
|
||||
tankModelDefinition: previewTankModel,
|
||||
}
|
||||
},
|
||||
);
|
||||
const changedCharacteristics: string[] = [];
|
||||
|
||||
@@ -244,17 +247,17 @@ if (changedTanks.length > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
notes += `### [${preview.name.locales.en}](${domain}/tanks/${preview.slug})\n`;
|
||||
notes += `### [${preview.name!.locales.en}](${domain}/tanks/${preview.slug})\n`;
|
||||
notes += `})\n\n`;
|
||||
|
||||
if (addedConsumables.length + removedConsumables.length > 0) {
|
||||
notes += `Consumables: `;
|
||||
notes += [
|
||||
...addedConsumables.map(
|
||||
(consumable) => `🟢 ${consumable.name.locales.en}`
|
||||
(consumable) => `🟢 ${consumable.name!.locales.en}`,
|
||||
),
|
||||
...removedConsumables.map(
|
||||
(consumable) => `🔴 ${consumable.name.locales.en}`
|
||||
(consumable) => `🔴 ${consumable.name!.locales.en}`,
|
||||
),
|
||||
].join(", ");
|
||||
notes += `\n`;
|
||||
@@ -264,10 +267,10 @@ if (changedTanks.length > 0) {
|
||||
notes += `Provisions: `;
|
||||
notes += [
|
||||
...addedProvisions.map(
|
||||
(provision) => `🟢 ${provision.name.locales.en}`
|
||||
(provision) => `🟢 ${provision.name!.locales.en}`,
|
||||
),
|
||||
...removedProvisions.map(
|
||||
(provision) => `🔴 ${provision.name.locales.en}`
|
||||
(provision) => `🔴 ${provision.name!.locales.en}`,
|
||||
),
|
||||
].join(", ");
|
||||
notes += `\n`;
|
||||
@@ -276,9 +279,11 @@ if (changedTanks.length > 0) {
|
||||
if (addedEquipment.length + removedEquipment.length > 0) {
|
||||
notes += `-Equipment: `;
|
||||
notes += [
|
||||
...addedEquipment.map((equipment) => `🟢 ${equipment.name.locales.en}`),
|
||||
...addedEquipment.map(
|
||||
(equipment) => `🟢 ${equipment.name!.locales.en}`,
|
||||
),
|
||||
...removedEquipment.map(
|
||||
(equipment) => `🔴 ${equipment.name.locales.en}`
|
||||
(equipment) => `🔴 ${equipment.name!.locales.en}`,
|
||||
),
|
||||
].join(", ");
|
||||
notes += `\n`;
|
||||
|
||||
@@ -26,8 +26,8 @@ for (const location of TS_PROTO_EXECUTABLE_LOCATIONS) {
|
||||
if (!tsProtoExecutableLocation) {
|
||||
throw new Error(
|
||||
`Could not find ts-proto executable. Checked locations: ${TS_PROTO_EXECUTABLE_LOCATIONS.join(
|
||||
", "
|
||||
)}`
|
||||
", ",
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Video,
|
||||
youtubers,
|
||||
} from "@blitzkit/core";
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import { google } from "googleapis";
|
||||
import { cloneDeep, uniqBy } from "lodash-es";
|
||||
import { AssetUploader } from "./core/github/assetUploader";
|
||||
@@ -42,9 +43,11 @@ for (const tank of tanksSanitized) {
|
||||
}
|
||||
|
||||
try {
|
||||
const q = `World of Tanks Blitz ${tank.name!.locales[locales.default]}`;
|
||||
|
||||
const results = await youtube.search.list({
|
||||
part: ["snippet"],
|
||||
q: `World of Tanks Blitz ${tank.name}`,
|
||||
q,
|
||||
});
|
||||
|
||||
if (!results.data.items || results.data.items.length === 0) continue;
|
||||
@@ -54,9 +57,9 @@ for (const tank of tanksSanitized) {
|
||||
(item) =>
|
||||
item.snippet?.channelId &&
|
||||
item.id?.videoId &&
|
||||
youtubers.some(({ id }) => id === item.snippet?.channelId)
|
||||
youtubers.some(({ id }) => id === item.snippet?.channelId),
|
||||
),
|
||||
(item) => item.snippet!.channelId
|
||||
(item) => item.snippet!.channelId,
|
||||
);
|
||||
|
||||
reviews.reviews[tank.id] = {
|
||||
@@ -66,11 +69,13 @@ for (const tank of tanksSanitized) {
|
||||
({
|
||||
id: item.id!.videoId!,
|
||||
author: item.snippet!.channelId!,
|
||||
} satisfies Video)
|
||||
}) satisfies Video,
|
||||
),
|
||||
};
|
||||
|
||||
console.log(`${++done} done; found ${items.length} for ${tank.name.locales.en}`);
|
||||
console.log(
|
||||
`${++done} done; found ${items.length} for ${tank.name!.locales.en}`,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 / 5));
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
name;maxHealthFactor;piercingPowerFactor;moduleHealthFactor;damageFactor;armorFactor
|
||||
china:Ch01_Type59;1.3;1.33;1.5;1.29;1.35
|
||||
china:Ch02_Type62;1.5;1.6;1.5;1.27;1.61
|
||||
china:Ch03_WZ-111;1.4;1.17;1.4;1.3;1.35
|
||||
china:Ch04_T34_1;1.5;1.54;1.4;1.3;1.61
|
||||
china:Ch05_T34_2;1.3;1.33;1.5;1.29;1.34
|
||||
china:Ch07_Vickers_MkE_Type_BT26;5.5;5.07;3.3;7.05;8.8
|
||||
china:Ch08_Type97_Chi_Ha;3.7;3.71;2.2;3.13;5.56
|
||||
china:Ch09_M5;2.9;2.91;1.9;2.54;3.47
|
||||
china:Ch10_IS2;1.6;1.44;1.4;1.33;1.61
|
||||
china:Ch11_110;1.4;1.17;1.4;1.29;1.35
|
||||
china:Ch12_111_1_2_3;1.2;1.02;1.4;1.17;1.14
|
||||
china:Ch14_T34_3;1.3;1.37;1.5;1.13;1.17
|
||||
china:Ch15_59_16;1.9;1.77;1.6;1.57;2.24
|
||||
china:Ch15_59_16_BP;1.9;1.7;1.6;1.56;2.24
|
||||
china:Ch16_WZ_131;1.6;1.52;1.5;1.28;1.61
|
||||
china:Ch17_WZ131_1_WZ132;1.4;1.22;1.5;1.29;1.34
|
||||
china:Ch18_WZ-120;1.1;1.09;1.4;1.11;1.17
|
||||
china:Ch20_Type58;1.9;1.88;1.5;1.57;1.86
|
||||
china:Ch21_T34;2.7;2.1;1.7;1.97;2.12
|
||||
china:Ch23_112;1.4;1.17;1.4;1.31;1.35
|
||||
china:Ch24_Type64;1.9;1.86;1.6;1.57;2.35
|
||||
china:Ch26_59_Patton;1.3;1.33;1.5;1.1;1.16
|
||||
china:Ch28_WZ_132A;1.1;1.04;1.5;1.11;1.24
|
||||
china:Ch34_WZ-131G_FT;2.1;1.57;1.4;1.51;1.73
|
||||
china:Ch34_WZ-131G_FT_BP;2.1;1.57;1.4;1.51;2.02
|
||||
china:Ch35_T-34-2G_FT;1.8;1.48;1.4;1.25;1.61
|
||||
china:Ch36_WZ-111-1G_FT;1.6;1.16;1.4;1.25;1.35
|
||||
china:Ch37_WZ-111G_FT;1.2;1.05;1.4;1.15;1.13
|
||||
china:Ch39_WZ120_1G_FT;1.6;1.16;1.4;1.24;1.35
|
||||
china:Ch40_WZ120G_FT;1.1;1.05;1.4;1.08;1.13
|
||||
china:Ch42_M41D;1.7;1.49;1.5;1.24;1.61
|
||||
china:Ch43_WZ-122_2;1.3;1.21;1.5;1.31;1.35
|
||||
china:Ch45_WZ_114;1.2;1.02;1.4;1.19;1.13
|
||||
china:Ch47_BZ_176;1.2;1.03;1.4;1.17;1.13
|
||||
china:Ch49_BZ_68;1.2;1.02;1.4;1.2;1.16
|
||||
china:Ch50_BZ_166;1.4;1.17;1.4;1.3;1.35
|
||||
china:Ch51_BZ_58;1.6;1.46;1.4;1.29;1.62
|
||||
china:Ch56_BZ_74_1;1.2;1.02;1.4;1.16;1.14
|
||||
china:Ch59_BZ58_2;1.2;1.02;1.4;1.19;1.13
|
||||
china:Ch60_Type59_spc;1.3;1.37;1.5;1.32;1.35
|
||||
china:Ch_LT_vz38;4.3;4.29;2.7;3.99;4.88
|
||||
china:Ch_WZ-112v2;1.4;1.17;1.4;1.31;1.35
|
||||
china:Type_58BP;1.9;1.88;1.5;1.57;2.04
|
||||
european:Cz02_T50;1.1;1.03;1.4;1.12;1.19
|
||||
european:Cz05_T34-100;1.5;1.45;1.4;1.29;1.61
|
||||
european:Cz07_TVP_46;1.3;1.12;1.5;1.29;1.35
|
||||
european:Cz08_T-25;1.9;1.83;1.5;1.56;2.02
|
||||
european:Cz08_T-25BP;1.9;1.83;1.5;1.55;2.02
|
||||
european:Cz08_T-25BPS;1.9;1.83;1.5;1.57;2.02
|
||||
european:Cz13_T-27;1.1;1.07;1.4;1.12;1.19
|
||||
european:Cz14_Skoda_T-56;1.4;1.17;1.4;1.33;1.35
|
||||
european:Cz15_Skoda_T-45;1.7;1.44;1.4;1.27;1.61
|
||||
european:Cz16_TNH_T_vz51;1.2;1.02;1.4;1.17;1.15
|
||||
european:Cz18_TNH_105_1000;1.4;1.16;1.4;1.28;1.35
|
||||
european:Cz19_Vz_44_1;1.7;1.43;1.4;1.3;1.61
|
||||
european:Cz20_ShPTK_TVP_100;1.5;1.16;1.4;1.24;1.34
|
||||
european:Cz29_Skoda_P-JS;2.2;1.67;1.4;1.56;1.78
|
||||
european:Cz29_Skoda_P-JS_BP;2.2;1.67;1.4;1.57;1.79
|
||||
european:F01_Vickers_MkF;5.5;5.07;3.3;6.16;8.8
|
||||
european:It09_P43_ter;1.5;1.54;1.4;1.3;1.61
|
||||
european:It10_P43_BD;1.9;1.74;1.5;1.57;1.82
|
||||
european:It10_P43_bis;1.9;1.74;1.5;1.57;1.82
|
||||
european:It10_P43_bis_BP;1.9;1.7;1.5;1.58;1.82
|
||||
european:It116_Carro_d_assalto_P88;1.6;1.33;1.4;1.28;1.61
|
||||
european:It117_ProgettoCC55mod54;1.3;1.13;1.4;1.29;1.35
|
||||
european:It119_ProgettoC50mod66;1.1;1.02;1.4;1.18;1.13
|
||||
european:It12_Prototipo_Standard_B;1.1;1.04;1.4;1.14;1.17
|
||||
european:It13_Progetto_M35_mod_46;1.3;1.33;1.5;1.27;1.35
|
||||
european:It14_P44_Pantera;1.3;1.21;1.5;1.26;1.35
|
||||
european:It15_Semovente_M41;3.2;1.98;1.5;1.97;2.13
|
||||
european:It16_Bassoto;2;1.59;1.4;1.53;1.8
|
||||
european:It17_Semovente_mod_56;1.7;1.4;1.4;1.28;1.62
|
||||
european:It18_Semovente_mod_64;1.4;1.2;1.4;1.24;1.35
|
||||
european:It19_Controcarro_1MK2_Ciclope;1.1;1.05;1.4;1.19;1.13
|
||||
european:It22_Progetto_C45_mod_71;1.3;1.14;1.4;1.23;1.35
|
||||
european:It30_CC_mod_64_Prem;1.4;1.17;1.4;1.25;1.35
|
||||
european:Pl03_PzV_Pudel;1.9;1.57;1.5;1.56;1.92
|
||||
european:Pl05_50TP_Tyszkiewicza;1.2;1.02;1.4;1.18;1.13
|
||||
european:Pl06_10TP;4.3;4.29;2.7;4.28;7.66
|
||||
european:Pl07_14TP;3.6;3.57;2.1;3.12;5.56
|
||||
european:Pl08_50TP_prot;1.2;1.02;1.4;1.17;1.14
|
||||
european:Pl10_40TP_Habicha;2.1;1.64;1.4;1.57;1.83
|
||||
european:Pl119_CS_52_LIS;1.5;1.56;1.4;1.31;1.61
|
||||
european:Pl11_45TP_Habicha;1.6;1.44;1.4;1.3;1.61
|
||||
european:Pl13_53TP_Markowskiego;1.4;1.14;1.4;1.32;1.35
|
||||
european:Pl17_DS_PZlnz;2.6;2.28;1.7;1.95;2.12
|
||||
european:Pl18_BUGI;1.9;1.57;1.5;1.58;1.94
|
||||
european:Pl20_CS_44;1.5;1.54;1.4;1.3;1.61
|
||||
european:Pl22_CS_59;1.1;1.04;1.4;1.1;1.21
|
||||
european:Pl23_CS_53;1.3;1.31;1.5;1.29;1.35
|
||||
european:Pl24_56TP;1.4;1.15;1.4;1.29;1.35
|
||||
european:Pl27_SDP_Wz_66T;1.5;1.17;1.4;1.27;1.35
|
||||
european:Pl28_SDP_Wz_60;1.1;1.05;1.4;1.11;1.12
|
||||
european:Pl30_SDP_Wz58T;1.4;1.16;1.4;1.26;1.35
|
||||
european:Pl31_SDP_Wz_57D;1.7;1.43;1.4;1.28;1.62
|
||||
european:Pl32_SDP_Wz_44;2;1.57;1.4;1.52;1.76
|
||||
european:Pl33_SDP_Wz_40;3.2;2.13;1.5;1.96;2.13
|
||||
european:S02_StrvM42;2.7;2.22;1.7;1.95;2.11
|
||||
european:S04_Lago-I;3.1;2.69;1.9;2.54;2.32
|
||||
european:S07_Strv74BP;1.9;1.67;1.5;1.55;1.9
|
||||
european:S07_Strv_74;1.9;1.67;1.5;1.55;1.9
|
||||
european:S13_Leo;1.5;1.58;1.4;1.3;1.61
|
||||
european:S13_Leo_NY;1.5;1.58;1.4;1.3;1.61
|
||||
european:S17_EMIL_1952E2;1.2;1.02;1.4;1.16;1.12
|
||||
european:S18_EMIL_1951E1;1.3;1.16;1.4;1.29;1.35
|
||||
european:S23_Strv_81;1.3;1.14;1.5;1.26;1.35
|
||||
european:S25_EMIL_51;1.3;1.12;1.4;1.29;1.35
|
||||
european:S26_Lancen_C;1.3;1.22;1.5;1.31;1.35
|
||||
european:S32_Bofors_Tornvagn;1.1;1.02;1.4;1.17;1.12
|
||||
france:AMX38;4.4;4.29;2.7;4.15;5.95
|
||||
france:AMX40;3;2.88;1.9;2.52;3.53
|
||||
france:AMX50_Foch;1.2;1.05;1.4;1.11;1.14
|
||||
france:AMX_12t;1.9;1.64;1.6;1.6;2.02
|
||||
france:AMX_13_75;1.7;1.61;1.5;1.25;1.61
|
||||
france:AMX_13_90;1.4;1.37;1.5;1.29;1.34
|
||||
france:AMX_50_100;1.4;1.12;1.4;1.26;1.35
|
||||
france:AMX_50_120;1.2;1.02;1.4;1.12;1.17
|
||||
france:AMX_AC_Mle1946;1.7;1.45;1.4;1.24;1.61
|
||||
france:AMX_AC_Mle1948;1.5;1.16;1.4;1.24;1.35
|
||||
france:AMX_M4_1945;1.6;1.47;1.4;1.28;1.61
|
||||
france:ARL_44;2.1;1.57;1.4;1.53;1.82
|
||||
france:ARL_44BP;2.1;1.53;1.4;1.58;1.81
|
||||
france:ARL_V39;2;1.59;1.4;1.52;1.82
|
||||
france:Achilles_FR;2.1;1.6;1.4;1.52;1.83
|
||||
france:B1;4.1;2.8;1.6;2.54;3.64
|
||||
france:BDR_G1B;3.2;1.91;1.5;1.95;2.13
|
||||
france:Bat-Chat-25CL;1.1;1.05;1.5;1.1;1.22
|
||||
france:D1;4.3;4.62;2.7;3.8;6.52
|
||||
france:D2;3.7;3.64;2.2;3;5.52
|
||||
france:ELC_AMX;2.8;1.97;2.3;2.31;2.09
|
||||
france:F113_PzV_FR;1.9;1.65;1.5;1.56;1.89
|
||||
france:F114_Projet_4_1;1.1;1.03;1.4;1.09;1.22
|
||||
france:F115_Lorraine_50t;1.2;1.02;1.4;1.13;1.16
|
||||
france:F116_Bat_Chat_Bourrasque;1.3;1.28;1.5;1.31;1.34
|
||||
france:F117_Alt_Proto_AMX_30;1.3;1.15;1.5;1.29;1.35
|
||||
france:F11_Renault_G1R;3.2;2;1.5;1.96;2.12
|
||||
france:F120_Project_Louis;1.1;1.04;1.4;1.12;1.14
|
||||
france:F123_ERAC_105_Proto;1.3;1.22;1.5;1.3;1.35
|
||||
france:F126_AMX_13_M24;1.5;1.58;1.4;1.32;1.62
|
||||
france:F127_ELC_AMX_901_Proto;1.4;1.28;1.5;1.28;1.34
|
||||
france:F140_Sentinelle;1.4;1.15;1.4;1.28;1.35
|
||||
france:F19_Lorraine40t;1.3;1.17;1.5;1.3;1.34
|
||||
france:F19_Lorraine40t_Com;1.3;1.17;1.5;1.29;1.34
|
||||
france:F34_ARL_V39_BP;2.1;1.59;1.4;1.51;1.82
|
||||
france:F68_AMX_Chasseur_de_char_46;1.3;1.17;1.5;1.25;1.33
|
||||
france:F69_AMX_13D;1.4;1.16;1.5;1.28;1.34
|
||||
france:F69_AMX_13_57GF;1.6;1.49;1.5;1.26;1.61
|
||||
france:F71_AMX_30_prototype;1.1;1.06;1.4;1.12;1.19
|
||||
france:F73_M4A1_Revalorise;1.3;1.23;1.5;1.27;1.34
|
||||
france:F74_AMX_M4_1949;1.4;1.12;1.4;1.28;1.35
|
||||
france:F75_Char_de_25t;1.1;1.02;1.5;1.11;1.22
|
||||
france:F84_Somua_SM;1.4;1.12;1.4;1.14;1.14
|
||||
france:F89_Canon_dassaut_105;1.6;1.15;1.4;1.26;1.35
|
||||
france:FCM_36Pak40;4.2;2.95;1.9;3.18;5.52
|
||||
france:FCM_50t;1.3;1.17;1.5;1.25;1.35
|
||||
france:F_AMX-12BP;1.9;1.64;1.6;1.6;2.02
|
||||
france:RenaultFT_AC;5.5;4.37;2.2;4.49;8.51
|
||||
france:RenaultR35;5.5;5.28;3.3;6.76;7.8
|
||||
france:RenaultUE57;4.2;2.79;1.9;2.68;5.95
|
||||
france:S_35CA;3.2;2.07;1.5;1.96;2.11
|
||||
france:Somua_Sau_40;3.7;2.75;1.8;2.54;3.62
|
||||
germany:B-1bis_captured;4.1;2.96;1.6;2.55;3.89
|
||||
germany:DW_II;3.7;2.52;1.6;2.52;3.53
|
||||
germany:DickerMax;2.1;1.57;1.4;1.53;2.02
|
||||
germany:E-25;1.8;1.46;1.4;1.24;1.61
|
||||
germany:E-50;1.1;1.09;1.4;1.1;1.15
|
||||
germany:E-75;1.2;1.02;1.4;1.17;1.14
|
||||
germany:Ferdinand;1.4;1.16;1.4;1.25;1.35
|
||||
germany:G100_Gtraktor_Krupp;4.9;3.31;1.9;3.49;5.75
|
||||
germany:G103_RU_251;1.4;1.3;1.5;1.25;1.33
|
||||
germany:G106_PzKpfwPanther_AusfF;1.3;1.21;1.5;1.26;1.35
|
||||
germany:G109_Steyr_WT;1.8;1.37;1.4;1.27;1.6
|
||||
germany:G110_Mauschen;1.2;1.02;1.4;1.18;1.12
|
||||
germany:G112_JagdPanzer_105;1.2;1.05;1.4;1.04;1.18
|
||||
germany:G113_SP_I_C;1.7;1.49;1.5;1.28;1.58
|
||||
germany:G114_Rheinmetall_Scorpion;1.6;1.16;1.4;1.24;1.33
|
||||
germany:G115_VK10001P;1.4;1.15;1.4;1.33;1.35
|
||||
germany:G118_VK4503;1.7;1.3;1.4;1.27;1.61
|
||||
germany:G119_Panzer58_SH;1.3;1.22;1.5;1.28;1.35
|
||||
germany:G119_Pz58_Mutz;1.3;1.17;1.5;1.26;1.35
|
||||
germany:G120_M41_90;1.4;1.29;1.5;1.26;1.34
|
||||
germany:G126_HWK_12;1.1;1.05;1.5;1.08;1.29
|
||||
germany:G136_Tiger_131;2.1;1.68;1.4;1.55;1.85
|
||||
germany:G138_VK16801P;1.5;1.15;1.4;1.16;1.13
|
||||
germany:G140_HWK_30;1.4;1.27;1.5;1.27;1.34
|
||||
germany:G142_M48RPz;1.3;1.18;1.5;1.27;1.35
|
||||
germany:G147_mPanzer_64;1.7;1.49;1.5;1.29;1.61
|
||||
germany:G148_Elephant;1.4;1.16;1.4;1.21;1.35
|
||||
germany:G154_Kpz_07_RH;1.3;1.15;1.5;1.26;1.35
|
||||
germany:G162_Project_Kpz_07P_E;1.1;1.02;1.4;1.16;1.14
|
||||
germany:G164_Kpz_Pr_68_P;1.3;1.22;1.5;1.3;1.34
|
||||
germany:G165_Erich_Koncept;1.1;1.06;1.4;1.14;1.19
|
||||
germany:G166_LKpz_70_K;1.1;1.02;1.5;1.11;1.32
|
||||
germany:G167_Tiger_Maus;1.2;1.03;1.4;1.11;1.13
|
||||
germany:G168_KJpz_T_III;1.2;1.05;1.4;1.05;1.29
|
||||
germany:G190_VK_1602_Quby;1.4;1.36;1.5;1.25;1.34
|
||||
germany:G20_Marder_II;4.2;3.01;1.9;3.18;5.54
|
||||
germany:G85_Auf_Panther;1.5;1.49;1.5;1.25;1.61
|
||||
germany:G_E-75RT;1.4;1.14;1.4;1.3;1.35
|
||||
germany:G_E-75TS;1.2;1.03;1.4;1.13;1.13
|
||||
germany:G_VK3502_BP;2.1;1.68;1.4;1.57;1.79
|
||||
germany:H39_captured;4.3;5.58;2.7;3.99;6.04
|
||||
germany:Hetzer;3.7;2.6;1.8;2.54;3.47
|
||||
germany:Indien_Panzer;1.3;1.17;1.5;1.25;1.35
|
||||
germany:JagdPanther;1.8;1.37;1.4;1.25;1.62
|
||||
germany:JagdPantherII;1.6;1.16;1.4;1.23;1.35
|
||||
germany:JagdPzIV;2.1;1.58;1.4;1.49;1.8
|
||||
germany:JagdPzIV_AusfBP;2;1.58;1.4;1.49;1.8
|
||||
germany:JagdTiger;1.1;1.05;1.4;1.04;1.12
|
||||
germany:JagdTiger_SdKfz_185;1.4;1.17;1.4;1.21;1.35
|
||||
germany:JagdTiger_SdKfz_185_Snowstorm;1.4;1.33;1.4;1.21;1.35
|
||||
germany:Krupp-38D;2.6;2.22;1.7;1.95;2.12
|
||||
germany:Lowe;1.4;1.11;1.4;1.29;1.35
|
||||
germany:Nashorn;2;1.51;1.4;1.51;2.24
|
||||
germany:Nashorn_BP;2;1.51;1.4;1.5;2.24
|
||||
germany:Panther_II;1.3;1.21;1.5;1.26;1.35
|
||||
germany:Panther_M10;1.5;1.54;1.4;1.26;1.61
|
||||
germany:PanzerJager_I;5.5;4.33;2.2;4.49;8.8
|
||||
germany:Pro_Ag_A;1.1;1.02;1.4;1.1;1.21
|
||||
germany:Pz35t;4.4;4.9;2.7;3.94;6.05
|
||||
germany:Pz38_NA;2.9;2.78;1.9;2.55;3.65
|
||||
germany:Pz38t;3.6;3.88;2.1;3.05;5.58
|
||||
germany:PzII;5.5;5.28;3.3;6.67;6.81
|
||||
germany:PzIII;3.7;3.69;2.2;3.26;5.49
|
||||
germany:PzIII_A;3.7;3.09;2.1;2.98;5.58
|
||||
germany:PzIII_IV;2.7;2.22;1.7;1.95;2.12
|
||||
germany:PzII_J;3.6;5.5;2.1;2.83;5.46
|
||||
germany:PzII_Luchs;2.9;2.94;1.9;2.55;2.2
|
||||
germany:PzIV;2.7;2.22;1.7;1.95;2.13
|
||||
germany:PzIV_Hydro;2.5;2.22;1.7;1.95;2.11
|
||||
germany:PzIV_schmalturm;1.9;1.65;1.5;1.53;1.87
|
||||
germany:PzV;1.5;1.33;1.4;1.26;1.61
|
||||
germany:PzVI;1.7;1.3;1.4;1.28;1.62
|
||||
germany:PzVIB_Tiger_II;1.4;1.14;1.4;1.27;1.35
|
||||
germany:PzVI_AusfBP;2.1;1.68;1.4;1.56;1.79
|
||||
germany:PzVI_Tiger_P;1.7;1.3;1.4;1.28;1.62
|
||||
germany:PzV_PzIV;2.6;2.22;1.7;1.95;2.12
|
||||
germany:Pz_II_AusfG;3.8;2.9;2.1;2.85;5.55
|
||||
germany:Pz_IV_AusfA;3.6;3.2;2.2;4.05;5.74
|
||||
germany:Pz_IV_AusfBT;2.7;2.53;1.7;1.96;2.12
|
||||
germany:Pz_IV_AusfD;3.1;2.43;1.9;2.54;3.09
|
||||
germany:Pz_Sfl_IVc;3;1.61;1.5;1.98;2.08
|
||||
germany:RhB_Waffentrager;1.6;1.26;1.4;1.28;1.33
|
||||
germany:S01_Frankentank;1.7;1.33;1.4;1.37;1.62
|
||||
germany:S35_captured;3.8;2.85;2.1;2.93;5.52
|
||||
germany:StuGIII;3.2;1.93;1.5;1.97;2.12
|
||||
germany:Sturer_Emil;1.8;1.31;1.4;1.28;1.61
|
||||
germany:T-15;3.8;4.72;2.1;2.82;5.55
|
||||
germany:T-25;2.5;2.13;1.7;1.95;2.11
|
||||
germany:VK1602;2.8;2.9;2.3;1.96;2.11
|
||||
germany:VK2801;1.9;1.53;1.6;1.58;1.95
|
||||
germany:VK2801_BP;1.9;1.53;1.6;1.59;1.95
|
||||
germany:VK3001H;3.2;1.75;1.5;1.96;2.13
|
||||
germany:VK3001P;1.9;1.61;1.5;1.57;1.81
|
||||
germany:VK3002DB;1.5;1.58;1.4;1.28;1.61
|
||||
germany:VK3002DB_BP;1.9;1.65;1.5;1.56;1.87
|
||||
germany:VK3002DB_V1;1.9;1.65;1.5;1.56;1.84
|
||||
germany:VK3002M;1.9;1.65;1.5;1.57;1.91
|
||||
germany:VK3601H;2.1;1.64;1.4;1.57;1.78
|
||||
germany:VK4502A;1.3;1.17;1.4;1.29;1.35
|
||||
germany:VK4502P;1.2;1.02;1.4;1.18;1.13
|
||||
germany:Waffentrager_IV;1.2;1.06;1.4;1.12;1.19
|
||||
germany:Waffentrager_IV_CL;1.2;1.05;1.4;1.06;1.19
|
||||
japan:Chi_Ha;3.6;3.8;2.2;3.2;5.54
|
||||
japan:Chi_He;3;2.72;1.9;2.55;3.24
|
||||
japan:Chi_Ni;4.4;4.62;2.5;4.09;6.52
|
||||
japan:Chi_Nu;2.6;2.02;1.7;1.94;2.12
|
||||
japan:Chi_Nu_Kai;2.6;2.02;1.7;1.95;2.11
|
||||
japan:Chi_Ri;1.5;1.58;1.4;1.25;1.61
|
||||
japan:Chi_To;1.9;1.61;1.5;1.55;1.9
|
||||
japan:Firefly_GuP;1.9;1.49;1.5;1.55;2.01
|
||||
japan:G09_Hetzer_GuP;3.7;2.6;1.8;2.54;3.47
|
||||
japan:Ha_Go;5.7;5.36;3.3;5.99;8.2
|
||||
japan:IS-2_GuP;1.6;1.44;1.4;1.33;1.61
|
||||
japan:J21_Ho_Ri;1.2;1.05;1.4;1.11;1.13
|
||||
japan:J22_Ho_Ri_type96;1.5;1.16;1.4;1.25;1.35
|
||||
japan:J23_Chi_To_SPG;1.8;1.37;1.4;1.25;1.62
|
||||
japan:J24_Type_57;1.4;1.16;1.4;1.33;1.35
|
||||
japan:J26_Type_68;1.2;1.02;1.4;1.13;1.13
|
||||
japan:J27_Chi_Se;1.4;1.15;1.4;1.27;1.35
|
||||
japan:J28_Ju_To;1.7;1.41;1.4;1.32;1.62
|
||||
japan:J29_Ju_Nu;2.1;1.73;1.4;1.57;1.8
|
||||
japan:J29_Ju_Nu_BP;2.1;1.73;1.4;1.57;1.8
|
||||
japan:J30_Mitsu_108;3.2;2.37;1.5;1.97;2.13
|
||||
japan:J31_Type_5_Ka_Ri;1.5;1.16;1.4;1.26;1.35
|
||||
japan:J36_Type_63_HT;1.4;1.15;1.4;1.29;1.35
|
||||
japan:J38_Type_95_Ji_Ro;1.5;1.16;1.4;1.22;1.35
|
||||
japan:Ke_Ho;2.9;2.71;1.9;2.55;1
|
||||
japan:Ke_Ni;3.8;4.07;2.1;2.67;5.71
|
||||
japan:Ke_Ni_B;4.5;4.85;2.1;2.14;5.71
|
||||
japan:PzVI_GuP;2.1;1.64;1.4;1.54;1.81
|
||||
japan:Pz_IV_AusfH;2.6;2.22;1.7;1.95;2.12
|
||||
japan:STA_1;1.3;1.15;1.5;1.28;1.34
|
||||
japan:Type_61;1.1;1.02;1.4;1.1;1.24
|
||||
other:AMX_dracula;1.5;1.58;1.4;1.3;1.61
|
||||
other:Akawara;1.3;1.28;1.5;1.28;1.35
|
||||
other:Oth06_Sega_Lupus;1.7;1.44;1.4;1.34;1.62
|
||||
other:Oth08_WH_Vindicator;1.8;1.74;1.4;1.24;1.61
|
||||
other:Oth09_WH_Predator;1.7;1.47;1.4;1.25;1.62
|
||||
other:Oth11_T-55A;1.1;1.1;1.4;1.09;1.16
|
||||
other:Oth12_Sega_Hafen;1.6;1.66;1.4;1.27;1.61
|
||||
other:Oth14_MdM_AshKnight;1.7;1.47;1.4;1.27;1.62
|
||||
other:Oth15_RustCrab;2.6;2.22;1.7;1.96;2.12
|
||||
other:Oth16_KV_TG;1.6;1.76;1.4;1.27;1.61
|
||||
other:Oth17_BDT-5A;2.6;2.22;1.7;1.95;2.12
|
||||
other:Oth18_BDT-5B;1.9;1.85;1.5;1.55;1.94
|
||||
other:Oth19_BDT-5C;1.5;1.66;1.4;1.25;1.61
|
||||
other:Oth20_M4FL10;1.5;1.67;1.4;1.27;1.61
|
||||
other:Oth22_Sega_Vulcan;1.7;1.41;1.4;1.33;1.62
|
||||
other:Oth23_Werewolf;1.6;1.44;1.4;1.33;1.61
|
||||
other:Oth24_StuH_42HW;3.2;2.6;1.5;1.97;2.13
|
||||
other:Oth25_HWT20_FalconFire;1.7;1.47;1.4;1.25;1.62
|
||||
other:Oth26_StrvM42HW;2.7;2.22;1.7;1.96;2.11
|
||||
other:Oth27_M24_AMX;1.9;1.64;1.6;1.6;2.02
|
||||
other:Oth28_Sturmfeur_HW;1.7;1.41;1.4;1.31;1.61
|
||||
other:Oth29_A15HW;2.8;1.83;2.3;1.96;2.12
|
||||
other:Oth30_T54MS;1.3;1.2;1.5;1.3;1.35
|
||||
other:Oth31_T150BP;2.1;1.71;1.4;1.56;1.79
|
||||
other:Oth33_Blizzard_BP;2;1.68;1.6;1.56;2.21
|
||||
other:Oth34_PanzerMummy;1.7;1.39;1.4;1.3;1.61
|
||||
other:Oth35_PanzerMejai;3.2;2.27;1.5;1.95;2.13
|
||||
other:Oth36_Indien_Panzer_S1;1.5;1.56;1.4;1.28;1.61
|
||||
other:Oth37_T26_E4_SuperPershing_S1;1.3;1.25;1.5;1.3;1.35
|
||||
other:Oth38_50TP_Tyszkiewicza_S1;1.1;1.02;1.4;1.17;1.14
|
||||
other:Oth39_T20_S2;1.5;1.62;1.4;1.28;1.61
|
||||
other:Oth40_TVP_46_S2;1.3;1.15;1.5;1.28;1.35
|
||||
other:Oth41_Centurion_Mk3_S2;1.1;1.05;1.4;1.11;1.18
|
||||
other:Oth42_ForestWitch_HW23;1.7;1.42;1.4;1.32;1.61
|
||||
other:Oth44_Charioteer_T;1.6;1.17;1.4;1.27;1.35
|
||||
other:Oth45_Sandstriker;1.4;1.16;1.4;1.33;1.35
|
||||
other:Oth46_Charioteer_S3;1.8;1.37;1.4;1.27;1.61
|
||||
other:Oth47_AMX_13_90_S3;1.4;1.3;1.5;1.28;1.34
|
||||
other:Oth48_EMIL_1951E1_S3;1.1;1.02;1.4;1.15;1.14
|
||||
other:Oth49_TM_04_NT;1.5;1.63;1.4;1.34;1.61
|
||||
other:Oth50_STT;1.6;1.55;1.5;1.28;1.61
|
||||
other:Oth51_FD_24;1.3;1.18;1.5;1.28;1.35
|
||||
other:Oth52_Toxique;1.3;1.32;1.5;1.31;1.35
|
||||
other:Oth53_Rammer;1.6;1.46;1.4;1.31;1.61
|
||||
other:Oth61_Pz_VM_Set;4.1;2.58;1.6;2.54;3.71
|
||||
other:Oth62_Pz_VlD_Set;3.2;1.97;1.5;1.96;2.13
|
||||
other:Oth63_Pz_VllF_Set;2.1;1.64;1.4;1.57;1.84
|
||||
other:Oth64_Pz_VlllK_Set;1.6;1.44;1.4;1.31;1.61
|
||||
other:Oth65_PzVIllM_Tigeom;2.1;1.67;1.4;1.59;1.86
|
||||
other:Oth66_E100ANN_S4;1.7;1.47;1.4;1.33;1.62
|
||||
other:Oth67_FV215b_183ANN_S4;1.5;1.16;1.4;1.24;1.35
|
||||
other:Oth68_KV2ANN_S4;1.2;1.03;1.4;1.23;1.14
|
||||
other:Oth69_Souleater;1.5;1.17;1.4;1.26;1.35
|
||||
other:Oth70_Lightbringer;1.6;1.47;1.4;1.29;1.61
|
||||
other:Oth71_Titan_Strife;1.3;1.16;1.4;1.28;1.35
|
||||
other:Oth72_Hazard_I;1.5;1.57;1.4;1.3;1.61
|
||||
other:Oth73_Nemini;1.4;1.17;1.4;1.31;1.35
|
||||
other:Oth74_Argonaut;1.1;1.05;1.4;1.06;1.18
|
||||
other:Oth75_Braniaq_DM5;1.1;1.06;1.4;1.14;1.18
|
||||
other:Oth76_Destiny;1.2;1.02;1.4;1.17;1.15
|
||||
other:Oth77_Prowler;1.4;1.16;1.4;1.3;1.35
|
||||
other:Oth78_AC_17_P_Sterope;1.2;1.02;1.4;1.13;1.12
|
||||
other:Oth80_AC_16_Celeno;1.2;1.02;1.4;1.13;1.12
|
||||
other:Oth81_AC_15_Taygete;1.4;1.24;1.4;1.33;1.35
|
||||
other:Oth82_AC_14_Pleione;1.4;1.15;1.4;1.29;1.35
|
||||
other:Oth83_AC_13_Merope;1.7;1.4;1.4;1.32;1.61
|
||||
other:Oth84_FVE_Defender;1.1;1.12;1.4;1.16;1.15
|
||||
other:Oth85_Ghoul;1.3;1.36;1.5;1.28;1.35
|
||||
other:Oth86_Survivalist;1.1;1.06;1.4;1.1;1.12
|
||||
other:Oth92_JagdPantherII_Titan;1.4;1.17;1.4;1.24;1.35
|
||||
other:Oth93_Convoyer;1.4;1.16;1.4;1.24;1.35
|
||||
other:Sega_Edelweiss;1.5;1.62;1.4;1.28;1.61
|
||||
other:Sega_Nameless;1.7;1.41;1.4;1.29;1.62
|
||||
other:Strv_74A2;1.9;1.8;1.5;1.54;1.9
|
||||
other:Van_Helsing;1.8;1.59;1.4;1.25;1.61
|
||||
other:WZ-135G_FT;1.8;1.5;1.4;1.3;1.61
|
||||
uk:GB03_Cruiser_Mk_I;4.4;4.9;2.7;3.96;9.12
|
||||
uk:GB04_Valentine;2.9;1.85;1.9;2.53;3.37
|
||||
uk:GB05_Vickers_Medium_Mk_II;6.5;3.84;2.2;3.8;12.37
|
||||
uk:GB06_Vickers_Medium_Mk_III;4.9;3.9;1.9;2.88;5.74
|
||||
uk:GB07_Matilda;4.1;2.83;1.6;2.56;3.64
|
||||
uk:GB08_Churchill_I;3.3;1.8;1.5;1.96;2.13
|
||||
uk:GB09_Churchill_VII;2.1;1.7;1.4;1.54;1.76
|
||||
uk:GB09_Churchill_VII_BP;2.1;1.67;1.4;1.54;1.79
|
||||
uk:GB101_FV1066_Senlac;1.4;1.16;1.5;1.24;1.33
|
||||
uk:GB106_Cobra;1.1;1.05;1.4;1.14;1.16
|
||||
uk:GB108_A46;1.5;1.56;1.5;1.25;1.6
|
||||
uk:GB109_GSOR_1008;1.6;1.16;1.4;1.25;1.34
|
||||
uk:GB109_GSOR_1008_Fearless;1.6;1.18;1.4;1.24;1.34
|
||||
uk:GB10_Black_Prince;1.7;1.47;1.4;1.23;1.62
|
||||
uk:GB110_FV4201_Chieftain_Prototype;1.1;1.02;1.4;1.13;1.15
|
||||
uk:GB111_Charlemagne;1.4;1.13;1.4;1.31;1.35
|
||||
uk:GB112_Caliban;1.4;1.16;1.4;1.45;1.35
|
||||
uk:GB114_Vickers_MBT_Mk3;1.3;1.28;1.5;1.12;1.14
|
||||
uk:GB116_Harry_Hopkins;2.5;1.97;2.3;1.96;2.12
|
||||
uk:GB11_Caernarvon;1.3;1.13;1.4;1.23;1.35
|
||||
uk:GB11_Caernarvon_D;1.4;1.16;1.4;1.3;1.35
|
||||
uk:GB128_Nemesis;1.3;1.29;1.5;1.28;1.35
|
||||
uk:GB12_Conqueror;1.1;1.02;1.4;1.1;1.13
|
||||
uk:GB19_Sherman_Firefly;1.9;1.49;1.5;1.55;2.01
|
||||
uk:GB20_Crusader;2.7;1.83;2.3;1.96;2.11
|
||||
uk:GB21_A27BP;1.9;1.7;1.5;1.55;1.94
|
||||
uk:GB21_Cromwell;1.9;1.7;1.5;1.55;1.94
|
||||
uk:GB22_Comet;1.5;1.58;1.4;1.25;1.61
|
||||
uk:GB23_Centurion;1.3;1.12;1.5;1.25;1.35
|
||||
uk:GB24_Centurion_Mk3;1.1;1.02;1.4;1.14;1.15
|
||||
uk:GB32_Tortoise;1.1;1.05;1.4;1.05;1.11
|
||||
uk:GB35_Sentinel_AC_IV;1.9;1.7;1.5;1.56;1.94
|
||||
uk:GB39_Universal_CarrierQF2;5.5;4.89;2.2;3.99;11.07
|
||||
uk:GB40_Gun_Carrier_BP;2;1.56;1.4;1.52;1.77
|
||||
uk:GB40_Gun_Carrier_Churchill;2.1;1.48;1.4;1.51;1.79
|
||||
uk:GB41_Challenger;1.8;1.35;1.4;1.26;1.61
|
||||
uk:GB42_Valentine_AT;4.3;2.68;1.9;2.43;5.48
|
||||
uk:GB44_Archer_Custom;3.2;1.76;1.5;1.98;2.08
|
||||
uk:GB51_Excelsior;3.2;2.27;1.5;1.97;2.13
|
||||
uk:GB52_FV201_A45;1.7;1.47;1.4;1.24;1.61
|
||||
uk:GB57_Alecto;3.7;2.93;1.8;2.54;1
|
||||
uk:GB58_Cruiser_Mk_III;4.4;4.9;2.5;4.26;6.52
|
||||
uk:GB59_Cruiser_Mk_IV;3.7;3.82;2.2;3.05;5.52
|
||||
uk:GB60_Covenanter;3;3;1.9;2.55;2.68
|
||||
uk:GB63_TOG_II;1.9;1.53;1.4;1.54;1.8
|
||||
uk:GB63_TOG_II_BP;1.9;1.53;1.4;1.57;1.79
|
||||
uk:GB68_Matilda_Black_Prince;2.5;1.96;1.7;1.98;2.12
|
||||
uk:GB69_Cruiser_Mk_II;5.9;5.88;3.3;5.32;8.2
|
||||
uk:GB71_AT_15A;1.7;1.58;1.4;1.22;1.62
|
||||
uk:GB72_AT15;1.4;1.19;1.4;1.19;1.35
|
||||
uk:GB73_AT2;3.2;2.45;1.5;1.97;2.13
|
||||
uk:GB74_AT8;2;1.6;1.4;1.51;1.76
|
||||
uk:GB75_AT7;1.7;1.33;1.4;1.23;1.62
|
||||
uk:GB76_Mk_VIC;4.3;5.58;2.7;4.01;9.12
|
||||
uk:GB80_Charioteer;1.6;1.16;1.4;1.27;1.34
|
||||
uk:GB81_FV4004;1.2;1.05;1.4;1.11;1.2
|
||||
uk:GB85_Cromwell_Berlin;1.9;1.7;1.5;1.55;1.94
|
||||
uk:GB87_Chieftain_T95_turret;1.4;1.17;1.4;1.25;1.35
|
||||
uk:GB93_Caernarvon_AX;1.3;1.13;1.4;1.25;1.35
|
||||
uk:GB94_Centurion_Mk5-1_RAAC;1.3;1.12;1.5;1.28;1.35
|
||||
uk:GB97_Chimera;1.3;1.15;1.5;1.32;1.35
|
||||
uk:GB99_Turtle_Mk1;1.4;1.18;1.4;1.22;1.35
|
||||
uk:GB_Centurion_MkDF;1.3;1.12;1.5;1.29;1.35
|
||||
uk:GB_Churchill_MkVI;2.1;1.73;1.4;1.54;1.77
|
||||
uk:GB_Churchill_MkVIII;2.1;1.64;1.4;1.56;1.79
|
||||
uk:GB_FV301;1.4;1.07;1.5;1.25;1.35
|
||||
uk:GB_Sherman_V;2.6;2.28;1.7;1.96;2.11
|
||||
uk:GB_Vickers_Cruiser;1.1;1;1.5;1.11;1.2
|
||||
uk:Sentinel_AC1;3;2.88;1.9;2.55;3.47
|
||||
usa:A05_M4_105BD;2.7;2.4;1.7;1.96;2.12
|
||||
usa:A100_T49;1.4;1.53;1.5;1.29;1.34
|
||||
usa:A102_T28_concept;1.7;1.47;1.4;1.27;1.62
|
||||
usa:A104_M4A3E8A;1.9;1.88;1.5;1.54;1.84
|
||||
usa:A111_T25_Pilot;1.3;1.27;1.5;1.26;1.35
|
||||
usa:A115_Chrysler_K;1.4;1.16;1.4;1.3;1.35
|
||||
usa:A117_T26E5;1.4;1.15;1.4;1.27;1.35
|
||||
usa:A121_T26E3;1.5;1.54;1.4;1.31;1.61
|
||||
usa:A122_TS-5;1.4;1.15;1.4;1.23;1.35
|
||||
usa:A124_T54E2;1.4;1.15;1.4;1.29;1.35
|
||||
usa:A125_AEP_1;1.1;1.02;1.4;1.15;1.14
|
||||
usa:A130_Super_Hellcat;1.7;1.51;1.4;1.24;1.6
|
||||
usa:A132_T77;1.3;1.15;1.4;1.29;1.35
|
||||
usa:A136_T42;1.5;1.54;1.4;1.29;1.61
|
||||
usa:A140_ASTRON_REX_105;1.3;1.31;1.5;1.31;1.35
|
||||
usa:A141_M-4-Y;1.4;1.16;1.4;1.27;1.35
|
||||
usa:A142_Pawlack_Tank;2.1;1.68;1.4;1.58;1.85
|
||||
usa:A146_TL_7_120;1.1;1.02;1.4;1.14;1.14
|
||||
usa:A150_MBT_B;1.2;1.02;1.4;1.21;1.13
|
||||
usa:A151_TS_60;1.3;1.14;1.4;1.06;1.15
|
||||
usa:A175_ATAC;1.1;1.02;1.5;1.11;1.32
|
||||
usa:A68_T28D;1.4;1.2;1.4;1.28;1.35
|
||||
usa:A77_T6M;3;2.72;1.9;2.52;3.24
|
||||
usa:A94_T37;1.9;1.73;1.6;1.55;2.21
|
||||
usa:A99_T92E1;1.1;1.1;1.5;1.2;1.2
|
||||
usa:M-3-Y;1.4;1.14;1.4;1.28;1.35
|
||||
usa:M-3-Y-Midas;1.4;1.14;1.4;1.28;1.35
|
||||
usa:M-5-Y;1.1;1.02;1.4;1.2;1.14
|
||||
usa:M-7-Y;1.7;1.32;1.4;1.32;1.61
|
||||
usa:M103;1.1;1.02;1.4;1.14;1.13
|
||||
usa:M10_Wolverine;3.2;2.18;1.5;1.96;2.13
|
||||
usa:M18E2BP;1.9;1.65;1.5;1.57;2.02
|
||||
usa:M18_Hellcat;2.1;1.58;1.4;1.51;2.99
|
||||
usa:M22_Locust;3.8;4.23;2.1;3;5.59
|
||||
usa:M24_Chaffee;1.9;1.73;1.6;1.55;2.36
|
||||
usa:M24_Chaffee_BP;1.9;1.73;1.6;1.55;2.36
|
||||
usa:M2_lt;5.5;5.28;3.3;6.76;7.39
|
||||
usa:M2_med;3.6;4.29;2.2;2.93;5.54
|
||||
usa:M36_BP;2.1;1.58;1.4;1.53;1.85
|
||||
usa:M36_Slagger;2.1;1.58;1.4;1.54;1.85
|
||||
usa:M3_Grant;3;2.38;1.9;2.55;3.13
|
||||
usa:M3_Stuart;4.4;4.29;2.7;4.23;5.89
|
||||
usa:M41_Bulldog;1.6;1.49;1.5;1.25;1.61
|
||||
usa:M46_CL;1.1;1.04;1.4;1.08;1.17
|
||||
usa:M46_Patton;1.1;1.08;1.4;1.09;1.17
|
||||
usa:M4A2E4;2.6;2.28;1.7;1.96;2.13
|
||||
usa:M4A3E8_BP;1.9;1.88;1.5;1.55;1.84
|
||||
usa:M4A3E8_Sherman;1.9;1.88;1.5;1.55;1.84
|
||||
usa:M4_Sherman;2.7;1.98;1.7;1.95;2.12
|
||||
usa:M5_Stuart;3.7;3.48;2.1;2.94;5.57
|
||||
usa:M6;2.1;1.6;1.4;1.58;1.8
|
||||
usa:M6A2E1;1.6;1.32;1.4;1.33;1.62
|
||||
usa:M6A2E1_exp;1.4;1.07;1.4;1.33;1.35
|
||||
usa:M6E2BP;2.1;1.6;1.4;1.57;1.77
|
||||
usa:M6E2V2_BP;2.1;1.64;1.4;1.53;1.78
|
||||
usa:M7_med;3.1;2.38;1.9;2.54;2.98
|
||||
usa:M8A1;4;2.73;1.8;2.54;2.87
|
||||
usa:Pershing;1.3;1.33;1.5;1.25;1.35
|
||||
usa:Ram-II;2.5;2.22;1.7;1.97;2.12
|
||||
usa:Sherman_Jumbo;1.9;1.88;1.5;1.56;1.8
|
||||
usa:T14;3.2;2.35;1.5;1.97;2.13
|
||||
usa:T18;5.5;4.48;2.2;4.43;5.65
|
||||
usa:T1_E6;4.4;5;2.7;4.12;8.54
|
||||
usa:T1_hvy;3.3;2;1.5;1.96;2.13
|
||||
usa:T20;1.5;1.54;1.4;1.28;1.61
|
||||
usa:T21;1.9;1.81;1.6;1.57;2.27
|
||||
usa:T21_BP;1.9;1.81;1.6;1.56;2.24
|
||||
usa:T23E3;1.5;1.54;1.4;1.25;1.61
|
||||
usa:T25_2;1.7;1.4;1.4;1.23;1.61
|
||||
usa:T25_AT;1.7;1.44;1.4;1.26;1.61
|
||||
usa:T26_E4_SuperPershing;1.3;1.2;1.5;1.29;1.35
|
||||
usa:T28;1.4;1.16;1.4;1.23;1.35
|
||||
usa:T28_Prototype;1.4;1.16;1.4;1.24;1.35
|
||||
usa:T29;1.7;1.32;1.4;1.31;1.61
|
||||
usa:T2_lt;4.3;5.45;2.7;3.9;8.26
|
||||
usa:T2_med;4.6;5;2.5;4.23;7.11
|
||||
usa:T30;1.1;1.05;1.4;1.12;1.17
|
||||
usa:T32;1.4;1.16;1.4;1.29;1.35
|
||||
usa:T34_hvy;1.3;1.07;1.4;1.3;1.35
|
||||
usa:T34_hvy_Freedom;1.4;1.07;1.4;1.31;1.35
|
||||
usa:T40;4;2.38;1.8;2.54;3.13
|
||||
usa:T49;3.2;2.18;1.5;1.97;2.08
|
||||
usa:T49_Fearless;1.4;1.53;1.5;1.28;1.34
|
||||
usa:T54E1;1.1;1.06;1.4;1.1;1.16
|
||||
usa:T69;1.3;1.33;1.5;1.27;1.35
|
||||
usa:T71;1.6;1.49;1.5;1.27;1.6
|
||||
usa:T7_Combat_Car;4.4;5.58;2.7;4.26;8.54
|
||||
usa:T82;4.1;13.5;1.9;2.78;5.78
|
||||
usa:T95;1.2;1.05;1.4;1.11;1.12
|
||||
usa:T95E2;1.3;1.33;1.5;1.25;1.35
|
||||
ussr:A-20;3.1;2.72;1.9;2.54;3
|
||||
ussr:A-32;3;2.85;1.9;2.52;3.43
|
||||
ussr:AT-1;5.5;3.99;2.2;3.99;8.51
|
||||
ussr:BT-2;4.3;4.71;2.7;4.18;6.52
|
||||
ussr:BT-7;3.6;2.98;2.1;3.06;5.51
|
||||
ussr:BT-SV;3.8;2.86;2.1;3.16;5.58
|
||||
ussr:Churchill_LL;3.2;2.27;1.5;1.97;2.13
|
||||
ussr:GAZ-74b;4;2.48;1.8;2.53;3.05
|
||||
ussr:IS;1.6;1.44;1.4;1.32;1.62
|
||||
ussr:IS-2Sh;1.4;1.15;1.4;1.31;1.35
|
||||
ussr:IS-3;1.4;1.16;1.4;1.3;1.35
|
||||
ussr:IS8;1.2;1.02;1.4;1.12;1.15
|
||||
ussr:ISU-152;1.6;1.13;1.4;1.38;1.35
|
||||
ussr:K-91;1.1;1.02;1.4;1.1;1.12
|
||||
ussr:KV-13;1.5;1.67;1.4;1.26;1.61
|
||||
ussr:KV-1s;2.3;1.6;1.4;1.6;1.82
|
||||
ussr:KV-1s-BP;2.3;1.5;1.4;1.64;1.83
|
||||
ussr:KV-220;3.2;2.27;1.5;1.96;2.13
|
||||
ussr:KV-3;1.7;1.44;1.4;1.33;1.62
|
||||
ussr:KV-5;1.5;1.15;1.4;1.29;1.35
|
||||
ussr:KV1;3.2;2.11;1.5;1.95;2.13
|
||||
ussr:KV2;2.4;2.25;1.4;1.52;1.85
|
||||
ussr:KV4;1.4;1.13;1.4;1.3;1.35
|
||||
ussr:LTP;3.8;2.88;2.1;3.16;5.56
|
||||
ussr:M3_Stuart_LL;3.7;5.04;2.1;2.86;5.51
|
||||
ussr:MS-1obr21;4.3;4.71;2.7;4.18;6.98
|
||||
ussr:MT25;2;1.79;1.6;1.56;2.06
|
||||
ussr:Matilda_II_LL;2.5;2.22;1.7;1.96;2.12
|
||||
ussr:Object252;1.4;1.14;1.4;1.33;1.35
|
||||
ussr:Object252_Fearless;1.4;1.41;1.4;1.33;1.35
|
||||
ussr:Object_704;1.1;1.05;1.4;1.1;1.13
|
||||
ussr:R105_BT_7A;3.8;6.27;2.1;3.86;5.64
|
||||
ussr:R107_LTB;1.6;1.56;1.5;1.28;1.61
|
||||
ussr:R109_T54S;1.4;1.34;1.5;1.29;1.34
|
||||
ussr:R111_ISU130;1.5;1.16;1.4;1.23;1.35
|
||||
ussr:R112_T54_45;1.3;1.37;1.5;1.31;1.35
|
||||
ussr:R113_Object_730;1.4;1.15;1.4;1.31;1.35
|
||||
ussr:R115_IS-3_auto;1.4;1.15;1.4;1.3;1.35
|
||||
ussr:R116_ISU122C_Berlin;1.8;1.45;1.4;1.22;1.62
|
||||
ussr:R127_T44_100_P;1.3;1.33;1.5;1.27;1.35
|
||||
ussr:R131_LTG;1.6;1.61;1.5;1.28;1.61
|
||||
ussr:R134_Object_252U;1.4;1.14;1.4;1.33;1.35
|
||||
ussr:R140_M4A2_Loza;1.9;1.88;1.5;1.55;1.84
|
||||
ussr:R146_STG;1.3;1.33;1.5;1.33;1.35
|
||||
ussr:R154_T-34E-43;2.6;2.22;1.7;1.96;2.11
|
||||
ussr:R158_LT_432;1.4;1.34;1.5;1.28;1.34
|
||||
ussr:R159_SU_130PM;1.6;1.16;1.4;1.26;1.34
|
||||
ussr:R160_object_244;2.1;1.71;1.4;1.55;1.82
|
||||
ussr:R172_Object_752;1.1;1.02;1.4;1.13;1.13
|
||||
ussr:R175_IS_2E;1.6;1.47;1.4;1.31;1.61
|
||||
ussr:R180_Object_274A;1.3;1.31;1.5;1.3;1.35
|
||||
ussr:R181_Object_452K;1.2;1.02;1.4;1.15;1.13
|
||||
ussr:R187_Object_590;1.3;1.32;1.5;1.29;1.35
|
||||
ussr:R192_K_2;1.4;1.12;1.4;1.33;1.35
|
||||
ussr:R194_ST_62_var_2;1.3;1.31;1.5;1.28;1.35
|
||||
ussr:R217_Object_Kust;1.5;1.16;1.4;1.27;1.35
|
||||
ussr:R43_T-70_57;3.8;3.41;2.1;3.43;5.49
|
||||
ussr:R63_ST_IBD;1.2;1.02;1.4;1.17;1.14
|
||||
ussr:R71_IS_2B;1.6;1.44;1.4;1.33;1.61
|
||||
ussr:R98_T44_85;1.5;1.64;1.4;1.26;1.61
|
||||
ussr:R_Object_84;1.1;1.07;1.5;1.12;1.17
|
||||
ussr:ST_I;1.2;1.02;1.4;1.16;1.13
|
||||
ussr:SU-100;2.1;1.57;1.4;1.55;1.81
|
||||
ussr:SU-101;1.5;1.15;1.4;1.26;1.35
|
||||
ussr:SU-152;1.8;2;1.4;1.38;1.61
|
||||
ussr:SU-76;4.2;3.6;1.9;3.18;5.56
|
||||
ussr:SU-85;3.2;1.99;1.5;1.97;2.12
|
||||
ussr:SU100M1;1.7;1.35;1.4;1.25;1.61
|
||||
ussr:SU100Y;2.1;1.53;1.4;1.57;1.89
|
||||
ussr:SU122_44;1.8;1.45;1.4;1.25;1.61
|
||||
ussr:SU122_54;1.2;1.05;1.4;1.03;1.14
|
||||
ussr:SU76I;4.1;3.31;1.9;3.18;5.47
|
||||
ussr:SU_85I;3.1;2.3;1.5;1.98;2.12
|
||||
ussr:T-127;3.8;2.76;2.1;3.16;5.52
|
||||
ussr:T-26;5.5;5.07;3.3;6.16;8.2
|
||||
ussr:T-28;3;2.85;1.9;2.54;3.09
|
||||
ussr:T-34;2.7;2.01;1.7;1.96;2.12
|
||||
ussr:T-34-85;1.9;1.71;1.5;1.58;1.87
|
||||
ussr:T-34-85_Rudy;1.5;1.67;1.4;1.26;1.61
|
||||
ussr:T-34-85_Victory;1.9;1.88;1.5;1.57;1.87
|
||||
ussr:T-43;1.5;1.6;1.4;1.25;1.61
|
||||
ussr:T-44;1.3;1.33;1.5;1.34;1.35
|
||||
ussr:T-46;3.8;4.32;2.1;2.83;5.6
|
||||
ussr:T-54;1.1;1.1;1.4;1.09;1.16
|
||||
ussr:T150;2.1;1.55;1.4;1.59;1.82
|
||||
ussr:T34_85BP;1.9;1.88;1.5;1.57;1.87
|
||||
ussr:T34_85TUR;1.5;1.58;1.4;1.29;1.62
|
||||
ussr:Tetrarch_LL;4.3;3.88;2.7;3.88;8.51
|
||||
ussr:Valentine_LL;2.9;4.55;1.9;2.55;3.42
|
||||
|
Reference in new issue
Block a user