diff --git a/ETL/L2_Builder.py b/ETL/L2_Builder.py index 849b1f4..83b0dce 100644 --- a/ETL/L2_Builder.py +++ b/ETL/L2_Builder.py @@ -1342,5 +1342,94 @@ def save_match(cursor, m: MatchData): m.match_id, r.round_num, pe.steam_id_64, pe.side, pe.start_money, pe.equipment_value, pe.main_weapon, pe.has_helmet, pe.has_defuser, pe.round_performance_score )) + # 6. Calculate & Save Clutch Attempts + _calculate_and_save_clutch_attempts(cursor, m.match_id, m.round_list_raw) + +def _calculate_and_save_clutch_attempts(cursor, match_id, round_list_raw): + if not round_list_raw: + return + + try: + round_list = json.loads(round_list_raw) + except: + return + + player_attempts = {} + + for round_data in round_list: + all_kills = round_data.get('all_kill', []) + if not all_kills: + continue + + team_members = {1: set(), 2: set()} + + # Scan for team members + for k in all_kills: + if k.get('attacker') and k['attacker'].get('steamid_64'): + tid = k['attacker'].get('team') + if tid in [1, 2]: + team_members[tid].add(k['attacker']['steamid_64']) + if k.get('victim') and k['victim'].get('steamid_64'): + tid = k['victim'].get('team') + if tid in [1, 2]: + team_members[tid].add(k['victim']['steamid_64']) + + if not team_members[1] or not team_members[2]: + continue + + alive = {1: team_members[1].copy(), 2: team_members[2].copy()} + clutch_triggered_players = set() + + # Sort kills by time + sorted_kills = sorted(all_kills, key=lambda x: x.get('pasttime', 0)) + + for k in sorted_kills: + victim = k.get('victim') + if not victim: continue + + v_sid = victim.get('steamid_64') + v_team = victim.get('team') + + if v_team not in [1, 2] or v_sid not in alive[v_team]: + continue + + alive[v_team].remove(v_sid) + + if len(alive[v_team]) == 1: + survivor_sid = list(alive[v_team])[0] + if survivor_sid not in clutch_triggered_players: + opponent_team = 3 - v_team + opponents_alive_count = len(alive[opponent_team]) + + if opponents_alive_count >= 1: + if survivor_sid not in player_attempts: + player_attempts[survivor_sid] = {'1v1': 0, '1v2': 0, '1v3': 0, '1v4': 0, '1v5': 0} + + n = min(opponents_alive_count, 5) + key = f'1v{n}' + player_attempts[survivor_sid][key] += 1 + clutch_triggered_players.add(survivor_sid) + + # Save to DB + cursor.execute(""" + CREATE TABLE IF NOT EXISTS fact_match_clutch_attempts ( + match_id TEXT, + steam_id_64 TEXT, + attempt_1v1 INTEGER DEFAULT 0, + attempt_1v2 INTEGER DEFAULT 0, + attempt_1v3 INTEGER DEFAULT 0, + attempt_1v4 INTEGER DEFAULT 0, + attempt_1v5 INTEGER DEFAULT 0, + PRIMARY KEY (match_id, steam_id_64) + ) + """) + + for pid, att in player_attempts.items(): + cursor.execute(""" + INSERT OR REPLACE INTO fact_match_clutch_attempts + (match_id, steam_id_64, attempt_1v1, attempt_1v2, attempt_1v3, attempt_1v4, attempt_1v5) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (match_id, pid, att['1v1'], att['1v2'], att['1v3'], att['1v4'], att['1v5'])) + if __name__ == "__main__": process_matches() diff --git a/database/L2/L2_Main.sqlite b/database/L2/L2_Main.sqlite index 81f8ab7..d1f04bb 100644 Binary files a/database/L2/L2_Main.sqlite and b/database/L2/L2_Main.sqlite differ diff --git a/web/routes/players.py b/web/routes/players.py index 3fb7344..00eb9af 100644 --- a/web/routes/players.py +++ b/web/routes/players.py @@ -102,13 +102,15 @@ def detail(steam_id): # --- New: Fetch Detailed Stats from L2 (Clutch, Multi-Kill, Multi-Assist) --- sql_l2 = """ SELECT - SUM(clutch_1v1) as c1, SUM(clutch_1v2) as c2, SUM(clutch_1v3) as c3, SUM(clutch_1v4) as c4, SUM(clutch_1v5) as c5, - SUM(kill_2) as k2, SUM(kill_3) as k3, SUM(kill_4) as k4, SUM(kill_5) as k5, - SUM(many_assists_cnt2) as a2, SUM(many_assists_cnt3) as a3, SUM(many_assists_cnt4) as a4, SUM(many_assists_cnt5) as a5, + SUM(p.clutch_1v1) as c1, SUM(p.clutch_1v2) as c2, SUM(p.clutch_1v3) as c3, SUM(p.clutch_1v4) as c4, SUM(p.clutch_1v5) as c5, + SUM(a.attempt_1v1) as att1, SUM(a.attempt_1v2) as att2, SUM(a.attempt_1v3) as att3, SUM(a.attempt_1v4) as att4, SUM(a.attempt_1v5) as att5, + SUM(p.kill_2) as k2, SUM(p.kill_3) as k3, SUM(p.kill_4) as k4, SUM(p.kill_5) as k5, + SUM(p.many_assists_cnt2) as a2, SUM(p.many_assists_cnt3) as a3, SUM(p.many_assists_cnt4) as a4, SUM(p.many_assists_cnt5) as a5, COUNT(*) as matches, - SUM(round_total) as total_rounds - FROM fact_match_players - WHERE steam_id_64 = ? + SUM(p.round_total) as total_rounds + FROM fact_match_players p + LEFT JOIN fact_match_clutch_attempts a ON p.match_id = a.match_id AND p.steam_id_64 = a.steam_id_64 + WHERE p.steam_id_64 = ? """ l2_stats = query_db('l2', sql_l2, [steam_id], one=True) l2_stats = dict(l2_stats) if l2_stats else {} diff --git a/web/services/stats_service.py b/web/services/stats_service.py index 9393060..e82906b 100644 --- a/web/services/stats_service.py +++ b/web/services/stats_service.py @@ -627,6 +627,52 @@ class StatsService: if target_steam_id not in stats_map: stats_map[target_steam_id] = {} + # --- New: Enrich with L2 Clutch/Multi Stats for Distribution --- + l2_placeholders = ','.join('?' for _ in active_roster_ids) + sql_l2 = f""" + SELECT + p.steam_id_64, + SUM(p.clutch_1v1) as c1, SUM(p.clutch_1v2) as c2, SUM(p.clutch_1v3) as c3, SUM(p.clutch_1v4) as c4, SUM(p.clutch_1v5) as c5, + SUM(a.attempt_1v1) as att1, SUM(a.attempt_1v2) as att2, SUM(a.attempt_1v3) as att3, SUM(a.attempt_1v4) as att4, SUM(a.attempt_1v5) as att5, + SUM(p.kill_2) as k2, SUM(p.kill_3) as k3, SUM(p.kill_4) as k4, SUM(p.kill_5) as k5, + SUM(p.many_assists_cnt2) as a2, SUM(p.many_assists_cnt3) as a3, SUM(p.many_assists_cnt4) as a4, SUM(p.many_assists_cnt5) as a5, + SUM(p.round_total) as total_rounds + FROM fact_match_players p + LEFT JOIN fact_match_clutch_attempts a ON p.match_id = a.match_id AND p.steam_id_64 = a.steam_id_64 + WHERE CAST(p.steam_id_64 AS TEXT) IN ({l2_placeholders}) + GROUP BY p.steam_id_64 + """ + l2_rows = query_db('l2', sql_l2, active_roster_ids) + + for r in l2_rows: + sid = str(r['steam_id_64']) + if sid not in stats_map: + stats_map[sid] = {} + + # Clutch Rates + for i in range(1, 6): + c = r[f'c{i}'] or 0 + att = r[f'att{i}'] or 0 + rate = (c / att) if att > 0 else 0 + stats_map[sid][f'clutch_rate_1v{i}'] = rate + + # Multi-Kill Rates + rounds = r['total_rounds'] or 1 # Avoid div by 0 + total_mk = 0 + for i in range(2, 6): + k = r[f'k{i}'] or 0 + total_mk += k + stats_map[sid][f'multikill_rate_{i}k'] = k / rounds + stats_map[sid]['total_multikill_rate'] = total_mk / rounds + + # Multi-Assist Rates + total_ma = 0 + for i in range(2, 6): + a = r[f'a{i}'] or 0 + total_ma += a + stats_map[sid][f'multiassist_rate_{i}a'] = a / rounds + stats_map[sid]['total_multiassist_rate'] = total_ma / rounds + # 3. Calculate Distribution for ALL metrics # Define metrics list (must match Detailed Panel keys) metrics = [ @@ -658,7 +704,12 @@ class StatsService: # New: Rating Distribution 'rating_dist_carry_rate', 'rating_dist_normal_rate', 'rating_dist_sacrifice_rate', 'rating_dist_sleeping_rate', # New: ELO Stratification - 'elo_lt1200_rating', 'elo_1200_1400_rating', 'elo_1400_1600_rating', 'elo_1600_1800_rating', 'elo_1800_2000_rating', 'elo_gt2000_rating' + 'elo_lt1200_rating', 'elo_1200_1400_rating', 'elo_1400_1600_rating', 'elo_1600_1800_rating', 'elo_1800_2000_rating', 'elo_gt2000_rating', + # New: Clutch & Multi (Real Calculation) + 'clutch_rate_1v1', 'clutch_rate_1v2', 'clutch_rate_1v3', 'clutch_rate_1v4', 'clutch_rate_1v5', + 'multikill_rate_2k', 'multikill_rate_3k', 'multikill_rate_4k', 'multikill_rate_5k', + 'multiassist_rate_2a', 'multiassist_rate_3a', 'multiassist_rate_4a', 'multiassist_rate_5a', + 'total_multikill_rate', 'total_multiassist_rate' ] # Mapping for L2 legacy calls (if any) - mainly map 'rating' to 'basic_avg_rating' etc if needed diff --git a/web/templates/players/profile.html b/web/templates/players/profile.html index 7126d04..1b75614 100644 --- a/web/templates/players/profile.html +++ b/web/templates/players/profile.html @@ -162,12 +162,20 @@ {% endif %} -