1.5.0: Clutch fully recovered.
This commit is contained in:
@@ -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()
|
||||
|
||||
Binary file not shown.
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -162,12 +162,20 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex items-baseline gap-1 mb-1">
|
||||
<span class="text-xl font-black text-gray-900 dark:text-white font-mono">
|
||||
{{ format_str.format(value if value is not none else 0) }}
|
||||
</span>
|
||||
{% if sublabel %}
|
||||
<span class="text-[10px] text-gray-400">{{ sublabel }}</span>
|
||||
<div class="flex justify-between items-end mb-1">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-xl font-black text-gray-900 dark:text-white font-mono">
|
||||
{{ format_str.format(value if value is not none else 0) }}
|
||||
</span>
|
||||
{% if sublabel %}
|
||||
<span class="text-[10px] text-gray-400">{{ sublabel }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if count_label is not none %}
|
||||
<div class="text-[10px] font-bold text-gray-400 font-mono mb-0.5">
|
||||
{{ count_label }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -186,13 +194,6 @@
|
||||
<span>H:{{ format_str.format(dist.max) }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Count Label (Bottom Right) -->
|
||||
{% if count_label is not none %}
|
||||
<div class="absolute bottom-0 right-0 text-[10px] font-bold text-gray-400 font-mono">
|
||||
{{ count_label }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -268,8 +269,8 @@
|
||||
HPS (Clutch/Pressure) & PTL (Pistol)
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
|
||||
{{ detail_item('1v1 Win% (1v1胜率)', features['hps_clutch_win_rate_1v1'], 'hps_clutch_win_rate_1v1', '{:.1%}') }}
|
||||
{{ detail_item('1v3+ Win% (残局大神)', features['hps_clutch_win_rate_1v3_plus'], 'hps_clutch_win_rate_1v3_plus', '{:.1%}') }}
|
||||
{{ detail_item('Avg 1v1 (场均1v1)', features['hps_clutch_win_rate_1v1'], 'hps_clutch_win_rate_1v1', '{:.2f}') }}
|
||||
{{ detail_item('Avg 1v3+ (场均1v3+)', features['hps_clutch_win_rate_1v3_plus'], 'hps_clutch_win_rate_1v3_plus', '{:.2f}') }}
|
||||
{{ detail_item('Match Pt Win% (赛点胜率)', features['hps_match_point_win_rate'], 'hps_match_point_win_rate', '{:.1%}') }}
|
||||
{{ detail_item('Pressure Entry (逆风首杀)', features['hps_pressure_entry_rate'], 'hps_pressure_entry_rate', '{:.1%}') }}
|
||||
{{ detail_item('Comeback KD (翻盘KD)', features['hps_comeback_kd_diff'], 'hps_comeback_kd_diff') }}
|
||||
@@ -300,19 +301,34 @@
|
||||
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
|
||||
SPECIAL (Clutch & Multi)
|
||||
</h4>
|
||||
{% set matches = l2_stats.get('matches', 0) or 1 %}
|
||||
{% set rounds = l2_stats.get('total_rounds', 0) or 1 %}
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
|
||||
{{ detail_item('1v1 Win%', (l2_stats.get('c1', 0) or 0) / rounds, 'l2_c1', '{:.1%}', count_label=l2_stats.get('c1', 0)) }}
|
||||
{{ detail_item('1v2 Win%', (l2_stats.get('c2', 0) or 0) / rounds, 'l2_c2', '{:.1%}', count_label=l2_stats.get('c2', 0)) }}
|
||||
{{ detail_item('1v3 Win%', (l2_stats.get('c3', 0) or 0) / rounds, 'l2_c3', '{:.1%}', count_label=l2_stats.get('c3', 0)) }}
|
||||
{{ detail_item('1v4 Win%', (l2_stats.get('c4', 0) or 0) / rounds, 'l2_c4', '{:.1%}', count_label=l2_stats.get('c4', 0)) }}
|
||||
{{ detail_item('1v5 Win%', (l2_stats.get('c5', 0) or 0) / rounds, 'l2_c5', '{:.1%}', count_label=l2_stats.get('c5', 0)) }}
|
||||
{% set c1 = l2_stats.get('c1', 0) or 0 %}
|
||||
{% set a1 = l2_stats.get('att1', 0) or 0 %}
|
||||
{{ detail_item('1v1 Win% (1v1胜率)', c1 / a1 if a1 > 0 else 0, 'clutch_rate_1v1', '{:.1%}', count_label=c1 ~ '/' ~ a1) }}
|
||||
|
||||
{% set c2 = l2_stats.get('c2', 0) or 0 %}
|
||||
{% set a2 = l2_stats.get('att2', 0) or 0 %}
|
||||
{{ detail_item('1v2 Win% (1v2胜率)', c2 / a2 if a2 > 0 else 0, 'clutch_rate_1v2', '{:.1%}', count_label=c2 ~ '/' ~ a2) }}
|
||||
|
||||
{% set c3 = l2_stats.get('c3', 0) or 0 %}
|
||||
{% set a3 = l2_stats.get('att3', 0) or 0 %}
|
||||
{{ detail_item('1v3 Win% (1v3胜率)', c3 / a3 if a3 > 0 else 0, 'clutch_rate_1v3', '{:.1%}', count_label=c3 ~ '/' ~ a3) }}
|
||||
|
||||
{% set c4 = l2_stats.get('c4', 0) or 0 %}
|
||||
{% set a4 = l2_stats.get('att4', 0) or 0 %}
|
||||
{{ detail_item('1v4 Win% (1v4胜率)', c4 / a4 if a4 > 0 else 0, 'clutch_rate_1v4', '{:.1%}', count_label=c4 ~ '/' ~ a4) }}
|
||||
|
||||
{% set c5 = l2_stats.get('c5', 0) or 0 %}
|
||||
{% set a5 = l2_stats.get('att5', 0) or 0 %}
|
||||
{{ detail_item('1v5 Win% (1v5胜率)', c5 / a5 if a5 > 0 else 0, 'clutch_rate_1v5', '{:.1%}', count_label=c5 ~ '/' ~ a5) }}
|
||||
|
||||
{% set mk_count = (l2_stats.get('k2', 0) or 0) + (l2_stats.get('k3', 0) or 0) + (l2_stats.get('k4', 0) or 0) + (l2_stats.get('k5', 0) or 0) %}
|
||||
{% set ma_count = (l2_stats.get('a2', 0) or 0) + (l2_stats.get('a3', 0) or 0) + (l2_stats.get('a4', 0) or 0) + (l2_stats.get('a5', 0) or 0) %}
|
||||
|
||||
{{ detail_item('Multi-Kill Rate', mk_count / rounds, 'l2_mk', '{:.1%}', count_label=mk_count) }}
|
||||
{{ detail_item('Multi-Assist Rate', ma_count / rounds, 'l2_ma', '{:.1%}', count_label=ma_count) }}
|
||||
{{ detail_item('Multi-K Rate (多杀率)', mk_count / rounds, 'total_multikill_rate', '{:.1%}', count_label=mk_count) }}
|
||||
{{ detail_item('Multi-A Rate (多助率)', ma_count / rounds, 'total_multiassist_rate', '{:.1%}', count_label=ma_count) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user