3.0.3: Cant fix team avg. removed.
This commit is contained in:
Binary file not shown.
@@ -239,7 +239,7 @@ def main(force_all: bool = False, workers: int = 1):
|
|||||||
result["last_match_date"],
|
result["last_match_date"],
|
||||||
)
|
)
|
||||||
success_count += 1
|
success_count += 1
|
||||||
if processed_count % 4 == 0:
|
if processed_count % 2 == 0:
|
||||||
conn_l3.commit()
|
conn_l3.commit()
|
||||||
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
|
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
|
||||||
else:
|
else:
|
||||||
@@ -267,7 +267,7 @@ def main(force_all: bool = False, workers: int = 1):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
processed_count = idx
|
processed_count = idx
|
||||||
if processed_count % 4 == 0:
|
if processed_count % 2 == 0:
|
||||||
conn_l3.commit()
|
conn_l3.commit()
|
||||||
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
|
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from web.database import execute_db, query_db
|
|||||||
from web.config import Config
|
from web.config import Config
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
bp = Blueprint('players', __name__, url_prefix='/players')
|
bp = Blueprint('players', __name__, url_prefix='/players')
|
||||||
@@ -231,6 +232,41 @@ def charts_data(steam_id):
|
|||||||
radar_data = {}
|
radar_data = {}
|
||||||
radar_dist = FeatureService.get_roster_features_distribution(steam_id)
|
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:
|
if features:
|
||||||
# Dimensions: AIM, DEFENSE, UTILITY, CLUTCH, ECONOMY, PACE (6 Dimensions)
|
# Dimensions: AIM, DEFENSE, UTILITY, CLUTCH, ECONOMY, PACE (6 Dimensions)
|
||||||
# Use calculated scores (0-100 scale)
|
# Use calculated scores (0-100 scale)
|
||||||
@@ -266,7 +302,8 @@ def charts_data(steam_id):
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'trend': {'labels': trend_labels, 'values': trend_values},
|
'trend': {'labels': trend_labels, 'values': trend_values},
|
||||||
'radar': radar_data,
|
'radar': radar_data,
|
||||||
'radar_dist': radar_dist
|
'radar_dist': radar_dist,
|
||||||
|
'team_avg_radar': team_avg_radar
|
||||||
})
|
})
|
||||||
|
|
||||||
# --- API for Comparison ---
|
# --- API for Comparison ---
|
||||||
@@ -297,14 +334,15 @@ def api_batch_stats():
|
|||||||
|
|
||||||
# 1. Radar Scores (Normalized 0-100)
|
# 1. Radar Scores (Normalized 0-100)
|
||||||
# Use safe conversion with default 0 if None
|
# Use safe conversion with default 0 if None
|
||||||
# Force 0.0 if value is 0 or None to ensure JSON compatibility
|
|
||||||
radar = {
|
radar = {
|
||||||
'STA': float(f.get('score_sta') or 0.0),
|
'AIM': float(f.get('score_aim') or 0.0),
|
||||||
'BAT': float(f.get('score_bat') or 0.0),
|
'DEFENSE': float(f.get('score_defense') or 0.0),
|
||||||
'HPS': float(f.get('score_hps') or 0.0),
|
'UTILITY': float(f.get('score_utility') or 0.0),
|
||||||
'PTL': float(f.get('score_ptl') or 0.0),
|
'CLUTCH': float(f.get('score_clutch') or 0.0),
|
||||||
'SIDE': float(f.get('score_tct') or 0.0),
|
'ECONOMY': float(f.get('score_economy') or 0.0),
|
||||||
'UTIL': float(f.get('score_util') 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
|
# 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),
|
'first_kill_ct': float(f.get('side_first_kill_rate_ct') or 0),
|
||||||
|
|
||||||
# Row 3
|
# Row 3
|
||||||
'first_death_t': float(f.get('side_first_death_rate_t') or 0),
|
'first_death_t': float(f.get('tac_fd_rate') or 0),
|
||||||
'first_death_ct': float(f.get('side_first_death_rate_ct') or 0),
|
'first_death_ct': float(f.get('tac_fd_rate') or 0),
|
||||||
'kast_t': float(f.get('side_kast_t') or 0),
|
'kast_t': float(f.get('side_kast_t') or 0),
|
||||||
'kast_ct': float(f.get('side_kast_ct') or 0),
|
'kast_ct': float(f.get('side_kast_ct') or 0),
|
||||||
|
|
||||||
# Row 4
|
# Row 4
|
||||||
'rws_t': float(f.get('side_rws_t') or 0),
|
'rws_t': float(f.get('core_avg_rws') or 0),
|
||||||
'rws_ct': float(f.get('side_rws_ct') or 0),
|
'rws_ct': float(f.get('core_avg_rws') or 0),
|
||||||
'multikill_t': float(f.get('side_multikill_rate_t') or 0),
|
'multikill_t': float(f.get('tac_multikill_rate') or 0),
|
||||||
'multikill_ct': float(f.get('side_multikill_rate_ct') or 0),
|
'multikill_ct': float(f.get('tac_multikill_rate') or 0),
|
||||||
|
|
||||||
# Row 5
|
# Row 5
|
||||||
'hs_t': float(f.get('side_headshot_rate_t') or 0),
|
'hs_t': float(f.get('core_hs_rate') or 0),
|
||||||
'hs_ct': float(f.get('side_headshot_rate_ct') or 0),
|
'hs_ct': float(f.get('core_hs_rate') or 0),
|
||||||
'obj_t': float(f.get('side_obj_t') or 0),
|
'obj_t': float(f.get('core_avg_plants') or 0),
|
||||||
'obj_ct': float(f.get('side_obj_ct') or 0)
|
'obj_ct': float(f.get('core_avg_defuses') or 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.append({
|
stats.append({
|
||||||
|
|||||||
@@ -53,6 +53,41 @@ def api_analyze():
|
|||||||
'adr': total_adr / 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
|
# 4. Map Stats Calculation
|
||||||
map_stats = {} # {map_name: {'count': 0, 'wins': 0}}
|
map_stats = {} # {map_name: {'count': 0, 'wins': 0}}
|
||||||
total_shared_matches = len(shared_matches)
|
total_shared_matches = len(shared_matches)
|
||||||
@@ -84,6 +119,8 @@ def api_analyze():
|
|||||||
'players': player_data,
|
'players': player_data,
|
||||||
'shared_matches': [dict(m) for m in shared_matches],
|
'shared_matches': [dict(m) for m in shared_matches],
|
||||||
'avg_stats': avg_stats,
|
'avg_stats': avg_stats,
|
||||||
|
'radar_stats': radar_stats,
|
||||||
|
'chemistry_score': chemistry_score,
|
||||||
'map_stats': map_stats_list,
|
'map_stats': map_stats_list,
|
||||||
'total_shared_matches': total_shared_matches
|
'total_shared_matches': total_shared_matches
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -166,33 +166,13 @@ class FeatureService:
|
|||||||
lineups = WebService.get_lineups()
|
lineups = WebService.get_lineups()
|
||||||
roster_ids: list[str] = []
|
roster_ids: list[str] = []
|
||||||
|
|
||||||
# Try to find a lineup containing this player
|
|
||||||
if lineups:
|
if lineups:
|
||||||
for lineup in lineups:
|
try:
|
||||||
try:
|
p_ids = [str(i) for i in json.loads(lineups[0].get("player_ids_json") or "[]")]
|
||||||
p_ids = [str(i) for i in json.loads(lineup.get("player_ids_json") or "[]")]
|
if str(target_steam_id) in p_ids:
|
||||||
if str(target_steam_id) in p_ids:
|
roster_ids = p_ids
|
||||||
roster_ids = p_ids
|
except Exception:
|
||||||
break
|
roster_ids = []
|
||||||
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))
|
|
||||||
|
|
||||||
if not roster_ids:
|
if not roster_ids:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<!-- Mini Stats -->
|
<!-- Mini Stats -->
|
||||||
<div class="grid grid-cols-3 gap-x-4 gap-y-2 text-xs text-gray-600 dark:text-gray-300 mb-4 w-full text-center">
|
<div class="grid grid-cols-3 gap-x-4 gap-y-2 text-xs text-gray-600 dark:text-gray-300 mb-4 w-full text-center">
|
||||||
<div>
|
<div>
|
||||||
<span class="block font-bold">{{ "%.2f"|format(player.basic_avg_rating|default(0)) }}</span>
|
<span class="block font-bold">{{ "%.2f"|format(player.core_avg_rating2|default(player.basic_avg_rating)|default(0)) }}</span>
|
||||||
<span class="text-gray-400">Rating</span>
|
<span class="text-gray-400">Rating</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -869,6 +869,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Prepare Distribution Data
|
// Prepare Distribution Data
|
||||||
const dist = data.radar_dist || {};
|
const dist = data.radar_dist || {};
|
||||||
|
const hasDist = Object.keys(dist).length > 0;
|
||||||
const getDist = (key) => dist[key] || { rank: '?', avg: 0 };
|
const getDist = (key) => dist[key] || { rank: '?', avg: 0 };
|
||||||
|
|
||||||
// Map friendly names to keys
|
// Map friendly names to keys
|
||||||
@@ -877,41 +878,49 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const rawLabels = ['枪法 (Aim)', '生存 (Defense)', '道具 (Utility)', '残局 (Clutch)', '经济 (Economy)', '节奏 (Pace)', '手枪 (Pistol)', '稳定 (Stability)'];
|
const rawLabels = ['枪法 (Aim)', '生存 (Defense)', '道具 (Utility)', '残局 (Clutch)', '经济 (Economy)', '节奏 (Pace)', '手枪 (Pistol)', '稳定 (Stability)'];
|
||||||
|
|
||||||
const labels = rawLabels.map((l, i) => {
|
const labels = rawLabels.map((l, i) => {
|
||||||
|
if (!hasDist) return l;
|
||||||
const k = keys[i];
|
const k = keys[i];
|
||||||
const d = getDist(k);
|
const d = getDist(k);
|
||||||
return `${l} #${d.rank}`;
|
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, {
|
new Chart(ctxRadar, {
|
||||||
type: 'radar',
|
type: 'radar',
|
||||||
data: {
|
data: {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
datasets: [{
|
datasets: 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]
|
|
||||||
}]
|
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
|
|
||||||
<span class="text-sm font-bold truncate w-full text-center dark:text-white mb-1" x-text="p.username || p.name"></span>
|
<span class="text-sm font-bold truncate w-full text-center dark:text-white mb-1" x-text="p.username || p.name"></span>
|
||||||
<div class="px-2.5 py-1 bg-white dark:bg-slate-900 rounded-full text-xs text-gray-500 dark:text-gray-400 shadow-inner border border-gray-100 dark:border-slate-700">
|
<div class="px-2.5 py-1 bg-white dark:bg-slate-900 rounded-full text-xs text-gray-500 dark:text-gray-400 shadow-inner border border-gray-100 dark:border-slate-700">
|
||||||
Rating: <span class="font-bold text-yrtv-600" x-text="(p.stats?.basic_avg_rating || 0).toFixed(2)"></span>
|
Rating: <span class="font-bold text-yrtv-600" x-text="((p.stats?.core_avg_rating2 || p.stats?.basic_avg_rating) || 0).toFixed(2)"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -153,6 +153,15 @@
|
|||||||
<span class="text-sm text-gray-500">Team Rating</span>
|
<span class="text-sm text-gray-500">Team Rating</span>
|
||||||
<span class="text-4xl font-black text-yrtv-600 tracking-tight" x-text="analysisResult.avg_stats.rating.toFixed(2)"></span>
|
<span class="text-4xl font-black text-yrtv-600 tracking-tight" x-text="analysisResult.avg_stats.rating.toFixed(2)"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-baseline gap-2">
|
||||||
|
<span class="text-sm text-gray-500">Chemistry Score</span>
|
||||||
|
<span class="text-4xl font-black text-blue-600 tracking-tight" x-text="(analysisResult.chemistry_score || 0).toFixed(0)"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Analysis Radar Chart -->
|
||||||
|
<div class="bg-gray-50 dark:bg-slate-700 p-4 rounded-xl border border-gray-100 dark:border-slate-600 h-[300px]">
|
||||||
|
<canvas id="analysisRadarChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-6 text-center">
|
<div class="grid grid-cols-3 gap-6 text-center">
|
||||||
@@ -336,6 +345,7 @@ function tacticsApp() {
|
|||||||
// Analysis State
|
// Analysis State
|
||||||
analysisLineup: [],
|
analysisLineup: [],
|
||||||
analysisResult: null,
|
analysisResult: null,
|
||||||
|
analysisChart: null,
|
||||||
debounceTimer: null,
|
debounceTimer: null,
|
||||||
|
|
||||||
// Data Center State
|
// Data Center State
|
||||||
@@ -410,7 +420,8 @@ function tacticsApp() {
|
|||||||
steam_id_64: player.steam_id_64,
|
steam_id_64: player.steam_id_64,
|
||||||
username: player.username || player.name,
|
username: player.username || player.name,
|
||||||
name: player.name || player.username,
|
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.setData('text/plain', JSON.stringify(payload));
|
||||||
event.dataTransfer.effectAllowed = 'copy';
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
@@ -529,8 +540,9 @@ function tacticsApp() {
|
|||||||
const datasets = rawData.map((p, idx) => {
|
const datasets = rawData.map((p, idx) => {
|
||||||
const color = this.getPlayerColor(idx);
|
const color = this.getPlayerColor(idx);
|
||||||
const d = [
|
const d = [
|
||||||
p.radar.BAT || 0, p.radar.PTL || 0, p.radar.HPS || 0,
|
p.radar.AIM || 0, p.radar.DEFENSE || 0, p.radar.UTILITY || 0,
|
||||||
p.radar.SIDE || 0, p.radar.UTIL || 0, p.radar.STA || 0
|
p.radar.CLUTCH || 0, p.radar.ECONOMY || 0, p.radar.PACE || 0,
|
||||||
|
p.radar.PISTOL || 0, p.radar.STABILITY || 0
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -548,7 +560,7 @@ function tacticsApp() {
|
|||||||
this.radarChart = new Chart(ctx, {
|
this.radarChart = new Chart(ctx, {
|
||||||
type: 'radar',
|
type: 'radar',
|
||||||
data: {
|
data: {
|
||||||
labels: ['BAT (火力)', 'PTL (手枪)', 'HPS (抗压)', 'SIDE (阵营)', 'UTIL (道具)', 'STA (稳定)'],
|
labels: ['AIM (枪法)', 'DEF (生存)', 'UTIL (道具)', 'CLUTCH (残局)', 'ECO (经济)', 'PACE (节奏)', 'PISTOL (手枪)', 'STA (稳定)'],
|
||||||
datasets: datasets
|
datasets: datasets
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@@ -595,7 +607,7 @@ function tacticsApp() {
|
|||||||
this.radarChart = new Chart(ctx, {
|
this.radarChart = new Chart(ctx, {
|
||||||
type: 'radar',
|
type: 'radar',
|
||||||
data: {
|
data: {
|
||||||
labels: ['BAT (火力)', 'PTL (手枪)', 'HPS (抗压)', 'SIDE (阵营)', 'UTIL (道具)', 'STA (稳定)'],
|
labels: ['AIM (枪法)', 'DEF (生存)', 'UTIL (道具)', 'CLUTCH (残局)', 'ECO (经济)', 'PACE (节奏)', 'PISTOL (手枪)', 'STA (稳定)'],
|
||||||
datasets: []
|
datasets: []
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@@ -659,6 +671,59 @@ function tacticsApp() {
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.analysisResult = 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 } }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -69,14 +69,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
<!-- Stats Grid -->
|
||||||
<div class="grid grid-cols-2 gap-2 w-full text-center mb-auto">
|
<div class="grid grid-cols-3 gap-1 w-full text-center mb-auto">
|
||||||
<div class="bg-gray-50 dark:bg-slate-700 rounded p-1">
|
<div class="bg-gray-50 dark:bg-slate-700 rounded p-1">
|
||||||
<div class="text-xs text-gray-400">Rating</div>
|
<div class="text-[10px] text-gray-400">Rating</div>
|
||||||
<div class="font-bold text-yrtv-600 dark:text-yrtv-400" x-text="(player.stats?.core_avg_rating || 0).toFixed(2)"></div>
|
<div class="font-bold text-yrtv-600 dark:text-yrtv-400 text-sm" x-text="(player.stats?.core_avg_rating2 || player.stats?.core_avg_rating || 0).toFixed(2)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 dark:bg-slate-700 rounded p-1">
|
<div class="bg-gray-50 dark:bg-slate-700 rounded p-1">
|
||||||
<div class="text-xs text-gray-400">K/D</div>
|
<div class="text-[10px] text-gray-400">K/D</div>
|
||||||
<div class="font-bold" x-text="(player.stats?.core_avg_kd || 0).toFixed(2)"></div>
|
<div class="font-bold text-sm" x-text="(player.stats?.core_avg_kd || 0).toFixed(2)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 dark:bg-slate-700 rounded p-1">
|
||||||
|
<div class="text-[10px] text-gray-400">OVR</div>
|
||||||
|
<div class="font-black text-sm text-yrtv-700 dark:text-yrtv-300" x-text="(player.stats?.score_overall || 0).toFixed(0)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600 truncate w-full text-center">
|
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600 truncate w-full text-center">
|
||||||
{{ p.username }}
|
{{ p.username }}
|
||||||
</a>
|
</a>
|
||||||
<span class="text-xs text-gray-500">Rating: {{ "%.2f"|format(p.rating if p.rating else 0) }}</span>
|
<div class="flex gap-2 text-xs text-gray-500 mt-1">
|
||||||
|
<span>R: <span class="font-bold {{ 'text-green-600' if p.rating >= 1.1 else '' }}">{{ "%.2f"|format(p.rating if p.rating else 0) }}</span></span>
|
||||||
|
<span class="border-l border-gray-300 pl-2">OVR: <span class="font-bold text-yrtv-600">{{ p.stats.get('score_overall', 0)|int }}</span></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user