crew.ts•4 kB
import { readXML } from './xml.js';
import { getString } from './strings.js';
import { join } from 'path';
import type { TankClass } from '../data/types.js';
interface RawSkillData {
userString?: string;
effectDescription?: string;
tipDescription?: string;
featuresDescription?: string;
type?: string;
[key: string]: unknown;
}
interface RawAvatarData {
root: {
skillsByClasses: Record<string, string>;
skills: Record<string, RawSkillData>;
};
}
export interface CrewSkill {
id: string;
name: string;
effect: string;
tip?: string;
features?: string;
type: 'trigger' | 'continuous';
tankClasses: TankClass[];
bonuses: Record<string, number>;
}
let skillsCache: CrewSkill[] = [];
let skillsByClass: Record<TankClass, string[]> = {
lightTank: [],
mediumTank: [],
heavyTank: [],
'AT-SPG': [],
};
function parseLocalizedString(key: string | undefined): string {
if (!key) return '';
const result = getString(key);
return result
.replace(/%\(highlight_start\)s?/g, '')
.replace(/%\(highlight_end\)s?/g, '')
.replace(/%\((\w+)\)s?/g, (_, name) => `{${name}}`);
}
function extractBonuses(skill: RawSkillData): Record<string, number> {
const bonuses: Record<string, number> = {};
const bonusKeys = [
'chancePerLevel',
'shotDispersionFactor',
'enginePowerFactorPerLevel',
'shotDispersionFactorPerLevel',
'shotInBurstDispersionFactorPerLevel',
'activationChancePerLevel',
'rangedHitDamageCoefficient',
'captureSpeedFactorPerLevel',
'reloadFactorPerLevel',
'armourPiercingFactor',
'xpBonusFactorPerLevel',
'penaltyFactorPerLevel',
'turretRotationSpeedFactorPerLevel',
'healthBurnPerSecLossFactorPerLevel',
'gunReloadTimeFactorPerLevel',
'repairSpeedFactorPerLevel',
'deviceChanceToHitBoost',
'visionRadiusFactorPerLevel',
'aimingFactorPerLevel',
'camouflageFactorPerLevel',
'rotationSpeedFactorPerLevel',
'enemyInRadius',
'enemyCount',
'vehicleHealthFraction',
];
for (const key of bonusKeys) {
if (typeof skill[key] === 'number') {
bonuses[key] = skill[key] as number;
} else if (typeof skill[key] === 'string') {
const num = parseFloat(skill[key] as string);
if (!isNaN(num)) bonuses[key] = num;
}
}
return bonuses;
}
export async function loadCrewSkills(vehiclesPath: string): Promise<void> {
const avatarPath = join(vehiclesPath, '..', 'tankmen', 'avatar.xml');
const data = await readXML<RawAvatarData>(avatarPath);
const classSkills = data.root.skillsByClasses;
for (const tankClass of Object.keys(classSkills) as TankClass[]) {
const skillList = classSkills[tankClass];
skillsByClass[tankClass] = typeof skillList === 'string' ? skillList.split(/\s+/) : [];
}
const skills = data.root.skills;
skillsCache = [];
for (const [id, rawSkill] of Object.entries(skills)) {
const skill = rawSkill as RawSkillData;
const tankClasses: TankClass[] = [];
for (const [tankClass, skillIds] of Object.entries(skillsByClass)) {
if (skillIds.includes(id)) {
tankClasses.push(tankClass as TankClass);
}
}
skillsCache.push({
id,
name: parseLocalizedString(skill.userString),
effect: parseLocalizedString(skill.effectDescription),
tip: skill.tipDescription ? parseLocalizedString(skill.tipDescription) : undefined,
features: skill.featuresDescription ? parseLocalizedString(skill.featuresDescription) : undefined,
type: (skill.type as 'trigger' | 'continuous') || 'continuous',
tankClasses,
bonuses: extractBonuses(skill),
});
}
}
export function getCrewSkills(): CrewSkill[] {
return skillsCache;
}
export function getSkillsByClass(tankClass: TankClass): CrewSkill[] {
const skillIds = skillsByClass[tankClass];
return skillsCache.filter(s => skillIds.includes(s.id));
}
export function getSkillById(id: string): CrewSkill | undefined {
return skillsCache.find(s => s.id === id);
}