mirror of
https://github.com/vkaelin/LeagueStats.git
synced 2026-03-25 21:07:27 +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 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
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 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
|
||||
}
|
||||
|
|
|
|||
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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue