style: Redesign match detail pages with neon esports aesthetic

Complete overhaul of all 7 match sub-pages (Overview, Flashes, Economy,
Details, Weapons, Damage, Chat) with consistent neon design system.

Key changes:
- Update Card/Tabs components with void backgrounds and neon accents
- Add decorative blur orbs and grid pattern to match layout hero
- Convert DaisyUI classes to custom Tailwind with neon colors
- Update chart components with neon-themed tooltips and grid styling
- Add RoundTimeline neon glow on selection with void-themed tooltips

Puns added throughout:
- "Hall of Shame" for players who flash teammates more than enemies
- "Needs Therapy Award" for high team damage
- "MVP (Most Violent Player)" badge
- "The Poverty Round", "YOLO Buy" economy labels
- "Multi-Threat Level", "Can't Touch This", "Molotov Mixologist"

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-07 17:54:29 +01:00
parent 51112df979
commit ee233bb6fb
14 changed files with 1174 additions and 565 deletions

View File

@@ -72,9 +72,9 @@
<Card padding="lg">
<div class="mb-6">
<h2 class="text-2xl font-bold text-base-content">Round Timeline</h2>
<p class="mt-2 text-sm text-base-content/60">
Click on a round to see detailed information. T = Terrorists, CT = Counter-Terrorists
<h2 class="text-2xl font-bold text-white">Round Timeline</h2>
<p class="mt-2 text-sm text-white/60">
Click on a round to see the battle details. T = Terrorists, CT = Counter-Terrorists
</p>
</div>
@@ -100,25 +100,20 @@
>
<!-- Round number -->
<div
class="mb-2 text-xs font-semibold transition-colors"
class:text-primary={isSelected}
class:opacity-60={!isSelected}
class="mb-2 text-xs font-semibold transition-colors {isSelected
? 'text-neon-blue'
: 'text-white/60'}"
>
{round.round}
</div>
<!-- Round indicator circle -->
<div
class="relative flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all"
class:border-terrorist={isWinner2}
class:bg-terrorist={isWinner2}
class:bg-opacity-20={isWinner2 || isWinner3}
class:border-ct={isWinner3}
class:bg-ct={isWinner3}
class:ring-4={isSelected}
class:ring-primary={isSelected}
class:ring-opacity-30={isSelected}
class:scale-110={isSelected}
class="relative flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all {isWinner2
? 'border-terrorist bg-terrorist/20'
: ''} {isWinner3 ? 'border-ct bg-ct/20' : ''} {isSelected
? 'scale-110 shadow-[0_0_15px_rgba(0,212,255,0.4)] ring-2 ring-neon-blue'
: ''}"
>
<!-- Win reason icon or T/CT badge -->
{#if Icon}
@@ -147,18 +142,18 @@
<!-- Connecting line to next round -->
{#if round.round < rounds.length}
<div
class="absolute left-[60px] top-[34px] h-0.5 w-[calc(100%-60px)] bg-base-300"
class="absolute left-[60px] top-[34px] h-0.5 w-[calc(100%-60px)] bg-white/10"
></div>
{/if}
<!-- Hover tooltip -->
<div
class="pointer-events-none absolute top-full z-10 mt-2 hidden w-48 rounded-lg bg-base-100 p-3 text-left shadow-xl ring-1 ring-base-300 group-hover:block"
class="pointer-events-none absolute top-full z-10 mt-2 hidden w-48 rounded-lg border border-white/10 bg-void-light p-3 text-left shadow-xl backdrop-blur-sm group-hover:block"
>
<div class="text-xs font-semibold text-base-content">
<div class="text-xs font-semibold text-white">
Round {round.round}
</div>
<div class="mt-1 text-xs text-base-content/80">
<div class="mt-1 text-xs text-white/80">
Winner:
<span
class="font-bold"
@@ -168,10 +163,10 @@
{isWinner2 ? 'Terrorists' : 'Counter-Terrorists'}
</span>
</div>
<div class="mt-1 text-xs text-base-content/60">
<div class="mt-1 text-xs text-white/60">
{getWinReasonText(round.win_reason)}
</div>
<div class="mt-2 text-xs text-base-content/60">
<div class="mt-2 text-xs text-white/60">
Score: {scoreAtRound.teamA} - {scoreAtRound.teamB}
</div>
</div>
@@ -196,13 +191,13 @@
<!-- Selected Round Details -->
{#if selectedRoundData}
<div class="mt-6 border-t border-base-300 pt-6">
<div class="mt-6 border-t border-white/10 pt-6">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-xl font-bold text-base-content">
<h3 class="text-xl font-bold text-white">
Round {selectedRoundData.round} Details
</h3>
<button
class="btn btn-ghost btn-sm"
class="rounded-lg px-3 py-1.5 text-sm text-white/60 transition-colors hover:bg-white/5 hover:text-white"
onclick={() => (selectedRound = null)}
aria-label="Close details"
>
@@ -212,7 +207,7 @@
<div class="grid gap-4 md:grid-cols-2">
<div>
<div class="text-sm text-base-content/60">Winner</div>
<div class="text-sm text-white/50">Winner</div>
<div
class="text-lg font-bold"
class:text-terrorist={selectedRoundData.winner === 2}
@@ -222,8 +217,8 @@
</div>
</div>
<div>
<div class="text-sm text-base-content/60">Win Reason</div>
<div class="text-lg font-semibold text-base-content">
<div class="text-sm text-white/50">Win Reason</div>
<div class="text-lg font-semibold text-white">
{getWinReasonText(selectedRoundData.win_reason)}
</div>
</div>
@@ -232,37 +227,46 @@
<!-- Player stats for the round if available -->
{#if selectedRoundData.players && selectedRoundData.players.length > 0}
<div class="mt-4">
<h4 class="mb-2 text-sm font-semibold text-base-content">Round Economy</h4>
<h4 class="mb-2 text-sm font-semibold text-white">Round Economy</h4>
<div class="overflow-x-auto">
<table class="table table-sm">
<table class="w-full text-sm">
<thead>
<tr class="border-base-300">
<th>Player</th>
<th>Bank</th>
<th>Equipment</th>
<th>Spent</th>
<tr class="border-b border-white/10 text-left text-white/50">
<th class="px-3 py-2">Player</th>
<th class="px-3 py-2">Bank</th>
<th class="px-3 py-2">Equipment</th>
<th class="px-3 py-2">Spent</th>
{#if selectedRoundData.players.some((p) => p.kills_in_round !== undefined)}
<th>Kills</th>
<th class="px-3 py-2">Kills</th>
{/if}
{#if selectedRoundData.players.some((p) => p.damage_in_round !== undefined)}
<th>Damage</th>
<th class="px-3 py-2">Damage</th>
{/if}
</tr>
</thead>
<tbody>
{#each selectedRoundData.players as player}
<tr class="border-base-300">
<td class="font-medium"
<tr class="border-b border-white/5 transition-colors hover:bg-white/5">
<td class="px-3 py-2 font-medium text-white"
>Player {player.player_id || player.match_player_id || '?'}</td
>
<td class="font-mono text-success">${player.bank.toLocaleString()}</td>
<td class="font-mono">${player.equipment.toLocaleString()}</td>
<td class="font-mono text-error">${player.spent.toLocaleString()}</td>
<td class="px-3 py-2 font-mono text-neon-green"
>${player.bank.toLocaleString()}</td
>
<td class="px-3 py-2 font-mono text-white/80"
>${player.equipment.toLocaleString()}</td
>
<td class="px-3 py-2 font-mono text-neon-red"
>${player.spent.toLocaleString()}</td
>
{#if selectedRoundData.players.some((p) => p.kills_in_round !== undefined)}
<td class="font-mono">{player.kills_in_round || 0}</td>
<td class="px-3 py-2 font-mono text-white/80">{player.kills_in_round || 0}</td
>
{/if}
{#if selectedRoundData.players.some((p) => p.damage_in_round !== undefined)}
<td class="font-mono">{player.damage_in_round || 0}</td>
<td class="px-3 py-2 font-mono text-white/80"
>{player.damage_in_round || 0}</td
>
{/if}
</tr>
{/each}

View File

@@ -56,7 +56,7 @@
display: true,
position: 'top',
labels: {
color: 'rgb(156, 163, 175)',
color: 'rgba(255, 255, 255, 0.7)',
font: {
family: 'Inter, system-ui, sans-serif',
size: 12
@@ -64,21 +64,21 @@
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
backgroundColor: 'rgba(18, 18, 26, 0.95)',
padding: 12,
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'rgba(255, 255, 255, 0.1)',
bodyColor: 'rgba(255, 255, 255, 0.8)',
borderColor: 'rgba(0, 212, 255, 0.3)',
borderWidth: 1
}
},
scales: {
x: {
grid: {
color: 'rgba(156, 163, 175, 0.1)'
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgb(156, 163, 175)',
color: 'rgba(255, 255, 255, 0.5)',
font: {
size: 11
}
@@ -86,10 +86,10 @@
},
y: {
grid: {
color: 'rgba(156, 163, 175, 0.1)'
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgb(156, 163, 175)',
color: 'rgba(255, 255, 255, 0.5)',
font: {
size: 11
}

View File

@@ -65,7 +65,7 @@
display: true,
position: 'top',
labels: {
color: 'rgb(156, 163, 175)',
color: 'rgba(255, 255, 255, 0.7)',
font: {
family: 'Inter, system-ui, sans-serif',
size: 12
@@ -73,21 +73,21 @@
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
backgroundColor: 'rgba(18, 18, 26, 0.95)',
padding: 12,
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'rgba(255, 255, 255, 0.1)',
bodyColor: 'rgba(255, 255, 255, 0.8)',
borderColor: 'rgba(0, 212, 255, 0.3)',
borderWidth: 1
}
},
scales: {
x: {
grid: {
color: 'rgba(156, 163, 175, 0.1)'
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgb(156, 163, 175)',
color: 'rgba(255, 255, 255, 0.5)',
font: {
size: 11
}
@@ -95,10 +95,10 @@
},
y: {
grid: {
color: 'rgba(156, 163, 175, 0.1)'
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgb(156, 163, 175)',
color: 'rgba(255, 255, 255, 0.5)',
font: {
size: 11
}

View File

@@ -54,7 +54,7 @@
display: true,
position: 'bottom',
labels: {
color: 'rgb(156, 163, 175)',
color: 'rgba(255, 255, 255, 0.7)',
font: {
family: 'Inter, system-ui, sans-serif',
size: 12
@@ -63,11 +63,11 @@
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
backgroundColor: 'rgba(18, 18, 26, 0.95)',
padding: 12,
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'rgba(255, 255, 255, 0.1)',
bodyColor: 'rgba(255, 255, 255, 0.8)',
borderColor: 'rgba(0, 212, 255, 0.3)',
borderWidth: 1
}
}

View File

@@ -17,13 +17,13 @@
children
}: Props = $props();
const baseClasses = 'bg-base-200 border border-base-300 rounded-md transition-all duration-200';
const baseClasses = 'bg-void-light border border-white/10 rounded-xl transition-all duration-300';
const variantClasses = {
default: 'shadow-sm',
elevated: 'shadow-lg shadow-black/10',
default: 'shadow-sm hover:shadow-[0_0_20px_rgba(0,212,255,0.05)]',
elevated: 'shadow-lg shadow-black/20 hover:shadow-[0_0_30px_rgba(0,212,255,0.1)]',
interactive:
'cursor-pointer hover:border-primary hover:shadow-lg hover:shadow-primary/20 hover:-translate-y-0.5'
'cursor-pointer hover:border-neon-blue/50 hover:shadow-[0_0_20px_rgba(0,212,255,0.15)] hover:-translate-y-0.5'
};
const paddingClasses = {

View File

@@ -12,8 +12,7 @@
tabs: Tab[];
activeTab?: string;
onTabChange?: (value: string) => void;
variant?: 'boxed' | 'bordered' | 'lifted';
size?: 'xs' | 'sm' | 'md' | 'lg';
size?: 'sm' | 'md' | 'lg';
class?: string;
}
@@ -21,7 +20,6 @@
tabs,
activeTab = $bindable(),
onTabChange,
variant = 'bordered',
size = 'md',
class: className = ''
}: Props = $props();
@@ -43,21 +41,36 @@
}
};
const variantClass =
variant === 'boxed' ? 'tabs-boxed' : variant === 'lifted' ? 'tabs-lifted' : '';
const sizeClass =
size === 'xs' ? 'tabs-xs' : size === 'sm' ? 'tabs-sm' : size === 'lg' ? 'tabs-lg' : '';
const sizeClasses = {
sm: 'text-xs px-3 py-1.5',
md: 'text-sm px-4 py-2',
lg: 'text-base px-5 py-2.5'
};
const baseTabClasses = 'rounded-md font-medium transition-all duration-200 whitespace-nowrap';
const inactiveClasses = 'text-white/60 hover:text-white hover:bg-white/5';
const activeClasses =
'text-neon-blue bg-neon-blue/10 border border-neon-blue/50 shadow-[0_0_10px_rgba(0,212,255,0.15)]';
const disabledClasses = 'opacity-40 cursor-not-allowed pointer-events-none';
</script>
<div role="tablist" class="tabs {variantClass} {sizeClass} {className}">
<div
role="tablist"
class="inline-flex gap-1 rounded-lg bg-void/50 p-1 backdrop-blur-sm {className}"
>
{#each tabs as tab}
{@const active = isActive(tab)}
{@const classes = `${baseTabClasses} ${sizeClasses[size]} ${active ? activeClasses : inactiveClasses} ${tab.disabled ? disabledClasses : ''}`}
{#if tab.href}
<a
href={tab.href}
role="tab"
class="tab"
class:tab-active={isActive(tab)}
class:tab-disabled={tab.disabled}
class={classes}
aria-selected={active}
aria-disabled={tab.disabled}
>
{tab.label}
@@ -65,9 +78,8 @@
{:else}
<button
role="tab"
class="tab"
class:tab-active={isActive(tab)}
class:tab-disabled={tab.disabled}
class={classes}
aria-selected={active}
disabled={tab.disabled}
onclick={() => handleTabClick(tab)}
>

View File

@@ -11,7 +11,6 @@
const { match } = data;
function handleBack() {
// Navigate back to matches page
goto('/matches');
}
@@ -47,21 +46,31 @@
alert('Share code not available for this match');
return;
}
// Open the demo download URL (typically from Valve servers or cached location)
// Format: steam://rungame/730/76561202255233023/+csgo_download_match%20{SHARE_CODE}
const downloadUrl = `steam://rungame/730/76561202255233023/+csgo_download_match%20${match.share_code}`;
window.location.href = downloadUrl;
}
</script>
<!-- Match Header with Background -->
<div class="relative overflow-hidden border-b border-base-300">
<div class="relative overflow-hidden border-b border-neon-blue/20 bg-void">
<!-- Background Image -->
<div class="absolute inset-0">
<img src={mapBg} alt={mapName} class="h-full w-full object-cover" onerror={handleImageError} />
<!-- Multi-layer gradient overlay for depth and framing -->
<div class="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-black/40"></div>
<div class="absolute inset-0 bg-gradient-to-r from-black/70 via-black/40 to-black/70"></div>
<!-- Multi-layer gradient overlay for depth -->
<div class="absolute inset-0 bg-gradient-to-b from-void/80 via-transparent to-void"></div>
<div class="absolute inset-0 bg-gradient-to-r from-void/70 via-void/30 to-void/70"></div>
</div>
<!-- Decorative Neon Blur Orbs -->
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="absolute -left-32 top-10 h-64 w-64 rounded-full bg-neon-blue/20 blur-[100px]"></div>
<div
class="absolute -right-32 top-20 h-64 w-64 rounded-full bg-neon-gold/15 blur-[100px]"
></div>
<div
class="absolute inset-0 opacity-10"
style="background-image: linear-gradient(rgba(0, 212, 255, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 212, 255, 0.05) 1px, transparent 1px); background-size: 50px 50px;"
></div>
</div>
<div class="container relative mx-auto px-4 py-8">
@@ -69,7 +78,7 @@
<div class="mb-4">
<button
onclick={handleBack}
class="btn btn-sm gap-2 bg-black/60 text-white backdrop-blur-sm hover:bg-black/80"
class="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-void/60 px-3 py-2 text-sm text-white/80 backdrop-blur-sm transition-all duration-200 hover:border-neon-blue/30 hover:bg-void/80 hover:text-white"
>
<ArrowLeft class="h-4 w-4" />
<span>Back to Matches</span>
@@ -79,14 +88,17 @@
<!-- Map Name -->
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-5xl font-bold text-white drop-shadow-2xl">
<h1
class="text-5xl font-bold text-white"
style="text-shadow: 0 0 40px rgba(0, 212, 255, 0.3), 0 4px 20px rgba(0, 0, 0, 0.5);"
>
{mapName}
</h1>
</div>
{#if match.demo_parsed && match.share_code}
<button
onclick={handleDownloadDemo}
class="btn btn-ghost gap-2 border border-white/25 bg-white/15 text-white backdrop-blur-md hover:bg-white/25"
class="inline-flex items-center gap-2 rounded-lg border border-neon-blue/30 bg-neon-blue/10 px-4 py-2 text-sm font-medium text-neon-blue backdrop-blur-md transition-all duration-200 hover:border-neon-blue/50 hover:bg-neon-blue/20 hover:shadow-[0_0_20px_rgba(0,212,255,0.2)]"
title="Download this match demo to your Steam client"
>
<Download class="h-4 w-4" />
@@ -95,59 +107,67 @@
{/if}
</div>
<!-- Hero Info Panel with translucent background -->
<!-- Hero Info Panel -->
<div
class="mx-auto max-w-3xl rounded-xl border border-white/10 bg-black/40 p-6 backdrop-blur-md"
class="mx-auto max-w-3xl rounded-xl border border-white/10 bg-void/60 p-6 backdrop-blur-xl"
>
<!-- Score -->
<div class="mb-4 flex items-center justify-center gap-8">
<div class="text-center">
<div class="mb-1 text-xs font-medium uppercase tracking-wider text-white/70">
<div class="mb-1 text-xs font-medium uppercase tracking-wider text-white/60">
Terrorists
</div>
<div class="font-mono text-6xl font-bold text-terrorist drop-shadow-lg">
<div
class="font-mono text-6xl font-bold text-terrorist"
style="text-shadow: 0 0 30px rgba(212, 167, 74, 0.5);"
>
{match.score_team_a}
</div>
</div>
<div class="text-4xl font-bold text-white/50">:</div>
<div class="text-4xl font-bold text-white/30">:</div>
<div class="text-center">
<div class="mb-1 text-xs font-medium uppercase tracking-wider text-white/70">
<div class="mb-1 text-xs font-medium uppercase tracking-wider text-white/60">
Counter-Terrorists
</div>
<div class="font-mono text-6xl font-bold text-ct drop-shadow-lg">
<div
class="font-mono text-6xl font-bold text-ct"
style="text-shadow: 0 0 30px rgba(94, 152, 217, 0.5);"
>
{match.score_team_b}
</div>
</div>
</div>
<!-- Match Meta -->
<div class="flex flex-wrap items-center justify-center gap-3 text-sm text-white/90">
<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" />
<Calendar class="h-3.5 w-3.5 text-neon-blue" />
<span>{formattedDate}</span>
</div>
<span class="text-white/30"></span>
<span class="text-white/20"></span>
<div class="flex items-center gap-1.5">
<Clock class="h-3.5 w-3.5" />
<Clock class="h-3.5 w-3.5 text-neon-blue" />
<span>{duration}</span>
</div>
<span class="text-white/30"></span>
<span class="text-white/20"></span>
<span>MR12 ({match.max_rounds} rounds)</span>
{#if match.demo_parsed}
<span class="text-white/30"></span>
<span class="text-white/20"></span>
<Badge variant="success" size="sm">Demo Parsed</Badge>
{/if}
</div>
</div>
<!-- Tabs -->
<div class="mt-6 rounded-lg border border-white/10 bg-black/35 p-4 backdrop-blur-lg">
<Tabs {tabs} variant="bordered" size="md" />
<div class="mt-6 flex justify-center">
<Tabs {tabs} size="md" class="border border-white/10" />
</div>
</div>
</div>
<!-- Tab Content -->
<div class="container mx-auto px-4 py-8">
<div class="min-h-screen bg-void">
<div class="container mx-auto px-4 py-8">
{@render children()}
</div>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Trophy } from 'lucide-svelte';
import { Trophy, Zap } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import PremierRatingBadge from '$lib/components/ui/PremierRatingBadge.svelte';
@@ -51,12 +51,16 @@
const teamAStats = calcTeamStats(sortedTeamA);
const teamBStats = calcTeamStats(sortedTeamB);
// Find the overall MVP (highest kills)
const allPlayers = [...sortedTeamA, ...sortedTeamB].sort((a, b) => b.kills - a.kills);
const mvpPlayerId = allPlayers[0]?.id;
</script>
<div class="space-y-8">
<!-- Team Statistics Overview -->
<div class="grid gap-6 md:grid-cols-2">
<Card padding="lg">
<Card padding="lg" class="border-l-4 border-l-terrorist">
<div class="mb-4 flex items-center justify-between">
<div class="flex flex-col gap-2">
<h2 class="text-2xl font-bold text-terrorist">Terrorists</h2>
@@ -64,29 +68,34 @@
<PremierRatingBadge rating={teamAStats.avgRating} {match} size="sm" showIcon={true} />
{/if}
</div>
<div class="font-mono text-3xl font-bold text-terrorist">{match.score_team_a}</div>
<div
class="font-mono text-3xl font-bold text-terrorist"
style="text-shadow: 0 0 20px rgba(212, 167, 74, 0.4);"
>
{match.score_team_a}
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-sm text-base-content/60">Team K/D</div>
<div class="text-xl font-bold">{teamAStats.kd}</div>
<div class="text-sm text-white/50">Team K/D</div>
<div class="text-xl font-bold text-white">{teamAStats.kd}</div>
</div>
<div>
<div class="text-sm text-base-content/60">Avg ADR</div>
<div class="text-xl font-bold">{teamAStats.adr}</div>
<div class="text-sm text-white/50">Avg ADR</div>
<div class="text-xl font-bold text-white">{teamAStats.adr}</div>
</div>
<div>
<div class="text-sm text-base-content/60">Total Kills</div>
<div class="text-xl font-bold">{teamAStats.kills}</div>
<div class="text-sm text-white/50">Total Kills</div>
<div class="text-xl font-bold text-white">{teamAStats.kills}</div>
</div>
<div>
<div class="text-sm text-base-content/60">Avg KAST</div>
<div class="text-xl font-bold">{teamAStats.kast}%</div>
<div class="text-sm text-white/50">Avg KAST</div>
<div class="text-xl font-bold text-white">{teamAStats.kast}%</div>
</div>
</div>
</Card>
<Card padding="lg">
<Card padding="lg" class="border-l-4 border-l-ct">
<div class="mb-4 flex items-center justify-between">
<div class="flex flex-col gap-2">
<h2 class="text-2xl font-bold text-ct">Counter-Terrorists</h2>
@@ -94,24 +103,29 @@
<PremierRatingBadge rating={teamBStats.avgRating} {match} size="sm" showIcon={true} />
{/if}
</div>
<div class="font-mono text-3xl font-bold text-ct">{match.score_team_b}</div>
<div
class="font-mono text-3xl font-bold text-ct"
style="text-shadow: 0 0 20px rgba(94, 152, 217, 0.4);"
>
{match.score_team_b}
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-sm text-base-content/60">Team K/D</div>
<div class="text-xl font-bold">{teamBStats.kd}</div>
<div class="text-sm text-white/50">Team K/D</div>
<div class="text-xl font-bold text-white">{teamBStats.kd}</div>
</div>
<div>
<div class="text-sm text-base-content/60">Avg ADR</div>
<div class="text-xl font-bold">{teamBStats.adr}</div>
<div class="text-sm text-white/50">Avg ADR</div>
<div class="text-xl font-bold text-white">{teamBStats.adr}</div>
</div>
<div>
<div class="text-sm text-base-content/60">Total Kills</div>
<div class="text-xl font-bold">{teamBStats.kills}</div>
<div class="text-sm text-white/50">Total Kills</div>
<div class="text-xl font-bold text-white">{teamBStats.kills}</div>
</div>
<div>
<div class="text-sm text-base-content/60">Avg KAST</div>
<div class="text-xl font-bold">{teamBStats.kast}%</div>
<div class="text-sm text-white/50">Avg KAST</div>
<div class="text-xl font-bold text-white">{teamBStats.kast}%</div>
</div>
</div>
</Card>
@@ -120,49 +134,61 @@
<!-- Scoreboard -->
<Card padding="none">
<div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Scoreboard</h2>
<h2 class="text-2xl font-bold text-white">Scoreboard</h2>
</div>
<!-- Team A -->
<div class="border-t border-base-300 bg-terrorist/5">
<div class="px-6 py-3">
<div class="border-t border-white/10 bg-terrorist/5">
<div class="flex items-center justify-between px-6 py-3">
<h3 class="text-lg font-semibold text-terrorist">Terrorists</h3>
</div>
<div class="overflow-x-auto">
<table class="table" style="table-layout: fixed;">
<table class="w-full" style="table-layout: fixed;">
<thead>
<tr class="border-base-300">
<th style="width: 200px;">Player</th>
<th style="width: 80px;">K</th>
<th style="width: 80px;">D</th>
<th style="width: 80px;">A</th>
<th style="width: 100px;">ADR</th>
<th style="width: 100px;">HS%</th>
<th style="width: 100px;">KAST%</th>
<th style="width: 180px;">Rating</th>
<tr class="border-b border-white/10 bg-void/50 text-left text-sm text-white/50">
<th class="px-6 py-3 font-medium" style="width: 200px;">Player</th>
<th class="px-4 py-3 font-medium" style="width: 80px;">K</th>
<th class="px-4 py-3 font-medium" style="width: 80px;">D</th>
<th class="px-4 py-3 font-medium" style="width: 80px;">A</th>
<th class="px-4 py-3 font-medium" style="width: 100px;">ADR</th>
<th class="px-4 py-3 font-medium" style="width: 100px;">HS%</th>
<th class="px-4 py-3 font-medium" style="width: 100px;">KAST%</th>
<th class="px-4 py-3 font-medium" style="width: 180px;">Rating</th>
</tr>
</thead>
<tbody>
{#each sortedTeamA as player, index}
<tr class="border-base-300">
<td>
<tr class="border-b border-white/5 transition-colors hover:bg-neon-blue/5">
<td class="px-6 py-3">
<a
href={`/player/${player.id}`}
class="font-medium transition-colors hover:text-primary"
class="font-medium text-white transition-colors hover:text-neon-blue"
>
{player.name}
</a>
{#if index === 0}
<Trophy class="ml-2 inline h-4 w-4 text-warning" />
{#if player.id === mvpPlayerId}
<span
class="ml-2 inline-flex items-center gap-1 rounded-full bg-neon-gold/20 px-2 py-0.5 text-xs font-medium text-neon-gold"
title="Most Violent Player"
>
<Zap class="h-3 w-3" />
MVP
</span>
{:else if index === 0}
<Trophy class="ml-2 inline h-4 w-4 text-terrorist" />
{/if}
</td>
<td class="font-mono font-semibold">{player.kills}</td>
<td class="font-mono">{player.deaths}</td>
<td class="font-mono">{player.assists}</td>
<td class="font-mono">{(player.adr || 0).toFixed(1)}</td>
<td class="font-mono">{(player.hs_percent || 0).toFixed(1)}%</td>
<td class="font-mono">{player.kast?.toFixed(1) || '0.0'}%</td>
<td class="h-12">
<td class="px-4 py-3 font-mono font-semibold text-white">{player.kills}</td>
<td class="px-4 py-3 font-mono text-white/80">{player.deaths}</td>
<td class="px-4 py-3 font-mono text-white/80">{player.assists}</td>
<td class="px-4 py-3 font-mono text-white/80">{(player.adr || 0).toFixed(1)}</td>
<td class="px-4 py-3 font-mono text-white/80"
>{(player.hs_percent || 0).toFixed(1)}%</td
>
<td class="px-4 py-3 font-mono text-white/80"
>{player.kast?.toFixed(1) || '0.0'}%</td
>
<td class="h-12 px-4 py-3">
<div class="flex h-full items-center">
<PremierRatingBadge
rating={player.rank_new}
@@ -181,45 +207,57 @@
</div>
<!-- Team B -->
<div class="border-t border-base-300 bg-ct/5">
<div class="px-6 py-3">
<div class="border-t border-white/10 bg-ct/5">
<div class="flex items-center justify-between px-6 py-3">
<h3 class="text-lg font-semibold text-ct">Counter-Terrorists</h3>
</div>
<div class="overflow-x-auto">
<table class="table" style="table-layout: fixed;">
<table class="w-full" style="table-layout: fixed;">
<thead>
<tr class="border-base-300">
<th style="width: 200px;">Player</th>
<th style="width: 80px;">K</th>
<th style="width: 80px;">D</th>
<th style="width: 80px;">A</th>
<th style="width: 100px;">ADR</th>
<th style="width: 100px;">HS%</th>
<th style="width: 100px;">KAST%</th>
<th style="width: 180px;">Rating</th>
<tr class="border-b border-white/10 bg-void/50 text-left text-sm text-white/50">
<th class="px-6 py-3 font-medium" style="width: 200px;">Player</th>
<th class="px-4 py-3 font-medium" style="width: 80px;">K</th>
<th class="px-4 py-3 font-medium" style="width: 80px;">D</th>
<th class="px-4 py-3 font-medium" style="width: 80px;">A</th>
<th class="px-4 py-3 font-medium" style="width: 100px;">ADR</th>
<th class="px-4 py-3 font-medium" style="width: 100px;">HS%</th>
<th class="px-4 py-3 font-medium" style="width: 100px;">KAST%</th>
<th class="px-4 py-3 font-medium" style="width: 180px;">Rating</th>
</tr>
</thead>
<tbody>
{#each sortedTeamB as player, index}
<tr class="border-base-300">
<td>
<tr class="border-b border-white/5 transition-colors hover:bg-neon-blue/5">
<td class="px-6 py-3">
<a
href={`/player/${player.id}`}
class="font-medium transition-colors hover:text-primary"
class="font-medium text-white transition-colors hover:text-neon-blue"
>
{player.name}
</a>
{#if index === 0}
<Trophy class="ml-2 inline h-4 w-4 text-warning" />
{#if player.id === mvpPlayerId}
<span
class="ml-2 inline-flex items-center gap-1 rounded-full bg-neon-gold/20 px-2 py-0.5 text-xs font-medium text-neon-gold"
title="Most Violent Player"
>
<Zap class="h-3 w-3" />
MVP
</span>
{:else if index === 0}
<Trophy class="ml-2 inline h-4 w-4 text-ct" />
{/if}
</td>
<td class="font-mono font-semibold">{player.kills}</td>
<td class="font-mono">{player.deaths}</td>
<td class="font-mono">{player.assists}</td>
<td class="font-mono">{(player.adr || 0).toFixed(1)}</td>
<td class="font-mono">{(player.hs_percent || 0).toFixed(1)}%</td>
<td class="font-mono">{player.kast?.toFixed(1) || '0.0'}%</td>
<td class="h-12">
<td class="px-4 py-3 font-mono font-semibold text-white">{player.kills}</td>
<td class="px-4 py-3 font-mono text-white/80">{player.deaths}</td>
<td class="px-4 py-3 font-mono text-white/80">{player.assists}</td>
<td class="px-4 py-3 font-mono text-white/80">{(player.adr || 0).toFixed(1)}</td>
<td class="px-4 py-3 font-mono text-white/80"
>{(player.hs_percent || 0).toFixed(1)}%</td
>
<td class="px-4 py-3 font-mono text-white/80"
>{player.kast?.toFixed(1) || '0.0'}%</td
>
<td class="h-12 px-4 py-3">
<div class="flex h-full items-center">
<PremierRatingBadge
rating={player.rank_new}
@@ -244,10 +282,13 @@
{:else}
<Card padding="lg">
<div class="text-center">
<h3 class="mb-2 text-xl font-semibold text-base-content">Round Timeline</h3>
<p class="text-base-content/60">
Round-by-round timeline data is not available for this match. This requires the demo to be
fully parsed.
<h3 class="mb-2 text-xl font-semibold text-white">Round Timeline</h3>
<p class="text-white/60">
{#if !match.demo_parsed}
Still processing the evidence of your crimes... Demo parsing in progress.
{:else}
Round-by-round timeline data is not available for this match.
{/if}
</p>
{#if !match.demo_parsed}
<Badge variant="warning" size="md" class="mt-4">Demo Not Yet Parsed</Badge>

View File

@@ -1,5 +1,12 @@
<script lang="ts">
import { MessageSquare, Filter, Search, AlertCircle, Languages } from 'lucide-svelte';
import {
MessageSquare,
Filter,
Search,
AlertCircle,
Languages,
MessageCircle
} from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import type { PageData } from './$types';
@@ -15,7 +22,6 @@
// Check if text likely needs translation (contains non-ASCII or Cyrillic characters)
const mightNeedTranslation = (text: string): boolean => {
// Check for Cyrillic, Chinese, Japanese, Korean, Arabic, etc.
const nonEnglishPattern =
/[\u0400-\u04FF\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\u0600-\u06FF]/;
return nonEnglishPattern.test(text);
@@ -24,12 +30,11 @@
// Open Google Translate for a message
const translateMessage = (text: string) => {
const encodedText = encodeURIComponent(text);
// Use Google Translate web interface (auto-detect language to English)
const translateUrl = `https://translate.google.com/?sl=auto&tl=en&text=${encodedText}&op=translate`;
window.open(translateUrl, '_blank', 'width=800,height=600,noopener,noreferrer');
};
// Get unique players who sent messages - use $derived for computed values
// Get unique players who sent messages
const messagePlayers = $derived(
chatData
? Array.from(new Set(chatData.messages.map((m) => m.player_id)))
@@ -49,18 +54,12 @@
const filteredMessages = $derived(
chatData
? chatData.messages.filter((msg) => {
// Chat type filter
if (!showTeamChat && !msg.all_chat) return false;
if (!showAllChat && msg.all_chat) return false;
// Player filter
if (selectedPlayer !== null && msg.player_id !== selectedPlayer) return false;
// Search filter
if (searchQuery && !msg.message.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
return true;
})
: []
@@ -100,40 +99,72 @@
{#if !chatData}
<Card padding="lg">
<div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
<h2 class="mb-2 text-2xl font-bold text-base-content">Match Not Parsed</h2>
<p class="mb-4 text-base-content/60">
<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 chat data is not available.
</p>
<Badge variant="warning" size="lg">Demo parsing required</Badge>
</div>
</Card>
{:else if totalMessages === 0}
<Card padding="lg">
<div class="text-center">
<MessageCircle class="mx-auto mb-4 h-16 w-16 text-white/30" />
<h2 class="mb-2 text-2xl font-bold text-white">No Chat Messages</h2>
<p class="mb-4 text-white/60">No comms? Either tactical geniuses or solo queue...</p>
</div>
</Card>
{:else}
<div class="space-y-6">
<!-- Stats -->
<div class="grid gap-6 md:grid-cols-3">
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<MessageSquare class="h-5 w-5 text-primary" />
<span class="text-sm font-medium text-base-content/70">Total Messages</span>
<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);"
>
<MessageSquare class="h-5 w-5 text-neon-blue" />
</div>
<div>
<div class="text-sm text-white/50">Total Messages</div>
<div class="text-3xl font-bold text-white">{totalMessages}</div>
</div>
</div>
<div class="text-3xl font-bold text-base-content">{totalMessages}</div>
</Card>
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<MessageSquare class="h-5 w-5 text-warning" />
<span class="text-sm font-medium text-base-content/70">Team Chat</span>
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-gold/20"
style="box-shadow: 0 0 15px rgba(255, 215, 0, 0.2);"
>
<MessageSquare class="h-5 w-5 text-neon-gold" />
</div>
<div>
<div class="text-sm text-white/50">Team Chat</div>
<div class="text-3xl font-bold text-white">{teamChatCount}</div>
</div>
</div>
<div class="text-3xl font-bold text-base-content">{teamChatCount}</div>
</Card>
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<MessageSquare class="h-5 w-5 text-success" />
<span class="text-sm font-medium text-base-content/70">All Chat</span>
<div class="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);"
>
<MessageSquare class="h-5 w-5 text-neon-green" />
</div>
<div>
<div class="text-sm text-white/50">All Chat</div>
<div class="text-3xl font-bold text-white">{allChatCount}</div>
</div>
</div>
<div class="text-3xl font-bold text-base-content">{allChatCount}</div>
</Card>
</div>
@@ -141,25 +172,36 @@
<Card padding="lg">
<div class="space-y-4">
<div class="flex items-center gap-2">
<Filter class="h-5 w-5 text-base-content" />
<h3 class="font-semibold">Filters</h3>
<Filter class="h-5 w-5 text-neon-blue" />
<h3 class="font-semibold text-white">Filters</h3>
</div>
<div class="flex flex-wrap gap-4">
<!-- Chat Type -->
<div class="flex gap-2">
<label class="label cursor-pointer gap-2">
<input type="checkbox" bind:checked={showTeamChat} class="checkbox checkbox-sm" />
<span class="label-text">Team Chat</span>
<div class="flex gap-4">
<label class="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
bind:checked={showTeamChat}
class="h-4 w-4 rounded border-white/20 bg-void text-neon-blue focus:ring-neon-blue/50"
/>
<span class="text-sm text-white/80">Team Chat</span>
</label>
<label class="label cursor-pointer gap-2">
<input type="checkbox" bind:checked={showAllChat} class="checkbox checkbox-sm" />
<span class="label-text">All Chat</span>
<label class="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
bind:checked={showAllChat}
class="h-4 w-4 rounded border-white/20 bg-void text-neon-blue focus:ring-neon-blue/50"
/>
<span class="text-sm text-white/80">All Chat</span>
</label>
</div>
<!-- Player Filter -->
<select bind:value={selectedPlayer} class="select select-bordered select-sm">
<select
bind:value={selectedPlayer}
class="rounded-lg border border-white/10 bg-void px-3 py-1.5 text-sm text-white focus:border-neon-blue/50 focus:outline-none focus:ring-1 focus:ring-neon-blue/50"
>
<option value={null}>All Players</option>
{#each messagePlayers as player}
<option value={player.id}>{player.name}</option>
@@ -168,12 +210,12 @@
<!-- Search -->
<div class="relative min-w-[200px] flex-1">
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-base-content/40" />
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
<input
type="text"
bind:value={searchQuery}
placeholder="Search messages..."
class="input input-sm input-bordered w-full pl-9"
class="w-full rounded-lg border border-white/10 bg-void py-1.5 pl-9 pr-3 text-sm text-white placeholder-white/40 focus:border-neon-blue/50 focus:outline-none focus:ring-1 focus:ring-neon-blue/50"
/>
</div>
</div>
@@ -183,7 +225,7 @@
<!-- Messages -->
{#if filteredMessages.length === 0}
<Card padding="lg">
<div class="text-center text-base-content/60">
<div class="text-center text-white/50">
<MessageSquare class="mx-auto mb-2 h-12 w-12" />
<p>No messages match your filters.</p>
</div>
@@ -192,12 +234,14 @@
{#each rounds as round}
<Card padding="none">
<!-- Round Header -->
<div class="border-b border-base-300 bg-base-200 px-6 py-3">
<div class="border-b border-white/10 bg-void/50 px-6 py-3">
<div class="flex items-center justify-between">
<h3 class="font-semibold text-base-content">
<h3 class="font-semibold text-white">
{round === 0 ? 'Warmup / Pre-Match' : `Round ${round}`}
</h3>
<Badge variant="default" size="sm">
<span
class="rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-xs text-white/60"
>
{messagesByRound[round] ? messagesByRound[round].length : 0} message{(messagesByRound[
round
]
@@ -205,25 +249,27 @@
: 0) !== 1
? 's'
: ''}
</Badge>
</span>
</div>
</div>
<!-- Messages -->
<div class="divide-y divide-base-300">
<div class="divide-y divide-white/5">
{#each messagesByRound[round] as message}
{@const player = match.players?.find((p) => p.id === String(message.player_id))}
{@const playerName =
message.player_name || player?.name || `Player ${message.player_id}`}
{@const teamId = player?.team_id || 0}
<div class="p-4 transition-colors hover:bg-base-200/50">
<div class="p-4 transition-colors hover:bg-white/5">
<div class="flex items-start gap-3">
<!-- Player Avatar/Icon -->
<div
class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold text-white"
class:bg-terrorist={teamId === 2}
class:bg-ct={teamId === 3}
class:bg-base-300={teamId === 0}
class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold text-white {teamId ===
2
? 'bg-terrorist'
: teamId === 3
? 'bg-ct'
: 'bg-white/20'}"
>
{playerName.charAt(0).toUpperCase()}
</div>
@@ -233,24 +279,33 @@
<div class="flex items-baseline gap-2">
<a
href={`/player/${message.player_id || 0}`}
class="font-semibold hover:underline"
class="font-semibold transition-colors hover:text-neon-blue"
class:text-terrorist={teamId === 2}
class:text-ct={teamId === 3}
class:text-white={teamId === 0}
>
{playerName}
</a>
{#if message.all_chat}
<Badge variant="success" size="sm">All Chat</Badge>
<span
class="rounded-md border border-neon-green/30 bg-neon-green/10 px-1.5 py-0.5 text-xs text-neon-green"
>
All Chat
</span>
{:else}
<Badge variant="default" size="sm">Team</Badge>
<span
class="rounded-md border border-white/20 bg-white/5 px-1.5 py-0.5 text-xs text-white/60"
>
Team
</span>
{/if}
</div>
<div class="mt-1 flex items-start gap-2">
<p class="break-words text-base-content">{message.message}</p>
<p class="break-words text-white/90">{message.message}</p>
{#if mightNeedTranslation(message.message)}
<button
onclick={() => translateMessage(message.message)}
class="btn btn-ghost btn-xs flex-shrink-0 gap-1"
class="flex shrink-0 items-center gap-1 rounded-md border border-neon-blue/30 bg-neon-blue/10 px-2 py-0.5 text-xs text-neon-blue transition-colors hover:bg-neon-blue/20"
title="Translate message"
aria-label="Translate to English"
>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Target, Crosshair, AlertCircle } from 'lucide-svelte';
import { Target, Crosshair, AlertCircle, Flame, Skull, Lightbulb } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import DataTable from '$lib/components/data-display/DataTable.svelte';
@@ -23,8 +23,6 @@
const damage = player.dmg_enemy || 0;
const avgDamagePerRound = match.max_rounds > 0 ? damage / match.max_rounds : 0;
// Note: Hit group breakdown would require weapon stats data
// For now, using total damage metrics
return {
...player,
damage,
@@ -69,6 +67,11 @@
// Top damage dealers (top 3)
const topDamageDealers = sortedByDamage.slice(0, 3);
// Find player with highest team damage (needs therapy)
const needsTherapyPlayer = [...playersWithDamageStats].sort(
(a, b) => (b.dmg_team || 0) - (a.dmg_team || 0)
)[0];
// Damage table columns
const damageColumns = [
{
@@ -77,7 +80,7 @@
sortable: true,
render: (value: unknown, row: (typeof playersWithDamageStats)[0]) => {
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
return `<a href="/player/${row.id}" class="font-medium hover:underline ${teamClass}">${value}</a>`;
return `<a href="/player/${row.id}" class="font-medium hover:text-neon-blue transition-colors ${teamClass}">${value}</a>`;
}
},
{
@@ -85,15 +88,15 @@
label: 'Damage Dealt',
sortable: true,
align: 'right' as const,
class: 'font-mono font-semibold',
class: 'font-mono font-semibold text-white',
format: (value: unknown) => (typeof value === 'number' ? value.toLocaleString() : '0')
},
{
key: 'avgDamagePerRound' as const,
label: 'Avg Damage/Round',
label: 'ADR',
sortable: true,
align: 'right' as const,
class: 'font-mono',
class: 'font-mono text-white/80',
format: (value: unknown) => (typeof value === 'number' ? value.toFixed(1) : '0.0')
},
{
@@ -101,14 +104,14 @@
label: 'Headshots',
sortable: true,
align: 'center' as const,
class: 'font-mono'
class: 'font-mono text-white/80'
},
{
key: 'kills' as const,
label: 'Kills',
sortable: true,
align: 'center' as const,
class: 'font-mono'
class: 'font-mono text-white/80'
},
{
key: 'dmg_team' as const,
@@ -118,14 +121,13 @@
class: 'font-mono',
render: (value: unknown) => {
const dmg = typeof value === 'number' ? value : 0;
if (!dmg || dmg === 0) return '<span class="text-base-content/40">-</span>';
return `<span class="text-error">${dmg.toLocaleString()}</span>`;
if (!dmg || dmg === 0) return '<span class="text-white/30">-</span>';
return `<span class="text-neon-red">${dmg.toLocaleString()}</span>`;
}
}
];
// Hit group distribution data (placeholder - would need weapon stats data)
// For now, showing utility damage breakdown instead
// Utility damage data with neon colors
const utilityDamageData = hasPlayerData
? {
labels: ['HE Grenades', 'Fire (Molotov/Inc)'],
@@ -137,10 +139,10 @@
playersWithDamageStats.reduce((sum, p) => sum + (p.ud_flames || 0), 0)
],
backgroundColor: [
'rgba(34, 197, 94, 0.8)', // Green for HE
'rgba(239, 68, 68, 0.8)' // Red for Fire
'rgba(0, 255, 136, 0.8)', // neon-green for HE
'rgba(255, 51, 102, 0.8)' // neon-red for Fire
],
borderColor: ['rgba(34, 197, 94, 1)', 'rgba(239, 68, 68, 1)'],
borderColor: ['#00ff88', '#ff3366'],
borderWidth: 2
}
]
@@ -158,10 +160,13 @@
{#if !hasPlayerData}
<Card padding="lg">
<div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
<h2 class="mb-2 text-2xl font-bold text-base-content">No Player Data Available</h2>
<p class="mb-4 text-base-content/60">
Detailed damage statistics are not available for this match.
<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">No Player Data Available</h2>
<p class="mb-4 text-white/60">
Detailed damage statistics are not available for this match. The pain remains unquantified.
</p>
<Badge variant="warning" size="lg">Player data unavailable</Badge>
</div>
@@ -171,18 +176,18 @@
<!-- Team Damage Summary Cards -->
<div class="grid gap-6 md:grid-cols-2">
<!-- Terrorists Damage Stats -->
<Card padding="lg">
<Card padding="lg" class="border-l-4 border-l-terrorist">
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Damage</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-sm text-base-content/60">Total Damage</div>
<div class="text-3xl font-bold text-base-content">
<div class="text-sm text-white/50">Total Damage</div>
<div class="text-3xl font-bold text-white">
{teamAStats.totalDamage.toLocaleString()}
</div>
</div>
<div>
<div class="text-sm text-base-content/60">Avg per Player</div>
<div class="text-3xl font-bold text-base-content">
<div class="text-sm text-white/50">Avg per Player</div>
<div class="text-3xl font-bold text-white">
{Math.round(teamAStats.avgDamagePerPlayer).toLocaleString()}
</div>
</div>
@@ -190,18 +195,18 @@
</Card>
<!-- Counter-Terrorists Damage Stats -->
<Card padding="lg">
<Card padding="lg" class="border-l-4 border-l-ct">
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists Damage</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-sm text-base-content/60">Total Damage</div>
<div class="text-3xl font-bold text-base-content">
<div class="text-sm text-white/50">Total Damage</div>
<div class="text-3xl font-bold text-white">
{teamBStats.totalDamage.toLocaleString()}
</div>
</div>
<div>
<div class="text-sm text-base-content/60">Avg per Player</div>
<div class="text-3xl font-bold text-base-content">
<div class="text-sm text-white/50">Avg per Player</div>
<div class="text-3xl font-bold text-white">
{Math.round(teamBStats.avgDamagePerPlayer).toLocaleString()}
</div>
</div>
@@ -215,47 +220,97 @@
<Card padding="lg">
<div class="mb-3 flex items-center justify-between">
<div class="flex items-center gap-2">
<Target
class="h-5 w-5 {index === 0
? 'text-warning'
<div
class="flex h-8 w-8 items-center justify-center rounded-lg {index === 0
? 'bg-neon-gold/20'
: index === 1
? 'text-base-content/70'
: 'text-base-content/50'}"
? 'bg-white/10'
: 'bg-white/5'}"
style={index === 0 ? 'box-shadow: 0 0 10px rgba(255, 215, 0, 0.2);' : ''}
>
<Target
class="h-4 w-4 {index === 0
? 'text-neon-gold'
: index === 1
? 'text-white/70'
: 'text-white/50'}"
/>
<h3 class="font-semibold text-base-content">
</div>
<h3 class="font-semibold text-white">
#{index + 1} Damage Dealer
</h3>
</div>
</div>
<div
class="text-2xl font-bold {player.team_id === firstTeamId
<a
href={`/player/${player.id}`}
class="text-2xl font-bold transition-colors hover:text-neon-blue {player.team_id ===
firstTeamId
? 'text-terrorist'
: 'text-ct'}"
>
{player.name}
</div>
<div class="mt-1 font-mono text-3xl font-bold text-primary">
</a>
<div
class="mt-1 font-mono text-3xl font-bold text-neon-blue"
style="text-shadow: 0 0 15px rgba(0, 212, 255, 0.4);"
>
{player.damage.toLocaleString()}
</div>
<div class="mt-2 text-xs text-base-content/60">
<div class="mt-2 text-xs text-white/50">
{player.avgDamagePerRound.toFixed(1)} ADR
</div>
</Card>
{/each}
</div>
<!-- Needs Therapy Badge -->
{#if needsTherapyPlayer && (needsTherapyPlayer.dmg_team || 0) > 50}
<Card padding="lg" class="border-neon-red/30 bg-neon-red/5">
<div class="flex items-center gap-4">
<div
class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.3);"
>
<Skull class="h-6 w-6 text-neon-red" />
</div>
<div>
<h3 class="text-lg font-bold text-neon-red">Needs Therapy Award</h3>
<p class="text-sm text-white/60">
<a
href={`/player/${needsTherapyPlayer.id}`}
class="font-medium text-white hover:text-neon-blue"
>
{needsTherapyPlayer.name}
</a>
dealt
<span class="font-mono font-bold text-neon-red">{needsTherapyPlayer.dmg_team}</span> damage
to their own team. Apologize in chat!
</p>
</div>
</div>
</Card>
{/if}
<!-- Utility Damage Distribution -->
<Card padding="lg">
<div class="mb-4">
<h2 class="text-2xl font-bold text-base-content">Utility Damage Distribution</h2>
<p class="text-sm text-base-content/60">
Breakdown of damage dealt by grenades and fire across all players
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.2);"
>
<Flame class="h-5 w-5 text-neon-red" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Utility Damage Distribution</h2>
<p class="text-sm text-white/50">
Breakdown of damage dealt by grenades and fire - The Molotov Mixologist's Report
</p>
</div>
</div>
{#if utilityDamageData.datasets.length > 0 && utilityDamageData.datasets[0]?.data.some((v) => v > 0)}
<PieChart data={utilityDamageData} height={300} />
{:else}
<div class="py-12 text-center text-base-content/40">
<div class="py-12 text-center text-white/40">
<Crosshair class="mx-auto mb-2 h-12 w-12" />
<p>No utility damage recorded for this match</p>
</div>
@@ -265,20 +320,27 @@
<!-- Player Damage Table -->
<Card padding="none">
<div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Player Damage Statistics</h2>
<p class="mt-1 text-sm text-base-content/60">Detailed damage breakdown for all players</p>
<h2 class="text-2xl font-bold text-white">Player Damage Statistics</h2>
<p class="mt-1 text-sm text-white/50">
Detailed damage breakdown for all players - The pain ledger
</p>
</div>
<DataTable data={sortedByDamage} columns={damageColumns} striped hoverable />
</Card>
<!-- Additional Info Note -->
<Card padding="lg">
<div class="flex items-start gap-3">
<AlertCircle class="h-5 w-5 flex-shrink-0 text-info" />
<Card padding="lg" class="border-neon-blue/20">
<div class="flex items-start gap-4">
<div
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-neon-blue/20"
style="box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);"
>
<Lightbulb class="h-5 w-5 text-neon-blue" />
</div>
<div class="text-sm">
<h3 class="mb-1 font-semibold text-base-content">About Damage Statistics</h3>
<p class="text-base-content/70">
<h3 class="mb-1 font-semibold text-white">About Damage Statistics</h3>
<p class="text-white/60">
Damage statistics show total damage dealt to enemies throughout the match. Average
damage per round (ADR) is calculated by dividing total damage by the number of rounds
played. Hit group breakdown (head, chest, legs, etc.) is available in weapon-specific

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Trophy, Target, Flame, AlertCircle } from 'lucide-svelte';
import { Trophy, Target, Flame, AlertCircle, Crosshair } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import DataTable from '$lib/components/data-display/DataTable.svelte';
@@ -62,7 +62,7 @@
align: 'center' as const,
render: (_value: unknown, row: PlayerWithStats) => {
const avatarUrl = row.avatar || '';
return `<img src="${avatarUrl}" alt="${row.name}" class="h-10 w-10 rounded-full border-2 border-base-300" />`;
return `<img src="${avatarUrl}" alt="${row.name}" class="h-10 w-10 rounded-full border-2 border-white/10" />`;
}
},
{
@@ -77,7 +77,7 @@
const colorDot = colorHex
? `<span class="inline-block h-3 w-3 rounded-full mr-2" style="background-color: ${colorHex}"></span>`
: '';
return `<a href="/player/${row.id}" class="flex items-center font-medium hover:underline ${teamClass}">${colorDot}${strValue}</a>`;
return `<a href="/player/${row.id}" class="flex items-center font-medium hover:text-neon-blue transition-colors ${teamClass}">${colorDot}${strValue}</a>`;
}
},
{
@@ -85,35 +85,35 @@
label: 'Score',
sortable: true,
align: 'center' as const,
class: 'font-mono font-semibold'
class: 'font-mono font-semibold text-white'
},
{
key: 'kills' as keyof (typeof playersWithStats)[0],
label: 'K',
sortable: true,
align: 'center' as const,
class: 'font-mono font-semibold'
class: 'font-mono font-semibold text-white'
},
{
key: 'deaths' as keyof (typeof playersWithStats)[0],
label: 'D',
sortable: true,
align: 'center' as const,
class: 'font-mono'
class: 'font-mono text-white/80'
},
{
key: 'assists' as keyof (typeof playersWithStats)[0],
label: 'A',
sortable: true,
align: 'center' as const,
class: 'font-mono'
class: 'font-mono text-white/80'
},
{
key: 'kd' as keyof PlayerWithStats,
label: 'K/D',
sortable: true,
align: 'center' as const,
class: 'font-mono',
class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(2) : '0.00')
},
{
@@ -121,7 +121,7 @@
label: 'ADR',
sortable: true,
align: 'center' as const,
class: 'font-mono',
class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0')
},
{
@@ -129,7 +129,7 @@
label: 'HS%',
sortable: true,
align: 'center' as const,
class: 'font-mono',
class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0')
},
{
@@ -137,7 +137,7 @@
label: 'KAST%',
sortable: true,
align: 'center' as const,
class: 'font-mono',
class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '-')
},
{
@@ -145,7 +145,7 @@
label: 'MVP',
sortable: true,
align: 'center' as const,
class: 'font-mono'
class: 'font-mono text-white/80'
},
{
key: 'mk_5' as keyof (typeof playersWithStats)[0],
@@ -157,8 +157,9 @@
_row: (typeof playersWithStats)[0]
) => {
const numValue = value !== undefined ? (value as number) : 0;
if (numValue > 0) return `<span class="badge badge-warning badge-sm">${numValue}</span>`;
return '<span class="text-base-content/40">-</span>';
if (numValue > 0)
return `<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-neon-gold/20 text-neon-gold border border-neon-gold/30">${numValue}</span>`;
return '<span class="text-white/30">-</span>';
}
},
{
@@ -172,42 +173,54 @@
) => {
const badges = [];
if (row.vac) {
badges.push('<span class="badge badge-error badge-sm" title="VAC Banned">VAC</span>');
badges.push(
'<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-neon-red/20 text-neon-red border border-neon-red/30" title="VAC Banned">VAC</span>'
);
}
if (row.game_ban) {
badges.push('<span class="badge badge-error badge-sm" title="Game Banned">BAN</span>');
badges.push(
'<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-neon-red/20 text-neon-red border border-neon-red/30" title="Game Banned">BAN</span>'
);
}
if (badges.length > 0) {
return `<div class="flex gap-1 justify-center">${badges.join('')}</div>`;
}
return '<span class="text-base-content/40">-</span>';
return '<span class="text-white/30">-</span>';
}
}
];
// Multi-kill chart data
// Multi-kill chart data with neon colors
const multiKillData = {
labels: sortedPlayers.map((p) => p.name),
datasets: [
{
label: '2K',
data: sortedPlayers.map((p) => p.mk_2 || 0),
backgroundColor: 'rgba(34, 197, 94, 0.8)'
backgroundColor: 'rgba(0, 255, 136, 0.7)', // neon-green
borderColor: '#00ff88',
borderWidth: 1
},
{
label: '3K',
data: sortedPlayers.map((p) => p.mk_3 || 0),
backgroundColor: 'rgba(59, 130, 246, 0.8)'
backgroundColor: 'rgba(0, 212, 255, 0.7)', // neon-blue
borderColor: '#00d4ff',
borderWidth: 1
},
{
label: '4K',
data: sortedPlayers.map((p) => p.mk_4 || 0),
backgroundColor: 'rgba(249, 115, 22, 0.8)'
backgroundColor: 'rgba(255, 215, 0, 0.7)', // neon-gold
borderColor: '#ffd700',
borderWidth: 1
},
{
label: '5K (Ace)',
data: sortedPlayers.map((p) => p.mk_5 || 0),
backgroundColor: 'rgba(239, 68, 68, 0.8)'
backgroundColor: 'rgba(255, 51, 102, 0.7)', // neon-red
borderColor: '#ff3366',
borderWidth: 1
}
]
};
@@ -264,10 +277,14 @@
{#if !hasPlayerData}
<Card padding="lg">
<div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
<h2 class="mb-2 text-2xl font-bold text-base-content">No Player Data Available</h2>
<p class="mb-4 text-base-content/60">
Detailed player statistics are not available for this match.
<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">No Player Data Available</h2>
<p class="mb-4 text-white/60">
Detailed player statistics are not available for this match. The scoreboard mysteries remain
unsolved.
</p>
<Badge variant="warning" size="lg">Player data unavailable</Badge>
</div>
@@ -277,47 +294,55 @@
<!-- Team Performance Summary -->
<div class="grid gap-6 md:grid-cols-2">
<!-- Terrorists Stats -->
<Card padding="lg">
<Card padding="lg" class="border-l-4 border-l-terrorist">
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Performance</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-base-content/60">Total Damage</div>
<div class="text-2xl font-bold">{teamAStats.totalDamage.toLocaleString()}</div>
<div class="text-white/50">Total Damage</div>
<div class="text-2xl font-bold text-white">
{teamAStats.totalDamage.toLocaleString()}
</div>
</div>
<div>
<div class="text-base-content/60">Utility Damage</div>
<div class="text-2xl font-bold">{teamAStats.totalUtilityDamage.toLocaleString()}</div>
<div class="text-white/50">Utility Damage</div>
<div class="text-2xl font-bold text-white">
{teamAStats.totalUtilityDamage.toLocaleString()}
</div>
</div>
<div>
<div class="text-base-content/60">Flash Assists</div>
<div class="text-2xl font-bold">{teamAStats.totalFlashAssists}</div>
<div class="text-white/50">Flash Assists</div>
<div class="text-2xl font-bold text-white">{teamAStats.totalFlashAssists}</div>
</div>
<div>
<div class="text-base-content/60">Avg KAST</div>
<div class="text-2xl font-bold">{teamAStats.avgKAST}%</div>
<div class="text-white/50">Avg KAST</div>
<div class="text-2xl font-bold text-white">{teamAStats.avgKAST}%</div>
</div>
</div>
</Card>
<!-- Counter-Terrorists Stats -->
<Card padding="lg">
<Card padding="lg" class="border-l-4 border-l-ct">
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists Performance</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-base-content/60">Total Damage</div>
<div class="text-2xl font-bold">{teamBStats.totalDamage.toLocaleString()}</div>
<div class="text-white/50">Total Damage</div>
<div class="text-2xl font-bold text-white">
{teamBStats.totalDamage.toLocaleString()}
</div>
</div>
<div>
<div class="text-base-content/60">Utility Damage</div>
<div class="text-2xl font-bold">{teamBStats.totalUtilityDamage.toLocaleString()}</div>
<div class="text-white/50">Utility Damage</div>
<div class="text-2xl font-bold text-white">
{teamBStats.totalUtilityDamage.toLocaleString()}
</div>
</div>
<div>
<div class="text-base-content/60">Flash Assists</div>
<div class="text-2xl font-bold">{teamBStats.totalFlashAssists}</div>
<div class="text-white/50">Flash Assists</div>
<div class="text-2xl font-bold text-white">{teamBStats.totalFlashAssists}</div>
</div>
<div>
<div class="text-base-content/60">Avg KAST</div>
<div class="text-2xl font-bold">{teamBStats.avgKAST}%</div>
<div class="text-white/50">Avg KAST</div>
<div class="text-2xl font-bold text-white">{teamBStats.avgKAST}%</div>
</div>
</div>
</Card>
@@ -325,21 +350,59 @@
<!-- Multi-Kills Chart -->
<Card padding="lg">
<div class="mb-4">
<h2 class="text-2xl font-bold text-base-content">Multi-Kill Distribution</h2>
<p class="text-sm text-base-content/60">
Double kills (2K), triple kills (3K), quad kills (4K), and aces (5K) per player
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.2);"
>
<Crosshair class="h-5 w-5 text-neon-red" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Multi-Threat Level</h2>
<p class="text-sm text-white/50">
Double kills, triple kills, quad kills, and aces - Who went absolutely mental
</p>
</div>
<BarChart data={multiKillData} height={300} />
</div>
<BarChart
data={multiKillData}
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>
<!-- Detailed Player Statistics Table -->
<Card padding="none">
<div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Detailed Player Statistics</h2>
<p class="mt-1 text-sm text-base-content/60">
Complete performance breakdown for all players
<h2 class="text-2xl font-bold text-white">Detailed Player Statistics</h2>
<p class="mt-1 text-sm text-white/50">
Complete performance breakdown for all players - The full criminal record
</p>
</div>
@@ -352,14 +415,27 @@
<!-- Most Kills -->
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Trophy class="h-5 w-5 text-warning" />
<h3 class="font-semibold text-base-content">Most Kills</h3>
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-neon-gold/20"
style="box-shadow: 0 0 10px rgba(255, 215, 0, 0.2);"
>
<Trophy class="h-4 w-4 text-neon-gold" />
</div>
<div class="text-2xl font-bold text-base-content">{sortedPlayers[0].name}</div>
<div class="mt-1 font-mono text-3xl font-bold text-primary">
<h3 class="font-semibold text-white">Most Kills</h3>
</div>
<a
href={`/player/${sortedPlayers[0].id}`}
class="text-2xl font-bold text-white transition-colors hover:text-neon-blue"
>
{sortedPlayers[0].name}
</a>
<div
class="mt-1 font-mono text-3xl font-bold text-neon-blue"
style="text-shadow: 0 0 15px rgba(0, 212, 255, 0.4);"
>
{sortedPlayers[0].kills}
</div>
<div class="mt-2 text-xs text-base-content/60">
<div class="mt-2 text-xs text-white/50">
{sortedPlayers[0].deaths} deaths, {sortedPlayers[0].kd.toFixed(2)} K/D
</div>
</Card>
@@ -369,12 +445,27 @@
{#if bestKD}
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Target class="h-5 w-5 text-success" />
<h3 class="font-semibold text-base-content">Best K/D Ratio</h3>
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-neon-green/20"
style="box-shadow: 0 0 10px rgba(0, 255, 136, 0.2);"
>
<Target class="h-4 w-4 text-neon-green" />
</div>
<div class="text-2xl font-bold text-base-content">{bestKD.name}</div>
<div class="mt-1 font-mono text-3xl font-bold text-success">{bestKD.kd.toFixed(2)}</div>
<div class="mt-2 text-xs text-base-content/60">
<h3 class="font-semibold text-white">Can't Touch This</h3>
</div>
<a
href={`/player/${bestKD.id}`}
class="text-2xl font-bold text-white transition-colors hover:text-neon-blue"
>
{bestKD.name}
</a>
<div
class="mt-1 font-mono text-3xl font-bold text-neon-green"
style="text-shadow: 0 0 15px rgba(0, 255, 136, 0.4);"
>
{bestKD.kd.toFixed(2)}
</div>
<div class="mt-2 text-xs text-white/50">
{bestKD.kills}K / {bestKD.deaths}D
</div>
</Card>
@@ -387,14 +478,27 @@
{#if bestUtility}
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Flame class="h-5 w-5 text-error" />
<h3 class="font-semibold text-base-content">Most Utility Damage</h3>
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 10px rgba(255, 51, 102, 0.2);"
>
<Flame class="h-4 w-4 text-neon-red" />
</div>
<div class="text-2xl font-bold text-base-content">{bestUtility.name}</div>
<div class="mt-1 font-mono text-3xl font-bold text-error">
<h3 class="font-semibold text-white">The Molotov Mixologist</h3>
</div>
<a
href={`/player/${bestUtility.id}`}
class="text-2xl font-bold text-white transition-colors hover:text-neon-blue"
>
{bestUtility.name}
</a>
<div
class="mt-1 font-mono text-3xl font-bold text-neon-red"
style="text-shadow: 0 0 15px rgba(255, 51, 102, 0.4);"
>
{((bestUtility.ud_he || 0) + (bestUtility.ud_flames || 0)).toLocaleString()}
</div>
<div class="mt-2 text-xs text-base-content/60">
<div class="mt-2 text-xs text-white/50">
HE: {bestUtility.ud_he || 0} | Fire: {bestUtility.ud_flames || 0}
</div>
</Card>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { TrendingUp, ShoppingCart, AlertCircle } from 'lucide-svelte';
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';
@@ -17,7 +17,7 @@
winner: number;
teamA_buyType: string;
teamB_buyType: string;
economyAdvantage: number; // Cumulative economy differential (teamA - teamB)
economyAdvantage: number;
}
let { data }: { data: PageData } = $props();
@@ -28,8 +28,6 @@
const ctTeamId = 3;
// Calculate halftime round based on max_rounds
// MR12 (24 rounds): halftime after round 12
// MR15 (30 rounds): halftime after round 15
const halfPoint = match.max_rounds === 30 ? 15 : 12;
// Only process if rounds data exists
@@ -93,19 +91,13 @@
return 'Full Buy';
};
// Calculate per-round economy advantage using bank + spent (like old portal)
// Teams swap sides at halftime, so we need to account for perspective flip
const t_totalEconomy = t_bank + t_spent;
const ct_totalEconomy = ct_bank + ct_spent;
// Determine perspective based on round (teams swap at half)
// halfPoint is calculated above based on match.max_rounds
let economyAdvantage;
if (roundData.round <= halfPoint) {
// First half: T - CT
economyAdvantage = t_totalEconomy - ct_totalEconomy;
} else {
// Second half: CT - T (teams swapped sides)
economyAdvantage = ct_totalEconomy - t_totalEconomy;
}
@@ -124,23 +116,23 @@
});
}
// Prepare equipment value chart data
// 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: 'rgb(249, 115, 22)',
backgroundColor: 'rgba(249, 115, 22, 0.1)',
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: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderColor: '#5e98d9', // ct color
backgroundColor: 'rgba(94, 152, 217, 0.1)',
fill: true,
tension: 0.4
}
@@ -148,7 +140,6 @@
};
// Prepare economy advantage chart data
// Positive = above 0, Negative = below 0
halfRoundIndex = Math.floor(teamEconomy.length / 2);
economyAdvantageChartData = {
labels: teamEconomy.map((r) => `${r.round}`),
@@ -156,8 +147,8 @@
{
label: 'Advantage',
data: teamEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.6)',
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,
@@ -167,8 +158,8 @@
{
label: 'Disadvantage',
data: teamEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)),
borderColor: 'rgb(249, 115, 22)',
backgroundColor: 'rgba(249, 115, 22, 0.6)',
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,
@@ -186,6 +177,14 @@
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 = [
{
@@ -200,15 +199,15 @@
sortable: true,
render: (value: string | number | boolean, _row: TeamEconomy) => {
const strValue = value as string;
const variant =
const colorClass =
strValue === 'Full Buy'
? 'success'
? 'bg-neon-green/20 text-neon-green border-neon-green/30'
: strValue === 'Eco'
? 'error'
? 'bg-neon-red/20 text-neon-red border-neon-red/30'
: strValue === 'Force'
? 'warning'
: 'default';
return `<span class="badge badge-${variant} badge-sm">${strValue}</span>`;
? '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>`;
}
},
{
@@ -225,15 +224,15 @@
sortable: true,
render: (value: string | number | boolean, _row: TeamEconomy) => {
const strValue = value as string;
const variant =
const colorClass =
strValue === 'Full Buy'
? 'success'
? 'bg-neon-green/20 text-neon-green border-neon-green/30'
: strValue === 'Eco'
? 'error'
? 'bg-neon-red/20 text-neon-red border-neon-red/30'
: strValue === 'Force'
? 'warning'
: 'default';
return `<span class="badge badge-${variant} badge-sm">${strValue}</span>`;
? '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>`;
}
},
{
@@ -251,10 +250,10 @@
render: (value: string | number | boolean, _row: TeamEconomy) => {
const numValue = value as number;
if (numValue === 2)
return '<span class="badge badge-sm" style="background-color: rgb(249, 115, 22); color: white;">T</span>';
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="badge badge-sm" style="background-color: rgb(59, 130, 246); color: white;">CT</span>';
return '<span class="text-base-content/40">-</span>';
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>';
}
}
];
@@ -263,10 +262,14 @@
{#if !roundsData}
<Card padding="lg">
<div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
<h2 class="mb-2 text-2xl font-bold text-base-content">Match Not Parsed</h2>
<p class="mb-4 text-base-content/60">
This match hasn't been parsed yet, so detailed economy data is not available.
<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>
@@ -275,9 +278,19 @@
<div class="space-y-6">
<!-- Economy Advantage Chart -->
<Card padding="lg">
<div class="mb-4">
<h2 class="text-2xl font-bold text-base-content">Economy</h2>
<p class="text-sm text-base-content/60">Net-worth differential (bank + spent)</p>
<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">
@@ -291,19 +304,37 @@
grid: {
color: (context) => {
if (context.tick.value === 0) {
return 'rgba(156, 163, 175, 0.5)'; // Stronger line at 0
return 'rgba(255, 255, 255, 0.3)';
}
return 'rgba(156, 163, 175, 0.1)';
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)'
}
}
}
}}
/>
@@ -312,11 +343,11 @@
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-base-content/20"></div>
<div class="h-full w-px bg-neon-blue/30"></div>
<div
class="absolute -top-1 left-1/2 -translate-x-1/2 rounded bg-base-300 px-2 py-1 text-xs font-medium text-base-content/70"
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-Point
Half-Time
</div>
</div>
{/if}
@@ -327,54 +358,111 @@
<!-- Summary Cards -->
<div class="grid gap-6 md:grid-cols-3">
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<ShoppingCart class="h-5 w-5 text-primary" />
<span class="text-sm font-medium text-base-content/70">Total Rounds</span>
<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 class="text-3xl font-bold text-base-content">{totalRounds}</div>
<div class="mt-1 text-xs text-base-content/60">
<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">
<div class="mb-2 flex items-center gap-2">
<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" />
<span class="text-sm font-medium text-base-content/70">Terrorists Buy Rounds</span>
</div>
<div class="text-3xl font-bold text-base-content">{teamA_fullBuys}</div>
<div class="mt-1 text-xs text-base-content/60">{teamA_ecos} eco rounds</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">
<div class="mb-2 flex items-center gap-2">
<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" />
<span class="text-sm font-medium text-base-content/70">CT Buy Rounds</span>
</div>
<div class="text-3xl font-bold text-base-content">{teamB_fullBuys}</div>
<div class="mt-1 text-xs text-base-content/60">{teamB_ecos} eco rounds</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">
<h2 class="text-2xl font-bold text-base-content">Equipment Value Over Time</h2>
<p class="text-sm text-base-content/60">
Total equipment value for each team across all rounds
</p>
<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} />
<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-base-content">Round-by-Round Economy</h2>
<p class="mt-1 text-sm text-base-content/60">
Detailed breakdown of buy types and equipment values
<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>
@@ -382,24 +470,42 @@
</Card>
<!-- Buy Type Legend -->
<Card padding="lg">
<h3 class="mb-3 text-lg font-semibold text-base-content">Buy Type Classification</h3>
<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">
<Badge variant="error" size="sm">Eco</Badge>
<span class="text-base-content/60">&lt; $1,500 avg equipment</span>
<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">&lt; $1,500 avg - "{buyTypeLabels['Eco']}"</span>
</div>
<div class="flex items-center gap-2">
<Badge variant="default" size="sm">Semi-Eco</Badge>
<span class="text-base-content/60">$1,500 - $2,500 avg equipment</span>
<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">
<Badge variant="warning" size="sm">Force</Badge>
<span class="text-base-content/60">$2,500 - $3,500 avg equipment</span>
<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">
<Badge variant="success" size="sm">Full Buy</Badge>
<span class="text-base-content/60">&gt; $3,500 avg equipment</span>
<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">&gt; $3,500 - "{buyTypeLabels['Full Buy']}"</span>
</div>
</div>
</Card>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Eye, Zap, Users } from 'lucide-svelte';
import { Eye, Zap, Users, Skull, AlertTriangle, Lightbulb } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte';
import DataTable from '$lib/components/data-display/DataTable.svelte';
import type { PageData } from './$types';
@@ -11,6 +11,7 @@
const flashStats = (match.players || [])
.map((player) => ({
name: player.name,
playerId: player.id,
team_id: player.team_id,
enemies_blinded: player.flash_total_enemy || 0,
teammates_blinded: player.flash_total_team || 0,
@@ -49,9 +50,15 @@
const teamATotals = calcTeamTotals(teamAFlashStats);
const teamBTotals = calcTeamTotals(teamBFlashStats);
// Hall of Shame - players who flashed more teammates than enemies
const hallOfShame = flashStats
.filter((p) => p.teammates_blinded > p.enemies_blinded && p.teammates_blinded > 0)
.sort((a, b) => b.teammates_blinded - a.teammates_blinded);
// Table columns with fixed widths for consistency across multiple tables
interface FlashStat {
name: string;
playerId: string;
team_id: number;
enemies_blinded: number;
teammates_blinded: number;
@@ -108,67 +115,144 @@
<!-- Summary Stats -->
<div class="grid gap-6 md:grid-cols-3">
<Card padding="lg">
<Eye class="mb-2 h-8 w-8 text-warning" />
<div class="text-3xl font-bold text-base-content">
<div class="flex items-center gap-3">
<div
class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-gold/20"
style="box-shadow: 0 0 20px rgba(255, 215, 0, 0.2);"
>
<Eye class="h-6 w-6 text-neon-gold" />
</div>
<div>
<div class="text-3xl font-bold text-white">
{teamATotals.total_enemies_blinded + teamBTotals.total_enemies_blinded}
</div>
<div class="text-sm text-base-content/60">Enemies Successfully Blinded</div>
<div class="mt-1 text-xs text-success">The correct way to use flashes</div>
<div class="text-sm text-white/60">Enemies Successfully Blinded</div>
</div>
</div>
<div class="mt-3 text-xs text-neon-green">The correct way to use flashes</div>
</Card>
<Card padding="lg">
<Zap class="mb-2 h-8 w-8 text-success" />
<div class="text-3xl font-bold text-base-content">
<div class="flex items-center gap-3">
<div
class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-green/20"
style="box-shadow: 0 0 20px rgba(0, 255, 136, 0.2);"
>
<Zap class="h-6 w-6 text-neon-green" />
</div>
<div>
<div class="text-3xl font-bold text-white">
{teamATotals.total_flash_assists + teamBTotals.total_flash_assists}
</div>
<div class="text-sm text-base-content/60">Flash Assists</div>
<div class="mt-1 text-xs text-success">Teamwork makes the dream work</div>
<div class="text-sm text-white/60">Flash Assists</div>
</div>
</div>
<div class="mt-3 text-xs text-neon-blue">Teamwork makes the dream work</div>
</Card>
<Card padding="lg">
<Users class="mb-2 h-8 w-8 text-error" />
<div class="text-3xl font-bold text-base-content">
<div class="flex items-center gap-3">
<div
class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 20px rgba(255, 51, 102, 0.2);"
>
<Users class="h-6 w-6 text-neon-red" />
</div>
<div>
<div class="text-3xl font-bold text-white">
{flashStats.reduce((sum, p) => sum + p.teammates_blinded, 0)}
</div>
<div class="text-sm text-base-content/60">Teammates Betrayed</div>
<div class="mt-1 text-xs text-error">These players owe apologies</div>
<div class="text-sm text-white/60">Teammates Betrayed</div>
</div>
</div>
<div class="mt-3 text-xs text-neon-red">These players owe apologies</div>
</Card>
</div>
<!-- Hall of Shame -->
{#if hallOfShame.length > 0}
<Card padding="lg" class="border-neon-red/30 bg-neon-red/5">
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.3);"
>
<Skull class="h-5 w-5 text-neon-red" />
</div>
<div>
<h3 class="text-lg font-bold text-neon-red">Hall of Shame</h3>
<p class="text-xs text-white/50">Players who flashed more teammates than enemies</p>
</div>
</div>
<div class="space-y-3">
{#each hallOfShame as shamePlayer, index}
<div
class="flex items-center justify-between rounded-lg border border-neon-red/20 bg-void/50 px-4 py-3"
>
<div class="flex items-center gap-3">
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-neon-red/20 text-sm font-bold text-neon-red"
>
{index + 1}
</div>
<a
href={`/player/${shamePlayer.playerId}`}
class="font-medium text-white transition-colors hover:text-neon-blue"
>
{shamePlayer.name}
</a>
</div>
<div class="flex items-center gap-4 text-sm">
<div class="text-white/60">
<span class="text-neon-green">{shamePlayer.enemies_blinded}</span> enemies
</div>
<div class="text-neon-red">
<span class="font-bold">{shamePlayer.teammates_blinded}</span> teammates
</div>
</div>
</div>
{/each}
</div>
<p class="mt-4 text-center text-xs italic text-white/40">
Maybe consider switching to smokes?
</p>
</Card>
{/if}
<!-- Team Comparison -->
<div class="grid gap-6 md:grid-cols-2">
<Card padding="lg">
<Card padding="lg" class="border-l-4 border-l-terrorist">
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-sm text-base-content/60">Enemies Blinded</span>
<span class="font-mono font-bold">{teamATotals.total_enemies_blinded}</span>
<span class="text-sm text-white/50">Enemies Blinded</span>
<span class="font-mono font-bold text-white">{teamATotals.total_enemies_blinded}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-base-content/60">Flash Assists</span>
<span class="font-mono font-bold">{teamATotals.total_flash_assists}</span>
<span class="text-sm text-white/50">Flash Assists</span>
<span class="font-mono font-bold text-white">{teamATotals.total_flash_assists}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-base-content/60">Avg per Player</span>
<span class="font-mono font-bold">{teamATotals.avg_per_player}</span>
<span class="text-sm text-white/50">Avg per Player</span>
<span class="font-mono font-bold text-white">{teamATotals.avg_per_player}</span>
</div>
</div>
</Card>
<Card padding="lg">
<Card padding="lg" class="border-l-4 border-l-ct">
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-sm text-base-content/60">Enemies Blinded</span>
<span class="font-mono font-bold">{teamBTotals.total_enemies_blinded}</span>
<span class="text-sm text-white/50">Enemies Blinded</span>
<span class="font-mono font-bold text-white">{teamBTotals.total_enemies_blinded}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-base-content/60">Flash Assists</span>
<span class="font-mono font-bold">{teamBTotals.total_flash_assists}</span>
<span class="text-sm text-white/50">Flash Assists</span>
<span class="font-mono font-bold text-white">{teamBTotals.total_flash_assists}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-base-content/60">Avg per Player</span>
<span class="font-mono font-bold">{teamBTotals.avg_per_player}</span>
<span class="text-sm text-white/50">Avg per Player</span>
<span class="font-mono font-bold text-white">{teamBTotals.avg_per_player}</span>
</div>
</div>
</Card>
@@ -177,18 +261,26 @@
<!-- Flash Effectiveness Leaderboard -->
<Card padding="none">
<div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Flash Hall of Fame (and Shame)</h2>
<p class="mt-1 text-sm text-base-content/60">
<div class="flex items-center gap-3">
<AlertTriangle
class="h-6 w-6 text-neon-gold"
style="filter: drop-shadow(0 0 8px rgba(255, 215, 0, 0.5));"
/>
<div>
<h2 class="text-2xl font-bold text-white">Flash Hall of Fame (and Shame)</h2>
<p class="mt-1 text-sm text-white/50">
Ranked by enemies blinded. Teammates blinded is tracked for... scientific purposes.
</p>
</div>
</div>
</div>
<DataTable data={flashStats} {columns} striped hoverable fixedLayout />
</Card>
<!-- Team A Details -->
<Card padding="none">
<div class="border-b border-base-300 bg-terrorist/5 p-6">
<div class="border-b border-white/10 bg-terrorist/10 p-6">
<h3 class="text-xl font-bold text-terrorist">Terrorists - Flash Stats</h3>
</div>
<DataTable data={teamAFlashStats} {columns} striped hoverable fixedLayout />
@@ -196,37 +288,69 @@
<!-- Team B Details -->
<Card padding="none">
<div class="border-b border-base-300 bg-ct/5 p-6">
<div class="border-b border-white/10 bg-ct/10 p-6">
<h3 class="text-xl font-bold text-ct">Counter-Terrorists - Flash Stats</h3>
</div>
<DataTable data={teamBFlashStats} {columns} striped hoverable fixedLayout />
</Card>
<!-- Info Box -->
<Card padding="lg" variant="elevated">
<div class="text-sm text-base-content/60">
<p class="mb-2 font-semibold">Flash Stats Explained (For the Visually Challenged):</p>
<ul class="list-inside list-disc space-y-1">
<li><strong>Victims (Correct):</strong> Enemies you blinded - the RIGHT people to flash</li>
<li>
<strong>Avg Suffering:</strong> Average time enemies spent regretting their peek
<Card padding="lg" variant="elevated" class="border-neon-blue/20">
<div class="flex items-start gap-4">
<div
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-neon-blue/20"
style="box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);"
>
<Lightbulb class="h-5 w-5 text-neon-blue" />
</div>
<div class="text-sm text-white/70">
<p class="mb-3 font-semibold text-white">
Flash Stats Explained (For the Visually Challenged):
</p>
<ul class="space-y-2">
<li class="flex items-start gap-2">
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-green"></span>
<span
><strong class="text-neon-green">Victims (Correct):</strong> Enemies you blinded - the
RIGHT people to flash</span
>
</li>
<li>
<strong>Actually Useful:</strong> Enemies killed by teammates while your flash was doing its
job
<li class="flex items-start gap-2">
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-gold"></span>
<span
><strong class="text-neon-gold">Avg Suffering:</strong> Average time enemies spent regretting
their peek</span
>
</li>
<li>
<strong>Friendly Crimes:</strong> Number of times you betrayed your own team - shame counter
<li class="flex items-start gap-2">
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-blue"></span>
<span
><strong class="text-neon-blue">Actually Useful:</strong> Enemies killed by teammates while
your flash was doing its job</span
>
</li>
<li>
<strong>Self-Inflicted L:</strong> Times you stared at your own flashbang like a moth to a
flame
<li class="flex items-start gap-2">
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-red"></span>
<span
><strong class="text-neon-red">Friendly Crimes:</strong> Number of times you betrayed your
own team - shame counter</span
>
</li>
<li class="flex items-start gap-2">
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-purple"></span>
<span
><strong class="text-neon-purple">Self-Inflicted L:</strong> Times you stared at your own
flashbang like a moth to a flame</span
>
</li>
</ul>
<p class="mt-4 text-xs italic">
Remember: If your "Friendly Crimes" is higher than "Victims (Correct)", you might want to
reconsider your flash lineups.
<p
class="mt-4 rounded-lg border border-neon-gold/20 bg-neon-gold/5 px-3 py-2 text-xs italic text-neon-gold"
>
Pro tip: If your "Friendly Crimes" is higher than "Victims (Correct)", you might want to
reconsider your flash lineups. Or your life choices.
</p>
</div>
</div>
</Card>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Crosshair, Target, AlertCircle, TrendingUp } from 'lucide-svelte';
import { Crosshair, Target, AlertCircle, TrendingUp, Swords } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import DataTable from '$lib/components/data-display/DataTable.svelte';
@@ -64,29 +64,29 @@
render: (value: unknown, row: PlayerWeapon) => {
const strValue = value !== undefined ? String(value) : '';
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
return `<a href="/player/${row.player_id}" class="font-medium hover:underline ${teamClass}">${strValue}</a>`;
return `<a href="/player/${row.player_id}" class="font-medium hover:text-neon-blue transition-colors ${teamClass}">${strValue}</a>`;
}
},
{
key: 'top_weapon' as const,
label: 'Top Weapon',
label: 'Weapon of Choice',
sortable: true,
align: 'left' as const,
class: 'font-medium'
class: 'font-medium text-white'
},
{
key: 'total_kills' as const,
label: 'Total Kills',
sortable: true,
align: 'center' as const,
class: 'font-mono font-semibold'
class: 'font-mono font-semibold text-white'
},
{
key: 'total_damage' as const,
label: 'Total Damage',
sortable: true,
align: 'center' as const,
class: 'font-mono',
class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toLocaleString() : '0')
},
{
@@ -94,7 +94,7 @@
label: 'Total Hits',
sortable: true,
align: 'center' as const,
class: 'font-mono'
class: 'font-mono text-white/80'
}
];
@@ -111,7 +111,7 @@
existing.kills += ws.kills;
existing.damage += ws.damage;
existing.hits += ws.hits;
existing.headshot_pct = ws.headshot_pct || 0; // Use latest
existing.headshot_pct = ws.headshot_pct || 0;
} else {
weaponAggregates.set(ws.weapon_name, {
kills: ws.kills,
@@ -129,19 +129,21 @@
.sort((a, b) => b.kills - a.kills)
.slice(0, 10);
// Weapon usage chart data
// Weapon usage chart data with neon colors
const weaponUsageData = {
labels: topWeapons.map((w) => w.name),
datasets: [
{
label: 'Kills',
data: topWeapons.map((w) => w.kills),
backgroundColor: 'rgba(59, 130, 246, 0.8)'
backgroundColor: 'rgba(0, 212, 255, 0.7)',
borderColor: '#00d4ff',
borderWidth: 1
}
]
};
// Hit group distribution (aggregate across all weapons)
// Hit group distribution with neon colors
const hitGroupTotals = {
head: 0,
chest: 0,
@@ -177,11 +179,11 @@
hitGroupTotals.left_leg + hitGroupTotals.right_leg
],
backgroundColor: [
'rgba(239, 68, 68, 0.8)', // Red for head
'rgba(59, 130, 246, 0.8)', // Blue for chest
'rgba(249, 115, 22, 0.8)', // Orange for stomach
'rgba(34, 197, 94, 0.8)', // Green for arms
'rgba(168, 85, 247, 0.8)' // Purple for legs
'rgba(255, 51, 102, 0.8)', // neon-red for head
'rgba(0, 212, 255, 0.8)', // neon-blue for chest
'rgba(255, 215, 0, 0.8)', // neon-gold for stomach
'rgba(0, 255, 136, 0.8)', // neon-green for arms
'rgba(139, 92, 246, 0.8)' // neon-purple for legs
]
}
]
@@ -195,9 +197,14 @@
{#if !hasWeaponsData}
<Card padding="lg">
<div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
<h2 class="mb-2 text-2xl font-bold text-base-content">No Weapons Data Available</h2>
<p class="mb-4 text-base-content/60">Weapon statistics are not available for this match.</p>
<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">No Weapons Data Available</h2>
<p class="mb-4 text-white/60">
Weapon statistics are not available for this match. The armory remains sealed.
</p>
<Badge variant="warning" size="lg">Weapons data unavailable</Badge>
</div>
</Card>
@@ -206,53 +213,125 @@
<!-- Top Stats Summary -->
<div class="grid gap-6 md:grid-cols-3">
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Crosshair class="h-5 w-5 text-primary" />
<h3 class="font-semibold text-base-content">Total Kills</h3>
<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);"
>
<Crosshair class="h-5 w-5 text-neon-blue" />
</div>
<div class="font-mono text-3xl font-bold text-primary">
<div>
<div class="text-sm text-white/50">Total Kills</div>
<div
class="font-mono text-3xl font-bold text-neon-blue"
style="text-shadow: 0 0 15px rgba(0, 212, 255, 0.4);"
>
{topWeapons.reduce((sum, w) => sum + w.kills, 0)}
</div>
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
</div>
</div>
<div class="mt-2 text-xs text-white/40">Across all weapons</div>
</Card>
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Target class="h-5 w-5 text-success" />
<h3 class="font-semibold text-base-content">Total Damage</h3>
<div class="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);"
>
<Target class="h-5 w-5 text-neon-green" />
</div>
<div class="font-mono text-3xl font-bold text-success">
<div>
<div class="text-sm text-white/50">Total Damage</div>
<div
class="font-mono text-3xl font-bold text-neon-green"
style="text-shadow: 0 0 15px rgba(0, 255, 136, 0.4);"
>
{topWeapons.reduce((sum, w) => sum + w.damage, 0).toLocaleString()}
</div>
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
</div>
</div>
<div class="mt-2 text-xs text-white/40">Across all weapons</div>
</Card>
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<TrendingUp class="h-5 w-5 text-warning" />
<h3 class="font-semibold text-base-content">Total Hits</h3>
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-gold/20"
style="box-shadow: 0 0 15px rgba(255, 215, 0, 0.2);"
>
<TrendingUp class="h-5 w-5 text-neon-gold" />
</div>
<div class="font-mono text-3xl font-bold text-warning">
<div>
<div class="text-sm text-white/50">Total Hits</div>
<div
class="font-mono text-3xl font-bold text-neon-gold"
style="text-shadow: 0 0 15px rgba(255, 215, 0, 0.4);"
>
{topWeapons.reduce((sum, w) => sum + w.hits, 0).toLocaleString()}
</div>
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
</div>
</div>
<div class="mt-2 text-xs text-white/40">Across all weapons</div>
</Card>
</div>
<!-- Top Weapons Chart -->
<Card padding="lg">
<div class="mb-4">
<h2 class="text-2xl font-bold text-base-content">Most Used Weapons</h2>
<p class="text-sm text-base-content/60">Weapons ranked by total kills</p>
<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);"
>
<Swords class="h-5 w-5 text-neon-purple" />
</div>
<BarChart data={weaponUsageData} height={300} />
<div>
<h2 class="text-2xl font-bold text-white">The Arsenal Rankings</h2>
<p class="text-sm text-white/50">
Weapons ranked by total kills - The tools of destruction
</p>
</div>
</div>
<BarChart
data={weaponUsageData}
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>
<!-- Hit Group Distribution -->
<Card padding="lg">
<div class="mb-4">
<h2 class="text-2xl font-bold text-base-content">Hit Location Distribution</h2>
<p class="text-sm text-base-content/60">Where shots landed across all weapons</p>
<h2 class="text-2xl font-bold text-white">Hit Location Distribution</h2>
<p class="text-sm text-white/50">
Where shots landed across all weapons - Anatomy of aggression
</p>
</div>
<div class="grid gap-6 md:grid-cols-2">
<PieChart data={hitGroupData} height={300} />
@@ -260,40 +339,40 @@
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(239,68,68,0.8)]"></div>
<span>Head</span>
<div class="h-4 w-4 rounded bg-neon-red"></div>
<span class="text-white/80">Head</span>
</span>
<span class="font-mono font-semibold">{hitGroupTotals.head}</span>
<span class="font-mono font-semibold text-white">{hitGroupTotals.head}</span>
</div>
<div class="flex items-center justify-between">
<span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(59,130,246,0.8)]"></div>
<span>Chest</span>
<div class="h-4 w-4 rounded bg-neon-blue"></div>
<span class="text-white/80">Chest</span>
</span>
<span class="font-mono font-semibold">{hitGroupTotals.chest}</span>
<span class="font-mono font-semibold text-white">{hitGroupTotals.chest}</span>
</div>
<div class="flex items-center justify-between">
<span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(249,115,22,0.8)]"></div>
<span>Stomach</span>
<div class="h-4 w-4 rounded bg-neon-gold"></div>
<span class="text-white/80">Stomach</span>
</span>
<span class="font-mono font-semibold">{hitGroupTotals.stomach}</span>
<span class="font-mono font-semibold text-white">{hitGroupTotals.stomach}</span>
</div>
<div class="flex items-center justify-between">
<span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(34,197,94,0.8)]"></div>
<span>Arms</span>
<div class="h-4 w-4 rounded bg-neon-green"></div>
<span class="text-white/80">Arms</span>
</span>
<span class="font-mono font-semibold"
<span class="font-mono font-semibold text-white"
>{hitGroupTotals.left_arm + hitGroupTotals.right_arm}</span
>
</div>
<div class="flex items-center justify-between">
<span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(168,85,247,0.8)]"></div>
<span>Legs</span>
<div class="h-4 w-4 rounded bg-neon-purple"></div>
<span class="text-white/80">Legs</span>
</span>
<span class="font-mono font-semibold"
<span class="font-mono font-semibold text-white"
>{hitGroupTotals.left_leg + hitGroupTotals.right_leg}</span
>
</div>
@@ -305,8 +384,10 @@
<!-- Player Weapons Table -->
<Card padding="none">
<div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Player Weapon Performance</h2>
<p class="mt-1 text-sm text-base-content/60">Individual player weapon statistics</p>
<h2 class="text-2xl font-bold text-white">Player Weapon Performance</h2>
<p class="mt-1 text-sm text-white/50">
Individual player weapon statistics - Who brought what to the fight
</p>
</div>
<DataTable data={sortedPlayerWeapons} columns={weaponColumns} striped hoverable />