feat: complete Phase 3 - Domain Modeling & Data Layer

Implements comprehensive type system, runtime validation, API client,
and testing infrastructure for CS2.WTF.

TypeScript Interfaces (src/lib/types/):
- Match.ts: Match, MatchPlayer, MatchListItem types
- Player.ts: Player, PlayerMeta, PlayerProfile types
- RoundStats.ts: Round economy and performance data
- Weapon.ts: Weapon statistics with hit groups (HitGroup, WeaponType enums)
- Message.ts: Chat messages with filtering support
- api.ts: API responses, errors, and APIException class
- Complete type safety with strict null checks

Zod Schemas (src/lib/schemas/):
- Runtime validation for all data models
- Schema parsers with safe/unsafe variants
- Special handling for backend typo (looses → losses)
- Share code validation regex
- CS2-specific validations (rank 0-30000, MR12 rounds)

API Client (src/lib/api/):
- client.ts: Axios-based HTTP client with error handling
  - Request cancellation support (AbortController)
  - Automatic retry logic for transient failures
  - Timeout handling (10s default)
  - Typed APIException errors
- players.ts: Player endpoints (profile, meta, track/untrack, search)
- matches.ts: Match endpoints (parse, details, weapons, rounds, chat, search)
- Zod validation on all API responses

MSW Mock Handlers (src/mocks/):
- fixtures.ts: Comprehensive mock data for testing
- handlers/players.ts: Mock player API endpoints
- handlers/matches.ts: Mock match API endpoints
- browser.ts: Browser MSW worker for development
- server.ts: Node MSW server for Vitest tests
- Realistic responses with delays and pagination
- Safe integer IDs to avoid precision loss

Configuration:
- .env.example: Complete environment variable documentation
- src/vite-env.d.ts: Vite environment type definitions
- All strict TypeScript checks passing (0 errors, 0 warnings)

Features:
- Cancellable requests for search (prevent race conditions)
- Data normalization (backend typo handling)
- Comprehensive error types (NetworkError, Timeout, etc.)
- Share code parsing and validation
- Pagination support for players and matches
- Mock data for offline development and testing

Build Status: ✓ Production build successful
Type Check: ✓ 0 errors, 0 warnings
Lint: ✓ All checks passed
Phase 3 Completion: 100%

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-04 20:31:20 +01:00
parent 66aea51c39
commit d811efc394
26 changed files with 2444 additions and 6 deletions

138
src/lib/types/Match.ts Normal file
View File

@@ -0,0 +1,138 @@
/**
* Match data model
* Represents a complete CS2 match with metadata and optional player stats
*/
export interface Match {
/** Unique match identifier (uint64) */
match_id: number;
/** CS:GO/CS2 share code */
share_code: string;
/** Map name (e.g., "de_inferno") */
map: string;
/** Match date and time (ISO 8601) */
date: string;
/** Final score for team A (T/CT side) */
score_team_a: number;
/** Final score for team B (CT/T side) */
score_team_b: number;
/** Match duration in seconds */
duration: number;
/** Match result: 0 = tie, 1 = team_a win, 2 = team_b win */
match_result: number;
/** Maximum rounds (24 for MR12, 30 for MR15) */
max_rounds: number;
/** Whether the demo has been successfully parsed */
demo_parsed: boolean;
/** Whether any player has a VAC ban */
vac_present: boolean;
/** Whether any player has a game ban */
gameban_present: boolean;
/** Server tick rate (64 or 128) */
tick_rate: number;
/** Array of player statistics (optional, included in detailed match view) */
players?: MatchPlayer[];
}
/**
* Minimal match information for lists
*/
export interface MatchListItem {
match_id: number;
map: string;
date: string;
score_team_a: number;
score_team_b: number;
duration: number;
demo_parsed: boolean;
player_count: number;
}
/**
* Match player statistics
* Player performance data for a specific match
*/
export interface MatchPlayer {
/** Player Steam ID */
id: number;
/** Player display name */
name: string;
/** Steam avatar URL */
avatar: string;
/** Team ID: 2 = T side, 3 = CT side */
team_id: number;
// Performance metrics
kills: number;
deaths: number;
assists: number;
/** Headshot kills */
headshot: number;
/** MVP stars earned */
mvp: number;
/** In-game score */
score: number;
/** KAST percentage (0-100): Kill/Assist/Survive/Trade */
kast: number;
// Rank tracking (CS2 Premier rating: 0-30000)
rank_old?: number;
rank_new?: number;
// Damage statistics
dmg_enemy?: number;
dmg_team?: number;
// Multi-kill counts
mk_2?: number; // Double kills
mk_3?: number; // Triple kills
mk_4?: number; // Quad kills
mk_5?: number; // Aces
// Utility damage
ud_he?: number; // HE grenade damage
ud_flames?: number; // Molotov/Incendiary damage
ud_flash?: number; // Flash grenades used
ud_smoke?: number; // Smoke grenades used
ud_decoy?: number; // Decoy grenades used
// Flash statistics
flash_assists?: number;
flash_duration_enemy?: number; // Total enemy blind time
flash_duration_team?: number; // Total team blind time
flash_duration_self?: number; // Self-flash time
flash_total_enemy?: number; // Enemies flashed count
flash_total_team?: number; // Teammates flashed count
flash_total_self?: number; // Self-flash count
// Other
crosshair?: string;
color?: 'green' | 'yellow' | 'purple' | 'blue' | 'orange' | 'grey';
avg_ping?: number;
}
/**
* Match with extended player details (full scoreboard)
*/
export type MatchWithPlayers = Match & {
players: MatchPlayer[];
};

