- 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>
215 lines
4.8 KiB
TypeScript
215 lines
4.8 KiB
TypeScript
/**
|
|
* 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()}`;
|
|
}
|