feat: add match details endpoints

This commit is contained in:
Valentin Kaelin 2020-10-11 17:31:16 +02:00
parent 749e9eda05
commit 0fffc8127f
7 changed files with 266 additions and 27 deletions

View file

@ -1,10 +1,34 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import mongodb from '@ioc:Mongodb/Database'
import DetailedMatch, { DetailedMatchModel } from 'App/Models/DetailedMatch'
import { ParticipantDetails } from 'App/Models/Match'
import Summoner from 'App/Models/Summoner'
import Jax from 'App/Services/Jax'
import MatchService from 'App/Services/MatchService'
import StatsService from 'App/Services/StatsService'
import SummonerService from 'App/Services/SummonerService'
import DetailedMatchTransformer from 'App/Transformers/DetailedMatchTransformer'
import DetailedMatchValidator from 'App/Validators/DetailedMatchValidator'
import MatchesIndexValidator from 'App/Validators/MatchesIndexValidator'
export default class MatchesController {
/**
* Get the soloQ rank of all the players of the team
* @param summoner all the data of the summoner
* @param region of the match
*/
private async getPlayerRank (summoner: ParticipantDetails, region: string) {
const account = await SummonerService.getAccount(summoner.name, region)
if (account) {
const ranked = await SummonerService.getRanked(account, region)
summoner.rank = ranked.soloQ ? (({ tier, shortName }) => ({ tier, shortName }))(ranked.soloQ) : null
} else {
summoner.rank = null
}
return summoner
}
/**
* POST - Return data from matches searched by gameIds
* @param ctx
@ -28,4 +52,61 @@ export default class MatchesController {
stats,
})
}
/**
* POST - Return details data for one specific match
* @param ctx
*/
public async show ({ request, response }: HttpContextContract) {
console.time('MatchDetails')
const { gameId, region } = await request.validate(DetailedMatchValidator)
let matchDetails: DetailedMatchModel
// TODO: replace it with Match Model once the package is fixed
const detailedMatchesCollection = await mongodb.connection().collection('detailed_matches')
const alreadySaved = await detailedMatchesCollection.findOne<DetailedMatchModel>({ gameId, region })
if (alreadySaved) {
console.log('MATCH DETAILS ALREADY SAVED')
matchDetails = alreadySaved
} else {
const match = await Jax.Match.get(gameId, region)
matchDetails = await DetailedMatchTransformer.transform(match)
await DetailedMatch.create(matchDetails)
}
console.timeEnd('MatchDetails')
return response.json({
matchDetails,
})
}
/**
* POST - Return ranks of players for a specific game
* @param ctx
*/
public async showRanks ({ request, response }: HttpContextContract) {
console.time('Ranks')
const { gameId, region } = await request.validate(DetailedMatchValidator)
let matchDetails = await DetailedMatch.findOne({ gameId, region })
if (!matchDetails) {
return response.json(null)
}
const requestsBlue = matchDetails.blueTeam.players.map(p => this.getPlayerRank(p, region))
matchDetails.blueTeam.players = await Promise.all(requestsBlue)
const requestsRed = matchDetails.redTeam.players.map(p => this.getPlayerRank(p, region))
matchDetails.redTeam.players = await Promise.all(requestsRed)
matchDetails.save()
console.timeEnd('Ranks')
return response.json({
blueTeam: matchDetails.blueTeam.players,
redTeam: matchDetails.redTeam.players,
})
}
}

View file

