diff --git a/src/routes/player/[id]/+page.svelte b/src/routes/player/[id]/+page.svelte index f4926a1..db57bc0 100644 --- a/src/routes/player/[id]/+page.svelte +++ b/src/routes/player/[id]/+page.svelte @@ -9,7 +9,20 @@ Heart, Crosshair, UserCheck, - ExternalLink + ExternalLink, + Flame, + Zap, + Eye, + AlertTriangle, + Swords, + Skull, + Star, + Crown, + Activity, + Medal, + ArrowUpRight, + ArrowDownRight, + Minus } from 'lucide-svelte'; import Card from '$lib/components/ui/Card.svelte'; import Button from '$lib/components/ui/Button.svelte'; @@ -84,7 +97,7 @@ } }; - // Performance trend chart data (K/D ratio over time) + // Performance trend chart data (K/D ratio over time) - NEON COLORS const performanceTrendData = { labels: playerStats.map((_stat, i) => `Match ${playerStats.length - i}`).reverse(), datasets: [ @@ -93,16 +106,16 @@ data: playerStats .map((stat) => (stat.deaths > 0 ? stat.kills / stat.deaths : stat.kills)) .reverse(), - borderColor: 'rgb(59, 130, 246)', - backgroundColor: 'rgba(59, 130, 246, 0.1)', + borderColor: '#00d4ff', // neon-blue + backgroundColor: 'rgba(0, 212, 255, 0.1)', tension: 0.4, fill: true }, { label: 'KAST %', data: playerStats.map((stat) => stat.kast || 0).reverse(), - borderColor: 'rgb(34, 197, 94)', - backgroundColor: 'rgba(34, 197, 94, 0.1)', + borderColor: '#00ff88', // neon-green + backgroundColor: 'rgba(0, 255, 136, 0.1)', tension: 0.4, fill: true, yAxisID: 'y1' @@ -124,7 +137,14 @@ position: 'left' as const, title: { display: true, - text: 'K/D Ratio' + text: 'K/D Ratio', + color: 'rgba(255, 255, 255, 0.5)' + }, + grid: { + color: 'rgba(255, 255, 255, 0.05)' + }, + ticks: { + color: 'rgba(255, 255, 255, 0.5)' } }, y1: { @@ -133,16 +153,28 @@ position: 'right' as const, title: { display: true, - text: 'KAST %' + text: 'KAST %', + color: 'rgba(255, 255, 255, 0.5)' }, grid: { drawOnChartArea: false + }, + 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)' } } } }; - // Map performance data (win rate per map) + // Map performance data (win rate per map) - NEON COLORS const mapStats = playerStats.reduce( (acc, stat) => { if (!acc[stat.map]) { @@ -164,10 +196,17 @@ data: Object.values(mapStats).map((stat) => (stat.wins / stat.total) * 100), backgroundColor: Object.values(mapStats).map((stat) => { const winRate = stat.wins / stat.total; - if (winRate >= 0.6) return 'rgba(34, 197, 94, 0.8)'; // Green for high win rate - if (winRate >= 0.4) return 'rgba(59, 130, 246, 0.8)'; // Blue for medium - return 'rgba(239, 68, 68, 0.8)'; // Red for low win rate - }) + if (winRate >= 0.6) return 'rgba(0, 255, 136, 0.8)'; // neon-green for high + if (winRate >= 0.4) return 'rgba(0, 212, 255, 0.8)'; // neon-blue for medium + return 'rgba(255, 51, 102, 0.8)'; // neon-red for low + }), + borderColor: Object.values(mapStats).map((stat) => { + const winRate = stat.wins / stat.total; + if (winRate >= 0.6) return '#00ff88'; + if (winRate >= 0.4) return '#00d4ff'; + return '#ff3366'; + }), + borderWidth: 1 } ] }; @@ -181,7 +220,22 @@ max: 100, title: { display: true, - text: 'Win Rate %' + text: 'Win Rate %', + color: 'rgba(255, 255, 255, 0.5)' + }, + 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)' } } } @@ -200,6 +254,51 @@ flashesUsed: playerStats.reduce((sum, stat) => sum + (stat.ud_flash || 0), 0), totalMatches: playerStats.length }; + + // Check if player is a "Flash Criminal" (more team flashes than enemy flashes) + const isFlashCriminal = + utilityStats.teammatesBlinded > utilityStats.enemiesBlinded && + utilityStats.teammatesBlinded > 10; + + // Combat Impact Stats - Multi-kills and damage + const combatStats = { + doubleKills: playerStats.reduce((sum, stat) => sum + (stat.mk_2 || 0), 0), + tripleKills: playerStats.reduce((sum, stat) => sum + (stat.mk_3 || 0), 0), + quadKills: playerStats.reduce((sum, stat) => sum + (stat.mk_4 || 0), 0), + aces: playerStats.reduce((sum, stat) => sum + (stat.mk_5 || 0), 0), + totalMvps: playerStats.reduce((sum, stat) => sum + (stat.mvp || 0), 0), + totalAssists: playerStats.reduce((sum, stat) => sum + (stat.assists || 0), 0), + totalScore: playerStats.reduce((sum, stat) => sum + (stat.score || 0), 0), + avgDamagePerRound: + playerStats.reduce((sum, stat) => sum + (stat.adr || 0), 0) / playerStats.length || 0, + totalMatches: playerStats.length + }; + + // Check if this player is an "Ace Hunter" (lots of aces) + const isAceHunter = combatStats.aces >= 3; + + // Rating Journey Stats + const ratingStats = (() => { + const ratings = playerStats + .filter((stat) => stat.rank_new !== undefined && stat.rank_new > 0) + .map((stat) => stat.rank_new!); + + if (ratings.length === 0) return null; + + const currentRatingValue = ratings[0] || 0; + const oldestRating = ratings[ratings.length - 1] || 0; + const peakRating = Math.max(...ratings); + const lowestRating = Math.min(...ratings); + const ratingDelta = currentRatingValue - oldestRating; + + return { + current: currentRatingValue, + peak: peakRating, + lowest: lowestRating, + delta: ratingDelta, + matchCount: ratings.length + }; + })(); @@ -207,352 +306,773 @@ -
- - -
- -
- +
+
+ +
+ +
+
+
+ +
- -
-
-

