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">
|
<Card padding="lg">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Round Timeline</h2>
|
<h2 class="text-2xl font-bold text-white">Round Timeline</h2>
|
||||||
<p class="mt-2 text-sm text-base-content/60">
|
<p class="mt-2 text-sm text-white/60">
|
||||||
Click on a round to see detailed information. T = Terrorists, CT = Counter-Terrorists
|
Click on a round to see the battle details. T = Terrorists, CT = Counter-Terrorists
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -100,25 +100,20 @@
|
|||||||
>
|
>
|
||||||
<!-- Round number -->
|
<!-- Round number -->
|
||||||
<div
|
<div
|
||||||
class="mb-2 text-xs font-semibold transition-colors"
|
class="mb-2 text-xs font-semibold transition-colors {isSelected
|
||||||
class:text-primary={isSelected}
|
? 'text-neon-blue'
|
||||||
class:opacity-60={!isSelected}
|
: 'text-white/60'}"
|
||||||
>
|
>
|
||||||
{round.round}
|
{round.round}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Round indicator circle -->
|
<!-- Round indicator circle -->
|
||||||
<div
|
<div
|
||||||
class="relative flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all"
|
class="relative flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all {isWinner2
|
||||||
class:border-terrorist={isWinner2}
|
? 'border-terrorist bg-terrorist/20'
|
||||||
class:bg-terrorist={isWinner2}
|
: ''} {isWinner3 ? 'border-ct bg-ct/20' : ''} {isSelected
|
||||||
class:bg-opacity-20={isWinner2 || isWinner3}
|
? 'scale-110 shadow-[0_0_15px_rgba(0,212,255,0.4)] ring-2 ring-neon-blue'
|
||||||
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}
|
|
||||||
>
|
>
|
||||||
<!-- Win reason icon or T/CT badge -->
|
<!-- Win reason icon or T/CT badge -->
|
||||||
{#if Icon}
|
{#if Icon}
|
||||||
@@ -147,18 +142,18 @@
|
|||||||
<!-- Connecting line to next round -->
|
<!-- Connecting line to next round -->
|
||||||
{#if round.round < rounds.length}
|
{#if round.round < rounds.length}
|
||||||
<div
|
<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>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Hover tooltip -->
|
<!-- Hover tooltip -->
|
||||||
<div
|
<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}
|
Round {round.round}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-xs text-base-content/80">
|
<div class="mt-1 text-xs text-white/80">
|
||||||
Winner:
|
Winner:
|
||||||
<span
|
<span
|
||||||
class="font-bold"
|
class="font-bold"
|
||||||
@@ -168,10 +163,10 @@
|
|||||||
{isWinner2 ? 'Terrorists' : 'Counter-Terrorists'}
|
{isWinner2 ? 'Terrorists' : 'Counter-Terrorists'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-xs text-base-content/60">
|
<div class="mt-1 text-xs text-white/60">
|
||||||
{getWinReasonText(round.win_reason)}
|
{getWinReasonText(round.win_reason)}
|
||||||
</div>
|
</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}
|
Score: {scoreAtRound.teamA} - {scoreAtRound.teamB}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,13 +191,13 @@
|
|||||||
|
|
||||||
<!-- Selected Round Details -->
|
<!-- Selected Round Details -->
|
||||||
{#if selectedRoundData}
|
{#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">
|
<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
|
Round {selectedRoundData.round} Details
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<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)}
|
onclick={() => (selectedRound = null)}
|
||||||
aria-label="Close details"
|
aria-label="Close details"
|
||||||
>
|
>
|
||||||
@@ -212,7 +207,7 @@
|
|||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Winner</div>
|
<div class="text-sm text-white/50">Winner</div>
|
||||||
<div
|
<div
|
||||||
class="text-lg font-bold"
|
class="text-lg font-bold"
|
||||||
class:text-terrorist={selectedRoundData.winner === 2}
|
class:text-terrorist={selectedRoundData.winner === 2}
|
||||||
@@ -222,8 +217,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Win Reason</div>
|
<div class="text-sm text-white/50">Win Reason</div>
|
||||||
<div class="text-lg font-semibold text-base-content">
|
<div class="text-lg font-semibold text-white">
|
||||||
{getWinReasonText(selectedRoundData.win_reason)}
|
{getWinReasonText(selectedRoundData.win_reason)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,37 +227,46 @@
|
|||||||
<!-- Player stats for the round if available -->
|
<!-- Player stats for the round if available -->
|
||||||
{#if selectedRoundData.players && selectedRoundData.players.length > 0}
|
{#if selectedRoundData.players && selectedRoundData.players.length > 0}
|
||||||
<div class="mt-4">
|
<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">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-base-300">
|
<tr class="border-b border-white/10 text-left text-white/50">
|
||||||
<th>Player</th>
|
<th class="px-3 py-2">Player</th>
|
||||||
<th>Bank</th>
|
<th class="px-3 py-2">Bank</th>
|
||||||
<th>Equipment</th>
|
<th class="px-3 py-2">Equipment</th>
|
||||||
<th>Spent</th>
|
<th class="px-3 py-2">Spent</th>
|
||||||
{#if selectedRoundData.players.some((p) => p.kills_in_round !== undefined)}
|
{#if selectedRoundData.players.some((p) => p.kills_in_round !== undefined)}
|
||||||
<th>Kills</th>
|
<th class="px-3 py-2">Kills</th>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedRoundData.players.some((p) => p.damage_in_round !== undefined)}
|
{#if selectedRoundData.players.some((p) => p.damage_in_round !== undefined)}
|
||||||
<th>Damage</th>
|
<th class="px-3 py-2">Damage</th>
|
||||||
{/if}
|
{/if}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each selectedRoundData.players as player}
|
{#each selectedRoundData.players as player}
|
||||||
<tr class="border-base-300">
|
<tr class="border-b border-white/5 transition-colors hover:bg-white/5">
|
||||||
<td class="font-medium"
|
<td class="px-3 py-2 font-medium text-white"
|
||||||
>Player {player.player_id || player.match_player_id || '?'}</td
|
>Player {player.player_id || player.match_player_id || '?'}</td
|
||||||
>
|
>
|
||||||
<td class="font-mono text-success">${player.bank.toLocaleString()}</td>
|
<td class="px-3 py-2 font-mono text-neon-green"
|
||||||
<td class="font-mono">${player.equipment.toLocaleString()}</td>
|
>${player.bank.toLocaleString()}</td
|
||||||
<td class="font-mono text-error">${player.spent.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)}
|
{#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}
|
||||||
{#if selectedRoundData.players.some((p) => p.damage_in_round !== undefined)}
|
{#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}
|
{/if}
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
display: true,
|
display: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
labels: {
|
labels: {
|
||||||
color: 'rgb(156, 163, 175)',
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
font: {
|
font: {
|
||||||
family: 'Inter, system-ui, sans-serif',
|
family: 'Inter, system-ui, sans-serif',
|
||||||
size: 12
|
size: 12
|
||||||
@@ -64,21 +64,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
backgroundColor: 'rgba(18, 18, 26, 0.95)',
|
||||||
padding: 12,
|
padding: 12,
|
||||||
titleColor: '#fff',
|
titleColor: '#fff',
|
||||||
bodyColor: '#fff',
|
bodyColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
borderColor: 'rgba(0, 212, 255, 0.3)',
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
grid: {
|
grid: {
|
||||||
color: 'rgba(156, 163, 175, 0.1)'
|
color: 'rgba(255, 255, 255, 0.05)'
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: 'rgb(156, 163, 175)',
|
color: 'rgba(255, 255, 255, 0.5)',
|
||||||
font: {
|
font: {
|
||||||
size: 11
|
size: 11
|
||||||
}
|
}
|
||||||
@@ -86,10 +86,10 @@
|
|||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
grid: {
|
grid: {
|
||||||
color: 'rgba(156, 163, 175, 0.1)'
|
color: 'rgba(255, 255, 255, 0.05)'
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: 'rgb(156, 163, 175)',
|
color: 'rgba(255, 255, 255, 0.5)',
|
||||||
font: {
|
font: {
|
||||||
size: 11
|
size: 11
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
display: true,
|
display: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
labels: {
|
labels: {
|
||||||
color: 'rgb(156, 163, 175)',
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
font: {
|
font: {
|
||||||
family: 'Inter, system-ui, sans-serif',
|
family: 'Inter, system-ui, sans-serif',
|
||||||
size: 12
|
size: 12
|
||||||
@@ -73,21 +73,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
backgroundColor: 'rgba(18, 18, 26, 0.95)',
|
||||||
padding: 12,
|
padding: 12,
|
||||||
titleColor: '#fff',
|
titleColor: '#fff',
|
||||||
bodyColor: '#fff',
|
bodyColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
borderColor: 'rgba(0, 212, 255, 0.3)',
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
grid: {
|
grid: {
|
||||||
color: 'rgba(156, 163, 175, 0.1)'
|
color: 'rgba(255, 255, 255, 0.05)'
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: 'rgb(156, 163, 175)',
|
color: 'rgba(255, 255, 255, 0.5)',
|
||||||
font: {
|
font: {
|
||||||
size: 11
|
size: 11
|
||||||
}
|
}
|
||||||
@@ -95,10 +95,10 @@
|
|||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
grid: {
|
grid: {
|
||||||
color: 'rgba(156, 163, 175, 0.1)'
|
color: 'rgba(255, 255, 255, 0.05)'
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: 'rgb(156, 163, 175)',
|
color: 'rgba(255, 255, 255, 0.5)',
|
||||||
font: {
|
font: {
|
||||||
size: 11
|
size: 11
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
display: true,
|
display: true,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
labels: {
|
labels: {
|
||||||
color: 'rgb(156, 163, 175)',
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
font: {
|
font: {
|
||||||
family: 'Inter, system-ui, sans-serif',
|
family: 'Inter, system-ui, sans-serif',
|
||||||
size: 12
|
size: 12
|
||||||
@@ -63,11 +63,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
backgroundColor: 'rgba(18, 18, 26, 0.95)',
|
||||||
padding: 12,
|
padding: 12,
|
||||||
titleColor: '#fff',
|
titleColor: '#fff',
|
||||||
bodyColor: '#fff',
|
bodyColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
borderColor: 'rgba(0, 212, 255, 0.3)',
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@
|
|||||||
children
|
children
|
||||||
}: Props = $props();
|
}: 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 = {
|
const variantClasses = {
|
||||||
default: 'shadow-sm',
|
default: 'shadow-sm hover:shadow-[0_0_20px_rgba(0,212,255,0.05)]',
|
||||||
elevated: 'shadow-lg shadow-black/10',
|
elevated: 'shadow-lg shadow-black/20 hover:shadow-[0_0_30px_rgba(0,212,255,0.1)]',
|
||||||
interactive:
|
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 = {
|
const paddingClasses = {
|
||||||
|
|||||||
@@ -12,8 +12,7 @@
|
|||||||
tabs: Tab[];
|
tabs: Tab[];
|
||||||
activeTab?: string;
|
activeTab?: string;
|
||||||
onTabChange?: (value: string) => void;
|
onTabChange?: (value: string) => void;
|
||||||
variant?: 'boxed' | 'bordered' | 'lifted';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
||||||
class?: string;
|
class?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@
|
|||||||
tabs,
|
tabs,
|
||||||
activeTab = $bindable(),
|
activeTab = $bindable(),
|
||||||
onTabChange,
|
onTabChange,
|
||||||
variant = 'bordered',
|
|
||||||
size = 'md',
|
size = 'md',
|
||||||
class: className = ''
|
class: className = ''
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
@@ -43,21 +41,36 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const variantClass =
|
const sizeClasses = {
|
||||||
variant === 'boxed' ? 'tabs-boxed' : variant === 'lifted' ? 'tabs-lifted' : '';
|
sm: 'text-xs px-3 py-1.5',
|
||||||
const sizeClass =
|
md: 'text-sm px-4 py-2',
|
||||||
size === 'xs' ? 'tabs-xs' : size === 'sm' ? 'tabs-sm' : size === 'lg' ? 'tabs-lg' : '';
|
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>
|
</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}
|
{#each tabs as tab}
|
||||||
|
{@const active = isActive(tab)}
|
||||||
|
{@const classes = `${baseTabClasses} ${sizeClasses[size]} ${active ? activeClasses : inactiveClasses} ${tab.disabled ? disabledClasses : ''}`}
|
||||||
|
|
||||||
{#if tab.href}
|
{#if tab.href}
|
||||||
<a
|
<a
|
||||||
href={tab.href}
|
href={tab.href}
|
||||||
role="tab"
|
role="tab"
|
||||||
class="tab"
|
class={classes}
|
||||||
class:tab-active={isActive(tab)}
|
aria-selected={active}
|
||||||
class:tab-disabled={tab.disabled}
|
|
||||||
aria-disabled={tab.disabled}
|
aria-disabled={tab.disabled}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
@@ -65,9 +78,8 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
role="tab"
|
role="tab"
|
||||||
class="tab"
|
class={classes}
|
||||||
class:tab-active={isActive(tab)}
|
aria-selected={active}
|
||||||
class:tab-disabled={tab.disabled}
|
|
||||||
disabled={tab.disabled}
|
disabled={tab.disabled}
|
||||||
onclick={() => handleTabClick(tab)}
|
onclick={() => handleTabClick(tab)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
const { match } = data;
|
const { match } = data;
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
// Navigate back to matches page
|
|
||||||
goto('/matches');
|
goto('/matches');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,21 +46,31 @@
|
|||||||
alert('Share code not available for this match');
|
alert('Share code not available for this match');
|
||||||
return;
|
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}`;
|
const downloadUrl = `steam://rungame/730/76561202255233023/+csgo_download_match%20${match.share_code}`;
|
||||||
window.location.href = downloadUrl;
|
window.location.href = downloadUrl;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Match Header with Background -->
|
<!-- 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 -->
|
<!-- Background Image -->
|
||||||
<div class="absolute inset-0">
|
<div class="absolute inset-0">
|
||||||
<img src={mapBg} alt={mapName} class="h-full w-full object-cover" onerror={handleImageError} />
|
<img src={mapBg} alt={mapName} class="h-full w-full object-cover" onerror={handleImageError} />
|
||||||
<!-- Multi-layer gradient overlay for depth and framing -->
|
<!-- Multi-layer gradient overlay for depth -->
|
||||||
<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-b from-void/80 via-transparent to-void"></div>
|
||||||
<div class="absolute inset-0 bg-gradient-to-r from-black/70 via-black/40 to-black/70"></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>
|
||||||
|
|
||||||
<div class="container relative mx-auto px-4 py-8">
|
<div class="container relative mx-auto px-4 py-8">
|
||||||
@@ -69,7 +78,7 @@
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<button
|
<button
|
||||||
onclick={handleBack}
|
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" />
|
<ArrowLeft class="h-4 w-4" />
|
||||||
<span>Back to Matches</span>
|
<span>Back to Matches</span>
|
||||||
@@ -79,14 +88,17 @@
|
|||||||
<!-- Map Name -->
|
<!-- Map Name -->
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="mb-6 flex items-center justify-between">
|
||||||
<div>
|
<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}
|
{mapName}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{#if match.demo_parsed && match.share_code}
|
{#if match.demo_parsed && match.share_code}
|
||||||
<button
|
<button
|
||||||
onclick={handleDownloadDemo}
|
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"
|
title="Download this match demo to your Steam client"
|
||||||
>
|
>
|
||||||
<Download class="h-4 w-4" />
|
<Download class="h-4 w-4" />
|
||||||
@@ -95,59 +107,67 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hero Info Panel with translucent background -->
|
<!-- Hero Info Panel -->
|
||||||
<div
|
<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 -->
|
<!-- Score -->
|
||||||
<div class="mb-4 flex items-center justify-center gap-8">
|
<div class="mb-4 flex items-center justify-center gap-8">
|
||||||
<div class="text-center">
|
<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
|
Terrorists
|
||||||
</div>
|
</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}
|
{match.score_team_a}
|
||||||
</div>
|
</div>
|
||||||
</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="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
|
Counter-Terrorists
|
||||||
</div>
|
</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}
|
{match.score_team_b}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Match Meta -->
|
<!-- 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">
|
<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>
|
<span>{formattedDate}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-white/30">•</span>
|
<span class="text-white/20">•</span>
|
||||||
<div class="flex items-center gap-1.5">
|
<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>
|
<span>{duration}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-white/30">•</span>
|
<span class="text-white/20">•</span>
|
||||||
<span>MR12 ({match.max_rounds} rounds)</span>
|
<span>MR12 ({match.max_rounds} rounds)</span>
|
||||||
{#if match.demo_parsed}
|
{#if match.demo_parsed}
|
||||||
<span class="text-white/30">•</span>
|
<span class="text-white/20">•</span>
|
||||||
<Badge variant="success" size="sm">Demo Parsed</Badge>
|
<Badge variant="success" size="sm">Demo Parsed</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="mt-6 rounded-lg border border-white/10 bg-black/35 p-4 backdrop-blur-lg">
|
<div class="mt-6 flex justify-center">
|
||||||
<Tabs {tabs} variant="bordered" size="md" />
|
<Tabs {tabs} size="md" class="border border-white/10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Content -->
|
<!-- Tab Content -->
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="min-h-screen bg-void">
|
||||||
{@render children()}
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Trophy } from 'lucide-svelte';
|
import { Trophy, Zap } from 'lucide-svelte';
|
||||||
import Card from '$lib/components/ui/Card.svelte';
|
import Card from '$lib/components/ui/Card.svelte';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import PremierRatingBadge from '$lib/components/ui/PremierRatingBadge.svelte';
|
import PremierRatingBadge from '$lib/components/ui/PremierRatingBadge.svelte';
|
||||||
@@ -51,12 +51,16 @@
|
|||||||
|
|
||||||
const teamAStats = calcTeamStats(sortedTeamA);
|
const teamAStats = calcTeamStats(sortedTeamA);
|
||||||
const teamBStats = calcTeamStats(sortedTeamB);
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<!-- Team Statistics Overview -->
|
<!-- Team Statistics Overview -->
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<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="mb-4 flex items-center justify-between">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h2 class="text-2xl font-bold text-terrorist">Terrorists</h2>
|
<h2 class="text-2xl font-bold text-terrorist">Terrorists</h2>
|
||||||
@@ -64,29 +68,34 @@
|
|||||||
<PremierRatingBadge rating={teamAStats.avgRating} {match} size="sm" showIcon={true} />
|
<PremierRatingBadge rating={teamAStats.avgRating} {match} size="sm" showIcon={true} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Team K/D</div>
|
<div class="text-sm text-white/50">Team K/D</div>
|
||||||
<div class="text-xl font-bold">{teamAStats.kd}</div>
|
<div class="text-xl font-bold text-white">{teamAStats.kd}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Avg ADR</div>
|
<div class="text-sm text-white/50">Avg ADR</div>
|
||||||
<div class="text-xl font-bold">{teamAStats.adr}</div>
|
<div class="text-xl font-bold text-white">{teamAStats.adr}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Total Kills</div>
|
<div class="text-sm text-white/50">Total Kills</div>
|
||||||
<div class="text-xl font-bold">{teamAStats.kills}</div>
|
<div class="text-xl font-bold text-white">{teamAStats.kills}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Avg KAST</div>
|
<div class="text-sm text-white/50">Avg KAST</div>
|
||||||
<div class="text-xl font-bold">{teamAStats.kast}%</div>
|
<div class="text-xl font-bold text-white">{teamAStats.kast}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</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="mb-4 flex items-center justify-between">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h2 class="text-2xl font-bold text-ct">Counter-Terrorists</h2>
|
<h2 class="text-2xl font-bold text-ct">Counter-Terrorists</h2>
|
||||||
@@ -94,24 +103,29 @@
|
|||||||
<PremierRatingBadge rating={teamBStats.avgRating} {match} size="sm" showIcon={true} />
|
<PremierRatingBadge rating={teamBStats.avgRating} {match} size="sm" showIcon={true} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Team K/D</div>
|
<div class="text-sm text-white/50">Team K/D</div>
|
||||||
<div class="text-xl font-bold">{teamBStats.kd}</div>
|
<div class="text-xl font-bold text-white">{teamBStats.kd}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Avg ADR</div>
|
<div class="text-sm text-white/50">Avg ADR</div>
|
||||||
<div class="text-xl font-bold">{teamBStats.adr}</div>
|
<div class="text-xl font-bold text-white">{teamBStats.adr}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Total Kills</div>
|
<div class="text-sm text-white/50">Total Kills</div>
|
||||||
<div class="text-xl font-bold">{teamBStats.kills}</div>
|
<div class="text-xl font-bold text-white">{teamBStats.kills}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Avg KAST</div>
|
<div class="text-sm text-white/50">Avg KAST</div>
|
||||||
<div class="text-xl font-bold">{teamBStats.kast}%</div>
|
<div class="text-xl font-bold text-white">{teamBStats.kast}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -120,49 +134,61 @@
|
|||||||
<!-- Scoreboard -->
|
<!-- Scoreboard -->
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
<div class="p-6">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Team A -->
|
<!-- Team A -->
|
||||||
<div class="border-t border-base-300 bg-terrorist/5">
|
<div class="border-t border-white/10 bg-terrorist/5">
|
||||||
<div class="px-6 py-3">
|
<div class="flex items-center justify-between px-6 py-3">
|
||||||
<h3 class="text-lg font-semibold text-terrorist">Terrorists</h3>
|
<h3 class="text-lg font-semibold text-terrorist">Terrorists</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table" style="table-layout: fixed;">
|
<table class="w-full" style="table-layout: fixed;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-base-300">
|
<tr class="border-b border-white/10 bg-void/50 text-left text-sm text-white/50">
|
||||||
<th style="width: 200px;">Player</th>
|
<th class="px-6 py-3 font-medium" style="width: 200px;">Player</th>
|
||||||
<th style="width: 80px;">K</th>
|
<th class="px-4 py-3 font-medium" style="width: 80px;">K</th>
|
||||||
<th style="width: 80px;">D</th>
|
<th class="px-4 py-3 font-medium" style="width: 80px;">D</th>
|
||||||
<th style="width: 80px;">A</th>
|
<th class="px-4 py-3 font-medium" style="width: 80px;">A</th>
|
||||||
<th style="width: 100px;">ADR</th>
|
<th class="px-4 py-3 font-medium" style="width: 100px;">ADR</th>
|
||||||
<th style="width: 100px;">HS%</th>
|
<th class="px-4 py-3 font-medium" style="width: 100px;">HS%</th>
|
||||||
<th style="width: 100px;">KAST%</th>
|
<th class="px-4 py-3 font-medium" style="width: 100px;">KAST%</th>
|
||||||
<th style="width: 180px;">Rating</th>
|
<th class="px-4 py-3 font-medium" style="width: 180px;">Rating</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each sortedTeamA as player, index}
|
{#each sortedTeamA as player, index}
|
||||||
<tr class="border-base-300">
|
<tr class="border-b border-white/5 transition-colors hover:bg-neon-blue/5">
|
||||||
<td>
|
<td class="px-6 py-3">
|
||||||
<a
|
<a
|
||||||
href={`/player/${player.id}`}
|
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}
|
{player.name}
|
||||||
</a>
|
</a>
|
||||||
{#if index === 0}
|
{#if player.id === mvpPlayerId}
|
||||||
<Trophy class="ml-2 inline h-4 w-4 text-warning" />
|
<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}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono font-semibold">{player.kills}</td>
|
<td class="px-4 py-3 font-mono font-semibold text-white">{player.kills}</td>
|
||||||
<td class="font-mono">{player.deaths}</td>
|
<td class="px-4 py-3 font-mono text-white/80">{player.deaths}</td>
|
||||||
<td class="font-mono">{player.assists}</td>
|
<td class="px-4 py-3 font-mono text-white/80">{player.assists}</td>
|
||||||
<td class="font-mono">{(player.adr || 0).toFixed(1)}</td>
|
<td class="px-4 py-3 font-mono text-white/80">{(player.adr || 0).toFixed(1)}</td>
|
||||||
<td class="font-mono">{(player.hs_percent || 0).toFixed(1)}%</td>
|
<td class="px-4 py-3 font-mono text-white/80"
|
||||||
<td class="font-mono">{player.kast?.toFixed(1) || '0.0'}%</td>
|
>{(player.hs_percent || 0).toFixed(1)}%</td
|
||||||
<td class="h-12">
|
>
|
||||||
|
<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">
|
<div class="flex h-full items-center">
|
||||||
<PremierRatingBadge
|
<PremierRatingBadge
|
||||||
rating={player.rank_new}
|
rating={player.rank_new}
|
||||||
@@ -181,45 +207,57 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Team B -->
|
<!-- Team B -->
|
||||||
<div class="border-t border-base-300 bg-ct/5">
|
<div class="border-t border-white/10 bg-ct/5">
|
||||||
<div class="px-6 py-3">
|
<div class="flex items-center justify-between px-6 py-3">
|
||||||
<h3 class="text-lg font-semibold text-ct">Counter-Terrorists</h3>
|
<h3 class="text-lg font-semibold text-ct">Counter-Terrorists</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table" style="table-layout: fixed;">
|
<table class="w-full" style="table-layout: fixed;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-base-300">
|
<tr class="border-b border-white/10 bg-void/50 text-left text-sm text-white/50">
|
||||||
<th style="width: 200px;">Player</th>
|
<th class="px-6 py-3 font-medium" style="width: 200px;">Player</th>
|
||||||
<th style="width: 80px;">K</th>
|
<th class="px-4 py-3 font-medium" style="width: 80px;">K</th>
|
||||||
<th style="width: 80px;">D</th>
|
<th class="px-4 py-3 font-medium" style="width: 80px;">D</th>
|
||||||
<th style="width: 80px;">A</th>
|
<th class="px-4 py-3 font-medium" style="width: 80px;">A</th>
|
||||||
<th style="width: 100px;">ADR</th>
|
<th class="px-4 py-3 font-medium" style="width: 100px;">ADR</th>
|
||||||
<th style="width: 100px;">HS%</th>
|
<th class="px-4 py-3 font-medium" style="width: 100px;">HS%</th>
|
||||||
<th style="width: 100px;">KAST%</th>
|
<th class="px-4 py-3 font-medium" style="width: 100px;">KAST%</th>
|
||||||
<th style="width: 180px;">Rating</th>
|
<th class="px-4 py-3 font-medium" style="width: 180px;">Rating</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each sortedTeamB as player, index}
|
{#each sortedTeamB as player, index}
|
||||||
<tr class="border-base-300">
|
<tr class="border-b border-white/5 transition-colors hover:bg-neon-blue/5">
|
||||||
<td>
|
<td class="px-6 py-3">
|
||||||
<a
|
<a
|
||||||
href={`/player/${player.id}`}
|
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}
|
{player.name}
|
||||||
</a>
|
</a>
|
||||||
{#if index === 0}
|
{#if player.id === mvpPlayerId}
|
||||||
<Trophy class="ml-2 inline h-4 w-4 text-warning" />
|
<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}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono font-semibold">{player.kills}</td>
|
<td class="px-4 py-3 font-mono font-semibold text-white">{player.kills}</td>
|
||||||
<td class="font-mono">{player.deaths}</td>
|
<td class="px-4 py-3 font-mono text-white/80">{player.deaths}</td>
|
||||||
<td class="font-mono">{player.assists}</td>
|
<td class="px-4 py-3 font-mono text-white/80">{player.assists}</td>
|
||||||
<td class="font-mono">{(player.adr || 0).toFixed(1)}</td>
|
<td class="px-4 py-3 font-mono text-white/80">{(player.adr || 0).toFixed(1)}</td>
|
||||||
<td class="font-mono">{(player.hs_percent || 0).toFixed(1)}%</td>
|
<td class="px-4 py-3 font-mono text-white/80"
|
||||||
<td class="font-mono">{player.kast?.toFixed(1) || '0.0'}%</td>
|
>{(player.hs_percent || 0).toFixed(1)}%</td
|
||||||
<td class="h-12">
|
>
|
||||||
|
<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">
|
<div class="flex h-full items-center">
|
||||||
<PremierRatingBadge
|
<PremierRatingBadge
|
||||||
rating={player.rank_new}
|
rating={player.rank_new}
|
||||||
@@ -244,10 +282,13 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h3 class="mb-2 text-xl font-semibold text-base-content">Round Timeline</h3>
|
<h3 class="mb-2 text-xl font-semibold text-white">Round Timeline</h3>
|
||||||
<p class="text-base-content/60">
|
<p class="text-white/60">
|
||||||
Round-by-round timeline data is not available for this match. This requires the demo to be
|
{#if !match.demo_parsed}
|
||||||
fully 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>
|
</p>
|
||||||
{#if !match.demo_parsed}
|
{#if !match.demo_parsed}
|
||||||
<Badge variant="warning" size="md" class="mt-4">Demo Not Yet Parsed</Badge>
|
<Badge variant="warning" size="md" class="mt-4">Demo Not Yet Parsed</Badge>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<script lang="ts">
|
<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 Card from '$lib/components/ui/Card.svelte';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
@@ -15,7 +22,6 @@
|
|||||||
|
|
||||||
// Check if text likely needs translation (contains non-ASCII or Cyrillic characters)
|
// Check if text likely needs translation (contains non-ASCII or Cyrillic characters)
|
||||||
const mightNeedTranslation = (text: string): boolean => {
|
const mightNeedTranslation = (text: string): boolean => {
|
||||||
// Check for Cyrillic, Chinese, Japanese, Korean, Arabic, etc.
|
|
||||||
const nonEnglishPattern =
|
const nonEnglishPattern =
|
||||||
/[\u0400-\u04FF\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\u0600-\u06FF]/;
|
/[\u0400-\u04FF\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\u0600-\u06FF]/;
|
||||||
return nonEnglishPattern.test(text);
|
return nonEnglishPattern.test(text);
|
||||||
@@ -24,12 +30,11 @@
|
|||||||
// Open Google Translate for a message
|
// Open Google Translate for a message
|
||||||
const translateMessage = (text: string) => {
|
const translateMessage = (text: string) => {
|
||||||
const encodedText = encodeURIComponent(text);
|
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`;
|
const translateUrl = `https://translate.google.com/?sl=auto&tl=en&text=${encodedText}&op=translate`;
|
||||||
window.open(translateUrl, '_blank', 'width=800,height=600,noopener,noreferrer');
|
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(
|
const messagePlayers = $derived(
|
||||||
chatData
|
chatData
|
||||||
? Array.from(new Set(chatData.messages.map((m) => m.player_id)))
|
? Array.from(new Set(chatData.messages.map((m) => m.player_id)))
|
||||||
@@ -49,18 +54,12 @@
|
|||||||
const filteredMessages = $derived(
|
const filteredMessages = $derived(
|
||||||
chatData
|
chatData
|
||||||
? chatData.messages.filter((msg) => {
|
? chatData.messages.filter((msg) => {
|
||||||
// Chat type filter
|
|
||||||
if (!showTeamChat && !msg.all_chat) return false;
|
if (!showTeamChat && !msg.all_chat) return false;
|
||||||
if (!showAllChat && msg.all_chat) return false;
|
if (!showAllChat && msg.all_chat) return false;
|
||||||
|
|
||||||
// Player filter
|
|
||||||
if (selectedPlayer !== null && msg.player_id !== selectedPlayer) return false;
|
if (selectedPlayer !== null && msg.player_id !== selectedPlayer) return false;
|
||||||
|
|
||||||
// Search filter
|
|
||||||
if (searchQuery && !msg.message.toLowerCase().includes(searchQuery.toLowerCase())) {
|
if (searchQuery && !msg.message.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
@@ -100,40 +99,72 @@
|
|||||||
{#if !chatData}
|
{#if !chatData}
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
|
<AlertCircle
|
||||||
<h2 class="mb-2 text-2xl font-bold text-base-content">Match Not Parsed</h2>
|
class="mx-auto mb-4 h-16 w-16 text-neon-gold"
|
||||||
<p class="mb-4 text-base-content/60">
|
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.
|
This match hasn't been parsed yet, so chat data is not available.
|
||||||
</p>
|
</p>
|
||||||
<Badge variant="warning" size="lg">Demo parsing required</Badge>
|
<Badge variant="warning" size="lg">Demo parsing required</Badge>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</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}
|
{:else}
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
<div class="grid gap-6 md:grid-cols-3">
|
<div class="grid gap-6 md:grid-cols-3">
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<MessageSquare class="h-5 w-5 text-primary" />
|
<div
|
||||||
<span class="text-sm font-medium text-base-content/70">Total Messages</span>
|
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>
|
||||||
<div class="text-3xl font-bold text-base-content">{totalMessages}</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<MessageSquare class="h-5 w-5 text-warning" />
|
<div
|
||||||
<span class="text-sm font-medium text-base-content/70">Team Chat</span>
|
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>
|
||||||
<div class="text-3xl font-bold text-base-content">{teamChatCount}</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<MessageSquare class="h-5 w-5 text-success" />
|
<div
|
||||||
<span class="text-sm font-medium text-base-content/70">All Chat</span>
|
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>
|
||||||
<div class="text-3xl font-bold text-base-content">{allChatCount}</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -141,25 +172,36 @@
|
|||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Filter class="h-5 w-5 text-base-content" />
|
<Filter class="h-5 w-5 text-neon-blue" />
|
||||||
<h3 class="font-semibold">Filters</h3>
|
<h3 class="font-semibold text-white">Filters</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
<!-- Chat Type -->
|
<!-- Chat Type -->
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-4">
|
||||||
<label class="label cursor-pointer gap-2">
|
<label class="flex cursor-pointer items-center gap-2">
|
||||||
<input type="checkbox" bind:checked={showTeamChat} class="checkbox checkbox-sm" />
|
<input
|
||||||
<span class="label-text">Team Chat</span>
|
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>
|
||||||
<label class="label cursor-pointer gap-2">
|
<label class="flex cursor-pointer items-center gap-2">
|
||||||
<input type="checkbox" bind:checked={showAllChat} class="checkbox checkbox-sm" />
|
<input
|
||||||
<span class="label-text">All Chat</span>
|
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player Filter -->
|
<!-- 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>
|
<option value={null}>All Players</option>
|
||||||
{#each messagePlayers as player}
|
{#each messagePlayers as player}
|
||||||
<option value={player.id}>{player.name}</option>
|
<option value={player.id}>{player.name}</option>
|
||||||
@@ -168,12 +210,12 @@
|
|||||||
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<div class="relative min-w-[200px] flex-1">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
placeholder="Search messages..."
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,7 +225,7 @@
|
|||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
{#if filteredMessages.length === 0}
|
{#if filteredMessages.length === 0}
|
||||||
<Card padding="lg">
|
<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" />
|
<MessageSquare class="mx-auto mb-2 h-12 w-12" />
|
||||||
<p>No messages match your filters.</p>
|
<p>No messages match your filters.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,12 +234,14 @@
|
|||||||
{#each rounds as round}
|
{#each rounds as round}
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
<!-- Round Header -->
|
<!-- 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">
|
<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}`}
|
{round === 0 ? 'Warmup / Pre-Match' : `Round ${round}`}
|
||||||
</h3>
|
</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[
|
{messagesByRound[round] ? messagesByRound[round].length : 0} message{(messagesByRound[
|
||||||
round
|
round
|
||||||
]
|
]
|
||||||
@@ -205,25 +249,27 @@
|
|||||||
: 0) !== 1
|
: 0) !== 1
|
||||||
? 's'
|
? 's'
|
||||||
: ''}
|
: ''}
|
||||||
</Badge>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
<div class="divide-y divide-base-300">
|
<div class="divide-y divide-white/5">
|
||||||
{#each messagesByRound[round] as message}
|
{#each messagesByRound[round] as message}
|
||||||
{@const player = match.players?.find((p) => p.id === String(message.player_id))}
|
{@const player = match.players?.find((p) => p.id === String(message.player_id))}
|
||||||
{@const playerName =
|
{@const playerName =
|
||||||
message.player_name || player?.name || `Player ${message.player_id}`}
|
message.player_name || player?.name || `Player ${message.player_id}`}
|
||||||
{@const teamId = player?.team_id || 0}
|
{@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">
|
<div class="flex items-start gap-3">
|
||||||
<!-- Player Avatar/Icon -->
|
<!-- Player Avatar/Icon -->
|
||||||
<div
|
<div
|
||||||
class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold text-white"
|
class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold text-white {teamId ===
|
||||||
class:bg-terrorist={teamId === 2}
|
2
|
||||||
class:bg-ct={teamId === 3}
|
? 'bg-terrorist'
|
||||||
class:bg-base-300={teamId === 0}
|
: teamId === 3
|
||||||
|
? 'bg-ct'
|
||||||
|
: 'bg-white/20'}"
|
||||||
>
|
>
|
||||||
{playerName.charAt(0).toUpperCase()}
|
{playerName.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
@@ -233,24 +279,33 @@
|
|||||||
<div class="flex items-baseline gap-2">
|
<div class="flex items-baseline gap-2">
|
||||||
<a
|
<a
|
||||||
href={`/player/${message.player_id || 0}`}
|
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-terrorist={teamId === 2}
|
||||||
class:text-ct={teamId === 3}
|
class:text-ct={teamId === 3}
|
||||||
|
class:text-white={teamId === 0}
|
||||||
>
|
>
|
||||||
{playerName}
|
{playerName}
|
||||||
</a>
|
</a>
|
||||||
{#if message.all_chat}
|
{#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}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 flex items-start gap-2">
|
<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)}
|
{#if mightNeedTranslation(message.message)}
|
||||||
<button
|
<button
|
||||||
onclick={() => translateMessage(message.message)}
|
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"
|
title="Translate message"
|
||||||
aria-label="Translate to English"
|
aria-label="Translate to English"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 Card from '$lib/components/ui/Card.svelte';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
||||||
@@ -23,8 +23,6 @@
|
|||||||
const damage = player.dmg_enemy || 0;
|
const damage = player.dmg_enemy || 0;
|
||||||
const avgDamagePerRound = match.max_rounds > 0 ? damage / match.max_rounds : 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 {
|
return {
|
||||||
...player,
|
...player,
|
||||||
damage,
|
damage,
|
||||||
@@ -69,6 +67,11 @@
|
|||||||
// Top damage dealers (top 3)
|
// Top damage dealers (top 3)
|
||||||
const topDamageDealers = sortedByDamage.slice(0, 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
|
// Damage table columns
|
||||||
const damageColumns = [
|
const damageColumns = [
|
||||||
{
|
{
|
||||||
@@ -77,7 +80,7 @@
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
render: (value: unknown, row: (typeof playersWithDamageStats)[0]) => {
|
render: (value: unknown, row: (typeof playersWithDamageStats)[0]) => {
|
||||||
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
|
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',
|
label: 'Damage Dealt',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'right' as const,
|
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')
|
format: (value: unknown) => (typeof value === 'number' ? value.toLocaleString() : '0')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'avgDamagePerRound' as const,
|
key: 'avgDamagePerRound' as const,
|
||||||
label: 'Avg Damage/Round',
|
label: 'ADR',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'right' as const,
|
align: 'right' as const,
|
||||||
class: 'font-mono',
|
class: 'font-mono text-white/80',
|
||||||
format: (value: unknown) => (typeof value === 'number' ? value.toFixed(1) : '0.0')
|
format: (value: unknown) => (typeof value === 'number' ? value.toFixed(1) : '0.0')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -101,14 +104,14 @@
|
|||||||
label: 'Headshots',
|
label: 'Headshots',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono'
|
class: 'font-mono text-white/80'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'kills' as const,
|
key: 'kills' as const,
|
||||||
label: 'Kills',
|
label: 'Kills',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono'
|
class: 'font-mono text-white/80'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'dmg_team' as const,
|
key: 'dmg_team' as const,
|
||||||
@@ -118,14 +121,13 @@
|
|||||||
class: 'font-mono',
|
class: 'font-mono',
|
||||||
render: (value: unknown) => {
|
render: (value: unknown) => {
|
||||||
const dmg = typeof value === 'number' ? value : 0;
|
const dmg = typeof value === 'number' ? value : 0;
|
||||||
if (!dmg || dmg === 0) return '<span class="text-base-content/40">-</span>';
|
if (!dmg || dmg === 0) return '<span class="text-white/30">-</span>';
|
||||||
return `<span class="text-error">${dmg.toLocaleString()}</span>`;
|
return `<span class="text-neon-red">${dmg.toLocaleString()}</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Hit group distribution data (placeholder - would need weapon stats data)
|
// Utility damage data with neon colors
|
||||||
// For now, showing utility damage breakdown instead
|
|
||||||
const utilityDamageData = hasPlayerData
|
const utilityDamageData = hasPlayerData
|
||||||
? {
|
? {
|
||||||
labels: ['HE Grenades', 'Fire (Molotov/Inc)'],
|
labels: ['HE Grenades', 'Fire (Molotov/Inc)'],
|
||||||
@@ -137,10 +139,10 @@
|
|||||||
playersWithDamageStats.reduce((sum, p) => sum + (p.ud_flames || 0), 0)
|
playersWithDamageStats.reduce((sum, p) => sum + (p.ud_flames || 0), 0)
|
||||||
],
|
],
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
'rgba(34, 197, 94, 0.8)', // Green for HE
|
'rgba(0, 255, 136, 0.8)', // neon-green for HE
|
||||||
'rgba(239, 68, 68, 0.8)' // Red for Fire
|
'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
|
borderWidth: 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -158,10 +160,13 @@
|
|||||||
{#if !hasPlayerData}
|
{#if !hasPlayerData}
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
|
<AlertCircle
|
||||||
<h2 class="mb-2 text-2xl font-bold text-base-content">No Player Data Available</h2>
|
class="mx-auto mb-4 h-16 w-16 text-neon-gold"
|
||||||
<p class="mb-4 text-base-content/60">
|
style="filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));"
|
||||||
Detailed damage statistics are not available for this match.
|
/>
|
||||||
|
<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>
|
</p>
|
||||||
<Badge variant="warning" size="lg">Player data unavailable</Badge>
|
<Badge variant="warning" size="lg">Player data unavailable</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,18 +176,18 @@
|
|||||||
<!-- Team Damage Summary Cards -->
|
<!-- Team Damage Summary Cards -->
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
<!-- Terrorists Damage Stats -->
|
<!-- 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>
|
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Damage</h3>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Total Damage</div>
|
<div class="text-sm text-white/50">Total Damage</div>
|
||||||
<div class="text-3xl font-bold text-base-content">
|
<div class="text-3xl font-bold text-white">
|
||||||
{teamAStats.totalDamage.toLocaleString()}
|
{teamAStats.totalDamage.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Avg per Player</div>
|
<div class="text-sm text-white/50">Avg per Player</div>
|
||||||
<div class="text-3xl font-bold text-base-content">
|
<div class="text-3xl font-bold text-white">
|
||||||
{Math.round(teamAStats.avgDamagePerPlayer).toLocaleString()}
|
{Math.round(teamAStats.avgDamagePerPlayer).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,18 +195,18 @@
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Counter-Terrorists Damage Stats -->
|
<!-- 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>
|
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists Damage</h3>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Total Damage</div>
|
<div class="text-sm text-white/50">Total Damage</div>
|
||||||
<div class="text-3xl font-bold text-base-content">
|
<div class="text-3xl font-bold text-white">
|
||||||
{teamBStats.totalDamage.toLocaleString()}
|
{teamBStats.totalDamage.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-base-content/60">Avg per Player</div>
|
<div class="text-sm text-white/50">Avg per Player</div>
|
||||||
<div class="text-3xl font-bold text-base-content">
|
<div class="text-3xl font-bold text-white">
|
||||||
{Math.round(teamBStats.avgDamagePerPlayer).toLocaleString()}
|
{Math.round(teamBStats.avgDamagePerPlayer).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,47 +220,97 @@
|
|||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Target
|
<div
|
||||||
class="h-5 w-5 {index === 0
|
class="flex h-8 w-8 items-center justify-center rounded-lg {index === 0
|
||||||
? 'text-warning'
|
? 'bg-neon-gold/20'
|
||||||
: index === 1
|
: index === 1
|
||||||
? 'text-base-content/70'
|
? 'bg-white/10'
|
||||||
: 'text-base-content/50'}"
|
: 'bg-white/5'}"
|
||||||
/>
|
style={index === 0 ? 'box-shadow: 0 0 10px rgba(255, 215, 0, 0.2);' : ''}
|
||||||
<h3 class="font-semibold text-base-content">
|
>
|
||||||
|
<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
|
#{index + 1} Damage Dealer
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<a
|
||||||
class="text-2xl font-bold {player.team_id === firstTeamId
|
href={`/player/${player.id}`}
|
||||||
|
class="text-2xl font-bold transition-colors hover:text-neon-blue {player.team_id ===
|
||||||
|
firstTeamId
|
||||||
? 'text-terrorist'
|
? 'text-terrorist'
|
||||||
: 'text-ct'}"
|
: 'text-ct'}"
|
||||||
>
|
>
|
||||||
{player.name}
|
{player.name}
|
||||||
</div>
|
</a>
|
||||||
<div class="mt-1 font-mono text-3xl font-bold text-primary">
|
<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()}
|
{player.damage.toLocaleString()}
|
||||||
</div>
|
</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
|
{player.avgDamagePerRound.toFixed(1)} ADR
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</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 -->
|
<!-- Utility Damage Distribution -->
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-4">
|
<div class="mb-4 flex items-center gap-3">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Utility Damage Distribution</h2>
|
<div
|
||||||
<p class="text-sm text-base-content/60">
|
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-red/20"
|
||||||
Breakdown of damage dealt by grenades and fire across all players
|
style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.2);"
|
||||||
</p>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
{#if utilityDamageData.datasets.length > 0 && utilityDamageData.datasets[0]?.data.some((v) => v > 0)}
|
{#if utilityDamageData.datasets.length > 0 && utilityDamageData.datasets[0]?.data.some((v) => v > 0)}
|
||||||
<PieChart data={utilityDamageData} height={300} />
|
<PieChart data={utilityDamageData} height={300} />
|
||||||
{:else}
|
{: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" />
|
<Crosshair class="mx-auto mb-2 h-12 w-12" />
|
||||||
<p>No utility damage recorded for this match</p>
|
<p>No utility damage recorded for this match</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,20 +320,27 @@
|
|||||||
<!-- Player Damage Table -->
|
<!-- Player Damage Table -->
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Player Damage Statistics</h2>
|
<h2 class="text-2xl font-bold text-white">Player Damage Statistics</h2>
|
||||||
<p class="mt-1 text-sm text-base-content/60">Detailed damage breakdown for all players</p>
|
<p class="mt-1 text-sm text-white/50">
|
||||||
|
Detailed damage breakdown for all players - The pain ledger
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable data={sortedByDamage} columns={damageColumns} striped hoverable />
|
<DataTable data={sortedByDamage} columns={damageColumns} striped hoverable />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Additional Info Note -->
|
<!-- Additional Info Note -->
|
||||||
<Card padding="lg">
|
<Card padding="lg" class="border-neon-blue/20">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-4">
|
||||||
<AlertCircle class="h-5 w-5 flex-shrink-0 text-info" />
|
<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">
|
<div class="text-sm">
|
||||||
<h3 class="mb-1 font-semibold text-base-content">About Damage Statistics</h3>
|
<h3 class="mb-1 font-semibold text-white">About Damage Statistics</h3>
|
||||||
<p class="text-base-content/70">
|
<p class="text-white/60">
|
||||||
Damage statistics show total damage dealt to enemies throughout the match. Average
|
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
|
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
|
played. Hit group breakdown (head, chest, legs, etc.) is available in weapon-specific
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 Card from '$lib/components/ui/Card.svelte';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (_value: unknown, row: PlayerWithStats) => {
|
render: (_value: unknown, row: PlayerWithStats) => {
|
||||||
const avatarUrl = row.avatar || '';
|
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
|
const colorDot = colorHex
|
||||||
? `<span class="inline-block h-3 w-3 rounded-full mr-2" style="background-color: ${colorHex}"></span>`
|
? `<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',
|
label: 'Score',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono font-semibold'
|
class: 'font-mono font-semibold text-white'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'kills' as keyof (typeof playersWithStats)[0],
|
key: 'kills' as keyof (typeof playersWithStats)[0],
|
||||||
label: 'K',
|
label: 'K',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono font-semibold'
|
class: 'font-mono font-semibold text-white'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'deaths' as keyof (typeof playersWithStats)[0],
|
key: 'deaths' as keyof (typeof playersWithStats)[0],
|
||||||
label: 'D',
|
label: 'D',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono'
|
class: 'font-mono text-white/80'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'assists' as keyof (typeof playersWithStats)[0],
|
key: 'assists' as keyof (typeof playersWithStats)[0],
|
||||||
label: 'A',
|
label: 'A',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono'
|
class: 'font-mono text-white/80'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'kd' as keyof PlayerWithStats,
|
key: 'kd' as keyof PlayerWithStats,
|
||||||
label: 'K/D',
|
label: 'K/D',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
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')
|
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(2) : '0.00')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
label: 'ADR',
|
label: 'ADR',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
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')
|
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
label: 'HS%',
|
label: 'HS%',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
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')
|
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
label: 'KAST%',
|
label: 'KAST%',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono',
|
class: 'font-mono text-white/80',
|
||||||
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '-')
|
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '-')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
label: 'MVP',
|
label: 'MVP',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono'
|
class: 'font-mono text-white/80'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'mk_5' as keyof (typeof playersWithStats)[0],
|
key: 'mk_5' as keyof (typeof playersWithStats)[0],
|
||||||
@@ -157,8 +157,9 @@
|
|||||||
_row: (typeof playersWithStats)[0]
|
_row: (typeof playersWithStats)[0]
|
||||||
) => {
|
) => {
|
||||||
const numValue = value !== undefined ? (value as number) : 0;
|
const numValue = value !== undefined ? (value as number) : 0;
|
||||||
if (numValue > 0) return `<span class="badge badge-warning badge-sm">${numValue}</span>`;
|
if (numValue > 0)
|
||||||
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-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 = [];
|
const badges = [];
|
||||||
if (row.vac) {
|
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) {
|
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) {
|
if (badges.length > 0) {
|
||||||
return `<div class="flex gap-1 justify-center">${badges.join('')}</div>`;
|
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 = {
|
const multiKillData = {
|
||||||
labels: sortedPlayers.map((p) => p.name),
|
labels: sortedPlayers.map((p) => p.name),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: '2K',
|
label: '2K',
|
||||||
data: sortedPlayers.map((p) => p.mk_2 || 0),
|
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',
|
label: '3K',
|
||||||
data: sortedPlayers.map((p) => p.mk_3 || 0),
|
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',
|
label: '4K',
|
||||||
data: sortedPlayers.map((p) => p.mk_4 || 0),
|
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)',
|
label: '5K (Ace)',
|
||||||
data: sortedPlayers.map((p) => p.mk_5 || 0),
|
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}
|
{#if !hasPlayerData}
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
|
<AlertCircle
|
||||||
<h2 class="mb-2 text-2xl font-bold text-base-content">No Player Data Available</h2>
|
class="mx-auto mb-4 h-16 w-16 text-neon-gold"
|
||||||
<p class="mb-4 text-base-content/60">
|
style="filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));"
|
||||||
Detailed player statistics are not available for this match.
|
/>
|
||||||
|
<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>
|
</p>
|
||||||
<Badge variant="warning" size="lg">Player data unavailable</Badge>
|
<Badge variant="warning" size="lg">Player data unavailable</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,47 +294,55 @@
|
|||||||
<!-- Team Performance Summary -->
|
<!-- Team Performance Summary -->
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
<!-- Terrorists Stats -->
|
<!-- 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>
|
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Performance</h3>
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base-content/60">Total Damage</div>
|
<div class="text-white/50">Total Damage</div>
|
||||||
<div class="text-2xl font-bold">{teamAStats.totalDamage.toLocaleString()}</div>
|
<div class="text-2xl font-bold text-white">
|
||||||
|
{teamAStats.totalDamage.toLocaleString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base-content/60">Utility Damage</div>
|
<div class="text-white/50">Utility Damage</div>
|
||||||
<div class="text-2xl font-bold">{teamAStats.totalUtilityDamage.toLocaleString()}</div>
|
<div class="text-2xl font-bold text-white">
|
||||||
|
{teamAStats.totalUtilityDamage.toLocaleString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base-content/60">Flash Assists</div>
|
<div class="text-white/50">Flash Assists</div>
|
||||||
<div class="text-2xl font-bold">{teamAStats.totalFlashAssists}</div>
|
<div class="text-2xl font-bold text-white">{teamAStats.totalFlashAssists}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base-content/60">Avg KAST</div>
|
<div class="text-white/50">Avg KAST</div>
|
||||||
<div class="text-2xl font-bold">{teamAStats.avgKAST}%</div>
|
<div class="text-2xl font-bold text-white">{teamAStats.avgKAST}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Counter-Terrorists Stats -->
|
<!-- 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>
|
<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 class="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base-content/60">Total Damage</div>
|
<div class="text-white/50">Total Damage</div>
|
||||||
<div class="text-2xl font-bold">{teamBStats.totalDamage.toLocaleString()}</div>
|
<div class="text-2xl font-bold text-white">
|
||||||
|
{teamBStats.totalDamage.toLocaleString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base-content/60">Utility Damage</div>
|
<div class="text-white/50">Utility Damage</div>
|
||||||
<div class="text-2xl font-bold">{teamBStats.totalUtilityDamage.toLocaleString()}</div>
|
<div class="text-2xl font-bold text-white">
|
||||||
|
{teamBStats.totalUtilityDamage.toLocaleString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base-content/60">Flash Assists</div>
|
<div class="text-white/50">Flash Assists</div>
|
||||||
<div class="text-2xl font-bold">{teamBStats.totalFlashAssists}</div>
|
<div class="text-2xl font-bold text-white">{teamBStats.totalFlashAssists}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base-content/60">Avg KAST</div>
|
<div class="text-white/50">Avg KAST</div>
|
||||||
<div class="text-2xl font-bold">{teamBStats.avgKAST}%</div>
|
<div class="text-2xl font-bold text-white">{teamBStats.avgKAST}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -325,21 +350,59 @@
|
|||||||
|
|
||||||
<!-- Multi-Kills Chart -->
|
<!-- Multi-Kills Chart -->
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-4">
|
<div class="mb-4 flex items-center gap-3">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Multi-Kill Distribution</h2>
|
<div
|
||||||
<p class="text-sm text-base-content/60">
|
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-red/20"
|
||||||
Double kills (2K), triple kills (3K), quad kills (4K), and aces (5K) per player
|
style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.2);"
|
||||||
</p>
|
>
|
||||||
|
<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>
|
</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>
|
</Card>
|
||||||
|
|
||||||
<!-- Detailed Player Statistics Table -->
|
<!-- Detailed Player Statistics Table -->
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Detailed Player Statistics</h2>
|
<h2 class="text-2xl font-bold text-white">Detailed Player Statistics</h2>
|
||||||
<p class="mt-1 text-sm text-base-content/60">
|
<p class="mt-1 text-sm text-white/50">
|
||||||
Complete performance breakdown for all players
|
Complete performance breakdown for all players - The full criminal record
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -352,14 +415,27 @@
|
|||||||
<!-- Most Kills -->
|
<!-- Most Kills -->
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-3 flex items-center gap-2">
|
<div class="mb-3 flex items-center gap-2">
|
||||||
<Trophy class="h-5 w-5 text-warning" />
|
<div
|
||||||
<h3 class="font-semibold text-base-content">Most Kills</h3>
|
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>
|
||||||
<div class="text-2xl font-bold text-base-content">{sortedPlayers[0].name}</div>
|
<a
|
||||||
<div class="mt-1 font-mono text-3xl font-bold text-primary">
|
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}
|
{sortedPlayers[0].kills}
|
||||||
</div>
|
</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
|
{sortedPlayers[0].deaths} deaths, {sortedPlayers[0].kd.toFixed(2)} K/D
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -369,12 +445,27 @@
|
|||||||
{#if bestKD}
|
{#if bestKD}
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-3 flex items-center gap-2">
|
<div class="mb-3 flex items-center gap-2">
|
||||||
<Target class="h-5 w-5 text-success" />
|
<div
|
||||||
<h3 class="font-semibold text-base-content">Best K/D Ratio</h3>
|
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>
|
||||||
<div class="text-2xl font-bold text-base-content">{bestKD.name}</div>
|
<a
|
||||||
<div class="mt-1 font-mono text-3xl font-bold text-success">{bestKD.kd.toFixed(2)}</div>
|
href={`/player/${bestKD.id}`}
|
||||||
<div class="mt-2 text-xs text-base-content/60">
|
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
|
{bestKD.kills}K / {bestKD.deaths}D
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -387,14 +478,27 @@
|
|||||||
{#if bestUtility}
|
{#if bestUtility}
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-3 flex items-center gap-2">
|
<div class="mb-3 flex items-center gap-2">
|
||||||
<Flame class="h-5 w-5 text-error" />
|
<div
|
||||||
<h3 class="font-semibold text-base-content">Most Utility Damage</h3>
|
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>
|
||||||
<div class="text-2xl font-bold text-base-content">{bestUtility.name}</div>
|
<a
|
||||||
<div class="mt-1 font-mono text-3xl font-bold text-error">
|
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()}
|
{((bestUtility.ud_he || 0) + (bestUtility.ud_flames || 0)).toLocaleString()}
|
||||||
</div>
|
</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}
|
HE: {bestUtility.ud_he || 0} | Fire: {bestUtility.ud_flames || 0}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 Card from '$lib/components/ui/Card.svelte';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import LineChart from '$lib/components/charts/LineChart.svelte';
|
import LineChart from '$lib/components/charts/LineChart.svelte';
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
winner: number;
|
winner: number;
|
||||||
teamA_buyType: string;
|
teamA_buyType: string;
|
||||||
teamB_buyType: string;
|
teamB_buyType: string;
|
||||||
economyAdvantage: number; // Cumulative economy differential (teamA - teamB)
|
economyAdvantage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
@@ -28,8 +28,6 @@
|
|||||||
const ctTeamId = 3;
|
const ctTeamId = 3;
|
||||||
|
|
||||||
// Calculate halftime round based on max_rounds
|
// 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;
|
const halfPoint = match.max_rounds === 30 ? 15 : 12;
|
||||||
|
|
||||||
// Only process if rounds data exists
|
// Only process if rounds data exists
|
||||||
@@ -93,19 +91,13 @@
|
|||||||
return 'Full Buy';
|
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 t_totalEconomy = t_bank + t_spent;
|
||||||
const ct_totalEconomy = ct_bank + ct_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;
|
let economyAdvantage;
|
||||||
if (roundData.round <= halfPoint) {
|
if (roundData.round <= halfPoint) {
|
||||||
// First half: T - CT
|
|
||||||
economyAdvantage = t_totalEconomy - ct_totalEconomy;
|
economyAdvantage = t_totalEconomy - ct_totalEconomy;
|
||||||
} else {
|
} else {
|
||||||
// Second half: CT - T (teams swapped sides)
|
|
||||||
economyAdvantage = ct_totalEconomy - t_totalEconomy;
|
economyAdvantage = ct_totalEconomy - t_totalEconomy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,23 +116,23 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare equipment value chart data
|
// Prepare equipment value chart data with neon colors
|
||||||
equipmentChartData = {
|
equipmentChartData = {
|
||||||
labels: teamEconomy.map((r) => `R${r.round}`),
|
labels: teamEconomy.map((r) => `R${r.round}`),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Terrorists Equipment',
|
label: 'Terrorists Equipment',
|
||||||
data: teamEconomy.map((r) => r.teamA_equipment),
|
data: teamEconomy.map((r) => r.teamA_equipment),
|
||||||
borderColor: 'rgb(249, 115, 22)',
|
borderColor: '#d4a74a', // terrorist color
|
||||||
backgroundColor: 'rgba(249, 115, 22, 0.1)',
|
backgroundColor: 'rgba(212, 167, 74, 0.1)',
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4
|
tension: 0.4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Counter-Terrorists Equipment',
|
label: 'Counter-Terrorists Equipment',
|
||||||
data: teamEconomy.map((r) => r.teamB_equipment),
|
data: teamEconomy.map((r) => r.teamB_equipment),
|
||||||
borderColor: 'rgb(59, 130, 246)',
|
borderColor: '#5e98d9', // ct color
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
backgroundColor: 'rgba(94, 152, 217, 0.1)',
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4
|
tension: 0.4
|
||||||
}
|
}
|
||||||
@@ -148,7 +140,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Prepare economy advantage chart data
|
// Prepare economy advantage chart data
|
||||||
// Positive = above 0, Negative = below 0
|
|
||||||
halfRoundIndex = Math.floor(teamEconomy.length / 2);
|
halfRoundIndex = Math.floor(teamEconomy.length / 2);
|
||||||
economyAdvantageChartData = {
|
economyAdvantageChartData = {
|
||||||
labels: teamEconomy.map((r) => `${r.round}`),
|
labels: teamEconomy.map((r) => `${r.round}`),
|
||||||
@@ -156,8 +147,8 @@
|
|||||||
{
|
{
|
||||||
label: 'Advantage',
|
label: 'Advantage',
|
||||||
data: teamEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)),
|
data: teamEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)),
|
||||||
borderColor: 'rgb(59, 130, 246)',
|
borderColor: '#5e98d9',
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.6)',
|
backgroundColor: 'rgba(94, 152, 217, 0.6)',
|
||||||
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
|
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
|
||||||
fill: 'origin',
|
fill: 'origin',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
@@ -167,8 +158,8 @@
|
|||||||
{
|
{
|
||||||
label: 'Disadvantage',
|
label: 'Disadvantage',
|
||||||
data: teamEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)),
|
data: teamEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)),
|
||||||
borderColor: 'rgb(249, 115, 22)',
|
borderColor: '#d4a74a',
|
||||||
backgroundColor: 'rgba(249, 115, 22, 0.6)',
|
backgroundColor: 'rgba(212, 167, 74, 0.6)',
|
||||||
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
|
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
|
||||||
fill: 'origin',
|
fill: 'origin',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
@@ -186,6 +177,14 @@
|
|||||||
teamB_ecos = teamEconomy.filter((r) => r.teamB_buyType === 'Eco').length;
|
teamB_ecos = teamEconomy.filter((r) => r.teamB_buyType === 'Eco').length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buy type labels with puns
|
||||||
|
const buyTypeLabels: Record<string, string> = {
|
||||||
|
Eco: 'The Poverty Round',
|
||||||
|
'Semi-Eco': 'Broke but Hopeful',
|
||||||
|
Force: 'YOLO Buy',
|
||||||
|
'Full Buy': 'Loaded'
|
||||||
|
};
|
||||||
|
|
||||||
// Table columns
|
// Table columns
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
{
|
{
|
||||||
@@ -200,15 +199,15 @@
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
render: (value: string | number | boolean, _row: TeamEconomy) => {
|
render: (value: string | number | boolean, _row: TeamEconomy) => {
|
||||||
const strValue = value as string;
|
const strValue = value as string;
|
||||||
const variant =
|
const colorClass =
|
||||||
strValue === 'Full Buy'
|
strValue === 'Full Buy'
|
||||||
? 'success'
|
? 'bg-neon-green/20 text-neon-green border-neon-green/30'
|
||||||
: strValue === 'Eco'
|
: strValue === 'Eco'
|
||||||
? 'error'
|
? 'bg-neon-red/20 text-neon-red border-neon-red/30'
|
||||||
: strValue === 'Force'
|
: strValue === 'Force'
|
||||||
? 'warning'
|
? 'bg-neon-gold/20 text-neon-gold border-neon-gold/30'
|
||||||
: 'default';
|
: 'bg-white/10 text-white/60 border-white/20';
|
||||||
return `<span class="badge badge-${variant} badge-sm">${strValue}</span>`;
|
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,
|
sortable: true,
|
||||||
render: (value: string | number | boolean, _row: TeamEconomy) => {
|
render: (value: string | number | boolean, _row: TeamEconomy) => {
|
||||||
const strValue = value as string;
|
const strValue = value as string;
|
||||||
const variant =
|
const colorClass =
|
||||||
strValue === 'Full Buy'
|
strValue === 'Full Buy'
|
||||||
? 'success'
|
? 'bg-neon-green/20 text-neon-green border-neon-green/30'
|
||||||
: strValue === 'Eco'
|
: strValue === 'Eco'
|
||||||
? 'error'
|
? 'bg-neon-red/20 text-neon-red border-neon-red/30'
|
||||||
: strValue === 'Force'
|
: strValue === 'Force'
|
||||||
? 'warning'
|
? 'bg-neon-gold/20 text-neon-gold border-neon-gold/30'
|
||||||
: 'default';
|
: 'bg-white/10 text-white/60 border-white/20';
|
||||||
return `<span class="badge badge-${variant} badge-sm">${strValue}</span>`;
|
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) => {
|
render: (value: string | number | boolean, _row: TeamEconomy) => {
|
||||||
const numValue = value as number;
|
const numValue = value as number;
|
||||||
if (numValue === 2)
|
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)
|
if (numValue === 3)
|
||||||
return '<span class="badge badge-sm" style="background-color: rgb(59, 130, 246); color: white;">CT</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-base-content/40">-</span>';
|
return '<span class="text-white/30">-</span>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -263,10 +262,14 @@
|
|||||||
{#if !roundsData}
|
{#if !roundsData}
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
|
<AlertCircle
|
||||||
<h2 class="mb-2 text-2xl font-bold text-base-content">Match Not Parsed</h2>
|
class="mx-auto mb-4 h-16 w-16 text-neon-gold"
|
||||||
<p class="mb-4 text-base-content/60">
|
style="filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));"
|
||||||
This match hasn't been parsed yet, so detailed economy data is not available.
|
/>
|
||||||
|
<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>
|
</p>
|
||||||
<Badge variant="warning" size="lg">Demo parsing required</Badge>
|
<Badge variant="warning" size="lg">Demo parsing required</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,9 +278,19 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Economy Advantage Chart -->
|
<!-- Economy Advantage Chart -->
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-4">
|
<div class="mb-4 flex items-center gap-3">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Economy</h2>
|
<div
|
||||||
<p class="text-sm text-base-content/60">Net-worth differential (bank + spent)</p>
|
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>
|
</div>
|
||||||
{#if economyAdvantageChartData}
|
{#if economyAdvantageChartData}
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -291,19 +304,37 @@
|
|||||||
grid: {
|
grid: {
|
||||||
color: (context) => {
|
color: (context) => {
|
||||||
if (context.tick.value === 0) {
|
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) => {
|
lineWidth: (context) => {
|
||||||
return context.tick.value === 0 ? 2 : 1;
|
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: {
|
interaction: {
|
||||||
mode: 'index',
|
mode: 'index',
|
||||||
intersect: false
|
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"
|
class="pointer-events-none absolute top-0 flex h-full items-center"
|
||||||
style="left: {(halfRoundIndex / (teamEconomy.length || 1)) * 100}%"
|
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
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -327,54 +358,111 @@
|
|||||||
<!-- Summary Cards -->
|
<!-- Summary Cards -->
|
||||||
<div class="grid gap-6 md:grid-cols-3">
|
<div class="grid gap-6 md:grid-cols-3">
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<ShoppingCart class="h-5 w-5 text-primary" />
|
<div
|
||||||
<span class="text-sm font-medium text-base-content/70">Total Rounds</span>
|
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>
|
||||||
<div class="text-3xl font-bold text-base-content">{totalRounds}</div>
|
<div class="mt-2 text-xs text-white/40">
|
||||||
<div class="mt-1 text-xs text-base-content/60">
|
|
||||||
{match.score_team_a} - {match.score_team_b}
|
{match.score_team_a} - {match.score_team_b}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card padding="lg">
|
<Card padding="lg" class="border-l-4 border-l-terrorist">
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<TrendingUp class="h-5 w-5 text-terrorist" />
|
<div
|
||||||
<span class="text-sm font-medium text-base-content/70">Terrorists Buy Rounds</span>
|
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>
|
||||||
<div class="text-3xl font-bold text-base-content">{teamA_fullBuys}</div>
|
<div class="mt-2 text-xs text-neon-red">{teamA_ecos} poverty rounds</div>
|
||||||
<div class="mt-1 text-xs text-base-content/60">{teamA_ecos} eco rounds</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card padding="lg">
|
<Card padding="lg" class="border-l-4 border-l-ct">
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<TrendingUp class="h-5 w-5 text-ct" />
|
<div
|
||||||
<span class="text-sm font-medium text-base-content/70">CT Buy Rounds</span>
|
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>
|
||||||
<div class="text-3xl font-bold text-base-content">{teamB_fullBuys}</div>
|
<div class="mt-2 text-xs text-neon-red">{teamB_ecos} poverty rounds</div>
|
||||||
<div class="mt-1 text-xs text-base-content/60">{teamB_ecos} eco rounds</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Equipment Value Chart -->
|
<!-- Equipment Value Chart -->
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-4">
|
<div class="mb-4 flex items-center gap-3">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Equipment Value Over Time</h2>
|
<div
|
||||||
<p class="text-sm text-base-content/60">
|
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-purple/20"
|
||||||
Total equipment value for each team across all rounds
|
style="box-shadow: 0 0 15px rgba(139, 92, 246, 0.2);"
|
||||||
</p>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
{#if equipmentChartData}
|
{#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}
|
{/if}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Round-by-Round Table -->
|
<!-- Round-by-Round Table -->
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Round-by-Round Economy</h2>
|
<h2 class="text-2xl font-bold text-white">Round-by-Round Economy</h2>
|
||||||
<p class="mt-1 text-sm text-base-content/60">
|
<p class="mt-1 text-sm text-white/50">
|
||||||
Detailed breakdown of buy types and equipment values
|
Detailed breakdown of buy types and equipment values - Where did all the money go?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -382,24 +470,42 @@
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Buy Type Legend -->
|
<!-- Buy Type Legend -->
|
||||||
<Card padding="lg">
|
<Card padding="lg" class="border-neon-blue/20">
|
||||||
<h3 class="mb-3 text-lg font-semibold text-base-content">Buy Type Classification</h3>
|
<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 flex-wrap gap-4 text-sm">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="error" size="sm">Eco</Badge>
|
<span
|
||||||
<span class="text-base-content/60">< $1,500 avg equipment</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>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="default" size="sm">Semi-Eco</Badge>
|
<span
|
||||||
<span class="text-base-content/60">$1,500 - $2,500 avg equipment</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>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="warning" size="sm">Force</Badge>
|
<span
|
||||||
<span class="text-base-content/60">$2,500 - $3,500 avg equipment</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>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="success" size="sm">Full Buy</Badge>
|
<span
|
||||||
<span class="text-base-content/60">> $3,500 avg equipment</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>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 Card from '$lib/components/ui/Card.svelte';
|
||||||
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
const flashStats = (match.players || [])
|
const flashStats = (match.players || [])
|
||||||
.map((player) => ({
|
.map((player) => ({
|
||||||
name: player.name,
|
name: player.name,
|
||||||
|
playerId: player.id,
|
||||||
team_id: player.team_id,
|
team_id: player.team_id,
|
||||||
enemies_blinded: player.flash_total_enemy || 0,
|
enemies_blinded: player.flash_total_enemy || 0,
|
||||||
teammates_blinded: player.flash_total_team || 0,
|
teammates_blinded: player.flash_total_team || 0,
|
||||||
@@ -49,9 +50,15 @@
|
|||||||
const teamATotals = calcTeamTotals(teamAFlashStats);
|
const teamATotals = calcTeamTotals(teamAFlashStats);
|
||||||
const teamBTotals = calcTeamTotals(teamBFlashStats);
|
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
|
// Table columns with fixed widths for consistency across multiple tables
|
||||||
interface FlashStat {
|
interface FlashStat {
|
||||||
name: string;
|
name: string;
|
||||||
|
playerId: string;
|
||||||
team_id: number;
|
team_id: number;
|
||||||
enemies_blinded: number;
|
enemies_blinded: number;
|
||||||
teammates_blinded: number;
|
teammates_blinded: number;
|
||||||
@@ -108,67 +115,144 @@
|
|||||||
<!-- Summary Stats -->
|
<!-- Summary Stats -->
|
||||||
<div class="grid gap-6 md:grid-cols-3">
|
<div class="grid gap-6 md:grid-cols-3">
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<Eye class="mb-2 h-8 w-8 text-warning" />
|
<div class="flex items-center gap-3">
|
||||||
<div class="text-3xl font-bold text-base-content">
|
<div
|
||||||
{teamATotals.total_enemies_blinded + teamBTotals.total_enemies_blinded}
|
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>
|
||||||
<div class="text-sm text-base-content/60">Enemies Successfully Blinded</div>
|
<div class="mt-3 text-xs text-neon-green">The correct way to use flashes</div>
|
||||||
<div class="mt-1 text-xs text-success">The correct way to use flashes</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<Zap class="mb-2 h-8 w-8 text-success" />
|
<div class="flex items-center gap-3">
|
||||||
<div class="text-3xl font-bold text-base-content">
|
<div
|
||||||
{teamATotals.total_flash_assists + teamBTotals.total_flash_assists}
|
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>
|
||||||
<div class="text-sm text-base-content/60">Flash Assists</div>
|
<div class="mt-3 text-xs text-neon-blue">Teamwork makes the dream work</div>
|
||||||
<div class="mt-1 text-xs text-success">Teamwork makes the dream work</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<Users class="mb-2 h-8 w-8 text-error" />
|
<div class="flex items-center gap-3">
|
||||||
<div class="text-3xl font-bold text-base-content">
|
<div
|
||||||
{flashStats.reduce((sum, p) => sum + p.teammates_blinded, 0)}
|
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>
|
||||||
<div class="text-sm text-base-content/60">Teammates Betrayed</div>
|
<div class="mt-3 text-xs text-neon-red">These players owe apologies</div>
|
||||||
<div class="mt-1 text-xs text-error">These players owe apologies</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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 -->
|
<!-- Team Comparison -->
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<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>
|
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-sm text-base-content/60">Enemies Blinded</span>
|
<span class="text-sm text-white/50">Enemies Blinded</span>
|
||||||
<span class="font-mono font-bold">{teamATotals.total_enemies_blinded}</span>
|
<span class="font-mono font-bold text-white">{teamATotals.total_enemies_blinded}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-sm text-base-content/60">Flash Assists</span>
|
<span class="text-sm text-white/50">Flash Assists</span>
|
||||||
<span class="font-mono font-bold">{teamATotals.total_flash_assists}</span>
|
<span class="font-mono font-bold text-white">{teamATotals.total_flash_assists}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-sm text-base-content/60">Avg per Player</span>
|
<span class="text-sm text-white/50">Avg per Player</span>
|
||||||
<span class="font-mono font-bold">{teamATotals.avg_per_player}</span>
|
<span class="font-mono font-bold text-white">{teamATotals.avg_per_player}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</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>
|
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-sm text-base-content/60">Enemies Blinded</span>
|
<span class="text-sm text-white/50">Enemies Blinded</span>
|
||||||
<span class="font-mono font-bold">{teamBTotals.total_enemies_blinded}</span>
|
<span class="font-mono font-bold text-white">{teamBTotals.total_enemies_blinded}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-sm text-base-content/60">Flash Assists</span>
|
<span class="text-sm text-white/50">Flash Assists</span>
|
||||||
<span class="font-mono font-bold">{teamBTotals.total_flash_assists}</span>
|
<span class="font-mono font-bold text-white">{teamBTotals.total_flash_assists}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-sm text-base-content/60">Avg per Player</span>
|
<span class="text-sm text-white/50">Avg per Player</span>
|
||||||
<span class="font-mono font-bold">{teamBTotals.avg_per_player}</span>
|
<span class="font-mono font-bold text-white">{teamBTotals.avg_per_player}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -177,10 +261,18 @@
|
|||||||
<!-- Flash Effectiveness Leaderboard -->
|
<!-- Flash Effectiveness Leaderboard -->
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Flash Hall of Fame (and Shame)</h2>
|
<div class="flex items-center gap-3">
|
||||||
<p class="mt-1 text-sm text-base-content/60">
|
<AlertTriangle
|
||||||
Ranked by enemies blinded. Teammates blinded is tracked for... scientific purposes.
|
class="h-6 w-6 text-neon-gold"
|
||||||
</p>
|
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>
|
</div>
|
||||||
|
|
||||||
<DataTable data={flashStats} {columns} striped hoverable fixedLayout />
|
<DataTable data={flashStats} {columns} striped hoverable fixedLayout />
|
||||||
@@ -188,7 +280,7 @@
|
|||||||
|
|
||||||
<!-- Team A Details -->
|
<!-- Team A Details -->
|
||||||
<Card padding="none">
|
<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>
|
<h3 class="text-xl font-bold text-terrorist">Terrorists - Flash Stats</h3>
|
||||||
</div>
|
</div>
|
||||||
<DataTable data={teamAFlashStats} {columns} striped hoverable fixedLayout />
|
<DataTable data={teamAFlashStats} {columns} striped hoverable fixedLayout />
|
||||||
@@ -196,37 +288,69 @@
|
|||||||
|
|
||||||
<!-- Team B Details -->
|
<!-- Team B Details -->
|
||||||
<Card padding="none">
|
<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>
|
<h3 class="text-xl font-bold text-ct">Counter-Terrorists - Flash Stats</h3>
|
||||||
</div>
|
</div>
|
||||||
<DataTable data={teamBFlashStats} {columns} striped hoverable fixedLayout />
|
<DataTable data={teamBFlashStats} {columns} striped hoverable fixedLayout />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Info Box -->
|
<!-- Info Box -->
|
||||||
<Card padding="lg" variant="elevated">
|
<Card padding="lg" variant="elevated" class="border-neon-blue/20">
|
||||||
<div class="text-sm text-base-content/60">
|
<div class="flex items-start gap-4">
|
||||||
<p class="mb-2 font-semibold">Flash Stats Explained (For the Visually Challenged):</p>
|
<div
|
||||||
<ul class="list-inside list-disc space-y-1">
|
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-neon-blue/20"
|
||||||
<li><strong>Victims (Correct):</strong> Enemies you blinded - the RIGHT people to flash</li>
|
style="box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);"
|
||||||
<li>
|
>
|
||||||
<strong>Avg Suffering:</strong> Average time enemies spent regretting their peek
|
<Lightbulb class="h-5 w-5 text-neon-blue" />
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div class="text-sm text-white/70">
|
||||||
<strong>Actually Useful:</strong> Enemies killed by teammates while your flash was doing its
|
<p class="mb-3 font-semibold text-white">
|
||||||
job
|
Flash Stats Explained (For the Visually Challenged):
|
||||||
</li>
|
</p>
|
||||||
<li>
|
<ul class="space-y-2">
|
||||||
<strong>Friendly Crimes:</strong> Number of times you betrayed your own team - shame counter
|
<li class="flex items-start gap-2">
|
||||||
</li>
|
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-green"></span>
|
||||||
<li>
|
<span
|
||||||
<strong>Self-Inflicted L:</strong> Times you stared at your own flashbang like a moth to a
|
><strong class="text-neon-green">Victims (Correct):</strong> Enemies you blinded - the
|
||||||
flame
|
RIGHT people to flash</span
|
||||||
</li>
|
>
|
||||||
</ul>
|
</li>
|
||||||
<p class="mt-4 text-xs italic">
|
<li class="flex items-start gap-2">
|
||||||
Remember: If your "Friendly Crimes" is higher than "Victims (Correct)", you might want to
|
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-gold"></span>
|
||||||
reconsider your flash lineups.
|
<span
|
||||||
</p>
|
><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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 Card from '$lib/components/ui/Card.svelte';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
||||||
@@ -64,29 +64,29 @@
|
|||||||
render: (value: unknown, row: PlayerWeapon) => {
|
render: (value: unknown, row: PlayerWeapon) => {
|
||||||
const strValue = value !== undefined ? String(value) : '';
|
const strValue = value !== undefined ? String(value) : '';
|
||||||
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
|
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,
|
key: 'top_weapon' as const,
|
||||||
label: 'Top Weapon',
|
label: 'Weapon of Choice',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'left' as const,
|
align: 'left' as const,
|
||||||
class: 'font-medium'
|
class: 'font-medium text-white'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'total_kills' as const,
|
key: 'total_kills' as const,
|
||||||
label: 'Total Kills',
|
label: 'Total Kills',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono font-semibold'
|
class: 'font-mono font-semibold text-white'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'total_damage' as const,
|
key: 'total_damage' as const,
|
||||||
label: 'Total Damage',
|
label: 'Total Damage',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono',
|
class: 'font-mono text-white/80',
|
||||||
format: (v: unknown) => (v !== undefined ? (v as number).toLocaleString() : '0')
|
format: (v: unknown) => (v !== undefined ? (v as number).toLocaleString() : '0')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
label: 'Total Hits',
|
label: 'Total Hits',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono'
|
class: 'font-mono text-white/80'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
existing.kills += ws.kills;
|
existing.kills += ws.kills;
|
||||||
existing.damage += ws.damage;
|
existing.damage += ws.damage;
|
||||||
existing.hits += ws.hits;
|
existing.hits += ws.hits;
|
||||||
existing.headshot_pct = ws.headshot_pct || 0; // Use latest
|
existing.headshot_pct = ws.headshot_pct || 0;
|
||||||
} else {
|
} else {
|
||||||
weaponAggregates.set(ws.weapon_name, {
|
weaponAggregates.set(ws.weapon_name, {
|
||||||
kills: ws.kills,
|
kills: ws.kills,
|
||||||
@@ -129,19 +129,21 @@
|
|||||||
.sort((a, b) => b.kills - a.kills)
|
.sort((a, b) => b.kills - a.kills)
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
// Weapon usage chart data
|
// Weapon usage chart data with neon colors
|
||||||
const weaponUsageData = {
|
const weaponUsageData = {
|
||||||
labels: topWeapons.map((w) => w.name),
|
labels: topWeapons.map((w) => w.name),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Kills',
|
label: 'Kills',
|
||||||
data: topWeapons.map((w) => w.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 = {
|
const hitGroupTotals = {
|
||||||
head: 0,
|
head: 0,
|
||||||
chest: 0,
|
chest: 0,
|
||||||
@@ -177,11 +179,11 @@
|
|||||||
hitGroupTotals.left_leg + hitGroupTotals.right_leg
|
hitGroupTotals.left_leg + hitGroupTotals.right_leg
|
||||||
],
|
],
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
'rgba(239, 68, 68, 0.8)', // Red for head
|
'rgba(255, 51, 102, 0.8)', // neon-red for head
|
||||||
'rgba(59, 130, 246, 0.8)', // Blue for chest
|
'rgba(0, 212, 255, 0.8)', // neon-blue for chest
|
||||||
'rgba(249, 115, 22, 0.8)', // Orange for stomach
|
'rgba(255, 215, 0, 0.8)', // neon-gold for stomach
|
||||||
'rgba(34, 197, 94, 0.8)', // Green for arms
|
'rgba(0, 255, 136, 0.8)', // neon-green for arms
|
||||||
'rgba(168, 85, 247, 0.8)' // Purple for legs
|
'rgba(139, 92, 246, 0.8)' // neon-purple for legs
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -195,9 +197,14 @@
|
|||||||
{#if !hasWeaponsData}
|
{#if !hasWeaponsData}
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
|
<AlertCircle
|
||||||
<h2 class="mb-2 text-2xl font-bold text-base-content">No Weapons Data Available</h2>
|
class="mx-auto mb-4 h-16 w-16 text-neon-gold"
|
||||||
<p class="mb-4 text-base-content/60">Weapon statistics are not available for this match.</p>
|
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>
|
<Badge variant="warning" size="lg">Weapons data unavailable</Badge>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -206,53 +213,125 @@
|
|||||||
<!-- Top Stats Summary -->
|
<!-- Top Stats Summary -->
|
||||||
<div class="grid gap-6 md:grid-cols-3">
|
<div class="grid gap-6 md:grid-cols-3">
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-3 flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<Crosshair class="h-5 w-5 text-primary" />
|
<div
|
||||||
<h3 class="font-semibold text-base-content">Total Kills</h3>
|
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>
|
||||||
<div class="font-mono text-3xl font-bold text-primary">
|
<div class="mt-2 text-xs text-white/40">Across all weapons</div>
|
||||||
{topWeapons.reduce((sum, w) => sum + w.kills, 0)}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-3 flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<Target class="h-5 w-5 text-success" />
|
<div
|
||||||
<h3 class="font-semibold text-base-content">Total Damage</h3>
|
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>
|
||||||
<div class="font-mono text-3xl font-bold text-success">
|
<div class="mt-2 text-xs text-white/40">Across all weapons</div>
|
||||||
{topWeapons.reduce((sum, w) => sum + w.damage, 0).toLocaleString()}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-3 flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<TrendingUp class="h-5 w-5 text-warning" />
|
<div
|
||||||
<h3 class="font-semibold text-base-content">Total Hits</h3>
|
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>
|
||||||
<div class="font-mono text-3xl font-bold text-warning">
|
<div class="mt-2 text-xs text-white/40">Across all weapons</div>
|
||||||
{topWeapons.reduce((sum, w) => sum + w.hits, 0).toLocaleString()}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top Weapons Chart -->
|
<!-- Top Weapons Chart -->
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-4">
|
<div class="mb-4 flex items-center gap-3">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Most Used Weapons</h2>
|
<div
|
||||||
<p class="text-sm text-base-content/60">Weapons ranked by total kills</p>
|
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>
|
</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>
|
</Card>
|
||||||
|
|
||||||
<!-- Hit Group Distribution -->
|
<!-- Hit Group Distribution -->
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Hit Location Distribution</h2>
|
<h2 class="text-2xl font-bold text-white">Hit Location Distribution</h2>
|
||||||
<p class="text-sm text-base-content/60">Where shots landed across all weapons</p>
|
<p class="text-sm text-white/50">
|
||||||
|
Where shots landed across all weapons - Anatomy of aggression
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
<PieChart data={hitGroupData} height={300} />
|
<PieChart data={hitGroupData} height={300} />
|
||||||
@@ -260,40 +339,40 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<div class="h-4 w-4 rounded bg-[rgba(239,68,68,0.8)]"></div>
|
<div class="h-4 w-4 rounded bg-neon-red"></div>
|
||||||
<span>Head</span>
|
<span class="text-white/80">Head</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono font-semibold">{hitGroupTotals.head}</span>
|
<span class="font-mono font-semibold text-white">{hitGroupTotals.head}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<div class="h-4 w-4 rounded bg-[rgba(59,130,246,0.8)]"></div>
|
<div class="h-4 w-4 rounded bg-neon-blue"></div>
|
||||||
<span>Chest</span>
|
<span class="text-white/80">Chest</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono font-semibold">{hitGroupTotals.chest}</span>
|
<span class="font-mono font-semibold text-white">{hitGroupTotals.chest}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<div class="h-4 w-4 rounded bg-[rgba(249,115,22,0.8)]"></div>
|
<div class="h-4 w-4 rounded bg-neon-gold"></div>
|
||||||
<span>Stomach</span>
|
<span class="text-white/80">Stomach</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono font-semibold">{hitGroupTotals.stomach}</span>
|
<span class="font-mono font-semibold text-white">{hitGroupTotals.stomach}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<div class="h-4 w-4 rounded bg-[rgba(34,197,94,0.8)]"></div>
|
<div class="h-4 w-4 rounded bg-neon-green"></div>
|
||||||
<span>Arms</span>
|
<span class="text-white/80">Arms</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono font-semibold"
|
<span class="font-mono font-semibold text-white"
|
||||||
>{hitGroupTotals.left_arm + hitGroupTotals.right_arm}</span
|
>{hitGroupTotals.left_arm + hitGroupTotals.right_arm}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<div class="h-4 w-4 rounded bg-[rgba(168,85,247,0.8)]"></div>
|
<div class="h-4 w-4 rounded bg-neon-purple"></div>
|
||||||
<span>Legs</span>
|
<span class="text-white/80">Legs</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono font-semibold"
|
<span class="font-mono font-semibold text-white"
|
||||||
>{hitGroupTotals.left_leg + hitGroupTotals.right_leg}</span
|
>{hitGroupTotals.left_leg + hitGroupTotals.right_leg}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,8 +384,10 @@
|
|||||||
<!-- Player Weapons Table -->
|
<!-- Player Weapons Table -->
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h2 class="text-2xl font-bold text-base-content">Player Weapon Performance</h2>
|
<h2 class="text-2xl font-bold text-white">Player Weapon Performance</h2>
|
||||||
<p class="mt-1 text-sm text-base-content/60">Individual player weapon statistics</p>
|
<p class="mt-1 text-sm text-white/50">
|
||||||
|
Individual player weapon statistics - Who brought what to the fight
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable data={sortedPlayerWeapons} columns={weaponColumns} striped hoverable />
|
<DataTable data={sortedPlayerWeapons} columns={weaponColumns} striped hoverable />
|
||||||
|
|||||||
Reference in New Issue
Block a user