3.0.1: fix
This commit is contained in:
Binary file not shown.
@@ -4,6 +4,8 @@ import os
|
||||
import sys
|
||||
import sqlite3
|
||||
import json
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
@@ -94,7 +96,61 @@ def _get_team_players():
|
||||
logger.error(f"Error reading Web DB: {e}")
|
||||
return set()
|
||||
|
||||
def main():
|
||||
def _get_match_date_range(steam_id: str, conn_l2: sqlite3.Connection):
|
||||
cursor = conn_l2.cursor()
|
||||
cursor.execute("""
|
||||
SELECT MIN(m.start_time), MAX(m.start_time)
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON p.match_id = m.match_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
date_row = cursor.fetchone()
|
||||
first_match_date = date_row[0] if date_row and date_row[0] else None
|
||||
last_match_date = date_row[1] if date_row and date_row[1] else None
|
||||
return first_match_date, last_match_date
|
||||
|
||||
def _build_player_record(steam_id: str):
|
||||
try:
|
||||
from database.L3.processors import (
|
||||
BasicProcessor,
|
||||
TacticalProcessor,
|
||||
IntelligenceProcessor,
|
||||
MetaProcessor,
|
||||
CompositeProcessor
|
||||
)
|
||||
conn_l2 = sqlite3.connect(L2_DB_PATH)
|
||||
conn_l2.row_factory = sqlite3.Row
|
||||
features = {}
|
||||
features.update(BasicProcessor.calculate(steam_id, conn_l2))
|
||||
features.update(TacticalProcessor.calculate(steam_id, conn_l2))
|
||||
features.update(IntelligenceProcessor.calculate(steam_id, conn_l2))
|
||||
features.update(MetaProcessor.calculate(steam_id, conn_l2))
|
||||
features.update(CompositeProcessor.calculate(steam_id, conn_l2, features))
|
||||
match_count = _get_match_count(steam_id, conn_l2)
|
||||
round_count = _get_round_count(steam_id, conn_l2)
|
||||
first_match_date, last_match_date = _get_match_date_range(steam_id, conn_l2)
|
||||
conn_l2.close()
|
||||
return {
|
||||
"steam_id": steam_id,
|
||||
"features": features,
|
||||
"match_count": match_count,
|
||||
"round_count": round_count,
|
||||
"first_match_date": first_match_date,
|
||||
"last_match_date": last_match_date,
|
||||
"error": None,
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"steam_id": steam_id,
|
||||
"features": None,
|
||||
"match_count": 0,
|
||||
"round_count": 0,
|
||||
"first_match_date": None,
|
||||
"last_match_date": None,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
def main(force_all: bool = False, workers: int = 1):
|
||||
"""
|
||||
Main L3 feature building pipeline using modular processors
|
||||
"""
|
||||
@@ -125,26 +181,29 @@ def main():
|
||||
conn_l3 = sqlite3.connect(L3_DB_PATH)
|
||||
|
||||
try:
|
||||
# 4. Get target players (Team Lineups only)
|
||||
team_players = _get_team_players()
|
||||
if not team_players:
|
||||
logger.warning("No players found in Team Lineups. Aborting L3 build.")
|
||||
return
|
||||
|
||||
# 5. Get distinct players from L2 matching Team Lineups
|
||||
cursor_l2 = conn_l2.cursor()
|
||||
if force_all:
|
||||
logger.info("Force mode enabled: building L3 for all players in L2.")
|
||||
sql = """
|
||||
SELECT DISTINCT steam_id_64
|
||||
FROM dim_players
|
||||
ORDER BY steam_id_64
|
||||
"""
|
||||
cursor_l2.execute(sql)
|
||||
else:
|
||||
team_players = _get_team_players()
|
||||
if not team_players:
|
||||
logger.warning("No players found in Team Lineups. Aborting L3 build.")
|
||||
return
|
||||
|
||||
# Build placeholder string for IN clause
|
||||
placeholders = ','.join(['?' for _ in team_players])
|
||||
|
||||
sql = f"""
|
||||
SELECT DISTINCT steam_id_64
|
||||
FROM dim_players
|
||||
WHERE steam_id_64 IN ({placeholders})
|
||||
ORDER BY steam_id_64
|
||||
"""
|
||||
|
||||
cursor_l2.execute(sql, list(team_players))
|
||||
placeholders = ','.join(['?' for _ in team_players])
|
||||
sql = f"""
|
||||
SELECT DISTINCT steam_id_64
|
||||
FROM dim_players
|
||||
WHERE steam_id_64 IN ({placeholders})
|
||||
ORDER BY steam_id_64
|
||||
"""
|
||||
cursor_l2.execute(sql, list(team_players))
|
||||
|
||||
players = cursor_l2.fetchall()
|
||||
total_players = len(players)
|
||||
@@ -156,51 +215,61 @@ def main():
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
processed_count = 0
|
||||
|
||||
# 6. Process each player
|
||||
for idx, row in enumerate(players, 1):
|
||||
steam_id = row[0]
|
||||
if workers and workers > 1:
|
||||
steam_ids = [row[0] for row in players]
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor:
|
||||
futures = [executor.submit(_build_player_record, sid) for sid in steam_ids]
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
processed_count += 1
|
||||
if result.get("error"):
|
||||
error_count += 1
|
||||
logger.error(f"Error processing player {result.get('steam_id')}: {result.get('error')}")
|
||||
else:
|
||||
_upsert_features(
|
||||
conn_l3,
|
||||
result["steam_id"],
|
||||
result["features"],
|
||||
result["match_count"],
|
||||
result["round_count"],
|
||||
None,
|
||||
result["first_match_date"],
|
||||
result["last_match_date"],
|
||||
)
|
||||
success_count += 1
|
||||
if processed_count % 4 == 0:
|
||||
conn_l3.commit()
|
||||
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
|
||||
else:
|
||||
for idx, row in enumerate(players, 1):
|
||||
steam_id = row[0]
|
||||
|
||||
try:
|
||||
# Calculate features from each processor tier by tier
|
||||
features = {}
|
||||
try:
|
||||
features = {}
|
||||
features.update(BasicProcessor.calculate(steam_id, conn_l2))
|
||||
features.update(TacticalProcessor.calculate(steam_id, conn_l2))
|
||||
features.update(IntelligenceProcessor.calculate(steam_id, conn_l2))
|
||||
features.update(MetaProcessor.calculate(steam_id, conn_l2))
|
||||
features.update(CompositeProcessor.calculate(steam_id, conn_l2, features))
|
||||
match_count = _get_match_count(steam_id, conn_l2)
|
||||
round_count = _get_round_count(steam_id, conn_l2)
|
||||
first_match_date, last_match_date = _get_match_date_range(steam_id, conn_l2)
|
||||
_upsert_features(conn_l3, steam_id, features, match_count, round_count, conn_l2, first_match_date, last_match_date)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
logger.error(f"Error processing player {steam_id}: {e}")
|
||||
if error_count <= 3:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
# Tier 1: CORE (41 columns)
|
||||
features.update(BasicProcessor.calculate(steam_id, conn_l2))
|
||||
|
||||
# Tier 2: TACTICAL (44 columns)
|
||||
features.update(TacticalProcessor.calculate(steam_id, conn_l2))
|
||||
|
||||
# Tier 3: INTELLIGENCE (53 columns)
|
||||
features.update(IntelligenceProcessor.calculate(steam_id, conn_l2))
|
||||
|
||||
# Tier 4: META (52 columns)
|
||||
features.update(MetaProcessor.calculate(steam_id, conn_l2))
|
||||
|
||||
# Tier 5: COMPOSITE (11 columns) - requires previous features
|
||||
features.update(CompositeProcessor.calculate(steam_id, conn_l2, features))
|
||||
|
||||
# Add metadata
|
||||
match_count = _get_match_count(steam_id, conn_l2)
|
||||
round_count = _get_round_count(steam_id, conn_l2)
|
||||
|
||||
# Insert/Update features in L3
|
||||
_upsert_features(conn_l3, steam_id, features, match_count, round_count, conn_l2)
|
||||
|
||||
success_count += 1
|
||||
|
||||
# Batch commit and progress logging
|
||||
if idx % 50 == 0:
|
||||
processed_count = idx
|
||||
if processed_count % 4 == 0:
|
||||
conn_l3.commit()
|
||||
logger.info(f"Progress: {idx}/{total_players} ({success_count} success, {error_count} errors)")
|
||||
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
logger.error(f"Error processing player {steam_id}: {e}")
|
||||
if error_count <= 3: # Show details for first 3 errors
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
|
||||
|
||||
# Final commit
|
||||
conn_l3.commit()
|
||||
@@ -244,23 +313,18 @@ def _get_round_count(steam_id: str, conn_l2: sqlite3.Connection) -> int:
|
||||
|
||||
|
||||
def _upsert_features(conn_l3: sqlite3.Connection, steam_id: str, features: dict,
|
||||
match_count: int, round_count: int, conn_l2: sqlite3.Connection):
|
||||
match_count: int, round_count: int, conn_l2: sqlite3.Connection | None,
|
||||
first_match_date=None, last_match_date=None):
|
||||
"""
|
||||
Insert or update player features in dm_player_features
|
||||
"""
|
||||
cursor_l3 = conn_l3.cursor()
|
||||
cursor_l2 = conn_l2.cursor()
|
||||
|
||||
# Get first and last match dates from L2
|
||||
cursor_l2.execute("""
|
||||
SELECT MIN(m.start_time), MAX(m.start_time)
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON p.match_id = m.match_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
date_row = cursor_l2.fetchone()
|
||||
first_match_date = date_row[0] if date_row and date_row[0] else None
|
||||
last_match_date = date_row[1] if date_row and date_row[1] else None
|
||||
if first_match_date is None or last_match_date is None:
|
||||
if conn_l2 is not None:
|
||||
first_match_date, last_match_date = _get_match_date_range(steam_id, conn_l2)
|
||||
else:
|
||||
first_match_date = None
|
||||
last_match_date = None
|
||||
|
||||
# Add metadata to features
|
||||
features['total_matches'] = match_count
|
||||
@@ -289,5 +353,12 @@ def _upsert_features(conn_l3: sqlite3.Connection, steam_id: str, features: dict,
|
||||
|
||||
cursor_l3.execute(sql, values)
|
||||
|
||||
def _parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--force", action="store_true")
|
||||
parser.add_argument("--workers", type=int, default=1)
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
args = _parse_args()
|
||||
main(force_all=args.force, workers=args.workers)
|
||||
|
||||
@@ -299,20 +299,27 @@ def api_batch_stats():
|
||||
# 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
|
||||
rating_val = f.get('core_avg_rating2')
|
||||
if rating_val is None:
|
||||
rating_val = f.get('core_avg_rating')
|
||||
if rating_val is None:
|
||||
rating_val = f.get('basic_avg_rating')
|
||||
basic = {
|
||||
'rating': float(f.get('basic_avg_rating') or 0),
|
||||
'kd': float(f.get('basic_avg_kd') or 0),
|
||||
'adr': float(f.get('basic_avg_adr') or 0),
|
||||
'kast': float(f.get('basic_avg_kast') or 0),
|
||||
'rating': float(rating_val or 0),
|
||||
'kd': float(f.get('core_avg_kd') or f.get('basic_avg_kd') or 0),
|
||||
'adr': float(f.get('core_avg_adr') or f.get('basic_avg_adr') or 0),
|
||||
'kast': float(f.get('core_avg_kast') or f.get('basic_avg_kast') or 0),
|
||||
'hs_rate': float(f.get('basic_headshot_rate') or 0),
|
||||
'fk_rate': float(f.get('basic_first_kill_rate') or 0),
|
||||
'matches': int(f.get('matches_played') or 0)
|
||||
|
||||
@@ -27,6 +27,7 @@ def api_analyze():
|
||||
total_kd = 0
|
||||
total_adr = 0
|
||||
count = 0
|
||||
radar_vectors = []
|
||||
|
||||
for p in players:
|
||||
p_dict = dict(p)
|
||||
@@ -37,10 +38,25 @@ def api_analyze():
|
||||
player_data.append(p_dict)
|
||||
|
||||
if stats:
|
||||
total_rating += stats.get('basic_avg_rating', 0) or 0
|
||||
total_kd += stats.get('basic_avg_kd', 0) or 0
|
||||
total_adr += stats.get('basic_avg_adr', 0) or 0
|
||||
rating_val = stats.get('core_avg_rating2')
|
||||
if rating_val is None:
|
||||
rating_val = stats.get('core_avg_rating')
|
||||
if rating_val is None:
|
||||
rating_val = stats.get('basic_avg_rating')
|
||||
total_rating += rating_val or 0
|
||||
total_kd += stats.get('core_avg_kd', stats.get('basic_avg_kd', 0)) or 0
|
||||
total_adr += stats.get('core_avg_adr', stats.get('basic_avg_adr', 0)) or 0
|
||||
count += 1
|
||||
radar_vectors.append([
|
||||
float(stats.get('score_aim') or 0),
|
||||
float(stats.get('score_defense') or 0),
|
||||
float(stats.get('score_utility') or 0),
|
||||
float(stats.get('score_clutch') or 0),
|
||||
float(stats.get('score_economy') or 0),
|
||||
float(stats.get('score_pace') or 0),
|
||||
float(stats.get('score_pistol') or 0),
|
||||
float(stats.get('score_stability') or 0)
|
||||
])
|
||||
|
||||
# 2. Shared Matches
|
||||
shared_matches = StatsService.get_shared_matches(steam_ids)
|
||||
@@ -53,6 +69,23 @@ def api_analyze():
|
||||
'adr': total_adr / count if count else 0
|
||||
}
|
||||
|
||||
chemistry = 0
|
||||
if len(radar_vectors) >= 2:
|
||||
def cosine_sim(a, b):
|
||||
dot = sum(x * y for x, y in zip(a, b))
|
||||
na = sum(x * x for x in a) ** 0.5
|
||||
nb = sum(y * y for y in b) ** 0.5
|
||||
if na == 0 or nb == 0:
|
||||
return 0
|
||||
return dot / (na * nb)
|
||||
|
||||
sims = []
|
||||
for i in range(len(radar_vectors)):
|
||||
for j in range(i + 1, len(radar_vectors)):
|
||||
sims.append(cosine_sim(radar_vectors[i], radar_vectors[j]))
|
||||
if sims:
|
||||
chemistry = sum(sims) / len(sims) * 100
|
||||
|
||||
# 4. Map Stats Calculation
|
||||
map_stats = {} # {map_name: {'count': 0, 'wins': 0}}
|
||||
total_shared_matches = len(shared_matches)
|
||||
@@ -85,7 +118,8 @@ def api_analyze():
|
||||
'shared_matches': [dict(m) for m in shared_matches],
|
||||
'avg_stats': avg_stats,
|
||||
'map_stats': map_stats_list,
|
||||
'total_shared_matches': total_shared_matches
|
||||
'total_shared_matches': total_shared_matches,
|
||||
'chemistry': chemistry
|
||||
})
|
||||
|
||||
# API: Save Board
|
||||
|
||||
@@ -78,8 +78,12 @@ class FeatureService:
|
||||
}
|
||||
|
||||
for legacy_key, l3_key in alias_map.items():
|
||||
if legacy_key not in f or f.get(legacy_key) is None:
|
||||
f[legacy_key] = f.get(l3_key)
|
||||
legacy_val = f.get(legacy_key)
|
||||
l3_val = f.get(l3_key)
|
||||
if legacy_val is None and l3_val is not None:
|
||||
f[legacy_key] = l3_val
|
||||
elif l3_val is None and legacy_val is not None:
|
||||
f[l3_key] = legacy_val
|
||||
|
||||
if f.get("matches_played") is None:
|
||||
f["matches_played"] = f.get("total_matches", 0) or 0
|
||||
|
||||
@@ -733,16 +733,19 @@ class StatsService:
|
||||
from web.services.feature_service import FeatureService
|
||||
import json
|
||||
|
||||
# 1. Get Active Roster IDs
|
||||
lineups = WebService.get_lineups()
|
||||
active_roster_ids = []
|
||||
target_steam_id = str(target_steam_id)
|
||||
if lineups:
|
||||
try:
|
||||
raw_ids = json.loads(lineups[0]['player_ids_json'])
|
||||
active_roster_ids = [str(uid) for uid in raw_ids]
|
||||
except:
|
||||
pass
|
||||
|
||||
for lineup in lineups:
|
||||
try:
|
||||
raw_ids = json.loads(lineup.get('player_ids_json') or '[]')
|
||||
roster_ids = [str(uid) for uid in raw_ids]
|
||||
if target_steam_id in roster_ids:
|
||||
active_roster_ids = roster_ids
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not active_roster_ids:
|
||||
return None
|
||||
|
||||
@@ -752,11 +755,8 @@ class StatsService:
|
||||
return None
|
||||
|
||||
stats_map = {str(row["steam_id_64"]): FeatureService._normalize_features(dict(row)) for row in rows}
|
||||
target_steam_id = str(target_steam_id)
|
||||
|
||||
# If target not in map (e.g. no L3 data), try to add empty default
|
||||
if target_steam_id not in stats_map:
|
||||
stats_map[target_steam_id] = {}
|
||||
return None
|
||||
|
||||
metrics = [
|
||||
# TIER 1: CORE
|
||||
|
||||
@@ -40,15 +40,15 @@
|
||||
<!-- 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>
|
||||
<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 or player.core_avg_rating or 0) }}</span>
|
||||
<span class="text-gray-400">Rating</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block font-bold">{{ "%.2f"|format(player.basic_avg_kd|default(0)) }}</span>
|
||||
<span class="block font-bold">{{ "%.2f"|format(player.core_avg_kd or 0) }}</span>
|
||||
<span class="text-gray-400">K/D</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block font-bold">{{ "%.1f"|format((player.basic_avg_kast|default(0)) * 100) }}%</span>
|
||||
<span class="block font-bold">{{ "%.1f"|format((player.core_avg_kast or 0) * 100) }}%</span>
|
||||
<span class="text-gray-400">KAST</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -338,10 +338,10 @@ function tacticsBoard() {
|
||||
this.radarChart = new Chart(ctx, {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: ['RTG', 'K/D', 'KST', 'ADR', 'IMP', 'UTL'],
|
||||
labels: ['枪法', '生存', '道具', '残局', '经济', '节奏', '手枪', '稳定'],
|
||||
datasets: [{
|
||||
label: 'Avg',
|
||||
data: [0, 0, 0, 0, 0, 0],
|
||||
data: [0, 0, 0, 0, 0, 0, 0, 0],
|
||||
backgroundColor: 'rgba(139, 92, 246, 0.2)',
|
||||
borderColor: 'rgba(139, 92, 246, 1)',
|
||||
pointBackgroundColor: 'rgba(139, 92, 246, 1)',
|
||||
@@ -354,7 +354,7 @@ function tacticsBoard() {
|
||||
scales: {
|
||||
r: {
|
||||
beginAtZero: true,
|
||||
max: 1.5,
|
||||
max: 100,
|
||||
grid: { color: 'rgba(156, 163, 175, 0.1)' },
|
||||
angleLines: { color: 'rgba(156, 163, 175, 0.1)' },
|
||||
pointLabels: { font: { size: 9 } },
|
||||
@@ -368,20 +368,22 @@ function tacticsBoard() {
|
||||
|
||||
updateRadar() {
|
||||
if (this.activePlayers.length === 0) {
|
||||
this.radarChart.data.datasets[0].data = [0, 0, 0, 0, 0, 0];
|
||||
this.radarChart.data.datasets[0].data = [0, 0, 0, 0, 0, 0, 0, 0];
|
||||
this.radarChart.update();
|
||||
return;
|
||||
}
|
||||
|
||||
let totals = [0, 0, 0, 0, 0, 0];
|
||||
let totals = [0, 0, 0, 0, 0, 0, 0, 0];
|
||||
this.activePlayers.forEach(p => {
|
||||
const s = p.stats || {};
|
||||
totals[0] += s.basic_avg_rating || 0;
|
||||
totals[1] += s.basic_avg_kd || 0;
|
||||
totals[2] += s.basic_avg_kast || 0;
|
||||
totals[3] += (s.basic_avg_adr || 0) / 100;
|
||||
totals[4] += s.bat_avg_impact || 1.0;
|
||||
totals[5] += s.util_usage_rate || 0.5;
|
||||
totals[0] += s.score_aim || 0;
|
||||
totals[1] += s.score_defense || 0;
|
||||
totals[2] += s.score_utility || 0;
|
||||
totals[3] += s.score_clutch || 0;
|
||||
totals[4] += s.score_economy || 0;
|
||||
totals[5] += s.score_pace || 0;
|
||||
totals[6] += s.score_pistol || 0;
|
||||
totals[7] += s.score_stability || 0;
|
||||
});
|
||||
|
||||
const count = this.activePlayers.length;
|
||||
|
||||
@@ -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>
|
||||
<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?.core_avg_rating || p.stats?.basic_avg_rating || 0).toFixed(2)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -149,9 +149,15 @@
|
||||
<h4 class="font-bold text-xl text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<span>📈</span> 综合评分
|
||||
</h4>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<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>
|
||||
<div class="flex items-baseline gap-6">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-sm text-gray-500">Chemistry</span>
|
||||
<span class="text-3xl font-black text-yrtv-600 tracking-tight" x-text="analysisResult.chemistry.toFixed(1)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -526,13 +532,10 @@ function tacticsApp() {
|
||||
// Unwrap proxy if needed
|
||||
const rawData = JSON.parse(JSON.stringify(this.dataResult));
|
||||
|
||||
const radarKeys = ['AIM', 'DEFENSE', 'UTILITY', 'CLUTCH', 'ECONOMY', 'PACE', 'PISTOL', 'STABILITY'];
|
||||
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
|
||||
];
|
||||
|
||||
const d = radarKeys.map(k => (p.radar?.[k] || 0));
|
||||
return {
|
||||
label: p.username,
|
||||
data: d,
|
||||
@@ -543,12 +546,49 @@ function tacticsApp() {
|
||||
};
|
||||
});
|
||||
|
||||
const valuesByDim = radarKeys.map(() => []);
|
||||
rawData.forEach(p => {
|
||||
radarKeys.forEach((k, i) => {
|
||||
valuesByDim[i].push(Number(p.radar?.[k] || 0));
|
||||
});
|
||||
});
|
||||
const avgVals = valuesByDim.map(arr => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0);
|
||||
const minVals = valuesByDim.map(arr => arr.length ? Math.min(...arr) : 0);
|
||||
const maxVals = valuesByDim.map(arr => arr.length ? Math.max(...arr) : 0);
|
||||
|
||||
datasets.push({
|
||||
label: 'Avg',
|
||||
data: avgVals,
|
||||
borderColor: '#64748b',
|
||||
backgroundColor: 'rgba(100, 116, 139, 0.08)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 0
|
||||
});
|
||||
datasets.push({
|
||||
label: 'Max',
|
||||
data: maxVals,
|
||||
borderColor: '#16a34a',
|
||||
backgroundColor: 'rgba(22, 163, 74, 0.05)',
|
||||
borderWidth: 1,
|
||||
borderDash: [4, 3],
|
||||
pointRadius: 0
|
||||
});
|
||||
datasets.push({
|
||||
label: 'Min',
|
||||
data: minVals,
|
||||
borderColor: '#dc2626',
|
||||
backgroundColor: 'rgba(220, 38, 38, 0.05)',
|
||||
borderWidth: 1,
|
||||
borderDash: [4, 3],
|
||||
pointRadius: 0
|
||||
});
|
||||
|
||||
// Recreate Chart with Profile-aligned config
|
||||
const ctx = canvas.getContext('2d');
|
||||
this.radarChart = new Chart(ctx, {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: ['BAT (火力)', 'PTL (手枪)', 'HPS (抗压)', 'SIDE (阵营)', 'UTIL (道具)', 'STA (稳定)'],
|
||||
labels: ['枪法 (Aim)', '生存 (Defense)', '道具 (Utility)', '残局 (Clutch)', '经济 (Economy)', '节奏 (Pace)', '手枪 (Pistol)', '稳定 (Stability)'],
|
||||
datasets: datasets
|
||||
},
|
||||
options: {
|
||||
@@ -595,7 +635,7 @@ function tacticsApp() {
|
||||
this.radarChart = new Chart(ctx, {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: ['BAT (火力)', 'PTL (手枪)', 'HPS (抗压)', 'SIDE (阵营)', 'UTIL (道具)', 'STA (稳定)'],
|
||||
labels: ['枪法 (Aim)', '生存 (Defense)', '道具 (Utility)', '残局 (Clutch)', '经济 (Economy)', '节奏 (Pace)', '手枪 (Pistol)', '稳定 (Stability)'],
|
||||
datasets: []
|
||||
},
|
||||
options: {
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-2 gap-2 w-full text-center mb-auto">
|
||||
<div class="grid grid-cols-3 gap-2 w-full text-center mb-auto">
|
||||
<div class="bg-gray-50 dark:bg-slate-700 rounded p-1">
|
||||
<div class="text-xs 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>
|
||||
@@ -78,6 +78,10 @@
|
||||
<div class="text-xs text-gray-400">K/D</div>
|
||||
<div class="font-bold" 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-xs text-gray-400">总评</div>
|
||||
<div class="font-bold" x-text="(player.stats?.score_overall || 0).toFixed(1)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
|
||||
Reference in New Issue
Block a user