- Add player meta stats API endpoint with teammates, weapons, and map data - Display avg_rank badge on match cards and match detail page - Add tick rate and demo download improvements to match layout - Create sitemap proxy routes to backend for SEO - Document backend data limitations in transformers (rounds/weapons) - Fix 400 error: backend limits meta stats to max 10 items 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
import type { WeaponsAPIResponse } from '$lib/types/api/WeaponsAPIResponse';
|
|
import type { MatchWeaponsResponse, PlayerWeaponStats, WeaponStats, Match } from '$lib/types';
|
|
|
|
/**
|
|
* Transform raw weapons API response into structured format
|
|
*
|
|
* NOTE: The backend API provides hit/damage data per weapon but not kill counts.
|
|
* Kill tracking would require the demo parser to correlate damage events with
|
|
* player death events. Currently only aggregated damage and hit group data is available.
|
|
*
|
|
* @param rawData - Raw API response containing damage/hit data per weapon
|
|
* @param matchId - Match ID
|
|
* @param match - Match data with player information
|
|
* @returns Structured weapons data with damage stats (kills unavailable)
|
|
*/
|
|
export function transformWeaponsResponse(
|
|
rawData: WeaponsAPIResponse,
|
|
matchId: string,
|
|
match?: Match
|
|
): MatchWeaponsResponse {
|
|
const playerWeaponsMap = new Map<
|
|
string,
|
|
Map<number, { damage: number; hits: number; hitGroups: number[] }>
|
|
>();
|
|
|
|
// Create player ID to name mapping
|
|
const playerMap = new Map<string, string>();
|
|
if (match?.players) {
|
|
for (const player of match.players) {
|
|
playerMap.set(player.id, player.name);
|
|
}
|
|
}
|
|
|
|
// Process all stats
|
|
for (const roundStats of rawData.stats) {
|
|
for (const [attackerId, victims] of Object.entries(roundStats)) {
|
|
if (!playerWeaponsMap.has(attackerId)) {
|
|
playerWeaponsMap.set(attackerId, new Map());
|
|
}
|
|
const weaponsMap = playerWeaponsMap.get(attackerId)!;
|
|
|
|
for (const [_, hits] of Object.entries(victims)) {
|
|
for (const [eqType, hitGroup, damage] of hits) {
|
|
if (!weaponsMap.has(eqType)) {
|
|
weaponsMap.set(eqType, { damage: 0, hits: 0, hitGroups: [] });
|
|
}
|
|
const weaponStats = weaponsMap.get(eqType)!;
|
|
weaponStats.damage += damage;
|
|
weaponStats.hits++;
|
|
weaponStats.hitGroups.push(hitGroup);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert to output format
|
|
const weapons: PlayerWeaponStats[] = [];
|
|
for (const [playerId, weaponsMap] of playerWeaponsMap.entries()) {
|
|
const playerName = playerMap.get(playerId) || `Player ${playerId}`;
|
|
const weapon_stats: WeaponStats[] = [];
|
|
|
|
for (const [eqType, stats] of weaponsMap.entries()) {
|
|
const hitGroupCounts = {
|
|
head: 0,
|
|
chest: 0,
|
|
stomach: 0,
|
|
left_arm: 0,
|
|
right_arm: 0,
|
|
left_leg: 0,
|
|
right_leg: 0
|
|
};
|
|
for (const hitGroup of stats.hitGroups) {
|
|
if (hitGroup === 1) hitGroupCounts.head++;
|
|
else if (hitGroup === 2) hitGroupCounts.chest++;
|
|
else if (hitGroup === 3) hitGroupCounts.stomach++;
|
|
else if (hitGroup === 4) hitGroupCounts.left_arm++;
|
|
else if (hitGroup === 5) hitGroupCounts.right_arm++;
|
|
else if (hitGroup === 6) hitGroupCounts.left_leg++;
|
|
else if (hitGroup === 7) hitGroupCounts.right_leg++;
|
|
}
|
|
|
|
weapon_stats.push({
|
|
eq_type: eqType,
|
|
weapon_name: rawData.equipment_map[String(eqType)] || `Weapon ${eqType}`,
|
|
// Kill data not available - API only provides hit/damage events
|
|
// Would require backend changes to correlate damage with death events
|
|
kills: 0,
|
|
damage: stats.damage,
|
|
hits: stats.hits,
|
|
hit_groups: hitGroupCounts,
|
|
headshot_pct: hitGroupCounts.head > 0 ? (hitGroupCounts.head / stats.hits) * 100 : 0
|
|
});
|
|
}
|
|
|
|
weapons.push({
|
|
player_id: Number(playerId),
|
|
player_name: playerName,
|
|
weapon_stats
|
|
});
|
|
}
|
|
|
|
return {
|
|
match_id: matchId,
|
|
weapons
|
|
};
|
|
}
|