78
src/lib/types/Message.ts Normal file
View File

@@ -0,0 +1,78 @@
/**
* Chat message data model
* In-game chat messages from match demos
*/
export interface Message {
/** Chat message text content */
message: string;
/** true = all chat (both teams), false = team chat only */
all_chat: boolean;
/** Game tick when message was sent */
tick: number;
/** Reference to MatchPlayer ID */
match_player_id?: number;
/** Player ID who sent the message */
player_id?: number;
/** Player name (included in API response) */
player_name?: string;
/** Round number when message was sent */
round?: number;
/** Message timestamp (ISO 8601) */
timestamp?: string;
}
/**
* Match chat response
*/
export interface MatchChatResponse {
match_id: number;
messages: Message[];
}
/**
* Chat message with enhanced player data
*/
export interface EnrichedMessage extends Message {
player_name: string;
player_avatar?: string;
team_id?: number;
round: number;
}
/**
* Chat filter options
*/
export interface ChatFilter {
/** Filter by player ID */
player_id?: number;
/** Filter by chat type */
chat_type?: 'all' | 'team' | 'all_chat';
/** Filter by round number */
round?: number;
/** Search message content */
search?: string;
}
/**
* Chat statistics
*/
export interface ChatStats {
total_messages: number;
team_chat_count: number;
all_chat_count: number;
messages_per_player: Record<number, number>;
most_active_player: {
player_id: number;
message_count: number;
};
}

107
src/lib/types/Player.ts Normal file
View File

@@ -0,0 +1,107 @@
import type { Match, MatchPlayer } from './Match';
/**
* Player profile data model
* Represents a Steam user with CS2 statistics
*/
export interface Player {
/** Steam ID (uint64) */
id: number;
/** Steam display name */
name: string;
/** Steam avatar URL */
avatar: string;
/** Custom Steam profile URL */
vanity_url?: string;
/** Actual vanity URL (may differ from vanity_url) */
vanity_url_real?: string;
/** Last time Steam profile was updated (ISO 8601) */
steam_updated: string;
/** Steam account creation date (ISO 8601) */
profile_created?: string;
/** Total competitive wins */
wins?: number;
/**
* Total competitive losses
* Note: Backend has typo "looses", we map it to "losses"
*/
losses?: number;
/** Total ties */
ties?: number;
/** Number of VAC bans on record */
vac_count?: number;
/** Date of last VAC ban (ISO 8601) */
vac_date?: string | null;
/** Number of game bans on record */
game_ban_count?: number;
/** Date of last game ban (ISO 8601) */
game_ban_date?: string | null;
/** Oldest match share code seen for this player */
oldest_sharecode_seen?: string;
/** Recent matches with player statistics */
matches?: PlayerMatch[];
}
/**
* Player match entry (includes match + player stats)
*/
export interface PlayerMatch extends Match {
/** Player's statistics for this match */
stats: MatchPlayer;
}
/**
* Lightweight player metadata for quick previews
*/
export interface PlayerMeta {
id: number;
name: string;
avatar: string;
recent_matches: number;
last_match_date: string;
avg_kills: number;
avg_deaths: number;
avg_kast: number;
win_rate: number;
}
/**
* Player profile with calculated aggregate statistics
*/
export interface PlayerProfile extends Player {
/** Total matches played */
total_matches: number;
/** Overall K/D ratio */
kd_ratio: number;
/** Overall win rate percentage */
win_rate: number;
/** Average headshot percentage */
avg_headshot_pct: number;
/** Average KAST percentage */
avg_kast: number;
/** Current CS2 Premier rating (0-30000) */
current_rating?: number;
/** Peak CS2 Premier rating */
peak_rating?: number;
}

