mirror of
https://github.com/vkaelin/LeagueStats.git
synced 2026-03-25 12:57:28 +00:00
feat: add match details endpoints
This commit is contained in:
parent
749e9eda05
commit
0fffc8127f
7 changed files with 266 additions and 27 deletions
|
|
@ -1,10 +1,34 @@
|
||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
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 Summoner from 'App/Models/Summoner'
|
||||||
|
import Jax from 'App/Services/Jax'
|
||||||
import MatchService from 'App/Services/MatchService'
|
import MatchService from 'App/Services/MatchService'
|
||||||
import StatsService from 'App/Services/StatsService'
|
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'
|
import MatchesIndexValidator from 'App/Validators/MatchesIndexValidator'
|
||||||
|
|
||||||
export default class MatchesController {
|
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
|
* POST - Return data from matches searched by gameIds
|
||||||
* @param ctx
|
* @param ctx
|
||||||
|
|
@ -28,4 +52,61 @@ export default class MatchesController {
|
||||||
stats,
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Model } from '@ioc:Mongodb/Model'
|
||||||
import { Champion, ParticipantDetails } from 'App/Models/Match'
|
import { Champion, ParticipantDetails } from 'App/Models/Match'
|
||||||
|
|
||||||
export interface DetailedMatchModel {
|
export interface DetailedMatchModel {
|
||||||
gameId: string,
|
gameId: number,
|
||||||
season: number,
|
season: number,
|
||||||
blueTeam: Team,
|
blueTeam: Team,
|
||||||
redTeam: Team,
|
redTeam: Team,
|
||||||
|
|
@ -26,10 +26,10 @@ interface Team {
|
||||||
towers: number
|
towers: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Ban {
|
export interface Ban {
|
||||||
championID: number,
|
championId: number,
|
||||||
pickTurn: number,
|
pickTurn: number,
|
||||||
champion: Champion
|
champion: Champion<null, null>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TeamStats {
|
interface TeamStats {
|
||||||
|
|
@ -45,7 +45,7 @@ interface TeamStats {
|
||||||
export default class DetailedMatch extends Model implements DetailedMatchModel {
|
export default class DetailedMatch extends Model implements DetailedMatchModel {
|
||||||
public static collectionName = 'detailed_matches'
|
public static collectionName = 'detailed_matches'
|
||||||
|
|
||||||
public gameId: string
|
public gameId: number
|
||||||
public season: number
|
public season: number
|
||||||
public blueTeam: Team
|
public blueTeam: Team
|
||||||
public redTeam: Team
|
public redTeam: Team
|
||||||
|
|
|
||||||
|
|
@ -25,19 +25,19 @@ export interface ParticipantDetails {
|
||||||
secondaryRune: string | null,
|
secondaryRune: string | null,
|
||||||
level: number,
|
level: number,
|
||||||
items: (Item | null)[],
|
items: (Item | null)[],
|
||||||
firstSum: SummonerSpell | number,
|
firstSum: SummonerSpell | number | null,
|
||||||
secondSum: SummonerSpell | number,
|
secondSum: SummonerSpell | number | null,
|
||||||
stats: Stats,
|
stats: Stats,
|
||||||
percentStats?: PercentStats
|
percentStats?: PercentStats
|
||||||
rank?: Rank
|
rank?: Rank | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Champion {
|
export interface Champion<T = number, U = string> {
|
||||||
id: number,
|
id: number | T,
|
||||||
name: string,
|
name: string | U,
|
||||||
alias: string,
|
alias?: string,
|
||||||
roles: string[],
|
roles?: string[],
|
||||||
icon: string
|
icon?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SummonerSpell {
|
export interface SummonerSpell {
|
||||||
|
|
@ -48,7 +48,7 @@ export interface SummonerSpell {
|
||||||
|
|
||||||
export interface Rank {
|
export interface Rank {
|
||||||
tier: string,
|
tier: string,
|
||||||
shortName: string
|
shortName: string | number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParticipantBasic {
|
export interface ParticipantBasic {
|
||||||
|
|
|
||||||
101
server-new/app/Transformers/DetailedMatchTransformer.ts
Normal file
101
server-new/app/Transformers/DetailedMatchTransformer.ts
Normal 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()
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { getSeasonNumber, queuesWithRole, sortTeamByRole, supportItems } from 'App/helpers'
|
import { getSeasonNumber, queuesWithRole, sortTeamByRole, supportItems } from 'App/helpers'
|
||||||
import Jax from 'App/Services/Jax'
|
import Jax from 'App/Services/Jax'
|
||||||
import { MatchDto, ParticipantDto, ParticipantTimelineDto } from 'App/Services/Jax/src/Endpoints/MatchEndpoint'
|
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'
|
import RoleIdentificationService from 'App/Services/RoleIdentiticationService'
|
||||||
|
|
||||||
export interface PlayerRole {
|
export interface PlayerRole {
|
||||||
|
|
@ -17,7 +17,7 @@ export default abstract class MatchTransformer {
|
||||||
protected perkstyles: any
|
protected perkstyles: any
|
||||||
protected summonerSpells: any
|
protected summonerSpells: any
|
||||||
protected championRoles: 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
|
* Get global Context with CDragon Data
|
||||||
*/
|
*/
|
||||||
|
|
@ -239,7 +239,11 @@ export default abstract class MatchTransformer {
|
||||||
* @param champs 5 champions + smite from the team
|
* @param champs 5 champions + smite from the team
|
||||||
* @param playerData data of the searched player, only for basic matches
|
* @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))]
|
// const actualRoles = [...new Set(team.map(p => p.role))]
|
||||||
// if (actualRoles.length === 5) {
|
// if (actualRoles.length === 5) {
|
||||||
// return
|
// return
|
||||||
|
|
@ -265,8 +269,8 @@ export default abstract class MatchTransformer {
|
||||||
*/
|
*/
|
||||||
public getMatchRoles (
|
public getMatchRoles (
|
||||||
match: MatchDto,
|
match: MatchDto,
|
||||||
allyTeam: ParticipantBasic[],
|
allyTeam: ParticipantBasic[] | ParticipantDetails[],
|
||||||
enemyTeam: ParticipantBasic[],
|
enemyTeam: ParticipantBasic[] | ParticipantDetails[],
|
||||||
allyTeamId = 100,
|
allyTeamId = 100,
|
||||||
playerData?: ParticipantDetails
|
playerData?: ParticipantDetails
|
||||||
) {
|
) {
|
||||||
|
|
@ -297,7 +301,7 @@ export default abstract class MatchTransformer {
|
||||||
* Get Summoner Spell Data from CDragon
|
* Get Summoner Spell Data from CDragon
|
||||||
* @param id of the summonerSpell
|
* @param id of the summonerSpell
|
||||||
*/
|
*/
|
||||||
public getSummonerSpell (id: number) {
|
public getSummonerSpell (id: number): SummonerSpell | null {
|
||||||
if (id === 0) {
|
if (id === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
53
server-new/app/Validators/DetailedMatchValidator.ts
Normal file
53
server-new/app/Validators/DetailedMatchValidator.ts
Normal 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 = {}
|
||||||
|
}
|
||||||
|
|
@ -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
|
* 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
|
* Get season number for a match
|
||||||
* @param timestamp
|
* @param timestamp
|
||||||
*/
|
*/
|
||||||
export function getSeasonNumber (timestamp: number) {
|
export function getSeasonNumber (timestamp: number): number {
|
||||||
const arrSeasons = Object.keys(seasons).map(k => Number(k))
|
const arrSeasons = Object.keys(seasons).map(k => Number(k))
|
||||||
arrSeasons.push(timestamp)
|
arrSeasons.push(timestamp)
|
||||||
arrSeasons.sort()
|
arrSeasons.sort()
|
||||||
|
|
@ -38,11 +38,11 @@ export function getSeasonNumber (timestamp: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort array of Roles according to a specific order
|
* Sort array of Players by roles according to a specific order
|
||||||
* @param a first role
|
* @param a first player
|
||||||
* @param b second role
|
* @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']
|
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
|
||||||
return sortingArr.indexOf(a.role) - sortingArr.indexOf(b.role)
|
return sortingArr.indexOf(a.role) - sortingArr.indexOf(b.role)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue