forked from CSGOWTF/csgowtf
feat: Merge economy and rounds pages with unified economy utilities
- Create economyUtils.ts with team-aware buy type classification (CT has higher thresholds due to M4 cost) - Add Economy Overview toggle to rounds page with charts - Resolve player names/avatars in round economy display - Remove standalone Economy tab (merged into Rounds) - Document missing backend API data (round winner, win reason) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,15 @@ export function transformRoundsResponse(
|
||||
): MatchRoundsResponse {
|
||||
const rounds: RoundDetail[] = [];
|
||||
|
||||
// Create player ID to team mapping for potential future use
|
||||
const playerTeamMap = new Map<string, number>();
|
||||
// Create player lookup map for name, team, and avatar resolution
|
||||
const playerInfoMap = new Map<string, { name: string; team_id: number; avatar: string }>();
|
||||
if (match?.players) {
|
||||
for (const player of match.players) {
|
||||
playerTeamMap.set(player.id, player.team_id);
|
||||
playerInfoMap.set(player.id, {
|
||||
name: player.name,
|
||||
team_id: player.team_id,
|
||||
avatar: player.avatar
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,17 +43,29 @@ export function transformRoundsResponse(
|
||||
|
||||
const players: RoundStats[] = [];
|
||||
|
||||
// Convert player data
|
||||
// Convert player data with name resolution
|
||||
for (const [playerId, [bank, equipment, spent]] of Object.entries(roundData)) {
|
||||
const playerInfo = playerInfoMap.get(playerId);
|
||||
players.push({
|
||||
round: roundNum + 1, // API uses 0-indexed, we use 1-indexed
|
||||
bank,
|
||||
equipment,
|
||||
spent,
|
||||
player_id: Number(playerId)
|
||||
player_id: Number(playerId),
|
||||
player_name: playerInfo?.name,
|
||||
team_id: playerInfo?.team_id,
|
||||
avatar: playerInfo?.avatar
|
||||
});
|
||||
}
|
||||
|
||||
// Sort players by team (CT first, then T) for consistent display
|
||||
players.sort((a, b) => {
|
||||
if (a.team_id !== b.team_id) {
|
||||
return (a.team_id ?? 0) - (b.team_id ?? 0);
|
||||
}
|
||||
return (a.player_name ?? '').localeCompare(b.player_name ?? '');
|
||||
});
|
||||
|
||||
rounds.push({
|
||||
round: roundNum + 1,
|
||||
// Round winner data not available from backend API
|
||||
|
||||
@@ -26,6 +26,15 @@ export interface RoundStats {
|
||||
|
||||
/** Player ID for this round data */
|
||||
player_id?: number;
|
||||
|
||||
/** Player display name (resolved from match.players) */
|
||||
player_name?: string;
|
||||
|
||||
/** Player's team ID (2 = T, 3 = CT) */
|
||||
team_id?: number;
|
||||
|
||||
/** Player avatar URL */
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
214
src/lib/utils/economyUtils.ts
Normal file
214
src/lib/utils/economyUtils.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* CS2 Economy Utilities
|
||||
*
|
||||
* Unified economy classification and display utilities for consistent
|
||||
* buy type detection across the application.
|
||||
*
|
||||
* Thresholds based on:
|
||||
* - Leetify economy groupings
|
||||
* - Steam community guides
|
||||
* - Professional CS2 analysis standards
|
||||
*/
|
||||
|
||||
export type BuyType = 'pistol' | 'eco' | 'force' | 'full';
|
||||
export type TeamSide = 'T' | 'CT';
|
||||
export type EconomyHealth = 'healthy' | 'tight' | 'broken';
|
||||
|
||||
export interface BuyTypeConfig {
|
||||
label: string;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
borderColor: string;
|
||||
}
|
||||
|
||||
export interface EconomyHealthConfig {
|
||||
label: string;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buy type thresholds based on average equipment value per player
|
||||
* CT side has higher thresholds due to more expensive rifles (M4 vs AK)
|
||||
*/
|
||||
const BUY_THRESHOLDS = {
|
||||
T: {
|
||||
eco: 1500,
|
||||
force: 3500,
|
||||
full: 3500
|
||||
},
|
||||
CT: {
|
||||
eco: 1500,
|
||||
force: 4000,
|
||||
full: 4000
|
||||
}
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Economy health thresholds based on average bank per player
|
||||
*/
|
||||
const ECONOMY_HEALTH_THRESHOLDS = {
|
||||
healthy: 4000, // Can full-buy next round
|
||||
tight: 2000 // Force-buy possible but risky
|
||||
// Below tight = broken
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Pistol round starting money
|
||||
*/
|
||||
export const PISTOL_ROUND_MONEY = 800;
|
||||
|
||||
/**
|
||||
* Visual configuration for each buy type
|
||||
*/
|
||||
export const BUY_TYPE_CONFIG: Record<BuyType, BuyTypeConfig> = {
|
||||
pistol: {
|
||||
label: 'Pistol',
|
||||
color: 'text-neon-purple',
|
||||
bgColor: 'bg-neon-purple/20',
|
||||
borderColor: 'border-neon-purple'
|
||||
},
|
||||
eco: {
|
||||
label: 'Eco',
|
||||
color: 'text-red-400',
|
||||
bgColor: 'bg-red-500/20',
|
||||
borderColor: 'border-red-500'
|
||||
},
|
||||
force: {
|
||||
label: 'Force',
|
||||
color: 'text-yellow-400',
|
||||
bgColor: 'bg-yellow-500/20',
|
||||
borderColor: 'border-yellow-500'
|
||||
},
|
||||
full: {
|
||||
label: 'Full Buy',
|
||||
color: 'text-green-400',
|
||||
bgColor: 'bg-green-500/20',
|
||||
borderColor: 'border-green-500'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Visual configuration for economy health status
|
||||
*/
|
||||
export const ECONOMY_HEALTH_CONFIG: Record<EconomyHealth, EconomyHealthConfig> = {
|
||||
healthy: {
|
||||
label: 'Healthy',
|
||||
color: 'text-green-400',
|
||||
bgColor: 'bg-green-500/20',
|
||||
description: 'Can full-buy next round'
|
||||
},
|
||||
tight: {
|
||||
label: 'Tight',
|
||||
color: 'text-yellow-400',
|
||||
bgColor: 'bg-yellow-500/20',
|
||||
description: 'Force-buy possible, risky'
|
||||
},
|
||||
broken: {
|
||||
label: 'Broken',
|
||||
color: 'text-red-400',
|
||||
bgColor: 'bg-red-500/20',
|
||||
description: 'Must eco or half-buy'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine buy type based on average equipment value
|
||||
*
|
||||
* @param avgEquipment - Average equipment value per player
|
||||
* @param teamSide - Team side ('T' or 'CT')
|
||||
* @param isPistolRound - Whether this is a pistol round
|
||||
* @returns Buy type classification
|
||||
*/
|
||||
export function getBuyType(
|
||||
avgEquipment: number,
|
||||
teamSide: TeamSide,
|
||||
isPistolRound: boolean = false
|
||||
): BuyType {
|
||||
if (isPistolRound) {
|
||||
return 'pistol';
|
||||
}
|
||||
|
||||
const thresholds = BUY_THRESHOLDS[teamSide];
|
||||
|
||||
if (avgEquipment < thresholds.eco) {
|
||||
return 'eco';
|
||||
}
|
||||
if (avgEquipment < thresholds.force) {
|
||||
return 'force';
|
||||
}
|
||||
return 'full';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visual configuration for a buy type
|
||||
*/
|
||||
export function getBuyTypeConfig(buyType: BuyType): BuyTypeConfig {
|
||||
return BUY_TYPE_CONFIG[buyType];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine economy health based on average bank per player
|
||||
*
|
||||
* @param avgBank - Average bank per player
|
||||
* @returns Economy health status
|
||||
*/
|
||||
export function getEconomyHealth(avgBank: number): EconomyHealth {
|
||||
if (avgBank >= ECONOMY_HEALTH_THRESHOLDS.healthy) {
|
||||
return 'healthy';
|
||||
}
|
||||
if (avgBank >= ECONOMY_HEALTH_THRESHOLDS.tight) {
|
||||
return 'tight';
|
||||
}
|
||||
return 'broken';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visual configuration for economy health
|
||||
*/
|
||||
export function getEconomyHealthConfig(health: EconomyHealth): EconomyHealthConfig {
|
||||
return ECONOMY_HEALTH_CONFIG[health];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a round is a pistol round
|
||||
*
|
||||
* @param roundNumber - Current round number (1-indexed)
|
||||
* @param halftimeRound - The halftime round number (12 for MR12, 15 for MR15)
|
||||
* @returns Whether this is a pistol round
|
||||
*/
|
||||
export function isPistolRound(roundNumber: number, halftimeRound: number): boolean {
|
||||
return roundNumber === 1 || roundNumber === halftimeRound + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the halftime round based on max rounds
|
||||
*
|
||||
* @param maxRounds - Maximum rounds in the match (24 for MR12, 30 for MR15)
|
||||
* @returns Halftime round number
|
||||
*/
|
||||
export function getHalftimeRound(maxRounds: number): number {
|
||||
return maxRounds === 30 ? 15 : 12;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total team economy (bank + equipment value)
|
||||
*
|
||||
* @param totalBank - Total team bank
|
||||
* @param totalEquipment - Total team equipment value
|
||||
* @returns Combined economy value
|
||||
*/
|
||||
export function calculateTeamEconomy(totalBank: number, totalEquipment: number): number {
|
||||
return totalBank + totalEquipment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format money value for display
|
||||
*
|
||||
* @param value - Money value
|
||||
* @returns Formatted string with $ prefix and comma separators
|
||||
*/
|
||||
export function formatMoney(value: number): string {
|
||||
return `$${value.toLocaleString()}`;
|
||||
}
|
||||
Reference in New Issue
Block a user