diff --git a/client/src/components/Match/DetailedMatch.vue b/client/src/components/Match/DetailedMatch.vue index bdf2686..5dfd121 100644 --- a/client/src/components/Match/DetailedMatch.vue +++ b/client/src/components/Match/DetailedMatch.vue @@ -5,6 +5,7 @@ :data="allyTeam" :all-players="[...allyTeam.players, ...enemyTeam.players]" :ally-team="true" + :ranks-loaded="data.ranksLoaded" />
@@ -23,6 +24,7 @@ :data="enemyTeam" :all-players="[...allyTeam.players, ...enemyTeam.players]" :ally-team="false" + :ranks-loaded="data.ranksLoaded" />
diff --git a/client/src/components/Match/DetailedMatchTeam.vue b/client/src/components/Match/DetailedMatchTeam.vue index 8be06fe..5d72a12 100644 --- a/client/src/components/Match/DetailedMatchTeam.vue +++ b/client/src/components/Match/DetailedMatchTeam.vue @@ -228,7 +228,7 @@ {{ player.rank.shortName }}
-
+
@@ -350,6 +350,10 @@ export default { type: Object, required: true, }, + ranksLoaded: { + type: Boolean, + default: false + } }, computed: { diff --git a/client/src/store/modules/detailedMatch.js b/client/src/store/modules/detailedMatch.js index b615aa4..47c977d 100644 --- a/client/src/store/modules/detailedMatch.js +++ b/client/src/store/modules/detailedMatch.js @@ -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 }) + } } } diff --git a/server-v2/app/Controllers/Http/MatchesController.ts b/server-v2/app/Controllers/Http/MatchesController.ts index 2cd563d..7319f41 100644 --- a/server-v2/app/Controllers/Http/MatchesController.ts +++ b/server-v2/app/Controllers/Http/MatchesController.ts @@ -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) + } } diff --git a/server-v2/app/Controllers/Http/SummonersController.ts b/server-v2/app/Controllers/Http/SummonersController.ts index d770ee7..ec58ec3 100644 --- a/server-v2/app/Controllers/Http/SummonersController.ts +++ b/server-v2/app/Controllers/Http/SummonersController.ts @@ -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') diff --git a/server-v2/app/Models/MatchPlayer.ts b/server-v2/app/Models/MatchPlayer.ts index b7006b7..e43bf6d 100644 --- a/server-v2/app/Models/MatchPlayer.ts +++ b/server-v2/app/Models/MatchPlayer.ts @@ -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 + @hasMany(() => MatchPlayerRank, { + localKey: 'id', + foreignKey: 'playerId', + }) + public ranks: HasMany + @column() public participantId: number diff --git a/server-v2/app/Models/MatchPlayerRank.ts b/server-v2/app/Models/MatchPlayerRank.ts new file mode 100644 index 0000000..0f5f366 --- /dev/null +++ b/server-v2/app/Models/MatchPlayerRank.ts @@ -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 + + @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 +} diff --git a/server-v2/app/Parsers/MatchPlayerRankParser.ts b/server-v2/app/Parsers/MatchPlayerRankParser.ts new file mode 100644 index 0000000..bf852e7 --- /dev/null +++ b/server-v2/app/Parsers/MatchPlayerRankParser.ts @@ -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 { + 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() diff --git a/server-v2/app/Parsers/ParsedType.ts b/server-v2/app/Parsers/ParsedType.ts index 2e2d0da..01ee5f5 100644 --- a/server-v2/app/Parsers/ParsedType.ts +++ b/server-v2/app/Parsers/ParsedType.ts @@ -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 +} diff --git a/server-v2/app/Serializers/DetailedMatchSerializer.ts b/server-v2/app/Serializers/DetailedMatchSerializer.ts index aa01c4f..2d429c9 100644 --- a/server-v2/app/Serializers/DetailedMatchSerializer.ts +++ b/server-v2/app/Serializers/DetailedMatchSerializer.ts @@ -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, + } } } diff --git a/server-v2/app/Serializers/MatchPlayerRankSerializer.ts b/server-v2/app/Serializers/MatchPlayerRankSerializer.ts new file mode 100644 index 0000000..4550afb --- /dev/null +++ b/server-v2/app/Serializers/MatchPlayerRankSerializer.ts @@ -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() diff --git a/server-v2/app/Serializers/MatchSerializer.ts b/server-v2/app/Serializers/MatchSerializer.ts index 4025102..e615e52 100644 --- a/server-v2/app/Serializers/MatchSerializer.ts +++ b/server-v2/app/Serializers/MatchSerializer.ts @@ -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), + } + } } diff --git a/server-v2/app/Serializers/SerializedTypes.ts b/server-v2/app/Serializers/SerializedTypes.ts index ec104c9..a8a852d 100644 --- a/server-v2/app/Serializers/SerializedTypes.ts +++ b/server-v2/app/Serializers/SerializedTypes.ts @@ -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 +} diff --git a/server-v2/app/Services/SummonerService.ts b/server-v2/app/Services/SummonerService.ts index f17f057..2026e59 100644 --- a/server-v2/app/Services/SummonerService.ts +++ b/server-v2/app/Services/SummonerService.ts @@ -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 { - const ranked = await Jax.League.summonerID(account.id, region) + public async getRanked(summonerId: string, region: string): Promise { + 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 } } diff --git a/server-v2/app/helpers.ts b/server-v2/app/helpers.ts index cf539f7..fc01650 100644 --- a/server-v2/app/helpers.ts +++ b/server-v2/app/helpers.ts @@ -110,3 +110,10 @@ export function sortTeamByRole(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(value: TValue | null | undefined): value is TValue { + if (value === null || value === undefined) return false + const testDummy: TValue = value + return true +} diff --git a/server-v2/database/migrations/1631807093726_match_player_ranks.ts b/server-v2/database/migrations/1631807093726_match_player_ranks.ts new file mode 100644 index 0000000..6cbf686 --- /dev/null +++ b/server-v2/database/migrations/1631807093726_match_player_ranks.ts @@ -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) + } +} diff --git a/server-v2/start/routes.ts b/server-v2/start/routes.ts index a20f3ad..9e0abe0 100644 --- a/server-v2/start/routes.ts +++ b/server-v2/start/routes.ts @@ -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')