forked from CSGOWTF/csgowtf
feat: Integrate player meta stats, match metadata, and sitemap proxies
- 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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { apiClient } from './client';
|
||||
import { parsePlayer } from '$lib/schemas';
|
||||
import type { Player, PlayerMeta, TrackPlayerResponse } from '$lib/types';
|
||||
import type { Player, PlayerMeta, PlayerMetaStats, TrackPlayerResponse } from '$lib/types';
|
||||
import { transformPlayerProfile, type LegacyPlayerProfile } from './transformers';
|
||||
|
||||
/**
|
||||
@@ -85,6 +85,18 @@ export const playersAPI = {
|
||||
return playerMeta;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get player aggregated meta stats from backend
|
||||
* Uses pre-computed stats (cached 30 days) including teammates, weapons, maps
|
||||
* @param steamId - Steam ID (uint64 as string to preserve precision)
|
||||
* @param limit - Number of items per category (max 10, default: 4)
|
||||
* @returns Player meta stats with teammates, weapons, map performance
|
||||
*/
|
||||
async getPlayerMetaStats(steamId: string, limit = 4): Promise<PlayerMetaStats> {
|
||||
const url = `/player/${steamId}/meta/${limit}`;
|
||||
return apiClient.get<PlayerMetaStats>(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add player to tracking system
|
||||
* @param steamId - Steam ID (uint64 as string to preserve precision)
|
||||
|
||||
@@ -55,6 +55,9 @@ export interface LegacyMatchDetail {
|
||||
vac: boolean; // NOT vac_present
|
||||
game_ban: boolean; // NOT gameban_present
|
||||
stats?: LegacyPlayerStats[]; // Player stats array
|
||||
tick_rate?: number; // Server tick rate (64 or 128)
|
||||
avg_rank?: number; // Average Premier rating (backend computed)
|
||||
replay_url?: string; // Demo download URL (if < 30 days old)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,6 +222,9 @@ export function transformMatchDetail(legacy: LegacyMatchDetail): Match {
|
||||
demo_parsed: legacy.parsed,
|
||||
vac_present: legacy.vac,
|
||||
gameban_present: legacy.game_ban,
|
||||
tick_rate: legacy.tick_rate,
|
||||
avg_rank: legacy.avg_rank,
|
||||
replay_url: legacy.replay_url,
|
||||
players: legacy.stats?.map(transformPlayerStats)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ import type { MatchRoundsResponse, RoundDetail, RoundStats, Match } from '$lib/t
|
||||
|
||||
/**
|
||||
* Transform raw rounds API response into structured format
|
||||
* @param rawData - Raw API response
|
||||
*
|
||||
* NOTE: The backend API only provides economy data (bank/equipment/spent) per round.
|
||||
* Round winners and win reasons are not currently stored in the database.
|
||||
* To add this data would require backend changes to the demo parser.
|
||||
*
|
||||
* @param rawData - Raw API response containing economy data per round
|
||||
* @param matchId - Match ID
|
||||
* @param match - Match data with player information
|
||||
* @returns Structured rounds data
|
||||
* @param match - Match data with player information and final score
|
||||
* @returns Structured rounds data with economy info (winner/win_reason unavailable)
|
||||
*/
|
||||
export function transformRoundsResponse(
|
||||
rawData: RoundsAPIResponse,
|
||||
@@ -15,7 +20,7 @@ export function transformRoundsResponse(
|
||||
): MatchRoundsResponse {
|
||||
const rounds: RoundDetail[] = [];
|
||||
|
||||
// Create player ID to team mapping
|
||||
// Create player ID to team mapping for potential future use
|
||||
const playerTeamMap = new Map<string, number>();
|
||||
if (match?.players) {
|
||||
for (const player of match.players) {
|
||||
@@ -47,8 +52,10 @@ export function transformRoundsResponse(
|
||||
|
||||
rounds.push({
|
||||
round: roundNum + 1,
|
||||
winner: 0, // TODO: Determine winner from data if available
|
||||
win_reason: '', // TODO: Determine win reason if available
|
||||
// Round winner data not available from backend API
|
||||
// Would require demo parser changes to store RoundEnd event winners
|
||||
winner: 0,
|
||||
win_reason: '',
|
||||
players
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ import type { MatchWeaponsResponse, PlayerWeaponStats, WeaponStats, Match } from
|
||||
|
||||
/**
|
||||
* Transform raw weapons API response into structured format
|
||||
* @param rawData - Raw API response
|
||||
*
|
||||
* 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
|
||||
* @returns Structured weapons data with damage stats (kills unavailable)
|
||||
*/
|
||||
export function transformWeaponsResponse(
|
||||
rawData: WeaponsAPIResponse,
|
||||
@@ -77,7 +82,9 @@ export function transformWeaponsResponse(
|
||||
weapon_stats.push({
|
||||
eq_type: eqType,
|
||||
weapon_name: rawData.equipment_map[String(eqType)] || `Weapon ${eqType}`,
|
||||
kills: 0, // TODO: Calculate kills if needed
|
||||
// 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,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { MatchListItem } from '$lib/types';
|
||||
import { storeMatchesState, type FilterState } from '$lib/utils/navigation';
|
||||
import { getMapBackground, formatMapName } from '$lib/utils/mapAssets';
|
||||
import PremierRatingBadge from '$lib/components/ui/PremierRatingBadge.svelte';
|
||||
|
||||
interface Props {
|
||||
match: MatchListItem;
|
||||
@@ -87,6 +88,11 @@
|
||||
</div>
|
||||
<!-- Status badges - horizontal layout -->
|
||||
<div class="flex items-center gap-1">
|
||||
{#if match.avg_rank && match.avg_rank > 0}
|
||||
<div class="backdrop-blur-sm" title="Average player rating">
|
||||
<PremierRatingBadge rating={match.avg_rank} size="sm" showTier={false} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if match.player_count}
|
||||
<span
|
||||
class="rounded-full border border-neon-blue/30 bg-neon-blue/20 px-1.5 py-0.5 text-[10px] text-neon-blue backdrop-blur-sm"
|
||||
|
||||
@@ -42,6 +42,12 @@ export interface Match {
|
||||
/** Server tick rate (64 or 128) - optional, not always provided by API */
|
||||
tick_rate?: number;
|
||||
|
||||
/** Average Premier rating of all players in the match - optional, backend computed */
|
||||
avg_rank?: number;
|
||||
|
||||
/** Demo replay download URL (only available for matches < 30 days old) */
|
||||
replay_url?: string;
|
||||
|
||||
/**
|
||||
* Game mode: 'premier' | 'competitive' | 'wingman'
|
||||
* - Premier: Uses CS Rating (numerical ELO, 0-30,000+)
|
||||
@@ -67,6 +73,8 @@ export interface MatchListItem {
|
||||
duration: number;
|
||||
demo_parsed: boolean;
|
||||
player_count?: number;
|
||||
/** Average Premier rating of all players (backend computed, optional) */
|
||||
avg_rank?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,3 +119,79 @@ export interface PlayerProfile extends Player {
|
||||
/** Peak CS2 Premier rating */
|
||||
peak_rating?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Teammate statistics from backend meta endpoint
|
||||
*/
|
||||
export interface TeammateStats {
|
||||
/** Player profile */
|
||||
player: {
|
||||
steamid64: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
vac?: boolean;
|
||||
game_ban?: boolean;
|
||||
};
|
||||
/** Win rate when playing together (0-1) */
|
||||
win_rate?: number;
|
||||
/** Tie rate when playing together (0-1) */
|
||||
tie_rate?: number;
|
||||
/** Total matches played together */
|
||||
total?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Weapon damage statistics from backend meta endpoint
|
||||
*/
|
||||
export interface WeaponDamageStats {
|
||||
/** Equipment type ID */
|
||||
eq: number;
|
||||
/** Total damage dealt with this weapon */
|
||||
dmg: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map statistics from backend meta endpoint
|
||||
*/
|
||||
export interface MapStats {
|
||||
/** Map name (e.g., "de_inferno") */
|
||||
map: string;
|
||||
/** Win rate on this map (0-1) */
|
||||
win_rate: number;
|
||||
/** Tie rate on this map (0-1) */
|
||||
tie_rate: number;
|
||||
/** Total matches on this map */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full player meta statistics from /player/:id/meta endpoint
|
||||
* Contains pre-aggregated stats from the backend (cached for 30 days)
|
||||
*/
|
||||
export interface PlayerMetaStats {
|
||||
/** Basic player info */
|
||||
player: {
|
||||
steamid64: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
vac?: boolean;
|
||||
vac_date?: number;
|
||||
game_ban?: boolean;
|
||||
game_ban_date?: number;
|
||||
tracked?: boolean;
|
||||
};
|
||||
/** Best teammates sorted by win rate */
|
||||
best_mates?: TeammateStats[];
|
||||
/** Most played teammates sorted by total games */
|
||||
most_mates?: TeammateStats[];
|
||||
/** Equipment ID to name mapping */
|
||||
eq_map?: Record<number, string>;
|
||||
/** Weapon damage stats sorted by damage */
|
||||
weapon_dmg?: WeaponDamageStats[];
|
||||
/** Win rate per map (map name -> rate 0-1) */
|
||||
win_maps?: Record<string, number>;
|
||||
/** Tie rate per map (map name -> rate 0-1) */
|
||||
tie_maps?: Record<string, number>;
|
||||
/** Total matches per map (map name -> count) */
|
||||
total_maps?: Record<string, number>;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,16 @@ export type { ChatAPIResponse } from './api/ChatAPIResponse';
|
||||
export type { Match, MatchListItem, MatchPlayer, MatchWithPlayers } from './Match';
|
||||
|
||||
// Player types
|
||||
export type { Player, PlayerMatch, PlayerMeta, PlayerProfile } from './Player';
|
||||
export type {
|
||||
Player,
|
||||
PlayerMatch,
|
||||
PlayerMeta,
|
||||
PlayerProfile,
|
||||
PlayerMetaStats,
|
||||
TeammateStats,
|
||||
WeaponDamageStats,
|
||||
MapStats
|
||||
} from './Player';
|
||||
|
||||
// Round statistics types
|
||||
export type {
|
||||
|
||||
Reference in New Issue
Block a user