fix: Fix match detail data loading and add API transformation layer
- Update Zod schemas to match raw API response formats - Create transformation layer (rounds, weapons, chat) to convert raw API to structured format - Add player name mapping in transformers for better UX - Fix Svelte 5 reactivity issues in chat page (replace $effect with $derived) - Fix Chart.js compatibility with Svelte 5 state proxies using JSON serialization - Add economy advantage chart with halftime perspective flip (WIP) - Remove stray comment from details page - Update layout to load match data first, then pass to API methods 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
46
src/lib/api/transformers/chatTransformer.ts
Normal file
46
src/lib/api/transformers/chatTransformer.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { ChatAPIResponse } from '$lib/types/api/ChatAPIResponse';
|
||||
import type { MatchChatResponse, Message, Match } from '$lib/types';
|
||||
|
||||
/**
|
||||
* Transform raw chat API response into structured format
|
||||
* @param rawData - Raw API response
|
||||
* @param matchId - Match ID
|
||||
* @param match - Match data with player information
|
||||
* @returns Structured chat data
|
||||
*/
|
||||
export function transformChatResponse(
|
||||
rawData: ChatAPIResponse,
|
||||
matchId: string,
|
||||
match?: Match
|
||||
): MatchChatResponse {
|
||||
const messages: Message[] = [];
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten all player messages into a single array
|
||||
for (const [playerId, playerMessages] of Object.entries(rawData)) {
|
||||
const playerName = playerMap.get(playerId) || `Player ${playerId}`;
|
||||
|
||||
for (const message of playerMessages) {
|
||||
messages.push({
|
||||
...message,
|
||||
player_id: Number(playerId),
|
||||
player_name: playerName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by tick
|
||||
messages.sort((a, b) => a.tick - b.tick);
|
||||
|
||||
return {
|
||||
match_id: matchId,
|
||||
messages
|
||||
};
|
||||
}
|
||||
60
src/lib/api/transformers/roundsTransformer.ts
Normal file
60
src/lib/api/transformers/roundsTransformer.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { RoundsAPIResponse } from '$lib/types/api/RoundsAPIResponse';
|
||||
import type { MatchRoundsResponse, RoundDetail, RoundStats, Match } from '$lib/types';
|
||||
|
||||
/**
|
||||
* Transform raw rounds API response into structured format
|
||||
* @param rawData - Raw API response
|
||||
* @param matchId - Match ID
|
||||
* @param match - Match data with player information
|
||||
* @returns Structured rounds data
|
||||
*/
|
||||
export function transformRoundsResponse(
|
||||
rawData: RoundsAPIResponse,
|
||||
matchId: string,
|
||||
match?: Match
|
||||
): MatchRoundsResponse {
|
||||
const rounds: RoundDetail[] = [];
|
||||
|
||||
// Create player ID to team mapping
|
||||
const playerTeamMap = new Map<string, number>();
|
||||
if (match?.players) {
|
||||
for (const player of match.players) {
|
||||
playerTeamMap.set(player.id, player.team_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert object keys to sorted round numbers
|
||||
const roundNumbers = Object.keys(rawData)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
for (const roundNum of roundNumbers) {
|
||||
const roundData = rawData[String(roundNum)];
|
||||
if (!roundData) continue;
|
||||
|
||||
const players: RoundStats[] = [];
|
||||
|
||||
// Convert player data
|
||||
for (const [playerId, [bank, equipment, spent]] of Object.entries(roundData)) {
|
||||
players.push({
|
||||
round: roundNum + 1, // API uses 0-indexed, we use 1-indexed
|
||||
bank,
|
||||
equipment,
|
||||
spent,
|
||||
player_id: Number(playerId)
|
||||
});
|
||||
}
|
||||
|
||||
rounds.push({
|
||||
round: roundNum + 1,
|
||||
winner: 0, // TODO: Determine winner from data if available
|
||||
win_reason: '', // TODO: Determine win reason if available
|
||||
players
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
match_id: matchId,
|
||||
rounds
|
||||
};
|
||||
}
|
||||
99
src/lib/api/transformers/weaponsTransformer.ts
Normal file
99
src/lib/api/transformers/weaponsTransformer.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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
|
||||
* @param rawData - Raw API response
|
||||
* @param matchId - Match ID
|
||||
* @param match - Match data with player information
|
||||
* @returns Structured weapons data
|
||||
*/
|
||||
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}`,
|
||||
kills: 0, // TODO: Calculate kills if needed
|
||||
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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user