Skip to content

Commit c3ba74b

Browse files
committed
feat: implement Analytics module with performance insights
- Add PerformanceController with team overview and trends - Add ChampionsController for champion pool analysis - Add KdaTrendController for KDA tracking over time - Add LaningController for CS and gold performance - Add TeamfightsController for damage and participation stats - Add VisionController for vision score analysis - Add TeamComparisonController for player comparisons
1 parent dd03e40 commit c3ba74b

7 files changed

Lines changed: 507 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class Api::V1::Analytics::ChampionsController < Api::V1::BaseController
2+
def show
3+
player = organization_scoped(Player).find(params[:player_id])
4+
champion_pools = player.champion_pools.order(games_played: :desc)
5+
6+
champion_data = {
7+
player: PlayerSerializer.render_as_hash(player),
8+
champion_pool: ChampionPoolSerializer.render_as_hash(champion_pools),
9+
top_champions: champion_pools.limit(5).map do |pool|
10+
{
11+
champion: pool.champion,
12+
games: pool.games_played,
13+
win_rate: pool.wins.to_f / pool.games_played * 100,
14+
avg_kda: pool.average_kda,
15+
mastery: pool.mastery_level
16+
}
17+
end,
18+
champion_diversity: {
19+
total_champions: champion_pools.count,
20+
highly_played: champion_pools.where('games_played >= ?', 10).count,
21+
mastery_points: champion_pools.sum(:mastery_points)
22+
}
23+
}
24+
25+
render_success(champion_data)
26+
end
27+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
class Api::V1::Analytics::KdaTrendController < Api::V1::BaseController
2+
def show
3+
player = organization_scoped(Player).find(params[:player_id])
4+
5+
# Get recent matches for the player
6+
stats = PlayerMatchStat.joins(:match)
7+
.where(player: player, match: { organization: current_organization })
8+
.order('matches.game_start DESC')
9+
.limit(50)
10+
.includes(:match)
11+
12+
trend_data = {
13+
player: PlayerSerializer.render_as_hash(player),
14+
kda_by_match: stats.map do |stat|
15+
kda = stat.deaths.zero? ? (stat.kills + stat.assists).to_f : ((stat.kills + stat.assists).to_f / stat.deaths)
16+
{
17+
match_id: stat.match.id,
18+
date: stat.match.game_start,
19+
kills: stat.kills,
20+
deaths: stat.deaths,
21+
assists: stat.assists,
22+
kda: kda.round(2),
23+
champion: stat.champion,
24+
victory: stat.match.victory
25+
}
26+
end,
27+
averages: {
28+
last_10_games: calculate_kda_average(stats.limit(10)),
29+
last_20_games: calculate_kda_average(stats.limit(20)),
30+
overall: calculate_kda_average(stats)
31+
}
32+
}
33+
34+
render_success(trend_data)
35+
end
36+
37+
private
38+
39+
def calculate_kda_average(stats)
40+
return 0 if stats.empty?
41+
42+
total_kills = stats.sum(:kills)
43+
total_deaths = stats.sum(:deaths)
44+
total_assists = stats.sum(:assists)
45+
46+
deaths = total_deaths.zero? ? 1 : total_deaths
47+
((total_kills + total_assists).to_f / deaths).round(2)
48+
end
49+
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
class Api::V1::Analytics::LaningController < Api::V1::BaseController
2+
def show
3+
player = organization_scoped(Player).find(params[:player_id])
4+
5+
stats = PlayerMatchStat.joins(:match)
6+
.where(player: player, match: { organization: current_organization })
7+
.order('matches.game_start DESC')
8+
.limit(20)
9+
10+
laning_data = {
11+
player: PlayerSerializer.render_as_hash(player),
12+
cs_performance: {
13+
avg_cs_total: stats.average('minions_killed + jungle_minions_killed')&.round(1),
14+
avg_cs_per_min: calculate_avg_cs_per_min(stats),
15+
best_cs_game: stats.maximum('minions_killed + jungle_minions_killed'),
16+
worst_cs_game: stats.minimum('minions_killed + jungle_minions_killed')
17+
},
18+
gold_performance: {
19+
avg_gold: stats.average(:gold_earned)&.round(0),
20+
best_gold_game: stats.maximum(:gold_earned),
21+
worst_gold_game: stats.minimum(:gold_earned)
22+
},
23+
cs_by_match: stats.map do |stat|
24+
match_duration_mins = stat.match.game_duration ? stat.match.game_duration / 60.0 : 25
25+
cs_total = (stat.minions_killed || 0) + (stat.jungle_minions_killed || 0)
26+
cs_per_min = cs_total / match_duration_mins
27+
28+
{
29+
match_id: stat.match.id,
30+
date: stat.match.game_start,
31+
cs_total: cs_total,
32+
cs_per_min: cs_per_min.round(1),
33+
gold: stat.gold_earned,
34+
champion: stat.champion,
35+
victory: stat.match.victory
36+
}
37+
end
38+
}
39+
40+
render_success(laning_data)
41+
end
42+
43+
private
44+
45+
def calculate_avg_cs_per_min(stats)
46+
total_cs = 0
47+
total_minutes = 0
48+
49+
stats.each do |stat|
50+
if stat.match.game_duration
51+
cs = (stat.minions_killed || 0) + (stat.jungle_minions_killed || 0)
52+
minutes = stat.match.game_duration / 60.0
53+
total_cs += cs
54+
total_minutes += minutes
55+
end
56+
end
57+
58+
return 0 if total_minutes.zero?
59+
(total_cs / total_minutes).round(1)
60+
end
61+
end
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
class Api::V1::Analytics::PerformanceController < Api::V1::BaseController
2+
def index
3+
# Team performance analytics
4+
matches = organization_scoped(Match)
5+
players = organization_scoped(Player).active
6+
7+
# Date range filter
8+
if params[:start_date].present? && params[:end_date].present?
9+
matches = matches.in_date_range(params[:start_date], params[:end_date])
10+
else
11+
matches = matches.recent(30) # Default to last 30 days
12+
end
13+
14+
performance_data = {
15+
overview: calculate_team_overview(matches),
16+
win_rate_trend: calculate_win_rate_trend(matches),
17+
performance_by_role: calculate_performance_by_role(matches),
18+
best_performers: identify_best_performers(players, matches),
19+
match_type_breakdown: calculate_match_type_breakdown(matches)
20+
}
21+
22+
render_success(performance_data)
23+
end
24+
25+
private
26+
27+
def calculate_team_overview(matches)
28+
stats = PlayerMatchStat.where(match: matches)
29+
30+
{
31+
total_matches: matches.count,
32+
wins: matches.victories.count,
33+
losses: matches.defeats.count,
34+
win_rate: calculate_win_rate(matches),
35+
avg_game_duration: matches.average(:game_duration)&.round(0),
36+
avg_kda: calculate_avg_kda(stats),
37+
avg_kills_per_game: stats.average(:kills)&.round(1),
38+
avg_deaths_per_game: stats.average(:deaths)&.round(1),
39+
avg_assists_per_game: stats.average(:assists)&.round(1),
40+
avg_gold_per_game: stats.average(:gold_earned)&.round(0),
41+
avg_damage_per_game: stats.average(:total_damage_dealt)&.round(0),
42+
avg_vision_score: stats.average(:vision_score)&.round(1)
43+
}
44+
end
45+
46+
def calculate_win_rate_trend(matches)
47+
# Calculate win rate for each week
48+
matches.group_by { |m| m.game_start.beginning_of_week }.map do |week, week_matches|
49+
wins = week_matches.count(&:victory?)
50+
total = week_matches.size
51+
win_rate = total.zero? ? 0 : ((wins.to_f / total) * 100).round(1)
52+
53+
{
54+
week: week.strftime('%Y-%m-%d'),
55+
matches: total,
56+
wins: wins,
57+
losses: total - wins,
58+
win_rate: win_rate
59+
}
60+
end.sort_by { |d| d[:week] }
61+
end
62+
63+
def calculate_performance_by_role(matches)
64+
stats = PlayerMatchStat.joins(:player).where(match: matches)
65+
66+
stats.group('players.role').select(
67+
'players.role',
68+
'COUNT(*) as games',
69+
'AVG(player_match_stats.kills) as avg_kills',
70+
'AVG(player_match_stats.deaths) as avg_deaths',
71+
'AVG(player_match_stats.assists) as avg_assists',
72+
'AVG(player_match_stats.gold_earned) as avg_gold',
73+
'AVG(player_match_stats.total_damage_dealt) as avg_damage',
74+
'AVG(player_match_stats.vision_score) as avg_vision'
75+
).map do |stat|
76+
{
77+
role: stat.role,
78+
games: stat.games,
79+
avg_kda: {
80+
kills: stat.avg_kills&.round(1) || 0,
81+
deaths: stat.avg_deaths&.round(1) || 0,
82+
assists: stat.avg_assists&.round(1) || 0
83+
},
84+
avg_gold: stat.avg_gold&.round(0) || 0,
85+
avg_damage: stat.avg_damage&.round(0) || 0,
86+
avg_vision: stat.avg_vision&.round(1) || 0
87+
}
88+
end
89+
end
90+
91+
def identify_best_performers(players, matches)
92+
players.map do |player|
93+
stats = PlayerMatchStat.where(player: player, match: matches)
94+
next if stats.empty?
95+
96+
{
97+
player: PlayerSerializer.render_as_hash(player),
98+
games: stats.count,
99+
avg_kda: calculate_avg_kda(stats),
100+
avg_performance_score: stats.average(:performance_score)&.round(1) || 0,
101+
mvp_count: stats.joins(:match).where(matches: { victory: true }).count
102+
}
103+
end.compact.sort_by { |p| -p[:avg_performance_score] }.take(5)
104+
end
105+
106+
def calculate_match_type_breakdown(matches)
107+
matches.group(:match_type).select(
108+
'match_type',
109+
'COUNT(*) as total',
110+
'SUM(CASE WHEN victory THEN 1 ELSE 0 END) as wins'
111+
).map do |stat|
112+
win_rate = stat.total.zero? ? 0 : ((stat.wins.to_f / stat.total) * 100).round(1)
113+
{
114+
match_type: stat.match_type,
115+
total: stat.total,
116+
wins: stat.wins,
117+
losses: stat.total - stat.wins,
118+
win_rate: win_rate
119+
}
120+
end
121+
end
122+
123+
def calculate_win_rate(matches)
124+
return 0 if matches.empty?
125+
((matches.victories.count.to_f / matches.count) * 100).round(1)
126+
end
127+
128+
def calculate_avg_kda(stats)
129+
return 0 if stats.empty?
130+
131+
total_kills = stats.sum(:kills)
132+
total_deaths = stats.sum(:deaths)
133+
total_assists = stats.sum(:assists)
134+
135+
deaths = total_deaths.zero? ? 1 : total_deaths
136+
((total_kills + total_assists).to_f / deaths).round(2)
137+
end
138+
end
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
class Api::V1::Analytics::TeamComparisonController < Api::V1::BaseController
2+
def index
3+
players = organization_scoped(Player).active.includes(:player_match_stats)
4+
5+
# Get matches in date range
6+
matches = organization_scoped(Match)
7+
if params[:start_date].present? && params[:end_date].present?
8+
matches = matches.in_date_range(params[:start_date], params[:end_date])
9+
else
10+
matches = matches.recent(30)
11+
end
12+
13+
comparison_data = {
14+
players: players.map do |player|
15+
stats = PlayerMatchStat.where(player: player, match: matches)
16+
next if stats.empty?
17+
18+
{
19+
player: PlayerSerializer.render_as_hash(player),
20+
games_played: stats.count,
21+
kda: calculate_kda(stats),
22+
avg_damage: stats.average(:total_damage_dealt)&.round(0) || 0,
23+
avg_gold: stats.average(:gold_earned)&.round(0) || 0,
24+
avg_cs: stats.average('minions_killed + jungle_minions_killed')&.round(1) || 0,
25+
avg_vision_score: stats.average(:vision_score)&.round(1) || 0,
26+
avg_performance_score: stats.average(:performance_score)&.round(1) || 0,
27+
multikills: {
28+
double: stats.sum(:double_kills),
29+
triple: stats.sum(:triple_kills),
30+
quadra: stats.sum(:quadra_kills),
31+
penta: stats.sum(:penta_kills)
32+
}
33+
}
34+
end.compact.sort_by { |p| -p[:avg_performance_score] },
35+
team_averages: calculate_team_averages(matches),
36+
role_rankings: calculate_role_rankings(players, matches)
37+
}
38+
39+
render_success(comparison_data)
40+
end
41+
42+
private
43+
44+
def calculate_kda(stats)
45+
total_kills = stats.sum(:kills)
46+
total_deaths = stats.sum(:deaths)
47+
total_assists = stats.sum(:assists)
48+
49+
deaths = total_deaths.zero? ? 1 : total_deaths
50+
((total_kills + total_assists).to_f / deaths).round(2)
51+
end
52+
53+
def calculate_team_averages(matches)
54+
all_stats = PlayerMatchStat.where(match: matches)
55+
56+
{
57+
avg_kda: calculate_kda(all_stats),
58+
avg_damage: all_stats.average(:total_damage_dealt)&.round(0) || 0,
59+
avg_gold: all_stats.average(:gold_earned)&.round(0) || 0,
60+
avg_cs: all_stats.average('minions_killed + jungle_minions_killed')&.round(1) || 0,
61+
avg_vision_score: all_stats.average(:vision_score)&.round(1) || 0
62+
}
63+
end
64+
65+
def calculate_role_rankings(players, matches)
66+
rankings = {}
67+
68+
%w[top jungle mid adc support].each do |role|
69+
role_players = players.where(role: role)
70+
role_data = role_players.map do |player|
71+
stats = PlayerMatchStat.where(player: player, match: matches)
72+
next if stats.empty?
73+
74+
{
75+
player_id: player.id,
76+
summoner_name: player.summoner_name,
77+
avg_performance: stats.average(:performance_score)&.round(1) || 0,
78+
games: stats.count
79+
}
80+
end.compact.sort_by { |p| -p[:avg_performance] }
81+
82+
rankings[role] = role_data
83+
end
84+
85+
rankings
86+
end
87+
end

0 commit comments

Comments
 (0)