1.4.0: Updated Profile
This commit is contained in:
Binary file not shown.
@@ -98,6 +98,52 @@ CREATE TABLE IF NOT EXISTS dm_player_features (
|
|||||||
side_kd_diff_ct_t REAL, -- CT KD - T KD
|
side_kd_diff_ct_t REAL, -- CT KD - T KD
|
||||||
|
|
||||||
-- New Side Comparisons
|
-- New Side Comparisons
|
||||||
|
side_rating_diff_ct_t REAL,
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- 6. Party Size Performance
|
||||||
|
-- ==========================================
|
||||||
|
party_1_win_rate REAL,
|
||||||
|
party_1_rating REAL,
|
||||||
|
party_1_adr REAL,
|
||||||
|
|
||||||
|
party_2_win_rate REAL,
|
||||||
|
party_2_rating REAL,
|
||||||
|
party_2_adr REAL,
|
||||||
|
|
||||||
|
party_3_win_rate REAL,
|
||||||
|
party_3_rating REAL,
|
||||||
|
party_3_adr REAL,
|
||||||
|
|
||||||
|
party_4_win_rate REAL,
|
||||||
|
party_4_rating REAL,
|
||||||
|
party_4_adr REAL,
|
||||||
|
|
||||||
|
party_5_win_rate REAL,
|
||||||
|
party_5_rating REAL,
|
||||||
|
party_5_adr REAL,
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- 7. Rating Distribution (Performance Tiers)
|
||||||
|
-- ==========================================
|
||||||
|
rating_dist_carry_rate REAL, -- > 1.5
|
||||||
|
rating_dist_normal_rate REAL, -- 1.0 - 1.5
|
||||||
|
rating_dist_sacrifice_rate REAL, -- 0.6 - 1.0
|
||||||
|
rating_dist_sleeping_rate REAL, -- < 0.6
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- 8. ELO Stratification (Performance vs ELO)
|
||||||
|
-- ==========================================
|
||||||
|
elo_lt1200_rating REAL,
|
||||||
|
elo_1200_1400_rating REAL,
|
||||||
|
elo_1400_1600_rating REAL,
|
||||||
|
elo_1600_1800_rating REAL,
|
||||||
|
elo_1800_2000_rating REAL,
|
||||||
|
elo_gt2000_rating REAL,
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- 9. More Side Stats (Restored)
|
||||||
|
-- ==========================================
|
||||||
side_kast_ct REAL,
|
side_kast_ct REAL,
|
||||||
side_kast_t REAL,
|
side_kast_t REAL,
|
||||||
side_rws_ct REAL,
|
side_rws_ct REAL,
|
||||||
|
|||||||
@@ -978,6 +978,163 @@ class FeatureService:
|
|||||||
df['util_usage_rate'] = df['util_usage_rate_backup'].fillna(0)
|
df['util_usage_rate'] = df['util_usage_rate_backup'].fillna(0)
|
||||||
df.drop(columns=['util_usage_rate_backup'], inplace=True)
|
df.drop(columns=['util_usage_rate_backup'], inplace=True)
|
||||||
|
|
||||||
|
# --- 8. New Feature Dimensions (Party, Rating Dist, ELO) ---
|
||||||
|
# Fetch Base Data for Calculation
|
||||||
|
q_new_feats = f"""
|
||||||
|
SELECT mp.steam_id_64, mp.match_id, mp.match_team_id, mp.team_id,
|
||||||
|
mp.rating, mp.adr, mp.is_win
|
||||||
|
FROM fact_match_players mp
|
||||||
|
WHERE mp.steam_id_64 IN ({placeholders})
|
||||||
|
"""
|
||||||
|
df_base = pd.read_sql_query(q_new_feats, conn, params=valid_ids)
|
||||||
|
|
||||||
|
if not df_base.empty:
|
||||||
|
# 8.1 Party Size Stats
|
||||||
|
# Get party sizes for these matches
|
||||||
|
# We need to query party sizes for ALL matches involved
|
||||||
|
match_ids = df_base['match_id'].unique()
|
||||||
|
if len(match_ids) > 0:
|
||||||
|
match_id_ph = ','.join(['?'] * len(match_ids))
|
||||||
|
q_party_size = f"""
|
||||||
|
SELECT match_id, match_team_id, COUNT(*) as party_size
|
||||||
|
FROM fact_match_players
|
||||||
|
WHERE match_id IN ({match_id_ph}) AND match_team_id > 0
|
||||||
|
GROUP BY match_id, match_team_id
|
||||||
|
"""
|
||||||
|
# Split match_ids into chunks if too many
|
||||||
|
chunk_size = 900
|
||||||
|
party_sizes_list = []
|
||||||
|
for i in range(0, len(match_ids), chunk_size):
|
||||||
|
chunk = match_ids[i:i+chunk_size]
|
||||||
|
chunk_ph = ','.join(['?'] * len(chunk))
|
||||||
|
q_chunk = q_party_size.replace(match_id_ph, chunk_ph)
|
||||||
|
party_sizes_list.append(pd.read_sql_query(q_chunk, conn, params=list(chunk)))
|
||||||
|
|
||||||
|
if party_sizes_list:
|
||||||
|
df_party_sizes = pd.concat(party_sizes_list)
|
||||||
|
|
||||||
|
# Merge party size to base data
|
||||||
|
df_base_party = df_base.merge(df_party_sizes, on=['match_id', 'match_team_id'], how='left')
|
||||||
|
|
||||||
|
# Calculate Stats per Party Size (1-5)
|
||||||
|
# We want columns like party_1_win_rate, party_1_rating, party_1_adr
|
||||||
|
party_stats = df_base_party.groupby(['steam_id_64', 'party_size']).agg({
|
||||||
|
'is_win': 'mean',
|
||||||
|
'rating': 'mean',
|
||||||
|
'adr': 'mean'
|
||||||
|
}).reset_index()
|
||||||
|
|
||||||
|
# Pivot
|
||||||
|
pivoted_party = party_stats.pivot(index='steam_id_64', columns='party_size').reset_index()
|
||||||
|
|
||||||
|
# Flatten and rename
|
||||||
|
new_party_cols = ['steam_id_64']
|
||||||
|
for col in pivoted_party.columns:
|
||||||
|
if col[0] == 'steam_id_64': continue
|
||||||
|
metric, size = col
|
||||||
|
if size in [1, 2, 3, 4, 5]:
|
||||||
|
# metric is is_win, rating, adr
|
||||||
|
metric_name = 'win_rate' if metric == 'is_win' else metric
|
||||||
|
new_party_cols.append(f"party_{int(size)}_{metric_name}")
|
||||||
|
|
||||||
|
# Handle MultiIndex column flattening properly
|
||||||
|
# The pivot creates MultiIndex. We need to construct a flat DataFrame.
|
||||||
|
flat_data = {'steam_id_64': pivoted_party['steam_id_64']}
|
||||||
|
for size in [1, 2, 3, 4, 5]:
|
||||||
|
if size in pivoted_party['is_win'].columns:
|
||||||
|
flat_data[f"party_{size}_win_rate"] = pivoted_party['is_win'][size]
|
||||||
|
if size in pivoted_party['rating'].columns:
|
||||||
|
flat_data[f"party_{size}_rating"] = pivoted_party['rating'][size]
|
||||||
|
if size in pivoted_party['adr'].columns:
|
||||||
|
flat_data[f"party_{size}_adr"] = pivoted_party['adr'][size]
|
||||||
|
|
||||||
|
df_party_flat = pd.DataFrame(flat_data)
|
||||||
|
df = df.merge(df_party_flat, on='steam_id_64', how='left')
|
||||||
|
|
||||||
|
# 8.2 Rating Distribution
|
||||||
|
# rating_dist_carry_rate (>1.5), normal (1.0-1.5), sacrifice (0.6-1.0), sleeping (<0.6)
|
||||||
|
df_base['rating_tier'] = pd.cut(df_base['rating'],
|
||||||
|
bins=[-1, 0.6, 1.0, 1.5, 100],
|
||||||
|
labels=['sleeping', 'sacrifice', 'normal', 'carry'],
|
||||||
|
right=False) # <0.6, 0.6-<1.0, 1.0-<1.5, >=1.5 (wait, cut behavior)
|
||||||
|
# Standard cut: right=True by default (a, b]. We want:
|
||||||
|
# < 0.6
|
||||||
|
# 0.6 <= x < 1.0
|
||||||
|
# 1.0 <= x < 1.5
|
||||||
|
# >= 1.5
|
||||||
|
# So bins=[-inf, 0.6, 1.0, 1.5, inf], right=False -> [a, b)
|
||||||
|
df_base['rating_tier'] = pd.cut(df_base['rating'],
|
||||||
|
bins=[-float('inf'), 0.6, 1.0, 1.5, float('inf')],
|
||||||
|
labels=['sleeping', 'sacrifice', 'normal', 'carry'],
|
||||||
|
right=False)
|
||||||
|
|
||||||
|
# Wait, 1.5 should be Normal or Carry?
|
||||||
|
# User: >1.5 Carry, 1.0~1.5 Normal. So 1.5 is Normal? Or Carry?
|
||||||
|
# Usually inclusive on lower bound.
|
||||||
|
# 1.5 -> Carry (>1.5 usually means >= 1.5 or strictly >).
|
||||||
|
# "1.0~1.5 正常" implies [1.0, 1.5]. ">1.5 Carry" implies (1.5, inf).
|
||||||
|
# Let's assume >= 1.5 is Carry.
|
||||||
|
# So bins: (-inf, 0.6), [0.6, 1.0), [1.0, 1.5), [1.5, inf)
|
||||||
|
# right=False gives [a, b).
|
||||||
|
# So [1.5, inf) is correct for Carry.
|
||||||
|
|
||||||
|
dist_stats = df_base.groupby(['steam_id_64', 'rating_tier']).size().unstack(fill_value=0)
|
||||||
|
# Calculate rates
|
||||||
|
dist_stats = dist_stats.div(dist_stats.sum(axis=1), axis=0)
|
||||||
|
dist_stats.columns = [f"rating_dist_{c}_rate" for c in dist_stats.columns]
|
||||||
|
dist_stats = dist_stats.reset_index()
|
||||||
|
|
||||||
|
df = df.merge(dist_stats, on='steam_id_64', how='left')
|
||||||
|
|
||||||
|
# 8.3 ELO Stratification
|
||||||
|
# Fetch Match Teams ELO
|
||||||
|
if len(match_ids) > 0:
|
||||||
|
q_elo = f"""
|
||||||
|
SELECT match_id, group_id, group_origin_elo
|
||||||
|
FROM fact_match_teams
|
||||||
|
WHERE match_id IN ({match_id_ph})
|
||||||
|
"""
|
||||||
|
# Use chunking again
|
||||||
|
elo_list = []
|
||||||
|
for i in range(0, len(match_ids), chunk_size):
|
||||||
|
chunk = match_ids[i:i+chunk_size]
|
||||||
|
chunk_ph = ','.join(['?'] * len(chunk))
|
||||||
|
q_chunk = q_elo.replace(match_id_ph, chunk_ph)
|
||||||
|
elo_list.append(pd.read_sql_query(q_chunk, conn, params=list(chunk)))
|
||||||
|
|
||||||
|
if elo_list:
|
||||||
|
df_elo_teams = pd.concat(elo_list)
|
||||||
|
|
||||||
|
# Merge to get Opponent ELO
|
||||||
|
# Player has match_id, team_id.
|
||||||
|
# Join on match_id.
|
||||||
|
# Filter where group_id != team_id
|
||||||
|
df_merged_elo = df_base.merge(df_elo_teams, on='match_id', how='left')
|
||||||
|
df_merged_elo = df_merged_elo[df_merged_elo['group_id'] != df_merged_elo['team_id']]
|
||||||
|
|
||||||
|
# Now df_merged_elo has 'group_origin_elo' which is Opponent ELO
|
||||||
|
# Binning: <1200, 1200-1400, 1400-1600, 1600-1800, 1800-2000, >2000
|
||||||
|
# bins: [-inf, 1200, 1400, 1600, 1800, 2000, inf]
|
||||||
|
elo_bins = [-float('inf'), 1200, 1400, 1600, 1800, 2000, float('inf')]
|
||||||
|
elo_labels = ['lt1200', '1200_1400', '1400_1600', '1600_1800', '1800_2000', 'gt2000']
|
||||||
|
|
||||||
|
df_merged_elo['elo_bin'] = pd.cut(df_merged_elo['group_origin_elo'], bins=elo_bins, labels=elo_labels, right=False)
|
||||||
|
|
||||||
|
elo_stats = df_merged_elo.groupby(['steam_id_64', 'elo_bin']).agg({
|
||||||
|
'rating': 'mean'
|
||||||
|
}).unstack(fill_value=0) # We only need rating for now
|
||||||
|
|
||||||
|
# Rename columns
|
||||||
|
# elo_stats columns are MultiIndex (rating, bin).
|
||||||
|
# We want: elo_{bin}_rating
|
||||||
|
flat_elo_data = {'steam_id_64': elo_stats.index}
|
||||||
|
for bin_label in elo_labels:
|
||||||
|
if bin_label in elo_stats['rating'].columns:
|
||||||
|
flat_elo_data[f"elo_{bin_label}_rating"] = elo_stats['rating'][bin_label].values
|
||||||
|
|
||||||
|
df_elo_flat = pd.DataFrame(flat_elo_data)
|
||||||
|
df = df.merge(df_elo_flat, on='steam_id_64', how='left')
|
||||||
|
|
||||||
# Final Mappings
|
# Final Mappings
|
||||||
df['total_matches'] = df['matches_played']
|
df['total_matches'] = df['matches_played']
|
||||||
|
|
||||||
|
|||||||
@@ -648,7 +648,17 @@ class StatsService:
|
|||||||
'side_multikill_rate_ct', 'side_multikill_rate_t',
|
'side_multikill_rate_ct', 'side_multikill_rate_t',
|
||||||
'side_headshot_rate_ct', 'side_headshot_rate_t',
|
'side_headshot_rate_ct', 'side_headshot_rate_t',
|
||||||
'side_defuses_ct', 'side_plants_t',
|
'side_defuses_ct', 'side_plants_t',
|
||||||
'util_avg_nade_dmg', 'util_avg_flash_time', 'util_avg_flash_enemy', 'util_usage_rate'
|
'util_avg_nade_dmg', 'util_avg_flash_time', 'util_avg_flash_enemy', 'util_usage_rate',
|
||||||
|
# New: Party Size Stats
|
||||||
|
'party_1_win_rate', 'party_1_rating', 'party_1_adr',
|
||||||
|
'party_2_win_rate', 'party_2_rating', 'party_2_adr',
|
||||||
|
'party_3_win_rate', 'party_3_rating', 'party_3_adr',
|
||||||
|
'party_4_win_rate', 'party_4_rating', 'party_4_adr',
|
||||||
|
'party_5_win_rate', 'party_5_rating', 'party_5_adr',
|
||||||
|
# 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'
|
||||||
]
|
]
|
||||||
|
|
||||||
# Mapping for L2 legacy calls (if any) - mainly map 'rating' to 'basic_avg_rating' etc if needed
|
# Mapping for L2 legacy calls (if any) - mainly map 'rating' to 'basic_avg_rating' etc if needed
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700 mt-auto">
|
<footer class="bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700 mt-auto">
|
||||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||||
<p class="text-center text-sm text-gray-500">© 2026 YRTV CS2 Data Platform. All rights reserved.</p>
|
<p class="text-center text-sm text-gray-500">© 2026 YRTV Data Platform. All rights reserved. 赣ICP备2026001600号</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -194,97 +194,86 @@
|
|||||||
|
|
||||||
<!-- Tab: Head to Head -->
|
<!-- 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;">
|
<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="flex justify-between items-end mb-6">
|
||||||
<div class="overflow-x-auto">
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Head-to-Head Matrix</h3>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Shows <span class="font-bold text-green-600 bg-green-50 px-1 rounded">Kills</span> : <span class="font-bold text-red-500 bg-red-50 px-1 rounded">Deaths</span> interaction between players</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400 font-mono">
|
||||||
|
Row: Team 1 Players<br>
|
||||||
|
Col: Team 2 Players
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl border border-gray-200 dark:border-gray-700">
|
||||||
<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/50">
|
||||||
<tr>
|
<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>
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider bg-gray-50 dark:bg-slate-700/50 sticky left-0 z-10">
|
||||||
|
Team 1 \ Team 2
|
||||||
|
</th>
|
||||||
{% for victim in team2_players %}
|
{% for victim in team2_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 }}">
|
<th class="px-2 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-300 tracking-wider min-w-[80px]" title="{{ victim.username }}">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center group">
|
||||||
|
<div class="relative">
|
||||||
{% if victim.avatar_url %}
|
{% if victim.avatar_url %}
|
||||||
<img class="h-6 w-6 rounded-full mb-1" src="{{ victim.avatar_url }}">
|
<img class="h-8 w-8 rounded-full mb-1 border-2 border-transparent group-hover:border-yrtv-400 transition-all" src="{{ victim.avatar_url }}">
|
||||||
{% else %}
|
{% 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">
|
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border-2 border-yrtv-200 mb-1 group-hover:border-yrtv-400 transition-all">
|
||||||
{{ (victim.username or victim.steam_id_64)[:2] | upper }}
|
{{ (victim.username or victim.steam_id_64)[:2] | upper }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="truncate w-16 text-center">{{ victim.username or 'Player' }}</span>
|
</div>
|
||||||
|
<span class="truncate w-20 text-center font-bold text-gray-700 dark:text-gray-300 group-hover:text-yrtv-600 transition-colors text-[10px]">{{ victim.username or 'Player' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
{% endfor %}
|
{% 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-100 dark:divide-gray-700">
|
||||||
{% for killer in team1_players %}
|
{% for killer in team1_players %}
|
||||||
<tr>
|
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/30 transition-colors">
|
||||||
<td class="px-3 py-2 whitespace-nowrap font-medium text-gray-900 dark:text-white flex items-center">
|
<td class="px-4 py-3 whitespace-nowrap font-medium text-gray-900 dark:text-white bg-white dark:bg-slate-800 sticky left-0 z-10 border-r border-gray-100 dark:border-gray-700 shadow-sm">
|
||||||
|
<div class="flex items-center group">
|
||||||
{% if killer.avatar_url %}
|
{% if killer.avatar_url %}
|
||||||
<img class="h-6 w-6 rounded-full mr-2" src="{{ killer.avatar_url }}">
|
<img class="h-8 w-8 rounded-full mr-3 border-2 border-transparent group-hover:border-blue-400 transition-all" src="{{ killer.avatar_url }}">
|
||||||
{% else %}
|
{% 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">
|
<div class="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold text-xs border-2 border-blue-200 mr-3 group-hover:border-blue-400 transition-all">
|
||||||
{{ (killer.username or killer.steam_id_64)[:2] | upper }}
|
{{ (killer.username or killer.steam_id_64)[:2] | upper }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="truncate w-24">{{ killer.username or 'Player' }}</span>
|
<span class="truncate w-28 font-bold group-hover:text-blue-600 transition-colors">{{ killer.username or 'Player' }}</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{% for victim in team2_players %}
|
{% for victim in team2_players %}
|
||||||
|
<!-- Kills: Killer -> Victim -->
|
||||||
{% set kills = h2h_matrix.get(killer.steam_id_64, {}).get(victim.steam_id_64, 0) %}
|
{% 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
|
<!-- Deaths: Victim -> Killer (which is Killer's death) -->
|
||||||
{% if kills > 0 %}font-bold text-gray-900 dark:text-white{% else %}text-gray-300 dark:text-gray-600{% endif %}"
|
{% set deaths = h2h_matrix.get(victim.steam_id_64, {}).get(killer.steam_id_64, 0) %}
|
||||||
style="{% if kills > 0 %}background-color: rgba(239, 68, 68, {{ kills * 0.1 }}){% endif %}">
|
|
||||||
{{ kills if kills > 0 else '-' }}
|
<td class="px-2 py-3 text-center border-l border-gray-50 dark:border-gray-700/50">
|
||||||
</td>
|
<div class="flex items-center justify-center gap-1.5 font-mono">
|
||||||
{% endfor %}
|
<!-- Kills -->
|
||||||
</tr>
|
<span class="{% if kills > deaths %}font-black text-lg text-green-600{% elif kills > 0 %}font-bold text-gray-900 dark:text-white{% else %}text-gray-300 dark:text-gray-600 text-xs{% endif %}">
|
||||||
{% endfor %}
|
{{ kills }}
|
||||||
</tbody>
|
</span>
|
||||||
</table>
|
|
||||||
|
<span class="text-gray-300 dark:text-gray-600 text-[10px]">:</span>
|
||||||
|
|
||||||
|
<!-- Deaths -->
|
||||||
|
<span class="{% if deaths > kills %}font-black text-lg text-red-500{% elif deaths > 0 %}font-bold text-gray-900 dark:text-white{% else %}text-gray-300 dark:text-gray-600 text-xs{% endif %}">
|
||||||
|
{{ deaths }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-6 border-t border-gray-200 dark:border-gray-700"></div>
|
<!-- Interaction Bar (Optional visual) -->
|
||||||
|
{% if kills + deaths > 0 %}
|
||||||
<div class="overflow-x-auto">
|
<div class="w-full h-1 bg-gray-100 dark:bg-slate-700 rounded-full mt-1 overflow-hidden flex">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
{% set total = kills + deaths %}
|
||||||
<thead class="bg-gray-50 dark:bg-slate-700">
|
<div class="bg-green-500 h-full" style="width: {{ (kills / total * 100) }}%"></div>
|
||||||
<tr>
|
<div class="bg-red-500 h-full" style="width: {{ (deaths / total * 100) }}%"></div>
|
||||||
<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>
|
</div>
|
||||||
{% endif %}
|
{% 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>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -416,6 +416,65 @@
|
|||||||
{{ vs_item_val('Multi-Assist Rate (多助攻)', t_ma, ct_ma, '{:.1%}') }}
|
{{ vs_item_val('Multi-Assist Rate (多助攻)', t_ma, ct_ma, '{:.1%}') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New Section: Party & Stratification -->
|
||||||
|
<div>
|
||||||
|
<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">
|
||||||
|
👥 组排与分层表现 (Party & Stratification)
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
<!-- Group 1: Party Size -->
|
||||||
|
<div>
|
||||||
|
<h5 class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-3">Party Size Performance (组排表现)</h5>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
|
||||||
|
{{ detail_item('Solo Win% (单排胜率)', features['party_1_win_rate'], 'party_1_win_rate', '{:.1%}') }}
|
||||||
|
{{ detail_item('Solo Rating (单排分)', features['party_1_rating'], 'party_1_rating') }}
|
||||||
|
{{ detail_item('Solo ADR (单排伤)', features['party_1_adr'], 'party_1_adr', '{:.1f}') }}
|
||||||
|
|
||||||
|
{{ detail_item('Duo Win% (双排胜率)', features['party_2_win_rate'], 'party_2_win_rate', '{:.1%}') }}
|
||||||
|
{{ detail_item('Duo Rating (双排分)', features['party_2_rating'], 'party_2_rating') }}
|
||||||
|
{{ detail_item('Duo ADR (双排伤)', features['party_2_adr'], 'party_2_adr', '{:.1f}') }}
|
||||||
|
|
||||||
|
{{ detail_item('Trio Win% (三排胜率)', features['party_3_win_rate'], 'party_3_win_rate', '{:.1%}') }}
|
||||||
|
{{ detail_item('Trio Rating (三排分)', features['party_3_rating'], 'party_3_rating') }}
|
||||||
|
{{ detail_item('Trio ADR (三排伤)', features['party_3_adr'], 'party_3_adr', '{:.1f}') }}
|
||||||
|
|
||||||
|
{{ detail_item('Quad Win% (四排胜率)', features['party_4_win_rate'], 'party_4_win_rate', '{:.1%}') }}
|
||||||
|
{{ detail_item('Quad Rating (四排分)', features['party_4_rating'], 'party_4_rating') }}
|
||||||
|
{{ detail_item('Quad ADR (四排伤)', features['party_4_adr'], 'party_4_adr', '{:.1f}') }}
|
||||||
|
|
||||||
|
{{ detail_item('Full Win% (五排胜率)', features['party_5_win_rate'], 'party_5_win_rate', '{:.1%}') }}
|
||||||
|
{{ detail_item('Full Rating (五排分)', features['party_5_rating'], 'party_5_rating') }}
|
||||||
|
{{ detail_item('Full ADR (五排伤)', features['party_5_adr'], 'party_5_adr', '{:.1f}') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group 2: Rating Distribution -->
|
||||||
|
<div>
|
||||||
|
<h5 class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-3">Performance Tiers (表现分层)</h5>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-y-6 gap-x-4">
|
||||||
|
{{ detail_item('Carry Rate (>1.5)', features['rating_dist_carry_rate'], 'rating_dist_carry_rate', '{:.1%}') }}
|
||||||
|
{{ detail_item('Normal Rate (1.0-1.5)', features['rating_dist_normal_rate'], 'rating_dist_normal_rate', '{:.1%}') }}
|
||||||
|
{{ detail_item('Sacrifice Rate (0.6-1.0)', features['rating_dist_sacrifice_rate'], 'rating_dist_sacrifice_rate', '{:.1%}') }}
|
||||||
|
{{ detail_item('Sleeping Rate (<0.6)', features['rating_dist_sleeping_rate'], 'rating_dist_sleeping_rate', '{:.1%}') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group 3: ELO Stratification -->
|
||||||
|
<div>
|
||||||
|
<h5 class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-3">Performance vs ELO (不同分段表现)</h5>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-y-6 gap-x-4">
|
||||||
|
{{ detail_item('<1200 Rating', features['elo_lt1200_rating'], 'elo_lt1200_rating') }}
|
||||||
|
{{ detail_item('1200-1400 Rating', features['elo_1200_1400_rating'], 'elo_1200_1400_rating') }}
|
||||||
|
{{ detail_item('1400-1600 Rating', features['elo_1400_1600_rating'], 'elo_1400_1600_rating') }}
|
||||||
|
{{ detail_item('1600-1800 Rating', features['elo_1600_1800_rating'], 'elo_1600_1800_rating') }}
|
||||||
|
{{ detail_item('1800-2000 Rating', features['elo_1800_2000_rating'], 'elo_1800_2000_rating') }}
|
||||||
|
{{ detail_item('>2000 Rating', features['elo_gt2000_rating'], 'elo_gt2000_rating') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user