diff --git a/database/L3/L3.db b/database/L3/L3.db index 8bb80ea..5fc8a70 100644 Binary files a/database/L3/L3.db and b/database/L3/L3.db differ diff --git a/database/L3/L3_Builder.py b/database/L3/L3_Builder.py index 526fa19..40743c4 100644 --- a/database/L3/L3_Builder.py +++ b/database/L3/L3_Builder.py @@ -239,7 +239,7 @@ def main(force_all: bool = False, workers: int = 1): result["last_match_date"], ) success_count += 1 - if processed_count % 4 == 0: + if processed_count % 2 == 0: conn_l3.commit() logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)") else: @@ -267,7 +267,7 @@ def main(force_all: bool = False, workers: int = 1): continue processed_count = idx - if processed_count % 4 == 0: + if processed_count % 2 == 0: conn_l3.commit() logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)") diff --git a/web/routes/players.py b/web/routes/players.py index a0f9d65..d05cbb4 100644 --- a/web/routes/players.py +++ b/web/routes/players.py @@ -6,6 +6,7 @@ from web.database import execute_db, query_db from web.config import Config from datetime import datetime import os +import json from werkzeug.utils import secure_filename bp = Blueprint('players', __name__, url_prefix='/players') @@ -231,6 +232,41 @@ def charts_data(steam_id): radar_data = {} radar_dist = FeatureService.get_roster_features_distribution(steam_id) + # Task 1: Strict Team Average Calculation + team_avg_radar = None + lineups = WebService.get_lineups() + if lineups: + target_lineup = None + try: + p_ids = [str(i) for i in json.loads(lineups[0].get("player_ids_json") or "[]")] + if str(steam_id) in p_ids: + target_lineup = p_ids + except: + target_lineup = None + + if target_lineup: + # Calculate strict average for this lineup + team_sums = { + 'score_aim': 0.0, 'score_defense': 0.0, 'score_utility': 0.0, + 'score_clutch': 0.0, 'score_economy': 0.0, 'score_pace': 0.0, + 'score_pistol': 0.0, 'score_stability': 0.0 + } + member_count = 0 + + for member_id in target_lineup: + mf = FeatureService.get_player_features(member_id) + if mf: + member_count += 1 + for k in team_sums: + team_sums[k] += float(mf.get(k) or 0.0) + + if member_count > 0: + team_avg_radar = {k: v / member_count for k, v in team_sums.items()} + # Fallback: if calculated avg is all zeros (e.g. teammates have no stats), + # treat as None to trigger global fallback in frontend + if sum(team_avg_radar.values()) == 0: + team_avg_radar = None + if features: # Dimensions: AIM, DEFENSE, UTILITY, CLUTCH, ECONOMY, PACE (6 Dimensions) # Use calculated scores (0-100 scale) @@ -266,7 +302,8 @@ def charts_data(steam_id): return jsonify({ 'trend': {'labels': trend_labels, 'values': trend_values}, 'radar': radar_data, - 'radar_dist': radar_dist + 'radar_dist': radar_dist, + 'team_avg_radar': team_avg_radar }) # --- API for Comparison --- @@ -297,14 +334,15 @@ def api_batch_stats(): # 1. Radar Scores (Normalized 0-100) # Use safe conversion with default 0 if None - # Force 0.0 if value is 0 or None to ensure JSON compatibility radar = { - 'STA': float(f.get('score_sta') or 0.0), - 'BAT': float(f.get('score_bat') or 0.0), - 'HPS': float(f.get('score_hps') or 0.0), - 'PTL': float(f.get('score_ptl') or 0.0), - 'SIDE': float(f.get('score_tct') or 0.0), - 'UTIL': float(f.get('score_util') or 0.0) + 'AIM': float(f.get('score_aim') or 0.0), + 'DEFENSE': float(f.get('score_defense') or 0.0), + 'UTILITY': float(f.get('score_utility') or 0.0), + 'CLUTCH': float(f.get('score_clutch') or 0.0), + 'ECONOMY': float(f.get('score_economy') or 0.0), + 'PACE': float(f.get('score_pace') or 0.0), + 'PISTOL': float(f.get('score_pistol') or 0.0), + 'STABILITY': float(f.get('score_stability') or 0.0) } # 2. Basic Stats for Table @@ -347,22 +385,22 @@ def api_batch_stats(): 'first_kill_ct': float(f.get('side_first_kill_rate_ct') or 0), # Row 3 - 'first_death_t': float(f.get('side_first_death_rate_t') or 0), - 'first_death_ct': float(f.get('side_first_death_rate_ct') or 0), + 'first_death_t': float(f.get('tac_fd_rate') or 0), + 'first_death_ct': float(f.get('tac_fd_rate') or 0), 'kast_t': float(f.get('side_kast_t') or 0), 'kast_ct': float(f.get('side_kast_ct') or 0), # Row 4 - 'rws_t': float(f.get('side_rws_t') or 0), - 'rws_ct': float(f.get('side_rws_ct') or 0), - 'multikill_t': float(f.get('side_multikill_rate_t') or 0), - 'multikill_ct': float(f.get('side_multikill_rate_ct') or 0), + 'rws_t': float(f.get('core_avg_rws') or 0), + 'rws_ct': float(f.get('core_avg_rws') or 0), + 'multikill_t': float(f.get('tac_multikill_rate') or 0), + 'multikill_ct': float(f.get('tac_multikill_rate') or 0), # Row 5 - 'hs_t': float(f.get('side_headshot_rate_t') or 0), - 'hs_ct': float(f.get('side_headshot_rate_ct') or 0), - 'obj_t': float(f.get('side_obj_t') or 0), - 'obj_ct': float(f.get('side_obj_ct') or 0) + 'hs_t': float(f.get('core_hs_rate') or 0), + 'hs_ct': float(f.get('core_hs_rate') or 0), + 'obj_t': float(f.get('core_avg_plants') or 0), + 'obj_ct': float(f.get('core_avg_defuses') or 0) } stats.append({ diff --git a/web/routes/tactics.py b/web/routes/tactics.py index dcf5d5c..0a2c746 100644 --- a/web/routes/tactics.py +++ b/web/routes/tactics.py @@ -52,6 +52,41 @@ def api_analyze(): 'kd': total_kd / count if count else 0, 'adr': total_adr / count if count else 0 } + + # Calculate 8-Dimension Averages + radar_keys = { + 'score_aim': 'AIM', 'score_defense': 'DEFENSE', 'score_utility': 'UTILITY', + 'score_clutch': 'CLUTCH', 'score_economy': 'ECONOMY', 'score_pace': 'PACE', + 'score_pistol': 'PISTOL', 'score_stability': 'STABILITY' + } + radar_stats = {v: 0.0 for v in radar_keys.values()} + + if count > 0: + for p in player_data: + stats = p.get('stats', {}) + for k, v in radar_keys.items(): + radar_stats[v] += float(stats.get(k) or 0.0) + + for k in radar_stats: + radar_stats[k] /= count + + # Calculate Chemistry + # Formula: Base on shared matches and win rate + # Max Score = 100 + # 50% weight on match count (Cap at 50 matches = 50 pts) + # 50% weight on win rate (100% WR = 50 pts) + + avg_shared_count = 0 + avg_shared_winrate = 0 + + if shared_matches: + avg_shared_count = len(shared_matches) + wins = sum(1 for m in shared_matches if m['is_win']) + avg_shared_winrate = wins / len(shared_matches) + + chem_match_score = min(50, avg_shared_count) # 1 point per match, max 50 + chem_win_score = avg_shared_winrate * 50 + chemistry_score = chem_match_score + chem_win_score # 4. Map Stats Calculation map_stats = {} # {map_name: {'count': 0, 'wins': 0}} @@ -84,6 +119,8 @@ def api_analyze(): 'players': player_data, 'shared_matches': [dict(m) for m in shared_matches], 'avg_stats': avg_stats, + 'radar_stats': radar_stats, + 'chemistry_score': chemistry_score, 'map_stats': map_stats_list, 'total_shared_matches': total_shared_matches }) diff --git a/web/services/feature_service.py b/web/services/feature_service.py index 115cb30..99eedf8 100644 --- a/web/services/feature_service.py +++ b/web/services/feature_service.py @@ -166,33 +166,13 @@ class FeatureService: lineups = WebService.get_lineups() roster_ids: list[str] = [] - # Try to find a lineup containing this player if lineups: - for lineup in lineups: - try: - p_ids = [str(i) for i in json.loads(lineup.get("player_ids_json") or "[]")] - if str(target_steam_id) in p_ids: - roster_ids = p_ids - break - except Exception: - continue - - # If not found in any lineup, use the most recent lineup as a fallback context - if not roster_ids and lineups: - try: - roster_ids = [str(i) for i in json.loads(lineups[0].get("player_ids_json") or "[]")] - except Exception: - roster_ids = [] - - # If still no roster (e.g. no lineups at all), fallback to a "Global Context" (Top 50 active players) - # This ensures we always have a distribution to compare against - if not roster_ids: - rows = query_db("l3", "SELECT steam_id_64 FROM dm_player_features ORDER BY last_match_date DESC LIMIT 50") - roster_ids = [str(r['steam_id_64']) for r in rows] if rows else [] - - # Ensure target player is in the list - if str(target_steam_id) not in roster_ids: - roster_ids.append(str(target_steam_id)) + try: + p_ids = [str(i) for i in json.loads(lineups[0].get("player_ids_json") or "[]")] + if str(target_steam_id) in p_ids: + roster_ids = p_ids + except Exception: + roster_ids = [] if not roster_ids: return None diff --git a/web/templates/players/list.html b/web/templates/players/list.html index eb663ba..b508d1e 100644 --- a/web/templates/players/list.html +++ b/web/templates/players/list.html @@ -40,7 +40,7 @@
- {{ "%.2f"|format(player.basic_avg_rating|default(0)) }} + {{ "%.2f"|format(player.core_avg_rating2|default(player.basic_avg_rating)|default(0)) }} Rating
diff --git a/web/templates/players/profile.html b/web/templates/players/profile.html index bace570..538bff5 100644 --- a/web/templates/players/profile.html +++ b/web/templates/players/profile.html @@ -869,6 +869,7 @@ document.addEventListener('DOMContentLoaded', function() { // Prepare Distribution Data const dist = data.radar_dist || {}; + const hasDist = Object.keys(dist).length > 0; const getDist = (key) => dist[key] || { rank: '?', avg: 0 }; // Map friendly names to keys @@ -877,41 +878,49 @@ document.addEventListener('DOMContentLoaded', function() { const rawLabels = ['枪法 (Aim)', '生存 (Defense)', '道具 (Utility)', '残局 (Clutch)', '经济 (Economy)', '节奏 (Pace)', '手枪 (Pistol)', '稳定 (Stability)']; const labels = rawLabels.map((l, i) => { + if (!hasDist) return l; const k = keys[i]; const d = getDist(k); return `${l} #${d.rank}`; }); - const teamAvgs = keys.map(k => getDist(k).avg); + let teamAvgs; + if (data.team_avg_radar) { + teamAvgs = keys.map(k => data.team_avg_radar[k] || 0); + } + + const datasets = [{ + label: 'Player', + data: [ + data.radar.AIM, data.radar.DEFENSE, data.radar.UTILITY, + data.radar.CLUTCH, data.radar.ECONOMY, data.radar.PACE, + data.radar.PISTOL, data.radar.STABILITY + ], + backgroundColor: 'rgba(124, 58, 237, 0.2)', + borderColor: '#7c3aed', + borderWidth: 2, + pointBackgroundColor: '#7c3aed', + pointBorderColor: '#fff', + pointHoverBackgroundColor: '#fff', + pointHoverBorderColor: '#7c3aed' + }]; + if (teamAvgs) { + datasets.push({ + label: 'Team Avg', + data: teamAvgs, + backgroundColor: 'rgba(148, 163, 184, 0.2)', + borderColor: '#94a3b8', + borderWidth: 2, + pointRadius: 0, + borderDash: [5, 5] + }); + } new Chart(ctxRadar, { type: 'radar', data: { labels: labels, - datasets: [{ - label: 'Player', - data: [ - data.radar.AIM, data.radar.DEFENSE, data.radar.UTILITY, - data.radar.CLUTCH, data.radar.ECONOMY, data.radar.PACE, - data.radar.PISTOL, data.radar.STABILITY - ], - backgroundColor: 'rgba(124, 58, 237, 0.2)', - borderColor: '#7c3aed', - borderWidth: 2, - pointBackgroundColor: '#7c3aed', - pointBorderColor: '#fff', - pointHoverBackgroundColor: '#fff', - pointHoverBorderColor: '#7c3aed' - }, - { - label: 'Team Avg', - data: teamAvgs, - backgroundColor: 'rgba(148, 163, 184, 0.2)', // Slate-400 - borderColor: '#94a3b8', - borderWidth: 2, - pointRadius: 0, - borderDash: [5, 5] - }] + datasets: datasets }, options: { plugins: { diff --git a/web/templates/tactics/index.html b/web/templates/tactics/index.html index 24f172a..fb23251 100644 --- a/web/templates/tactics/index.html +++ b/web/templates/tactics/index.html @@ -120,7 +120,7 @@
- Rating: + Rating:
@@ -153,6 +153,15 @@ Team Rating
+
+ Chemistry Score + +
+ + + +
+
@@ -336,6 +345,7 @@ function tacticsApp() { // Analysis State analysisLineup: [], analysisResult: null, + analysisChart: null, debounceTimer: null, // Data Center State @@ -410,7 +420,8 @@ function tacticsApp() { steam_id_64: player.steam_id_64, username: player.username || player.name, name: player.name || player.username, - avatar_url: player.avatar_url + avatar_url: player.avatar_url, + stats: player.stats || { basic_avg_rating: 0.0 } // Include stats for drag preview }; event.dataTransfer.setData('text/plain', JSON.stringify(payload)); event.dataTransfer.effectAllowed = 'copy'; @@ -529,8 +540,9 @@ function tacticsApp() { const datasets = rawData.map((p, idx) => { const color = this.getPlayerColor(idx); const d = [ - p.radar.BAT || 0, p.radar.PTL || 0, p.radar.HPS || 0, - p.radar.SIDE || 0, p.radar.UTIL || 0, p.radar.STA || 0 + p.radar.AIM || 0, p.radar.DEFENSE || 0, p.radar.UTILITY || 0, + p.radar.CLUTCH || 0, p.radar.ECONOMY || 0, p.radar.PACE || 0, + p.radar.PISTOL || 0, p.radar.STABILITY || 0 ]; return { @@ -548,7 +560,7 @@ function tacticsApp() { this.radarChart = new Chart(ctx, { type: 'radar', data: { - labels: ['BAT (火力)', 'PTL (手枪)', 'HPS (抗压)', 'SIDE (阵营)', 'UTIL (道具)', 'STA (稳定)'], + labels: ['AIM (枪法)', 'DEF (生存)', 'UTIL (道具)', 'CLUTCH (残局)', 'ECO (经济)', 'PACE (节奏)', 'PISTOL (手枪)', 'STA (稳定)'], datasets: datasets }, options: { @@ -595,7 +607,7 @@ function tacticsApp() { this.radarChart = new Chart(ctx, { type: 'radar', data: { - labels: ['BAT (火力)', 'PTL (手枪)', 'HPS (抗压)', 'SIDE (阵营)', 'UTIL (道具)', 'STA (稳定)'], + labels: ['AIM (枪法)', 'DEF (生存)', 'UTIL (道具)', 'CLUTCH (残局)', 'ECO (经济)', 'PACE (节奏)', 'PISTOL (手枪)', 'STA (稳定)'], datasets: [] }, options: { @@ -659,6 +671,59 @@ function tacticsApp() { .then(res => res.json()) .then(data => { this.analysisResult = data; + this.$nextTick(() => { + this.updateAnalysisChart(); + }); + }); + }, + + updateAnalysisChart() { + if (this.analysisChart) { + this.analysisChart.destroy(); + this.analysisChart = null; + } + + const canvas = document.getElementById('analysisRadarChart'); + if (!canvas || !this.analysisResult || !this.analysisResult.radar_stats) return; + + const stats = this.analysisResult.radar_stats; + const data = [ + stats.AIM || 0, stats.DEFENSE || 0, stats.UTILITY || 0, + stats.CLUTCH || 0, stats.ECONOMY || 0, stats.PACE || 0, + stats.PISTOL || 0, stats.STABILITY || 0 + ]; + + const ctx = canvas.getContext('2d'); + this.analysisChart = new Chart(ctx, { + type: 'radar', + data: { + labels: ['AIM (枪法)', 'DEF (生存)', 'UTIL (道具)', 'CLUTCH (残局)', 'ECO (经济)', 'PACE (节奏)', 'PISTOL (手枪)', 'STA (稳定)'], + datasets: [{ + label: 'Team Average', + data: data, + backgroundColor: 'rgba(59, 130, 246, 0.2)', + borderColor: '#3b82f6', + borderWidth: 2, + pointRadius: 3 + }] + }, + options: { + maintainAspectRatio: false, + scales: { + r: { + min: 0, max: 100, + ticks: { display: false, stepSize: 20 }, + pointLabels: { + font: { size: 11, weight: 'bold' }, + color: (ctx) => document.documentElement.classList.contains('dark') ? '#cbd5e1' : '#374151' + }, + grid: { + color: (ctx) => document.documentElement.classList.contains('dark') ? 'rgba(51, 65, 85, 0.5)' : 'rgba(229, 231, 235, 0.8)' + } + } + }, + plugins: { legend: { display: false } } + } }); }, diff --git a/web/templates/teams/clubhouse.html b/web/templates/teams/clubhouse.html index 0026181..5858143 100644 --- a/web/templates/teams/clubhouse.html +++ b/web/templates/teams/clubhouse.html @@ -69,14 +69,18 @@
-
+
-
Rating
-
+
Rating
+
-
K/D
-
+
K/D
+
+
+
+
OVR
+
diff --git a/web/templates/teams/detail.html b/web/templates/teams/detail.html index f643d84..13f246b 100644 --- a/web/templates/teams/detail.html +++ b/web/templates/teams/detail.html @@ -16,7 +16,10 @@ {{ p.username }} - Rating: {{ "%.2f"|format(p.rating if p.rating else 0) }} +
+ R: {{ "%.2f"|format(p.rating if p.rating else 0) }} + OVR: {{ p.stats.get('score_overall', 0)|int }} +
{% endfor %}