diff --git a/server-new/app/Controllers/Http/MatchesController.ts b/server-new/app/Controllers/Http/MatchesController.ts index 9a74267..2cc41e9 100644 --- a/server-new/app/Controllers/Http/MatchesController.ts +++ b/server-new/app/Controllers/Http/MatchesController.ts @@ -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({ 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, + }) + } } diff --git a/server-new/app/Models/DetailedMatch.ts b/server-new/app/Models/DetailedMatch.ts index 9a7caa2..f4e9e3b 100644 --- a/server-new/app/Models/DetailedMatch.ts +++ b/server-new/app/Models/DetailedMatch.ts @@ -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 } 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 diff --git a/server-new/app/Models/Match.ts b/server-new/app/Models/Match.ts index 85c2525..489b2e8 100644 --- a/server-new/app/Models/Match.ts +++ b/server-new/app/Models/Match.ts @@ -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 { + 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 { diff --git a/server-new/app/Transformers/DetailedMatchTransformer.ts b/server-new/app/Transformers/DetailedMatchTransformer.ts new file mode 100644 index 0000000..cbd085a --- /dev/null +++ b/server-new/app/Transformers/DetailedMatchTransformer.ts @@ -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 = (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 { + 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() diff --git a/server-new/app/Transformers/MatchTransformer.ts b/server-new/app/Transformers/MatchTransformer.ts index 17b3dbc..c006cfc 100644 --- a/server-new/app/Transformers/MatchTransformer.ts +++ b/server-new/app/Transformers/MatchTransformer.ts @@ -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 } diff --git a/server-new/app/Validators/DetailedMatchValidator.ts b/server-new/app/Validators/DetailedMatchValidator.ts new file mode 100644 index 0000000..98b7f69 --- /dev/null +++ b/server-new/app/Validators/DetailedMatchValidator.ts @@ -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 = {} +} diff --git a/server-new/app/helpers.ts b/server-new/app/helpers.ts index 9fa9548..378eaf7 100644 --- a/server-new/app/helpers.ts +++ b/server-new/app/helpers.ts @@ -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) }