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:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
{@render children()}
|
||||
<div class="min-h-screen bg-void">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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'}"
|
||||
/>
|
||||
<h3 class="font-semibold text-base-content">
|
||||
? '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'}"
|
||||
/>
|
||||
</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
|
||||
</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-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
|
||||
|
||||
@@ -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
|
||||
</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-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>
|
||||
</div>
|
||||
<BarChart data={multiKillData} height={300} />
|
||||
<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>
|
||||
<h3 class="font-semibold text-white">Most Kills</h3>
|
||||
</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">
|
||||
<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>
|
||||
<h3 class="font-semibold text-white">Can't Touch This</h3>
|
||||
</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">
|
||||
<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>
|
||||
<h3 class="font-semibold text-white">The Molotov Mixologist</h3>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
<div class="text-sm text-white/50">Total Rounds</div>
|
||||
<div class="text-3xl font-bold text-white">{totalRounds}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-base-content">{totalRounds}</div>
|
||||
<div class="mt-1 text-xs text-base-content/60">
|
||||
<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">
|
||||
<TrendingUp class="h-5 w-5 text-terrorist" />
|
||||
<span class="text-sm font-medium text-base-content/70">Terrorists Buy Rounds</span>
|
||||
<Card padding="lg" class="border-l-4 border-l-terrorist">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-terrorist/20"
|
||||
style="box-shadow: 0 0 15px rgba(212, 167, 74, 0.2);"
|
||||
>
|
||||
<TrendingUp class="h-5 w-5 text-terrorist" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-white/50">T Full Buys</div>
|
||||
<div class="text-3xl font-bold text-white">{teamA_fullBuys}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="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 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">
|
||||
<TrendingUp class="h-5 w-5 text-ct" />
|
||||
<span class="text-sm font-medium text-base-content/70">CT Buy Rounds</span>
|
||||
<Card padding="lg" class="border-l-4 border-l-ct">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-ct/20"
|
||||
style="box-shadow: 0 0 15px rgba(94, 152, 217, 0.2);"
|
||||
>
|
||||
<TrendingUp class="h-5 w-5 text-ct" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-white/50">CT Full Buys</div>
|
||||
<div class="text-3xl font-bold text-white">{teamB_fullBuys}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="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 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">< $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">< $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">> $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">> $3,500 - "{buyTypeLabels['Full Buy']}"</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -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">
|
||||
{teamATotals.total_enemies_blinded + teamBTotals.total_enemies_blinded}
|
||||
<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-white/60">Enemies Successfully Blinded</div>
|
||||
</div>
|
||||
</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="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">
|
||||
{teamATotals.total_flash_assists + teamBTotals.total_flash_assists}
|
||||
<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-white/60">Flash Assists</div>
|
||||
</div>
|
||||
</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="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">
|
||||
{flashStats.reduce((sum, p) => sum + p.teammates_blinded, 0)}
|
||||
<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-white/60">Teammates Betrayed</div>
|
||||
</div>
|
||||
</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="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,10 +261,18 @@
|
||||
<!-- 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">
|
||||
Ranked by enemies blinded. Teammates blinded is tracked for... scientific purposes.
|
||||
</p>
|
||||
<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 />
|
||||
@@ -188,7 +280,7 @@
|
||||
|
||||
<!-- 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
|
||||
</li>
|
||||
<li>
|
||||
<strong>Actually Useful:</strong> Enemies killed by teammates while your flash was doing its
|
||||
job
|
||||
</li>
|
||||
<li>
|
||||
<strong>Friendly Crimes:</strong> Number of times you betrayed your own team - shame counter
|
||||
</li>
|
||||
<li>
|
||||
<strong>Self-Inflicted L:</strong> Times you stared at your own flashbang like a moth to a
|
||||
flame
|
||||
</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>
|
||||
<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 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 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 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 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>
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="font-mono text-3xl font-bold text-primary">
|
||||
{topWeapons.reduce((sum, w) => sum + w.kills, 0)}
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-base-content/60">Across all weapons</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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="font-mono text-3xl font-bold text-success">
|
||||
{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 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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="font-mono text-3xl font-bold text-warning">
|
||||
{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 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>
|
||||
<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} />
|
||||
<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 />
|
||||
|
||||
Reference in New Issue
Block a user