View File

@@ -0,0 +1,85 @@
/**
* Round statistics data model
* Economy and performance data for a single round
*/
export interface RoundStats {
/** Round number (1-24 for MR12, 1-30 for MR15) */
round: number;
/** Money available at round start */
bank: number;
/** Value of equipment purchased/held */
equipment: number;
/** Total money spent this round */
spent: number;
/** Kills achieved in this round */
kills_in_round?: number;
/** Damage dealt in this round */
damage_in_round?: number;
/** Reference to MatchPlayer ID */
match_player_id?: number;
/** Player ID for this round data */
player_id?: number;
}
/**
* Team round statistics
* Aggregated economy data for a team in a round
*/
export interface TeamRoundStats {
round: number;
team_id: number; // 2 = T, 3 = CT
/** Total team money at round start */
total_bank: number;
/** Total equipment value */
total_equipment: number;
/** Average equipment value per player */
avg_equipment: number;
/** Total money spent */
total_spent: number;
/** Round winner (2 = T, 3 = CT) */
winner?: number;
/** Win reason */
win_reason?: 'elimination' | 'bomb_defused' | 'bomb_exploded' | 'time' | 'target_saved';
/** Buy type classification */
buy_type?: 'eco' | 'semi-eco' | 'force' | 'full';
}
/**
* Complete match rounds data
*/
export interface MatchRoundsData {
match_id: number;
rounds: RoundStats[];
}
/**
* Round details with player breakdown
*/
export interface RoundDetail {
round: number;
winner: number;
win_reason: string;
players: RoundStats[];
}
/**
* Complete match rounds response
*/
export interface MatchRoundsResponse {
match_id: number;
rounds: RoundDetail[];
}

147
src/lib/types/Weapon.ts Normal file
View File

@@ -0,0 +1,147 @@
/**
* Weapon statistics data model
* Tracks weapon usage, damage, and hit locations
*/
export interface Weapon {
/** Player ID of the victim who was hit/killed */
victim: number;
/** Damage dealt with this hit */
dmg: number;
/** Weapon equipment type ID */
eq_type: number;
/** Hit location group (1=head, 2=chest, 3=stomach, 4=left_arm, 5=right_arm, 6=left_leg, 7=right_leg) */
hit_group: number;
/** Reference to MatchPlayer ID */
match_player_id?: number;
}
/**
* Weapon performance statistics for a player
*/
export interface WeaponStats {
/** Weapon equipment type ID */
eq_type: number;
/** Weapon display name */
weapon_name: string;
/** Total kills with this weapon */
kills: number;
/** Total damage dealt */
damage: number;
/** Total hits landed */
hits: number;
/** Hit group distribution */
hit_groups: {
head: number;
chest: number;
stomach: number;
left_arm: number;
right_arm: number;
left_leg: number;
right_leg: number;
};
/** Headshot percentage */
headshot_pct?: number;
/** Accuracy percentage (hits / shots) if available */
accuracy?: number;
}
/**
* Player weapon statistics
*/
export interface PlayerWeaponStats {
player_id: number;
weapon_stats: WeaponStats[];
}
/**
* Match weapons response
*/
export interface MatchWeaponsResponse {
match_id: number;
weapons: PlayerWeaponStats[];
}
/**
* Hit group enumeration
*/
export enum HitGroup {
Generic = 0,
Head = 1,
Chest = 2,
Stomach = 3,
LeftArm = 4,
RightArm = 5,
LeftLeg = 6,
RightLeg = 7
}
/**
* Weapon type enumeration
* Equipment type IDs from CS2
*/
export enum WeaponType {
// Pistols
Glock = 1,
USP = 2,
P2000 = 3,
P250 = 4,
Deagle = 5,
FiveSeven = 6,
Tec9 = 7,
CZ75 = 8,
DualBerettas = 9,
// SMGs
MP9 = 10,
MAC10 = 11,
MP7 = 12,
UMP45 = 13,
P90 = 14,
PPBizon = 15,
MP5SD = 16,
// Rifles
AK47 = 17,
M4A4 = 18,
M4A1S = 19,
Galil = 20,
Famas = 21,
AUG = 22,
SG553 = 23,
// Sniper Rifles
AWP = 24,
SSG08 = 25,
SCAR20 = 26,
G3SG1 = 27,
// Heavy
Nova = 28,
XM1014 = 29,
Mag7 = 30,
SawedOff = 31,
M249 = 32,
Negev = 33,
// Equipment
Zeus = 34,
Knife = 35,
HEGrenade = 36,
Flashbang = 37,
Smoke = 38,
Molotov = 39,
Decoy = 40,
Incendiary = 41,
C4 = 42
}

