feat: add ranks back in match-details

This commit is contained in:
Kalane 2021-09-16 22:29:20 +02:00
parent 973355c201
commit 6f0beec0d2
17 changed files with 291 additions and 31 deletions

View file

@ -5,6 +5,7 @@
:data="allyTeam"
:all-players="[...allyTeam.players, ...enemyTeam.players]"
:ally-team="true"
:ranks-loaded="data.ranksLoaded"
/>
<div class="flex items-start justify-between px-3 py-2">
@ -23,6 +24,7 @@
:data="enemyTeam"
:all-players="[...allyTeam.players, ...enemyTeam.players]"
:ally-team="false"
:ranks-loaded="data.ranksLoaded"
/>
</div>
<div v-else-if="data.status === 'loading' && detailsOpen">

View file

@ -228,7 +228,7 @@
{{ player.rank.shortName }}
</div>
</div>
<div v-else-if="player.rank === undefined">
<div v-else-if="!ranksLoaded">
<DotsLoader width="30px" dot-width="10px" />
</div>
<div v-else class="w-5 h-5">
@ -350,6 +350,10 @@ export default {
type: Object,
required: true,
},
ranksLoaded: {
type: Boolean,
default: false
}
},
computed: {

View file

@ -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 })
}
}
}

View file

@ -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)
}
}

View file

@ -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')

View file

@ -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<typeof Match>
@hasMany(() => MatchPlayerRank, {
localKey: 'id',
foreignKey: 'playerId',
})
public ranks: HasMany<typeof MatchPlayerRank>
@column()
public participantId: number

View file

@ -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<typeof MatchPlayer>
@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
}

View file

@ -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<PlayerRankParsed[]> {
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()

View file

@ -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
}

View file

@ -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,
}
}
}

View file

@ -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()

View file

@ -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),
}
}
}

View file

@ -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
}

View file

@ -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<LeagueEntriesByQueue> {
const ranked = await Jax.League.summonerID(account.id, region)
public async getRanked(summonerId: string, region: string): Promise<LeagueEntriesByQueue> {
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
}
}

View file

@ -110,3 +110,10 @@ export function sortTeamByRole<T extends SortableByRole>(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<TValue>(value: TValue | null | undefined): value is TValue {
if (value === null || value === undefined) return false
const testDummy: TValue = value
return true
}

View file

@ -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)
}
}

View file

@ -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')