From d8b70c1cf7241915ae04224ba34b179ed1b74617 Mon Sep 17 00:00:00 2001 From: Jacky Yang Date: Mon, 26 Jan 2026 02:53:31 +0800 Subject: [PATCH] 1.0.2-hotfix: Added matchlist new features. --- database/Web/Web_App.sqlite | Bin 36864 -> 36864 bytes web/routes/matches.py | 61 ++++++++++++- web/services/stats_service.py | 132 ++++++++++++++++++++++++++--- web/templates/matches/detail.html | 62 ++++++++++++-- web/templates/matches/list.html | 65 ++++++++++++-- web/templates/players/list.html | 8 +- web/templates/players/profile.html | 25 +++++- 7 files changed, 320 insertions(+), 33 deletions(-) diff --git a/database/Web/Web_App.sqlite b/database/Web/Web_App.sqlite index 0921300eb2707e52181f972f40f9058c64e42d25..9daf78508a361173e51ab2c7d6401b9724c2d6b9 100644 GIT binary patch delta 194 zcmZozz|^pSX@WGP)HZuk=gW2`~SDX^h diff --git a/web/routes/matches.py b/web/routes/matches.py index e1f2819..3fdafed 100644 --- a/web/routes/matches.py +++ b/web/routes/matches.py @@ -23,11 +23,66 @@ def detail(match_id): return "Match not found", 404 players = StatsService.get_match_players(match_id) + # Convert sqlite3.Row objects to dicts to allow modification + players = [dict(p) for p in players] + rounds = StatsService.get_match_rounds(match_id) - # Organize players by team - team1_players = [p for p in players if p['team_id'] == 1] - team2_players = [p for p in players if p['team_id'] == 2] + # --- Roster Identification --- + # Fetch active roster to identify "Our Team" players + from web.services.web_service import WebService + lineups = WebService.get_lineups() + # Assume we use the first/active lineup + active_roster_ids = [] + if lineups: + try: + active_roster_ids = json.loads(lineups[0]['player_ids_json']) + except: + pass + + # Mark roster players (Ensure strict string comparison) + roster_set = set(str(uid) for uid in active_roster_ids) + for p in players: + p['is_in_roster'] = str(p['steam_id_64']) in roster_set + + # --- Party Size Calculation --- + # Only calculate party size for OUR ROSTER members. + # Group roster members by match_team_id + roster_parties = {} # match_team_id -> count of roster members + + for p in players: + if p['is_in_roster']: + mtid = p.get('match_team_id') + if mtid and mtid > 0: + key = f"tid_{mtid}" + roster_parties[key] = roster_parties.get(key, 0) + 1 + + # Assign party size ONLY to roster members + for p in players: + if p['is_in_roster']: + mtid = p.get('match_team_id') + if mtid and mtid > 0: + p['party_size'] = roster_parties.get(f"tid_{mtid}", 1) + else: + p['party_size'] = 1 # Solo roster player + else: + p['party_size'] = 0 # Hide party info for non-roster players + + # Organize players by Side (team_id) + # team_id 1 = Team 1, team_id 2 = Team 2 + # Note: group_id 1/2 usually corresponds to Team 1/2. + # Fallback to team_id if group_id is missing or 0 (legacy data compatibility) + team1_players = [p for p in players if p.get('group_id') == 1] + team2_players = [p for p in players if p.get('group_id') == 2] + + # If group_id didn't work (empty lists), try team_id grouping (if team_id is 1/2 only) + if not team1_players and not team2_players: + team1_players = [p for p in players if p['team_id'] == 1] + team2_players = [p for p in players if p['team_id'] == 2] + + # Explicitly sort by Rating DESC + 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) return render_template('matches/detail.html', match=match, team1_players=team1_players, team2_players=team2_players, diff --git a/web/services/stats_service.py b/web/services/stats_service.py index cc42527..0c70ec3 100644 --- a/web/services/stats_service.py +++ b/web/services/stats_service.py @@ -44,6 +44,109 @@ class StatsService: matches = query_db('l2', sql, args) + # Enrich matches with Avg ELO, Party info, and Our Team Result + if matches: + match_ids = [m['match_id'] for m in matches] + placeholders = ','.join('?' for _ in match_ids) + + # Fetch ELO + elo_sql = f""" + SELECT match_id, AVG(group_origin_elo) as avg_elo + FROM fact_match_teams + WHERE match_id IN ({placeholders}) AND group_origin_elo > 0 + GROUP BY match_id + """ + elo_rows = query_db('l2', elo_sql, match_ids) + elo_map = {row['match_id']: row['avg_elo'] for row in elo_rows} + + # Fetch Max Party Size + party_sql = f""" + SELECT match_id, MAX(cnt) as max_party + FROM ( + SELECT match_id, match_team_id, COUNT(*) as cnt + FROM fact_match_players + WHERE match_id IN ({placeholders}) AND match_team_id > 0 + GROUP BY match_id, match_team_id + ) + GROUP BY match_id + """ + party_rows = query_db('l2', party_sql, match_ids) + party_map = {row['match_id']: row['max_party'] for row in party_rows} + + # --- New: Determine "Our Team" Result --- + # Logic: Check if any player from `active_roster` played in these matches. + # Use WebService to get the active roster + from web.services.web_service import WebService + import json + + lineups = WebService.get_lineups() + active_roster_ids = [] + if lineups: + try: + # Load IDs and ensure they are all strings for DB comparison consistency + raw_ids = json.loads(lineups[0]['player_ids_json']) + active_roster_ids = [str(uid) for uid in raw_ids] + except: + pass + + # If no roster, we can't determine "Our Result" + if not active_roster_ids: + result_map = {} + else: + roster_placeholders = ','.join('?' for _ in active_roster_ids) + + # We cast steam_id_64 to TEXT to ensure match even if stored as int + our_result_sql = f""" + SELECT mp.match_id, mp.team_id, m.winner_team, COUNT(*) as our_count + FROM fact_match_players mp + 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 + """ + + # Combine args: match_ids + roster_ids + combined_args = match_ids + active_roster_ids + our_rows = query_db('l2', our_result_sql, combined_args) + + # Map match_id -> result ('win', 'loss', 'draw', 'mixed') + result_map = {} + + match_sides = {} + match_winners = {} + + for r in our_rows: + mid = r['match_id'] + if mid not in match_sides: match_sides[mid] = {} + match_sides[mid][r['team_id']] = r['our_count'] + match_winners[mid] = r['winner_team'] + + for mid, sides in match_sides.items(): + winner = match_winners.get(mid) + 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: + result_map[mid] = 'win' + elif our_on_loser > 0 and our_on_winner == 0: + result_map[mid] = 'loss' + elif our_on_winner > 0 and our_on_loser > 0: + result_map[mid] = 'mixed' + else: + result_map[mid] = None + + # 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']) + # Count total for pagination count_sql = f"SELECT COUNT(*) as cnt FROM fact_matches WHERE {where_str}" total = query_db('l2', count_sql, args[:-2], one=True)['cnt'] @@ -243,20 +346,25 @@ class StatsService: @staticmethod def get_player_trend(steam_id, limit=20): - sql = """ - SELECT m.start_time, mp.rating, mp.kd_ratio, mp.adr, m.match_id, m.map_name - FROM fact_match_players mp - JOIN fact_matches m ON mp.match_id = m.match_id - WHERE mp.steam_id_64 = ? - ORDER BY m.start_time ASC - """ - # We fetch all then slice last 'limit' in python or use subquery. - # DESC LIMIT gets recent, but we want chronological for chart. - # So: SELECT ... ORDER BY time DESC LIMIT ? -> then reverse in code. - + # We need party_size: count of players with same match_team_id in the same match + # Using a correlated subquery for party_size sql = """ SELECT * FROM ( - SELECT m.start_time, mp.rating, mp.kd_ratio, mp.adr, m.match_id, m.map_name, mp.is_win + SELECT + m.start_time, + mp.rating, + mp.kd_ratio, + mp.adr, + m.match_id, + m.map_name, + mp.is_win, + mp.match_team_id, + (SELECT COUNT(*) + FROM fact_match_players p2 + WHERE p2.match_id = mp.match_id + AND p2.match_team_id = mp.match_team_id + AND p2.match_team_id > 0 -- Ensure we don't count 0 (solo default) as a massive party + ) as party_size FROM fact_match_players mp JOIN fact_matches m ON mp.match_id = m.match_id WHERE mp.steam_id_64 = ? diff --git a/web/templates/matches/detail.html b/web/templates/matches/detail.html index 4731782..591e6be 100644 --- a/web/templates/matches/detail.html +++ b/web/templates/matches/detail.html @@ -47,12 +47,35 @@
- + {% if p.avatar_url %} + + {% else %} +
+ {{ (p.username or p.steam_id_64)[:2] | upper }} +
+ {% endif %}
- - {{ p.username or p.steam_id_64 }} - +
+ + {{ p.username or p.steam_id_64 }} + + {% 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 %} + + + + + {{ p.party_size }} + + {% endif %} +
@@ -97,12 +120,35 @@
- + {% if p.avatar_url %} + + {% else %} +
+ {{ (p.username or p.steam_id_64)[:2] | upper }} +
+ {% endif %}
- - {{ p.username or p.steam_id_64 }} - +
+ + {{ p.username or p.steam_id_64 }} + + {% 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 %} + + + + + {{ p.party_size }} + + {% endif %} +
diff --git a/web/templates/matches/list.html b/web/templates/matches/list.html index a7a1f09..808b5cf 100644 --- a/web/templates/matches/list.html +++ b/web/templates/matches/list.html @@ -17,6 +17,8 @@ 时间 地图 比分 + ELO + Party 时长 操作 @@ -31,13 +33,62 @@ {{ match.map_name }} - - {{ match.score_team1 }} - - - - - {{ match.score_team2 }} - +
+ + {{ match.score_team1 }} + {% if match.winner_team == 1 %} + + {% endif %} + + - + + {{ match.score_team2 }} + {% if match.winner_team == 2 %} + + {% endif %} + + + + {% if match.our_result %} + {% if match.our_result == 'win' %} + + VICTORY + + {% elif match.our_result == 'loss' %} + + DEFEAT + + {% elif match.our_result == 'mixed' %} + + CIVIL WAR + + {% endif %} + {% endif %} +
+ + + {% if match.avg_elo and match.avg_elo > 0 %} + {{ "%.0f"|format(match.avg_elo) }} + {% else %} + - + {% endif %} + + + {% if match.max_party and match.max_party > 1 %} + {% set p = match.max_party %} + {% set party_class = 'bg-gray-100 text-gray-800' %} + {% if p == 2 %} {% set party_class = 'bg-indigo-100 text-indigo-800 border border-indigo-200' %} + {% elif p == 3 %} {% set party_class = 'bg-blue-100 text-blue-800 border border-blue-200' %} + {% elif p == 4 %} {% set party_class = 'bg-purple-100 text-purple-800 border border-purple-200' %} + {% elif p >= 5 %} {% set party_class = 'bg-orange-100 text-orange-800 border border-orange-200' %} + {% endif %} + + + 👥 {{ match.max_party }} + + {% else %} + Solo + {% endif %} {{ (match.duration / 60) | int }} min diff --git a/web/templates/players/list.html b/web/templates/players/list.html index beb8c69..eb663ba 100644 --- a/web/templates/players/list.html +++ b/web/templates/players/list.html @@ -26,10 +26,14 @@
{% for player in players %}
- -
+ + {% if player.avatar_url %} + {{ player.username }} + {% else %} +
{{ player.username[:2] | upper if player.username else '??' }}
+ {% endif %}

{{ player.username }}

{{ player.steam_id_64 }}

diff --git a/web/templates/players/profile.html b/web/templates/players/profile.html index b40306b..260a9cf 100644 --- a/web/templates/players/profile.html +++ b/web/templates/players/profile.html @@ -150,6 +150,8 @@ Date Map + Result + Party Rating K/D ADR @@ -157,12 +159,33 @@ - {% for m in history %} + {% for m in history | reverse %} {{ m.map_name }} + + + {{ 'WIN' if m.is_win else 'LOSS' }} + + + + {% if m.party_size and m.party_size > 1 %} + {% set p = m.party_size %} + {% set party_class = 'bg-gray-100 text-gray-800' %} + {% if p == 2 %} {% set party_class = 'bg-indigo-100 text-indigo-800' %} + {% elif p == 3 %} {% set party_class = 'bg-blue-100 text-blue-800' %} + {% elif p == 4 %} {% set party_class = 'bg-purple-100 text-purple-800' %} + {% elif p >= 5 %} {% set party_class = 'bg-orange-100 text-orange-800' %} + {% endif %} + + 👥 {{ m.party_size }} + + {% else %} + Solo + {% endif %} + {{ "%.2f"|format(m.rating or 0) }} {{ "%.2f"|format(m.kd_ratio or 0) }} {{ "%.1f"|format(m.adr or 0) }}