1.0.3: Enhanced match detail - Added h2h and roundhistory.

This commit is contained in:
2026-01-26 17:08:43 +08:00
parent f147b4d65a
commit 57fb6ce1f4
4 changed files with 574 additions and 157 deletions

45
check_round_data.py Normal file
View File

@@ -0,0 +1,45 @@
import sqlite3
import pandas as pd
match_id = 'g161-n-20251222204652101389654'
def check_data():
conn = sqlite3.connect('database/L2/L2_Main.sqlite')
print(f"--- Check Match: {match_id} ---")
# 1. Source Type
c = conn.cursor()
c.execute("SELECT data_source_type FROM fact_matches WHERE match_id = ?", (match_id,))
row = c.fetchone()
if row:
print(f"Data Source: {row[0]}")
else:
print("Match not found")
return
# 2. Round Events (Sample)
print("\n--- Round Events Sample ---")
try:
df = pd.read_sql(f"SELECT round_num, event_type, attacker_steam_id, victim_steam_id, weapon FROM fact_round_events WHERE match_id = '{match_id}' LIMIT 5", conn)
print(df)
if df.empty:
print("WARNING: No events found.")
except Exception as e:
print(e)
# 3. Economy (Sample)
print("\n--- Economy Sample ---")
try:
df_eco = pd.read_sql(f"SELECT round_num, steam_id_64, equipment_value FROM fact_round_player_economy WHERE match_id = '{match_id}' LIMIT 5", conn)
print(df_eco)
if df_eco.empty:
print("Info: No economy data (Likely Classic source).")
except Exception as e:
print(e)
conn.close()
if __name__ == "__main__":
check_data()

View File

@@ -84,9 +84,36 @@ def detail(match_id):
team1_players.sort(key=lambda x: x.get('rating', 0) or 0, reverse=True) team1_players.sort(key=lambda x: x.get('rating', 0) or 0, reverse=True)
team2_players.sort(key=lambda x: x.get('rating', 0) or 0, reverse=True) team2_players.sort(key=lambda x: x.get('rating', 0) or 0, reverse=True)
# New Data for Enhanced Detail View
h2h_stats = StatsService.get_head_to_head_stats(match_id)
round_details = StatsService.get_match_round_details(match_id)
# Convert H2H stats to a more usable format (nested dict)
# h2h_matrix[attacker_id][victim_id] = kills
h2h_matrix = {}
if h2h_stats:
for row in h2h_stats:
a_id = row['attacker_steam_id']
v_id = row['victim_steam_id']
kills = row['kills']
if a_id not in h2h_matrix: h2h_matrix[a_id] = {}
h2h_matrix[a_id][v_id] = kills
# Create a mapping of SteamID -> Username for the template
# We can use the players list we already have
player_name_map = {}
for p in players:
sid = p.get('steam_id_64')
name = p.get('username')
if sid and name:
player_name_map[str(sid)] = name
return render_template('matches/detail.html', match=match, return render_template('matches/detail.html', match=match,
team1_players=team1_players, team2_players=team2_players, team1_players=team1_players, team2_players=team2_players,
rounds=rounds) rounds=rounds,
h2h_matrix=h2h_matrix,
round_details=round_details,
player_name_map=player_name_map)
@bp.route('/<match_id>/raw') @bp.route('/<match_id>/raw')
def raw_json(match_id): def raw_json(match_id):

View File

