Compare commits

...
10 Commits
Author SHA1 Message Date
tresabhi 4e470599e8 speed 2026-09-22 16:39:51 -05:00
tresabhi 5f24887535 api strings 2026-09-22 16:38:30 -05:00
tresabhi 99317c2075 hasUpgrades 2026-09-22 16:24:29 -05:00
tresabhi 99965118b3 useProtagonist 2026-09-22 16:20:39 -05:00
tresabhi 7592381b12 ComputedCharacteristics 2026-09-22 16:17:55 -05:00
tresabhi 7d02f6311f tank states 2026-09-22 16:13:27 -05:00
tresabhi 6c9ce58af5 scripts api 2026-09-22 15:36:12 -05:00
tresabhi 402b6d5113 blitz scripts 2026-09-22 15:25:25 -05:00
tresabhi 1733180f2c oh my god this is hideous 2026-09-22 14:52:33 -05:00
tresabhi 11c2c9749c more entries 2026-09-22 11:55:03 -05:00
52 changed files with 1264 additions and 579 deletions

No files matched your search

+2
View File
@@ -1,9 +1,11 @@
{
"words": [
"Debuff",
"gamemodes",
"Passability",
"Recoverkit",
"Repairkit",
"Respawns",
"Tankopedia"
]
}
+5
View File
@@ -0,0 +1,5 @@
export async function fetchJSON<Type>(url: string) {
const response = await fetch(url);
const json = await response.json();
return json as Type;
}
@@ -1,4 +1,4 @@
import type { MessageFns } from '../../../protos/src/blitzkit';
import type { MessageFns } from "@blitzkit/protos/src/blitzkit";
export async function fetchPB<Type>(url: string, message: MessageFns<Type>) {
const response = await fetch(url);
File renamed without changes.
@@ -10,21 +10,31 @@ import {
SkillDefinitions,
TankDefinitions,
} from "@blitzkit/core";
import { Strings } from "@blitzkit/i18n";
import { BlitzScripts } from "../../types/blitzScripts";
export abstract class BlitzKitAPI {
export abstract class AbstractBlitzKitAPI {
abstract skills(): Promise<SkillDefinitions>;
abstract provisions(): Promise<ProvisionDefinitions>;
abstract consumables(): Promise<ConsumableDefinitions>;
abstract equipments(): Promise<EquipmentDefinitions>;
abstract equipment(): Promise<EquipmentDefinitions>;
abstract maps(): Promise<MapDefinitions>;
abstract models(): Promise<ModelDefinitions>;
abstract camouflages(): Promise<CamouflageDefinitions>;
abstract tanks(): Promise<TankDefinitions>;
abstract game(): Promise<GameDefinitions>;
abstract gallery(): Promise<Gallery>;
abstract scripts(): Promise<BlitzScripts>;
abstract strings(locale: string): Promise<Strings>;
async tank(id: number) {
const tanks = await this.tanks();
return tanks.tanks[id];
}
async model(id: number) {
const models = await this.models();
return models.models[id];
}
}
+15 -4
View File
@@ -1,4 +1,5 @@
import { alias } from "@blitzkit/core";
import { Strings } from "@blitzkit/i18n";
import {
CamouflageDefinitions,
ConsumableDefinitions,
@@ -11,10 +12,12 @@ import {
SkillDefinitions,
TankDefinitions,
} from "@blitzkit/protos";
import { fetchPB } from "../../protobuf";
import { BlitzKitAPI } from "./base";
import { fetchPB } from "../../api";
import { fetchJSON } from "../../api/fetchJSON";
import { BlitzScripts } from "../../types/blitzScripts";
import { AbstractBlitzKitAPI } from "./abstract";
export class ClientBlitzKitAPI extends BlitzKitAPI {
export class ClientBlitzKitAPI extends AbstractBlitzKitAPI {
game() {
return fetchPB(alias("api", "/definitions/game.pb"), GameDefinitions);
}
@@ -52,7 +55,7 @@ export class ClientBlitzKitAPI extends BlitzKitAPI {
return fetchPB(alias("api", "/definitions/skills.pb"), SkillDefinitions);
}
equipments() {
equipment() {
return fetchPB(
alias("api", "/definitions/equipment.pb"),
EquipmentDefinitions,
@@ -66,4 +69,12 @@ export class ClientBlitzKitAPI extends BlitzKitAPI {
maps() {
return fetchPB(alias("api", "/definitions/maps.pb"), MapDefinitions);
}
scripts() {
return fetchJSON<BlitzScripts>(alias("api", "/definitions/scripts.json"));
}
strings(locale: string) {
return fetchJSON<Strings>(alias("api", `/strings/${locale}.json`));
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { AbstractVFS, BlitzKitAPI } from "@blitzkit/core";
import { AbstractBlitzKitAPI, AbstractVFS } from "@blitzkit/core";
export async function createDynamicAPI(vfs: () => Promise<AbstractVFS>) {
let _api: BlitzKitAPI;
let _api: AbstractBlitzKitAPI;
if (import.meta.env.SSR) {
const { ServerBlitzKitAPI } = await import(
+1 -1
View File
@@ -1,4 +1,4 @@
export * from "./base";
export * from "./abstract";
export * from "./client";
export * from "./dynamic";
export * from "./server";
@@ -25,13 +25,12 @@ import {
VehicleDefinitionList,
} from "@blitzkit/core";
import { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import type { extname, parse } from "node:path";
import { parse as parseYaml } from "yaml";
import { BlitzKitAPI } from "../base";
import { AbstractBlitzKitAPI } from "../abstract";
export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
export abstract class ServerBlitzKitAPI0 extends AbstractBlitzKitAPI {
protected vfs: AbstractVFS;
protected parsePath?: typeof parse;
protected extname?: typeof extname;
@@ -171,8 +170,8 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
let fetchedLocalizations = 0;
await Promise.all(
locales.supported.map(async ({ locale }) => {
const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale];
locales.supported.map(async (locale) => {
const blitzLocale = locale.variant_blitz_cdn ?? locale.locale;
const cache = await fetch(
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`,
)
@@ -182,7 +181,7 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
`Data/Strings/${blitzLocale}.yaml`,
);
this.stringsI18n[locale] = {
this.stringsI18n[locale.locale] = {
...cache,
...preInstalled,
};
@@ -0,0 +1,51 @@
import { ConsumablesCommon, OptionalDevices } from "../../../types";
import { BlitzScripts } from "../../../types/blitzScripts";
import { AbstractVFS } from "../../vfs";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI10 } from "./10_avatars";
export abstract class ServerBlitzKitAPI11 extends ServerBlitzKitAPI10 {
@Cache()
async scripts() {
const scripts: BlitzScripts = {
consumables: {},
equipment: {},
provisions: {},
};
const optionalDevices = await this.vfs.xml<{
root: OptionalDevices;
}>("Data/XML/item_defs/vehicles/common/optional_devices.xml");
for (const key in optionalDevices.root) {
const equipment = optionalDevices.root[key];
scripts.equipment[equipment.id] = equipment.script;
}
async function assign(
vfs: AbstractVFS,
dir: string,
target: keyof BlitzScripts,
) {
const root = `Data/XML/item_defs/vehicles/common/${dir}`;
const list = await vfs.text(`${root}/list.xml`);
const pattern = /<items path="(\w+)\.xml"\/>/gm;
for (const match of list.matchAll(pattern)) {
const group = match[1];
const path = `${root}/${group}.xml`;
const things = await vfs.xml<{ root: ConsumablesCommon }>(path);
for (const key in things.root) {
const thing = things.root[key];
scripts[target][thing.id] = things.root[key].script;
}
}
}
await assign(this.vfs, "consumables", "consumables");
await assign(this.vfs, "provisions", "provisions");
return scripts;
}
}
@@ -0,0 +1,25 @@
import { Strings } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import { DeepPartial } from "@blitzkit/protos";
import { merge } from "lodash-es";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI11 } from "./11_scripts";
const globbedStrings = import.meta.glob("../../../../../i18n/strings/*.json", {
import: "default",
});
export abstract class ServerBlitzKitAPI12 extends ServerBlitzKitAPI11 {
@Cache()
async strings(locale: string) {
const localized = (await globbedStrings[
`../../../../../i18n/strings/${locale}.json`
]()) as DeepPartial<Strings>;
const defaults = (await globbedStrings[
`../../../../../i18n/strings/${locales.default}.json`
]()) as Strings;
const strings = merge({}, defaults, localized);
return strings;
}
}
@@ -4,7 +4,7 @@ import { ServerBlitzKitAPI4 } from "./4_maps";
export abstract class ServerBlitzKitAPI5 extends ServerBlitzKitAPI4 {
@Cache()
async equipments() {
async equipment() {
const equipmentDefinitions = EquipmentDefinitions.create();
Object.entries(this.optionalDevices!.root).forEach(
@@ -105,228 +105,6 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
});
}
}
function applyCanonicalBonuses(map: Record<string, string>) {
for (const key in map) {
entry.bonuses[key] = provision.script.bonusValues![map[key]];
}
}
function applyRootBonuses(map: Record<string, string>) {
for (const key in map) {
entry.bonuses[key] = provision.script[map[key]];
}
}
function applyAttributeBonuses(map: Record<string, string>) {
for (const key in map) {
if (!(map[key] in provision.script.attributes!)) continue;
entry.bonuses[key] = provision.script.attributes![map[key]];
}
}
switch (provision.script["#text"]) {
case "Stimulator":
applyCanonicalBonuses({
crew_level_increase: "crewLevelIncrease",
});
break;
case "AntiHighExplosive":
applyCanonicalBonuses({
anti_high_explosive_factor: "factor",
});
break;
case "Fuel":
applyCanonicalBonuses({
engine_power_increase: "enginePowerIncrease",
turret_rotation_speed_increase: "turretRotationSpeedIncrease",
});
break;
case "SafetySet":
applyCanonicalBonuses({
crew_chance_to_hit_factor: "crewChanceToHitFactor",
repair_speed_increase: "repairSpeedIncrease",
fire_protection_increase: "fireProtectionIncrease",
});
break;
case "SmallHPStock":
applyRootBonuses({
hp_stock_percent: "hpStockPercent",
});
break;
case "GearOil":
applyCanonicalBonuses({
forward_speed_limit_bias: "fwdSpeedLimitBias",
backward_speed_limit_bias: "bkwdSpeedLimitBias",
engine_power_increase: "enginePowerIncrease",
});
break;
case "GunPowder":
applyCanonicalBonuses({
projectile_speed_factor: "projectileSpeedFactor",
});
break;
case "AttributesModifier":
applyAttributeBonuses({
firm_ground_passability_increase: "firmGroundPassabilityIncrease",
medium_ground_passability_increase:
"mediumGroundPassabilityIncrease",
repair_speed_factor: "repairSpeedFactor",
common_damage_factor: "commonDamageFactor",
gun_reload_factor: "gunReloadFactor",
pump_gun_reload_factor: "pumpGunReloadFactor",
gun_aiming_factor: "gunAimingFactor",
engine_power_factor: "enginePowerFactor",
speed_limits_factor: "speedLimitsFactor",
chassis_health_factor: "chassisHealthFactor",
});
break;
case "LifestealHeal":
applyCanonicalBonuses({
life_steal_factor: "lifestealFactor",
life_steal_random_factor: "lifestealRandomFactor",
kill_heal_factor: "killHealFactor",
heal_factor: "healFactor",
heal_random_factor: "healRandomFactor",
});
break;
case "RammingDamage":
applyCanonicalBonuses({
ramming_damage_factor: "rammingDamageFactor",
dealing_ramming_damage_factor: "dealingRammingDamageFactor",
});
break;
case "AllyBuff":
applyCanonicalBonuses({
gun_reloading_bonus: "gunReloadingBonus",
gun_aiming_time_bonus: "gunAimingTimeBonus",
vision_radius_bonus: "visionRadiusBonus",
});
applyRootBonuses({
range: "range",
max_bonus_sources: "maxBonusSources",
});
break;
case "Stealth":
applyRootBonuses({
sixth_sense_delay: "sixthSenseDelay",
stop_observing_delay_factor: "stopObservingDelayFactor",
});
break;
case "Revenge":
applyRootBonuses({
revenge_damage_percentage: "revengeDamagePercentage",
effect_duration: "effectDuration",
});
break;
case "ComeBackKit":
applyRootBonuses({
time_for_come_back: "timeForComeBack",
simple_mode: "simpleMode",
});
break;
case "EngineAndSpeedBoost":
applyCanonicalBonuses({
engine_power_ability_factor: "enginePowerAbilityFactor",
speed_limit_ability_bonus: "speedLimitAbilityBonus",
});
break;
case "EnemyReloadTime":
// bonusValues is empty in the observed data.
break;
case "Duplet":
applyCanonicalBonuses({
shot_dispersion_angle_factor: "shotDispersionAngleFactor",
aiming_time_factor: "aimingTimeFactor",
time_between_shots: "timeBetweenShots",
});
break;
case "Fortune":
applyCanonicalBonuses({
increased_damage_chance: "increasedDamageChance",
increase_damage_factor: "increaseDamageFactor",
});
break;
case "VampiricCurse":
applyRootBonuses({
curse_strength: "curseStrength",
curse_reduction: "curseReduction",
curse_heal: "curseHeal",
});
break;
case "VampiricDeath":
applyRootBonuses({
explode_damage_percent: "explodeDamagePercent",
range: "range",
});
break;
case "PermanentDamageShield":
applyRootBonuses({
damage_reduction_percent: "damageReductionPercent",
});
break;
case "BigBoss":
applyCanonicalBonuses({
gun_reload_factor: "gunReloadFactor",
common_damage_factor: "commonDamageFactor",
engine_power_factor: "enginePowerFactor",
speed_limits_factor: "speedLimitsFactor",
max_health_percent_bonus: "maxHealthPercentBonus",
max_statics_damage_percent: "maxStaticsDamagePercent",
});
break;
case "VampiricBoost":
break;
case "ZombieMark":
applyAttributeBonuses({
dealing_ramming_damage_factor: "dealingRammingDamageFactor",
ramming_damage_factor: "rammingDamageFactor",
engine_power_factor: "enginePowerFactor",
speed_limits_factor: "speedLimitsFactor",
common_damage_factor: "commonDamageFactor",
});
break;
case "HumanMark":
applyRootBonuses({
opt_device_ramming_damage_override:
"optDeviceRammingDamageOverride",
});
// `bonusValues.item` is a nested array and therefore isn't
// appropriate for applyCanonicalBonuses().
break;
default:
console.warn(`Unhandled script type ${provision.script["#text"]}`);
console.dir(provision.script, { depth: null, colors: true });
}
});
return provisionDefinitions;
@@ -1,3 +1,3 @@
import { ServerBlitzKitAPI10 } from "./10_avatars";
import { ServerBlitzKitAPI12 } from "./12_strings";
export class ServerBlitzKitAPI extends ServerBlitzKitAPI10 {}
export class ServerBlitzKitAPI extends ServerBlitzKitAPI12 {}
@@ -1,12 +1,16 @@
import { GunDefinition, ProvisionDefinitions, TankDefinition } from "../../../protos/src/blitzkit";
import {
GunDefinition,
ProvisionDefinitions,
TankDefinition,
} from "../../../protos/src/blitzkit";
import { checkConsumableProvisionInclusivity } from "./checkConsumableProvisionInclusivity";
export function availableProvisions(
tank: TankDefinition,
gun: GunDefinition,
provisionDefinitions: ProvisionDefinitions
provisionDefinitions: ProvisionDefinitions,
) {
return Object.values(provisionDefinitions.provisions).filter((provision) =>
checkConsumableProvisionInclusivity(provision, tank, gun)
checkConsumableProvisionInclusivity(provision, tank, gun),
);
}
@@ -1,6 +1,6 @@
import { staticAsset } from "@blitzkit/core";
import { fetchPB } from "../../protobuf";
import { AverageDefinitions } from "../../../../protos/src/blitzkit";
import { fetchPB } from "../../api";
export interface AverageDefinitionsManifest {
version: 1;
@@ -1,5 +1,10 @@
import { GunDefinition, ProvisionDefinitions, TankDefinition } from '../protos';
import { availableProvisions } from './availableProvisions';
import {
GunDefinition,
ProvisionDefinitions,
TankDefinition,
} from "@blitzkit/protos";
import { BlitzScripts } from "../types/blitzScripts";
import { availableProvisions } from "./availableProvisions";
const PROVISION_PREFERENCES = [
19, // improved fuel
@@ -14,9 +19,10 @@ function infinityFallback(value: number) {
export function createDefaultProvisions(
tank: TankDefinition,
gun: GunDefinition,
provisionDefinitions: ProvisionDefinitions,
provisions: ProvisionDefinitions,
scripts: BlitzScripts,
) {
const provisionsList = availableProvisions(tank, gun, provisionDefinitions);
const provisionsList = availableProvisions(tank, gun, provisions);
return provisionsList
.sort(
@@ -24,7 +30,11 @@ export function createDefaultProvisions(
infinityFallback(PROVISION_PREFERENCES.indexOf(a.id)) -
infinityFallback(PROVISION_PREFERENCES.indexOf(b.id)),
)
.sort((a, b) => (b.crew ?? 0) - (a.crew ?? 0))
.sort(
(a, b) =>
(scripts.provisions[b.id].bonusValues?.crewLevelIncrease ?? 0) -
(scripts.provisions[a.id].bonusValues?.crewLevelIncrease ?? 0),
)
.slice(0, tank.max_provisions)
.map(({ id }) => id);
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { staticAsset } from "@blitzkit/core";
import { fetchPB } from "../protobuf";
import { PopularTanks } from "../../../protos/src/blitzkit";
import { fetchPB } from "../api";
export function fetchPopularTanks() {
return fetchPB(staticAsset("definitions/popular-tanks.pb"), PopularTanks);
+1 -1
View File
@@ -1,6 +1,6 @@
import { staticAsset } from "@blitzkit/core";
import { fetchPB } from "../protobuf";
import { Reviews } from "../../../protos/src/blitzkit";
import { fetchPB } from "../api";
export function fetchReviews() {
return fetchPB(staticAsset("definitions/reviews.pb"), Reviews);
+2 -2
View File
@@ -1,10 +1,10 @@
export * from "../../protos/src/blitzkit";
export * from "./api";
export * from "./blitz";
export * from "./blitzkit";
export * from "./imgur";
export * from "./math";
export * from "./patreon";
export * from "./protobuf";
export * from "../../protos/src/blitzkit";
export * from "./statistics";
export * from "./streams";
export * from "./types";
-1
View File
@@ -1 +0,0 @@
export type PBMap<T> = T[keyof T] extends number ? T[keyof T] : never;
@@ -0,0 +1,76 @@
export type BlitzEffectScript = RSN & {
"#text": string;
attribute?: string;
cooldown?: number;
duration?: number;
startCooldown?: number;
automatic?: boolean;
keepSpoted?: boolean;
breakTracks?: boolean;
disableTracksCollisionDamage?: boolean;
simpleMode?: boolean;
shotEffect?: string;
contusionChances?: string;
modules?: string;
damageFactors?: string;
extrasMustBeStopped?: string;
targetExtrasMustBeStopped?: string;
conflictExtras?: string;
effectExtraName?: string;
clientEffectVariant?: string;
piercingFactors?: RSN;
visionRadiusPercentageBonus?: RSN;
bonus?: RSN;
enginePowerPercentageBonus?: RSN;
bonusPercentage?: RSN;
attributes?: RSN;
respawnBoost?: RSN;
maxHealthFactor?: RSN;
bonusValues?: BonusValues;
modulesHealthFactors?: ModuleConstants;
chancesToHit?: ChangesToHit;
respawns?: Respawns;
slowDebuff?: SlowDebuff;
};
type RSN = Record<string, number>;
type BonusValues = RSN & {
chancesToHit?: ModuleConstants;
modulesMaxHpFactors?: ModuleConstants;
modulesMaxRegenHpFactors?: ModuleConstants;
item?: RSN[];
};
interface ModuleConstants {
modules: Record<string, number | number[]>;
}
interface ModulesMaxHPFactorsModules {
ammoBayHealth: number;
}
interface ChangesToHit {
crew: number;
modules: RSN;
}
interface Respawns {
respawn: RSN;
}
interface SlowDebuff {
item: SlowDebuffItem[];
}
interface SlowDebuffItem {
duration: number;
enginePowerFactor: RSN;
speedLimitsFactor: RSN;
}
+7
View File
@@ -0,0 +1,7 @@
import { BlitzEffectScript } from "./blitzEffectScript";
export interface BlitzScripts {
consumables: Record<number, BlitzEffectScript>;
provisions: Record<number, BlitzEffectScript>;
equipment: Record<number, BlitzEffectScript>;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { ProvisionConsumableScript } from "@blitzkit/core/src/types/provisionConsumableScript";
import { BlitzEffectScript } from "./blitzEffectScript";
import { ConsumablesVehicleFilter } from "./consumablesVehicleFilter";
export interface ConsumablesCommon {
@@ -13,6 +13,6 @@ export interface ConsumablesCommon {
include: { vehicle: ConsumablesVehicleFilter; nations?: string };
exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string };
};
script: ProvisionConsumableScript;
script: BlitzEffectScript;
};
}
+3 -1
View File
@@ -1,10 +1,12 @@
import { BlitzEffectScript } from "./blitzEffectScript";
export interface OptionalDevices {
[key: string]: {
id: number;
userString: string;
description: string;
icon: string;
script: unknown;
script: BlitzEffectScript;
display_params: unknown;
};
}
@@ -1,10 +0,0 @@
export type ProvisionConsumableScript = {
"#text": string;
cooldown?: number;
automatic?: boolean;
bonusValues?: Record<string, number>;
attributes?: Record<string, number>;
shotEffect?: string;
} & Record<string, number>;
+2 -2
View File
@@ -1,4 +1,4 @@
import { ProvisionConsumableScript } from "@blitzkit/core/src/types/provisionConsumableScript";
import { BlitzEffectScript } from "./blitzEffectScript";
import { ConsumablesVehicleFilter } from "./consumablesVehicleFilter";
export interface ProvisionsCommon {
@@ -13,6 +13,6 @@ export interface ProvisionsCommon {
include: { vehicle: ConsumablesVehicleFilter; nations?: string };
exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string };
};
script: ProvisionConsumableScript;
script: BlitzEffectScript;
};
}
+40 -9
View File
@@ -1,14 +1,45 @@
{
"default": "en",
"supported": [
{ "locale": "en" },
{ "locale": "es" },
{ "locale": "fr" },
{ "locale": "ja" },
{ "locale": "pl" },
{ "locale": "pt" },
{ "locale": "ru" },
{ "locale": "uk" },
{ "locale": "zh", "blitz": "zh_Hans_SG" }
{
"locale": "en",
"flag": "🇺🇸"
},
{
"locale": "es",
"flag": "🇪🇸"
},
{
"locale": "fr",
"flag": "🇫🇷"
},
{
"locale": "ja",
"flag": "🇯🇵"
},
{
"locale": "pl",
"flag": "🇵🇱"
},
{
"locale": "pt",
"flag": "🇵🇹"
},
{
"locale": "ru",
"flag": "🇷🇺"
},
{
"locale": "uk",
"flag": "🇺🇦"
},
{
"locale": "zh",
"variant_blitz_local": "zh_Hans_SG",
"variant_blitz_cdn": "zh-Hans",
"flag": "🇨🇳"
}
]
}
+1 -66
View File
@@ -1,68 +1,3 @@
import { merge } from "ts-deepmerge";
import locales from "../locales.json";
import en from "../strings/en.json";
import es from "../strings/es.json";
import fr from "../strings/fr.json";
import ja from "../strings/ja.json";
import pl from "../strings/pl.json";
import pt from "../strings/pt.json";
import ru from "../strings/ru.json";
import uk from "../strings/uk.json";
import zh from "../strings/zh.json";
export type BlitzKitStrings = typeof en;
const stringsRaw: Record<string, TranslationTree> = {
en,
es,
fr,
ja,
pl,
pt,
ru,
uk,
zh,
};
export const STRINGS: Record<string, BlitzKitStrings> = {};
for (const locale in stringsRaw) {
if (locale === locales.default) {
STRINGS[locales.default] = stringsRaw[locales.default] as BlitzKitStrings;
continue;
}
const localeStrings = stringsRaw[locale];
STRINGS[locale] = merge(
stringsRaw[locales.default],
localeStrings,
) as BlitzKitStrings;
}
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<string, string> = {
en: "en",
es: "es",
fr: "fr",
ja: "ja",
pt: "pt",
ru: "ru",
uk: "uk",
zh: "zh-Hans",
pl: "pl",
};
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
en: "🇺🇸",
es: "🇪🇸",
fr: "🇫🇷",
ja: "🇯🇵",
pt: "🇵🇹",
ru: "🇷🇺",
uk: "🇺🇦",
zh: "🇨🇳",
pl: "🇵🇱",
};
export type TranslationTree = { [key: string]: TranslationNode };
export type TranslationNode = string | TranslationTree;
export type Strings = typeof en;
@@ -11,8 +11,6 @@ message ConsumableDefinitions {
message Consumable {
required uint32 id = 1;
required I18nString name = 2;
required uint32 cooldown = 3;
optional float duration = 4;
required bool game_mode_exclusive = 5;
repeated ConsumableTankFilter include = 6;
repeated ConsumableTankFilter exclude = 7;
@@ -17,7 +17,4 @@ message Provision {
required bool game_mode_exclusive = 5;
repeated ConsumableTankFilter include = 6;
repeated ConsumableTankFilter exclude = 7;
map<string, float> bonuses = 8;
map<string, string> effects = 9;
}
-49
View File
@@ -1,49 +0,0 @@
syntax = "proto3";
package blitzkit;
import "tank_definitions.proto";
/**
* Wargaming's script system for provisions, consumables, and equipment is
* genuinely the most repulsive and inconsistent XML schema I have ever seen.
* Here are my notes on how I normalize the data into BlitzKit. I can totally
* see why they want to move to Unreal Engine. This is unhealthy.
*/
message Script {
/**
* If #text is "StaticFactorDevice", type will be script.attribute, otherwise
* type will assume the value of #text.
*/
string type = 1;
/**
* Bonus values enjoys Record<string, number> entries from
* script.bonusValues.
*
* I also augment Record<string, number | ""> values from the root of the
* script object. That is, if an entry on the root script object isn't
* "attribute", "bonusValues", "#text", etc., it goes in here. Empty strings
* values get omitted.
*/
map<string, float> bonus_values = 2;
/**
* I thought piercing_factors distinct enough from bonus_values to separate
* them since the keys here are no longer properties of the tank, but
* specific shell types. The keys will be mapped to BlitzKit's ShellType.
*/
map<int32, float> piercing_factors = 3;
/**
* Same story here, the keys are different. But this time, the values are
* stored deep in script.modulesHealthFactors.modules. Also, all keys end
* with "Health" for no reason like "leftTrackHealth". That will be trimmed.
*/
map<string, float> module_health_factors = 4;
/**
*
*/
map<string, float> chances_to_hit = 5;
}
@@ -1,5 +1,7 @@
export function computeCharacteristics() {
const characteristics = {};
import type { ComputedCharacteristics } from "../types/characteristics";
return characteristics;
export function computeCharacteristics() {
const characteristics: Partial<ComputedCharacteristics> = {};
return characteristics as ComputedCharacteristics;
}
@@ -0,0 +1,183 @@
.section {
display: flex;
flex-direction: column;
gap: var(--space-4);
.header {
display: flex;
align-items: center;
gap: var(--space-3);
.upgrade-buttons {
display: flex;
}
}
.state {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.status {
display: flex;
flex-direction: column;
gap: var(--space-2);
.group {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
.state {
font-size: 1.25em;
&[data-selected="true"] {
img {
filter: sepia(1) saturate(10);
}
}
}
}
}
.consumables {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-3);
.consumable-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
.consumable {
font-size: var(--space-5);
img {
width: 1em;
height: 1em;
}
.info {
display: flex;
flex-direction: column;
.entry {
display: flex;
gap: var(--space-2);
svg {
width: 1em;
height: 1em;
}
}
}
}
}
}
.equipment {
display: flex;
flex-direction: column;
gap: var(--space-3);
.row {
display: flex;
gap: var(--space-3);
.slots {
display: flex;
flex-direction: column;
gap: var(--space-2);
align-items: center;
.options {
display: flex;
.slot {
aspect-ratio: 1 / 1;
padding: var(--space-3);
font-size: var(--space-5);
&[data-selected="true"] {
z-index: 1;
}
}
}
}
}
}
.lines {
display: flex;
flex-direction: column;
gap: var(--space-3);
.line {
display: flex;
gap: var(--space-3);
.chunks-wrapper {
display: flex;
flex-direction: column;
gap: var(--space-3);
.line-wrap {
display: flex;
align-items: center;
gap: var(--space-3);
.wrap-marker {
padding-bottom: var(--font-size-minor);
svg {
width: 1em;
height: 1em;
}
}
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
.stage {
position: relative;
padding: var(--space-3);
font-size: 2em;
.icon {
width: 1em;
height: 1em;
transform: translate(var(--space-1), var(--space-1));
}
.tier {
position: absolute;
left: var(--space-2);
top: var(--space-2);
}
.price {
position: absolute;
left: 50%;
bottom: var(--space-2);
transform: translateX(-50%);
}
}
}
}
}
}
}
}
@@ -0,0 +1,645 @@
import { useProtagonist } from "../../hooks/useProtagonist";
import { useStrings } from "../../hooks/useStrings";
import { Tankopedia } from "../../stores/tankopedia";
import { hasUpgrades } from "../../tankopedia/hasUpgrades";
import type { ComputedCharacteristics } from "../../types/characteristics";
interface TankopediaLoadoutProps {
characteristics: ComputedCharacteristics;
}
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
const tank = useProtagonist();
const showModules = hasUpgrades(tank);
return (
<>
{showModules && <Modules />}
<Equipment />
<Consumables characteristics={characteristics} />
<State />
<Status />
</>
);
}
function State() {
const strings = useStrings();
const shell = Tankopedia.use((state) => state.protagonist.shell);
const speed = Tankopedia.use((state) => state.protagonist.speed);
return (
<div className={styles.section}>
<div className={styles.header}>
<Heading size="4">{strings.tanks.loadout.dynamics}</Heading>
<IconButton
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.consumables = [];
});
}}
>
<TrashIcon />
</IconButton>
</div>
<div className={styles.state}>
<span>shell: {shell}</span>
<span>speed: {speed}</span>
</div>
</div>
);
}
function Status() {
const strings = useStrings();
const status = Tankopedia.use((state) => state.protagonist.status);
return (
<div className={styles.section}>
<div className={styles.header}>
<Heading size="4">{strings.tanks.loadout.status}</Heading>
<IconButton
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.consumables = [];
});
}}
>
<TrashIcon />
</IconButton>
</div>
<div className={styles.status}>
{vehicleStatusKeys.map((group, index) => (
<div key={index} className={styles.group}>
{group.map((key) => (
<IconButton
key={key}
radius="1"
className={styles.state}
data-selected={status[key]}
color={status[key] ? "amber" : "gray"}
variant={status[key] ? "surface" : "soft"}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.status[key] =
!draft.protagonist.status[key];
});
}}
>
<img src={`/media/status/${key}.png`} />
</IconButton>
))}
</div>
))}
</div>
</div>
);
}
interface ConsumablesProps {
characteristics: ComputedCharacteristics;
}
function Consumables({ characteristics }: ConsumablesProps) {
const strings = useStrings();
const consumables = useConsumables();
const tank = useProtagonist();
const isCompatible = useCompatibility(tank, characteristics);
return (
<div className={styles.section}>
<div className={styles.header}>
<Heading size="4">{strings.tanks.loadout.consumables}</Heading>
<IconButton
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.consumables = [];
});
}}
>
<TrashIcon />
</IconButton>
</div>
<div className={styles.consumables}>
{Object.entries(consumables.consumables).map(
([id, { compatibility, consumable, purchase }]) => {
if (!isCompatible(compatibility!)) return null;
return (
<Consumable
key={id}
id={id}
consumable={consumable!}
purchase={purchase!}
/>
);
},
)}
</div>
</div>
);
}
interface ConsumableProps {
id: string;
consumable: ConsumableComponent;
purchase: BlitzStaticPurchaseComponent;
}
function Consumable({ id, consumable, purchase }: ConsumableProps) {
const gameStrings = useGameStrings("ConsumableEntity");
const isSelected = Tankopedia.use((state) =>
state.protagonist.consumables.includes(id),
);
const tank = useProtagonist();
const tierPrices = useTierPrices();
const price = useMemo(() => {
const { prices } = tierPrices.prices[purchase.price_per_tier_catalog_id];
for (const { tier_catalog_id, unlock_price } of prices) {
if (tier_catalog_id === tank.tank!.tier_catalog_id) {
return unlock_price;
}
}
throw new Error(
`No price found for tier catalog id ${tank.tank!.tier_catalog_id}`,
);
}, [tank.id, purchase.price_per_tier_catalog_id]);
return (
<div className={styles["consumable-wrapper"]}>
<Tooltip tooltip={gameStrings[consumable!.name_key]}>
<Button
radius="1"
variant={isSelected ? "surface" : "soft"}
color={isSelected ? undefined : "gray"}
className={styles.consumable}
onClick={() => {
Tankopedia.mutate((draft) => {
const index = draft.protagonist.consumables.indexOf(id);
if (index === -1) {
draft.protagonist.consumables.push(id);
} else {
draft.protagonist.consumables.splice(index, 1);
}
});
}}
>
<img src={`/media/consumables/${id}.webp`} />
<div className={styles.info}>
<Text lowContrast size="minor">
<div className={styles.entry}>
<ClockIcon />
10s
</div>
</Text>
<Text lowContrast size="minor">
<div className={styles.entry}>
<ResetIcon />
10s
</div>
</Text>
</div>
</Button>
</Tooltip>
<Price price={price} />
</div>
);
}
function Equipment() {
const strings = useStrings();
const tank = useProtagonist();
const equipment = useEquipment(tank.tank!);
return (
<div className={styles.section}>
<div className={styles.header}>
<Heading size="4">{strings.tanks.loadout.equipment}</Heading>
<IconButton
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
for (const key in draft.protagonist.equipment) {
delete draft.protagonist.equipment[key];
}
});
}}
>
<TrashIcon />
</IconButton>
</div>
<div className={styles.equipment}>
{chunk(equipment.preset.slots, equipmentColumns).map((chunk, index) => (
<EquipmentRow key={index} chunk={chunk} rowIndex={index} />
))}
</div>
</div>
);
}
interface EquipmentRowProps {
chunk: BlitzStaticEquipmentPresetComponent_EquipmentSlot[];
rowIndex: number;
}
function EquipmentRow({ chunk, rowIndex }: EquipmentRowProps) {
return (
<div className={styles.row}>
{chunk.map((slot, index) => (
<EquipmentSlot
key={index}
slot={slot}
rowIndex={rowIndex}
columnIndex={index}
/>
))}
</div>
);
}
interface EquipmentSlotProps {
slot: BlitzStaticEquipmentPresetComponent_EquipmentSlot;
rowIndex: number;
columnIndex: number;
}
function EquipmentSlot({ slot, rowIndex, columnIndex }: EquipmentSlotProps) {
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
const tank = useProtagonist();
const equipment = useEquipment(tank.tank!);
const { price } = equipment.price.unlock_slot_prices[equipmentIndex];
return (
<div className={styles.slots}>
<div className={styles.options}>
{slot.options_catalog_i_ds.map((id, index) => (
<EquipmentOption
key={id}
optionIndex={index}
rowIndex={rowIndex}
columnIndex={columnIndex}
/>
))}
</div>
<Price price={price!} />
</div>
);
}
interface EquipmentOptionProps {
optionIndex: number;
rowIndex: number;
columnIndex: number;
}
function EquipmentOption({
optionIndex,
rowIndex,
columnIndex,
}: EquipmentOptionProps) {
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
const tank = useProtagonist();
const equipment = useEquipment(tank.tank!);
const slot = equipment.preset.slots[equipmentIndex];
const id = slot.options_catalog_i_ds[optionIndex];
const gameStrings = useGameStrings("EquipmentEntity");
const isSelected = Tankopedia.use(
(state) =>
equipmentIndex in state.protagonist.equipment &&
state.protagonist.equipment[equipmentIndex] === optionIndex,
);
return (
<Tooltip tooltip={gameStrings[equipment.equipments[id].equipment_name]}>
<Button
color={isSelected ? undefined : "gray"}
variant={isSelected ? "surface" : "soft"}
data-selected={isSelected}
radius="1"
parentArray
className={styles.slot}
onClick={() => {
Tankopedia.mutate((draft) => {
if (isSelected) {
delete draft.protagonist.equipment[equipmentIndex];
} else {
draft.protagonist.equipment[equipmentIndex] = optionIndex;
}
});
}}
>
<img src={`/media/equipment/${id}.webp`} />
</Button>
</Tooltip>
);
}
function Modules() {
const strings = useStrings();
const tank = useProtagonist();
const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
const alternates = Tankopedia.use((state) => state.protagonist.alternates);
const isStock = useMemo(() => {
return tank.tank!.upgrade_lines.every((line) => {
if (isAlternativeLine(line.name)) return true;
return upgrades[line.name] === 0;
});
}, [upgrades]);
const isUpgraded = useMemo(() => {
return tank.tank!.upgrade_lines.every((line) => {
if (isAlternativeLine(line.name)) return true;
return (
upgrades[line.name] === line.stages.length - 1 ||
(line.name in alternates && alternates[line.name])
);
});
}, [upgrades]);
return (
<div className={styles.section}>
<div className={styles.header}>
<Heading size="4">{strings.tanks.loadout.modules}</Heading>
<div className={styles["upgrade-buttons"]}>
<IconButton
variant={isStock ? "solid" : "soft"}
size="minor"
array
onClick={() => {
Tankopedia.mutate((draft) => {
for (const line of tank.tank!.upgrade_lines) {
if (isAlternativeLine(line.name)) continue;
draft.protagonist.upgrades[line.name] = 0;
draft.protagonist.alternates[line.name] = false;
}
});
}}
color="gray"
>
<DoubleArrowLeftIcon />
</IconButton>
<IconButton
variant={isUpgraded ? "solid" : "soft"}
size="minor"
array
onClick={() => {
Tankopedia.mutate((draft) => {
for (const line of tank.tank!.upgrade_lines) {
draft.protagonist.upgrades[line.name] =
line.stages.length - 1;
if (isAlternativeLine(line.name)) {
const original = originalLineName(line.name)!;
draft.protagonist.alternates[original] = true;
}
}
});
}}
color="gray"
>
<DoubleArrowRightIcon />
</IconButton>
</div>
</div>
<div className={styles.lines}>
{/* TODO: hide lines with 1 module except for alts */}
{tank.tank!.upgrade_lines.map((line) => (
<Line
key={line.name}
name={line.name}
lines={tank.tank!.upgrade_lines}
/>
))}
</div>
</div>
);
}
interface LineProps {
name: string;
lines: UpgradeLine[];
}
function Line({ name, lines }: LineProps) {
const combinedLines = useMemo(() => {
const names: string[] = [name];
for (const originalLine in alternativeLines) {
const alternativeLine = alternativeLines[originalLine];
if (
originalLine === name &&
lines.some((line) => line.name === alternativeLine)
) {
names.push(alternativeLine);
}
}
return names;
}, [name]);
return (
<div className={styles.line}>
{!isAlternativeLine(name) &&
combinedLines.map((name) => (
<div className={styles["chunks-wrapper"]}>
<LineInner key={name} name={name} lines={lines} />
</div>
))}
</div>
);
}
function LineInner({ name, lines }: LineProps) {
const line = lines.find((line) => line.name === name)!;
return chunk(line.stages, maxModulesPerRow).map((chunk, wrapIndex) => (
<div className={styles["line-wrap"]}>
{wrapIndex > 0 && (
<Text size="minor" className={styles["wrap-marker"]} lowContrast>
<CornerBottomLeftIcon />
</Text>
)}
{chunk.map((stage, elementIndex) => (
<LineElement
key={elementIndex}
index={wrapIndex * maxModulesPerRow + elementIndex}
lineName={name}
stage={stage}
/>
))}
</div>
));
}
interface LineElementProps {
index: number;
lineName: string;
stage: StageParameters;
}
function LineElement({ index, lineName, stage }: LineElementProps) {
const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
const alternates = Tankopedia.use((state) => state.protagonist.alternates);
const tank = useProtagonist();
const strings = useStrings();
const gameStrings = useGameStrings("TankEntity");
const upgradePreset = useUpgradePreset(tank.tank!.tank_upgrade_preset);
let price: StandardPrice | undefined;
switch (stage.grade) {
case Grade.GRADE_COMMON:
price = upgradePreset.common_grade_price;
break;
case Grade.GRADE_RARE:
price = upgradePreset.rare_grade_price;
break;
case Grade.GRADE_EPIC:
price = upgradePreset.unique_grade_price;
break;
case Grade.GRADE_LEGENDARY:
price = upgradePreset.legendary_grade_price;
break;
}
for (const override of upgradePreset.prices_overrides) {
if (override.stage_tech_name !== stage.tech_name) continue;
price = override.price;
}
price ??= StandardPrice.create();
let isSelected: boolean;
if (isAlternativeLine(lineName)) {
const originalLine = originalLineName(lineName);
isSelected = alternates[originalLine!];
} else {
if (alternates[lineName]) {
isSelected = false;
} else {
isSelected = upgrades[lineName] === index;
}
}
return (
<div className={styles.wrapper}>
<Tooltip tooltip={gameStrings[stage.display_name]}>
<Button
className={styles.stage}
color={isSelected ? undefined : "gray"}
variant={isSelected ? "surface" : "soft"}
radius="1"
onClick={() => {
// TODO: make minimum selectable upgrade the last free module for tanks like the destiny
Tankopedia.mutate((draft) => {
const tankData = tank.tank!;
if (isAlternativeLine(lineName)) {
const originalLine = originalLineName(lineName)!;
draft.protagonist.alternates[originalLine] = true;
draft.protagonist.upgrades[lineName] = index;
} else {
draft.protagonist.alternates[lineName] = false;
draft.protagonist.upgrades[lineName] = index;
}
for (const required of stage.required_upgrades) {
for (const line of tankData.upgrade_lines) {
let i = 0;
for (const candidateStage of line.stages) {
if (candidateStage.tech_name === required) {
draft.protagonist.upgrades[line.name] = Math.max(
draft.protagonist.upgrades[line.name],
i,
);
}
i++;
}
}
}
for (const line of tankData.upgrade_lines) {
if (draft.protagonist.alternates[line.name]) {
continue;
}
let i = draft.protagonist.upgrades[line.name];
while (i > 0) {
const currentStage = line.stages[i];
const valid = currentStage.required_upgrades.every(
(required) => {
for (const otherLine of tankData.upgrade_lines) {
const idx = draft.protagonist.upgrades[otherLine.name];
if (otherLine.stages[idx]?.tech_name === required) {
return true;
}
}
return false;
},
);
if (valid) break;
i--;
}
draft.protagonist.upgrades[line.name] = i;
}
});
}}
>
<img
className={styles.icon}
src={`/media/modules/${stage.stage_type}.webp`}
/>
<Text weight="light" lowContrast size="minor" className={styles.tier}>
{isAlternativeLine(lineName)
? strings.tanks.loadout.alternative
: romanize(stage.number)}
</Text>
</Button>
</Tooltip>
{<Price className={styles.price} price={price} />}
</div>
);
}
@@ -1,39 +0,0 @@
import {
createDefaultProvisions,
Equalizer,
type ProvisionDefinitions,
type TankDefinition,
} from "@blitzkit/core";
import type { DuelMember } from "../../stores/duel";
import { genericDefaultEquipmentMatrix } from "../../stores/duel/constants";
export function tankToDuelMember(
tank: TankDefinition,
provisionDefinitions: ProvisionDefinitions,
) {
const turret = tank.turrets.at(-1)!;
const gun = turret.guns.at(-1)!;
return {
camouflage: true,
consumables: [],
equipmentMatrix: genericDefaultEquipmentMatrix,
cooldownBooster: 0,
provisions: createDefaultProvisions(tank, gun, provisionDefinitions),
engine: tank.engines.at(-1)!,
gun,
shell: tank.turrets.at(-1)!.guns.at(-1)!.shells[0],
tank,
track: tank.tracks.at(-1)!,
turret,
assaultDistance: (gun.assault_ranges?.ranges[0].distance ?? 0) / 2,
} satisfies DuelMember;
}
export const defaultEqualizer: Equalizer = {
armor: 1,
damage: 1,
health: 1,
module_health: 1,
penetration: 1,
};
@@ -1,6 +0,0 @@
import { STRINGS } from "@blitzkit/i18n";
export function getStrings(locale: string) {
if (locale in STRINGS) return STRINGS[locale];
throw new Error(`Unsupported locale: ${locale}`);
}
+3 -11
View File
@@ -1,11 +1,8 @@
import { unwrapper } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import { createContext, useContext, type ReactNode } from "react";
import { getStrings } from "../core/i18n/getStrings";
const LocaleContext = createContext<{
locale: string;
localeRaw: string | undefined;
} | null>(null);
interface LocaleProviderProps extends LocaleAcceptorProps {
@@ -14,9 +11,7 @@ interface LocaleProviderProps extends LocaleAcceptorProps {
export function LocaleProvider({ locale, children }: LocaleProviderProps) {
return (
<LocaleContext.Provider
value={{ locale: locale ?? locales.default, localeRaw: locale }}
>
<LocaleContext.Provider value={{ locale: locale ?? locales.default }}>
{children}
</LocaleContext.Provider>
);
@@ -29,12 +24,9 @@ export function useLocale() {
throw new Error("useLocale must be used within a LocaleProvider");
}
const strings = getStrings(context.locale);
const unwrap = unwrapper(context.locale);
return { locale: context.locale, strings, unwrap };
return context.locale;
}
export interface LocaleAcceptorProps {
locale: string | undefined;
locale: string;
}
@@ -0,0 +1,10 @@
import { api } from "../blitzkit/api";
import { Tankopedia } from "../stores/tankopedia";
import { useAwait } from "./useAwait";
export function useProtagonist() {
const id = Tankopedia.use((state) => state.protagonist.tank);
const tank = useAwait(() => api.tank(id), `tank-${id}`);
return tank;
}
+9
View File
@@ -0,0 +1,9 @@
import { api } from "../blitzkit/api";
import { useAwait } from "./useAwait";
import { useLocale } from "./useLocale";
export function useStrings() {
const locale = useLocale();
const strings = useAwait(() => api.strings(locale), `strings-${locale}`);
return strings;
}
@@ -1,54 +1,9 @@
import {
ConsumableDefinitions,
type ConsumablesCommon,
type OptionalDevices,
} from "@blitzkit/core";
import { ConsumableDefinitions } from "@blitzkit/core";
import { api } from "../../../blitzkit/api";
import { vfs } from "../../../core/blitzkit/vfs";
export { getStaticPaths } from "../_index";
export async function GET() {
const samples: Record<string, any> = {};
const optionalDevices = await vfs.xml<{
root: OptionalDevices;
}>("Data/XML/item_defs/vehicles/common/optional_devices.xml");
for (const key in optionalDevices.root) {
samples[key] = optionalDevices.root[key].script;
}
const consumablesRoot = "Data/XML/item_defs/vehicles/common/consumables";
const consumablesList = await vfs.text(`${consumablesRoot}/list.xml`);
const consumablesListEntryPattern = /<items path="(\w+)\.xml"\/>/gm;
for (const match of consumablesList.matchAll(consumablesListEntryPattern)) {
const group = match[1];
const path = `${consumablesRoot}/${group}.xml`;
const consumables = await vfs.xml<{ root: ConsumablesCommon }>(path);
for (const key in consumables.root) {
samples[key] = consumables.root[key].script;
}
}
const provisionsRoot = "Data/XML/item_defs/vehicles/common/provisions";
const provisionsList = await vfs.text(`${provisionsRoot}/list.xml`);
const provisionsListEntryPattern = /<items path="(\w+)\.xml"\/>/gm;
for (const match of provisionsList.matchAll(provisionsListEntryPattern)) {
const group = match[1];
const path = `${provisionsRoot}/${group}.xml`;
const provisions = await vfs.xml<{ root: ConsumablesCommon }>(path);
for (const key in provisions.root) {
samples[key] = provisions.root[key].script;
}
}
return Response.json(samples);
const definitions = await api.consumables();
const bytes = ConsumableDefinitions.encode(definitions).finish();
@@ -5,9 +5,6 @@ export { getStaticPaths } from "../_index";
export async function GET() {
const definitions = await api.provisions();
return Response.json(definitions);
const bytes = ProvisionDefinitions.encode(definitions).finish();
return new Response(bytes);
@@ -0,0 +1,8 @@
import { api } from "../../../blitzkit/api";
export { getStaticPaths } from "../_index";
export async function GET() {
const definitions = await api.scripts();
return Response.json(definitions);
}
@@ -1,9 +1,9 @@
import type { ConsumablesCommon } from "@blitzkit/core";
import type { APIContext, GetStaticPathsItem } from "astro";
import sharp from "sharp";
import { mixStaticPaths } from "../../../../astro/mixStaticPaths";
import { extractPackedIcon } from "../../../../core/blitz/extractPackedIcon";
import { parsePackedSpriteRect } from "../../../../core/blitz/parsePackedSpriteRect";
import { mixStaticPaths } from "../../../../astro/mixStaticPaths";
import { vfs } from "../../../../core/blitzkit/vfs";
import { getStaticPaths as _getStaticPaths } from "../../_index";
@@ -47,6 +47,8 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
),
);
console.log(consumable);
if (!styleSheet) {
console.warn(
`No style sheet found for consumable ${consumable.icon}; skipping...`,
@@ -0,0 +1,14 @@
import locales from "@blitzkit/i18n/locales.json";
import type { APIContext } from "astro";
import { mixStaticPaths } from "../../../astro/mixStaticPaths";
import { api } from "../../../blitzkit/api";
import { getStaticPaths as _getStaticPaths } from "../_index";
export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
return locales.supported.map(({ locale }) => ({ params: { locale } }));
});
export async function GET({ params }: APIContext<never, { locale: string }>) {
const strings = await api.strings(params.locale);
return Response.json(strings);
}
@@ -4,7 +4,6 @@ import { computeCharacteristics } from "../../../../../blitzkit/computeCharacter
import { withErrorWrapper } from "../../../../../hocs/withErrorWrapper";
import { withLocale } from "../../../../../hocs/withLocale";
import { useAwait } from "../../../../../hooks/useAwait";
import { useEquipment } from "../../../../../hooks/useEquipment";
import { Tankopedia } from "../../../../../stores/tankopedia";
import styles from "./_index.module.css";
@@ -19,7 +18,7 @@ export const Page = withErrorWrapper(
Tankopedia.useInitialization(protagonistTank);
const protagonist = Tankopedia.use((state) => state.protagonist);
const protagonistEquipment = useEquipment(protagonistTank.tank!);
// const protagonistEquipment = useEquipment(protagonistTank.tank!);
const characteristics = useMemo(
() => computeCharacteristics(),
@@ -1,7 +0,0 @@
import type { EquipmentMatrix } from '.';
export const genericDefaultEquipmentMatrix: EquipmentMatrix = [
[-1, 1, -1],
[0, 0, 0],
[0, 0, 0],
];
-56
View File
@@ -1,56 +0,0 @@
import {
ModelDefinition,
type EngineDefinition,
type GunDefinition,
type ShellDefinition,
type TankDefinition,
type TrackDefinition,
type TurretDefinition,
} from "@blitzkit/core";
import { Varuna } from "varuna";
import { api } from "../../blitzkit/api";
import { tankToDuelMember } from "../../core/blitzkit/tankToDuelMember";
type EquipmentMatrixItem = -1 | 0 | 1;
type EquipmentMatrixRow = [
EquipmentMatrixItem,
EquipmentMatrixItem,
EquipmentMatrixItem,
];
export type EquipmentMatrix = [
EquipmentMatrixRow,
EquipmentMatrixRow,
EquipmentMatrixRow,
];
export interface DuelMember {
tank: TankDefinition;
engine: EngineDefinition;
turret: TurretDefinition;
gun: GunDefinition;
shell: ShellDefinition;
track: TrackDefinition;
equipmentMatrix: EquipmentMatrix;
consumables: number[];
provisions: number[];
camouflage: boolean;
cooldownBooster: number;
assaultDistance: number;
}
export interface DuelStore {
protagonist: DuelMember;
antagonist: DuelMember;
equalize: boolean;
}
const provisionDefinitions = await api.provisions();
export const Duel = new Varuna<
DuelStore,
{ tank: TankDefinition; model: ModelDefinition }
>(({ tank, model }) => {
const protagonist = tankToDuelMember(tank, provisionDefinitions);
return { protagonist, antagonist: protagonist, model, equalize: false };
});
+38 -5
View File
@@ -1,6 +1,7 @@
import {
createDefaultSkills,
ShellDefinition,
TankDefinition,
type ModelDefinition,
} from "@blitzkit/core";
import type { Vector3 } from "three";
@@ -9,6 +10,10 @@ import { api } from "../blitzkit/api";
import type { ArmorType } from "../components/Armor/components/SpacedArmorScene";
import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent";
import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection";
import {
createTankState,
type EquipmentMatrix,
} from "../tankopedia/createTankState";
import { TankopediaDisplay } from "./tankopediaPersistent/constants";
export interface ShotLayerBase {
@@ -76,13 +81,37 @@ export enum TankopediaRelativeAgainst {
All,
}
export interface TankState {
tank: number;
engine: number;
turret: number;
gun: number;
shell: number;
track: number;
assault_distance: number;
speed: number;
model: ModelDefinition;
equipment_matrix: EquipmentMatrix;
consumables: number[];
provisions: number[];
camouflage: boolean;
cooldown_booster: number;
}
interface Tankopedia {
disturbed: boolean;
revealed: boolean;
protagonist: TankState;
shot?: Shot;
skills: Record<string, number>;
relativeAgainst: TankopediaRelativeAgainst;
model: ModelDefinition;
editStatic: boolean;
highlightArmor?: {
editingPlate: boolean;
@@ -105,15 +134,19 @@ interface Tankopedia {
statSearch?: string;
}
const skillDefinitions = await api.skills();
const skills = await api.skills();
const models = await api.models();
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
export const Tankopedia = new Varuna<Tankopedia, TankDefinition>((tank) => ({
disturbed: false,
revealed: false,
protagonist: createTankState(tank),
model: models.models[tank.id],
relativeAgainst: TankopediaRelativeAgainst.Class,
editStatic: false,
skills: createDefaultSkills(skillDefinitions),
model,
skills: createDefaultSkills(skills),
xpMultiplier: 1,
requestedDisplay: TankopediaDisplay.Model,
display: TankopediaDisplay.Model,
@@ -0,0 +1,45 @@
import { createDefaultProvisions, type TankDefinition } from "@blitzkit/core";
import { api } from "../blitzkit/api";
import type { TankState } from "../stores/tankopedia";
export type EquipmentMatrix = [
[number, number, number],
[number, number, number],
[number, number, number],
];
const models = await api.models();
const provisions = await api.provisions();
const scripts = await api.scripts();
export const genericDefaultEquipmentMatrix: EquipmentMatrix = [
[-1, 1, -1],
[0, 0, 0],
[0, 0, 0],
];
export function createTankState(tank: TankDefinition) {
const turret = tank.turrets.at(-1)!;
const gun = turret.guns.at(-1)!;
return {
tank: tank.id,
model: models.models[turret.id],
engine: tank.engines.at(-1)!.id,
turret: turret.id,
gun: gun.id,
shell: gun.shells[0].id,
track: tank.tracks.at(-1)!.id,
assault_distance: (gun.assault_ranges?.ranges[0].distance ?? 0) / 2,
speed: 0,
equipment_matrix: genericDefaultEquipmentMatrix,
consumables: [],
provisions: createDefaultProvisions(tank, gun, provisions, scripts),
camouflage: true,
cooldown_booster: 0,
} satisfies TankState;
}
@@ -0,0 +1,10 @@
import type { TankDefinition } from "@blitzkit/core";
export function hasUpgrades(tank: TankDefinition) {
return (
tank.tracks.length > 1 ||
tank.engines.length > 1 ||
tank.turrets.length > 1 ||
tank.turrets.some((turret) => turret.guns.length > 1)
);
}
@@ -23,7 +23,7 @@ interface CharacteristicContext {
assault_distance: number;
characteristic(
name: keyof typeof characteristics,
name: CharacteristicName,
): ReturnType<Characteristic["compute"]>;
}
@@ -31,3 +31,11 @@ export enum CharacteristicType {
Enum,
Number,
}
type CharacteristicName = keyof typeof characteristics;
type CharacteristicReturnType = ReturnType<Characteristic["compute"]>;
export type ComputedCharacteristics = Record<
CharacteristicName,
CharacteristicReturnType
>;