161
src/lib/types/api.ts Normal file
View File

@@ -0,0 +1,161 @@
/**
* API response types and error handling
*/
import type { Match, MatchListItem } from './Match';
import type { Player, PlayerMeta } from './Player';
/**
* Standard API error response
*/
export interface APIError {
error: string;
message: string;
status_code: number;
timestamp?: string;
}
/**
* Generic API response wrapper
*/
export interface APIResponse<T> {
data: T;
success: boolean;
error?: APIError;
}
/**
* Match parse response
*/
export interface MatchParseResponse {
match_id: number;
status: 'parsing' | 'queued' | 'completed' | 'error';
message: string;
estimated_time?: number; // seconds
}
/**
* Match parse status
*/
export interface MatchParseStatus {
match_id: number;
status: 'pending' | 'parsing' | 'completed' | 'error';
progress?: number; // 0-100
error_message?: string;
}
/**
* Matches list response with pagination
*/
export interface MatchesListResponse {
matches: MatchListItem[];
next_page_time?: number; // Unix timestamp
has_more: boolean;
total_count?: number;
}
/**
* Match list query parameters
*/
export interface MatchesQueryParams {
limit?: number; // 1-100
map?: string;
player_id?: number;
before_time?: number; // Unix timestamp for pagination
}
/**
* Player track/untrack response
*/
export interface TrackPlayerResponse {
success: boolean;
message: string;
}
/**
* Player profile response
*/
export type PlayerProfileResponse = Player;
/**
* Player metadata response
*/
export type PlayerMetaResponse = PlayerMeta;
/**
* Match details response
*/
export type MatchDetailsResponse = Match;
/**
* Error types for better error handling
*/
export enum APIErrorType {
NetworkError = 'NETWORK_ERROR',
ServerError = 'SERVER_ERROR',
NotFound = 'NOT_FOUND',
BadRequest = 'BAD_REQUEST',
Unauthorized = 'UNAUTHORIZED',
Timeout = 'TIMEOUT',
ValidationError = 'VALIDATION_ERROR',
UnknownError = 'UNKNOWN_ERROR'
}
/**
* Typed API error class
*/
export class APIException extends Error {
constructor(
public type: APIErrorType,
public message: string,
public statusCode?: number,
public details?: unknown
) {
super(message);
this.name = 'APIException';
}
static fromResponse(statusCode: number, data?: unknown): APIException {
let type: APIErrorType;
let message: string;
switch (statusCode) {
case 400:
type = APIErrorType.BadRequest;
message = 'Invalid request parameters';
break;
case 401:
type = APIErrorType.Unauthorized;
message = 'Unauthorized access';
break;
case 404:
type = APIErrorType.NotFound;
message = 'Resource not found';
break;
case 500:
case 502:
case 503:
type = APIErrorType.ServerError;
message = 'Server error occurred';
break;
default:
type = APIErrorType.UnknownError;
message = 'An unknown error occurred';
}
// Extract message from response data if available
if (data && typeof data === 'object' && 'message' in data) {
message = String(data.message);
}
return new APIException(type, message, statusCode, data);
}
static networkError(message = 'Network connection failed'): APIException {
return new APIException(APIErrorType.NetworkError, message);
}
static timeout(message = 'Request timed out'): APIException {
return new APIException(APIErrorType.Timeout, message);
}
}

42
src/lib/types/index.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* Central export for all CS2.WTF type definitions
*/
// Match types
export type { Match, MatchListItem, MatchPlayer, MatchWithPlayers } from './Match';
// Player types
export type { Player, PlayerMatch, PlayerMeta, PlayerProfile } from './Player';
// Round statistics types
export type {
RoundStats,
TeamRoundStats,
MatchRoundsData,
RoundDetail,
MatchRoundsResponse
} from './RoundStats';
// Weapon types
export type { Weapon, WeaponStats, PlayerWeaponStats, MatchWeaponsResponse } from './Weapon';
export { HitGroup, WeaponType } from './Weapon';
// Message/Chat types
export type { Message, MatchChatResponse, EnrichedMessage, ChatFilter, ChatStats } from './Message';
// API response types
export type {
APIError,
APIResponse,
MatchParseResponse,
MatchParseStatus,
MatchesListResponse,
MatchesQueryParams,
TrackPlayerResponse,
PlayerProfileResponse,
PlayerMetaResponse,
MatchDetailsResponse
} from './api';
export { APIErrorType, APIException } from './api';