- Add dynamic MR12/MR15 halftime calculation to RoundTimeline component - Fix TrackPlayerModal API signature (remove shareCode, change isOpen to open binding) - Update Player types for string IDs and add ban fields (vac_count, vac_date, game_ban_count, game_ban_date) - Add target/rel props to Button component for external links - Enhance homepage with processing matches indicator - Update preferences store for string player IDs - Document Phase 5 progress and TypeScript fixes in TODO.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
316 lines
10 KiB
Svelte
316 lines
10 KiB
Svelte
<script lang="ts">
|
|
import { Crosshair, Target, AlertCircle, TrendingUp } from 'lucide-svelte';
|
|
import Card from '$lib/components/ui/Card.svelte';
|
|
import Badge from '$lib/components/ui/Badge.svelte';
|
|
import DataTable from '$lib/components/data-display/DataTable.svelte';
|
|
import BarChart from '$lib/components/charts/BarChart.svelte';
|
|
import PieChart from '$lib/components/charts/PieChart.svelte';
|
|
import type { PageData } from './$types';
|
|
|
|
let { data }: { data: PageData } = $props();
|
|
const { match, weapons } = data;
|
|
|
|
// Check if we have weapons data
|
|
const hasWeaponsData = weapons && weapons.weapons && weapons.weapons.length > 0;
|
|
|
|
// Get player names map from match data
|
|
const playerNames = new Map(
|
|
match.players?.map((p) => [p.id, { name: p.name, team_id: p.team_id }]) || []
|
|
);
|
|
|
|
// Get unique team IDs
|
|
const uniqueTeamIds = match.players ? [...new Set(match.players.map((p) => p.team_id))] : [];
|
|
const firstTeamId = uniqueTeamIds[0] ?? 2;
|
|
|
|
// Process weapons data for display
|
|
const playerWeaponsData =
|
|
hasWeaponsData && weapons
|
|
? weapons.weapons.map((pw) => {
|
|
const playerInfo = playerNames.get(String(pw.player_id));
|
|
const totalKills = pw.weapon_stats.reduce((sum, w) => sum + w.kills, 0);
|
|
const totalDamage = pw.weapon_stats.reduce((sum, w) => sum + w.damage, 0);
|
|
const totalHits = pw.weapon_stats.reduce((sum, w) => sum + w.hits, 0);
|
|
|
|
// Find most used weapon (by kills)
|
|
const topWeapon = pw.weapon_stats.reduce(
|
|
(max, w) => (w.kills > max.kills ? w : max),
|
|
pw.weapon_stats[0] || { weapon_name: 'None', kills: 0 }
|
|
);
|
|
|
|
return {
|
|
player_id: pw.player_id,
|
|
player_name: playerInfo?.name || 'Unknown',
|
|
team_id: playerInfo?.team_id || 2,
|
|
total_kills: totalKills,
|
|
total_damage: totalDamage,
|
|
total_hits: totalHits,
|
|
top_weapon: topWeapon.weapon_name,
|
|
top_weapon_kills: topWeapon.kills,
|
|
weapon_stats: pw.weapon_stats
|
|
};
|
|
})
|
|
: [];
|
|
|
|
// Sort by total kills descending
|
|
const sortedPlayerWeapons = playerWeaponsData.sort((a, b) => b.total_kills - a.total_kills);
|
|
|
|
// Prepare data table columns
|
|
type PlayerWeapon = (typeof sortedPlayerWeapons)[0];
|
|
const weaponColumns = [
|
|
{
|
|
key: 'player_name' as const,
|
|
label: 'Player',
|
|
sortable: true,
|
|
render: (value: unknown, row: PlayerWeapon) => {
|
|
const strValue = value !== undefined ? String(value) : '';
|
|
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
|
|
return `<a href="/player/${row.player_id}" class="font-medium hover:underline ${teamClass}">${strValue}</a>`;
|
|
}
|
|
},
|
|
{
|
|
key: 'top_weapon' as const,
|
|
label: 'Top Weapon',
|
|
sortable: true,
|
|
align: 'left' as const,
|
|
class: 'font-medium'
|
|
},
|
|
{
|
|
key: 'total_kills' as const,
|
|
label: 'Total Kills',
|
|
sortable: true,
|
|
align: 'center' as const,
|
|
class: 'font-mono font-semibold'
|
|
},
|
|
{
|
|
key: 'total_damage' as const,
|
|
label: 'Total Damage',
|
|
sortable: true,
|
|
align: 'center' as const,
|
|
class: 'font-mono',
|
|
format: (v: unknown) => (v !== undefined ? (v as number).toLocaleString() : '0')
|
|
},
|
|
{
|
|
key: 'total_hits' as const,
|
|
label: 'Total Hits',
|
|
sortable: true,
|
|
align: 'center' as const,
|
|
class: 'font-mono'
|
|
}
|
|
];
|
|
|
|
// Aggregate all weapon stats across players
|
|
const weaponAggregates = new Map<
|
|
string,
|
|
{ kills: number; damage: number; hits: number; headshot_pct: number }
|
|
>();
|
|
if (hasWeaponsData && weapons) {
|
|
for (const pw of weapons.weapons) {
|
|
for (const ws of pw.weapon_stats) {
|
|
const existing = weaponAggregates.get(ws.weapon_name);
|
|
if (existing) {
|
|
existing.kills += ws.kills;
|
|
existing.damage += ws.damage;
|
|
existing.hits += ws.hits;
|
|
existing.headshot_pct = ws.headshot_pct || 0; // Use latest
|
|
} else {
|
|
weaponAggregates.set(ws.weapon_name, {
|
|
kills: ws.kills,
|
|
damage: ws.damage,
|
|
hits: ws.hits,
|
|
headshot_pct: ws.headshot_pct || 0
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const topWeapons = Array.from(weaponAggregates.entries())
|
|
.map(([name, stats]) => ({ name, ...stats }))
|
|
.sort((a, b) => b.kills - a.kills)
|
|
.slice(0, 10);
|
|
|
|
// Weapon usage chart data
|
|
const weaponUsageData = {
|
|
labels: topWeapons.map((w) => w.name),
|
|
datasets: [
|
|
{
|
|
label: 'Kills',
|
|
data: topWeapons.map((w) => w.kills),
|
|
backgroundColor: 'rgba(59, 130, 246, 0.8)'
|
|
}
|
|
]
|
|
};
|
|
|
|
// Hit group distribution (aggregate across all weapons)
|
|
const hitGroupTotals = {
|
|
head: 0,
|
|
chest: 0,
|
|
stomach: 0,
|
|
left_arm: 0,
|
|
right_arm: 0,
|
|
left_leg: 0,
|
|
right_leg: 0
|
|
};
|
|
if (hasWeaponsData && weapons) {
|
|
for (const pw of weapons.weapons) {
|
|
for (const ws of pw.weapon_stats) {
|
|
hitGroupTotals.head += ws.hit_groups.head || 0;
|
|
hitGroupTotals.chest += ws.hit_groups.chest || 0;
|
|
hitGroupTotals.stomach += ws.hit_groups.stomach || 0;
|
|
hitGroupTotals.left_arm += ws.hit_groups.left_arm || 0;
|
|
hitGroupTotals.right_arm += ws.hit_groups.right_arm || 0;
|
|
hitGroupTotals.left_leg += ws.hit_groups.left_leg || 0;
|
|
hitGroupTotals.right_leg += ws.hit_groups.right_leg || 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
const hitGroupData = {
|
|
labels: ['Head', 'Chest', 'Stomach', 'Arms', 'Legs'],
|
|
datasets: [
|
|
{
|
|
data: [
|
|
hitGroupTotals.head,
|
|
hitGroupTotals.chest,
|
|
hitGroupTotals.stomach,
|
|
hitGroupTotals.left_arm + hitGroupTotals.right_arm,
|
|
hitGroupTotals.left_leg + hitGroupTotals.right_leg
|
|
],
|
|
backgroundColor: [
|
|
'rgba(239, 68, 68, 0.8)', // Red for head
|
|
'rgba(59, 130, 246, 0.8)', // Blue for chest
|
|
'rgba(249, 115, 22, 0.8)', // Orange for stomach
|
|
'rgba(34, 197, 94, 0.8)', // Green for arms
|
|
'rgba(168, 85, 247, 0.8)' // Purple for legs
|
|
]
|
|
}
|
|
]
|
|
};
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Match Weapons - CS2.WTF</title>
|
|
</svelte:head>
|
|
|
|
{#if !hasWeaponsData}
|
|
<Card padding="lg">
|
|
<div class="text-center">
|
|
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
|
|
<h2 class="mb-2 text-2xl font-bold text-base-content">No Weapons Data Available</h2>
|
|
<p class="mb-4 text-base-content/60">Weapon statistics are not available for this match.</p>
|
|
<Badge variant="warning" size="lg">Weapons data unavailable</Badge>
|
|
</div>
|
|
</Card>
|
|
{:else}
|
|
<div class="space-y-6">
|
|
<!-- Top Stats Summary -->
|
|
<div class="grid gap-6 md:grid-cols-3">
|
|
<Card padding="lg">
|
|
<div class="mb-3 flex items-center gap-2">
|
|
<Crosshair class="h-5 w-5 text-primary" />
|
|
<h3 class="font-semibold text-base-content">Total Kills</h3>
|
|
</div>
|
|
<div class="font-mono text-3xl font-bold text-primary">
|
|
{topWeapons.reduce((sum, w) => sum + w.kills, 0)}
|
|
</div>
|
|
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
|
|
</Card>
|
|
|
|
<Card padding="lg">
|
|
<div class="mb-3 flex items-center gap-2">
|
|
<Target class="h-5 w-5 text-success" />
|
|
<h3 class="font-semibold text-base-content">Total Damage</h3>
|
|
</div>
|
|
<div class="font-mono text-3xl font-bold text-success">
|
|
{topWeapons.reduce((sum, w) => sum + w.damage, 0).toLocaleString()}
|
|
</div>
|
|
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
|
|
</Card>
|
|
|
|
<Card padding="lg">
|
|
<div class="mb-3 flex items-center gap-2">
|
|
<TrendingUp class="h-5 w-5 text-warning" />
|
|
<h3 class="font-semibold text-base-content">Total Hits</h3>
|
|
</div>
|
|
<div class="font-mono text-3xl font-bold text-warning">
|
|
{topWeapons.reduce((sum, w) => sum + w.hits, 0).toLocaleString()}
|
|
</div>
|
|
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<!-- Top Weapons Chart -->
|
|
<Card padding="lg">
|
|
<div class="mb-4">
|
|
<h2 class="text-2xl font-bold text-base-content">Most Used Weapons</h2>
|
|
<p class="text-sm text-base-content/60">Weapons ranked by total kills</p>
|
|
</div>
|
|
<BarChart data={weaponUsageData} height={300} />
|
|
</Card>
|
|
|
|
<!-- Hit Group Distribution -->
|
|
<Card padding="lg">
|
|
<div class="mb-4">
|
|
<h2 class="text-2xl font-bold text-base-content">Hit Location Distribution</h2>
|
|
<p class="text-sm text-base-content/60">Where shots landed across all weapons</p>
|
|
</div>
|
|
<div class="grid gap-6 md:grid-cols-2">
|
|
<PieChart data={hitGroupData} height={300} />
|
|
<div class="flex flex-col justify-center">
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<span class="flex items-center gap-2">
|
|
<div class="h-4 w-4 rounded bg-[rgba(239,68,68,0.8)]"></div>
|
|
<span>Head</span>
|
|
</span>
|
|
<span class="font-mono font-semibold">{hitGroupTotals.head}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="flex items-center gap-2">
|
|
<div class="h-4 w-4 rounded bg-[rgba(59,130,246,0.8)]"></div>
|
|
<span>Chest</span>
|
|
</span>
|
|
<span class="font-mono font-semibold">{hitGroupTotals.chest}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="flex items-center gap-2">
|
|
<div class="h-4 w-4 rounded bg-[rgba(249,115,22,0.8)]"></div>
|
|
<span>Stomach</span>
|
|
</span>
|
|
<span class="font-mono font-semibold">{hitGroupTotals.stomach}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="flex items-center gap-2">
|
|
<div class="h-4 w-4 rounded bg-[rgba(34,197,94,0.8)]"></div>
|
|
<span>Arms</span>
|
|
</span>
|
|
<span class="font-mono font-semibold"
|
|
>{hitGroupTotals.left_arm + hitGroupTotals.right_arm}</span
|
|
>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="flex items-center gap-2">
|
|
<div class="h-4 w-4 rounded bg-[rgba(168,85,247,0.8)]"></div>
|
|
<span>Legs</span>
|
|
</span>
|
|
<span class="font-mono font-semibold"
|
|
>{hitGroupTotals.left_leg + hitGroupTotals.right_leg}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<!-- Player Weapons Table -->
|
|
<Card padding="none">
|
|
<div class="p-6">
|
|
<h2 class="text-2xl font-bold text-base-content">Player Weapon Performance</h2>
|
|
<p class="mt-1 text-sm text-base-content/60">Individual player weapon statistics</p>
|
|
</div>
|
|
|
|
<DataTable data={sortedPlayerWeapons} columns={weaponColumns} striped hoverable />
|
|
</Card>
|
|
</div>
|
|
{/if}
|