@ -2,7 +2,7 @@ import { Model } from '@ioc:Mongodb/Model'
import { Champion, ParticipantDetails } from 'App/Models/Match'
export interface DetailedMatchModel {
gameId: string,
gameId: number,
season: number,
blueTeam: Team,
redTeam: Team,
@ -26,10 +26,10 @@ interface Team {
towers: number
}
interface Ban {
championID: number,
export interface Ban {
championId: number,
pickTurn: number,
champion: Champion
champion: Champion<null, null>
}
interface TeamStats {
@ -45,7 +45,7 @@ interface TeamStats {
export default class DetailedMatch extends Model implements DetailedMatchModel {
public static collectionName = 'detailed_matches'
public gameId: string
public gameId: number
public season: number
public blueTeam: Team
public redTeam: Team

View file

@ -25,19 +25,19 @@ export interface ParticipantDetails {
secondaryRune: string | null,
level: number,
items: (Item | null)[],
firstSum: SummonerSpell | number,
secondSum: SummonerSpell | number,
firstSum: SummonerSpell | number | null,
secondSum: SummonerSpell | number | null,
stats: Stats,
percentStats?: PercentStats
rank?: Rank
rank?: Rank | null
}
export interface Champion {
id: number,
name: string,
alias: string,
roles: string[],
icon: string
export interface Champion<T = number, U = string> {
id: number | T,
name: string | U,
alias?: string,
roles?: string[],
icon?: string
}
export interface SummonerSpell {
@ -48,7 +48,7 @@ export interface SummonerSpell {
export interface Rank {
tier: string,
shortName: string
shortName: string | number
}
export interface ParticipantBasic {

View file

@ -0,0 +1,101 @@
import { Ban, DetailedMatchModel } from 'App/Models/DetailedMatch'
import { Champion } from 'App/Models/Match'
import { MatchDto, TeamStatsDto } from 'App/Services/Jax/src/Endpoints/MatchEndpoint'
import MatchTransformer from './MatchTransformer'
/**
* DetailedMatchTransformer class
*
* @class DetailedMatchTransformer
*/
class DetailedMatchTransformer extends MatchTransformer {
/**
* Get all data of one team
* @param match raw match data from Riot API
* @param team raw team data from Riot API
*/
private getTeamData (match: MatchDto, team: TeamStatsDto) {
let win = team.win
if (match.gameDuration < 300) {
win = 'Remake'
}
// Global stats of the team
const teamPlayers = match.participants.filter(p => p.teamId === team.teamId)
const teamStats = teamPlayers.reduce((prev, cur) => {
prev.kills += cur.stats.kills
prev.deaths += cur.stats.deaths
prev.assists += cur.stats.assists
prev.gold += cur.stats.goldEarned
prev.dmgChamp += cur.stats.totalDamageDealtToChampions
prev.dmgObj += cur.stats.damageDealtToObjectives
prev.dmgTaken += cur.stats.totalDamageTaken
return prev
}, { kills: 0, deaths: 0, assists: 0, gold: 0, dmgChamp: 0, dmgObj: 0, dmgTaken: 0 })
// Bans
const bans: Ban[] = []
if (team.bans) {
for (const ban of team.bans) {
const champion: Champion<null, null> = (ban.championId === -1)
? { id: null, name: null }
: super.getChampion(ban.championId)
bans.push({
...ban,
champion,
})
}
}
// Players
let players = teamPlayers
.map(p => super.getPlayerData(match, p, true, teamStats))
.map(p => {
p.firstSum = super.getSummonerSpell(p.firstSum as number)
p.secondSum = super.getSummonerSpell(p.secondSum as number)
return p
})
.sort(this.sortTeamByRole)
return {
bans,
barons: team.baronKills,
color: team.teamId === 100 ? 'Blue' : 'Red',
dragons: team.dragonKills,
inhibitors: team.inhibitorKills,
players,
result: win,
riftHerald: team.riftHeraldKills,
teamStats,
towers: team.towerKills,
}
}
/**
* Transform raw data from Riot API
* @param match data from Riot API
*/
public async transform (match: MatchDto): Promise<DetailedMatchModel> {
await super.getContext()
// Global data
const globalInfos = super.getGameInfos(match)
// Teams
const firstTeam = this.getTeamData(match, match.teams[0])
const secondTeam = this.getTeamData(match, match.teams[1])
// Roles
super.getMatchRoles(match, firstTeam.players, secondTeam.players)
return {
gameId: match.gameId,
blueTeam: firstTeam.color === 'Blue' ? firstTeam : secondTeam,
redTeam: firstTeam.color === 'Blue' ? secondTeam : firstTeam,
...globalInfos,
}
}
}
export default new DetailedMatchTransformer()

View file

@ -1,7 +1,7 @@
import { getSeasonNumber, queuesWithRole, sortTeamByRole, supportItems } from 'App/helpers'
import Jax from 'App/Services/Jax'
import { MatchDto, ParticipantDto, ParticipantTimelineDto } from 'App/Services/Jax/src/Endpoints/MatchEndpoint'
import { Item, ParticipantBasic, ParticipantDetails, PercentStats, Stats } from 'App/Models/Match'
import { Item, ParticipantBasic, ParticipantDetails, PercentStats, Stats, SummonerSpell } from 'App/Models/Match'
import RoleIdentificationService from 'App/Services/RoleIdentiticationService'
export interface PlayerRole {
@ -17,7 +17,7 @@ export default abstract class MatchTransformer {
protected perkstyles: any
protected summonerSpells: any
protected championRoles: any
protected sortTeamByRole: (a: ParticipantBasic, b: ParticipantBasic) => number
protected sortTeamByRole: (a: ParticipantBasic | ParticipantDetails, b: ParticipantBasic | ParticipantDetails) => number
/**
* Get global Context with CDragon Data
*/
@ -239,7 +239,11 @@ export default abstract class MatchTransformer {
* @param champs 5 champions + smite from the team
* @param playerData data of the searched player, only for basic matches
*/
public updateTeamRoles (team: ParticipantBasic[], champs: PlayerRole[], playerData?: ParticipantDetails) {
public updateTeamRoles (
team: ParticipantBasic[] | ParticipantDetails[],
champs: PlayerRole[],
playerData?: ParticipantDetails
) {
// const actualRoles = [...new Set(team.map(p => p.role))]
// if (actualRoles.length === 5) {
// return
@ -265,8 +269,8 @@ export default abstract class MatchTransformer {
*/
public getMatchRoles (
match: MatchDto,
allyTeam: ParticipantBasic[],
enemyTeam: ParticipantBasic[],
allyTeam: ParticipantBasic[] | ParticipantDetails[],
enemyTeam: ParticipantBasic[] | ParticipantDetails[],
allyTeamId = 100,
playerData?: ParticipantDetails
) {
@ -297,7 +301,7 @@ export default abstract class MatchTransformer {
* Get Summoner Spell Data from CDragon
* @param id of the summonerSpell
*/
public getSummonerSpell (id: number) {
public getSummonerSpell (id: number): SummonerSpell | null {
if (id === 0) {
return null
}

View file

@ -0,0 +1,53 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
export default class DetailedMatchValidator {
constructor (private ctx: HttpContextContract) {
}
/**
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
*
* For example:
* 1. The username must be of data type string. But then also, it should
* not contain special characters or numbers.
* ```
* schema.string({}, [ rules.alpha() ])
* ```
*
* 2. The email must be of data type string, formatted as a valid
* email. But also, not used by any other user.
* ```
* schema.string({}, [
* rules.email(),
* rules.unique({ table: 'users', column: 'email' }),
* ])
* ```
*/
public schema = schema.create({
gameId: schema.number(),
region: schema.string(),
})
/**
* The `schema` first gets compiled to a reusable function and then that compiled
* function validates the data at runtime.
*
* Since, compiling the schema is an expensive operation, you must always cache it by
* defining a unique cache key. The simplest way is to use the current request route
* key, which is a combination of the route pattern and HTTP method.
*/
public cacheKey = this.ctx.routeKey
/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*/
public messages = {}
}

View file

@ -1,4 +1,4 @@
import { ParticipantBasic } from './Models/Match'
import { ParticipantBasic, ParticipantDetails } from './Models/Match'
/**
* League of Legends queues with defined role for each summoner
@ -29,7 +29,7 @@ export const supportItems = [3850, 3851, 3853, 3854, 3855, 3857, 3858, 3859, 386
* Get season number for a match
* @param timestamp
*/
export function getSeasonNumber (timestamp: number) {
export function getSeasonNumber (timestamp: number): number {
const arrSeasons = Object.keys(seasons).map(k => Number(k))
arrSeasons.push(timestamp)
arrSeasons.sort()
@ -38,11 +38,11 @@ export function getSeasonNumber (timestamp: number) {
}
/**
* Sort array of Roles according to a specific order
* @param a first role
* @param b second role
* Sort array of Players by roles according to a specific order
* @param a first player
* @param b second player
*/
export function sortTeamByRole (a:ParticipantBasic, b:ParticipantBasic) {
export function sortTeamByRole (a: ParticipantBasic | ParticipantDetails, b: ParticipantBasic | ParticipantDetails) {
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
return sortingArr.indexOf(a.role) - sortingArr.indexOf(b.role)
}