camouflages.ts•3.97 kB
import { readXML } from './xml.js';
import { getString } from './strings.js';
import type { Config } from '../config.js';
import type { Camouflage, TankClass } from '../data/types.js';
interface CamoCategory {
lightTank: number;
mediumTank: number;
heavyTank: number;
'AT-SPG': number;
}
interface RawCamouflage {
id: number;
userString: string;
description?: string;
category: string;
group: string;
kind: 'summer' | 'winter' | 'desert';
notInShop?: boolean;
vehicleFilter?: {
include?: { nations?: string | string[] };
exclude?: unknown;
};
}
interface CamouflagesXML {
root: {
groups: Record<string, { userString: string; rarity: number }>;
invisibility: {
categories: Record<string, CamoCategory>;
};
camouflages: Record<string, RawCamouflage>;
};
}
export interface CamoBonusTable {
[category: string]: {
lightTank: number;
mediumTank: number;
heavyTank: number;
'AT-SPG': number;
};
}
let camouflagesCache: Camouflage[] | null = null;
let bonusTableCache: CamoBonusTable | null = null;
export async function loadCamouflages(config: Config): Promise<void> {
if (camouflagesCache) return;
camouflagesCache = [];
bonusTableCache = {};
const data = await readXML<CamouflagesXML>(config.paths.camouflages);
for (const [categoryName, bonuses] of Object.entries(data.root.invisibility.categories)) {
bonusTableCache[categoryName] = {
lightTank: bonuses.lightTank,
mediumTank: bonuses.mediumTank,
heavyTank: bonuses.heavyTank,
'AT-SPG': bonuses['AT-SPG'],
};
}
for (const [camoTag, rawCamo] of Object.entries(data.root.camouflages)) {
if (typeof rawCamo !== 'object' || !rawCamo.id) continue;
const nameKey = rawCamo.userString?.replace('#', '') ?? camoTag;
const name = getString(nameKey);
const nations = rawCamo.vehicleFilter?.include?.nations;
let nationList: string[] | undefined;
if (nations) {
nationList = Array.isArray(nations) ? nations : [nations];
}
const descKey = rawCamo.description?.startsWith('#vehicle_customization:')
? rawCamo.description
: rawCamo.description?.replace('#', '');
const categoryBonus = bonusTableCache[rawCamo.category];
camouflagesCache.push({
id: rawCamo.id,
name: name !== nameKey ? name : formatCamoName(camoTag),
description: descKey ? getString(descKey) : undefined,
category: rawCamo.category,
kind: rawCamo.kind ?? 'universal',
nations: nationList,
invisibilityBonus: categoryBonus,
});
}
}
function formatCamoName(tag: string): string {
return tag
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
export function getCamouflages(): Camouflage[] {
return camouflagesCache ?? [];
}
export function getCamouflage(id: number): Camouflage | undefined {
return camouflagesCache?.find(c => c.id === id);
}
export function getCamouflageByName(name: string): Camouflage | undefined {
if (!camouflagesCache) return undefined;
const lowerName = name.toLowerCase();
return camouflagesCache.find(c => c.name.toLowerCase().includes(lowerName));
}
export function getCamouflageBonus(category: string, tankClass: TankClass): number {
return bonusTableCache?.[category]?.[tankClass] ?? 0;
}
export function getCamoBonusTable(): CamoBonusTable {
return bonusTableCache ?? {};
}
export function searchCamouflages(options: {
nation?: string;
kind?: 'summer' | 'winter' | 'desert' | 'universal';
query?: string;
}): Camouflage[] {
let results = getCamouflages();
if (options.nation) {
results = results.filter(c => !c.nations || c.nations.includes(options.nation!));
}
if (options.kind) {
results = results.filter(c => c.kind === options.kind);
}
if (options.query) {
const q = options.query.toLowerCase();
results = results.filter(c => c.name.toLowerCase().includes(q));
}
return results;
}