mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 20:19:49 +02:00
Compare commits
10
Commits
267bb271c9
...
4e470599e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e470599e8 | ||
|
|
5f24887535 | ||
|
|
99317c2075 | ||
|
|
99965118b3 | ||
|
|
7592381b12 | ||
|
|
7d02f6311f | ||
|
|
6c9ce58af5 | ||
|
|
402b6d5113 | ||
|
|
1733180f2c | ||
|
|
11c2c9749c |
No files matched your search
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"words": [
|
||||
"Debuff",
|
||||
"gamemodes",
|
||||
"Passability",
|
||||
"Recoverkit",
|
||||
"Repairkit",
|
||||
"Respawns",
|
||||
"Tankopedia"
|
||||
]
|
||||
}
|
||||
@@ -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.
+12
-2
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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`));
|
||||
}
|
||||
}
|
||||
@@ -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,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,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,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);
|
||||
|
||||
@@ -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 +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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BlitzEffectScript } from "./blitzEffectScript";
|
||||
|
||||
export interface BlitzScripts {
|
||||
consumables: Record<number, BlitzEffectScript>;
|
||||
provisions: Record<number, BlitzEffectScript>;
|
||||
equipment: Record<number, BlitzEffectScript>;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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],
|
||||
];
|
||||
@@ -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 };
|
||||
});
|
||||
@@ -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
|
||||
>;
|
||||
Reference in new issue
Block a user