{profile.name}

- -
-
- - - {#if profile.vac_count && profile.vac_count > 0} -
- +
+ + +
+
+

- - - VAC Ban{profile.vac_count > 1 ? `s (${profile.vac_count})` : ''} - {#if profile.vac_date} - - {new Date(profile.vac_date).toLocaleDateString()} - + {profile.name} +

+ +
+
+ + + {#if profile.vac_count && profile.vac_count > 0} +
+ + + + VAC Ban{profile.vac_count > 1 ? `s (${profile.vac_count})` : ''} + {#if profile.vac_date} + + {new Date(profile.vac_date).toLocaleDateString()} + + {/if} +
+ {/if} + {#if profile.game_ban_count && profile.game_ban_count > 0} +
+ + Game Ban{profile.game_ban_count > 1 ? `s (${profile.game_ban_count})` : ''} + {#if profile.game_ban_date} + + {new Date(profile.game_ban_date).toLocaleDateString()} + + {/if} +
{/if}
- {/if} - {#if profile.game_ban_count && profile.game_ban_count > 0} -
- - - - Game Ban{profile.game_ban_count > 1 ? `s (${profile.game_ban_count})` : ''} - {#if profile.game_ban_date} - - {new Date(profile.game_ban_date).toLocaleDateString()} - - {/if} +
+ Steam ID: {profile.id} + Last match: {new Date(profile.last_match_date).toLocaleDateString()}
- {/if} -
-
- Steam ID: {profile.id} - Last match: {new Date(profile.last_match_date).toLocaleDateString()} +
+ + +
+ + + +
+
- -
- - - + +
+

Career Statistics

+
+
+ +
+ + K/D Ratio +
+
+ {kd} +
+
+ {profile.avg_kills.toFixed(1)} K / {profile.avg_deaths.toFixed(1)} D avg +
+
+ + +
+ + Win Rate +
+
+ {winRate}% +
+
Last {profile.recent_matches} matches
+
+ + +
+ + Match Record +
+
+ {wins}W + / + {losses}L + / + {ties}T +
+
Last {totalMatchesWithStats} matches
+
+ + +
+ + KAST % +
+
+ {profile.avg_kast.toFixed(1)}% +
+
Kill/Assist/Survive/Trade
+
+ + +
+ + Headshot % +
+
+ {hsPercent}% +
+
{totalHeadshots} of {totalKills} kills
+
- - - + + {#if playerStats.length > 0} +
+
+
+ +
+
+

Combat Impact

+

The highlight reel stats

+
+
- -
-

Career Statistics

-
- -
- - K/D Ratio -
-
{kd}
-
- {profile.avg_kills.toFixed(1)} K / {profile.avg_deaths.toFixed(1)} D avg -
-
+ + {#if isAceHunter} + +
+
+ +
+
+

Ace Hunter Detected

+

+ {profile.name} has {combatStats.aces} aces in the last {combatStats.totalMatches} matches. + Absolute menace. +

+
+
+
+ {/if} - -
- - Win Rate -
-
{winRate}%
-
- Last {profile.recent_matches} matches -
-
+
+ + +
+ + Double Kills +
+
+ {combatStats.doubleKills} +
+
+ {(combatStats.doubleKills / combatStats.totalMatches).toFixed(1)} per match +
+
- -
- - Match Record -
-
- {wins}W - / - {losses}L - / - {ties}T -
-
- Last {totalMatchesWithStats} matches -
-
+ +
+ + Triple Kills +
+
+ {combatStats.tripleKills} +
+
+ {(combatStats.tripleKills / combatStats.totalMatches).toFixed(1)} per match - Hat tricks +
+
- -
- - KAST % -
-
{profile.avg_kast.toFixed(1)}%
-
Kill/Assist/Survive/Trade average
-
+ +
+ + Quad Kills +
+
+ {combatStats.quadKills} +
+
+ {(combatStats.quadKills / combatStats.totalMatches).toFixed(2)} per match - So close +
+
- -
- - Headshot % -
-
{hsPercent}%
-
- {totalHeadshots} of {totalKills} kills -
-
-
-
+ +
+ + Aces +
+
+ {combatStats.aces} +
+
+ {combatStats.aces > 0 ? 'Entire enemy team deleted' : 'Keep trying'} +
+
- -
-
-

Recent Matches

- -
+ + +
+ + MVP Stars +
+
+ {combatStats.totalMvps} +
+
+ {(combatStats.totalMvps / combatStats.totalMatches).toFixed(1)} per match +
+
- {#if recentMatches.length > 0} -
- {#each recentMatches as match} - - {/each} + +
+ + Total Assists +
+
+ {combatStats.totalAssists} +
+
+ {(combatStats.totalAssists / combatStats.totalMatches).toFixed(1)} per match - Team player +
+
+ + +
+ + Avg ADR +
+
+ {combatStats.avgDamagePerRound.toFixed(1)} +
+
+ {combatStats.avgDamagePerRound >= 80 + ? 'High impact' + : combatStats.avgDamagePerRound >= 60 + ? 'Solid damage' + : 'Room to grow'} +
+
+ + +
+ + Total Score +
+
+ {combatStats.totalScore.toLocaleString()} +
+
+ {Math.round(combatStats.totalScore / combatStats.totalMatches)} avg per match +
+
+
- {:else} + {/if} + + + {#if ratingStats} +
+
+
+ +
+
+

Rating Journey

+

+ Premier rating over last {ratingStats.matchCount} matches +

+
+
+ +
+ +
+ + Peak Rating +
+
+ {ratingStats.peak.toLocaleString()} +
+
Personal best
+
+ + +
+ + Current Rating +
+
+ {ratingStats.current.toLocaleString()} +
+
+ {ratingStats.current >= ratingStats.peak + ? 'At peak!' + : `${(ratingStats.peak - ratingStats.current).toLocaleString()} from peak`} +
+
+ + = 0 ? 'border-neon-green/20' : 'border-neon-red/20'} + > +
+ {#if ratingStats.delta > 0} + + {:else if ratingStats.delta < 0} + + {:else} + + {/if} + Rating Delta +
+
+ {ratingStats.delta >= 0 ? '+' : ''}{ratingStats.delta.toLocaleString()} +
+
+ {ratingStats.delta > 0 + ? 'Climbing the ranks' + : ratingStats.delta < 0 + ? 'Rough stretch' + : 'Holding steady'} +
+
+ + +
+ + Rating Range +
+
+ {ratingStats.lowest.toLocaleString()} + - + {ratingStats.peak.toLocaleString()} +
+
+ {(ratingStats.peak - ratingStats.lowest).toLocaleString()} point swing +
+
+
+
+ {/if} + + + {#if playerStats.length === 0 && recentMatches.length > 0} -
- -

No recent matches found for this player.

+
+
+ +
+
+

Detailed Stats Unavailable

+

+ This player's matches haven't been parsed yet. Detailed combat stats, multi-kills, + rating progression, and utility analysis require parsed demo files. +

+
{/if} + + +
+
+
+
+ +
+

Recent Matches

+
+ +
+ + {#if recentMatches.length > 0} +
+ {#each recentMatches as match} + + {/each} +
+ {:else} + +
+ +

No recent matches found for this player.

+
+
+ {/if} +
+ + + {#if playerStats.length > 0} +
+
+
+ +
+

Performance Analysis

+
+
+ + +

+
+ + Performance Trend (Last {playerStats.length} Matches) +
+

+
+ +
+

+ Shows K/D ratio and KAST percentage across recent matches. Higher is better. +

+
+ + + +

+
+ + Map Win Rate +
+

+
+ +
+

+ Win rate by map. Green = strong, + Blue = average, + Red = needs work. +

+
+
+
+ + +
+
+
+ +
+
+

Utility Effectiveness

+

The tactical toolkit analysis

+
+
+ + + {#if isFlashCriminal} + +
+
+ +
+
+

Flash Criminal Detected

+

+ {profile.name} has blinded more teammates than enemies. Consider an intervention. +

+
+
+
+ {/if} + +
+ + +
+ + Flash Assists +
+
+ {utilityStats.flashAssists} +
+
+ {(utilityStats.flashAssists / utilityStats.totalMatches).toFixed(1)} per match +
+
+ + +
+ + Enemies Blinded +
+
+ {utilityStats.enemiesBlinded} +
+
+ {(utilityStats.enemiesBlinded / utilityStats.totalMatches).toFixed(1)} per match - As intended +
+
+ + + +
+ + Self Flashes +
+
+ {utilityStats.selfFlashes} +
+
+ {(utilityStats.selfFlashes / utilityStats.totalMatches).toFixed(1)} per match - Self-inflicted + L's +
+
+ + +
+ + Team Flashes +
+
+ {utilityStats.teammatesBlinded} +
+
+ {(utilityStats.teammatesBlinded / utilityStats.totalMatches).toFixed(1)} per match - Friendly + crimes +
+
+ + + +
+ + HE Damage +
+
+ {Math.round(utilityStats.heDamage)} +
+
+ {Math.round(utilityStats.heDamage / utilityStats.totalMatches)} per match +
+
+ + +
+ + Flame Damage +
+
+ {Math.round(utilityStats.flameDamage)} +
+
+ {Math.round(utilityStats.flameDamage / utilityStats.totalMatches)} per match - Molotov + mixology +
+
+ + + +
+ + Smokes Used +
+
{utilityStats.smokesUsed}
+
+ {(utilityStats.smokesUsed / utilityStats.totalMatches).toFixed(1)} per match +
+
+ + +
+ + Decoys Used +
+
+ {utilityStats.decoysUsed} +
+
+ {(utilityStats.decoysUsed / utilityStats.totalMatches).toFixed(1)} per match - Big brain + plays +
+
+
+
+ {/if}
- - - {#if playerStats.length > 0} -
-

Performance Analysis

-
- - -

-
- - Performance Trend (Last {playerStats.length} Matches) -
-

-
- -
-

- Shows K/D ratio and KAST percentage across recent matches. Higher is better. -

-
- - - -

-
- - Map Win Rate -
-

-
- -
-

- Win rate percentage by map. Green = strong (≥60%), Blue = average (40-60%), Red = weak - (<40%). -

-
-
-
- - -
-

Utility Effectiveness

-
- -
- - Flash Assists -
-
{utilityStats.flashAssists}
-
- {(utilityStats.flashAssists / utilityStats.totalMatches).toFixed(1)} per match -
-
- - -
- - Enemies Blinded -
-
{utilityStats.enemiesBlinded}
-
- {(utilityStats.enemiesBlinded / utilityStats.totalMatches).toFixed(1)} per match -
-
- - -
- - HE Damage -
-
- {Math.round(utilityStats.heDamage)} -
-
- {Math.round(utilityStats.heDamage / utilityStats.totalMatches)} per match -
-
- - -
- - Flame Damage -
-
- {Math.round(utilityStats.flameDamage)} -
-
- {Math.round(utilityStats.flameDamage / utilityStats.totalMatches)} per match -
-
- - -
- - Self Flashes -
-
{utilityStats.selfFlashes}
-
- {(utilityStats.selfFlashes / utilityStats.totalMatches).toFixed(1)} per match -
-
- - -
- - Team Flashes -
-
{utilityStats.teammatesBlinded}
-
- {(utilityStats.teammatesBlinded / utilityStats.totalMatches).toFixed(1)} per match -
-
- - -
- - Smokes Used -
-
{utilityStats.smokesUsed}
-
- {(utilityStats.smokesUsed / utilityStats.totalMatches).toFixed(1)} per match -
-
- - -
- - Decoys Used -
-
{utilityStats.decoysUsed}
-
- {(utilityStats.decoysUsed / utilityStats.totalMatches).toFixed(1)} per match -
-
-
-
- {/if}