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()}`;
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Download, Calendar, Clock, ArrowLeft, Server, Users } from 'lucide-svelte';
|
||||
import {
|
||||
Download,
|
||||
Calendar,
|
||||
Clock,
|
||||
ArrowLeft,
|
||||
Server,
|
||||
Users,
|
||||
CheckCircle2,
|
||||
Timer
|
||||
} from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Badge from '$lib/components/ui/Badge.svelte';
|
||||
import Tabs from '$lib/components/ui/Tabs.svelte';
|
||||
import PremierRatingBadge from '$lib/components/ui/PremierRatingBadge.svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
@@ -17,7 +25,7 @@
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Overview', href: `/match/${match.match_id}` },
|
||||
{ label: 'Economy', href: `/match/${match.match_id}/economy` },
|
||||
{ label: 'Rounds', href: `/match/${match.match_id}/rounds` },
|
||||
{ label: 'Details', href: `/match/${match.match_id}/details` },
|
||||
{ label: 'Weapons', href: `/match/${match.match_id}/weapons` },
|
||||
{ label: 'Flashes', href: `/match/${match.match_id}/flashes` },
|
||||
@@ -150,37 +158,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Match Meta -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-3 text-sm text-white/70">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Calendar class="h-3.5 w-3.5 text-neon-blue" />
|
||||
<span>{formattedDate}</span>
|
||||
<!-- Match Meta Grid -->
|
||||
<div class="mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-6">
|
||||
<!-- Date -->
|
||||
<div class="flex items-center gap-2 rounded-lg border border-white/5 bg-white/5 px-3 py-2">
|
||||
<Calendar class="h-4 w-4 shrink-0 text-neon-blue" />
|
||||
<span class="truncate text-xs text-white/70">{formattedDate}</span>
|
||||
</div>
|
||||
<span class="text-white/20">•</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Clock class="h-3.5 w-3.5 text-neon-blue" />
|
||||
<span>{duration}</span>
|
||||
|
||||
<!-- Duration -->
|
||||
<div class="flex items-center gap-2 rounded-lg border border-white/5 bg-white/5 px-3 py-2">
|
||||
<Timer class="h-4 w-4 shrink-0 text-neon-green" />
|
||||
<span class="text-xs text-white/70">{duration}</span>
|
||||
</div>
|
||||
<span class="text-white/20">•</span>
|
||||
<span>MR12 ({match.max_rounds} rounds)</span>
|
||||
|
||||
<!-- Rounds -->
|
||||
<div class="flex items-center gap-2 rounded-lg border border-white/5 bg-white/5 px-3 py-2">
|
||||
<Clock class="h-4 w-4 shrink-0 text-neon-purple" />
|
||||
<span class="text-xs text-white/70">{match.max_rounds} rounds</span>
|
||||
</div>
|
||||
|
||||
<!-- Tick Rate -->
|
||||
{#if match.tick_rate}
|
||||
<span class="text-white/20">•</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Server class="h-3.5 w-3.5 text-neon-purple" />
|
||||
<span class="font-mono">{match.tick_rate} tick</span>
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-lg border border-white/5 bg-white/5 px-3 py-2"
|
||||
>
|
||||
<Server class="h-4 w-4 shrink-0 text-neon-gold" />
|
||||
<span class="font-mono text-xs text-white/70">{match.tick_rate} tick</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Avg Rating -->
|
||||
{#if match.avg_rank && match.avg_rank > 0}
|
||||
<span class="text-white/20">•</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Users class="h-3.5 w-3.5 text-neon-gold" />
|
||||
<span>Avg Rating:</span>
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-lg border border-white/5 bg-white/5 px-3 py-2"
|
||||
>
|
||||
<Users class="h-4 w-4 shrink-0 text-neon-blue" />
|
||||
<PremierRatingBadge rating={match.avg_rank} size="sm" showTier={false} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Demo Status -->
|
||||
{#if match.demo_parsed}
|
||||
<span class="text-white/20">•</span>
|
||||
<Badge variant="success" size="sm">Demo Parsed</Badge>
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-lg border border-neon-green/20 bg-neon-green/10 px-3 py-2"
|
||||
>
|
||||
<CheckCircle2 class="h-4 w-4 shrink-0 text-neon-green" />
|
||||
<span class="text-xs font-medium text-neon-green">Parsed</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,513 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { TrendingUp, ShoppingCart, AlertCircle, Wallet, DollarSign } from 'lucide-svelte';
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import Badge from '$lib/components/ui/Badge.svelte';
|
||||
import LineChart from '$lib/components/charts/LineChart.svelte';
|
||||
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface TeamEconomy {
|
||||
round: number;
|
||||
teamA_bank: number;
|
||||
teamB_bank: number;
|
||||
teamA_equipment: number;
|
||||
teamB_equipment: number;
|
||||
teamA_spent: number;
|
||||
teamB_spent: number;
|
||||
winner: number;
|
||||
teamA_buyType: string;
|
||||
teamB_buyType: string;
|
||||
economyAdvantage: number;
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const { match, roundsData } = data;
|
||||
|
||||
// Team IDs - Terrorists are always team_id 2, Counter-Terrorists are always team_id 3
|
||||
const tTeamId = 2;
|
||||
const ctTeamId = 3;
|
||||
|
||||
// Calculate halftime round based on max_rounds
|
||||
const halfPoint = match.max_rounds === 30 ? 15 : 12;
|
||||
|
||||
// Only process if rounds data exists
|
||||
let teamEconomy = $state<TeamEconomy[]>([]);
|
||||
let equipmentChartData = $state<{
|
||||
labels: string[];
|
||||
datasets: Array<{
|
||||
label: string;
|
||||
data: number[];
|
||||
borderColor?: string;
|
||||
backgroundColor?: string;
|
||||
fill?: boolean;
|
||||
tension?: number;
|
||||
}>;
|
||||
} | null>(null);
|
||||
let economyAdvantageChartData = $state<{
|
||||
labels: string[];
|
||||
datasets: Array<{
|
||||
label: string;
|
||||
data: number[];
|
||||
borderColor?: string;
|
||||
backgroundColor?: string;
|
||||
fill?: boolean;
|
||||
tension?: number;
|
||||
}>;
|
||||
} | null>(null);
|
||||
let totalRounds = $state(0);
|
||||
let teamA_fullBuys = $state(0);
|
||||
let teamB_fullBuys = $state(0);
|
||||
let teamA_ecos = $state(0);
|
||||
let teamB_ecos = $state(0);
|
||||
let halfRoundIndex = $state<number>(0);
|
||||
|
||||
if (roundsData) {
|
||||
// Process rounds data to calculate team totals
|
||||
for (const roundData of roundsData.rounds) {
|
||||
const tPlayers = roundData.players.filter((p) => {
|
||||
const matchPlayer = match.players?.find((mp) => mp.id === String(p.player_id));
|
||||
return matchPlayer?.team_id === tTeamId;
|
||||
});
|
||||
|
||||
const ctPlayers = roundData.players.filter((p) => {
|
||||
const matchPlayer = match.players?.find((mp) => mp.id === String(p.player_id));
|
||||
return matchPlayer?.team_id === ctTeamId;
|
||||
});
|
||||
|
||||
const t_bank = tPlayers.reduce((sum, p) => sum + (p.bank || 0), 0);
|
||||
const ct_bank = ctPlayers.reduce((sum, p) => sum + (p.bank || 0), 0);
|
||||
const t_equipment = tPlayers.reduce((sum, p) => sum + (p.equipment || 0), 0);
|
||||
const ct_equipment = ctPlayers.reduce((sum, p) => sum + (p.equipment || 0), 0);
|
||||
const t_spent = tPlayers.reduce((sum, p) => sum + (p.spent || 0), 0);
|
||||
const ct_spent = ctPlayers.reduce((sum, p) => sum + (p.spent || 0), 0);
|
||||
|
||||
const avgT_equipment = tPlayers.length > 0 ? t_equipment / tPlayers.length : 0;
|
||||
const avgCT_equipment = ctPlayers.length > 0 ? ct_equipment / ctPlayers.length : 0;
|
||||
|
||||
const classifyBuyType = (avgEquipment: number): string => {
|
||||
if (avgEquipment < 1500) return 'Eco';
|
||||
if (avgEquipment < 2500) return 'Semi-Eco';
|
||||
if (avgEquipment < 3500) return 'Force';
|
||||
return 'Full Buy';
|
||||
};
|
||||
|
||||
const t_totalEconomy = t_bank + t_spent;
|
||||
const ct_totalEconomy = ct_bank + ct_spent;
|
||||
|
||||
let economyAdvantage;
|
||||
if (roundData.round <= halfPoint) {
|
||||
economyAdvantage = t_totalEconomy - ct_totalEconomy;
|
||||
} else {
|
||||
economyAdvantage = ct_totalEconomy - t_totalEconomy;
|
||||
}
|
||||
|
||||
teamEconomy.push({
|
||||
round: roundData.round,
|
||||
teamA_bank: t_bank,
|
||||
teamB_bank: ct_bank,
|
||||
teamA_equipment: t_equipment,
|
||||
teamB_equipment: ct_equipment,
|
||||
teamA_spent: t_spent,
|
||||
teamB_spent: ct_spent,
|
||||
winner: roundData.winner || 0,
|
||||
teamA_buyType: classifyBuyType(avgT_equipment),
|
||||
teamB_buyType: classifyBuyType(avgCT_equipment),
|
||||
economyAdvantage
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare equipment value chart data with neon colors
|
||||
equipmentChartData = {
|
||||
labels: teamEconomy.map((r) => `R${r.round}`),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Terrorists Equipment',
|
||||
data: teamEconomy.map((r) => r.teamA_equipment),
|
||||
borderColor: '#d4a74a', // terrorist color
|
||||
backgroundColor: 'rgba(212, 167, 74, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Counter-Terrorists Equipment',
|
||||
data: teamEconomy.map((r) => r.teamB_equipment),
|
||||
borderColor: '#5e98d9', // ct color
|
||||
backgroundColor: 'rgba(94, 152, 217, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Prepare economy advantage chart data
|
||||
halfRoundIndex = Math.floor(teamEconomy.length / 2);
|
||||
economyAdvantageChartData = {
|
||||
labels: teamEconomy.map((r) => `${r.round}`),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Advantage',
|
||||
data: teamEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)),
|
||||
borderColor: '#5e98d9',
|
||||
backgroundColor: 'rgba(94, 152, 217, 0.6)',
|
||||
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
|
||||
fill: 'origin',
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4
|
||||
},
|
||||
{
|
||||
label: 'Disadvantage',
|
||||
data: teamEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)),
|
||||
borderColor: '#d4a74a',
|
||||
backgroundColor: 'rgba(212, 167, 74, 0.6)',
|
||||
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
|
||||
fill: 'origin',
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Calculate summary stats
|
||||
totalRounds = teamEconomy.length;
|
||||
teamA_fullBuys = teamEconomy.filter((r) => r.teamA_buyType === 'Full Buy').length;
|
||||
teamB_fullBuys = teamEconomy.filter((r) => r.teamB_buyType === 'Full Buy').length;
|
||||
teamA_ecos = teamEconomy.filter((r) => r.teamA_buyType === 'Eco').length;
|
||||
teamB_ecos = teamEconomy.filter((r) => r.teamB_buyType === 'Eco').length;
|
||||
}
|
||||
|
||||
// Buy type labels with puns
|
||||
const buyTypeLabels: Record<string, string> = {
|
||||
Eco: 'The Poverty Round',
|
||||
'Semi-Eco': 'Broke but Hopeful',
|
||||
Force: 'YOLO Buy',
|
||||
'Full Buy': 'Loaded'
|
||||
};
|
||||
|
||||
// Table columns
|
||||
const tableColumns = [
|
||||
{
|
||||
key: 'round' as keyof TeamEconomy,
|
||||
label: 'Round',
|
||||
sortable: true,
|
||||
align: 'center' as const
|
||||
},
|
||||
{
|
||||
key: 'teamA_buyType' as keyof TeamEconomy,
|
||||
label: 'T Buy',
|
||||
sortable: true,
|
||||
render: (value: string | number | boolean, _row: TeamEconomy) => {
|
||||
const strValue = value as string;
|
||||
const colorClass =
|
||||
strValue === 'Full Buy'
|
||||
? 'bg-neon-green/20 text-neon-green border-neon-green/30'
|
||||
: strValue === 'Eco'
|
||||
? 'bg-neon-red/20 text-neon-red border-neon-red/30'
|
||||
: strValue === 'Force'
|
||||
? 'bg-neon-gold/20 text-neon-gold border-neon-gold/30'
|
||||
: 'bg-white/10 text-white/60 border-white/20';
|
||||
return `<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border ${colorClass}">${strValue}</span>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'teamA_equipment' as keyof TeamEconomy,
|
||||
label: 'T Equipment',
|
||||
sortable: true,
|
||||
align: 'right' as const,
|
||||
format: (value: string | number | boolean, _row: TeamEconomy) =>
|
||||
`$${(value as number).toLocaleString()}`
|
||||
},
|
||||
{
|
||||
key: 'teamB_buyType' as keyof TeamEconomy,
|
||||
label: 'CT Buy',
|
||||
sortable: true,
|
||||
render: (value: string | number | boolean, _row: TeamEconomy) => {
|
||||
const strValue = value as string;
|
||||
const colorClass =
|
||||
strValue === 'Full Buy'
|
||||
? 'bg-neon-green/20 text-neon-green border-neon-green/30'
|
||||
: strValue === 'Eco'
|
||||
? 'bg-neon-red/20 text-neon-red border-neon-red/30'
|
||||
: strValue === 'Force'
|
||||
? 'bg-neon-gold/20 text-neon-gold border-neon-gold/30'
|
||||
: 'bg-white/10 text-white/60 border-white/20';
|
||||
return `<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border ${colorClass}">${strValue}</span>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'teamB_equipment' as keyof TeamEconomy,
|
||||
label: 'CT Equipment',
|
||||
sortable: true,
|
||||
align: 'right' as const,
|
||||
format: (value: string | number | boolean, _row: TeamEconomy) =>
|
||||
`$${(value as number).toLocaleString()}`
|
||||
},
|
||||
{
|
||||
key: 'winner' as keyof TeamEconomy,
|
||||
label: 'Winner',
|
||||
align: 'center' as const,
|
||||
render: (value: string | number | boolean, _row: TeamEconomy) => {
|
||||
const numValue = value as number;
|
||||
if (numValue === 2)
|
||||
return '<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-terrorist/20 text-terrorist border border-terrorist/30">T</span>';
|
||||
if (numValue === 3)
|
||||
return '<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-ct/20 text-ct border border-ct/30">CT</span>';
|
||||
return '<span class="text-white/30">-</span>';
|
||||
}
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
{#if !roundsData}
|
||||
<Card padding="lg">
|
||||
<div class="text-center">
|
||||
<AlertCircle
|
||||
class="mx-auto mb-4 h-16 w-16 text-neon-gold"
|
||||
style="filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));"
|
||||
/>
|
||||
<h2 class="mb-2 text-2xl font-bold text-white">Match Not Parsed</h2>
|
||||
<p class="mb-4 text-white/60">
|
||||
This match hasn't been parsed yet, so detailed economy data is not available. The evidence
|
||||
of everyone's financial decisions remains hidden.
|
||||
</p>
|
||||
<Badge variant="warning" size="lg">Demo parsing required</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
{:else}
|
||||
<div class="space-y-6">
|
||||
<!-- Economy Advantage Chart -->
|
||||
<Card padding="lg">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-green/20"
|
||||
style="box-shadow: 0 0 15px rgba(0, 255, 136, 0.2);"
|
||||
>
|
||||
<DollarSign class="h-5 w-5 text-neon-green" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Economy Flow</h2>
|
||||
<p class="text-sm text-white/50">
|
||||
Net-worth differential (bank + spent) - The money story
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if economyAdvantageChartData}
|
||||
<div class="relative">
|
||||
<LineChart
|
||||
data={economyAdvantageChartData}
|
||||
height={400}
|
||||
options={{
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: (context) => {
|
||||
if (context.tick.value === 0) {
|
||||
return 'rgba(255, 255, 255, 0.3)';
|
||||
}
|
||||
return 'rgba(255, 255, 255, 0.05)';
|
||||
},
|
||||
lineWidth: (context) => {
|
||||
return context.tick.value === 0 ? 2 : 1;
|
||||
}
|
||||
},
|
||||
ticks: {
|
||||
color: 'rgba(255, 255, 255, 0.5)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
color: 'rgba(255, 255, 255, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: 'rgba(255, 255, 255, 0.5)'
|
||||
}
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if halfRoundIndex > 0}
|
||||
<div
|
||||
class="pointer-events-none absolute top-0 flex h-full items-center"
|
||||
style="left: {(halfRoundIndex / (teamEconomy.length || 1)) * 100}%"
|
||||
>
|
||||
<div class="h-full w-px bg-neon-blue/30"></div>
|
||||
<div
|
||||
class="absolute -top-1 left-1/2 -translate-x-1/2 rounded-md border border-neon-blue/30 bg-void-light px-2 py-1 text-xs font-medium text-neon-blue"
|
||||
>
|
||||
Half-Time
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid gap-6 md:grid-cols-3">
|
||||
<Card padding="lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-blue/20"
|
||||
style="box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);"
|
||||
>
|
||||
<ShoppingCart class="h-5 w-5 text-neon-blue" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-white/50">Total Rounds</div>
|
||||
<div class="text-3xl font-bold text-white">{totalRounds}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-white/40">
|
||||
{match.score_team_a} - {match.score_team_b}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card padding="lg" class="border-l-4 border-l-terrorist">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-terrorist/20"
|
||||
style="box-shadow: 0 0 15px rgba(212, 167, 74, 0.2);"
|
||||
>
|
||||
<TrendingUp class="h-5 w-5 text-terrorist" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-white/50">T Full Buys</div>
|
||||
<div class="text-3xl font-bold text-white">{teamA_fullBuys}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-neon-red">{teamA_ecos} poverty rounds</div>
|
||||
</Card>
|
||||
|
||||
<Card padding="lg" class="border-l-4 border-l-ct">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-ct/20"
|
||||
style="box-shadow: 0 0 15px rgba(94, 152, 217, 0.2);"
|
||||
>
|
||||
<TrendingUp class="h-5 w-5 text-ct" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-white/50">CT Full Buys</div>
|
||||
<div class="text-3xl font-bold text-white">{teamB_fullBuys}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-neon-red">{teamB_ecos} poverty rounds</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Equipment Value Chart -->
|
||||
<Card padding="lg">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-purple/20"
|
||||
style="box-shadow: 0 0 15px rgba(139, 92, 246, 0.2);"
|
||||
>
|
||||
<Wallet class="h-5 w-5 text-neon-purple" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Equipment Value Over Time</h2>
|
||||
<p class="text-sm text-white/50">Total equipment value for each team across all rounds</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if equipmentChartData}
|
||||
<LineChart
|
||||
data={equipmentChartData}
|
||||
height={350}
|
||||
options={{
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
color: 'rgba(255, 255, 255, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: 'rgba(255, 255, 255, 0.5)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
color: 'rgba(255, 255, 255, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: 'rgba(255, 255, 255, 0.5)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
<!-- Round-by-Round Table -->
|
||||
<Card padding="none">
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl font-bold text-white">Round-by-Round Economy</h2>
|
||||
<p class="mt-1 text-sm text-white/50">
|
||||
Detailed breakdown of buy types and equipment values - Where did all the money go?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DataTable data={teamEconomy} columns={tableColumns} striped hoverable />
|
||||
</Card>
|
||||
|
||||
<!-- Buy Type Legend -->
|
||||
<Card padding="lg" class="border-neon-blue/20">
|
||||
<h3 class="mb-3 text-lg font-semibold text-white">
|
||||
Buy Type Classification (A Financial Guide)
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center rounded-md border border-neon-red/30 bg-neon-red/20 px-2 py-0.5 text-xs font-medium text-neon-red"
|
||||
>
|
||||
Eco
|
||||
</span>
|
||||
<span class="text-white/50">< $1,500 avg - "{buyTypeLabels['Eco']}"</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center rounded-md border border-white/20 bg-white/10 px-2 py-0.5 text-xs font-medium text-white/60"
|
||||
>
|
||||
Semi-Eco
|
||||
</span>
|
||||
<span class="text-white/50">$1,500 - $2,500 - "{buyTypeLabels['Semi-Eco']}"</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center rounded-md border border-neon-gold/30 bg-neon-gold/20 px-2 py-0.5 text-xs font-medium text-neon-gold"
|
||||
>
|
||||
Force
|
||||
</span>
|
||||
<span class="text-white/50">$2,500 - $3,500 - "{buyTypeLabels['Force']}"</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center rounded-md border border-neon-green/30 bg-neon-green/20 px-2 py-0.5 text-xs font-medium text-neon-green"
|
||||
>
|
||||
Full Buy
|
||||
</span>
|
||||
<span class="text-white/50">> $3,500 - "{buyTypeLabels['Full Buy']}"</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
// Get all data from parent layout (already loaded upfront)
|
||||
const { match, rounds } = await parent();
|
||||
|
||||
return {
|
||||
match,
|
||||
roundsData: rounds,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Economy - Match ${match.match_id} - teamflash.rip`
|
||||
}
|
||||
};
|
||||
};
|
||||
751
src/routes/match/[id]/rounds/+page.svelte
Normal file
751
src/routes/match/[id]/rounds/+page.svelte
Normal file
@@ -0,0 +1,751 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
DollarSign,
|
||||
Info,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
TrendingUp,
|
||||
Minus,
|
||||
BarChart3,
|
||||
Wallet
|
||||
} from 'lucide-svelte';
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import LineChart from '$lib/components/charts/LineChart.svelte';
|
||||
import type { LayoutData } from '../$types';
|
||||
import type { RoundDetail, RoundStats } from '$lib/types/RoundStats';
|
||||
import {
|
||||
getBuyType,
|
||||
getBuyTypeConfig,
|
||||
getEconomyHealth,
|
||||
getEconomyHealthConfig,
|
||||
isPistolRound as checkIsPistolRound,
|
||||
getHalftimeRound,
|
||||
formatMoney,
|
||||
PISTOL_ROUND_MONEY,
|
||||
BUY_TYPE_CONFIG,
|
||||
type BuyType
|
||||
} from '$lib/utils/economyUtils';
|
||||
|
||||
let { data }: { data: LayoutData } = $props();
|
||||
const { match, rounds } = data;
|
||||
|
||||
// Calculate halftime round based on max_rounds
|
||||
const halftimeRound = $derived(getHalftimeRound(match.max_rounds));
|
||||
const totalRounds = $derived(rounds?.rounds?.length ?? 0);
|
||||
|
||||
// Current selected round
|
||||
let selectedRound = $state(1);
|
||||
|
||||
// Get current round data
|
||||
const currentRoundData = $derived(
|
||||
rounds?.rounds?.find((r: RoundDetail) => r.round === selectedRound)
|
||||
);
|
||||
|
||||
// Determine if we're in second half (teams have swapped sides)
|
||||
const isSecondHalf = $derived(selectedRound > halftimeRound);
|
||||
|
||||
// In first half: team_id 1 = T, team_id 2 = CT
|
||||
// In second half: team_id 1 = CT, team_id 2 = T (swapped!)
|
||||
const terroristTeamId = $derived(isSecondHalf ? 2 : 1);
|
||||
const ctTeamId = $derived(isSecondHalf ? 1 : 2);
|
||||
|
||||
// Pistol round detection using unified utility
|
||||
const isPistolRound = $derived(checkIsPistolRound(selectedRound, halftimeRound));
|
||||
|
||||
// Helper to get player bank (override for pistol rounds)
|
||||
const getPlayerBank = (player: RoundStats) => (isPistolRound ? PISTOL_ROUND_MONEY : player.bank);
|
||||
|
||||
// Group players by team for current round (accounting for side swap)
|
||||
const teamAPlayers = $derived(
|
||||
currentRoundData?.players?.filter((p: RoundStats) => p.team_id === terroristTeamId) ?? []
|
||||
);
|
||||
const teamBPlayers = $derived(
|
||||
currentRoundData?.players?.filter((p: RoundStats) => p.team_id === ctTeamId) ?? []
|
||||
);
|
||||
|
||||
// Calculate team economy totals (with pistol round override)
|
||||
const calcTeamEconomy = (players: RoundStats[]) => {
|
||||
const totalBank = isPistolRound
|
||||
? players.length * PISTOL_ROUND_MONEY
|
||||
: players.reduce((sum, p) => sum + p.bank, 0);
|
||||
const totalEquipment = players.reduce((sum, p) => sum + p.equipment, 0);
|
||||
const totalSpent = players.reduce((sum, p) => sum + p.spent, 0);
|
||||
const avgBank = players.length > 0 ? totalBank / players.length : 0;
|
||||
const avgEquipment = players.length > 0 ? totalEquipment / players.length : 0;
|
||||
return { totalBank, totalEquipment, totalSpent, avgBank, avgEquipment };
|
||||
};
|
||||
|
||||
const teamAEconomy = $derived(calcTeamEconomy(teamAPlayers));
|
||||
const teamBEconomy = $derived(calcTeamEconomy(teamBPlayers));
|
||||
|
||||
// Get buy types using unified utilities with team-aware thresholds
|
||||
const teamABuyType = $derived(getBuyType(teamAEconomy.avgEquipment, 'T', isPistolRound));
|
||||
const teamBBuyType = $derived(getBuyType(teamBEconomy.avgEquipment, 'CT', isPistolRound));
|
||||
|
||||
const teamABuyConfig = $derived(getBuyTypeConfig(teamABuyType));
|
||||
const teamBBuyConfig = $derived(getBuyTypeConfig(teamBBuyType));
|
||||
|
||||
// Get economy health using unified utilities
|
||||
const teamAHealth = $derived(getEconomyHealth(teamAEconomy.avgBank));
|
||||
const teamBHealth = $derived(getEconomyHealth(teamBEconomy.avgBank));
|
||||
|
||||
const teamAHealthConfig = $derived(getEconomyHealthConfig(teamAHealth));
|
||||
const teamBHealthConfig = $derived(getEconomyHealthConfig(teamBHealth));
|
||||
|
||||
// Navigate rounds
|
||||
const nextRound = () => {
|
||||
if (selectedRound < totalRounds) selectedRound++;
|
||||
};
|
||||
const prevRound = () => {
|
||||
if (selectedRound > 1) selectedRound--;
|
||||
};
|
||||
|
||||
// Check if we have round data
|
||||
const hasRoundData = $derived(rounds && rounds.rounds && rounds.rounds.length > 0);
|
||||
|
||||
// Economy advantage calculation
|
||||
const economyAdvantage = $derived(
|
||||
teamAEconomy.totalBank +
|
||||
teamAEconomy.totalEquipment -
|
||||
(teamBEconomy.totalBank + teamBEconomy.totalEquipment)
|
||||
);
|
||||
|
||||
// Calculate economy data for all rounds (for charts)
|
||||
interface RoundEconomyData {
|
||||
round: number;
|
||||
t_bank: number;
|
||||
ct_bank: number;
|
||||
t_equipment: number;
|
||||
ct_equipment: number;
|
||||
t_buyType: BuyType;
|
||||
ct_buyType: BuyType;
|
||||
economyAdvantage: number;
|
||||
}
|
||||
|
||||
const allRoundsEconomy = $derived.by(() => {
|
||||
if (!rounds?.rounds) return [];
|
||||
|
||||
return rounds.rounds.map((roundData: RoundDetail) => {
|
||||
const roundNum = roundData.round;
|
||||
const roundIsSecondHalf = roundNum > halftimeRound;
|
||||
const tTeamId = roundIsSecondHalf ? 2 : 1;
|
||||
const ctTeamIdForRound = roundIsSecondHalf ? 1 : 2;
|
||||
|
||||
const tPlayers = roundData.players?.filter((p: RoundStats) => p.team_id === tTeamId) ?? [];
|
||||
const ctPlayers =
|
||||
roundData.players?.filter((p: RoundStats) => p.team_id === ctTeamIdForRound) ?? [];
|
||||
|
||||
const roundIsPistol = checkIsPistolRound(roundNum, halftimeRound);
|
||||
|
||||
const t_bank = roundIsPistol
|
||||
? tPlayers.length * PISTOL_ROUND_MONEY
|
||||
: tPlayers.reduce((sum, p) => sum + p.bank, 0);
|
||||
const ct_bank = roundIsPistol
|
||||
? ctPlayers.length * PISTOL_ROUND_MONEY
|
||||
: ctPlayers.reduce((sum, p) => sum + p.bank, 0);
|
||||
const t_equipment = tPlayers.reduce((sum, p) => sum + p.equipment, 0);
|
||||
const ct_equipment = ctPlayers.reduce((sum, p) => sum + p.equipment, 0);
|
||||
|
||||
const avgT_equipment = tPlayers.length > 0 ? t_equipment / tPlayers.length : 0;
|
||||
const avgCT_equipment = ctPlayers.length > 0 ? ct_equipment / ctPlayers.length : 0;
|
||||
|
||||
// Calculate economy advantage (positive = T advantage in first half, CT advantage in second half)
|
||||
const t_total = t_bank + t_equipment;
|
||||
const ct_total = ct_bank + ct_equipment;
|
||||
const economyAdv = roundIsSecondHalf ? ct_total - t_total : t_total - ct_total;
|
||||
|
||||
return {
|
||||
round: roundNum,
|
||||
t_bank,
|
||||
ct_bank,
|
||||
t_equipment,
|
||||
ct_equipment,
|
||||
t_buyType: getBuyType(avgT_equipment, 'T', roundIsPistol),
|
||||
ct_buyType: getBuyType(avgCT_equipment, 'CT', roundIsPistol),
|
||||
economyAdvantage: economyAdv
|
||||
} as RoundEconomyData;
|
||||
});
|
||||
});
|
||||
|
||||
// Chart data for economy flow
|
||||
const economyFlowChartData = $derived.by(() => {
|
||||
if (!allRoundsEconomy.length) return null;
|
||||
|
||||
return {
|
||||
labels: allRoundsEconomy.map((r) => `${r.round}`),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Advantage',
|
||||
data: allRoundsEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)),
|
||||
borderColor: '#5e98d9',
|
||||
backgroundColor: 'rgba(94, 152, 217, 0.6)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Disadvantage',
|
||||
data: allRoundsEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)),
|
||||
borderColor: '#d4a74a',
|
||||
backgroundColor: 'rgba(212, 167, 74, 0.6)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// Chart data for equipment value over time
|
||||
const equipmentChartData = $derived.by(() => {
|
||||
if (!allRoundsEconomy.length) return null;
|
||||
|
||||
return {
|
||||
labels: allRoundsEconomy.map((r) => `R${r.round}`),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Terrorists',
|
||||
data: allRoundsEconomy.map((r) => r.t_equipment),
|
||||
borderColor: '#d4a74a',
|
||||
backgroundColor: 'rgba(212, 167, 74, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Counter-Terrorists',
|
||||
data: allRoundsEconomy.map((r) => r.ct_equipment),
|
||||
borderColor: '#5e98d9',
|
||||
backgroundColor: 'rgba(94, 152, 217, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// Summary stats
|
||||
const fullBuyCount = $derived({
|
||||
t: allRoundsEconomy.filter((r) => r.t_buyType === 'full').length,
|
||||
ct: allRoundsEconomy.filter((r) => r.ct_buyType === 'full').length
|
||||
});
|
||||
const ecoCount = $derived({
|
||||
t: allRoundsEconomy.filter((r) => r.t_buyType === 'eco').length,
|
||||
ct: allRoundsEconomy.filter((r) => r.ct_buyType === 'eco').length
|
||||
});
|
||||
|
||||
// Toggle for showing charts vs details
|
||||
let showCharts = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
{#if hasRoundData}
|
||||
<!-- View Toggle -->
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="inline-flex rounded-lg border border-white/10 bg-void-light p-1">
|
||||
<button
|
||||
onclick={() => (showCharts = false)}
|
||||
class="flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all {!showCharts
|
||||
? 'bg-neon-blue/20 text-neon-blue'
|
||||
: 'text-white/60 hover:text-white'}"
|
||||
>
|
||||
<DollarSign class="h-4 w-4" />
|
||||
Round Details
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (showCharts = true)}
|
||||
class="flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all {showCharts
|
||||
? 'bg-neon-blue/20 text-neon-blue'
|
||||
: 'text-white/60 hover:text-white'}"
|
||||
>
|
||||
<BarChart3 class="h-4 w-4" />
|
||||
Economy Overview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showCharts}
|
||||
<!-- Economy Overview Charts -->
|
||||
<div class="space-y-6">
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<Card padding="lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-blue/20">
|
||||
<BarChart3 class="h-5 w-5 text-neon-blue" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-white/50">Total Rounds</div>
|
||||
<div class="text-3xl font-bold text-white">{totalRounds}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card padding="lg" class="border-l-4 border-l-terrorist">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-terrorist/20">
|
||||
<TrendingUp class="h-5 w-5 text-terrorist" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-white/50">T Full Buys</div>
|
||||
<div class="text-3xl font-bold text-white">{fullBuyCount.t}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-red-400">{ecoCount.t} eco rounds</div>
|
||||
</Card>
|
||||
|
||||
<Card padding="lg" class="border-l-4 border-l-ct">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-ct/20">
|
||||
<TrendingUp class="h-5 w-5 text-ct" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-white/50">CT Full Buys</div>
|
||||
<div class="text-3xl font-bold text-white">{fullBuyCount.ct}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-red-400">{ecoCount.ct} eco rounds</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Economy Flow Chart -->
|
||||
{#if economyFlowChartData}
|
||||
<Card padding="lg">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-green/20">
|
||||
<DollarSign class="h-5 w-5 text-neon-green" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Economy Flow</h2>
|
||||
<p class="text-sm text-white/50">Net-worth differential over time</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<LineChart
|
||||
data={economyFlowChartData}
|
||||
height={350}
|
||||
options={{
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: 'rgba(255, 255, 255, 0.5)' }
|
||||
},
|
||||
x: {
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: 'rgba(255, 255, 255, 0.5)' }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { labels: { color: 'rgba(255, 255, 255, 0.7)' } }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if totalRounds > halftimeRound}
|
||||
<div
|
||||
class="pointer-events-none absolute top-0 flex h-full items-center"
|
||||
style="left: {(halftimeRound / totalRounds) * 100}%"
|
||||
>
|
||||
<div class="h-full w-px bg-neon-purple/40"></div>
|
||||
<div
|
||||
class="absolute -top-1 left-1/2 -translate-x-1/2 rounded-md border border-neon-purple/30 bg-void-light px-2 py-1 text-xs font-medium text-neon-purple"
|
||||
>
|
||||
Half
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<!-- Equipment Value Chart -->
|
||||
{#if equipmentChartData}
|
||||
<Card padding="lg">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-purple/20">
|
||||
<Wallet class="h-5 w-5 text-neon-purple" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Equipment Value</h2>
|
||||
<p class="text-sm text-white/50">Total equipment value per team across rounds</p>
|
||||
</div>
|
||||
</div>
|
||||
<LineChart
|
||||
data={equipmentChartData}
|
||||
height={300}
|
||||
options={{
|
||||
scales: {
|
||||
y: {
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: 'rgba(255, 255, 255, 0.5)' }
|
||||
},
|
||||
x: {
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: 'rgba(255, 255, 255, 0.5)' }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { labels: { color: 'rgba(255, 255, 255, 0.7)' } }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Round Details View -->
|
||||
<!-- Round Selector Header -->
|
||||
<Card padding="lg">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Round Economy</h2>
|
||||
<p class="mt-1 text-sm text-white/50">
|
||||
Track team economy and buy patterns throughout the match
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={prevRound}
|
||||
disabled={selectedRound <= 1}
|
||||
class="rounded-lg border border-white/10 bg-white/5 p-2 transition-colors hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<ChevronLeft class="h-5 w-5 text-white/70" />
|
||||
</button>
|
||||
<div class="min-w-[100px] text-center">
|
||||
<span class="font-mono text-xl font-bold text-white">Round {selectedRound}</span>
|
||||
<span class="text-white/40"> / {totalRounds}</span>
|
||||
</div>
|
||||
<button
|
||||
onclick={nextRound}
|
||||
disabled={selectedRound >= totalRounds}
|
||||
class="rounded-lg border border-white/10 bg-white/5 p-2 transition-colors hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<ChevronRight class="h-5 w-5 text-white/70" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Economy Advantage Indicator -->
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if economyAdvantage > 1000}
|
||||
<TrendingUp class="h-4 w-4 text-terrorist" />
|
||||
<span class="text-sm font-medium text-terrorist"
|
||||
>T Advantage: {formatMoney(Math.abs(economyAdvantage))}</span
|
||||
>
|
||||
{:else if economyAdvantage < -1000}
|
||||
<TrendingUp class="h-4 w-4 text-ct" />
|
||||
<span class="text-sm font-medium text-ct"
|
||||
>CT Advantage: {formatMoney(Math.abs(economyAdvantage))}</span
|
||||
>
|
||||
{:else}
|
||||
<Minus class="h-4 w-4 text-white/50" />
|
||||
<span class="text-sm font-medium text-white/50">Economy Even</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Round Timeline -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-1">
|
||||
{#each { length: totalRounds } as _, index}
|
||||
{@const roundNum = index + 1}
|
||||
{@const isHalftime = roundNum === halftimeRound && totalRounds > halftimeRound}
|
||||
{@const isSelected = roundNum === selectedRound}
|
||||
{@const isFirstHalf = roundNum <= halftimeRound}
|
||||
{@const roundIsPistol = checkIsPistolRound(roundNum, halftimeRound)}
|
||||
|
||||
<button
|
||||
onclick={() => (selectedRound = roundNum)}
|
||||
class="relative flex h-8 w-8 items-center justify-center rounded-md text-xs font-bold transition-all
|
||||
{isSelected
|
||||
? 'bg-neon-blue text-white ring-2 ring-neon-blue/50'
|
||||
: roundIsPistol
|
||||
? 'bg-neon-purple/30 text-neon-purple hover:bg-neon-purple/40'
|
||||
: isFirstHalf
|
||||
? 'bg-terrorist/20 text-terrorist hover:bg-terrorist/30'
|
||||
: 'bg-ct/20 text-ct hover:bg-ct/30'}"
|
||||
title="Round {roundNum}{roundIsPistol ? ' (Pistol)' : ''}"
|
||||
>
|
||||
{roundNum}
|
||||
</button>
|
||||
|
||||
{#if isHalftime}
|
||||
<div
|
||||
class="mx-2 flex items-center gap-1 rounded-full bg-neon-purple/20 px-3 py-1 text-xs font-medium text-neon-purple"
|
||||
>
|
||||
<span>HT</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Team Economy Cards -->
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<!-- Team A (Terrorists) -->
|
||||
<Card padding="none" class="overflow-hidden border-l-4 border-l-terrorist">
|
||||
<!-- Team Header -->
|
||||
<div class="flex items-center justify-between bg-terrorist/10 px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-xl font-bold text-terrorist">Terrorists</h3>
|
||||
<span
|
||||
class={`rounded-full px-3 py-1 text-xs font-semibold ${teamABuyConfig.bgColor} ${teamABuyConfig.color}`}
|
||||
>
|
||||
{teamABuyConfig.label}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<span class={`text-xs ${teamAHealthConfig.color}`}>{teamAHealthConfig.label}</span>
|
||||
</div>
|
||||
<div class="font-mono text-lg font-bold text-neon-green">
|
||||
{formatMoney(teamAEconomy.totalBank)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Stats Summary -->
|
||||
<div class="grid grid-cols-3 gap-4 border-b border-white/10 bg-void/30 px-6 py-3">
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-white/40">Total Bank</div>
|
||||
<div class="font-mono text-sm font-semibold text-neon-green">
|
||||
{formatMoney(teamAEconomy.totalBank)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-white/40">Equipment</div>
|
||||
<div class="font-mono text-sm font-semibold text-white">
|
||||
{formatMoney(teamAEconomy.totalEquipment)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-white/40">Spent</div>
|
||||
<div class="font-mono text-sm font-semibold text-neon-red">
|
||||
{formatMoney(teamAEconomy.totalSpent)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player List -->
|
||||
<div class="divide-y divide-white/5">
|
||||
{#each teamAPlayers as player}
|
||||
<a
|
||||
href="/player/{player.player_id}"
|
||||
class="flex items-center gap-4 px-6 py-3 transition-colors hover:bg-white/5"
|
||||
>
|
||||
<!-- Avatar -->
|
||||
{#if player.avatar}
|
||||
<img
|
||||
src={player.avatar}
|
||||
alt={player.player_name || 'Player'}
|
||||
class="h-10 w-10 rounded-full border border-terrorist/30"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-terrorist/20 text-terrorist"
|
||||
>
|
||||
<DollarSign class="h-5 w-5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Player Info -->
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate font-medium text-white">
|
||||
{player.player_name || `Player ${player.player_id}`}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-white/50">
|
||||
<span
|
||||
>Bank: <span class="text-neon-green"
|
||||
>{formatMoney(getPlayerBank(player))}</span
|
||||
></span
|
||||
>
|
||||
<span
|
||||
>Equip: <span class="text-white/70">{formatMoney(player.equipment)}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spent -->
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-white/40">Spent</div>
|
||||
<div
|
||||
class="font-mono text-sm font-semibold {player.spent > 0
|
||||
? 'text-neon-red'
|
||||
: 'text-white/30'}"
|
||||
>
|
||||
{player.spent > 0 ? `-${formatMoney(player.spent)}` : '$0'}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
{#if teamAPlayers.length === 0}
|
||||
<div class="px-6 py-8 text-center text-white/40">No player data available</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Team B (Counter-Terrorists) -->
|
||||
<Card padding="none" class="overflow-hidden border-l-4 border-l-ct">
|
||||
<!-- Team Header -->
|
||||
<div class="flex items-center justify-between bg-ct/10 px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-xl font-bold text-ct">Counter-Terrorists</h3>
|
||||
<span
|
||||
class={`rounded-full px-3 py-1 text-xs font-semibold ${teamBBuyConfig.bgColor} ${teamBBuyConfig.color}`}
|
||||
>
|
||||
{teamBBuyConfig.label}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<span class={`text-xs ${teamBHealthConfig.color}`}>{teamBHealthConfig.label}</span>
|
||||
</div>
|
||||
<div class="font-mono text-lg font-bold text-neon-green">
|
||||
{formatMoney(teamBEconomy.totalBank)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Stats Summary -->
|
||||
<div class="grid grid-cols-3 gap-4 border-b border-white/10 bg-void/30 px-6 py-3">
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-white/40">Total Bank</div>
|
||||
<div class="font-mono text-sm font-semibold text-neon-green">
|
||||
{formatMoney(teamBEconomy.totalBank)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-white/40">Equipment</div>
|
||||
<div class="font-mono text-sm font-semibold text-white">
|
||||
{formatMoney(teamBEconomy.totalEquipment)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-white/40">Spent</div>
|
||||
<div class="font-mono text-sm font-semibold text-neon-red">
|
||||
{formatMoney(teamBEconomy.totalSpent)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player List -->
|
||||
<div class="divide-y divide-white/5">
|
||||
{#each teamBPlayers as player}
|
||||
<a
|
||||
href="/player/{player.player_id}"
|
||||
class="flex items-center gap-4 px-6 py-3 transition-colors hover:bg-white/5"
|
||||
>
|
||||
<!-- Avatar -->
|
||||
{#if player.avatar}
|
||||
<img
|
||||
src={player.avatar}
|
||||
alt={player.player_name || 'Player'}
|
||||
class="h-10 w-10 rounded-full border border-ct/30"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-ct/20 text-ct"
|
||||
>
|
||||
<DollarSign class="h-5 w-5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Player Info -->
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate font-medium text-white">
|
||||
{player.player_name || `Player ${player.player_id}`}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-white/50">
|
||||
<span
|
||||
>Bank: <span class="text-neon-green"
|
||||
>{formatMoney(getPlayerBank(player))}</span
|
||||
></span
|
||||
>
|
||||
<span
|
||||
>Equip: <span class="text-white/70">{formatMoney(player.equipment)}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spent -->
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-white/40">Spent</div>
|
||||
<div
|
||||
class="font-mono text-sm font-semibold {player.spent > 0
|
||||
? 'text-neon-red'
|
||||
: 'text-white/30'}"
|
||||
>
|
||||
{player.spent > 0 ? `-${formatMoney(player.spent)}` : '$0'}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
{#if teamBPlayers.length === 0}
|
||||
<div class="px-6 py-8 text-center text-white/40">No player data available</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Economy Legend -->
|
||||
<Card padding="md">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-center text-xs text-white/40">
|
||||
Buy Type Classification (based on avg equipment per player)
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-center gap-6 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class={`rounded-full px-2 py-0.5 text-xs font-semibold ${BUY_TYPE_CONFIG.pistol.bgColor} ${BUY_TYPE_CONFIG.pistol.color}`}
|
||||
>Pistol</span
|
||||
>
|
||||
<span class="text-white/50">Round 1 & {halftimeRound + 1}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class={`rounded-full px-2 py-0.5 text-xs font-semibold ${BUY_TYPE_CONFIG.eco.bgColor} ${BUY_TYPE_CONFIG.eco.color}`}
|
||||
>Eco</span
|
||||
>
|
||||
<span class="text-white/50">< $1,500</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class={`rounded-full px-2 py-0.5 text-xs font-semibold ${BUY_TYPE_CONFIG.force.bgColor} ${BUY_TYPE_CONFIG.force.color}`}
|
||||
>Force</span
|
||||
>
|
||||
<span class="text-white/50">$1,500 - $3,500 (T) / $4,000 (CT)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class={`rounded-full px-2 py-0.5 text-xs font-semibold ${BUY_TYPE_CONFIG.full.bgColor} ${BUY_TYPE_CONFIG.full.color}`}
|
||||
>Full Buy</span
|
||||
>
|
||||
<span class="text-white/50">> $3,500 (T) / $4,000 (CT)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- No Data State -->
|
||||
<Card padding="lg">
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="mb-4 rounded-full bg-white/5 p-4">
|
||||
<Info class="h-12 w-12 text-white/30" />
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-semibold text-white">Round Data Not Available</h3>
|
||||
<p class="max-w-md text-white/60">
|
||||
{#if !match.demo_parsed}
|
||||
The demo for this match is still being processed. Round-by-round economy data will be
|
||||
available once parsing is complete.
|
||||
{:else}
|
||||
Round economy data is not available for this match.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user