1.0.1-fix: Fixed 'winner-team' regarded as win.
This commit is contained in:
@@ -44,6 +44,7 @@ def api_analyze():
|
||||
|
||||
# 2. Shared Matches
|
||||
shared_matches = StatsService.get_shared_matches(steam_ids)
|
||||
# They are already dicts now with 'result_str' and 'is_win'
|
||||
|
||||
# 3. Aggregates
|
||||
avg_stats = {
|
||||
|
||||
@@ -178,19 +178,29 @@ class StatsService:
|
||||
|
||||
@staticmethod
|
||||
def get_shared_matches(steam_ids):
|
||||
# Find matches where ALL steam_ids were present in the SAME team (or just present?)
|
||||
# "共同经历" usually means played together.
|
||||
# Query: Intersect match_ids for each player.
|
||||
# SQLite doesn't have INTERSECT ALL easily for dynamic list, but we can group by match_id.
|
||||
|
||||
if not steam_ids or len(steam_ids) < 2:
|
||||
# Find matches where ALL steam_ids were present
|
||||
if not steam_ids or len(steam_ids) < 1:
|
||||
return []
|
||||
|
||||
placeholders = ','.join('?' for _ in steam_ids)
|
||||
count = len(steam_ids)
|
||||
|
||||
# We need to know which team the players were on to determine win/loss
|
||||
# Assuming they were on the SAME team for "shared experience"
|
||||
# If count=1, it's just match history
|
||||
|
||||
# Query: Get matches where all steam_ids are present
|
||||
# Also join to get team_id to check if they were on the same team (optional but better)
|
||||
# For simplicity in v1: Just check presence in the match.
|
||||
# AND check if the player won.
|
||||
|
||||
# We need to return: match_id, map_name, score, result (Win/Loss)
|
||||
# "Result" is relative to the lineup.
|
||||
# If they were on the winning team, it's a Win.
|
||||
|
||||
sql = f"""
|
||||
SELECT m.match_id, m.start_time, m.map_name, m.score_team1, m.score_team2, m.winner_team
|
||||
SELECT m.match_id, m.start_time, m.map_name, m.score_team1, m.score_team2, m.winner_team,
|
||||
MAX(mp.team_id) as player_team_id -- Just take one team_id (assuming same)
|
||||
FROM fact_matches m
|
||||
JOIN fact_match_players mp ON m.match_id = mp.match_id
|
||||
WHERE mp.steam_id_64 IN ({placeholders})
|
||||
@@ -203,7 +213,33 @@ class StatsService:
|
||||
args = list(steam_ids)
|
||||
args.append(count)
|
||||
|
||||
return query_db('l2', sql, args)
|
||||
rows = query_db('l2', sql, args)
|
||||
|
||||
results = []
|
||||
for r in rows:
|
||||
# Determine if Win
|
||||
# winner_team in DB is 'Team 1' or 'Team 2' usually, or the team name.
|
||||
# fact_matches.winner_team stores the NAME of the winner? Or 'team1'/'team2'?
|
||||
# Let's check how L2_Builder stores it. Usually it stores the name.
|
||||
# But fact_match_players.team_id stores the name too.
|
||||
|
||||
# Logic: If m.winner_team == mp.team_id, then Win.
|
||||
is_win = (r['winner_team'] == r['player_team_id'])
|
||||
|
||||
# If winner_team is NULL or empty, it's a draw?
|
||||
if not r['winner_team']:
|
||||
result_str = 'Draw'
|
||||
elif is_win:
|
||||
result_str = 'Win'
|
||||
else:
|
||||
result_str = 'Loss'
|
||||
|
||||
res = dict(r)
|
||||
res['is_win'] = is_win # Boolean for styling
|
||||
res['result_str'] = result_str # Text for display
|
||||
results.append(res)
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def get_player_trend(steam_id, limit=20):
|
||||
|
||||
@@ -34,8 +34,14 @@
|
||||
draggable="true"
|
||||
@dragstart="dragStart($event, player)">
|
||||
|
||||
<img :src="player.avatar_url || 'https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg'"
|
||||
class="w-10 h-10 rounded-full border border-gray-200 dark:border-slate-600 object-cover pointer-events-none">
|
||||
<template x-if="player.avatar_url">
|
||||
<img :src="player.avatar_url" class="w-10 h-10 rounded-full border border-gray-200 dark:border-slate-600 object-cover pointer-events-none">
|
||||
</template>
|
||||
<template x-if="!player.avatar_url">
|
||||
<div class="w-10 h-10 rounded-full bg-yrtv-100 flex items-center justify-center border border-gray-200 dark:border-slate-600 text-yrtv-600 font-bold text-xs pointer-events-none">
|
||||
<span x-text="(player.username || player.name || player.steam_id_64).substring(0, 2).toUpperCase()"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="ml-3 flex-1 min-w-0 pointer-events-none">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white truncate" x-text="player.username || player.name || player.steam_id_64"></div>
|
||||
@@ -98,7 +104,17 @@
|
||||
<template x-for="(p, idx) in analysisLineup" :key="p.steam_id_64">
|
||||
<div class="relative bg-gray-50 dark:bg-slate-700 p-2 rounded border border-gray-200 dark:border-slate-600 flex flex-col items-center">
|
||||
<button @click="removeFromAnalysis(idx)" class="absolute top-1 right-1 text-red-400 hover:text-red-600">×</button>
|
||||
<img :src="p.avatar_url" class="w-12 h-12 rounded-full mb-2">
|
||||
|
||||
<!-- Avatar -->
|
||||
<template x-if="p.avatar_url">
|
||||
<img :src="p.avatar_url" class="w-12 h-12 rounded-full mb-2 object-cover">
|
||||
</template>
|
||||
<template x-if="!p.avatar_url">
|
||||
<div class="w-12 h-12 rounded-full mb-2 bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-sm">
|
||||
<span x-text="(p.username || p.name || p.steam_id_64).substring(0, 2).toUpperCase()"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<span class="text-xs font-bold truncate w-full text-center dark:text-white" x-text="p.username || p.name"></span>
|
||||
<span class="text-[10px] text-gray-500" x-text="'R: ' + (p.stats?.basic_avg_rating || 0).toFixed(2)"></span>
|
||||
</div>
|
||||
@@ -163,7 +179,7 @@
|
||||
<tr>
|
||||
<td class="px-2 py-1 dark:text-gray-300" x-text="m.map_name"></td>
|
||||
<td class="px-2 py-1 text-right dark:text-gray-300" x-text="m.score_team1 + ':' + m.score_team2"></td>
|
||||
<td class="px-2 py-1 text-right font-bold" :class="m.winner_team ? 'text-green-600' : 'text-gray-500'" x-text="m.winner_team ? 'Win' : 'Draw/Loss'"></td>
|
||||
<td class="px-2 py-1 text-right font-bold" :class="m.is_win ? 'text-green-600' : 'text-red-500'" x-text="m.result_str"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
@@ -411,8 +427,10 @@ function tacticsApp() {
|
||||
const displayName = player.username || player.name || player.steam_id_64;
|
||||
const iconHtml = `
|
||||
<div class="flex flex-col items-center justify-center transform hover:scale-110 transition duration-200">
|
||||
<img src="${player.avatar_url || 'https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg'}"
|
||||
class="w-8 h-8 rounded-full border-2 border-white shadow-lg box-content">
|
||||
${player.avatar_url ?
|
||||
`<img src="${player.avatar_url}" class="w-8 h-8 rounded-full border-2 border-white shadow-lg box-content object-cover">` :
|
||||
`<div class="w-8 h-8 rounded-full bg-yrtv-100 border-2 border-white shadow-lg box-content flex items-center justify-center text-yrtv-600 font-bold text-[10px]">${(player.username || player.name).substring(0, 2).toUpperCase()}</div>`
|
||||
}
|
||||
<span class="mt-1 text-[10px] font-bold text-white bg-black/60 px-1.5 py-0.5 rounded backdrop-blur-sm whitespace-nowrap overflow-hidden max-w-[80px] text-ellipsis">
|
||||
${displayName}
|
||||
</span>
|
||||
|
||||
@@ -21,6 +21,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sorting Controls -->
|
||||
<div class="flex justify-end mb-4">
|
||||
<div class="inline-flex shadow-sm rounded-md" role="group">
|
||||
<button type="button" @click="sortBy('rating')" :class="{'bg-yrtv-600 text-white': currentSort === 'rating', 'bg-white text-gray-700 hover:bg-gray-50': currentSort !== 'rating'}" class="px-4 py-2 text-sm font-medium border border-gray-200 rounded-l-lg dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600">
|
||||
Rating
|
||||
</button>
|
||||
<button type="button" @click="sortBy('kd')" :class="{'bg-yrtv-600 text-white': currentSort === 'kd', 'bg-white text-gray-700 hover:bg-gray-50': currentSort !== 'kd'}" class="px-4 py-2 text-sm font-medium border-t border-b border-gray-200 dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600">
|
||||
K/D
|
||||
</button>
|
||||
<button type="button" @click="sortBy('matches')" :class="{'bg-yrtv-600 text-white': currentSort === 'matches', 'bg-white text-gray-700 hover:bg-gray-50': currentSort !== 'matches'}" class="px-4 py-2 text-sm font-medium border border-gray-200 rounded-r-lg dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600">
|
||||
Matches
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Roster (Grid) -->
|
||||
<div class="mb-10">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white mb-4">Active Roster</h3>
|
||||
@@ -32,7 +47,15 @@
|
||||
|
||||
<div class="w-full h-full flex flex-col items-center">
|
||||
<div class="relative w-32 h-32 mb-4">
|
||||
<img :src="player.avatar_url || 'https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg'" class="w-32 h-32 rounded-full object-cover border-4 border-yrtv-500 shadow-lg">
|
||||
<!-- Avatar Logic: Image or Initials -->
|
||||
<template x-if="player.avatar_url">
|
||||
<img :src="player.avatar_url" class="w-32 h-32 rounded-full object-cover border-4 border-yrtv-500 shadow-lg">
|
||||
</template>
|
||||
<template x-if="!player.avatar_url">
|
||||
<div class="w-32 h-32 rounded-full bg-yrtv-100 flex items-center justify-center border-4 border-yrtv-500 shadow-lg text-yrtv-600 font-bold text-4xl">
|
||||
<span x-text="(player.username || player.name || player.steam_id_64).substring(0, 2).toUpperCase()"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<h4 class="text-lg font-bold text-gray-900 dark:text-white truncate w-full text-center" x-text="player.username || player.name || player.steam_id_64"></h4>
|
||||
@@ -140,6 +163,7 @@ function clubhouse() {
|
||||
return {
|
||||
team: {},
|
||||
roster: [],
|
||||
currentSort: 'rating', // Default sort
|
||||
showScoutModal: false,
|
||||
searchQuery: '',
|
||||
searchResults: [],
|
||||
@@ -154,6 +178,39 @@ function clubhouse() {
|
||||
.then(data => {
|
||||
this.team = data.team;
|
||||
this.roster = data.roster;
|
||||
this.sortRoster(); // Apply default sort
|
||||
});
|
||||
},
|
||||
|
||||
sortBy(key) {
|
||||
this.currentSort = key;
|
||||
this.sortRoster();
|
||||
},
|
||||
|
||||
sortRoster() {
|
||||
if (!this.roster || this.roster.length === 0) return;
|
||||
|
||||
this.roster.sort((a, b) => {
|
||||
let valA = 0, valB = 0;
|
||||
|
||||
if (this.currentSort === 'rating') {
|
||||
valA = a.stats?.basic_avg_rating || 0;
|
||||
valB = b.stats?.basic_avg_rating || 0;
|
||||
} else if (this.currentSort === 'kd') {
|
||||
valA = a.stats?.basic_avg_kd || 0;
|
||||
valB = b.stats?.basic_avg_kd || 0;
|
||||
} else if (this.currentSort === 'matches') {
|
||||
// matches_played is usually on the player object now? or stats?
|
||||
// Check API: it's not explicitly in 'stats', but search added it.
|
||||
// Roster API usually doesn't attach matches_played unless we ask.
|
||||
// Let's assume stats.total_matches or check object root.
|
||||
// Looking at roster API: we attach match counts? No, only search.
|
||||
// But we can use total_matches from stats.
|
||||
valA = a.stats?.total_matches || 0;
|
||||
valB = b.stats?.total_matches || 0;
|
||||
}
|
||||
|
||||
return valB - valA; // Descending
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user