@@ -93,52 +93,87 @@ class StatsService:
if not active_roster_ids: if not active_roster_ids:
result_map = {} result_map = {}
else: else:
# 1. Get UIDs for Roster Members involved in these matches
# We query fact_match_players to ensure we get the UIDs actually used in these matches
roster_placeholders = ','.join('?' for _ in active_roster_ids) roster_placeholders = ','.join('?' for _ in active_roster_ids)
uid_sql = f"""
# We cast steam_id_64 to TEXT to ensure match even if stored as int SELECT DISTINCT steam_id_64, uid
our_result_sql = f""" FROM fact_match_players
SELECT mp.match_id, mp.team_id, m.winner_team, COUNT(*) as our_count WHERE match_id IN ({placeholders})
FROM fact_match_players mp AND CAST(steam_id_64 AS TEXT) IN ({roster_placeholders})
JOIN fact_matches m ON mp.match_id = m.match_id
WHERE mp.match_id IN ({placeholders})
AND CAST(mp.steam_id_64 AS TEXT) IN ({roster_placeholders})
GROUP BY mp.match_id, mp.team_id
""" """
combined_args_uid = match_ids + active_roster_ids
uid_rows = query_db('l2', uid_sql, combined_args_uid)
# Combine args: match_ids + roster_ids # Set of "Our UIDs" (as strings)
combined_args = match_ids + active_roster_ids our_uids = set()
our_rows = query_db('l2', our_result_sql, combined_args) for r in uid_rows:
if r['uid']:
our_uids.add(str(r['uid']))
# Map match_id -> result ('win', 'loss', 'draw', 'mixed') # 2. Get Group UIDs and Winner info from fact_match_teams
# We need to know which group contains our UIDs
teams_sql = f"""
SELECT fmt.match_id, fmt.group_id, fmt.group_uids, m.winner_team
FROM fact_match_teams fmt
JOIN fact_matches m ON fmt.match_id = m.match_id
WHERE fmt.match_id IN ({placeholders})
"""
teams_rows = query_db('l2', teams_sql, match_ids)
# 3. Determine Result per Match
result_map = {} result_map = {}
match_sides = {} # Group data by match
match_winners = {} match_groups = {} # match_id -> {group_id: [uids...], winner: int}
for r in our_rows: for r in teams_rows:
mid = r['match_id'] mid = r['match_id']
if mid not in match_sides: match_sides[mid] = {} gid = r['group_id']
match_sides[mid][r['team_id']] = r['our_count'] uids_str = r['group_uids'] or ""
match_winners[mid] = r['winner_team'] # Split and clean UIDs
uids = set(str(u).strip() for u in uids_str.split(',') if u.strip())
for mid, sides in match_sides.items(): if mid not in match_groups:
winner = match_winners.get(mid) match_groups[mid] = {'groups': {}, 'winner': r['winner_team']}
if not winner:
result_map[mid] = 'draw'
continue
our_on_winner = sides.get(winner, 0)
loser = 2 if winner == 1 else 1
our_on_loser = sides.get(loser, 0)
if our_on_winner > 0 and our_on_loser == 0: match_groups[mid]['groups'][gid] = uids
# Analyze
for mid, data in match_groups.items():
winner_gid = data['winner']
groups = data['groups']
our_in_winner = False
our_in_loser = False
# Check each group
for gid, uids in groups.items():
# Intersection of Our UIDs and Group UIDs
common = our_uids.intersection(uids)
if common:
if gid == winner_gid:
our_in_winner = True
else:
our_in_loser = True
if our_in_winner and not our_in_loser:
result_map[mid] = 'win' result_map[mid] = 'win'
elif our_on_loser > 0 and our_on_winner == 0: elif our_in_loser and not our_in_winner:
result_map[mid] = 'loss' result_map[mid] = 'loss'
elif our_on_winner > 0 and our_on_loser > 0: elif our_in_winner and our_in_loser:
result_map[mid] = 'mixed' result_map[mid] = 'mixed'
else: else:
result_map[mid] = None # Fallback: If UID matching failed (maybe missing UIDs), try old team_id method?
# Or just leave it as None (safe)
pass
# Convert to dict to modify
matches = [dict(m) for m in matches]
for m in matches:
m['avg_elo'] = elo_map.get(m['match_id'], 0)
m['max_party'] = party_map.get(m['match_id'], 1)
m['our_result'] = result_map.get(m['match_id'])
# Convert to dict to modify # Convert to dict to modify
matches = [dict(m) for m in matches] matches = [dict(m) for m in matches]
@@ -387,3 +422,78 @@ class StatsService:
""" """
return query_db('l2', sql) return query_db('l2', sql)
@staticmethod
def get_head_to_head_stats(match_id):
"""
Returns a matrix of kills between players.
List of {attacker_steam_id, victim_steam_id, kills}
"""
sql = """
SELECT attacker_steam_id, victim_steam_id, COUNT(*) as kills
FROM fact_round_events
WHERE match_id = ? AND event_type = 'kill'
GROUP BY attacker_steam_id, victim_steam_id
"""
return query_db('l2', sql, [match_id])
@staticmethod
def get_match_round_details(match_id):
"""
Returns a detailed dictionary of rounds, events, and economy.
{
round_num: {
info: {winner_side, win_reason_desc, end_time_stamp...},
events: [ {event_type, event_time, attacker..., weapon...}, ... ],
economy: { steam_id: {main_weapon, equipment_value...}, ... }
}
}
"""
# 1. Base Round Info
rounds_sql = "SELECT * FROM fact_rounds WHERE match_id = ? ORDER BY round_num"
rounds_rows = query_db('l2', rounds_sql, [match_id])
if not rounds_rows:
return {}
# 2. Events
events_sql = """
SELECT * FROM fact_round_events
WHERE match_id = ?
ORDER BY round_num, event_time
"""
events_rows = query_db('l2', events_sql, [match_id])
# 3. Economy (if avail)
eco_sql = """
SELECT * FROM fact_round_player_economy
WHERE match_id = ?
"""
eco_rows = query_db('l2', eco_sql, [match_id])
# Structure Data
result = {}
# Initialize rounds
for r in rounds_rows:
r_num = r['round_num']
result[r_num] = {
'info': dict(r),
'events': [],
'economy': {}
}
# Group events
for e in events_rows:
r_num = e['round_num']
if r_num in result:
result[r_num]['events'].append(dict(e))
# Group economy
for eco in eco_rows:
r_num = eco['round_num']
sid = eco['steam_id_64']
if r_num in result:
result[r_num]['economy'][sid] = dict(eco)
return result

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="space-y-6"> <div class="space-y-6" x-data="{ tab: 'overview' }">
<!-- Header --> <!-- Header -->
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-6"> <div class="bg-white dark:bg-slate-800 shadow rounded-lg p-6">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
@@ -20,74 +20,273 @@
<a href="{{ url_for('matches.raw_json', match_id=match.match_id) }}" target="_blank" class="text-sm text-yrtv-600 hover:underline">Download Raw JSON</a> <a href="{{ url_for('matches.raw_json', match_id=match.match_id) }}" target="_blank" class="text-sm text-yrtv-600 hover:underline">Download Raw JSON</a>
</div> </div>
</div> </div>
<!-- Tab Navigation -->
<div class="mt-6 border-b border-gray-200 dark:border-gray-700">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
<button @click="tab = 'overview'"
:class="tab === 'overview' ? 'border-yrtv-500 text-yrtv-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
Overview
</button>
<button @click="tab = 'h2h'"
:class="tab === 'h2h' ? 'border-yrtv-500 text-yrtv-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
Head to Head
</button>
<button @click="tab = 'rounds'"
:class="tab === 'rounds' ? 'border-yrtv-500 text-yrtv-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
Round History
</button>
</nav>
</div>
</div> </div>
<!-- Team 1 Stats --> <!-- Tab: Overview -->
<div class="bg-white dark:bg-slate-800 shadow rounded-lg overflow-hidden"> <div x-show="tab === 'overview'" class="space-y-6">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-slate-700"> <!-- Team 1 Stats -->
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Team 1</h3> <div class="bg-white dark:bg-slate-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-slate-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Team 1</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-slate-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Player</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">K</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">D</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">A</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">+/-</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ADR</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">KAST</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Rating</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
{% for p in team1_players %}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-8 w-8">
{% if p.avatar_url %}
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url }}" alt="">
{% else %}
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200">
{{ (p.username or p.steam_id_64)[:2] | upper }}
</div>
{% endif %}
</div>
<div class="ml-4">
<div class="flex items-center space-x-2">
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
{{ p.username or p.steam_id_64 }}
</a>
{% if p.party_size > 1 %}
{% set pc = p.party_size %}
{% set p_color = 'bg-blue-100 text-blue-800' %}
{% if pc == 2 %}{% set p_color = 'bg-indigo-100 text-indigo-800' %}
{% elif pc == 3 %}{% set p_color = 'bg-blue-100 text-blue-800' %}
{% elif pc == 4 %}{% set p_color = 'bg-purple-100 text-purple-800' %}
{% elif pc >= 5 %}{% set p_color = 'bg-orange-100 text-orange-800' %}
{% endif %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium {{ p_color }} dark:bg-opacity-20" title="Roster Party of {{ p.party_size }}">
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
{{ p.party_size }}
</span>
{% endif %}
</div>
</div>
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-900 dark:text-white">{{ p.kills }}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ p.deaths }}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ p.assists }}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right font-medium {% if (p.kills - p.deaths) >= 0 %}text-green-600{% else %}text-red-600{% endif %}">
{{ p.kills - p.deaths }}
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(p.adr or 0) }}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(p.kast or 0) }}%</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right font-bold text-gray-900 dark:text-white">{{ "%.2f"|format(p.rating or 0) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
<!-- Team 2 Stats -->
<div class="bg-white dark:bg-slate-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-slate-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Team 2</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-slate-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Player</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">K</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">D</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">A</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">+/-</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ADR</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">KAST</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Rating</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
{% for p in team2_players %}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-8 w-8">
{% if p.avatar_url %}
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url }}" alt="">
{% else %}
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200">
{{ (p.username or p.steam_id_64)[:2] | upper }}
</div>
{% endif %}
</div>
<div class="ml-4">
<div class="flex items-center space-x-2">
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
{{ p.username or p.steam_id_64 }}
</a>
{% if p.party_size > 1 %}
{% set pc = p.party_size %}
{% set p_color = 'bg-blue-100 text-blue-800' %}
{% if pc == 2 %}{% set p_color = 'bg-indigo-100 text-indigo-800' %}
{% elif pc == 3 %}{% set p_color = 'bg-blue-100 text-blue-800' %}
{% elif pc == 4 %}{% set p_color = 'bg-purple-100 text-purple-800' %}
{% elif pc >= 5 %}{% set p_color = 'bg-orange-100 text-orange-800' %}
{% endif %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium {{ p_color }} dark:bg-opacity-20" title="Roster Party of {{ p.party_size }}">
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
{{ p.party_size }}
</span>
{% endif %}
</div>
</div>
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-900 dark:text-white">{{ p.kills }}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ p.deaths }}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ p.assists }}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right font-medium {% if (p.kills - p.deaths) >= 0 %}text-green-600{% else %}text-red-600{% endif %}">
{{ p.kills - p.deaths }}
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(p.adr or 0) }}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(p.kast or 0) }}%</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right font-bold text-gray-900 dark:text-white">{{ "%.2f"|format(p.rating or 0) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Tab: Head to Head -->
<div x-show="tab === 'h2h'" class="bg-white dark:bg-slate-800 shadow rounded-lg overflow-hidden p-6" style="display: none;">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Head-to-Head Kills</h3>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-slate-700"> <thead class="bg-gray-50 dark:bg-slate-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Player</th> <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Killer \ Victim</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">K</th> {% for victim in team2_players %}
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">D</th> <th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-300 tracking-wider w-20" title="{{ victim.username }}">
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">A</th> <div class="flex flex-col items-center">
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">+/-</th> {% if victim.avatar_url %}
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ADR</th> <img class="h-6 w-6 rounded-full mb-1" src="{{ victim.avatar_url }}">
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">KAST</th> {% else %}
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Rating</th> <div class="h-6 w-6 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200 mb-1">
{{ (victim.username or victim.steam_id_64)[:2] | upper }}
</div>
{% endif %}
<span class="truncate w-16 text-center">{{ victim.username or 'Player' }}</span>
</div>
</th>
{% endfor %}
</tr> </tr>
</thead> </thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700"> <tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
{% for p in team1_players %} {% for killer in team1_players %}
<tr> <tr>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-3 py-2 whitespace-nowrap font-medium text-gray-900 dark:text-white flex items-center">
<div class="flex items-center"> {% if killer.avatar_url %}
<div class="flex-shrink-0 h-8 w-8"> <img class="h-6 w-6 rounded-full mr-2" src="{{ killer.avatar_url }}">
{% if p.avatar_url %} {% else %}
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url }}" alt=""> <div class="h-6 w-6 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200 mr-2">
{% else %} {{ (killer.username or killer.steam_id_64)[:2] | upper }}
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200">
{{ (p.username or p.steam_id_64)[:2] | upper }}
</div>
{% endif %}
</div>
<div class="ml-4">
<div class="flex items-center space-x-2">
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
{{ p.username or p.steam_id_64 }}
</a>
{% if p.party_size > 1 %}
{% set pc = p.party_size %}
{% set p_color = 'bg-blue-100 text-blue-800' %}
{% if pc == 2 %}{% set p_color = 'bg-indigo-100 text-indigo-800' %}
{% elif pc == 3 %}{% set p_color = 'bg-blue-100 text-blue-800' %}
{% elif pc == 4 %}{% set p_color = 'bg-purple-100 text-purple-800' %}
{% elif pc >= 5 %}{% set p_color = 'bg-orange-100 text-orange-800' %}
{% endif %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium {{ p_color }} dark:bg-opacity-20" title="Roster Party of {{ p.party_size }}">
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
{{ p.party_size }}
</span>
{% endif %}
</div>
</div>
</div> </div>
{% endif %}
<span class="truncate w-24">{{ killer.username or 'Player' }}</span>
</td> </td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-900 dark:text-white">{{ p.kills }}</td> {% for victim in team2_players %}
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ p.deaths }}</td> {% set kills = h2h_matrix.get(killer.steam_id_64, {}).get(victim.steam_id_64, 0) %}
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ p.assists }}</td> <td class="px-3 py-2 text-center text-sm border-l border-gray-100 dark:border-gray-700
<td class="px-4 py-4 whitespace-nowrap text-sm text-right font-medium {% if (p.kills - p.deaths) >= 0 %}text-green-600{% else %}text-red-600{% endif %}"> {% if kills > 0 %}font-bold text-gray-900 dark:text-white{% else %}text-gray-300 dark:text-gray-600{% endif %}"
{{ p.kills - p.deaths }} style="{% if kills > 0 %}background-color: rgba(239, 68, 68, {{ kills * 0.1 }}){% endif %}">
{{ kills if kills > 0 else '-' }}
</td> </td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(p.adr or 0) }}</td> {% endfor %}
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(p.kast or 0) }}%</td> </tr>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right font-bold text-gray-900 dark:text-white">{{ "%.2f"|format(p.rating or 0) }}</td> {% endfor %}
</tbody>
</table>
</div>
<div class="my-6 border-t border-gray-200 dark:border-gray-700"></div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-slate-700">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Killer \ Victim</th>
{% for victim in team1_players %}
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-300 tracking-wider w-20" title="{{ victim.username }}">
<div class="flex flex-col items-center">
{% if victim.avatar_url %}
<img class="h-6 w-6 rounded-full mb-1" src="{{ victim.avatar_url }}">
{% else %}
<div class="h-6 w-6 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200 mb-1">
{{ (victim.username or victim.steam_id_64)[:2] | upper }}
</div>
{% endif %}
<span class="truncate w-16 text-center">{{ victim.username or 'Player' }}</span>
</div>
</th>
{% endfor %}
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
{% for killer in team2_players %}
<tr>
<td class="px-3 py-2 whitespace-nowrap font-medium text-gray-900 dark:text-white flex items-center">
{% if killer.avatar_url %}
<img class="h-6 w-6 rounded-full mr-2" src="{{ killer.avatar_url }}">
{% else %}
<div class="h-6 w-6 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200 mr-2">
{{ (killer.username or killer.steam_id_64)[:2] | upper }}
</div>
{% endif %}
<span class="truncate w-24">{{ killer.username or 'Player' }}</span>
</td>
{% for victim in team1_players %}
{% set kills = h2h_matrix.get(killer.steam_id_64, {}).get(victim.steam_id_64, 0) %}
<td class="px-3 py-2 text-center text-sm border-l border-gray-100 dark:border-gray-700
{% if kills > 0 %}font-bold text-gray-900 dark:text-white{% else %}text-gray-300 dark:text-gray-600{% endif %}"
style="{% if kills > 0 %}background-color: rgba(59, 130, 246, {{ kills * 0.1 }}){% endif %}">
{{ kills if kills > 0 else '-' }}
</td>
{% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -95,77 +294,113 @@
</div> </div>
</div> </div>
<!-- Team 2 Stats --> <!-- Tab: Round History -->
<div class="bg-white dark:bg-slate-800 shadow rounded-lg overflow-hidden"> <div x-show="tab === 'rounds'" class="bg-white dark:bg-slate-800 shadow rounded-lg p-6 space-y-4" style="display: none;">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-slate-700"> <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Round by Round History</h3>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Team 2</h3>
</div> {% if not round_details %}
<div class="overflow-x-auto"> <p class="text-gray-500">No round detail data available for this match.</p>
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> {% else %}
<thead class="bg-gray-50 dark:bg-slate-700"> <div class="space-y-2">
<tr> {% for r_num, data in round_details.items() %}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Player</th> <div x-data="{ expanded: false }" class="border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden">
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">K</th> <!-- Round Header -->
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">D</th> <div @click="expanded = !expanded"
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">A</th> class="flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-slate-700 cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-600 transition">
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">+/-</th> <div class="flex items-center space-x-4">
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ADR</th> <span class="text-sm font-bold text-gray-500 dark:text-gray-400">Round {{ r_num }}</span>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">KAST</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Rating</th> <!-- Winner Icon -->
</tr> {% if data.info.winner_side == 'CT' %}
</thead> <span class="px-2 py-0.5 rounded text-xs font-bold bg-blue-100 text-blue-800 border border-blue-200">
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700"> CT Win
{% for p in team2_players %} </span>
<tr> {% elif data.info.winner_side == 'T' %}
<td class="px-6 py-4 whitespace-nowrap"> <span class="px-2 py-0.5 rounded text-xs font-bold bg-yellow-100 text-yellow-800 border border-yellow-200">
<div class="flex items-center"> T Win
<div class="flex-shrink-0 h-8 w-8"> </span>
{% if p.avatar_url %} {% else %}
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url }}" alt=""> <span class="px-2 py-0.5 rounded text-xs font-bold bg-gray-100 text-gray-800">
{% else %} {{ data.info.winner_side }}
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200"> </span>
{{ (p.username or p.steam_id_64)[:2] | upper }} {% endif %}
</div>
{% endif %} <span class="text-xs text-gray-500 dark:text-gray-400">
{{ data.info.win_reason_desc }}
</span>
</div>
<div class="flex items-center space-x-4">
<span class="text-lg font-mono font-bold text-gray-900 dark:text-white">
{{ data.info.ct_score }} - {{ data.info.t_score }}
</span>
<svg :class="{'rotate-180': expanded}" class="h-5 w-5 text-gray-400 transform transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<!-- Round Details (Expanded) -->
<div x-show="expanded" class="p-4 bg-white dark:bg-slate-800 border-t border-gray-200 dark:border-gray-700">
<!-- Economy Section (if available) -->
{% if data.economy %}
<div class="mb-4">
<h4 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Economy Snapshot</h4>
<div class="grid grid-cols-2 gap-4">
<!-- Left Team (usually CT start, but let's just list keys for now) -->
<!-- We can map steam_id to username via existing players list if passed, or just show summary -->
<!-- For simplicity v1: Just show count of weapons -->
</div>
<div class="text-xs text-gray-400 italic">
(Detailed economy view coming soon)
</div>
</div>
{% endif %}
<!-- Events Timeline -->
<div class="space-y-2">
{% for event in data.events %}
<div class="flex items-center text-sm">
<span class="w-12 text-right text-gray-400 font-mono text-xs mr-4">{{ event.event_time }}s</span>
{% if event.event_type == 'kill' %}
<div class="flex items-center flex-1">
<span class="font-medium {% if event.is_headshot %}text-red-600{% else %}text-gray-900 dark:text-white{% endif %}">
{{ player_name_map.get(event.attacker_steam_id, event.attacker_steam_id) }}
</span>
<span class="mx-2 text-gray-400">
{% if event.is_headshot %}⌖{% else %}🔫{% endif %}
</span>
<span class="text-gray-600 dark:text-gray-300">
{{ player_name_map.get(event.victim_steam_id, event.victim_steam_id) }}
</span>
<span class="ml-2 text-xs text-gray-400 bg-gray-100 dark:bg-slate-700 px-1 rounded">{{ event.weapon }}</span>
</div> </div>
<div class="ml-4"> {% elif event.event_type == 'bomb_plant' %}
<div class="flex items-center space-x-2"> <div class="flex items-center text-yellow-600 font-medium">
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600"> <span>💣 Bomb Planted</span>
{{ p.username or p.steam_id_64 }}
</a>
{% if p.party_size > 1 %}
{% set pc = p.party_size %}
{% set p_color = 'bg-blue-100 text-blue-800' %}
{% if pc == 2 %}{% set p_color = 'bg-indigo-100 text-indigo-800' %}
{% elif pc == 3 %}{% set p_color = 'bg-blue-100 text-blue-800' %}
{% elif pc == 4 %}{% set p_color = 'bg-purple-100 text-purple-800' %}
{% elif pc >= 5 %}{% set p_color = 'bg-orange-100 text-orange-800' %}
{% endif %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium {{ p_color }} dark:bg-opacity-20" title="Roster Party of {{ p.party_size }}">
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
{{ p.party_size }}
</span>
{% endif %}
</div>
</div> </div>
</div> {% elif event.event_type == 'bomb_defuse' %}
</td> <div class="flex items-center text-blue-600 font-medium">
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-900 dark:text-white">{{ p.kills }}</td> <span>✂️ Bomb Defused</span>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ p.deaths }}</td> </div>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ p.assists }}</td> {% endif %}
<td class="px-4 py-4 whitespace-nowrap text-sm text-right font-medium {% if (p.kills - p.deaths) >= 0 %}text-green-600{% else %}text-red-600{% endif %}"> </div>
{{ p.kills - p.deaths }} {% endfor %}
</td> </div>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(p.adr or 0) }}</td> </div>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(p.kast or 0) }}%</td> </div>
<td class="px-4 py-4 whitespace-nowrap text-sm text-right font-bold text-gray-900 dark:text-white">{{ "%.2f"|format(p.rating or 0) }}</td> {% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
<!-- Add Player Name Map for JS/Frontend Lookup if needed -->
<script>
// Optional: Pass player mapping to JS to replace IDs with Names in Timeline
// But Jinja is cleaner if we had the map.
</script>
{% endblock %} {% endblock %}