index.ts•16.1 kB
import { z } from 'zod';
import { getTank, getTankByName, searchTanks, getAllTanks } from '../parsers/tanks.js';
import { getEquipment, getConsumables, getProvisions } from '../parsers/equipment.js';
import { getMaps, getMapByName, getMapWithTerrain } from '../parsers/maps.js';
import { getCrewSkills, getSkillsByClass } from '../parsers/crew.js';
import { getCamouflages, getCamouflageByName, searchCamouflages, getCamoBonusTable, getCamouflageBonus } from '../parsers/camouflages.js';
import { getTopTurret, getTankTopGun, getTopTracks, formatGunSummary } from '../utils/tools.js';
import { NATIONS, type Nation, type TankClass, type TankType } from '../data/types.js';
import { loadMapImage, hasMapImage } from '../images/maps.js';
export interface ImageResult {
__type: 'image';
data: string;
mimeType: string;
}
const NATION_VALUES = ['ussr', 'germany', 'usa', 'china', 'france', 'uk', 'japan', 'european', 'other'] as const;
export const tools = {
get_tank: {
description: 'Get detailed stats for a specific tank by name or ID',
inputSchema: z.object({
name: z.string().optional().describe('Tank name (fuzzy match)'),
id: z.number().optional().describe('Tank ID'),
}),
handler: async (input: { name?: string; id?: number }) => {
if (input.id) {
const tank = getTank(input.id);
if (!tank) return { error: `Tank with ID ${input.id} not found` };
return tank;
}
if (input.name) {
const tank = getTankByName(input.name);
if (!tank) {
const similar = searchTanks({ query: input.name }).slice(0, 5);
return {
error: `Tank "${input.name}" not found`,
suggestions: similar.map(t => t.name),
};
}
return tank;
}
return { error: 'Provide either name or id' };
},
},
search_tanks: {
description: 'Search for tanks by nation, tier, type, or name query',
inputSchema: z.object({
nation: z.enum(NATION_VALUES).optional().describe('Filter by nation'),
tier: z.number().min(1).max(10).optional().describe('Filter by tier (1-10)'),
class: z.enum(['lightTank', 'mediumTank', 'heavyTank', 'AT-SPG']).optional().describe('Filter by tank class'),
type: z.enum(['researchable', 'premium', 'collector']).optional().describe('Filter by tank type'),
query: z.string().optional().describe('Search query for tank name'),
}),
handler: async (input: { nation?: Nation; tier?: number; class?: TankClass; type?: TankType; query?: string }) => {
const results = searchTanks(input);
return {
count: results.length,
tanks: results.slice(0, 50),
};
},
},
compare_tanks: {
description: 'Compare two tanks side by side',
inputSchema: z.object({
tankA: z.string().describe('First tank name'),
tankB: z.string().describe('Second tank name'),
}),
handler: async (input: { tankA: string; tankB: string }) => {
const a = getTankByName(input.tankA);
const b = getTankByName(input.tankB);
if (!a) return { error: `Tank "${input.tankA}" not found` };
if (!b) return { error: `Tank "${input.tankB}" not found` };
const topGunA = getTankTopGun(a);
const topGunB = getTankTopGun(b);
const topTurretA = getTopTurret(a);
const topTurretB = getTopTurret(b);
return {
tankA: {
name: a.name,
tier: a.tier,
class: a.class,
nation: a.nation,
health: a.health,
armor: a.hullArmor,
speed: { forward: a.speedForward, backward: a.speedBackward },
gun: formatGunSummary(topGunA),
viewRange: topTurretA?.viewRange,
camouflage: { still: a.camouflageStill, moving: a.camouflageMoving },
},
tankB: {
name: b.name,
tier: b.tier,
class: b.class,
nation: b.nation,
health: b.health,
armor: b.hullArmor,
speed: { forward: b.speedForward, backward: b.speedBackward },
gun: formatGunSummary(topGunB),
viewRange: topTurretB?.viewRange,
camouflage: { still: b.camouflageStill, moving: b.camouflageMoving },
},
};
},
},
list_nations: {
description: 'List all available nations in the game',
inputSchema: z.object({}),
handler: async () => {
const nationNames: Record<Nation, string> = {
ussr: 'USSR',
germany: 'Germany',
usa: 'USA',
china: 'China',
france: 'France',
uk: 'United Kingdom',
japan: 'Japan',
european: 'European',
other: 'Other',
};
return NATIONS.map(code => ({
code,
name: nationNames[code],
}));
},
},
list_maps: {
description: 'List all maps with spawn points and tactical data',
inputSchema: z.object({}),
handler: async () => {
const maps = getMaps();
return maps.map(m => ({
id: m.id,
name: m.name,
tag: m.tag,
modes: m.modes,
spawns: {
allies: m.alliesSpawns,
enemies: m.enemiesSpawns,
},
}));
},
},
get_map: {
description: 'Get detailed map data including terrain objects (bushes, buildings, rocks) for tactical analysis',
inputSchema: z.object({
name: z.string().describe('Map name'),
includeTerrain: z.boolean().optional().default(true).describe('Include terrain objects (bushes, trees, buildings, rocks)'),
}),
handler: async (input: { name: string; includeTerrain?: boolean }) => {
const includeTerrain = input.includeTerrain !== false;
if (includeTerrain) {
const map = await getMapWithTerrain(input.name);
if (!map) {
const available = getMaps().map(m => m.name);
return { error: `Map "${input.name}" not found`, available };
}
return {
id: map.id,
name: map.name,
tag: map.tag,
modes: map.modes,
bounds: map.bounds,
terrainSummary: map.terrainSummary,
spawns: {
allies: map.alliesSpawns,
enemies: map.enemiesSpawns,
},
terrain: {
bushes: map.terrain?.filter(e => e.type === 'bush') || [],
trees: map.terrain?.filter(e => e.type === 'tree') || [],
buildings: map.terrain?.filter(e => e.type === 'building') || [],
rocks: map.terrain?.filter(e => e.type === 'rock') || [],
walls: map.terrain?.filter(e => e.type === 'wall') || [],
capturePoints: map.terrain?.filter(e => e.type === 'capture_point') || [],
},
};
}
const map = getMapByName(input.name);
if (!map) {
const available = getMaps().map(m => m.name);
return { error: `Map "${input.name}" not found`, available };
}
return map;
},
},
list_equipment: {
description: 'List all available equipment modules',
inputSchema: z.object({}),
handler: async () => {
return getEquipment();
},
},
list_consumables: {
description: 'List all available consumables',
inputSchema: z.object({}),
handler: async () => {
return getConsumables();
},
},
list_provisions: {
description: 'List all available provisions',
inputSchema: z.object({}),
handler: async () => {
return getProvisions();
},
},
list_crew_skills: {
description: 'List all crew skills with effects and bonuses',
inputSchema: z.object({
tankClass: z.enum(['lightTank', 'mediumTank', 'heavyTank', 'AT-SPG']).optional().describe('Filter by tank class'),
}),
handler: async (input: { tankClass?: TankClass }) => {
if (input.tankClass) {
return getSkillsByClass(input.tankClass);
}
return getCrewSkills();
},
},
analyze_matchup: {
description: 'Analyze a 1v1 matchup between two tanks',
inputSchema: z.object({
tankA: z.string().describe('First tank name'),
tankB: z.string().describe('Second tank name'),
}),
handler: async (input: { tankA: string; tankB: string }) => {
const a = getTankByName(input.tankA);
const b = getTankByName(input.tankB);
if (!a) return { error: `Tank "${input.tankA}" not found` };
if (!b) return { error: `Tank "${input.tankB}" not found` };
const topTurretA = getTopTurret(a);
const topTurretB = getTopTurret(b);
const topGunA = getTankTopGun(a);
const topGunB = getTankTopGun(b);
const topTracksA = getTopTracks(a);
const topTracksB = getTopTracks(b);
return {
[a.name]: {
tier: a.tier,
class: a.class,
health: a.health,
armor: a.hullArmor,
mobility: {
speedForward: a.speedForward,
speedBackward: a.speedBackward,
hullTraverse: topTracksA?.traverseSpeed,
turretTraverse: topTurretA?.traverseSpeed,
},
firepower: topGunA ? {
damage: topGunA.damage,
penetration: topGunA.penetration,
dpm: topGunA.dpm,
reload: topGunA.reloadTime,
accuracy: topGunA.accuracy,
aimTime: topGunA.aimTime,
depression: topGunA.depression,
gunType: topGunA.gunType,
clipSize: topGunA.clipSize,
} : null,
vision: {
viewRange: topTurretA?.viewRange,
camoStill: a.camouflageStill,
camoMoving: a.camouflageMoving,
camoFiring: a.camouflageFiring,
},
shells: topGunA?.shells,
},
[b.name]: {
tier: b.tier,
class: b.class,
health: b.health,
armor: b.hullArmor,
mobility: {
speedForward: b.speedForward,
speedBackward: b.speedBackward,
hullTraverse: topTracksB?.traverseSpeed,
turretTraverse: topTurretB?.traverseSpeed,
},
firepower: topGunB ? {
damage: topGunB.damage,
penetration: topGunB.penetration,
dpm: topGunB.dpm,
reload: topGunB.reloadTime,
accuracy: topGunB.accuracy,
aimTime: topGunB.aimTime,
depression: topGunB.depression,
gunType: topGunB.gunType,
clipSize: topGunB.clipSize,
} : null,
vision: {
viewRange: topTurretB?.viewRange,
camoStill: b.camouflageStill,
camoMoving: b.camouflageMoving,
camoFiring: b.camouflageFiring,
},
shells: topGunB?.shells,
},
};
},
},
analyze_match: {
description: 'Analyze a full match with team compositions on a specific map',
inputSchema: z.object({
map: z.string().describe('Map name'),
myTank: z.string().describe('Your tank name'),
friendlyTeam: z.array(z.string()).describe('Friendly team tank names'),
enemyTeam: z.array(z.string()).describe('Enemy team tank names'),
}),
handler: async (input: { map: string; myTank: string; friendlyTeam: string[]; enemyTeam: string[] }) => {
const map = getMapByName(input.map);
const myTank = getTankByName(input.myTank);
if (!map) return { error: `Map "${input.map}" not found` };
if (!myTank) return { error: `Tank "${input.myTank}" not found` };
const resolveTanks = (names: string[]) => names.map(name => {
const tank = getTankByName(name);
if (!tank) return { name, error: 'not found' };
const topTurret = getTopTurret(tank);
const topGun = getTankTopGun(tank);
return {
name: tank.name,
tier: tank.tier,
class: tank.class,
health: tank.health,
dpm: topGun?.dpm,
penetration: topGun?.penetration,
viewRange: topTurret?.viewRange,
};
});
return {
map: {
name: map.name,
spawns: {
allies: map.alliesSpawns,
enemies: map.enemiesSpawns,
},
},
myTank: {
name: myTank.name,
tier: myTank.tier,
class: myTank.class,
health: myTank.health,
armor: myTank.hullArmor,
speed: myTank.speedForward,
},
friendlyTeam: resolveTanks(input.friendlyTeam),
enemyTeam: resolveTanks(input.enemyTeam),
};
},
},
list_camouflages: {
description: 'List all camouflages with their invisibility bonus categories',
inputSchema: z.object({
nation: z.string().optional().describe('Filter by nation'),
kind: z.enum(['summer', 'winter', 'desert', 'universal']).optional().describe('Filter by camo type'),
query: z.string().optional().describe('Search query for camouflage name'),
}),
handler: async (input: { nation?: string; kind?: 'summer' | 'winter' | 'desert' | 'universal'; query?: string }) => {
const results = searchCamouflages(input);
return {
count: results.length,
camouflages: results.slice(0, 100),
};
},
},
get_camo_bonus: {
description: 'Get camouflage invisibility bonus values per tank class for each category',
inputSchema: z.object({
tankClass: z.enum(['lightTank', 'mediumTank', 'heavyTank', 'AT-SPG']).optional().describe('Filter by tank class'),
}),
handler: async (input: { tankClass?: TankClass }) => {
const bonusTable = getCamoBonusTable();
if (input.tankClass) {
const result: Record<string, number> = {};
for (const [category, bonuses] of Object.entries(bonusTable)) {
result[category] = bonuses[input.tankClass];
}
return {
tankClass: input.tankClass,
bonusByCategory: result,
};
}
return {
categories: bonusTable,
note: 'Values are additional invisibility percentage added to tank base camo',
};
},
},
get_camouflage: {
description: 'Get details for a specific camouflage including its bonus values',
inputSchema: z.object({
name: z.string().describe('Camouflage name to search for'),
}),
handler: async (input: { name: string }) => {
const camo = getCamouflageByName(input.name);
if (!camo) {
const all = getCamouflages();
const suggestions = all.filter(c => c.name.toLowerCase().includes(input.name.toLowerCase())).slice(0, 5);
return {
error: `Camouflage "${input.name}" not found`,
suggestions: suggestions.map(c => c.name),
};
}
const bonusTable = getCamoBonusTable();
const categoryBonuses = bonusTable[camo.category];
return {
...camo,
bonuses: categoryBonuses ? {
lightTank: `+${(categoryBonuses.lightTank * 100).toFixed(0)}%`,
mediumTank: `+${(categoryBonuses.mediumTank * 100).toFixed(0)}%`,
heavyTank: `+${(categoryBonuses.heavyTank * 100).toFixed(0)}%`,
'AT-SPG': `+${(categoryBonuses['AT-SPG'] * 100).toFixed(0)}%`,
} : null,
};
},
},
get_map_image: {
description: 'Get top-down tactical map image for visual analysis',
inputSchema: z.object({
name: z.string().describe('Map name'),
}),
handler: async (input: { name: string }): Promise<ImageResult | { error: string; available?: string[] }> => {
const map = getMapByName(input.name);
if (!map) {
const available = getMaps().map(m => m.name);
return { error: `Map "${input.name}" not found`, available };
}
if (!hasMapImage(map.tag)) {
return { error: `No image available for map "${map.name}"` };
}
const image = await loadMapImage(map.tag);
if (!image) {
return { error: `Failed to load image for map "${map.name}"` };
}
return {
__type: 'image',
data: image.data,
mimeType: image.mimeType,
};
},
},
};
export type ToolName = keyof typeof tools;