@@ -350,6 +350,10 @@ export default {
type: Object,
required: true,
},
+ ranksLoaded: {
+ type: Boolean,
+ default: false
+ }
},
computed: {
diff --git a/client/src/store/modules/detailedMatch.js b/client/src/store/modules/detailedMatch.js
index b615aa4..47c977d 100644
--- a/client/src/store/modules/detailedMatch.js
+++ b/client/src/store/modules/detailedMatch.js
@@ -14,16 +14,39 @@ export const mutations = {
state.matches.push({ matchId, status: 'loading' })
}
},
- MATCH_FOUND(state, matchDetails) {
+ MATCH_FOUND(state, {matchDetails, ranksLoaded }) {
matchDetails.status = 'loaded'
+ matchDetails.ranksLoaded = ranksLoaded
+
+ // Set SoloQ as rank for now
+ if(ranksLoaded) {
+ for (const player of matchDetails.blueTeam.players) {
+ player.rank = player.rank && player.rank[420]
+ }
+ for (const player of matchDetails.redTeam.players) {
+ player.rank = player.rank && player.rank[420]
+ }
+ }
const index = state.matches.findIndex(m => m.gameId === matchDetails.gameId)
Vue.set(state.matches, index, matchDetails)
},
- MATCH_RANKS_FOUND(state, { gameId, blueTeam, redTeam }) {
+ MATCH_RANKS_FOUND(state, { gameId, ranksByPlayer }) {
const match = state.matches.find(m => m.gameId === gameId)
- match.blueTeam.players = blueTeam
- match.redTeam.players = redTeam
+
+ for (const player of match.blueTeam.players) {
+ const ranks = ranksByPlayer[player.id]
+ if(!ranks) continue
+ Vue.set(player, 'rank', ranks[420])
+ }
+
+ for (const player of match.redTeam.players) {
+ const ranks = ranksByPlayer[player.id]
+ if(!ranks) continue
+ Vue.set(player, 'rank', ranks[420])
+ }
+
+ match.ranksLoaded = true
},
}
@@ -35,17 +58,17 @@ export const actions = {
const resp = await axios(({ url: 'match/details', data: { matchId }, method: 'POST' })).catch(() => { })
console.log('--- DETAILS INFOS ---')
console.log(resp.data)
- commit('MATCH_FOUND', resp.data.matchDetails)
+ const {matchDetails, ranksLoaded} = resp.data
+ commit('MATCH_FOUND', {matchDetails, ranksLoaded })
- // TODO: add ranks back when it's done on the API
- // // If the ranks of the players are not yet known
- // if (resp.data.matchDetails.blueTeam.players[0].rank === undefined) {
- // const ranks = await axios(({ url: 'match/details/ranks', data: { gameId, region }, method: 'POST' })).catch(() => { })
- // if (!ranks) return
- // console.log('--- RANK OF MATCH DETAILS ---')
- // console.log(ranks.data)
- // commit('MATCH_RANKS_FOUND', { gameId, ...ranks.data })
- // }
+ // If the ranks of the players are not yet known
+ if (!ranksLoaded) {
+ const ranks = await axios(({ url: 'match/details/ranks', data: { matchId }, method: 'POST' })).catch(() => { })
+ if (!ranks) return
+ console.log('--- RANK OF MATCH DETAILS ---')
+ console.log(ranks.data)
+ commit('MATCH_RANKS_FOUND', { matchId, ranksByPlayer: ranks.data })
+ }
}
}
diff --git a/server-v2/app/Controllers/Http/MatchesController.ts b/server-v2/app/Controllers/Http/MatchesController.ts
index 2cd563d..7319f41 100644
--- a/server-v2/app/Controllers/Http/MatchesController.ts
+++ b/server-v2/app/Controllers/Http/MatchesController.ts
@@ -1,6 +1,8 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Match from 'App/Models/Match'
+import MatchPlayerRankParser from 'App/Parsers/MatchPlayerRankParser'
import DetailedMatchSerializer from 'App/Serializers/DetailedMatchSerializer'
+import MatchPlayerRankSerializer from 'App/Serializers/MatchPlayerRankSerializer'
import MatchService from 'App/Services/MatchService'
import StatsService from 'App/Services/StatsService'
import DetailedMatchValidator from 'App/Validators/DetailedMatchValidator'
@@ -8,7 +10,7 @@ import MatchesIndexValidator from 'App/Validators/MatchesIndexValidator'
export default class MatchesController {
/**
- * POST - Return data from matches searched by gameIds
+ * POST - Return data from matches searched by matchIds
* @param ctx
*/
public async index({ request, response }: HttpContextContract) {
@@ -33,15 +35,33 @@ export default class MatchesController {
const match = await Match.query()
.where('id', matchId)
.preload('teams')
- .preload('players')
+ .preload('players', (playersQuery) => {
+ playersQuery.preload('ranks')
+ })
.firstOrFail()
- const matchDetails = DetailedMatchSerializer.serializeOneMatch(match)
+ const { match: matchDetails, ranksLoaded } = DetailedMatchSerializer.serializeOneMatch(match)
console.timeEnd('MatchDetails')
return response.json({
matchDetails,
+ ranksLoaded,
})
}
+
+ /**
+ * POST - Return ranks of players for a specific game
+ * @param ctx
+ */
+ public async showRanks({ request, response }: HttpContextContract) {
+ console.time('Ranks')
+ const { matchId } = await request.validate(DetailedMatchValidator)
+ const match = await Match.query().where('id', matchId).preload('players').firstOrFail()
+ const parsedRanks = await MatchPlayerRankParser.parse(match)
+ const serializedRanks = MatchPlayerRankSerializer.serialize(parsedRanks)
+
+ console.timeEnd('Ranks')
+ return response.json(serializedRanks)
+ }
}
diff --git a/server-v2/app/Controllers/Http/SummonersController.ts b/server-v2/app/Controllers/Http/SummonersController.ts
index d770ee7..ec58ec3 100644
--- a/server-v2/app/Controllers/Http/SummonersController.ts
+++ b/server-v2/app/Controllers/Http/SummonersController.ts
@@ -45,7 +45,7 @@ export default class SummonersController {
finalJSON.current = currentGame
// RANKED STATS
- finalJSON.ranked = await SummonerService.getRanked(account, region)
+ finalJSON.ranked = await SummonerService.getRanked(account.id, region)
// RECENT ACTIVITY
finalJSON.recentActivity = await StatsService.getRecentActivity(account.puuid)
@@ -78,7 +78,6 @@ export default class SummonersController {
finalJSON.matchesDetails = await MatchService.getMatches(region, matchIds, puuid)
- // TODO: STATS
console.time('STATS')
finalJSON.stats = await StatsService.getSummonerStats(puuid, season)
console.timeEnd('STATS')
diff --git a/server-v2/app/Models/MatchPlayer.ts b/server-v2/app/Models/MatchPlayer.ts
index b7006b7..e43bf6d 100644
--- a/server-v2/app/Models/MatchPlayer.ts
+++ b/server-v2/app/Models/MatchPlayer.ts
@@ -1,5 +1,6 @@
-import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'
+import { BaseModel, BelongsTo, belongsTo, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm'
import Match from './Match'
+import MatchPlayerRank from './MatchPlayerRank'
import Summoner from './Summoner'
export default class MatchPlayer extends BaseModel {
@@ -12,6 +13,12 @@ export default class MatchPlayer extends BaseModel {
@belongsTo(() => Match)
public match: BelongsTo
+ @hasMany(() => MatchPlayerRank, {
+ localKey: 'id',
+ foreignKey: 'playerId',
+ })
+ public ranks: HasMany
+
@column()
public participantId: number
diff --git a/server-v2/app/Models/MatchPlayerRank.ts b/server-v2/app/Models/MatchPlayerRank.ts
new file mode 100644
index 0000000..0f5f366
--- /dev/null
+++ b/server-v2/app/Models/MatchPlayerRank.ts
@@ -0,0 +1,38 @@
+import { DateTime } from 'luxon'
+import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'
+import MatchPlayer from './MatchPlayer'
+
+export default class MatchPlayerRank extends BaseModel {
+ @column({ isPrimary: true })
+ public id: number
+
+ @column()
+ public playerId: number
+
+ @belongsTo(() => MatchPlayer, {
+ localKey: 'id',
+ foreignKey: 'playerId',
+ })
+ public player: BelongsTo
+
+ @column()
+ public gamemode: number
+
+ @column()
+ public tier: string
+
+ @column()
+ public rank: number
+
+ @column()
+ public lp: number
+
+ @column()
+ public wins: number
+
+ @column()
+ public losses: number
+
+ @column.dateTime({ autoCreate: true })
+ public createdAt: DateTime
+}
diff --git a/server-v2/app/Parsers/MatchPlayerRankParser.ts b/server-v2/app/Parsers/MatchPlayerRankParser.ts
new file mode 100644
index 0000000..bf852e7
--- /dev/null
+++ b/server-v2/app/Parsers/MatchPlayerRankParser.ts
@@ -0,0 +1,43 @@
+import Database from '@ioc:Adonis/Lucid/Database'
+import { notEmpty } from 'App/helpers'
+import Match from 'App/Models/Match'
+import MatchPlayer from 'App/Models/MatchPlayer'
+import SummonerService from 'App/Services/SummonerService'
+import { PlayerRankParsed } from './ParsedType'
+
+class MatchPlayerRankParser {
+ public async parse(match: Match): Promise {
+ const requests = match.players.map((p) => SummonerService.getRanked(p.summonerId, match.region))
+ const ranks = await Promise.all(requests)
+
+ const parsedRanks = ranks
+ .map((rank) => {
+ return Object.entries(rank).map(([queue, data]) => {
+ let player: MatchPlayer | undefined
+ if (!data || !(player = match.players.find((p) => p.summonerId === data.summonerId))) {
+ return
+ }
+
+ const rank: PlayerRankParsed = {
+ player_id: player.id,
+ gamemode: queue === 'soloQ' ? 420 : 440,
+ tier: data.tier,
+ rank: SummonerService.leaguesNumbers[data.rank],
+ lp: data.leaguePoints,
+ wins: data.wins,
+ losses: data.losses,
+ }
+ return rank
+ })
+ })
+ .flat()
+ .filter(notEmpty)
+
+ // Store ranks in DB
+ await Database.table('match_player_ranks').multiInsert(parsedRanks)
+
+ return parsedRanks
+ }
+}
+
+export default new MatchPlayerRankParser()
diff --git a/server-v2/app/Parsers/ParsedType.ts b/server-v2/app/Parsers/ParsedType.ts
index 2e2d0da..01ee5f5 100644
--- a/server-v2/app/Parsers/ParsedType.ts
+++ b/server-v2/app/Parsers/ParsedType.ts
@@ -15,3 +15,13 @@ export enum TeamPosition {
BOTTOM,
UTILITY,
}
+
+export interface PlayerRankParsed {
+ player_id: number
+ gamemode: number
+ tier: string
+ rank: number
+ lp: number
+ wins: number
+ losses: number
+}
diff --git a/server-v2/app/Serializers/DetailedMatchSerializer.ts b/server-v2/app/Serializers/DetailedMatchSerializer.ts
index aa01c4f..2d429c9 100644
--- a/server-v2/app/Serializers/DetailedMatchSerializer.ts
+++ b/server-v2/app/Serializers/DetailedMatchSerializer.ts
@@ -75,11 +75,19 @@ class DetailedMatchSerializer extends MatchSerializer {
).toFixed(1) + '%',
dmgTaken: +((player.damageTaken * 100) / teamStats.dmgTaken).toFixed(1) + '%',
}
+ const rank = player.ranks.length
+ ? player.ranks.reduce((acc, rank) => {
+ acc[rank.gamemode] = this.getPlayerRank(rank)
+ return acc
+ }, {})
+ : undefined
return {
...this.getPlayerBase(player),
...this.getRuneIcons(player.perksSelected, player.perksSecondaryStyle),
+ id: player.id,
stats,
percentStats,
+ rank,
}
})
.sort(sortTeamByRole)
@@ -106,18 +114,24 @@ class DetailedMatchSerializer extends MatchSerializer {
}
}
- public serializeOneMatch(match: Match): SerializedDetailedMatch {
+ public serializeOneMatch(match: Match): { match: SerializedDetailedMatch; ranksLoaded: boolean } {
const blueTeam = match.teams.find((team) => team.color === 100)!
const redTeam = match.teams.find((team) => team.color === 200)!
const bluePlayers: MatchPlayer[] = []
const redPlayers: MatchPlayer[] = []
+ let ranksLoaded = false
+
for (const p of match.players) {
p.team === 100 ? bluePlayers.push(p) : redPlayers.push(p)
+
+ if (p.ranks.length) {
+ ranksLoaded = true
+ }
}
- return {
+ const serializedMatch = {
blueTeam: this.getTeamDetailed(blueTeam, bluePlayers, match.gameDuration),
date: match.date,
matchId: match.id,
@@ -128,6 +142,11 @@ class DetailedMatchSerializer extends MatchSerializer {
season: match.season,
time: match.gameDuration,
}
+
+ return {
+ match: serializedMatch,
+ ranksLoaded,
+ }
}
}
diff --git a/server-v2/app/Serializers/MatchPlayerRankSerializer.ts b/server-v2/app/Serializers/MatchPlayerRankSerializer.ts
new file mode 100644
index 0000000..4550afb
--- /dev/null
+++ b/server-v2/app/Serializers/MatchPlayerRankSerializer.ts
@@ -0,0 +1,20 @@
+import { PlayerRankParsed } from 'App/Parsers/ParsedType'
+import MatchSerializer from './MatchSerializer'
+import { SerializedPlayerRanksList } from './SerializedTypes'
+
+class MatchPlayerRankSerializer extends MatchSerializer {
+ public serialize(ranks: PlayerRankParsed[]): SerializedPlayerRanksList {
+ const result = ranks.reduce((acc, rank) => {
+ if (!acc[rank.player_id]) {
+ acc[rank.player_id] = {}
+ }
+
+ acc[rank.player_id][rank.gamemode] = this.getPlayerRank(rank)
+ return acc
+ }, {} as SerializedPlayerRanksList)
+
+ return result
+ }
+}
+
+export default new MatchPlayerRankSerializer()
diff --git a/server-v2/app/Serializers/MatchSerializer.ts b/server-v2/app/Serializers/MatchSerializer.ts
index 4025102..e615e52 100644
--- a/server-v2/app/Serializers/MatchSerializer.ts
+++ b/server-v2/app/Serializers/MatchSerializer.ts
@@ -1,6 +1,7 @@
import MatchPlayer from 'App/Models/MatchPlayer'
-import { TeamPosition } from 'App/Parsers/ParsedType'
+import { PlayerRankParsed, TeamPosition } from 'App/Parsers/ParsedType'
import CDragonService from 'App/Services/CDragonService'
+import SummonerService from 'App/Services/SummonerService'
import {
SerializedBasePlayer,
SerializedMatchChampion,
@@ -99,4 +100,15 @@ export default abstract class MatchSerializer {
summonerSpell2: this.getSummonerSpell(player.summoner2Id),
}
}
+
+ protected getPlayerRank(rank: PlayerRankParsed) {
+ return {
+ tier: rank.tier,
+ rank: rank.rank,
+ lp: rank.lp,
+ wins: rank.wins,
+ losses: rank.losses,
+ shortName: SummonerService.getRankedShortName(rank),
+ }
+ }
}
diff --git a/server-v2/app/Serializers/SerializedTypes.ts b/server-v2/app/Serializers/SerializedTypes.ts
index ec104c9..a8a852d 100644
--- a/server-v2/app/Serializers/SerializedTypes.ts
+++ b/server-v2/app/Serializers/SerializedTypes.ts
@@ -121,6 +121,7 @@ export interface SerializedDetailedMatchBan {
}
export interface SerializedDetailedMatchPlayer extends SerializedBasePlayer {
+ id: number
stats: SerializedDetailedMatchStats
percentStats: SerializedDetailedMatchPercentStats
primaryRune: string | null
@@ -160,3 +161,20 @@ export interface SerializedDetailedMatchPercentStats {
minions: number
vision: number
}
+
+export interface SerializedPlayerRanksList {
+ [summonerId: string]: SerializedPlayerRanks
+}
+
+export interface SerializedPlayerRanks {
+ [gamemode: number]: SerializedPlayerRank
+}
+
+export interface SerializedPlayerRank {
+ tier: string
+ rank: number
+ lp: number
+ wins: number
+ losses: number
+ shortName: number | string
+}
diff --git a/server-v2/app/Services/SummonerService.ts b/server-v2/app/Services/SummonerService.ts
index f17f057..2026e59 100644
--- a/server-v2/app/Services/SummonerService.ts
+++ b/server-v2/app/Services/SummonerService.ts
@@ -2,6 +2,8 @@ import Jax from './Jax'
import { SummonerDTO } from 'App/Services/Jax/src/Endpoints/SummonerEndpoint'
import { LeagueEntryDTO } from './Jax/src/Endpoints/LeagueEndpoint'
import Summoner from 'App/Models/Summoner'
+import { PlayerRankParsed } from 'App/Parsers/ParsedType'
+import MatchPlayerRank from 'App/Models/MatchPlayerRank'
export interface LeagueEntriesByQueue {
soloQ?: LeagueEntryByQueue
@@ -15,8 +17,16 @@ export interface LeagueEntryByQueue extends LeagueEntryDTO {
}
class SummonerService {
- private uniqueLeagues = ['CHALLENGER', 'GRANDMASTER', 'MASTER']
- private leaguesNumbers = { I: 1, II: 2, III: 3, IV: 4 }
+ private readonly uniqueLeagues = ['CHALLENGER', 'GRANDMASTER', 'MASTER']
+ public readonly leaguesNumbers = { I: 1, II: 2, III: 3, IV: 4 }
+
+ public getRankedShortName(rank: PlayerRankParsed | MatchPlayerRank) {
+ return this.uniqueLeagues.includes(rank.tier) ? rank.lp : rank.tier[0] + rank.rank
+ }
+
+ public getWinrate(wins: number, losses: number) {
+ return +((wins * 100) / (wins + losses)).toFixed(1) + '%'
+ }
/**
* Helper to transform League Data from the Riot API
@@ -29,7 +39,7 @@ class SummonerService {
const fullRank = this.uniqueLeagues.includes(league.tier)
? league.tier
: `${league.tier} ${league.rank}`
- const winrate = +((league.wins * 100) / (league.wins + league.losses)).toFixed(1) + '%'
+ const winrate = this.getWinrate(league.wins, league.losses)
const shortName = this.uniqueLeagues.includes(league.tier)
? league.leaguePoints
: league.tier[0] + this.leaguesNumbers[league.rank]
@@ -70,8 +80,8 @@ class SummonerService {
* @param account
* @param region
*/
- public async getRanked(account: SummonerDTO, region: string): Promise {
- const ranked = await Jax.League.summonerID(account.id, region)
+ public async getRanked(summonerId: string, region: string): Promise {
+ const ranked = await Jax.League.summonerID(summonerId, region)
const result: LeagueEntriesByQueue = {}
if (ranked && ranked.length) {
@@ -80,7 +90,6 @@ class SummonerService {
result.flex5v5 =
this.getleagueData(ranked.find((e) => e.queueType === 'RANKED_FLEX_SR')) || undefined
}
-
return result
}
}
diff --git a/server-v2/app/helpers.ts b/server-v2/app/helpers.ts
index cf539f7..fc01650 100644
--- a/server-v2/app/helpers.ts
+++ b/server-v2/app/helpers.ts
@@ -110,3 +110,10 @@ export function sortTeamByRole(a: T, b: T) {
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
return sortingArr.indexOf(a.role) - sortingArr.indexOf(b.role)
}
+
+// https://stackoverflow.com/a/46700791/9188650
+export function notEmpty(value: TValue | null | undefined): value is TValue {
+ if (value === null || value === undefined) return false
+ const testDummy: TValue = value
+ return true
+}
diff --git a/server-v2/database/migrations/1631807093726_match_player_ranks.ts b/server-v2/database/migrations/1631807093726_match_player_ranks.ts
new file mode 100644
index 0000000..6cbf686
--- /dev/null
+++ b/server-v2/database/migrations/1631807093726_match_player_ranks.ts
@@ -0,0 +1,29 @@
+import BaseSchema from '@ioc:Adonis/Lucid/Schema'
+
+export default class MatchPlayerRanks extends BaseSchema {
+ protected tableName = 'match_player_ranks'
+
+ public async up() {
+ this.schema.createTable(this.tableName, (table) => {
+ table.increments('id')
+
+ table.integer('player_id').unsigned().notNullable()
+
+ table.integer('gamemode').notNullable()
+ table.string('tier', 11).notNullable()
+ table.integer('rank').notNullable()
+ table.integer('lp').notNullable()
+ table.integer('wins').notNullable()
+ table.integer('losses').notNullable()
+
+ /**
+ * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
+ */
+ table.timestamp('created_at', { useTz: true })
+ })
+ }
+
+ public async down() {
+ this.schema.dropTable(this.tableName)
+ }
+}
diff --git a/server-v2/start/routes.ts b/server-v2/start/routes.ts
index a20f3ad..9e0abe0 100644
--- a/server-v2/start/routes.ts
+++ b/server-v2/start/routes.ts
@@ -37,6 +37,6 @@ Route.post('/summoner/records', 'SummonersController.records')
Route.post('/match', 'MatchesController.index')
Route.post('/match/details', 'MatchesController.show')
-// Route.post('/match/details/ranks', 'MatchesController.showRanks')
+Route.post('/match/details/ranks', 'MatchesController.showRanks')
Route.get('/cdragon/runes', 'CDragonController.runes')