Merge branch 'V2'

This commit is contained in:
Kalane 2021-09-20 19:58:35 +02:00
commit 11fb369051
114 changed files with 31057 additions and 9074 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
.vscode
.DS_STORE

1
LeagueStatsSQL.diagram Normal file

File diff suppressed because one or more lines are too long

View file

@ -3,7 +3,6 @@
[![Netlify Status](https://api.netlify.com/api/v1/badges/caa8be10-e095-4934-81ef-b662fb73483f/deploy-status)](https://app.netlify.com/sites/leaguestats-gg/deploys)
<a href="https://discord.gg/RjBzjfk"><img src="https://img.shields.io/badge/Discord-join%20chat-738bd7.svg" alt="LeagueStats.gg official Discord"></a>
The goal of [leaguestats.gg](https://leaguestats.gg) is to provide global complete data for all League of Legends summoners.
Here is an [example](https://leaguestats.gg/summoner/euw/SammyWinchester) of stats for some summoner.
@ -18,11 +17,15 @@ Here is an [example](https://leaguestats.gg/summoner/euw/SammyWinchester) of sta
## Installation
Development environment requirements :
- [Node.js](https://nodejs.org/en/download/) >= 12.0.0
- [MongoDB](https://www.mongodb.com/download-center/community) >= 4.4
- [PostgreSQL](https://www.postgresql.org/download/)
- [Redis](https://redis.io/download)
You can use the `docker-compose.yml` file to quickly setup Postgre and Redis in development.
Setting up your development environment on your local machine :
```bash
> git clone https://github.com/vkaelin/LeagueStats.git
> cd leaguestats/client
@ -33,11 +36,13 @@ Setting up your development environment on your local machine :
> cd leaguestats/server
> npm install
> cp .env.example .env # edit the values
> node ace mongodb:migration:run # your MongoDB installation needs to by a Replica Set and not a Standalone
> node ace migration:run
```
## Useful commands
Running the app :
```bash
> cd client
> npm run dev
@ -49,6 +54,7 @@ Running the app :
```
Deploying the app :
```bash
> cd client
> npm run build
@ -77,7 +83,6 @@ Adapt — remix, transform, and build upon the material
### Under the following terms:
NonCommercial — You may not use the material for commercial purposes.
ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.

21512
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -5,6 +5,7 @@
:data="allyTeam"
:all-players="[...allyTeam.players, ...enemyTeam.players]"
:ally-team="true"
:ranks-loaded="data.ranksLoaded"
/>
<div class="flex items-start justify-between px-3 py-2">
@ -23,6 +24,7 @@
:data="enemyTeam"
:all-players="[...allyTeam.players, ...enemyTeam.players]"
:ally-team="false"
:ranks-loaded="data.ranksLoaded"
/>
</div>
<div v-else-if="data.status === 'loading' && detailsOpen">

View file

@ -91,29 +91,29 @@
<div
:style="{
backgroundImage: `url(${
player.firstSum ? player.firstSum.icon : null
player.summonerSpell1 ? player.summonerSpell1.icon : null
})`,
}"
:class="{ 'cursor-pointer': player.firstSum }"
:class="{ 'cursor-pointer': player.summonerSpell1 }"
class="w-4 h-4 bg-center bg-cover rounded-md bg-blue-1000"
></div>
</template>
<template v-if="player.firstSum" #default>
<template v-if="player.summonerSpell1" #default>
<div
class="flex max-w-sm p-2 text-xs text-left text-white select-none"
>
<div
:style="{
backgroundImage: `url('${player.firstSum.icon}')`,
backgroundImage: `url('${player.summonerSpell1.icon}')`,
}"
class="flex-shrink-0 w-12 h-12 ml-1 bg-center bg-cover rounded-md bg-blue-1000"
></div>
<div class="ml-2 leading-tight">
<div class="text-base leading-none">
{{ player.firstSum.name }}
{{ player.summonerSpell1.name }}
</div>
<div class="mt-1 font-light text-blue-200">
{{ player.firstSum.description }}
{{ player.summonerSpell1.description }}
</div>
</div>
</div>
@ -124,29 +124,29 @@
<div
:style="{
backgroundImage: `url(${
player.secondSum ? player.secondSum.icon : null
player.summonerSpell2 ? player.summonerSpell2.icon : null
})`,
}"
:class="{ 'cursor-pointer': player.secondSum }"
:class="{ 'cursor-pointer': player.summonerSpell2 }"
class="w-4 h-4 bg-center bg-cover rounded-md bg-blue-1000"
></div>
</template>
<template v-if="player.secondSum" #default>
<template v-if="player.summonerSpell2" #default>
<div
class="flex max-w-sm p-2 text-xs text-left text-white select-none"
>
<div
:style="{
backgroundImage: `url('${player.secondSum.icon}')`,
backgroundImage: `url('${player.summonerSpell2.icon}')`,
}"
class="flex-shrink-0 w-12 h-12 ml-1 bg-center bg-cover rounded-md bg-blue-1000"
></div>
<div class="ml-2 leading-tight">
<div class="text-base leading-none">
{{ player.secondSum.name }}
{{ player.summonerSpell2.name }}
</div>
<div class="mt-1 font-light text-blue-200">
{{ player.secondSum.description }}
{{ player.summonerSpell2.description }}
</div>
</div>
</div>
@ -195,7 +195,7 @@
class="flex flex-col items-start justify-center ml-1 leading-none"
>
<router-link
v-if="player.firstSum"
v-if="player.summonerSpell1"
:to="{
name: 'summoner',
params: { region: $route.params.region, name: player.name },
@ -228,7 +228,7 @@
{{ player.rank.shortName }}
</div>
</div>
<div v-else-if="player.rank === undefined">
<div v-else-if="!ranksLoaded">
<DotsLoader width="30px" dot-width="10px" />
</div>
<div v-else class="w-5 h-5">
@ -350,6 +350,10 @@ export default {
type: Object,
required: true,
},
ranksLoaded: {
type: Boolean,
default: false
}
},
computed: {

View file

@ -35,13 +35,23 @@
></div>
<div class="flex flex-col justify-around ml-2">
<div
:style="{backgroundImage: `url(${data.firstSum})`}"
v-if="data.summonerSpell1"
:style="{backgroundImage: `url(${data.summonerSpell1.icon})`}"
class="w-6 h-6 bg-center bg-cover rounded-md bg-blue-1000"
></div>
<div
:style="{backgroundImage: `url(${data.secondSum})`}"
v-else
class="w-6 h-6 rounded-md bg-blue-1000"
></div>
<div
v-if="data.summonerSpell2"
:style="{backgroundImage: `url(${data.summonerSpell2.icon})`}"
class="w-6 h-6 bg-center bg-cover rounded-md bg-blue-1000"
></div>
<div
v-else
class="w-6 h-6 rounded-md bg-blue-1000"
></div>
</div>
<div class="flex flex-col justify-around ml-1">
<div
@ -156,7 +166,7 @@
<svg class="w-5 h-5 text-blue-200">
<use xlink:href="#stopwatch" />
</svg>
<div class="text-lg font-medium text-teal-400">{{ data.time|secToTime }}</div>
<div class="text-lg font-medium text-teal-400">{{ (data.time)|secToTime }}</div>
<Tooltip>
<template #trigger>
<div class="text-xs font-medium text-white">{{ data.date }}</div>
@ -175,7 +185,7 @@
</div>
</div>
</Ripple>
<DetailedMatch :data="getMatchDetails(data.gameId) || {}" :details-open="showDetails" />
<DetailedMatch :data="getMatchDetails(data.matchId) || {}" :details-open="showDetails" />
</li>
</template>
@ -223,8 +233,8 @@ export default {
displayDetails() {
this.showDetails = !this.showDetails
if (!this.getMatchDetails(this.data.gameId)) {
this.matchDetails(this.data.gameId)
if (!this.getMatchDetails(this.data.matchId)) {
this.matchDetails(this.data.matchId)
}
},
isSummonerProfile(account_id) {

View file

@ -30,11 +30,11 @@
class="flex flex-col items-center runes"
>
<div
:style="{backgroundImage: `url('${player.runes.primaryRune}')`}"
:style="{backgroundImage: `url('${getPrimarRune(player.perks)}')`}"
class="w-6 h-6 bg-center bg-cover"
></div>
<div
:style="{backgroundImage: `url('${player.runes.secondaryRune}')`}"
:style="{backgroundImage: `url('${getSecondaryRune(player.perks)}')`}"
class="w-3 h-3 mt-1 bg-center bg-cover"
></div>
</div>
@ -72,7 +72,11 @@
:class="[player.summonerId === account.id ? 'text-yellow-500' : 'hover:text-blue-200']"
class="font-semibold"
>{{ player.summonerName }}</router-link>
<div class="text-xs">Level {{ player.level }}</div>
<div
:class="[ally ? 'text-teal-300 ' : 'text-red-400 ']"
class="text-xs"
>{{ player.champion.name }}
</div>
</div>
</div>
</td>
@ -185,7 +189,7 @@
<script>
import { mapActions, mapState } from 'vuex'
import { getSummonerLink } from '@/helpers/summoner.js'
import { getSummonerLink, getPrimarRune, getSecondaryRune } from '@/helpers/summoner.js'
import { ContentLoader } from 'vue-content-loader'
export default {
@ -264,17 +268,13 @@ export default {
}
},
selectRunes(player) {
if(!player.perks) {
if(!player.perks)
return
}
this.displayOrHideRunes({
primaryStyle: player.perks.perkStyle,
secondaryStyle: player.perks.perkSubStyle,
selected: player.perks.perkIds
})
this.displayOrHideRunes(player.perks)
},
getSummonerLink,
getPrimarRune,
getSecondaryRune,
...mapActions('cdragon', ['displayOrHideRunes']),
}
}

View file

@ -19,7 +19,7 @@
<ul class="mt-1 text-gray-100">
<li
v-for="mate in mates.slice(0, maxMates)"
:key="mate._id"
:key="mate.name"
class="flex items-center justify-between"
>
<router-link

View file

@ -185,12 +185,12 @@
</div>
<ul class="mt-1 text-gray-100">
<li
v-for="(championClass, index) in championClasses"
v-for="(championClass, index) in stats.class"
:key="index"
:class="{'bg-blue-760': index % 2 !== 0}"
class="flex items-center justify-between px-4 py-1 leading-tight"
>
<div class="w-2/4 text-left capitalize">{{ championClass._id }}</div>
<div class="w-2/4 text-left capitalize">{{ championClass.id }}</div>
<div
:class="calculateWinrate(championClass.wins, championClass.count).color"
class="w-1/4"
@ -241,16 +241,12 @@ export default {
},
computed: {
championClasses() {
const classes = [...this.stats.class]
return classes.sort((a, b) => b.count - a.count)
},
mostPlayedRole() {
return Math.max(...this.stats.role.map(r => r.count), 0)
},
globalStatsKeys() {
// eslint-disable-next-line no-unused-vars
const { _id, wins, losses, count, time, kp, ...rest } = this.stats.global
const { id, wins, losses, count, time, kp, ...rest } = this.stats.global
return rest
},
...mapState({
@ -270,10 +266,9 @@ export default {
leagueStatsByType(typeName) {
return this.stats.league
.map(l => {
return { ...l, ...gameModes[l._id] }
return { ...l, ...gameModes[l.id] }
})
.filter(l => l.type === typeName)
.sort((a, b) => b.count - a.count)
},
roundedRoleLosses(win, count) {
return win === count ? 'rounded-full' : 'rounded-b-full'

View file

@ -57,6 +57,7 @@
</template>
<script>
import { mapState } from 'vuex'
import Tooltip from '@/components/Common/Tooltip.vue'
export default {
@ -64,15 +65,6 @@ export default {
Tooltip,
},
props: {
matches: {
type: Array,
default() {
return []
}
}
},
data() {
return {
gridDays: [],
@ -86,8 +78,14 @@ export default {
}
},
computed: {
...mapState({
recentActivity: state => state.summoner.basic.recentActivity
}),
},
watch: {
matches() {
recentActivity() {
this.fillGrid()
}
},
@ -118,16 +116,15 @@ export default {
},
fillGrid() {
// Add all the matches made by the summoner
for (const key in this.matches) {
const match = this.matches[key]
const matchTime = new Date(match.timestamp)
for (const match of this.recentActivity) {
const matchTime = new Date(match.day)
const formattedTime = matchTime.toLocaleString(undefined, this.options)
const dayOfTheMatch = this.gridDays.filter(
e => e.date === formattedTime
)
if (dayOfTheMatch.length > 0) {
dayOfTheMatch[0].matches++
dayOfTheMatch[0].matches = match.count
}
}

View file

@ -5,7 +5,7 @@
:style="{
backgroundImage:
`${hover ? gradientHover : gradient},
url('https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-splashes/${record.champion.id}/${record.champion.id}000.jpg')`
url('https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-splashes/${record.champion_id}/${record.champion_id}000.jpg')`
}"
:class="borderColor"
class="relative w-full p-4 mx-2 mt-6 leading-none bg-center bg-cover border rounded-lg record-card"
@ -21,18 +21,18 @@
<span :class="textColor" class="ml-0">{{ title }}</span>
</div>
<img
:src="`https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/${record.champion.id}.png`"
:src="`https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/${record.champion_id}.png`"
:class="[{'opacity-0 scale-125': hover}, borderColor]"
class="block w-16 h-16 mx-auto mt-10 transition duration-500 ease-in transform border-2 rounded-full"
alt="Champion Played"
/>
<div :style="{textShadow: `-2px 1px 6px ${color}`}" class="mt-6 text-4xl">{{ record[property] }}</div>
<div :style="{textShadow: `-2px 1px 6px ${color}`}" class="mt-6 text-4xl">{{ record.amount }}</div>
<div class="text-sm">
<div class="mt-6">
<span
:class="record.result === 'Win' ? 'text-green-400' : 'text-red-400'"
>{{ record.result === 'Win' ? 'Won' : 'Lost' }}</span>
:class="record.result ? 'text-green-400' : 'text-red-400'"
>{{ record.result ? 'Won' : 'Lost' }}</span>
<span class="ml-1 font-semibold">{{ timeDifference(record.date) }}</span>
</div>
<div class="mt-2 text-gray-500">
@ -41,7 +41,7 @@
</div>
</div>
<div class="mt-6 text-xs font-light text-right text-gray-200 opacity-25">
<span v-if="hover">match {{ record.gameId }}</span>
<span v-if="hover">{{ record.id }}</span>
<span v-else>{{ gameModes[record.gamemode].name }}</span>
</div>
</div>
@ -65,10 +65,6 @@ export default {
type: String,
required: true
},
property: {
type: String,
required: true
},
record: {
type: Object,
required: true

View file

@ -40,7 +40,7 @@ export function secToTime(seconds) {
* Sort an array of players by role
*/
export function sortTeamByRole(a, b) {
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
return sortingArr.indexOf(a.role) - sortingArr.indexOf(b.role)
}

View file

@ -1,17 +1,37 @@
import { secToTime, timeDifference } from '@/helpers/functions.js'
import { createCDragonAssetUrl, secToTime, timeDifference } from '@/helpers/functions.js'
import { maps, gameModes } from '@/data/data.js'
import summonerSpells from '@/data/summonerSpells.json'
import store from '@/store'
const leaguesNumbers = { 'I': 1, 'II': 2, 'III': 3, 'IV': 4 }
/**
* Get the url of the of the player primary rune
* @param {Object} perks : from the API
*/
export function getPrimarRune(perks) {
const primaryRune = perks.selected.length ? store.state.cdragon.runes.perks[perks.selected[0]] : null
return primaryRune ? createCDragonAssetUrl(primaryRune.icon) : null
}
/**
* Get the url of the of the player secondary rune
* @param {Object} perks : from the API
*/
export function getSecondaryRune(perks) {
const secondaryRune = store.state.cdragon.runes.perkstyles[perks.secondaryStyle]
return secondaryRune ? createCDragonAssetUrl(secondaryRune.icon) : null
}
/**
* Return all the infos about a list of matches built with the Riot API data
* @param {Object} RiotData : all data from the Riot API
*/
export function createMatchData(matches) {
for (const match of matches) {
match.firstSum = getSummonerLink(match.firstSum)
match.secondSum = getSummonerLink(match.secondSum)
// Runes
match.primaryRune = getPrimarRune(match.perks)
match.secondaryRune = getSecondaryRune(match.perks)
const date = new Date(match.date)
const dateOptions = { day: '2-digit', month: '2-digit', year: 'numeric' }
@ -62,21 +82,22 @@ export function createBasicSummonerData(RiotData) {
/**
* Return the formatted records of a summoner
* @param {Object} records : raw records from the database stats
* @param {Object} recordsDto : raw records from the database stats
*/
export function createRecordsData(records) {
records.maxTime.time = secToTime(records.maxTime.time)
records.maxGold.gold = records.maxGold.gold.toLocaleString()
records.maxDmgTaken.dmgTaken = records.maxDmgTaken.dmgTaken.toLocaleString()
records.maxDmgChamp.dmgChamp = records.maxDmgChamp.dmgChamp.toLocaleString()
records.maxDmgObj.dmgObj = records.maxDmgObj.dmgObj.toLocaleString()
records.maxKp.kp = `${records.maxKp.kp}%`
export function createRecordsData(recordsDto) {
const records = recordsDto.reduce((acc, record) => {
acc[record.what] = record
return acc
}, {})
// New record fields
if (records.maxLiving) {
records.maxLiving.longestLiving = secToTime(records.maxLiving.longestLiving)
records.maxHeal.heal = records.maxHeal.heal.toLocaleString()
}
records.game_duration.amount = secToTime(records.game_duration.amount)
records.gold.amount = records.gold.amount.toLocaleString()
records.damage_taken.amount = records.damage_taken.amount.toLocaleString()
records.damage_dealt_champions.amount = records.damage_dealt_champions.amount.toLocaleString()
records.damage_dealt_objectives.amount = records.damage_dealt_objectives.amount.toLocaleString()
records.kp.amount = `${records.kp.amount}%`
records.time_spent_living.amount = secToTime(records.time_spent_living.amount)
records.heal.amount = records.heal.amount.toLocaleString()
return records
}

View file

@ -114,7 +114,7 @@
</div>
<div>
<RecentActivity :matches="basic.matchList" />
<RecentActivity />
</div>
</div>
<div class="flex items-center justify-between">

View file

@ -32,7 +32,7 @@ export default new Vuex.Store({
'tr': 'tr1',
'ru': 'ru'
},
roles: ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
roles: ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
},
strict: debug
})

View file

@ -8,47 +8,70 @@ export const state = {
}
export const mutations = {
MATCH_LOADING(state, gameId) {
const alreadyIn = state.matches.find(m => m.gameId === gameId)
MATCH_LOADING(state, matchId) {
const alreadyIn = state.matches.find(m => m.matchId === matchId)
if (!alreadyIn) {
state.matches.push({ gameId: gameId, status: 'loading' })
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
},
}
export const actions = {
async matchDetails({ commit, rootState }, gameId) {
commit('MATCH_LOADING', gameId)
const region = rootState.regionsList[rootState.settings.region]
console.log('MATCH DETAILS STORE', gameId, region)
async matchDetails({ commit }, matchId) {
commit('MATCH_LOADING', matchId)
console.log('MATCH DETAILS STORE', matchId)
const resp = await axios(({ url: 'match/details', data: { gameId, region }, method: 'POST' })).catch(() => { })
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 })
// 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 (!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', { gameId, ...ranks.data })
commit('MATCH_RANKS_FOUND', { matchId, ranksByPlayer: ranks.data })
}
}
}
export const getters = {
getMatchDetails: state => gameId => state.matches.find(m => m.gameId === gameId),
getMatchDetails: state => matchId => state.matches.find(m => m.matchId === matchId),
}

View file

@ -9,6 +9,7 @@ export const state = {
currentSeason: null,
matchList: [],
ranked: {},
recentActivity: [],
seasons: [],
status: '',
},
@ -24,7 +25,7 @@ export const state = {
championsLoaded: false
},
records: {
list: [],
list: {},
recordsLoaded: false
},
live: {
@ -66,18 +67,21 @@ export const mutations = {
state.overview.matchesLoading = true
},
MATCHES_FOUND(state, { newMatches, stats }) {
state.basic.recentActivity = stats.recentActivity
state.overview.matchesLoading = false
state.overview.matches = [...state.overview.matches, ...newMatches]
state.overview.matchIndex += newMatches.length
state.overview.matchIndex += 10
state.overview.stats = stats
state.champions.championsLoaded = false
state.records.recordsLoaded = false
},
OVERVIEW_FOUND(state, infos) {
state.basic.recentActivity = infos.stats.recentActivity
state.overview.matches = infos.matches
state.overview.matchIndex = infos.matches.length
state.overview.stats = infos.stats
state.overview.loaded = true
state.records.recordsLoaded = false
},
RECORDS_FOUND(state, { records }) {
state.records.list = records
@ -87,6 +91,7 @@ export const mutations = {
state.basic.account = infos.account
state.basic.matchList = infos.matchList
state.basic.ranked = infos.ranked
state.basic.recentActivity = infos.recentActivity
state.basic.seasons = infos.seasons.sort((a, b) => b - a)
state.basic.status = 'found'
state.live.match = infos.current
@ -178,17 +183,15 @@ export const actions = {
async moreMatches({ commit, getters, rootState }) {
commit('MATCHES_LOADING')
const gameIds = getters.filteredMatchList
const matchIds = getters.filteredMatchList
.slice(state.overview.matchIndex, state.overview.matchIndex + 10)
.map(({ gameId }) => gameId)
const resp = await axios(({
url: 'match',
data: {
puuid: state.basic.account.puuid,
accountId: state.basic.account.accountId,
region: rootState.regionsList[rootState.settings.region],
gameIds
matchIds
},
method: 'POST'
})).catch(() => { })
@ -216,7 +219,7 @@ export const actions = {
const resp = await axios(({ url: 'summoner/records', data: { puuid: state.basic.account.puuid }, method: 'POST' })).catch(() => { })
console.log('---RECORDS---')
console.log(resp.data)
const records = resp.data ? createRecordsData(resp.data) : {}
const records = resp.data.length ? createRecordsData(resp.data) : {}
commit('RECORDS_FOUND', { records })
},

View file

@ -59,6 +59,8 @@ export default {
created() {
this.fetchData()
this.getRunes()
},
methods: {
@ -67,6 +69,7 @@ export default {
this.liveMatchRequest()
}
},
...mapActions('cdragon', ['getRunes']),
...mapActions('summoner', ['liveMatchRequest']),
},

View file

@ -1,6 +1,6 @@
<template>
<div key="records">
<template v-if="!recordsLoaded || (recordsLoaded && records.maxKda)">
<template v-if="!recordsLoaded || (recordsLoaded && records.assists)">
<div
class="relative pl-6 text-2xl text-blue-200 border-b-2 border-blue-800 category blue-900"
>Basics</div>
@ -10,48 +10,42 @@
color="#63b3ed"
text-color="text-blue-400"
border-color="border-blue-400"
property="kda"
:record="records.maxKda"
:record="records.kda"
title="KDA"
/>
<RecordCard
color="#68D391"
text-color="text-green-400"
border-color="border-green-400"
property="kills"
:record="records.maxKills"
:record="records.kills"
title="Kills"
/>
<RecordCard
color="#9F7AEA"
text-color="text-purple-500"
border-color="border-purple-500"
property="assists"
:record="records.maxAssists"
:record="records.assists"
title="Assists"
/>
<RecordCard
color="#F56565"
text-color="text-red-500"
border-color="border-red-500"
property="deaths"
:record="records.maxDeaths"
:record="records.deaths"
title="Deaths"
/>
<RecordCard
color="#D69E2E"
text-color="text-yellow-600"
border-color="border-yellow-600"
property="gold"
:record="records.maxGold"
:record="records.gold"
title="Gold earned"
/>
<RecordCard
color="#81E6D9"
text-color="text-teal-300"
border-color="border-teal-300"
property="minions"
:record="records.maxMinions"
:record="records.minions"
title="Minions killed"
/>
</template>
@ -81,24 +75,21 @@
color="#FC8181"
text-color="text-red-400"
border-color="border-red-400"
property="dmgChamp"
:record="records.maxDmgChamp"
:record="records.damage_dealt_champions"
title="Damage champions"
/>
<RecordCard
color="#D69E2E"
text-color="text-yellow-400"
border-color="border-yellow-400"
property="dmgObj"
:record="records.maxDmgObj"
:record="records.damage_dealt_objectives"
title="Damage objectives"
/>
<RecordCard
color="#FC8181"
text-color="text-red-400"
border-color="border-red-400"
property="dmgTaken"
:record="records.maxDmgTaken"
:record="records.damage_taken"
title="Damage taken"
/>
<RecordCard
@ -106,24 +97,21 @@
color="#D69E2E"
text-color="text-yellow-400"
border-color="border-yellow-400"
property="towers"
:record="records.maxTowers"
:record="records.turret_kills"
title="Towers"
/>
<RecordCard
color="#68D391"
text-color="text-green-400"
border-color="border-green-400"
property="kp"
:record="records.maxKp"
:record="records.kp"
title="Kill participation"
/>
<RecordCard
color="#D69E2E"
text-color="text-yellow-400"
border-color="border-yellow-400"
property="vision"
:record="records.maxVision"
:record="records.vision_score"
title="Vision score"
/>
</template>
@ -153,35 +141,28 @@
color="#4299E1"
text-color="text-blue-500"
border-color="border-blue-500"
property="time"
:record="records.maxTime"
:record="records.game_duration"
title="Longest game"
/>
<RecordCard
v-if="records.maxLiving"
color="#4299E1"
text-color="text-blue-500"
border-color="border-blue-500"
property="longestLiving"
:record="records.maxLiving"
:record="records.time_spent_living"
title="Longest living"
/>
<RecordCard
v-if="records.maxCriticalStrike"
color="#D69E2E"
text-color="text-yellow-400"
border-color="border-yellow-400"
property="criticalStrike"
:record="records.maxCriticalStrike"
:record="records.critical_strike"
title="Critical Strike"
/>
<RecordCard
v-if="records.maxHeal"
color="#68D391"
text-color="text-green-400"
border-color="border-green-400"
property="heal"
:record="records.maxHeal"
:record="records.heal"
title="Heal"
/>
</template>
@ -204,47 +185,42 @@
</div>
</template>
</div>
<div v-if="records.maxDouble" class="relative pl-6 mt-3 text-2xl text-blue-200 border-b-2 border-blue-800 category">Multi kills</div>
<div v-if="records.maxDouble" class="flex flex-wrap -mx-2">
<div class="relative pl-6 mt-3 text-2xl text-blue-200 border-b-2 border-blue-800 category">Multi kills</div>
<div class="flex flex-wrap -mx-2">
<template v-if="recordsLoaded">
<RecordCard
color="#FEFCBF"
text-color="text-yellow-200"
border-color="border-yellow-200"
property="doubleKills"
:record="records.maxDouble"
:record="records.double_kills"
title="Double kills"
/>
<RecordCard
color="#F6E05E"
text-color="text-yellow-400"
border-color="border-yellow-400"
property="tripleKills"
:record="records.maxTriple"
:record="records.triple_kills"
title="Triple kills"
/>
<RecordCard
color="#D69E2E"
text-color="text-yellow-600"
border-color="border-yellow-600"
property="quadraKills"
:record="records.maxQuadra"
:record="records.quadra_kills"
title="Quadra kills"
/>
<RecordCard
color="#F56565"
text-color="text-red-500"
border-color="border-red-500"
property="pentaKills"
:record="records.maxPenta"
:record="records.penta_kills"
title="Penta kills"
/>
<RecordCard
color="#63b3ed"
text-color="text-blue-400"
border-color="border-blue-400"
property="killingSpree"
:record="records.maxKillingSpree"
:record="records.killing_spree"
title="Killing Spree"
/>
</template>
@ -268,7 +244,7 @@
</template>
</div>
</template>
<template v-if="recordsLoaded && !records.maxKda">
<template v-if="recordsLoaded && !records.assists">
<div class="flex flex-col items-center mt-4">
<div>No records have been found.</div>
<div>😕</div>

26
docker-compose.yml Normal file
View file

@ -0,0 +1,26 @@
version: "3"
services:
leaguestats-redis:
container_name: leaguestats-redis
image: redis:6-alpine
ports:
- '127.0.0.1:6379:6379'
volumes:
- leaguestats-redisData:/data
restart: always
leaguestats-postgres:
container_name: leaguestats-postgres
image: postgres:12-alpine
ports:
- '127.0.0.1:5432:5432'
environment:
- POSTGRES_DB=leaguestats
- POSTGRES_USER=root
- POSTGRES_PASSWORD=root
- POSTGRES_HOST_AUTH_METHOD=trust
volumes:
- leaguestats-postgresData:/var/lib/postgresql/data
restart: always
volumes:
leaguestats-redisData:
leaguestats-postgresData:

View file

@ -2,28 +2,36 @@
"typescript": true,
"commands": [
"./commands",
"@adonisjs/core/build/commands",
"@zakodium/adonis-mongodb/lib/commands"
"@adonisjs/core/build/commands/index.js",
"@adonisjs/repl/build/commands",
"@adonisjs/lucid/build/commands"
],
"exceptionHandlerNamespace": "App/Exceptions/Handler",
"aliases": {
"App": "app",
"Contracts": "contracts",
"Config": "config",
"Database": "database"
"Database": "database",
"Contracts": "contracts"
},
"preloads": [
"./start/routes",
"./start/kernel"
"./start/kernel",
{
"file": "./start/events",
"environment": [
"console",
"repl",
"web"
]
}
],
"providers": [
"./providers/AppProvider",
"@adonisjs/core",
"@zakodium/adonis-mongodb",
"@adonisjs/lucid",
"@adonisjs/redis"
],
"metaFiles": [
".env",
".adonisrc.json"
"aceProviders": [
"@adonisjs/repl"
]
}

View file

@ -2,13 +2,18 @@ PORT=3333
HOST=0.0.0.0
NODE_ENV=development
APP_KEY=
DRIVE_DISK=local
MONGODB_URL=mongodb://localhost:27017
MONGODB_DATABASE=leaguestats
DB_CONNECTION=pg
PG_HOST=localhost
PG_PORT=5432
PG_USER=
PG_PASSWORD=
PG_DB_NAME=
REDIS_CONNECTION=local
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
API_KEY=RGAPI-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
RIOT_API_KEY=

View file

@ -1,13 +1,7 @@
{
"extends": [
"plugin:adonis/typescriptApp"
],
"extends": ["plugin:adonis/typescriptApp", "prettier"],
"plugins": ["prettier"],
"rules": {
"max-len": [
"error",
{
"code": 130
}
]
"prettier/prettier": ["error"]
}
}

1
server/.prettierignore Normal file
View file

@ -0,0 +1 @@
build

11
server/.prettierrc Normal file
View file

@ -0,0 +1,11 @@
{
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100,
"endOfLine": "auto"
}

View file

@ -3,17 +3,14 @@
| Ace Commands
|--------------------------------------------------------------------------
|
| This file is the entry point for running ace commands. For typescript
| projects, the ace commands will fallback to the compiled code and
| hence this file has to be executable by node directly.
| This file is the entry point for running ace commands.
|
*/
require('reflect-metadata')
require('source-map-support').install({ handleUncaughtExceptions: false })
const { Ignitor } = require('@adonisjs/core/build/src/Ignitor')
const { Ignitor } = require('@adonisjs/core/build/standalone')
new Ignitor(__dirname)
.ace()
.handle(process.argv.slice(2))
.catch(console.error)

View file

@ -1 +1,294 @@
{"dump:rcfile":{"settings":{},"commandPath":"@adonisjs/core/build/commands/DumpRc","commandName":"dump:rcfile","description":"Dump contents of .adonisrc.json file along with defaults","args":[],"flags":[]},"list:routes":{"settings":{"loadApp":true},"commandPath":"@adonisjs/core/build/commands/ListRoutes","commandName":"list:routes","description":"List application routes","args":[],"flags":[{"name":"json","propertyName":"json","type":"boolean","description":"Output as JSON"}]},"generate:key":{"settings":{},"commandPath":"@adonisjs/core/build/commands/GenerateKey","commandName":"generate:key","description":"Generate a new APP_KEY secret","args":[],"flags":[]},"mongodb:make:migration":{"settings":{"loadApp":true},"commandPath":"@zakodium/adonis-mongodb/lib/commands/MongodbMakeMigration","commandName":"mongodb:make:migration","description":"Make a new migration file","args":[{"type":"string","propertyName":"name","name":"name","required":true,"description":"Name of the migration file"}],"flags":[{"name":"connection","propertyName":"connection","type":"string","description":"Database connection to use for the migration"}]},"mongodb:migration:run":{"settings":{"loadApp":true},"commandPath":"@zakodium/adonis-mongodb/lib/commands/MongodbMigrate","commandName":"mongodb:migration:run","description":"Execute pending migrations","args":[],"flags":[{"name":"connection","propertyName":"connection","type":"string","description":"Database connection to use for the migration"}]},"mongodb:migration:status":{"settings":{"loadApp":true},"commandPath":"@zakodium/adonis-mongodb/lib/commands/MongodbListMigrations","commandName":"mongodb:migration:status","description":"Show pending migrations","args":[],"flags":[{"name":"connection","propertyName":"connection","type":"string","description":"Database connection to use for the migration"}]}}
{
"commands": {
"load:v4": {
"settings": {
"loadApp": true,
"stayAlive": false
},
"commandPath": "./commands/LoadV4Matches",
"commandName": "load:v4",
"description": "Load matches for a given Summoner from the old Match-V4 endpoint",
"args": [
{
"type": "string",
"propertyName": "summoner",
"name": "summoner",
"required": true,
"description": "Summoner name to seach"
},
{
"type": "string",
"propertyName": "region",
"name": "region",
"required": true,
"description": "League region of the summoner"
}
],
"aliases": [],
"flags": []
},
"dump:rcfile": {
"settings": {},
"commandPath": "@adonisjs/core/build/commands/DumpRc",
"commandName": "dump:rcfile",
"description": "Dump contents of .adonisrc.json file along with defaults",
"args": [],
"aliases": [],
"flags": []
},
"list:routes": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/core/build/commands/ListRoutes",
"commandName": "list:routes",
"description": "List application routes",
"args": [],
"aliases": [],
"flags": [
{
"name": "json",
"propertyName": "json",
"type": "boolean",
"description": "Output as JSON"
}
]
},
"generate:key": {
"settings": {},
"commandPath": "@adonisjs/core/build/commands/GenerateKey",
"commandName": "generate:key",
"description": "Generate a new APP_KEY secret",
"args": [],
"aliases": [],
"flags": []
},
"repl": {
"settings": {
"loadApp": true,
"environment": "repl",
"stayAlive": true
},
"commandPath": "@adonisjs/repl/build/commands/AdonisRepl",
"commandName": "repl",
"description": "Start a new REPL session",
"args": [],
"aliases": [],
"flags": []
},
"db:seed": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/DbSeed",
"commandName": "db:seed",
"description": "Execute database seeder files",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection for the seeders",
"alias": "c"
},
{
"name": "interactive",
"propertyName": "interactive",
"type": "boolean",
"description": "Run seeders in interactive mode",
"alias": "i"
},
{
"name": "files",
"propertyName": "files",
"type": "array",
"description": "Define a custom set of seeders files names to run",
"alias": "f"
}
]
},
"make:model": {
"settings": {},
"commandPath": "@adonisjs/lucid/build/commands/MakeModel",
"commandName": "make:model",
"description": "Make a new Lucid model",
"args": [
{
"type": "string",
"propertyName": "name",
"name": "name",
"required": true,
"description": "Name of the model class"
}
],
"aliases": [],
"flags": [
{
"name": "migration",
"propertyName": "migration",
"type": "boolean",
"alias": "m",
"description": "Generate the migration for the model"
},
{
"name": "controller",
"propertyName": "controller",
"type": "boolean",
"alias": "c",
"description": "Generate the controller for the model"
}
]
},
"make:migration": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/MakeMigration",
"commandName": "make:migration",
"description": "Make a new migration file",
"args": [
{
"type": "string",
"propertyName": "name",
"name": "name",
"required": true,
"description": "Name of the migration file"
}
],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "The connection flag is used to lookup the directory for the migration file"
},
{
"name": "folder",
"propertyName": "folder",
"type": "string",
"description": "Pre-select a migration directory"
},
{
"name": "create",
"propertyName": "create",
"type": "string",
"description": "Define the table name for creating a new table"
},
{
"name": "table",
"propertyName": "table",
"type": "string",
"description": "Define the table name for altering an existing table"
}
]
},
"make:seeder": {
"settings": {},
"commandPath": "@adonisjs/lucid/build/commands/MakeSeeder",
"commandName": "make:seeder",
"description": "Make a new Seeder file",
"args": [
{
"type": "string",
"propertyName": "name",
"name": "name",
"required": true,
"description": "Name of the seeder class"
}
],
"aliases": [],
"flags": []
},
"migration:run": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/Migration/Run",
"commandName": "migration:run",
"description": "Run pending migrations",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
},
{
"name": "force",
"propertyName": "force",
"type": "boolean",
"description": "Explicitly force to run migrations in production"
},
{
"name": "dry-run",
"propertyName": "dryRun",
"type": "boolean",
"description": "Print SQL queries, instead of running the migrations"
}
]
},
"migration:rollback": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/Migration/Rollback",
"commandName": "migration:rollback",
"description": "Rollback migrations to a given batch number",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
},
{
"name": "force",
"propertyName": "force",
"type": "boolean",
"description": "Explictly force to run migrations in production"
},
{
"name": "dry-run",
"propertyName": "dryRun",
"type": "boolean",
"description": "Print SQL queries, instead of running the migrations"
},
{
"name": "batch",
"propertyName": "batch",
"type": "number",
"description": "Define custom batch number for rollback. Use 0 to rollback to initial state"
}
]
},
"migration:status": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/Migration/Status",
"commandName": "migration:status",
"description": "Check migrations current status.",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
}
]
}
},
"aliases": {}
}

View file

@ -1,10 +1,10 @@
import Redis from '@ioc:Adonis/Addons/Redis'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import RuneSerializer from 'App/Serializers/RuneSerializer'
import Jax from 'App/Services/Jax'
import RuneTransformer from 'App/Transformers/RuneTransformer'
export default class CDragonController {
public async runes ({ response }: HttpContextContract) {
public async runes({ response }: HttpContextContract) {
const cacheUrl = 'cdragon-runes'
const requestCached = await Redis.get(cacheUrl)
@ -16,8 +16,8 @@ export default class CDragonController {
const perkstyles = await Jax.CDragon.perkstyles()
const runesData = {
perks: RuneTransformer.transformPerks(perks),
perkstyles: RuneTransformer.transformStyles(perkstyles.styles),
perks: RuneSerializer.serializePerks(perks),
perkstyles: RuneSerializer.serializeStyles(perkstyles.styles),
}
await Redis.set(cacheUrl, JSON.stringify(runesData), 'EX', 36000)

View file

@ -1,51 +1,23 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
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 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 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 Jax.Summoner.summonerId(summoner.summonerId, 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 matchIds
* @param ctx
*/
public async index ({ request, response }: HttpContextContract) {
console.log('More Matches Request')
const { puuid, accountId, region, gameIds, season } = await request.validate(MatchesIndexValidator)
const summonerDB = await Summoner.findOne({ puuid })
if (!summonerDB) {
return response.json(null)
}
const matches = await MatchService.getMatches(puuid, accountId, region, gameIds, summonerDB)
await summonerDB.save()
public async index({ request, response }: HttpContextContract) {
const { puuid, region, matchIds, season } = await request.validate(MatchesIndexValidator)
const matches = await MatchService.getMatches(region, matchIds, puuid)
const stats = await StatsService.getSummonerStats(puuid, season)
return response.json({
matches,
stats,
@ -56,26 +28,25 @@ export default class MatchesController {
* POST - Return details data for one specific match
* @param ctx
*/
public async show ({ request, response }: HttpContextContract) {
public async show({ request, response }: HttpContextContract) {
console.time('MatchDetails')
const { gameId, region } = await request.validate(DetailedMatchValidator)
const { matchId } = await request.validate(DetailedMatchValidator)
let matchDetails: DetailedMatchModel
const alreadySaved = await DetailedMatch.findOne({ gameId, region })
const match = await Match.query()
.where('id', matchId)
.preload('teams')
.preload('players', (playersQuery) => {
playersQuery.preload('ranks')
})
.firstOrFail()
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)
}
const { match: matchDetails, ranksLoaded } = DetailedMatchSerializer.serializeOneMatch(match)
console.timeEnd('MatchDetails')
return response.json({
matchDetails,
ranksLoaded,
})
}
@ -83,28 +54,14 @@ export default class MatchesController {
* POST - Return ranks of players for a specific game
* @param ctx
*/
public async showRanks ({ request, response }: HttpContextContract) {
public async showRanks({ request, response }: HttpContextContract) {
console.time('Ranks')
const { gameId, region } = await request.validate(DetailedMatchValidator)
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)
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,
})
return response.json(serializedRanks)
}
}

View file

@ -1,11 +1,13 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { getCurrentSeason } from 'App/helpers'
import Summoner from 'App/Models/Summoner'
import MatchRepository from 'App/Repositories/MatchRepository'
import BasicMatchSerializer from 'App/Serializers/BasicMatchSerializer'
import LiveMatchSerializer from 'App/Serializers/LiveMatchSerializer'
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 LiveMatchTransformer from 'App/Transformers/LiveMatchTransformer'
import SummonerBasicValidator from 'App/Validators/SummonerBasicValidator'
import SummonerChampionValidator from 'App/Validators/SummonerChampionValidator'
import SummonerLiveValidator from 'App/Validators/SummonerLiveValidator'
@ -13,21 +15,8 @@ import SummonerOverviewValidator from 'App/Validators/SummonerOverviewValidator'
import SummonerRecordValidator from 'App/Validators/SummonerRecordValidator'
export default class SummonersController {
/**
* Get all played seasons for a summoner
* @param puuid of the summoner
*/
private async getSeasons (puuid: string): Promise<number[]> {
const seasons = await MatchRepository.seasons(puuid)
return seasons.length ? seasons.map(s => s._id) : [10]
}
/**
* POST: get basic summoner data
* @param ctx
*/
public async basic ({ request, response }: HttpContextContract) {
console.time('all')
public async basic({ request, response }: HttpContextContract) {
console.time('BASIC_REQUEST')
const { summoner, region } = await request.validate(SummonerBasicValidator)
const finalJSON: any = {}
@ -37,24 +26,20 @@ export default class SummonersController {
if (!account) {
return response.json(null)
}
account.region = region
finalJSON.account = account
// Summoner in DB
let summonerDB = await Summoner.findOne({ puuid: account.puuid })
if (!summonerDB) {
summonerDB = await Summoner.create({ puuid: account.puuid })
}
const summonerDB = await Summoner.firstOrCreate({ puuid: account.puuid })
// Summoner names
finalJSON.account.names = SummonerService.getAllSummonerNames(account, summonerDB)
finalJSON.account.names = await SummonerService.getAllSummonerNames(account, summonerDB)
// MATCH LIST
await MatchService.updateMatchList(account, summonerDB)
finalJSON.matchList = summonerDB.matchList
finalJSON.matchList = await MatchService.updateMatchList(account, region, summonerDB)
// All seasons the summoner has played
finalJSON.seasons = await this.getSeasons(account.puuid)
// TODO: check if there is a way to do that with V5...
finalJSON.seasons = [getCurrentSeason()]
// CURRENT GAME
const currentGame = await Jax.Spectator.summonerID(account.id, region)
@ -62,53 +47,44 @@ export default class SummonersController {
finalJSON.current = currentGame
// RANKED STATS
finalJSON.ranked = await SummonerService.getRanked(account, region)
finalJSON.ranked = await SummonerService.getRanked(account.id, region)
// SAVE IN DB
await summonerDB.save()
} catch (error) {
console.log('username not found')
console.log(error)
// RECENT ACTIVITY
finalJSON.recentActivity = await StatsService.getRecentActivity(account.puuid)
} catch (e) {
console.log(e)
console.timeEnd('BASIC_REQUEST')
return response.json(null)
}
console.timeEnd('all')
console.timeEnd('BASIC_REQUEST')
return response.json(finalJSON)
}
/**
* POST: get overview view summoner data
* @param ctx
*/
public async overview ({ request, response }: HttpContextContract) {
console.time('overview')
const { puuid, accountId, region, season } = await request.validate(SummonerOverviewValidator)
public async overview({ request, response }: HttpContextContract) {
console.time('OVERVIEW_REQUEST')
const { puuid, region, season } = await request.validate(SummonerOverviewValidator)
const finalJSON: any = {}
// Summoner in DB
let summonerDB = await Summoner.findOne({ puuid: puuid })
if (!summonerDB) {
summonerDB = await Summoner.create({ puuid: puuid })
}
const summonerDB = await Summoner.firstOrCreate({ puuid: puuid })
// MATCHES BASIC
const gameIds = summonerDB.matchList!.slice(0)
.filter(m => {
return season ? m.seasonMatch === season : true
})
.slice(0, 10)
.map(({ gameId }) => gameId)
finalJSON.matchesDetails = await MatchService.getMatches(puuid, accountId, region, gameIds, summonerDB)
const matchlist = await summonerDB
.related('matchList')
.query()
.select('matchId')
.orderBy('matchId', 'desc')
.limit(10)
const matchIds = matchlist.map((m) => m.matchId)
finalJSON.matchesDetails = await MatchService.getMatches(region, matchIds, puuid)
// STATS
console.time('STATS')
finalJSON.stats = await StatsService.getSummonerStats(puuid, season)
console.timeEnd('STATS')
// SAVE IN DB
await summonerDB.save()
console.timeEnd('overview')
console.timeEnd('OVERVIEW_REQUEST')
return response.json(finalJSON)
}
@ -116,44 +92,57 @@ export default class SummonersController {
* POST: get champions view summoner data
* @param ctx
*/
public async champions ({ request, response }: HttpContextContract) {
public async champions({ request, response }: HttpContextContract) {
console.time('championsRequest')
const { puuid, queue, season } = await request.validate(SummonerChampionValidator)
const championStats = await MatchRepository.championCompleteStats(puuid, queue, season)
const championStatsSerialized = championStats.map((champion) => {
return {
...champion,
champion: BasicMatchSerializer.getChampion(champion.id),
}
})
console.timeEnd('championsRequest')
return response.json(championStats)
return response.json(championStatsSerialized)
}
/**
* POST: get records view summoner data
* @param ctx
*/
public async records ({ request, response }: HttpContextContract) {
public async records({ request, response }: HttpContextContract) {
console.time('recordsRequest')
const { puuid, season } = await request.validate(SummonerRecordValidator)
const records = await MatchRepository.records(puuid, season)
const records = await MatchRepository.records(puuid)
const recordsSerialized = records.map((record) => {
return {
...record,
what: record.what.split('.')[1],
champion: BasicMatchSerializer.getChampion(record.champion_id),
}
})
console.timeEnd('recordsRequest')
return response.json(records)
return response.json(recordsSerialized)
}
/**
* POST - Return live match detail
* @param ctx
*/
public async liveMatchDetails ({ request, response }: HttpContextContract) {
public async liveMatchDetails({ request, response }: HttpContextContract) {
console.time('liveMatchDetails')
const { id, region } = await request.validate(SummonerLiveValidator)
// CURRENT GAME
let currentGame = await Jax.Spectator.summonerID(id, region)
const currentGame = await Jax.Spectator.summonerID(id, region)
if (!currentGame) {
return response.json(null)
}
currentGame = await LiveMatchTransformer.transform(currentGame, { region })
const currentGameSerialized = await LiveMatchSerializer.serializeOneMatch(currentGame, region)
console.timeEnd('liveMatchDetails')
return response.json(currentGame)
return response.json(currentGameSerialized)
}
}

View file

@ -17,7 +17,7 @@ import Logger from '@ioc:Adonis/Core/Logger'
import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler'
export default class ExceptionHandler extends HttpExceptionHandler {
constructor () {
constructor() {
super(Logger)
}
}

View file

@ -1,57 +0,0 @@
import { Model } from '@ioc:Mongodb/Model'
import { Champion, ParticipantDetails } from 'App/Models/Match'
export interface DetailedMatchModel {
gameId: number,
season: number,
blueTeam: Team,
redTeam: Team,
map: number,
gamemode: number,
date: number,
region: string,
time: number
}
interface Team {
bans: Ban[],
barons: number,
color: string,
dragons: number,
inhibitors: number,
players: ParticipantDetails[],
result: string,
riftHerald: number,
teamStats: TeamStats,
towers: number
}
export interface Ban {
championId: number,
pickTurn: number,
champion: Champion<null | number, null | string>
}
export interface TeamStats {
kills: number,
deaths: number,
assists: number,
gold: number,
dmgChamp: number,
dmgObj: number,
dmgTaken: number
}
export default class DetailedMatch extends Model implements DetailedMatchModel {
public static collectionName = 'detailed_matches'
public gameId: number
public season: number
public blueTeam: Team
public redTeam: Team
public map: number
public gamemode: number
public date: number
public region: string
public time: number
}

View file

@ -1,134 +1,40 @@
import { Model } from '@ioc:Mongodb/Model'
import { BaseModel, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm'
import MatchPlayer from './MatchPlayer'
import MatchTeam from './MatchTeam'
export interface MatchModel extends ParticipantDetails {
account_id: string,
summoner_puuid: string,
gameId: number,
result: string,
allyTeam: ParticipantBasic[],
enemyTeam: ParticipantBasic[],
map: number,
gamemode: number,
date: number,
region: string,
season: number,
time: number,
newMatch?: boolean,
}
export default class Match extends BaseModel {
public static selfAssignPrimaryKey = true
export interface ParticipantDetails {
name: string,
summonerId: string,
champion: Champion,
role: string,
primaryRune: string | null,
secondaryRune: string | null,
level: number,
items: (Item | null)[],
firstSum: SummonerSpell | number | null,
secondSum: SummonerSpell | number | null,
stats: Stats,
percentStats?: PercentStats
rank?: Rank | null,
perks?: Perks
}
@column({ isPrimary: true })
public id: string
export interface Champion<T = number, U = string> {
id: number | T,
name: string | U,
alias?: string,
roles?: string[],
icon?: string
}
export interface SummonerSpell {
name: string,
description: string,
icon: string
}
export interface Rank {
tier: string,
shortName: string | number
}
export interface Perks {
primaryStyle: number;
secondaryStyle: number;
selected: number[];
}
export interface ParticipantBasic {
account_id: string,
name: string,
role: string,
champion: Champion
}
export interface Item {
image: string,
name: string,
description: string,
price: number
}
export interface Stats {
kills: number,
deaths: number,
assists: number,
minions: number,
vision: number,
gold: number,
dmgChamp: number,
dmgObj: number,
dmgTaken: number,
kda: number | string,
realKda: number,
criticalStrike?: number,
killingSpree?: number,
doubleKills?: number,
tripleKills?: number,
quadraKills?: number,
pentaKills?: number,
heal?: number,
towers?: number,
longestLiving?: number,
kp: number | string,
}
export interface PercentStats {
minions: number,
vision: number,
gold: string,
dmgChamp: string,
dmgObj: string,
dmgTaken: string,
}
export default class Match extends Model implements MatchModel {
public static collectionName = 'matches'
public account_id: string
public summoner_puuid: string
@column()
public gameId: number
public result: string
public allyTeam: ParticipantBasic[]
public enemyTeam: ParticipantBasic[]
@column()
public map: number
@column()
public gamemode: number
@column()
public date: number
@column()
public region: string
@column()
public result: number
@column()
public season: number
public time: number
public name: string
public summonerId: string
public champion: Champion
public role: string
public primaryRune: string
public secondaryRune: string
public level: number
public items: Item[]
public firstSum: number
public secondSum: number
public stats: Stats
@column()
public gameDuration: number
@hasMany(() => MatchTeam)
public teams: HasMany<typeof MatchTeam>
@hasMany(() => MatchPlayer)
public players: HasMany<typeof MatchPlayer>
}

View file

@ -0,0 +1,165 @@
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 {
@column({ isPrimary: true })
public id: number
@column()
public matchId: number
@belongsTo(() => Match)
public match: BelongsTo<typeof Match>
@hasMany(() => MatchPlayerRank, {
localKey: 'id',
foreignKey: 'playerId',
})
public ranks: HasMany<typeof MatchPlayerRank>
@column()
public participantId: number
@column()
public summonerId: string
@column()
public summonerPuuid: string
@belongsTo(() => Summoner, {
localKey: 'puuid',
foreignKey: 'summonerPuuid',
})
public summoner: BelongsTo<typeof Summoner>
@column()
public summonerName: string
@column()
public team: number
@column()
public teamPosition: number
@column()
public win: number
@column()
public loss: number
@column()
public remake: number
@column()
public kills: number
@column()
public deaths: number
@column()
public assists: number
@column()
public kda: number
@column()
public kp: number
@column()
public champLevel: number
@column()
public championId: number
@column()
public championRole: number
@column()
public doubleKills: number
@column()
public tripleKills: number
@column()
public quadraKills: number
@column()
public pentaKills: number
@column()
public baronKills: number
@column()
public dragonKills: number
@column()
public turretKills: number
@column()
public visionScore: number
@column()
public gold: number
@column()
public summoner1Id: number
@column()
public summoner2Id: number
@column()
public item0: number
@column()
public item1: number
@column()
public item2: number
@column()
public item3: number
@column()
public item4: number
@column()
public item5: number
@column()
public item6: number
@column()
public damageDealtObjectives: number
@column()
public damageDealtChampions: number
@column()
public damageTaken: number
@column()
public heal: number
@column()
public minions: number
@column()
public criticalStrike: number
@column()
public killingSpree: number
@column()
public timeSpentLiving: number
@column()
public perksPrimaryStyle: number
@column()
public perksSecondaryStyle: number
@column()
public perksSelected: number[]
}

View file

@ -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<typeof MatchPlayer>
@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
}

View file

@ -0,0 +1,40 @@
import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'
import Match from './Match'
export default class MatchTeam extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
public matchId: string
@belongsTo(() => Match)
public match: BelongsTo<typeof Match>
@column()
public color: number
@column()
public result: string
@column()
public barons: number
@column()
public dragons: number
@column()
public inhibitors: number
@column()
public riftHeralds: number
@column()
public towers: number
@column()
public bans?: number[]
@column()
public banOrders?: number[]
}

View file

@ -1,21 +1,36 @@
import { Model } from '@ioc:Mongodb/Model'
import { MatchReferenceDto } from 'App/Services/Jax/src/Endpoints/MatchlistEndpoint'
import { DateTime } from 'luxon'
import { BaseModel, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm'
import SummonerMatchlist from './SummonerMatchlist'
import SummonerName from './SummonerName'
import MatchPlayer from './MatchPlayer'
export interface SummonerModel {
puuid: string,
matchList?: MatchReferenceDto[],
names?: SummonerNames[]
}
interface SummonerNames {
name: string,
date: Date
}
export default class Summoner extends Model implements SummonerModel {
public static collectionName = 'summoners'
export default class Summoner extends BaseModel {
public static selfAssignPrimaryKey = true
@column({ isPrimary: true })
public puuid: string
public matchList?: MatchReferenceDto[]
public names?: SummonerNames[]
@column.dateTime({ autoCreate: true })
public createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
@hasMany(() => SummonerMatchlist, {
localKey: 'puuid',
foreignKey: 'summonerPuuid',
})
public matchList: HasMany<typeof SummonerMatchlist>
@hasMany(() => MatchPlayer, {
localKey: 'puuid',
foreignKey: 'summonerPuuid',
})
public matches: HasMany<typeof MatchPlayer>
@hasMany(() => SummonerName, {
localKey: 'puuid',
foreignKey: 'summonerPuuid',
})
public names: HasMany<typeof SummonerName>
}

View file

@ -0,0 +1,21 @@
import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'
import Summoner from './Summoner'
export default class SummonerMatchlist extends BaseModel {
public static table = 'summoner_matchlist'
@column({ isPrimary: true })
public id: number
@column()
public summonerPuuid: string
@column()
public matchId: string
@belongsTo(() => Summoner, {
localKey: 'puuid',
foreignKey: 'summonerPuuid',
})
public summoner: BelongsTo<typeof Summoner>
}

View file

@ -0,0 +1,23 @@
import { DateTime } from 'luxon'
import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'
import Summoner from './Summoner'
export default class SummonerName extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
public summonerPuuid: string
@belongsTo(() => Summoner, {
localKey: 'puuid',
foreignKey: 'summonerPuuid',
})
public summoner: BelongsTo<typeof Summoner>
@column()
public name: string
@column.dateTime({ autoCreate: true })
public createdAt: DateTime
}

View file

@ -0,0 +1,160 @@
import Database from '@ioc:Adonis/Lucid/Database'
import { MatchDto } from 'App/Services/Jax/src/Endpoints/MatchEndpoint'
import Match from 'App/Models/Match'
import { getSeasonNumber, queuesWithRole } from 'App/helpers'
import CDragonService from 'App/Services/CDragonService'
import { ChampionRoles, TeamPosition } from './ParsedType'
class MatchParser {
public async parseOneMatch(match: MatchDto) {
// Parse + store in database
// - 1x Match
const parsedMatch = await Match.create({
id: match.metadata.matchId,
gameId: match.info.gameId,
map: match.info.mapId,
gamemode: match.info.queueId,
date: match.info.gameCreation,
region: match.info.platformId.toLowerCase(),
result: match.info.teams[0].win ? match.info.teams[0].teamId : match.info.teams[1].teamId,
season: getSeasonNumber(match.info.gameCreation),
gameDuration: Math.round(match.info.gameDuration / 1000),
})
const isRemake = match.info.gameDuration < 300000
// - 2x MatchTeam : Red and Blue
for (const team of match.info.teams) {
let result = team.win ? 'Win' : 'Fail'
if (isRemake) {
result = 'Remake'
}
await parsedMatch.related('teams').create({
matchId: match.metadata.matchId,
color: team.teamId,
result: result,
barons: team.objectives.baron.kills,
dragons: team.objectives.dragon.kills,
inhibitors: team.objectives.inhibitor.kills,
riftHeralds: team.objectives.riftHerald.kills,
towers: team.objectives.tower.kills,
bans: team.bans.length ? team.bans.map((ban) => ban.championId) : undefined,
banOrders: team.bans.length ? team.bans.map((ban) => ban.pickTurn) : undefined,
})
}
// - 10x MatchPlayer
const matchPlayers: any[] = []
for (const player of match.info.participants) {
const kda =
player.kills + player.assists !== 0 && player.deaths === 0
? player.kills + player.assists
: +(player.deaths === 0 ? 0 : (player.kills + player.assists) / player.deaths).toFixed(2)
const team =
match.info.teams[0].teamId === player.teamId ? match.info.teams[0] : match.info.teams[1]
const teamKills = team.objectives.champion.kills
const kp =
teamKills === 0 ? 0 : +(((player.kills + player.assists) * 100) / teamKills).toFixed(1)
const primaryStyle = player.perks.styles.find((s) => s.description === 'primaryStyle')
const secondaryStyle = player.perks.styles.find((s) => s.description === 'subStyle')
const perksSelected: number[] = []
for (const styles of player.perks.styles) {
for (const perk of styles.selections) {
perksSelected.push(perk.perk)
}
}
// Fix championId bug in older matches
if (player.championId > 1000) {
const championId = Object.keys(CDragonService.champions).find(
(key) =>
CDragonService.champions[key].name === player.championName ||
CDragonService.champions[key].alias === player.championName
)
if (!championId) {
console.log(
`CHAMPION NOT FOUND AT ALL: ${player.championId} FROM: ${match.metadata.matchId}`
)
}
player.championId = championId ? Number(championId) : 1
}
const originalChampionData = CDragonService.champions[player.championId]
const champRoles = originalChampionData.roles
matchPlayers.push({
match_id: match.metadata.matchId,
participant_id: player.participantId,
summoner_id: player.summonerId,
summoner_puuid: player.puuid,
summoner_name: player.summonerName,
win: team.win ? 1 : 0,
loss: team.win ? 0 : 1,
remake: isRemake ? 1 : 0,
team: player.teamId,
team_position:
player.teamPosition.length && queuesWithRole.includes(match.info.queueId)
? TeamPosition[player.teamPosition]
: TeamPosition.NONE,
kills: player.kills,
deaths: player.deaths,
assists: player.assists,
kda: kda,
kp: kp,
champ_level: player.champLevel,
champion_id: player.championId,
champion_role: ChampionRoles[champRoles[0]],
double_kills: player.doubleKills,
triple_kills: player.tripleKills,
quadra_kills: player.quadraKills,
penta_kills: player.pentaKills,
baron_kills: player.baronKills,
dragon_kills: player.dragonKills,
turret_kills: player.turretKills,
vision_score: player.visionScore,
gold: player.goldEarned,
summoner1_id: player.summoner1Id,
summoner2_id: player.summoner2Id,
item0: player.item0,
item1: player.item1,
item2: player.item2,
item3: player.item3,
item4: player.item4,
item5: player.item5,
item6: player.item6,
damage_dealt_objectives: player.damageDealtToObjectives,
damage_dealt_champions: player.totalDamageDealtToChampions,
damage_taken: player.totalDamageTaken,
heal: player.totalHeal,
minions: player.totalMinionsKilled + player.neutralMinionsKilled,
critical_strike: player.largestCriticalStrike,
killing_spree: player.killingSprees,
time_spent_living: player.longestTimeSpentLiving,
perks_primary_style: primaryStyle!.style,
perks_secondary_style: secondaryStyle!.style,
perks_selected: perksSelected.concat(Object.values(player.perks.statPerks)),
})
}
await Database.table('match_players').multiInsert(matchPlayers)
// Load Match relations
await parsedMatch.load((loader) => {
loader.load('teams').load('players')
})
return parsedMatch
}
public async parse(matches: MatchDto[]) {
// Loop on all matches and call .parseOneMatch on it
const parsedMatches: Match[] = []
for (const match of matches) {
parsedMatches.push(await this.parseOneMatch(match))
}
return parsedMatches
}
}
export default new MatchParser()

View file

@ -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<PlayerRankParsed[]> {
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()

View file

@ -0,0 +1,246 @@
import Database from '@ioc:Adonis/Lucid/Database'
import Match from 'App/Models/Match'
import { getSeasonNumber, notEmpty, PlayerRole, queuesWithRole, supportItems } from 'App/helpers'
import CDragonService from 'App/Services/CDragonService'
import { ChampionRoles, TeamPosition } from './ParsedType'
import { V4MatchDto } from 'App/Services/Jax/src/Endpoints/MatchV4Endpoint'
import RoleIdentificationService from 'App/Services/RoleIdentificationService'
import Jax from 'App/Services/Jax'
class MatchV4Parser {
public createMatchId(gameId: number, region: string) {
return `${region.toUpperCase()}_${gameId}`
}
private getTeamRoles(team: PlayerRole[]) {
const teamJunglers = team.filter((p) => p.jungle && !p.support)
const jungle = teamJunglers.length === 1 ? teamJunglers[0].champion : undefined
const teamSupports = team.filter((p) => p.support && !p.jungle)
const support = teamSupports.length === 1 ? teamSupports[0].champion : undefined
return RoleIdentificationService.getRoles(
CDragonService.championRoles,
team.map((p) => p.champion),
jungle,
support
)
}
private getMatchRoles(match: V4MatchDto) {
const blueChamps: PlayerRole[] = []
const redChamps: PlayerRole[] = []
match.participants.map((p) => {
const items = [
p.stats.item0,
p.stats.item1,
p.stats.item2,
p.stats.item3,
p.stats.item4,
p.stats.item5,
]
const playerRole = {
champion: p.championId,
jungle: p.spell1Id === 11 || p.spell2Id === 11,
support: supportItems.some((suppItem) => items.includes(suppItem)),
}
p.teamId === 100 ? blueChamps.push(playerRole) : redChamps.push(playerRole)
})
return {
blue: this.getTeamRoles(blueChamps),
red: this.getTeamRoles(redChamps),
}
}
public async parseOneMatch(match: V4MatchDto) {
// Parse + store in database
const matchId = this.createMatchId(match.gameId, match.platformId)
if (match.participants.length !== 10) {
console.log(`Match not saved because < 10 players. Gamemode: ${match.queueId}`)
return
}
// PUUID of the 10 players
const accountRequests = match.participantIdentities
.filter((p) => p.player.accountId !== '0')
.map((p) => Jax.Summoner.accountId(p.player.currentAccountId, match.platformId.toLowerCase()))
const playerAccounts = (await Promise.all(accountRequests)).filter(notEmpty)
if (!playerAccounts || !playerAccounts.length) {
console.log(`0 Account found from match: ${matchId}`)
return
}
const isRemake = match.gameDuration < 300
// Roles
const { blue: blueRoles, red: redRoles } = this.getMatchRoles(match)
// - 10x MatchPlayer
const matchPlayers: any[] = []
for (const player of match.participants) {
const identity = match.participantIdentities.find(
(p) => p.participantId === player.participantId
)!
const isBot = identity.player.accountId === '0'
const account = isBot
? null
: playerAccounts.find((p) => p.accountId === identity.player.currentAccountId)
if (!account && !isBot) {
console.log(`Account not found ${identity.player.currentAccountId}`)
console.log(`Match ${matchId} not saved in the database.`)
return
}
const kda =
player.stats.kills + player.stats.assists !== 0 && player.stats.deaths === 0
? player.stats.kills + player.stats.assists
: +(
player.stats.deaths === 0
? 0
: (player.stats.kills + player.stats.assists) / player.stats.deaths
).toFixed(2)
const team = match.teams[0].teamId === player.teamId ? match.teams[0] : match.teams[1]
const totalKills = match.participants.reduce((prev, current) => {
if (current.teamId !== player.teamId) {
return prev
}
return prev + current.stats.kills
}, 0)
const kp =
totalKills === 0
? 0
: +(((player.stats.kills + player.stats.assists) * 100) / totalKills).toFixed(1)
// Perks
const primaryStyle = player.stats.perkPrimaryStyle
const secondaryStyle = player.stats.perkSubStyle
const perksSelected: number[] = []
for (let i = 0; i < 6; i++) {
perksSelected.push(player.stats[`perk${i}`])
}
for (let i = 0; i < 3; i++) {
perksSelected.push(player.stats[`statPerk${i}`])
}
const originalChampionData = CDragonService.champions[player.championId]
const champRoles = originalChampionData.roles
// Role
const teamRoles = player.teamId === 100 ? blueRoles : redRoles
const role = Object.entries(teamRoles).find(
([, champion]) => player.championId === champion
)![0]
matchPlayers.push({
match_id: matchId,
participant_id: player.participantId,
summoner_id: isBot ? 'BOT' : identity.player.summonerId,
summoner_puuid: account ? account.puuid : 'BOT',
summoner_name: identity.player.summonerName,
win: team.win === 'Win' ? 1 : 0,
loss: team.win === 'Fail' ? 1 : 0,
remake: isRemake ? 1 : 0,
team: player.teamId,
team_position: queuesWithRole.includes(match.queueId)
? TeamPosition[role]
: TeamPosition.NONE,
kills: player.stats.kills,
deaths: player.stats.deaths,
assists: player.stats.assists,
kda: kda,
kp: kp,
champ_level: player.stats.champLevel,
champion_id: player.championId,
champion_role: ChampionRoles[champRoles[0]],
double_kills: player.stats.doubleKills,
triple_kills: player.stats.tripleKills,
quadra_kills: player.stats.quadraKills,
penta_kills: player.stats.pentaKills,
baron_kills: 0,
dragon_kills: 0,
turret_kills: player.stats.turretKills,
vision_score: player.stats.visionScore,
gold: player.stats.goldEarned,
summoner1_id: player.spell1Id,
summoner2_id: player.spell2Id,
item0: player.stats.item0,
item1: player.stats.item1,
item2: player.stats.item2,
item3: player.stats.item3,
item4: player.stats.item4,
item5: player.stats.item5,
item6: player.stats.item6,
damage_dealt_objectives: player.stats.damageDealtToObjectives,
damage_dealt_champions: player.stats.totalDamageDealtToChampions,
damage_taken: player.stats.totalDamageTaken,
heal: player.stats.totalHeal,
minions: player.stats.totalMinionsKilled + player.stats.neutralMinionsKilled,
critical_strike: player.stats.largestCriticalStrike,
killing_spree: player.stats.killingSprees,
time_spent_living: player.stats.longestTimeSpentLiving,
perks_primary_style: primaryStyle ?? 8100,
perks_secondary_style: secondaryStyle ?? 8000,
perks_selected: perksSelected,
})
}
await Database.table('match_players').multiInsert(matchPlayers)
// - 1x Match
const parsedMatch = await Match.create({
id: matchId,
gameId: match.gameId,
map: match.mapId,
gamemode: match.queueId,
date: match.gameCreation,
region: match.platformId.toLowerCase(),
result: match.teams[0].win === 'Win' ? match.teams[0].teamId : match.teams[1].teamId,
season: getSeasonNumber(match.gameCreation),
gameDuration: match.gameDuration,
})
// - 2x MatchTeam : Red and Blue
for (const team of match.teams) {
let result = team.win === 'Win' ? 'Win' : 'Fail'
if (isRemake) {
result = 'Remake'
}
await parsedMatch.related('teams').create({
matchId: matchId,
color: team.teamId,
result: result,
barons: team.baronKills,
dragons: team.dragonKills,
inhibitors: team.inhibitorKills,
riftHeralds: team.riftHeraldKills,
towers: team.towerKills,
bans: team.bans.length ? team.bans.map((ban) => ban.championId) : undefined,
banOrders: team.bans.length ? team.bans.map((ban) => ban.pickTurn) : undefined,
})
}
// Load Match relations
await parsedMatch.load((loader) => {
loader.load('teams').load('players')
})
return parsedMatch
}
public async parse(matches: V4MatchDto[]) {
// Loop on all matches and call .parseOneMatch on it
const parsedMatches: Match[] = []
for (const match of matches) {
const parsed = await this.parseOneMatch(match)
if (parsed) {
parsedMatches.push(parsed)
}
}
return parsedMatches.length
}
}
export default new MatchV4Parser()

View file

@ -0,0 +1,27 @@
export enum ChampionRoles {
assassin,
fighter,
mage,
marksman,
support,
tank,
}
export enum TeamPosition {
NONE,
TOP,
JUNGLE,
MIDDLE,
BOTTOM,
UTILITY,
}
export interface PlayerRankParsed {
player_id: number
gamemode: number
tier: string
rank: number
lp: number
wins: number
losses: number
}

View file

@ -1,337 +1,257 @@
import mongodb from '@ioc:Mongodb/Database'
import { Collection } from 'mongodb'
import Database from '@ioc:Adonis/Lucid/Database'
class MatchRepository {
private collection: Collection
private readonly JOIN_MATCHES = 'INNER JOIN matches ON matches.id = match_players.match_id'
private readonly JOIN_TEAMS =
'INNER JOIN match_teams ON match_players.match_id = match_teams.match_id AND match_players.team = match_teams.color'
private readonly JOIN_ALL = `${this.JOIN_MATCHES} ${this.JOIN_TEAMS}`
constructor () {
this.getCollection()
private readonly GLOBAL_FILTERS = `
match_players.summoner_puuid = :puuid
AND match_players.remake = 0
AND matches.gamemode NOT IN (800, 810, 820, 830, 840, 850, 2000, 2010, 2020)
`
public async recentActivity(puuid: string) {
const query = `
SELECT
to_timestamp(matches.date/1000)::date as day,
COUNT(match_players.id) as count
FROM
match_players
${this.JOIN_MATCHES}
WHERE
match_players.summoner_puuid = :puuid
GROUP BY
day
ORDER BY
day
`
const { rows } = await Database.rawQuery(query, { puuid })
return rows
}
/**
* Basic matchParams used in a lot of requests
* @param puuid of the summoner
*/
private matchParams (puuid: string, season?: number) {
return {
summoner_puuid: puuid,
result: { $not: { $eq: 'Remake' } },
gamemode: { $nin: [800, 810, 820, 830, 840, 850, 2000, 2010, 2020] },
season: season ? season : { $exists: true },
}
public async globalStats(puuid: string) {
const query = `
SELECT
SUM(match_players.kills) as kills,
SUM(match_players.deaths) as deaths,
SUM(match_players.assists) as assists,
SUM(match_players.minions) as minions,
SUM(matches.game_duration) as time,
SUM(match_players.vision_score) as vision,
COUNT(match_players.id) as count,
AVG(match_players.kp) as kp,
SUM(match_players.win) as wins,
SUM(match_players.loss) as losses
FROM
match_players
${this.JOIN_MATCHES}
WHERE
${this.GLOBAL_FILTERS}
LIMIT
1
`
const { rows } = await Database.rawQuery(query, { puuid })
return rows[0]
}
/**
* Build the aggregate mongo query
* @param puuid
* @param matchParams
* @param intermediateSteps
* @param groupId
* @param groupParams
* @param finalSteps
*/
private async aggregate (
puuid: string,
matchParams: object,
intermediateSteps: any[],
groupId: any,
groupParams: object,
finalSteps: any[],
season?: number,
) {
return this.collection.aggregate([
{
$match: {
...this.matchParams(puuid, season),
...matchParams,
},
},
...intermediateSteps,
{
$group: {
_id: groupId,
count: { $sum: 1 },
wins: {
$sum: {
$cond: [{ $eq: ['$result', 'Win'] }, 1, 0],
},
},
losses: {
$sum: {
$cond: [{ $eq: ['$result', 'Fail'] }, 1, 0],
},
},
...groupParams,
},
},
...finalSteps,
]).toArray()
public async gamemodeStats(puuid: string) {
const query = `
SELECT
matches.gamemode as id,
COUNT(match_players.id) as count,
SUM(match_players.win) as wins,
SUM(match_players.loss) as losses
FROM
match_players
${this.JOIN_MATCHES}
WHERE
${this.GLOBAL_FILTERS}
GROUP BY
matches.gamemode
ORDER BY
count DESC
`
const { rows } = await Database.rawQuery(query, { puuid })
return rows
}
/**
* Get MongoDB matches collection
*/
public async getCollection () {
if (!this.collection) {
this.collection = await mongodb.connection().collection('matches')
}
public async roleStats(puuid: string) {
const query = `
SELECT
match_players.team_position as role,
COUNT(match_players.id) as count,
SUM(match_players.win) as wins,
SUM(match_players.loss) as losses
FROM
match_players
${this.JOIN_MATCHES}
WHERE
${this.GLOBAL_FILTERS}
AND match_players.team_position != 0
GROUP BY
role
`
const { rows } = await Database.rawQuery(query, { puuid })
return rows
}
/**
* Get Summoner's statistics for the N most played champions
* @param puuid of the summoner
* @param limit number of champions to fetch
* @param season
*/
public async championStats (puuid: string, limit = 5, season?: number) {
const groupParams = {
champion: { $first: '$champion' },
kills: { $sum: '$stats.kills' },
deaths: { $sum: '$stats.deaths' },
assists: { $sum: '$stats.assists' },
public async championStats(puuid: string, limit: number) {
const query = `
SELECT
match_players.champion_id as id,
SUM(match_players.assists) as assists,
SUM(match_players.deaths) as deaths,
SUM(match_players.kills) as kills,
COUNT(match_players.id) as count,
SUM(match_players.win) as wins,
SUM(match_players.loss) as losses
FROM
match_players
${this.JOIN_MATCHES}
WHERE
${this.GLOBAL_FILTERS}
GROUP BY
match_players.champion_id
ORDER BY
count DESC, match_players.champion_id
LIMIT
:limit
`
const { rows } = await Database.rawQuery(query, { puuid, limit })
return rows
}
const finalSteps = [
{ $sort: { 'count': -1, 'champion.name': 1 } },
{ $limit: limit },
public async championClassStats(puuid: string) {
const query = `
SELECT
match_players.champion_role as id,
COUNT(match_players.id) as count,
SUM(match_players.win) as wins,
SUM(match_players.loss) as losses
FROM
match_players
${this.JOIN_MATCHES}
WHERE
${this.GLOBAL_FILTERS}
GROUP BY
match_players.champion_role
ORDER BY
count DESC
`
const { rows } = await Database.rawQuery(query, { puuid })
return rows
}
public async championCompleteStats(puuid: string, queue?: number, season?: number) {
const query = `
SELECT
match_players.champion_id as id,
SUM(match_players.assists) as assists,
SUM(match_players.deaths) as deaths,
SUM(match_players.kills) as kills,
COUNT(match_players.id) as count,
SUM(match_players.win) as wins,
SUM(match_players.loss) as losses,
AVG(matches.game_duration)::int as "gameLength",
AVG(match_players.minions)::int as minions,
AVG(match_players.gold)::int as gold,
AVG(match_players.damage_dealt_champions)::int as "dmgChamp",
AVG(match_players.damage_taken)::int as "dmgTaken",
AVG(match_players.kp) as kp,
MAX(matches.date) as date
FROM
match_players
${this.JOIN_MATCHES}
WHERE
${this.GLOBAL_FILTERS}
GROUP BY
match_players.champion_id
ORDER BY
count DESC, match_players.champion_id
`
const { rows } = await Database.rawQuery(query, { puuid })
return rows
}
public async mates(puuid: string) {
const query = `
SELECT
(array_agg(mates.summoner_name ORDER BY mates.match_id DESC))[1] as name,
COUNT(match_players.id) as count,
SUM(match_players.win) as wins,
SUM(match_players.loss) as losses
FROM
match_players
${this.JOIN_ALL}
INNER JOIN match_players as mates ON match_players.match_id = mates.match_id AND match_players.team = mates.team
WHERE
${this.GLOBAL_FILTERS}
GROUP BY
mates.summoner_puuid
ORDER BY
count DESC, wins DESC
LIMIT
15
`
const { rows } = await Database.rawQuery(query, { puuid })
// Remove the Summoner himself + unique game mates
return rows.splice(1).filter((row) => row.count > 1)
}
public async records(puuid: string) {
const fields = [
'match_players.kills',
'match_players.deaths',
'match_players.assists',
'match_players.gold',
'matches.game_duration',
'match_players.minions',
'match_players.kda',
'match_players.damage_taken',
'match_players.damage_dealt_champions',
'match_players.damage_dealt_objectives',
'match_players.kp',
'match_players.vision_score',
'match_players.critical_strike',
'match_players.time_spent_living',
'match_players.heal',
'match_players.turret_kills',
'match_players.killing_spree',
'match_players.double_kills',
'match_players.triple_kills',
'match_players.quadra_kills',
'match_players.penta_kills',
]
return this.aggregate(puuid, {}, [], '$champion.id', groupParams, finalSteps, season)
}
/**
* Get Summoner's statistics for all played champion classes
* @param puuid of the summoner
* @param season
*/
public async championClassStats (puuid: string, season?: number) {
const groupId = { '$arrayElemAt': ['$champion.roles', 0] }
return this.aggregate(puuid, {}, [], groupId, {}, [], season)
}
const query = fields
.map((field) => {
return `
(SELECT
'${field}' AS what,
${field} AS amount,
match_players.win as result,
matches.id,
matches.date,
matches.gamemode,
match_players.champion_id
FROM
match_players
${this.JOIN_MATCHES}
WHERE
${this.GLOBAL_FILTERS}
ORDER BY
${field} DESC, matches.id
LIMIT
1)
`
})
.join('UNION ALL ')
/**
* Get Summoner's complete statistics for the all played champs
* @param puuid of the summoner
* @param queue of the matches to fetch, if not set: get all matches
* @param season of the matches to fetch, if not set: get all seasons
*/
public async championCompleteStats (puuid: string, queue?: number, season?: number) {
const matchParams = queue ? { gamemode: { $eq: Number(queue) } } : {}
const groupParams = {
time: { $sum: '$time' },
gameLength: { $avg: '$time' },
date: { $max: '$date' },
champion: { $first: '$champion' },
kills: { $sum: '$stats.kills' },
deaths: { $sum: '$stats.deaths' },
assists: { $sum: '$stats.assists' },
minions: { $avg: '$stats.minions' },
gold: { $avg: '$stats.gold' },
dmgChamp: { $avg: '$stats.dmgChamp' },
dmgTaken: { $avg: '$stats.dmgTaken' },
kp: { $avg: '$stats.kp' },
}
const finalSteps = [
{ $sort: { 'count': -1, 'champion.name': 1 } },
]
return this.aggregate(puuid, matchParams, [], '$champion.id', groupParams, finalSteps, season)
}
/**
* Get Summoner's statistics for all played modes
* @param puuid of the summoner
* @param season
*/
public async gamemodeStats (puuid: string, season?: number) {
return this.aggregate(puuid, {}, [], '$gamemode', {}, [], season)
}
/**
* Get global Summoner's statistics
* @param puuid of the summoner
* @param season
*/
public async globalStats (puuid: string, season?: number) {
const groupParams = {
time: { $sum: '$time' },
kills: { $sum: '$stats.kills' },
deaths: { $sum: '$stats.deaths' },
assists: { $sum: '$stats.assists' },
minions: { $sum: '$stats.minions' },
vision: { $sum: '$stats.vision' },
kp: { $avg: '$stats.kp' },
}
return this.aggregate(puuid, {}, [], null, groupParams, [], season)
}
/**
* Get Summoner's all records
* @param puuid of the summoner
* @param season of the matches to fetch, if null get all seasons
*/
public async records (puuid: string, season?: number) {
const records = await this.collection.aggregate([
{
$match: {
...this.matchParams(puuid, season),
},
},
{
$group: {
_id: null,
maxKills: { $max: '$stats.kills' },
maxDeaths: { $max: '$stats.deaths' },
maxAssists: { $max: '$stats.assists' },
maxGold: { $max: '$stats.gold' },
maxTime: { $max: '$time' },
maxMinions: { $max: '$stats.minions' },
maxKda: { $max: '$stats.realKda' },
maxDmgTaken: { $max: '$stats.dmgTaken' },
maxDmgChamp: { $max: '$stats.dmgChamp' },
maxDmgObj: { $max: '$stats.dmgObj' },
maxKp: { $max: '$stats.kp' },
maxVision: { $max: '$stats.vision' },
maxCriticalStrike: { $max: '$stats.criticalStrike' },
maxLiving: { $max: '$stats.longestLiving' },
maxHeal: { $max: '$stats.heal' },
maxTowers: { $max: '$stats.towers' },
maxKillingSpree: { $max: '$stats.killingSpree' },
maxDouble: { $max: '$stats.doubleKills' },
maxTriple: { $max: '$stats.tripleKills' },
maxQuadra: { $max: '$stats.quadraKills' },
maxPenta: { $max: '$stats.pentaKills' },
docs: {
'$push': {
'champion': '$champion',
'gameId': '$gameId',
'kills': '$stats.kills',
'deaths': '$stats.deaths',
'assists': '$stats.assists',
'gold': '$stats.gold',
'time': '$time',
'minions': '$stats.minions',
'kda': '$stats.realKda',
'dmgTaken': '$stats.dmgTaken',
'dmgChamp': '$stats.dmgChamp',
'dmgObj': '$stats.dmgObj',
'kp': '$stats.kp',
'vision': '$stats.vision',
'criticalStrike': '$stats.criticalStrike',
'longestLiving': '$stats.longestLiving',
'heal': '$stats.heal',
'towers': '$stats.towers',
'killingSpree': '$stats.killingSpree',
'doubleKills': '$stats.doubleKills',
'tripleKills': '$stats.tripleKills',
'quadraKills': '$stats.quadraKills',
'pentaKills': '$stats.pentaKills',
'result': '$result',
'date': '$date',
'gamemode': '$gamemode',
},
},
},
},
{
$project: {
_id: 0,
/* eslint-disable max-len */
maxKills: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.kills', '$maxKills'] } } }, 0] },
maxDeaths: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.deaths', '$maxDeaths'] } } }, 0] },
maxAssists: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.assists', '$maxAssists'] } } }, 0] },
maxGold: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.gold', '$maxGold'] } } }, 0] },
maxTime: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.time', '$maxTime'] } } }, 0] },
maxMinions: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.minions', '$maxMinions'] } } }, 0] },
maxKda: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.kda', '$maxKda'] } } }, 0] },
maxDmgTaken: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.dmgTaken', '$maxDmgTaken'] } } }, 0] },
maxDmgChamp: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.dmgChamp', '$maxDmgChamp'] } } }, 0] },
maxDmgObj: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.dmgObj', '$maxDmgObj'] } } }, 0] },
maxKp: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.kp', '$maxKp'] } } }, 0] },
maxVision: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.vision', '$maxVision'] } } }, 0] },
maxCriticalStrike: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.criticalStrike', '$maxCriticalStrike'] } } }, 0] },
maxLiving: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.longestLiving', '$maxLiving'] } } }, 0] },
maxHeal: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.heal', '$maxHeal'] } } }, 0] },
maxTowers: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.towers', '$maxTowers'] } } }, 0] },
maxKillingSpree: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.killingSpree', '$maxKillingSpree'] } } }, 0] },
maxDouble: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.doubleKills', '$maxDouble'] } } }, 0] },
maxTriple: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.tripleKills', '$maxTriple'] } } }, 0] },
maxQuadra: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.quadraKills', '$maxQuadra'] } } }, 0] },
maxPenta: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.pentaKills', '$maxPenta'] } } }, 0] },
},
},
]).toArray()
return records[0]
}
/**
* Get Summoner's statistics for the 5 differnt roles
* @param puuid of the summoner
* @param season
*/
public async roleStats (puuid: string, season?: number) {
const matchParams = {
role: { $not: { $eq: 'NONE' } },
}
const finalSteps = [
{
$project: {
role: '$_id',
count: '$count',
wins: '$wins',
losses: '$losses',
},
},
]
return this.aggregate(puuid, matchParams, [], '$role', {}, finalSteps, season)
}
/**
* Get Summoner's played seasons
* @param puuid of the summoner
*/
public async seasons (puuid: string) {
return this.collection.aggregate([
{
$match: {
...this.matchParams(puuid),
},
},
{
$group: { _id: '$season' },
},
]).toArray()
}
/**
* Get Summoner's mates list
* @param puuid of the summoner
* @param season
*/
public async mates (puuid: string, season?: number) {
const intermediateSteps = [
{ $sort: { 'gameId': -1 } },
{ $unwind: '$allyTeam' },
]
const groupParams = {
account_id: { $first: '$account_id' },
name: { $first: '$allyTeam.name' },
mateId: { $first: '$allyTeam.account_id' },
}
const finalSteps = [
{
'$addFields': {
'idEq': { '$eq': ['$mateId', '$account_id'] },
},
},
{
$match: {
'idEq': false,
'count': { $gte: 2 },
},
},
{ $sort: { 'count': -1, 'name': 1 } },
{ $limit: 15 },
]
return this.aggregate(puuid, {}, intermediateSteps, '$allyTeam.account_id', groupParams, finalSteps, season)
const { rows } = await Database.rawQuery(query, { puuid })
return rows
}
}

View file

@ -0,0 +1,80 @@
import { getSeasonNumber, sortTeamByRole } from 'App/helpers'
import Match from 'App/Models/Match'
import MatchPlayer from 'App/Models/MatchPlayer'
import { TeamPosition } from 'App/Parsers/ParsedType'
import MatchSerializer from './MatchSerializer'
import { SerializedMatch, SerializedMatchStats, SerializedMatchTeamPlayer } from './SerializedTypes'
class BasicMatchSerializer extends MatchSerializer {
protected getPlayerSummary(player: MatchPlayer): SerializedMatchTeamPlayer {
return {
puuid: player.summonerPuuid,
champion: this.getChampion(player.championId),
name: player.summonerName,
role: TeamPosition[player.teamPosition],
}
}
protected getTeamSummary(players: MatchPlayer[]): SerializedMatchTeamPlayer[] {
return players.map((p) => this.getPlayerSummary(p)).sort(sortTeamByRole)
}
protected getStats(player: MatchPlayer): SerializedMatchStats {
return {
kills: player.kills,
deaths: player.deaths,
assists: player.assists,
minions: player.minions,
vision: player.visionScore,
gold: player.gold,
dmgChamp: player.damageDealtChampions,
dmgObj: player.damageDealtObjectives,
dmgTaken: player.damageTaken,
kp: player.kp,
kda: player.kills + player.assists !== 0 && player.deaths === 0 ? '∞' : player.kda,
realKda: player.kda,
criticalStrike: player.criticalStrike,
killingSpree: player.killingSpree,
doubleKills: player.doubleKills,
tripleKills: player.tripleKills,
quadraKills: player.quadraKills,
pentaKills: player.pentaKills,
heal: player.heal,
towers: player.turretKills,
longestLiving: player.timeSpentLiving,
}
}
public serializeOneMatch(match: Match, puuid: string, newMatch = false): SerializedMatch {
const identity = match.players.find((p) => p.summonerPuuid === puuid)!
const allyTeam = match.teams.find((t) => t.color === identity.team)!
const allyPlayers: MatchPlayer[] = []
const enemyPlayers: MatchPlayer[] = []
for (const p of match.players) {
p.team === identity.team ? allyPlayers.push(p) : enemyPlayers.push(p)
}
return {
allyTeam: this.getTeamSummary(allyPlayers),
date: match.date,
enemyTeam: this.getTeamSummary(enemyPlayers),
matchId: match.id,
gamemode: match.gamemode,
map: match.map,
newMatch,
region: match.region,
result: allyTeam.result,
season: getSeasonNumber(match.date),
stats: this.getStats(identity),
time: match.gameDuration,
...this.getPlayerBase(identity),
}
}
public serialize(matches: Match[], puuid: string, newMatches = false): SerializedMatch[] {
return matches.map((match) => this.serializeOneMatch(match, puuid, newMatches))
}
}
export default new BasicMatchSerializer()

View file

@ -0,0 +1,153 @@
import { sortTeamByRole } from 'App/helpers'
import Match from 'App/Models/Match'
import MatchPlayer from 'App/Models/MatchPlayer'
import MatchTeam from 'App/Models/MatchTeam'
import MatchSerializer from './MatchSerializer'
import {
SerializedDetailedMatch,
SerializedDetailedMatchBan,
SerializedDetailedMatchPlayer,
SerializedDetailedMatchStats,
SerializedDetailedMatchTeam,
SerializedDetailedMatchTeamStats,
} from './SerializedTypes'
class DetailedMatchSerializer extends MatchSerializer {
protected getTeamBans(team: MatchTeam): SerializedDetailedMatchBan[] {
if (!team.bans || !team.banOrders) {
return []
}
return team.bans.map((banId, index) => {
return {
champion: this.getChampion(banId),
championId: banId,
pickTurn: team.banOrders![index],
}
})
}
protected getTeamStats(players: MatchPlayer[]): SerializedDetailedMatchTeamStats {
return players.reduce(
(acc, player) => {
acc.kills += player.kills
acc.deaths += player.deaths
acc.assists += player.assists
acc.gold += player.gold
acc.dmgChamp += player.damageDealtChampions
acc.dmgObj += player.damageDealtObjectives
acc.dmgTaken += player.damageTaken
return acc
},
{ kills: 0, deaths: 0, assists: 0, gold: 0, dmgChamp: 0, dmgObj: 0, dmgTaken: 0 }
)
}
protected getPlayersDetailed(
players: MatchPlayer[],
teamStats: SerializedDetailedMatchTeamStats,
gameDuration: number
): SerializedDetailedMatchPlayer[] {
return players
.map((player) => {
const stats: SerializedDetailedMatchStats = {
kills: player.kills,
deaths: player.deaths,
assists: player.assists,
minions: player.minions,
vision: player.visionScore,
gold: player.gold,
dmgChamp: player.damageDealtChampions,
dmgObj: player.damageDealtObjectives,
dmgTaken: player.damageTaken,
kp: player.kp.toFixed(1) + '%',
kda: player.kills + player.assists !== 0 && player.deaths === 0 ? '∞' : player.kda,
realKda: player.kda,
}
const percentStats = {
minions: +(player.minions / (gameDuration / 60)).toFixed(2),
vision: +(player.visionScore / (gameDuration / 60)).toFixed(2),
gold: +((player.gold * 100) / teamStats.gold).toFixed(1) + '%',
dmgChamp: +((player.damageDealtChampions * 100) / teamStats.dmgChamp).toFixed(1) + '%',
dmgObj:
+(
teamStats.dmgObj ? (player.damageDealtObjectives * 100) / teamStats.dmgObj : 0
).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)
}
protected getTeamDetailed(
team: MatchTeam,
players: MatchPlayer[],
gameDuration: number
): SerializedDetailedMatchTeam {
const teamStats = this.getTeamStats(players)
return {
bans: this.getTeamBans(team),
barons: team.barons,
color: team.color === 100 ? 'Blue' : 'Red',
dragons: team.dragons,
inhibitors: team.inhibitors,
players: this.getPlayersDetailed(players, teamStats, gameDuration),
result: team.result,
riftHeralds: team.riftHeralds,
teamStats,
towers: team.towers,
}
}
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
}
}
const serializedMatch = {
blueTeam: this.getTeamDetailed(blueTeam, bluePlayers, match.gameDuration),
date: match.date,
matchId: match.id,
gamemode: match.gamemode,
map: match.map,
redTeam: this.getTeamDetailed(redTeam, redPlayers, match.gameDuration),
region: match.region,
season: match.season,
time: match.gameDuration,
}
return {
match: serializedMatch,
ranksLoaded,
}
}
}
export default new DetailedMatchSerializer()

View file

@ -0,0 +1,81 @@
import { PlayerRole, queuesWithRole } from 'App/helpers'
import CDragonService from 'App/Services/CDragonService'
import { CurrentGameInfoDTO } from 'App/Services/Jax/src/Endpoints/SpectatorEndpoint'
import { RoleComposition } from 'App/Services/RoleIdentificationService'
import SummonerService from 'App/Services/SummonerService'
import MatchSerializer from './MatchSerializer'
import { SerializedLiveMatch, SerializedLiveMatchPlayer } from './SerializedTypes'
class LiveMatchSerializer extends MatchSerializer {
public async serializeOneMatch(
liveMatch: CurrentGameInfoDTO,
region: string
): Promise<SerializedLiveMatch> {
// Roles
const blueTeam: PlayerRole[] = [] // 100
const redTeam: PlayerRole[] = [] // 200
let blueRoles: RoleComposition = {}
let redRoles: RoleComposition = {}
const needsRole =
CDragonService.championRoles &&
(queuesWithRole.includes(liveMatch.gameQueueConfigId) ||
(liveMatch.gameType === 'CUSTOM_GAME' && liveMatch.participants.length === 10))
if (needsRole) {
liveMatch.participants.map((p) => {
const playerRole = {
champion: p.championId,
jungle: p.spell1Id === 11 || p.spell2Id === 11,
}
p.teamId === 100 ? blueTeam.push(playerRole) : redTeam.push(playerRole)
})
blueRoles = super.getTeamRoles(blueTeam)
redRoles = super.getTeamRoles(redTeam)
}
// Ranks
const requestsRanks = liveMatch.participants.map((p) =>
SummonerService.getRanked(p.summonerId, region)
)
const ranks = await Promise.all(requestsRanks)
// Players
const players: SerializedLiveMatchPlayer[] = liveMatch.participants.map((player, index) => {
let role: string | undefined
// Roles
if (needsRole) {
const roles = player.teamId === 100 ? blueRoles : redRoles
role = Object.entries(roles).find(([, champion]) => player.championId === champion)![0]
}
return {
...player,
role,
rank: ranks[index],
champion: this.getChampion(player.championId),
perks: {
primaryStyle: player.perks.perkStyle,
secondaryStyle: player.perks.perkSubStyle,
selected: player.perks.perkIds,
},
}
})
return {
gameId: liveMatch.gameId,
gameType: liveMatch.gameType,
gameStartTime: liveMatch.gameStartTime,
mapId: liveMatch.mapId,
gameLength: liveMatch.gameLength,
platformId: liveMatch.platformId,
gameMode: liveMatch.gameMode,
bannedChampions: liveMatch.bannedChampions,
gameQueueConfigId: liveMatch.gameQueueConfigId,
observers: liveMatch.observers,
participants: players,
}
}
}
export default new LiveMatchSerializer()

View file

@ -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()

View file

@ -0,0 +1,135 @@
import { PlayerRole } from 'App/helpers'
import MatchPlayer from 'App/Models/MatchPlayer'
import MatchPlayerRank from 'App/Models/MatchPlayerRank'
import { PlayerRankParsed, TeamPosition } from 'App/Parsers/ParsedType'
import CDragonService from 'App/Services/CDragonService'
import RoleIdentificationService, { RoleComposition } from 'App/Services/RoleIdentificationService'
import SummonerService from 'App/Services/SummonerService'
import {
SerializedBasePlayer,
SerializedMatchChampion,
SerializedMatchItem,
SerializedMatchPerks,
SerializedMatchSummonerSpell,
} from './SerializedTypes'
export default abstract class MatchSerializer {
/**
* Get champion specific data
* @param id of the champion
*/
public getChampion(id: number): SerializedMatchChampion {
const originalChampionData = CDragonService.champions[id]
const icon = CDragonService.createAssetUrl(originalChampionData.squarePortraitPath)
return {
icon,
id: originalChampionData.id,
name: originalChampionData.name,
alias: originalChampionData.alias,
roles: originalChampionData.roles,
}
}
/**
* Get Summoner Spell Data from CDragon
* @param id of the summonerSpell
*/
public getSummonerSpell(id: number): SerializedMatchSummonerSpell | null {
const spell = CDragonService.summonerSpells[id]
if (id === 0 || !spell) {
return null
}
return {
name: spell.name,
description: spell.description,
icon: CDragonService.createAssetUrl(spell.iconPath),
}
}
protected getItems(player: MatchPlayer): Array<SerializedMatchItem | null> {
const items: (SerializedMatchItem | null)[] = []
for (let i = 0; i < 6; i++) {
const id = player['item' + i]
if (id === 0) {
items.push(null)
continue
}
const item = CDragonService.items[id]
if (!item) {
items.push(null)
continue
}
items.push({
image: CDragonService.createAssetUrl(item.iconPath),
name: item.name,
description: item.description,
price: item.priceTotal,
})
}
return items
}
protected getPerks(player: MatchPlayer): SerializedMatchPerks {
return {
primaryStyle: player.perksPrimaryStyle,
secondaryStyle: player.perksSecondaryStyle,
selected: player.perksSelected,
}
}
protected getRuneIcons(perksSelected: number[], perksSecondaryStyle: number) {
const primaryRune = perksSelected.length ? CDragonService.perks[perksSelected[0]] : null
const secondaryRune = CDragonService.perkstyles[perksSecondaryStyle]
return {
primaryRune: primaryRune ? CDragonService.createAssetUrl(primaryRune.iconPath) : null,
secondaryRune: secondaryRune ? CDragonService.createAssetUrl(secondaryRune.iconPath) : null,
}
}
protected getPlayerBase(player: MatchPlayer): SerializedBasePlayer {
return {
champion: this.getChampion(player.championId),
items: this.getItems(player),
level: player.champLevel,
name: player.summonerName,
perks: this.getPerks(player),
role: TeamPosition[player.teamPosition],
summonerId: player.summonerId,
summonerPuuid: player.summonerPuuid,
summonerSpell1: this.getSummonerSpell(player.summoner1Id),
summonerSpell2: this.getSummonerSpell(player.summoner2Id),
}
}
protected getPlayerRank(rank: PlayerRankParsed | MatchPlayerRank) {
return {
tier: rank.tier,
rank: rank.rank,
lp: rank.lp,
wins: rank.wins,
losses: rank.losses,
shortName: SummonerService.getRankedShortName(rank),
}
}
/**
* Return the 5 roles of a team based on champions
* @param team 5 champions + smite from a team
*/
protected getTeamRoles(team: PlayerRole[]): RoleComposition {
const teamJunglers = team.filter((p) => p.jungle && !p.support)
const jungle = teamJunglers.length === 1 ? teamJunglers[0].champion : undefined
const teamSupports = team.filter((p) => p.support && !p.jungle)
const support = teamSupports.length === 1 ? teamSupports[0].champion : undefined
return RoleIdentificationService.getRoles(
CDragonService.championRoles,
team.map((p) => p.champion),
jungle,
support
)
}
}

View file

@ -1,7 +1,7 @@
import { PerkDTO, PerkStyleDTO } from 'App/Services/Jax/src/Endpoints/CDragonEndpoint'
class RuneTransformer {
public transformPerks (perks: PerkDTO[]) {
class RuneSerializer {
public serializePerks(perks: PerkDTO[]) {
return perks.reduce((acc, perk) => {
acc[perk.id] = {
name: perk.name,
@ -12,18 +12,16 @@ class RuneTransformer {
}, {})
}
public transformStyles (styles: PerkStyleDTO[]) {
public serializeStyles(styles: PerkStyleDTO[]) {
return styles.reduce((acc, style) => {
acc[style.id] = {
name: style.name,
icon: style.iconPath,
slots: style.slots
.filter(s => s.type !== 'kStatMod')
.map(s => s.perks),
slots: style.slots.filter((s) => s.type !== 'kStatMod').map((s) => s.perks),
}
return acc
}, {})
}
}
export default new RuneTransformer()
export default new RuneSerializer()

View file

@ -0,0 +1,211 @@
import {
CurrentGameInfoDTO,
GameCustomizationObjectDTO,
} from 'App/Services/Jax/src/Endpoints/SpectatorEndpoint'
import { LeagueEntriesByQueue } from 'App/Services/SummonerService'
export interface SerializedBasePlayer {
champion: SerializedMatchChampion
items: Array<SerializedMatchItem | null>
level: number
name: string
perks: SerializedMatchPerks
role: string
summonerId: string
summonerPuuid: string
summonerSpell1: SerializedMatchSummonerSpell | null
summonerSpell2: SerializedMatchSummonerSpell | null
}
export interface SerializedMatch extends SerializedBasePlayer {
allyTeam: SerializedMatchTeamPlayer[]
date: number
enemyTeam: SerializedMatchTeamPlayer[]
matchId: string
gamemode: number
map: number
newMatch: boolean
region: string
result: string
season: number
stats: SerializedMatchStats
time: number
}
export interface SerializedMatchTeamPlayer {
puuid: string
champion: SerializedMatchChampion
name: string
role: string
}
export interface SerializedMatchChampion {
alias: string
icon: string
id: number
name: string
roles: string[]
}
export interface SerializedMatchSummonerSpell {
name: string
description: string
icon: string
}
export interface SerializedMatchItem {
description: string
image: string
name: string
price: number
}
export interface SerializedMatchPerks {
primaryStyle: number
secondaryStyle: number
selected: number[]
}
export interface SerializedMatchStats {
assists: number
criticalStrike: number
deaths: number
dmgChamp: number
dmgObj: number
dmgTaken: number
doubleKills: number
gold: number
heal: number
kda: number | string
killingSpree: number
kills: number
kp: number
longestLiving: number
minions: number
pentaKills: number
quadraKills: number
realKda: number
towers: number
tripleKills: number
vision: number
}
/* ============================
Detailed Match
============================ */
export interface SerializedDetailedMatch {
blueTeam: SerializedDetailedMatchTeam
date: number
matchId: string
gamemode: number
map: number
redTeam: SerializedDetailedMatchTeam
region: string
season: number
time: number
}
export interface SerializedDetailedMatchTeam {
bans: SerializedDetailedMatchBan[]
barons: number
color: string
dragons: number
inhibitors: number
players: SerializedDetailedMatchPlayer[]
result: string
riftHeralds: number
teamStats: SerializedDetailedMatchTeamStats
towers: number
}
export interface SerializedDetailedMatchBan {
champion: SerializedMatchChampion
championId: number
pickTurn: number
}
export interface SerializedDetailedMatchPlayer extends SerializedBasePlayer {
id: number
stats: SerializedDetailedMatchStats
percentStats: SerializedDetailedMatchPercentStats
primaryRune: string | null
secondaryRune: string | null
}
export interface SerializedDetailedMatchTeamStats {
assists: number
deaths: number
dmgChamp: number
dmgObj: number
dmgTaken: number
gold: number
kills: number
}
export interface SerializedDetailedMatchStats {
assists: number
deaths: number
dmgChamp: number
dmgObj: number
dmgTaken: number
gold: number
kda: string | number
kills: number
kp: string
minions: number
realKda: number
vision: number
}
export interface SerializedDetailedMatchPercentStats {
dmgChamp: string
dmgObj: string
dmgTaken: string
gold: string
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
}
/* ============================
Live Match
============================ */
export interface SerializedLiveMatch extends Omit<CurrentGameInfoDTO, 'participants'> {
participants: SerializedLiveMatchPlayer[]
}
export interface SerializedLiveMatchPlayer {
bot: boolean
champion: SerializedMatchChampion
championId: number
gameCustomizationObjects: GameCustomizationObjectDTO[]
perks: SerializedMatchPerks
profileIconId: number
rank: LeagueEntriesByQueue
role?: string
spell1Id: number
spell2Id: number
summonerId: string
summonerName: string
teamId: number
}

View file

@ -0,0 +1,64 @@
import Jax from 'App/Services/Jax'
import {
ChampionDTO,
ItemDTO,
PerkDTO,
PerkStyleDTO,
SummonerSpellDTO,
} from 'App/Services/Jax/src/Endpoints/CDragonEndpoint'
import RoleIdentificationService, {
ChampionsPlayRate,
} from 'App/Services/RoleIdentificationService'
interface Identifiable {
id: number
}
export interface CDragonCache<T> {
[id: string]: T
}
class CDragonService {
public champions: CDragonCache<ChampionDTO>
public items: CDragonCache<ItemDTO>
public perks: CDragonCache<PerkDTO>
public perkstyles: CDragonCache<PerkStyleDTO>
public summonerSpells: CDragonCache<SummonerSpellDTO>
public championRoles: ChampionsPlayRate
public readonly BASE_URL =
'https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/'
private setupCache<T extends Identifiable>(dto: T[]) {
return dto.reduce((obj, item) => ((obj[item.id] = item), obj), {})
}
/**
* Give the full CDragon image path from the iconPath field
*/
public createAssetUrl(iconPath: string) {
const name = iconPath.split('/assets/')[1].toLowerCase()
return `${this.BASE_URL}${name}`
}
/**
* Get global Context with CDragon Data
*/
public async getContext() {
const items = await Jax.CDragon.items()
const champions = await Jax.CDragon.champions()
const perks = await Jax.CDragon.perks()
const perkstyles = await Jax.CDragon.perkstyles()
const summonerSpells = await Jax.CDragon.summonerSpells()
const championRoles = await RoleIdentificationService.pullData().catch(() => {})
this.champions = this.setupCache(champions)
this.items = this.setupCache(items)
this.perks = this.setupCache(perks)
this.perkstyles = this.setupCache(perkstyles.styles)
this.summonerSpells = this.setupCache(summonerSpells)
this.championRoles = championRoles as ChampionsPlayRate
}
}
export default new CDragonService()

View file

@ -1,18 +1,18 @@
import Env from '@ioc:Adonis/Core/Env'
export interface JaxConfig {
key: string,
region: string,
key: string
region: string
requestOptions: JaxConfigRequestOptions
}
export interface JaxConfigRequestOptions {
retriesBeforeAbort: number,
delayBeforeRetry: number,
retriesBeforeAbort: number
delayBeforeRetry: number
}
export const JAX_CONFIG: JaxConfig = {
key: Env.get('API_KEY') as string,
key: Env.get('RIOT_API_KEY') as string,
region: 'euw1',
requestOptions: {
retriesBeforeAbort: 3,

View file

@ -11,7 +11,7 @@ export default class CDragonRequest {
private retries: number
private sleep: { (ms: number): Promise<void>; <T>(ms: number, value: T): Promise<T> }
constructor (config: JaxConfig, endpoint: string, cacheTime: number) {
constructor(config: JaxConfig, endpoint: string, cacheTime: number) {
this.config = config
this.endpoint = endpoint
this.cacheTime = cacheTime
@ -26,7 +26,7 @@ export default class CDragonRequest {
// https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json
// https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/summoner-spells.json
public async execute () {
public async execute() {
const url = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/${this.endpoint}`
const requestCached = await Redis.get(url)

View file

@ -2,103 +2,103 @@ import { JaxConfig } from '../../JaxConfig'
import CDragonRequest from '../CDragonRequest'
export interface ChampionDTO {
id: number,
name: string,
alias: string,
squarePortraitPath: string,
id: number
name: string
alias: string
squarePortraitPath: string
roles: string[]
}
export interface ItemDTO {
id: number,
name: string,
description: string,
active: boolean,
inStore: boolean,
from: number[],
to: number[],
categories: string[],
mapStringIdInclusions: string[],
maxStacks: number,
modeNameInclusions: string[],
requiredChampion: string,
requiredAlly: string,
requiredBuffCurrencyName: string,
requiredBuffCurrencyCost: number,
specialRecipe: number,
isEnchantment: boolean,
price: number,
priceTotal: number,
id: number
name: string
description: string
active: boolean
inStore: boolean
from: number[]
to: number[]
categories: string[]
mapStringIdInclusions: string[]
maxStacks: number
modeNameInclusions: string[]
requiredChampion: string
requiredAlly: string
requiredBuffCurrencyName: string
requiredBuffCurrencyCost: number
specialRecipe: number
isEnchantment: boolean
price: number
priceTotal: number
iconPath: string
}
export interface PerkDTO {
id: number,
name: string,
majorChangePatchVersion: string,
tooltip: string,
shortDesc: string,
longDesc: string,
iconPath: string,
id: number
name: string
majorChangePatchVersion: string
tooltip: string
shortDesc: string
longDesc: string
iconPath: string
endOfGameStatDescs: string[]
}
export interface PerkStyleResponse {
schemaVersion: string,
schemaVersion: string
styles: PerkStyleDTO[]
}
export interface PerkStyleDTO {
id: number,
name: string,
tooltip: string,
iconPath: string,
assetMap: { [key: string]: string },
isAdvanced: boolean,
allowedSubStyles: number[],
subStyleBonus: { styleId: number, perkId: number }[],
slots: { type: string, slotLabel: string, perks: number[] }[],
defaultPageName: string,
defaultSubStyle: number,
defaultPerks: number[],
defaultPerksWhenSplashed: number[],
defaultStatModsPerSubStyle: { id: string, perks: number[] }[]
id: number
name: string
tooltip: string
iconPath: string
assetMap: { [key: string]: string }
isAdvanced: boolean
allowedSubStyles: number[]
subStyleBonus: { styleId: number; perkId: number }[]
slots: { type: string; slotLabel: string; perks: number[] }[]
defaultPageName: string
defaultSubStyle: number
defaultPerks: number[]
defaultPerksWhenSplashed: number[]
defaultStatModsPerSubStyle: { id: string; perks: number[] }[]
}
export interface SummonerSpellDTO {
id: number,
name: string,
description: string,
summonerLevel: number,
cooldown: number,
gameModes: string[],
id: number
name: string
description: string
summonerLevel: number
cooldown: number
gameModes: string[]
iconPath: string
}
export default class CDragonEndpoint {
private config: JaxConfig
constructor (config: JaxConfig) {
constructor(config: JaxConfig) {
this.config = config
}
public async champions (): Promise<ChampionDTO[]> {
public async champions(): Promise<ChampionDTO[]> {
return new CDragonRequest(this.config, 'champion-summary.json', 36000).execute()
}
public async items (): Promise<ItemDTO[]> {
public async items(): Promise<ItemDTO[]> {
return new CDragonRequest(this.config, 'items.json', 36000).execute()
}
public async perks (): Promise<PerkDTO[]> {
public async perks(): Promise<PerkDTO[]> {
return new CDragonRequest(this.config, 'perks.json', 36000).execute()
}
public async perkstyles (): Promise<PerkStyleResponse> {
public async perkstyles(): Promise<PerkStyleResponse> {
return new CDragonRequest(this.config, 'perkstyles.json', 36000).execute()
}
public async summonerSpells (): Promise<SummonerSpellDTO[]> {
public async summonerSpells(): Promise<SummonerSpellDTO[]> {
return new CDragonRequest(this.config, 'summoner-spells.json', 36000).execute()
}
}

View file

@ -4,26 +4,26 @@ import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest'
export interface LeagueEntryDTO {
leagueId: string;
queueType: string;
tier: string;
rank: string;
summonerId: string;
summonerName: string;
leaguePoints: number;
wins: number;
losses: number;
veteran: boolean;
inactive: boolean;
freshBlood: boolean;
hotStreak: boolean;
leagueId: string
queueType: string
tier: string
rank: string
summonerId: string
summonerName: string
leaguePoints: number
wins: number
losses: number
veteran: boolean
inactive: boolean
freshBlood: boolean
hotStreak: boolean
miniSeries?: MiniSeriesDTO
}
interface MiniSeriesDTO {
losses: number,
progress: string,
target: number,
losses: number
progress: string
target: number
wins: number
}
@ -31,12 +31,12 @@ export default class LeagueEndpoint {
private config: JaxConfig
private limiter: RiotRateLimiter
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config
this.limiter = limiter
}
public summonerID (summonerID: string, region: string): Promise<LeagueEntryDTO[]> {
public summonerID(summonerID: string, region: string): Promise<LeagueEntryDTO[]> {
return new JaxRequest(
region,
this.config,

View file

@ -1,231 +1,216 @@
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
import { getRiotRegion } from 'App/helpers'
import RiotRateLimiter from 'riot-ratelimiter'
import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest'
export interface MatchDto {
gameId: number,
participantIdentities: ParticipantIdentityDto[],
queueId: number,
gameType: string,
gameDuration: number,
teams: TeamStatsDto[],
metadata: MetadataDto
info: InfoDto
}
export interface MetadataDto {
dataVersion: string
matchId: string
participants: string[]
}
export interface InfoDto {
gameCreation: number
gameDuration: number
gameId: number
gameMode: string
gameName: string
gameStartTimestamp: number
gameType: string
gameVersion: string
mapId: number
participants: ParticipantDto[]
platformId: string
gameCreation: number,
seasonId: number,
gameVersion: string,
mapId: number,
gameMode: string,
participants: ParticipantDto[],
}
export interface ParticipantIdentityDto {
participantId: number,
player: PlayerDto
}
export interface PlayerDto {
profileIcon: number,
accountId: string,
matchHistoryUri: string,
currentAccountId: string,
currentPlatformId: string,
summonerName: string,
summonerId: string,
platformId: string,
}
export interface TeamStatsDto {
towerKills: number,
riftHeraldKills: number,
firstBlood: boolean,
inhibitorKills: number,
bans: TeamBansDto[],
firstBaron: boolean,
firstDragon: boolean,
dominionVictoryScore: number,
dragonKills: number,
baronKills: number,
firstInhibitor: boolean
firstTower: boolean
vilemawKills: number,
firstRiftHerald: boolean
teamId: number, // 100 for blue side. 200 for red side.
win: string
}
export interface TeamBansDto {
championId: number,
pickTurn: number,
queueId: number
teams: TeamDto[]
tournamentCode?: string
}
export interface ParticipantDto {
participantId: number,
championId: number,
runes: RuneDto[],
stats: ParticipantStatsDto,
teamId: number,
timeline: ParticipantTimelineDto,
spell1Id: number,
spell2Id: number,
highestAchievedSeasonTier?:
'CHALLENGER' | 'MASTER' | 'DIAMOND' | 'PLATINUM' | 'GOLD' | 'SILVER' | 'BRONZE' | 'UNRANKED',
masteries: MasteryDto[]
}
export interface RuneDto {
runeId: number,
rank: number,
}
export interface ParticipantStatsDto {
item0: number,
item2: number,
totalUnitsHealed: number,
item1: number,
largestMultiKill: number,
goldEarned: number,
firstInhibitorKill: boolean,
physicalDamageTaken: number,
nodeNeutralizeAssist: number,
totalPlayerScore: number,
champLevel: number,
damageDealtToObjectives: number,
totalDamageTaken: number,
neutralMinionsKilled: number,
deaths: number,
tripleKills: number,
magicDamageDealtToChampions: number,
wardsKilled: number,
pentaKills: number,
damageSelfMitigated: number,
largestCriticalStrike: number,
nodeNeutralize: number,
totalTimeCrowdControlDealt: number,
firstTowerKill: boolean
magicDamageDealt: number,
totalScoreRank: number,
nodeCapture: number,
wardsPlaced: number,
totalDamageDealt: number,
timeCCingOthers: number,
magicalDamageTaken: number,
largestKillingSpree: number,
totalDamageDealtToChampions: number,
physicalDamageDealtToChampions: number,
neutralMinionsKilledTeamJungle: number,
totalMinionsKilled: number,
firstInhibitorAssist: boolean
visionWardsBoughtInGame: number,
objectivePlayerScore: number,
kills: number,
firstTowerAssist: boolean
combatPlayerScore: number,
inhibitorKills: number,
turretKills: number,
participantId: number,
trueDamageTaken: number,
assists: number
baronKills: number
bountyLevel: number
champExperience: number
champLevel: number
championId: number
championName: string
championTransform: ChampionTransformDto
consumablesPurchased: number
damageDealtToObjectives: number
damageDealtToTurrets: number
damageSelfMitigated: number
deaths: number
detectorWardsPlaced: number
doubleKills: number
dragonKills: number
firstBloodAssist: boolean
nodeCaptureAssist: number,
assists: number,
teamObjective: number,
altarsNeutralized: number,
goldSpent: number,
damageDealtToTurrets: number,
altarsCaptured: number,
win: boolean,
totalHeal: number,
unrealKills: number,
visionScore: number,
physicalDamageDealt: number,
firstBloodKill: boolean,
longestTimeSpentLiving: number,
killingSprees: number,
sightWardsBoughtInGame: number,
trueDamageDealtToChampions: number,
neutralMinionsKilledEnemyJungle: number,
doubleKills: number,
trueDamageDealt: number,
quadraKills: number,
item4: number,
item3: number,
item6: number,
item5: number,
playerScore0: number,
playerScore1: number,
playerScore2: number,
playerScore3: number,
playerScore4: number,
playerScore5: number,
playerScore6: number,
playerScore7: number,
playerScore8: number,
playerScore9: number,
perk0: number,
perk0Var1: number,
perk0Var2: number,
perk0Var3: number,
perk1: number,
perk1Var1: number,
perk1Var2: number,
perk1Var3: number,
perk2: number,
perk2Var1: number,
perk2Var2: number,
perk2Var3: number,
perk3: number,
perk3Var1: number,
perk3Var2: number,
perk3Var3: number,
perk4: number,
perk4Var1: number,
perk4Var2: number,
perk4Var3: number,
perk5: number,
perk5Var1: number,
perk5Var2: number,
perk5Var3: number,
perkPrimaryStyle: number,
perkSubStyle: number,
statPerk0: number,
statPerk1: number,
statPerk2: number,
firstBloodKill: boolean
firstTowerAssist: boolean
firstTowerKill: boolean
gameEndedInEarlySurrender: boolean
gameEndedInSurrender: boolean
goldEarned: number
goldSpent: number
individualPosition: 'Invalid' | TeamPositionDto // TODO
inhibitorKills: number
item0: number
item1: number
item2: number
item3: number
item4: number
item5: number
item6: number
itemsPurchased: number
killingSprees: number
kills: number
lane: LaneDto // TODO
largestCriticalStrike: number
largestKillingSpree: number
largestMultiKill: number
longestTimeSpentLiving: number
magicDamageDealt: number
magicDamageDealtToChampions: number
magicDamageTaken: number
neutralMinionsKilled: number
nexusKills: number
objectivesStolen: number
objectivesStolenAssists: number
participantId: number
pentaKills: number
perks: PerksDto
physicalDamageDealt: number
physicalDamageDealtToChampions: number
physicalDamageTaken: number
profileIcon: number
puuid: string
quadraKills: number
riotIdName: string
riotIdTagline: string
role: RoleDto // TODO
sightWardsBoughtInGame: number
spell1Casts: number
spell2Casts: number
spell3Casts: number
spell4Casts: number
summoner1Casts: number
summoner1Id: number
summoner2Casts: number
summoner2Id: number
summonerId: string
summonerLevel: number
summonerName: string
teamEarlySurrendered: boolean
teamId: number
teamPosition: TeamPositionDto // TODO
timeCCingOthers: number
timePlayed: number
totalDamageDealt: number
totalDamageDealtToChampions: number
totalDamageShieldedOnTeammates: number
totalDamageTaken: number
totalHeal: number
totalHealsOnTeammates: number
totalMinionsKilled: number
totalTimeCCDealt: number
totalTimeSpentDead: number
totalUnitsHealed: number
tripleKills: number
trueDamageDealt: number
trueDamageDealtToChampions: number
trueDamageTaken: number
turretKills: number
unrealKills: number
visionScore: number
visionWardsBoughtInGame: number
wardsKilled: number
wardsPlaced: number
win: boolean
}
export interface ParticipantTimelineDto {
participantId: number,
csDiffPerMinDeltas: { [index: string]: number },
damageTakenPerMinDeltas: { [index: string]: number },
role: 'DUO' | 'NONE' | 'SOLO' | 'DUO_CARRY' | 'DUO_SUPPORT',
damageTakenDiffPerMinDeltas: { [index: string]: number },
xpPerMinDeltas: { [index: string]: number },
xpDiffPerMinDeltas: { [index: string]: number },
lane: 'MID' | 'MIDDLE' | 'TOP' | 'JUNGLE' | 'BOT' | 'BOTTOM',
creepsPerMinDeltas: { [index: string]: number },
goldPerMinDeltas: { [index: string]: number },
export enum ChampionTransformDto {
None,
Slayer,
Assasin,
}
export interface MasteryDto {
rank: number,
masteryId: number,
export type LaneDto = 'TOP' | 'JUNGLE' | 'MIDDLE' | 'BOTTOM'
export interface PerksDto {
statPerks: PerkStatsDto
styles: PerkStyleDto[]
}
export interface PerkStatsDto {
defense: number
flex: number
offense: number
}
export interface PerkStyleDto {
description: 'primaryStyle' | 'subStyle'
selections: PerkStyleSelectionDto[]
style: number
}
export interface PerkStyleSelectionDto {
perk: number
var1: number
var2: number
var3: number
}
export type RoleDto = 'NONE' | 'DUO' | 'SOLO' | 'CARRY' | 'SUPPORT'
export type TeamPositionDto = 'TOP' | 'JUNGLE' | 'MIDDLE' | 'BOTTOM' | 'UTILITY'
export interface TeamDto {
bans: BanDto[]
objectives: ObjectivesDto
teamId: number
win: boolean
}
export interface BanDto {
championId: number
pickTurn: number
}
export interface ObjectivesDto {
baron: ObjectiveDto
champion: ObjectiveDto
dragon: ObjectiveDto
inhibitor: ObjectiveDto
riftHerald: ObjectiveDto
tower: ObjectiveDto
}
export interface ObjectiveDto {
first: boolean
kills: number
}
export default class MatchEndpoint {
private config: JaxConfig
private limiter: RiotRateLimiter
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config
this.limiter = limiter
this.get = this.get.bind(this)
}
public get (matchID: number, region: string): Promise<MatchDto> {
public get(matchID: string, region: string): Promise<MatchDto> {
return new JaxRequest(
region,
getRiotRegion(region),
this.config,
`match/v4/matches/${matchID}`,
`match/v5/matches/${matchID}`,
this.limiter,
1500
).execute()

View file

@ -0,0 +1,240 @@
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
import RiotRateLimiter from 'riot-ratelimiter'
import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest'
export interface V4MatchDto {
gameId: number
participantIdentities: V4ParticipantIdentityDto[]
queueId: number
gameType: string
gameDuration: number
teams: V4TeamStatsDto[]
platformId: string
gameCreation: number
seasonId: number
gameVersion: string
mapId: number
gameMode: string
participants: V4ParticipantDto[]
}
export interface V4ParticipantIdentityDto {
participantId: number
player: V4PlayerDto
}
export interface V4PlayerDto {
profileIcon: number
accountId: string
matchHistoryUri: string
currentAccountId: string
currentPlatformId: string
summonerName: string
summonerId: string
platformId: string
}
export interface V4TeamStatsDto {
towerKills: number
riftHeraldKills: number
firstBlood: boolean
inhibitorKills: number
bans: V4TeamBansDto[]
firstBaron: boolean
firstDragon: boolean
dominionVictoryScore: number
dragonKills: number
baronKills: number
firstInhibitor: boolean
firstTower: boolean
vilemawKills: number
firstRiftHerald: boolean
teamId: number // 100 for blue side. 200 for red side.
win: string
}
export interface V4TeamBansDto {
championId: number
pickTurn: number
}
export interface V4ParticipantDto {
participantId: number
championId: number
runes: V4RuneDto[]
stats: V4ParticipantStatsDto
teamId: number
timeline: V4ParticipantTimelineDto
spell1Id: number
spell2Id: number
highestAchievedSeasonTier?:
| 'CHALLENGER'
| 'MASTER'
| 'DIAMOND'
| 'PLATINUM'
| 'GOLD'
| 'SILVER'
| 'BRONZE'
| 'UNRANKED'
masteries: V4MasteryDto[]
}
export interface V4RuneDto {
runeId: number
rank: number
}
export interface V4ParticipantStatsDto {
item0: number
item2: number
totalUnitsHealed: number
item1: number
largestMultiKill: number
goldEarned: number
firstInhibitorKill: boolean
physicalDamageTaken: number
nodeNeutralizeAssist: number
totalPlayerScore: number
champLevel: number
damageDealtToObjectives: number
totalDamageTaken: number
neutralMinionsKilled: number
deaths: number
tripleKills: number
magicDamageDealtToChampions: number
wardsKilled: number
pentaKills: number
damageSelfMitigated: number
largestCriticalStrike: number
nodeNeutralize: number
totalTimeCrowdControlDealt: number
firstTowerKill: boolean
magicDamageDealt: number
totalScoreRank: number
nodeCapture: number
wardsPlaced: number
totalDamageDealt: number
timeCCingOthers: number
magicalDamageTaken: number
largestKillingSpree: number
totalDamageDealtToChampions: number
physicalDamageDealtToChampions: number
neutralMinionsKilledTeamJungle: number
totalMinionsKilled: number
firstInhibitorAssist: boolean
visionWardsBoughtInGame: number
objectivePlayerScore: number
kills: number
firstTowerAssist: boolean
combatPlayerScore: number
inhibitorKills: number
turretKills: number
participantId: number
trueDamageTaken: number
firstBloodAssist: boolean
nodeCaptureAssist: number
assists: number
teamObjective: number
altarsNeutralized: number
goldSpent: number
damageDealtToTurrets: number
altarsCaptured: number
win: boolean
totalHeal: number
unrealKills: number
visionScore: number
physicalDamageDealt: number
firstBloodKill: boolean
longestTimeSpentLiving: number
killingSprees: number
sightWardsBoughtInGame: number
trueDamageDealtToChampions: number
neutralMinionsKilledEnemyJungle: number
doubleKills: number
trueDamageDealt: number
quadraKills: number
item4: number
item3: number
item6: number
item5: number
playerScore0: number
playerScore1: number
playerScore2: number
playerScore3: number
playerScore4: number
playerScore5: number
playerScore6: number
playerScore7: number
playerScore8: number
playerScore9: number
perk0: number
perk0Var1: number
perk0Var2: number
perk0Var3: number
perk1: number
perk1Var1: number
perk1Var2: number
perk1Var3: number
perk2: number
perk2Var1: number
perk2Var2: number
perk2Var3: number
perk3: number
perk3Var1: number
perk3Var2: number
perk3Var3: number
perk4: number
perk4Var1: number
perk4Var2: number
perk4Var3: number
perk5: number
perk5Var1: number
perk5Var2: number
perk5Var3: number
perkPrimaryStyle: number
perkSubStyle: number
statPerk0: number
statPerk1: number
statPerk2: number
}
export interface V4ParticipantTimelineDto {
participantId: number
csDiffPerMinDeltas: { [index: string]: number }
damageTakenPerMinDeltas: { [index: string]: number }
role: 'DUO' | 'NONE' | 'SOLO' | 'DUO_CARRY' | 'DUO_SUPPORT'
damageTakenDiffPerMinDeltas: { [index: string]: number }
xpPerMinDeltas: { [index: string]: number }
xpDiffPerMinDeltas: { [index: string]: number }
lane: 'MID' | 'MIDDLE' | 'TOP' | 'JUNGLE' | 'BOT' | 'BOTTOM'
creepsPerMinDeltas: { [index: string]: number }
goldPerMinDeltas: { [index: string]: number }
}
export interface V4MasteryDto {
rank: number
masteryId: number
}
export default class MatchV4Endpoint {
private config: JaxConfig
private limiter: RiotRateLimiter
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config
this.limiter = limiter
this.get = this.get.bind(this)
}
public get(matchID: number, region: string): Promise<V4MatchDto> {
return new JaxRequest(
region,
this.config,
`match/v4/matches/${matchID}`,
this.limiter,
1500
).execute()
}
}

View file

@ -1,41 +1,52 @@
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
import { getRiotRegion } from 'App/helpers'
import RiotRateLimiter from 'riot-ratelimiter'
import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest'
export interface MatchlistDto {
startIndex: number,
totalGames: number,
endIndex: number,
matches: MatchReferenceDto[]
}
// export interface MatchlistDto {
// startIndex: number,
// totalGames: number,
// endIndex: number,
// matches: MatchReferenceDto[]
// }
export interface MatchReferenceDto {
gameId: number,
role: string,
season: number,
platformId: string,
champion: number,
queue: number,
lane: string,
timestamp: number,
seasonMatch?: number
}
// export interface MatchReferenceDto {
// gameId: number,
// role: string,
// season: number,
// platformId: string,
// champion: number,
// queue: number,
// lane: string,
// timestamp: number,
// seasonMatch?: number
// }
/**
*
* ===============================================
* V5
* ===============================================
*
*/
export type MatchlistDto = string[]
export default class MatchlistEndpoint {
private config: JaxConfig
private limiter: RiotRateLimiter
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config
this.limiter = limiter
}
public accountID (accountID: string, region: string, beginIndex = 0): Promise<MatchlistDto> {
public puuid(puuid: string, region: string, beginIndex = 0, count = 100): Promise<MatchlistDto> {
return new JaxRequest(
region,
getRiotRegion(region),
this.config,
`match/v4/matchlists/by-account/${accountID}?beginIndex=${beginIndex}`,
`match/v5/matches/by-puuid/${puuid}/ids?start=${beginIndex}&count=${count}`,
this.limiter,
0
).execute()

View file

@ -0,0 +1,43 @@
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
import RiotRateLimiter from 'riot-ratelimiter'
import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest'
export interface V4MatchlistDto {
startIndex: number
totalGames: number
endIndex: number
matches: V4MatchReferenceDto[]
}
export interface V4MatchReferenceDto {
gameId: number
role: string
season: number
platformId: string
champion: number
queue: number
lane: string
timestamp: number
seasonMatch?: number
}
export default class MatchlistV4Endpoint {
private config: JaxConfig
private limiter: RiotRateLimiter
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config
this.limiter = limiter
}
public accountID(accountID: string, region: string, beginIndex = 0): Promise<V4MatchlistDto> {
return new JaxRequest(
region,
this.config,
`match/v4/matchlists/by-account/${accountID}?beginIndex=${beginIndex}`,
this.limiter,
0
).execute()
}
}

View file

@ -1,59 +1,53 @@
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
import RiotRateLimiter from 'riot-ratelimiter'
import { LeagueEntriesByQueue } from 'App/Services/SummonerService'
import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest'
export interface CurrentGameInfo {
gameId: number,
export interface CurrentGameInfoDTO {
gameId: number
gameType: string
gameStartTime: number,
mapId: number,
gameLength: number,
platformId: string,
gameMode: string,
bannedChampions: BannedChampion[],
gameQueueConfigId: number,
observers: Observer,
participants: CurrentGameParticipant[],
gameStartTime: number
mapId: number
gameLength: number
platformId: string
gameMode: string
bannedChampions: BannedChampionDTO[]
gameQueueConfigId: number
observers: ObserverDTO
participants: CurrentGameParticipantDTO[]
}
export interface BannedChampion {
pickTurn: number,
championId: number,
teamId: number,
export interface BannedChampionDTO {
pickTurn: number
championId: number
teamId: number
}
export interface Observer {
encryptionKey: string,
export interface ObserverDTO {
encryptionKey: string
}
export interface CurrentGameParticipant {
championId: number,
perks: Perks,
profileIconId: number,
bot: boolean,
teamId: number,
summonerName: string,
summonerId: string,
spell1Id: number,
spell2Id: number,
gameCustomizationObjects: GameCustomizationObject[],
// Custom types from here
role?: string,
runes?: { primaryRune: string, secondaryRune: string } | {}
level?: number,
rank?: LeagueEntriesByQueue
export interface CurrentGameParticipantDTO {
championId: number
perks: PerksDTO
profileIconId: number
bot: boolean
teamId: number
summonerName: string
summonerId: string
spell1Id: number
spell2Id: number
gameCustomizationObjects: GameCustomizationObjectDTO[]
}
export interface Perks {
export interface PerksDTO {
perkIds: number[]
perkStyle: number,
perkStyle: number
perkSubStyle: number
}
export interface GameCustomizationObject {
category: string,
export interface GameCustomizationObjectDTO {
category: string
content: string
}
@ -61,12 +55,12 @@ export default class SpectatorEndpoint {
private config: JaxConfig
private limiter: RiotRateLimiter
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config
this.limiter = limiter
}
public summonerID (summonerID: string, region: string) {
public summonerID(summonerID: string, region: string): Promise<CurrentGameInfoDTO | undefined> {
return new JaxRequest(
region,
this.config,

View file

@ -4,26 +4,35 @@ import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest'
export interface SummonerDTO {
accountId: string,
profileIconId: number,
revisionDate: number,
name: string,
id: string,
puuid: string,
summonerLevel: number,
region?: string
accountId: string
profileIconId: number
revisionDate: number
name: string
id: string
puuid: string
summonerLevel: number
}
export default class SummonerEndpoint {
private config: JaxConfig
private limiter: RiotRateLimiter
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config
this.limiter = limiter
}
public summonerId (summonerId: string, region: string): Promise<SummonerDTO> {
public accountId(accountId: string, region: string): Promise<SummonerDTO> {
return new JaxRequest(
region,
this.config,
`summoner/v4/summoners/by-account/${accountId}`,
this.limiter,
36000
).execute()
}
public summonerId(summonerId: string, region: string): Promise<SummonerDTO> {
return new JaxRequest(
region,
this.config,
@ -33,7 +42,7 @@ export default class SummonerEndpoint {
).execute()
}
public summonerName (summonerName: string, region: string): Promise<SummonerDTO> {
public summonerName(summonerName: string, region: string): Promise<SummonerDTO> {
return new JaxRequest(
region,
this.config,

View file

@ -8,6 +8,8 @@ import { JaxConfig } from '../JaxConfig'
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
import RiotRateLimiter from 'riot-ratelimiter'
import { STRATEGY } from 'riot-ratelimiter/dist/RateLimiter'
import MatchV4Endpoint from './Endpoints/MatchV4Endpoint'
import MatchlistV4Endpoint from './Endpoints/MatchlistV4Endpoint'
export default class Jax {
public key: string
@ -15,12 +17,14 @@ export default class Jax {
public config: JaxConfig
public League: LeagueEndpoint
public Match: MatchEndpoint
public MatchV4: MatchV4Endpoint
public Matchlist: MatchlistEndpoint
public MatchlistV4: MatchlistV4Endpoint
public Spectator: SpectatorEndpoint
public Summoner: SummonerEndpoint
public CDragon: CDragonEndpoint
constructor (config:JaxConfig) {
constructor(config: JaxConfig) {
this.key = config.key
// this.limiter = new RiotRateLimiter({
// debug: true,
@ -34,7 +38,9 @@ export default class Jax {
this.League = new LeagueEndpoint(this.config, this.limiter)
this.Match = new MatchEndpoint(this.config, this.limiter)
this.MatchV4 = new MatchV4Endpoint(this.config, this.limiter)
this.Matchlist = new MatchlistEndpoint(this.config, this.limiter)
this.MatchlistV4 = new MatchlistV4Endpoint(this.config, this.limiter)
this.Spectator = new SpectatorEndpoint(this.config, this.limiter)
this.Summoner = new SummonerEndpoint(this.config, this.limiter)
this.CDragon = new CDragonEndpoint(this.config)

View file

@ -14,7 +14,13 @@ export default class JaxRequest {
private retries: number
private sleep: { (ms: number): Promise<void>; <T>(ms: number, value: T): Promise<T> }
constructor (region: string, config: JaxConfig, endpoint: string, limiter: RiotRateLimiter, cacheTime: number) {
constructor(
region: string,
config: JaxConfig,
endpoint: string,
limiter: RiotRateLimiter,
cacheTime: number
) {
this.region = region
this.config = config
this.endpoint = endpoint
@ -25,7 +31,7 @@ export default class JaxRequest {
this.sleep = promisify(setTimeout)
}
public async execute () {
public async execute() {
const url = `https://${this.region}.api.riotgames.com/lol/${this.endpoint}`
// Redis cache
@ -37,16 +43,7 @@ export default class JaxRequest {
}
try {
// const resp = await this.limiter.execute({
// url,
// options: {
// headers: {
// 'X-Riot-Token': this.config.key,
// },
// },
// })
const resp:any = await this.limiter.executing({
const resp: any = await this.limiter.executing({
url,
token: this.config.key,
resolveWithFullResponse: false,
@ -56,17 +53,28 @@ export default class JaxRequest {
await Redis.setex(url, this.cacheTime, resp)
}
return JSON.parse(resp)
} catch ({ statusCode , ...rest }) {
} catch ({ statusCode, ...rest }) {
this.retries--
if (statusCode !== 500 && statusCode !== 503 && statusCode !== 504) { //
console.log('JAX ERROR')
console.log(rest?.cause?.code)
if (
statusCode !== 500 &&
statusCode !== 503 &&
statusCode !== 504 &&
rest?.cause?.code !== 'ETIMEDOUT'
) {
//
// Don't log 404 when summoner isn't playing or the summoner doesn't exist
// Or if summoner has no MatchList
if (!this.endpoint.includes('spectator/v4/active-games/by-summoner') &&
if (
!this.endpoint.includes('spectator/v4/active-games/by-summoner') &&
!this.endpoint.includes('summoner/v4/summoners/by-name') &&
!this.endpoint.includes('match/v4/matchlists/by-account')
) {
Logger.error(`JaxRequest Error ${statusCode}: `, rest)
Logger.error(`URL ${url}: `)
// Logger.error(`JaxRequest Error ${statusCode}: `, rest)
}
return

View file

@ -1,142 +1,138 @@
import Jax from './Jax'
import Logger from '@ioc:Adonis/Core/Logger'
import { getSeasonNumber } from 'App/helpers'
import { MatchReferenceDto } from './Jax/src/Endpoints/MatchlistEndpoint'
import { MatchlistDto } from './Jax/src/Endpoints/MatchlistEndpoint'
import { SummonerDTO } from './Jax/src/Endpoints/SummonerEndpoint'
import { SummonerModel } from 'App/Models/Summoner'
import Match, { MatchModel } from 'App/Models/Match'
import BasicMatchTransformer from 'App/Transformers/BasicMatchTransformer'
import Summoner from 'App/Models/Summoner'
import Database from '@ioc:Adonis/Lucid/Database'
import MatchParser from 'App/Parsers/MatchParser'
import BasicMatchSerializer from 'App/Serializers/BasicMatchSerializer'
import { SerializedMatch } from 'App/Serializers/SerializedTypes'
import Match from 'App/Models/Match'
import { notEmpty, tutorialQueues } from 'App/helpers'
class MatchService {
/**
* Add 100 matches at a time to MatchList until the stopFetching condition is true
* @param account of the summoner
* @param region of the summoner
* @param stopFetching condition to stop fetching the MatchList
*/
private async _fetchMatchListUntil (account: SummonerDTO, stopFetching: any) {
let matchList: MatchReferenceDto[] = []
private async _fetchMatchListUntil(account: SummonerDTO, region: string, stopFetching: any) {
let matchList: MatchlistDto = []
let alreadyIn = false
let index = 0
do {
let newMatchList = await Jax.Matchlist.accountID(account.accountId, account.region as string, index)
const newMatchList = await Jax.Matchlist.puuid(account.puuid, region, index)
// Error while fetching Riot API
if (!newMatchList) {
matchList = matchList.map(m => {
m.seasonMatch = getSeasonNumber(m.timestamp)
return m
})
return matchList
}
matchList = [...matchList, ...newMatchList.matches]
alreadyIn = newMatchList.matches.length === 0 || stopFetching(newMatchList.matches)
matchList = [...matchList, ...newMatchList]
alreadyIn = newMatchList.length === 0 || stopFetching(newMatchList)
// If the match is made in another region : we stop fetching
if (matchList[matchList.length - 1].platformId.toLowerCase() !== account.region) {
if (matchList[matchList.length - 1].split('_')[0].toLowerCase() !== region.toLowerCase()) {
alreadyIn = true
}
index += 100
} while (!alreadyIn)
// Remove matches from MatchList made in another region and tutorial games
const tutorialModes = [2000, 2010, 2020]
matchList = matchList
.filter(m => {
const sameRegion = m.platformId.toLowerCase() === account.region
const notATutorialGame = !tutorialModes.includes(m.queue)
return sameRegion && notATutorialGame
})
.map(m => {
m.seasonMatch = getSeasonNumber(m.timestamp)
return m
})
return matchList
}
/**
* Update the full MatchList of the summoner (min. 4 months)
* @param account of the summoner
* @param summonerDB summoner in the database
* Update the full MatchList of the summoner
*/
public async updateMatchList (account: SummonerDTO, summonerDB: SummonerModel) {
public async updateMatchList(
account: SummonerDTO,
region: string,
summonerDB: Summoner
): Promise<MatchlistDto> {
console.time('matchList')
// Summoner has already been searched : we already have a MatchList and we need to update it
if (summonerDB.matchList) {
// Get MatchList
const matchList = await this._fetchMatchListUntil(account, (newMatchList: MatchReferenceDto[]) => {
return summonerDB.matchList!.some(m => m.gameId === newMatchList[newMatchList.length - 1].gameId)
})
// Update Summoner's MatchList
for (const match of matchList.reverse()) {
if (!summonerDB.matchList.some(m => m.gameId === match.gameId)) {
summonerDB.matchList.unshift(match)
const currentMatchList = await summonerDB.related('matchList').query().orderBy('matchId', 'asc')
const currentMatchListIds = currentMatchList.map((m) => m.matchId)
const newMatchList = await this._fetchMatchListUntil(
account,
region,
(newMatchList: MatchlistDto) => {
return currentMatchListIds.some((id) => id === newMatchList[newMatchList.length - 1])
}
)
const matchListToSave: MatchlistDto = []
for (const matchId of newMatchList.reverse()) {
if (!currentMatchListIds.some((id) => id === matchId)) {
matchListToSave.push(matchId)
currentMatchListIds.push(matchId)
}
}
} else { // First search of the Summoner
const today = Date.now()
// Get MatchList
const matchList = await this._fetchMatchListUntil(account, (newMatchList: MatchReferenceDto[]) => {
return (newMatchList.length !== 100 || today - newMatchList[newMatchList.length - 1].timestamp > 10368000000)
})
// Create Summoner's MatchList in Database
summonerDB.matchList = matchList
// If there is new matchIds to save in database
if (matchListToSave.length) {
await Database.table('summoner_matchlist').multiInsert(
matchListToSave.map((id) => ({
match_id: id,
summoner_puuid: summonerDB.puuid,
}))
)
}
console.timeEnd('matchList')
return currentMatchListIds.reverse()
}
/**
* Fetch list of matches for a specific Summoner
* @param puuid
* @param accountId
* @param region
* @param gameIds
* @param summonerDB
*/
public async getMatches (puuid: string, accountId: string, region: string, gameIds: number[], summonerDB: SummonerModel) {
public async getMatches(
region: string,
matchIds: string[],
puuid: string
): Promise<SerializedMatch[]> {
console.time('getMatches')
let matchesDetails: MatchModel[] = []
const matchesToGetFromRiot: number[] = []
for (let i = 0; i < gameIds.length; ++i) {
const matchSaved = await Match.findOne({
summoner_puuid: puuid,
gameId: gameIds[i],
})
const matches: SerializedMatch[] = []
const matchesToGetFromRiot: MatchlistDto = []
for (let i = 0; i < matchIds.length; ++i) {
const matchSaved = await Match.query()
.where('id', matchIds[i])
.preload('teams')
.preload('players')
.first()
if (matchSaved) {
matchesDetails.push(matchSaved)
// TODO: Serialize match from DB + put it in Redis + push it in "matches"
matches.push(BasicMatchSerializer.serializeOneMatch(matchSaved, puuid))
} else {
matchesToGetFromRiot.push(gameIds[i])
matchesToGetFromRiot.push(matchIds[i])
}
}
const requests = matchesToGetFromRiot.map(gameId => Jax.Match.get(gameId, region))
let matchesFromApi = await Promise.all(requests)
const requests = matchesToGetFromRiot.map((gameId) => Jax.Match.get(gameId, region))
const matchesFromApi = await Promise.all(requests)
/* If we have to store some matches in the db */
if (matchesFromApi.length !== 0) {
// Try to see why matches are sometimes undefined
matchesFromApi.filter(m => {
if (m === undefined) {
Logger.info(`Match undefined, summoner: ${summonerDB.puuid}`, m)
}
})
// Remove bugged matches from the Riot API + tutorial games
const filteredMatches = matchesFromApi
.filter(notEmpty)
.filter(
(m) =>
!tutorialQueues.includes(m.info.queueId) &&
m.info.teams.length > 0 &&
m.info.participants.length > 0
)
// Transform raw matches data
const transformedMatches = await BasicMatchTransformer.transform(matchesFromApi, { puuid, accountId })
const parsedMatches: any = await MatchParser.parse(filteredMatches)
/* Save all matches from Riot Api in db */
for (const match of transformedMatches) {
await Match.create(match)
match.newMatch = true
}
matchesDetails = [...matchesDetails, ...transformedMatches]
// TODO: Serialize match from DB + put it in Redis + push it in "matches"
const serializedMatches = BasicMatchSerializer.serialize(parsedMatches, puuid, true)
matches.push(...serializedMatches)
}
/* Sort matches */
matchesDetails.sort((a, b) => (a.date < b.date) ? 1 : -1)
// Todo: check if we need to sort here
matches.sort((a, b) => (a.date < b.date ? 1 : -1))
console.timeEnd('getMatches')
return matchesDetails
return matches
}
}

View file

@ -0,0 +1,72 @@
import Jax from './Jax'
import { getSeasonNumber, notEmpty } from 'App/helpers'
import { V4MatchReferenceDto } from './Jax/src/Endpoints/MatchlistV4Endpoint'
import { SummonerDTO } from './Jax/src/Endpoints/SummonerEndpoint'
import MatchV4Parser from 'App/Parsers/MatchV4Parser'
import Match from 'App/Models/Match'
class MatchService {
private async _fetchMatchListUntil(account: SummonerDTO, region: string) {
let matchList: V4MatchReferenceDto[] = []
let alreadyIn = false
let index = 0
do {
let newMatchList = await Jax.MatchlistV4.accountID(account.accountId, region, index)
// Error while fetching Riot API
if (!newMatchList) {
matchList = matchList.map((m) => {
m.seasonMatch = getSeasonNumber(m.timestamp)
return m
})
return matchList
}
matchList = [...matchList, ...newMatchList.matches]
alreadyIn = newMatchList.matches.length === 0
// If the match is made in another region : we stop fetching
if (matchList[matchList.length - 1].platformId.toLowerCase() !== region) {
alreadyIn = true
}
index += 100
} while (!alreadyIn)
// Remove matches from MatchList made in another region, tutorial games, 3v3 games, Coop vs IA games
const tutorialModes = [2000, 2010, 2020, 460, 470, 800, 810, 820, 830, 840, 850]
matchList = matchList
.filter((m) => {
const sameRegion = m.platformId.toLowerCase() === region
const notATutorialGame = !tutorialModes.includes(m.queue)
return sameRegion && notATutorialGame
})
.map((m) => {
m.seasonMatch = getSeasonNumber(m.timestamp)
return m
})
return matchList
}
public async updateMatchList(account: SummonerDTO, region: string) {
return this._fetchMatchListUntil(account, region)
}
public async getMatches(region: string, matchlist: V4MatchReferenceDto[]) {
const matchesToGetFromRiot: number[] = []
for (const match of matchlist) {
const matchSaved = await Match.query()
.where('id', MatchV4Parser.createMatchId(match.gameId, region))
.first()
if (!matchSaved) {
matchesToGetFromRiot.push(match.gameId)
}
}
const requests = matchesToGetFromRiot.map((gameId) => Jax.MatchV4.get(gameId, region))
const matchesFromApi = await Promise.all(requests)
const filteredMatches = matchesFromApi.filter(notEmpty)
return filteredMatches.length ? await MatchV4Parser.parse(filteredMatches) : 0
}
}
export default new MatchService()

View file

@ -3,28 +3,20 @@ import got from 'got/dist/source'
export interface ChampionsPlayRate {
[champion: string]: {
TOP: number,
JUNGLE: number,
MIDDLE: number,
BOTTOM: number,
UTILITY: number,
TOP: number
JUNGLE: number
MIDDLE: number
BOTTOM: number
UTILITY: number
}
}
export interface FinalRoleComposition {
TOP?: number,
JUNGLE?: number,
MIDDLE?: number,
BOTTOM?: number,
SUPPORT?: number,
}
export interface RoleComposition {
TOP?: number,
JUNGLE?: number,
MIDDLE?: number,
BOTTOM?: number,
UTILITY?: number,
TOP?: number
JUNGLE?: number
MIDDLE?: number
BOTTOM?: number
UTILITY?: number
}
export interface ChampionComposition {
@ -32,22 +24,22 @@ export interface ChampionComposition {
}
export interface ApiRoleResponse {
data: { [key: string]: ChampionInitialRates };
patch: string;
data: { [key: string]: ChampionInitialRates }
patch: string
}
export interface ChampionInitialRates {
MIDDLE?: RoleRate;
UTILITY?: RoleRate;
JUNGLE?: RoleRate;
TOP?: RoleRate;
BOTTOM?: RoleRate;
MIDDLE?: RoleRate
UTILITY?: RoleRate
JUNGLE?: RoleRate
TOP?: RoleRate
BOTTOM?: RoleRate
}
export interface RoleRate {
playRate: number;
winRate: number;
banRate: number;
playRate: number
winRate: number
banRate: number
}
export interface ChampionsRates {
@ -55,7 +47,7 @@ export interface ChampionsRates {
}
class RoleIdentificationService {
private _getPermutations (array: number[]) {
private _getPermutations(array: number[]) {
const result: number[][] = []
for (let i = 0; i < array.length; i++) {
@ -72,13 +64,15 @@ class RoleIdentificationService {
return result
}
private _calculateMetric (championPositions: ChampionsRates, bestPositions: RoleComposition) {
return Object.entries(bestPositions).reduce((agg, [position, champion]) => {
private _calculateMetric(championPositions: ChampionsRates, bestPositions: RoleComposition) {
return (
Object.entries(bestPositions).reduce((agg, [position, champion]) => {
return agg + (championPositions[champion][position] || 0)
}, 0) / Object.keys(bestPositions).length
)
}
private _getPositions (
private _getPositions(
championPositions: ChampionsRates,
composition: number[],
top?: number,
@ -89,11 +83,11 @@ class RoleIdentificationService {
) {
// Set the initial guess to be the champion in the composition, order doesn't matter
let bestPositions: RoleComposition = {
'TOP': composition[0],
'JUNGLE': composition[1],
'MIDDLE': composition[2],
'BOTTOM': composition[3],
'UTILITY': composition[4],
TOP: composition[0],
JUNGLE: composition[1],
MIDDLE: composition[2],
BOTTOM: composition[3],
UTILITY: composition[4],
}
let bestMetric = this._calculateMetric(championPositions, bestPositions)
@ -102,19 +96,23 @@ class RoleIdentificationService {
// Figure out which champions and positions we need to fill
const knownChampions = [top, jungle, middle, adc, support].filter(Boolean)
const unknownChampions = composition.filter(champ => !knownChampions.includes(champ))
const unknownChampions = composition.filter((champ) => !knownChampions.includes(champ))
const unknownPositions = Object.entries({
'TOP': top, 'JUNGLE': jungle, 'MIDDLE': middle, 'BOTTOM': adc, 'UTILITY': support,
TOP: top,
JUNGLE: jungle,
MIDDLE: middle,
BOTTOM: adc,
UTILITY: support,
})
.filter(pos => !pos[1])
.map(pos => pos[0])
.filter((pos) => !pos[1])
.map((pos) => pos[0])
const testComposition: RoleComposition = {
'TOP': top,
'JUNGLE': jungle,
'MIDDLE': middle,
'BOTTOM': adc,
'UTILITY': support,
TOP: top,
JUNGLE: jungle,
MIDDLE: middle,
BOTTOM: adc,
UTILITY: support,
}
// Iterate over the positions we need to fill and record how well each composition "performs"
@ -163,7 +161,7 @@ class RoleIdentificationService {
/**
* Get the CDN data of the champion playrates by role
*/
public async pullData (): Promise<ChampionsPlayRate> {
public async pullData(): Promise<ChampionsPlayRate> {
const url = 'http://cdn.merakianalytics.com/riot/lol/resources/latest/en-US/championrates.json'
// Check if cached
@ -204,12 +202,12 @@ class RoleIdentificationService {
* @param jungle
* @param support
*/
public getRoles (
public getRoles(
championPositions: ChampionsRates,
composition: number[],
jungle?: number,
support?: number
): FinalRoleComposition {
): RoleComposition {
// Set composition champion playrate to 0% if not present in the json data
for (const compChamp of composition) {
if (championPositions[compChamp]) {
@ -239,9 +237,18 @@ class RoleIdentificationService {
}
while (Object.keys(identified).length < composition.length - 1) {
let { bestPositions, bestMetric: metric, secondBestPositions: sbp } =
this._getPositions(championPositions, composition,
identified.TOP, identified.JUNGLE, identified.MIDDLE, identified.BOTTOM, identified.UTILITY
let {
bestPositions,
bestMetric: metric,
secondBestPositions: sbp,
} = this._getPositions(
championPositions,
composition,
identified.TOP,
identified.JUNGLE,
identified.MIDDLE,
identified.BOTTOM,
identified.UTILITY
)
positions = bestPositions
@ -261,7 +268,11 @@ class RoleIdentificationService {
// Done! Grab the results.
const positionsWithMetric = {}
for (const [position, champion] of Object.entries(positions)) {
if (Object.keys(identified).includes(position) || champion === jungle || champion === support) {
if (
Object.keys(identified).includes(position) ||
champion === jungle ||
champion === support
) {
continue
}
positionsWithMetric[position] = {
@ -285,13 +296,7 @@ class RoleIdentificationService {
identified[best[0]] = best[1]
}
// Rename UTILITY to SUPPORT
const {
UTILITY: SUPPORT,
...rest
} = positions
return { ...rest, SUPPORT }
return positions
}
}

View file

@ -1,20 +1,28 @@
import MatchRepository from 'App/Repositories/MatchRepository'
import { sortTeamByRole } from 'App/helpers'
import { ChampionRoles, TeamPosition } from 'App/Parsers/ParsedType'
import MatchRepository from 'App/Repositories/MatchRepository'
import BasicMatchSerializer from 'App/Serializers/BasicMatchSerializer'
class StatsService {
public async getSummonerStats (puuid: string, season?: number) {
public async getRecentActivity(puuid: string) {
return MatchRepository.recentActivity(puuid)
}
public async getSummonerStats(puuid: string, season?: number) {
console.time('GLOBAL')
const globalStats = await MatchRepository.globalStats(puuid, season)
const globalStats = await MatchRepository.globalStats(puuid)
console.timeEnd('GLOBAL')
console.time('GAMEMODE')
const gamemodeStats = await MatchRepository.gamemodeStats(puuid, season)
const gamemodeStats = await MatchRepository.gamemodeStats(puuid)
console.timeEnd('GAMEMODE')
console.time('ROLE')
const roleStats = await MatchRepository.roleStats(puuid, season)
const roleStats = await MatchRepository.roleStats(puuid)
// Check if all roles are in the array
const roles = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
const roles = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
for (const role of roles) {
if (!roleStats.find(r => r.role === role)) {
const findedRole = roleStats.find((r) => TeamPosition[r.role] === role)
if (findedRole) {
findedRole.role = TeamPosition[findedRole.role]
} else {
roleStats.push({
count: 0,
losses: 0,
@ -25,22 +33,33 @@ class StatsService {
}
console.timeEnd('ROLE')
console.time('CHAMPION')
const championStats = await MatchRepository.championStats(puuid, 5, season)
const championStats = await MatchRepository.championStats(puuid, 5)
for (const champ of championStats) {
champ.champion = BasicMatchSerializer.getChampion(champ.id)
}
console.timeEnd('CHAMPION')
console.time('CHAMPION-CLASS')
const championClassStats = await MatchRepository.championClassStats(puuid, season)
const championClassStats = await MatchRepository.championClassStats(puuid)
for (const champ of championClassStats) {
champ.id = ChampionRoles[champ.id]
}
console.timeEnd('CHAMPION-CLASS')
console.time('MATES')
const mates = await MatchRepository.mates(puuid, season)
const mates = await MatchRepository.mates(puuid)
console.timeEnd('MATES')
console.time('RECENT_ACTIVITY')
const recentActivity = await MatchRepository.recentActivity(puuid)
console.timeEnd('RECENT_ACTIVITY')
return {
global: globalStats[0],
global: globalStats,
league: gamemodeStats,
role: roleStats.sort(sortTeamByRole),
champion: championStats,
class: championClassStats,
mates,
champion: championStats,
recentActivity,
}
}
}

View file

@ -1,36 +1,48 @@
import Jax from './Jax'
import { SummonerDTO } from 'App/Services/Jax/src/Endpoints/SummonerEndpoint'
import { LeagueEntryDTO } from './Jax/src/Endpoints/LeagueEndpoint'
import { SummonerModel } from 'App/Models/Summoner'
import Summoner from 'App/Models/Summoner'
import { PlayerRankParsed } from 'App/Parsers/ParsedType'
import MatchPlayerRank from 'App/Models/MatchPlayerRank'
export interface LeagueEntriesByQueue {
soloQ?: LeagueEntryByQueue,
soloQ?: LeagueEntryByQueue
flex5v5?: LeagueEntryByQueue
}
export interface LeagueEntryByQueue extends LeagueEntryDTO {
fullRank: string,
winrate: string,
fullRank: string
winrate: string
shortName: string | number
}
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
* @param league raw data of the league from Riot API
*/
private getleagueData (league?: LeagueEntryDTO): LeagueEntryByQueue | null {
private getleagueData(league?: LeagueEntryDTO): LeagueEntryByQueue | null {
if (!league) {
return null
}
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 shortName = this.uniqueLeagues.includes(league.tier) ?
league.leaguePoints :
league.tier[0] + this.leaguesNumbers[league.rank]
const fullRank = this.uniqueLeagues.includes(league.tier)
? league.tier
: `${league.tier} ${league.rank}`
const winrate = this.getWinrate(league.wins, league.losses)
const shortName = this.uniqueLeagues.includes(league.tier)
? league.leaguePoints
: league.tier[0] + this.leaguesNumbers[league.rank]
return {
...league,
@ -45,7 +57,7 @@ class SummonerService {
* @param summonerName
* @param region
*/
public async getAccount (summonerName: string, region: string) {
public async getAccount(summonerName: string, region: string) {
const name = summonerName.toLowerCase()
const account = await Jax.Summoner.summonerName(name, region)
return account
@ -56,18 +68,11 @@ class SummonerService {
* @param account of the summoner
* @param summonerDB summoner in the database
*/
public getAllSummonerNames (account: SummonerDTO, summonerDB: SummonerModel) {
const names = summonerDB.names ? summonerDB.names : []
if (!names.find(n => n.name === account.name)) {
names.push({
public async getAllSummonerNames(account: SummonerDTO, summonerDB: Summoner) {
await summonerDB.related('names').firstOrCreate({
name: account.name,
date: new Date(),
})
summonerDB.names = names
}
return names
return summonerDB.related('names').query().select('name', 'created_at')
}
/**
@ -75,15 +80,16 @@ class SummonerService {
* @param account
* @param region
*/
public async getRanked (account: SummonerDTO, region: string): Promise<LeagueEntriesByQueue> {
const ranked = await Jax.League.summonerID(account.id, region)
const result:LeagueEntriesByQueue = {}
public async getRanked(summonerId: string, region: string): Promise<LeagueEntriesByQueue> {
const ranked = await Jax.League.summonerID(summonerId, region)
const result: LeagueEntriesByQueue = {}
if (ranked && ranked.length) {
result.soloQ = this.getleagueData(ranked.find(e => e.queueType === 'RANKED_SOLO_5x5')) || undefined
result.flex5v5 = this.getleagueData(ranked.find(e => e.queueType === 'RANKED_FLEX_SR')) || undefined
result.soloQ =
this.getleagueData(ranked.find((e) => e.queueType === 'RANKED_SOLO_5x5')) || undefined
result.flex5v5 =
this.getleagueData(ranked.find((e) => e.queueType === 'RANKED_FLEX_SR')) || undefined
}
return result
}
}

View file

@ -1,79 +0,0 @@
import { MatchModel, ParticipantBasic } from 'App/Models/Match'
import { MatchDto } from 'App/Services/Jax/src/Endpoints/MatchEndpoint'
import MatchTransformer from 'App/Transformers/MatchTransformer'
class BasicMatchTransformer extends MatchTransformer {
/**
* Transform raw data for 1 match
* @param match
* @param puuid
* @param accountId
*/
private transformOneMatch (match: MatchDto, puuid: string, accountId: string): MatchModel {
// Global data about the match
const globalInfos = super.getGameInfos(match)
const identity = match.participantIdentities.find((p) => p.player.currentAccountId === accountId)
const player = match.participants[identity!.participantId - 1]
let win = match.teams.find((t) => t.teamId === player.teamId)!.win
// Match less than 5min
if (match.gameDuration < 300) {
win = 'Remake'
}
// Player data
const playerData = super.getPlayerData(match, player, false)
// Teams data
const allyTeam: ParticipantBasic[] = []
const enemyTeam: ParticipantBasic[] = []
for (let summoner of match.participantIdentities) {
const allData = match.participants[summoner.participantId - 1]
const playerInfos = {
account_id: summoner.player.currentAccountId,
name: summoner.player.summonerName,
role: super.getRoleName(allData.timeline, match.queueId),
champion: super.getChampion(allData.championId),
}
if (allData.teamId === player.teamId) {
allyTeam.push(playerInfos)
} else {
enemyTeam.push(playerInfos)
}
}
// Roles
super.getMatchRoles(match, allyTeam, enemyTeam, player.teamId, playerData)
return {
account_id: identity!.player.currentAccountId,
summoner_puuid: puuid,
gameId: match.gameId,
result: win,
allyTeam,
enemyTeam,
...globalInfos,
...playerData,
}
}
/**
* Transform raw data from Riot API
* @param matches data from Riot API, Array of matches
* @param ctx context
*/
public async transform (matches: MatchDto[], { puuid, accountId }: { puuid: string, accountId: string }) {
await super.getContext()
const finalMatches: MatchModel[] = []
matches.forEach((match, index) => {
finalMatches[index] = this.transformOneMatch(match, puuid, accountId)
})
return finalMatches
}
}
export default new BasicMatchTransformer()

View file

@ -1,98 +0,0 @@
import { Ban, DetailedMatchModel } from 'App/Models/DetailedMatch'
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 = (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,76 +0,0 @@
import { queuesWithRole } from 'App/helpers'
import Jax from 'App/Services/Jax'
import { CurrentGameInfo, CurrentGameParticipant } from 'App/Services/Jax/src/Endpoints/SpectatorEndpoint'
import { FinalRoleComposition } from 'App/Services/RoleIdentiticationService'
import SummonerService, { LeagueEntriesByQueue } from 'App/Services/SummonerService'
import MatchTransformer, { PlayerRole } from './MatchTransformer'
class LiveMatchTransformer extends MatchTransformer {
/**
* Get player soloQ and flex rank from his summonerName
* @param participant
* @param region
*/
private async getPlayerRank (participant: CurrentGameParticipant, region: string) {
const account = await Jax.Summoner.summonerId(participant.summonerId, region)
let ranked: LeagueEntriesByQueue
if (account) {
ranked = await SummonerService.getRanked(account, region)
}
return {
...participant,
level: account ? account.summonerLevel : undefined,
rank: account ? ranked! : undefined,
}
}
/**
* Transform raw data from Riot API
* @param liveMatch
* @param ctx
*/
public async transform (liveMatch: CurrentGameInfo, { region }: { region: string }) {
await super.getContext()
// Roles
const blueTeam: PlayerRole[] = [] // 100
const redTeam: PlayerRole[] = [] // 200
let blueRoles: FinalRoleComposition = {}
let redRoles: FinalRoleComposition = {}
const needsRole = this.championRoles &&
(queuesWithRole.includes(liveMatch.gameQueueConfigId) ||
(liveMatch.gameType === 'CUSTOM_GAME' && liveMatch.participants.length === 10))
if (needsRole) {
liveMatch.participants.map(p => {
const playerRole = { champion: p.championId, jungle: p.spell1Id === 11 || p.spell2Id === 11 }
p.teamId === 100 ? blueTeam.push(playerRole) : redTeam.push(playerRole)
})
blueRoles = super.getTeamRoles(blueTeam)
redRoles = super.getTeamRoles(redTeam)
}
for (const participant of liveMatch.participants) {
// Perks
participant.runes = participant.perks ?
super.getPerksImages(participant.perks.perkIds[0], participant.perks.perkSubStyle)
: {}
// Roles
if (needsRole) {
const roles = participant.teamId === 100 ? blueRoles : redRoles
participant.role = Object.entries(roles).find(([, champion]) => participant.championId === champion)![0]
}
}
// Ranks
const requestsParticipants = liveMatch.participants.map(p => this.getPlayerRank(p, region))
liveMatch.participants = await Promise.all(requestsParticipants)
return liveMatch
}
}
export default new LiveMatchTransformer()

View file

@ -1,355 +0,0 @@
import { getSeasonNumber, queuesWithRole, sortTeamByRole, supportItems } from 'App/helpers'
import Jax from 'App/Services/Jax'
import { Champion, Item, ParticipantBasic, ParticipantDetails, PercentStats, Perks, Stats, SummonerSpell } from 'App/Models/Match'
import RoleIdentificationService, { ChampionsPlayRate } from 'App/Services/RoleIdentiticationService'
import { ChampionDTO, ItemDTO, PerkDTO, PerkStyleDTO, SummonerSpellDTO } from 'App/Services/Jax/src/Endpoints/CDragonEndpoint'
import { TeamStats } from 'App/Models/DetailedMatch'
import {
MatchDto,
ParticipantDto,
ParticipantStatsDto,
ParticipantTimelineDto,
} from 'App/Services/Jax/src/Endpoints/MatchEndpoint'
export interface PlayerRole {
champion: number,
jungle?: boolean,
support?: boolean,
}
export default abstract class MatchTransformer {
protected champions: ChampionDTO[]
protected items: ItemDTO[]
protected perks: PerkDTO[]
protected perkstyles: PerkStyleDTO[]
protected summonerSpells: SummonerSpellDTO[]
protected championRoles: ChampionsPlayRate
protected sortTeamByRole: (a: ParticipantBasic | ParticipantDetails, b: ParticipantBasic | ParticipantDetails) => number
/**
* Get global Context with CDragon Data
*/
public async getContext () {
const items = await Jax.CDragon.items()
const champions = await Jax.CDragon.champions()
const perks = await Jax.CDragon.perks()
const perkstyles = await Jax.CDragon.perkstyles()
const summonerSpells = await Jax.CDragon.summonerSpells()
const championRoles = await RoleIdentificationService.pullData().catch(() => { })
this.champions = champions
this.items = items
this.perks = perks
this.perkstyles = perkstyles.styles
this.summonerSpells = summonerSpells
this.championRoles = championRoles as ChampionsPlayRate
this.sortTeamByRole = sortTeamByRole
}
/**
* Get champion specific data
* @param id of the champion
*/
public getChampion (id: number): Champion {
const originalChampionData = this.champions.find(c => c.id === id)
const icon = 'https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/'
+ originalChampionData!.squarePortraitPath.split('/assets/')[1].toLowerCase()
return {
icon,
id: originalChampionData!.id,
name: originalChampionData!.name,
alias: originalChampionData!.alias,
roles: originalChampionData!.roles,
}
}
/**
* Get global data about the match
*/
public getGameInfos (match: MatchDto) {
return {
map: match.mapId,
gamemode: match.queueId,
date: match.gameCreation,
region: match.platformId.toLowerCase(),
season: getSeasonNumber(match.gameCreation),
time: match.gameDuration,
}
}
/**
* Get player specific data during the match
* @param match
* @param player
* @param detailed : detailed or not stats
* @param teamStats : if detailed, the teamStats argument is mandatory
*/
public getPlayerData (match: MatchDto, player: ParticipantDto, detailed: boolean, teamStats?: TeamStats) {
const identity = match.participantIdentities.find(p => p.participantId === player.participantId)
const name = identity!.player.summonerName
const champion = this.getChampion(player.championId)
const role = this.getRoleName(player.timeline, match.queueId)
const level = player.stats.champLevel
// Regular stats / Full match stats
const stats: Stats = {
kills: player.stats.kills,
deaths: player.stats.deaths,
assists: player.stats.assists,
minions: player.stats.totalMinionsKilled + player.stats.neutralMinionsKilled,
vision: player.stats.visionScore,
gold: player.stats.goldEarned,
dmgChamp: player.stats.totalDamageDealtToChampions,
dmgObj: player.stats.damageDealtToObjectives,
dmgTaken: player.stats.totalDamageTaken,
kp: 0,
kda: 0,
realKda: 0,
}
if (stats.kills + stats.assists !== 0 && stats.deaths === 0) {
stats.kda = '∞'
stats.realKda = stats.kills + stats.assists
} else {
stats.kda = +(stats.deaths === 0 ? 0 : ((stats.kills + stats.assists) / stats.deaths)).toFixed(2)
stats.realKda = stats.kda
}
// Percent stats / Per minute stats : only for detailed match
let percentStats: PercentStats
if (detailed) {
teamStats = teamStats!
percentStats = {
minions: +(stats.minions / (match.gameDuration / 60)).toFixed(2),
vision: +(stats.vision / (match.gameDuration / 60)).toFixed(2),
gold: +(player.stats.goldEarned * 100 / teamStats.gold).toFixed(1) + '%',
dmgChamp: +(player.stats.totalDamageDealtToChampions * 100 / teamStats.dmgChamp).toFixed(1) + '%',
dmgObj: +(teamStats.dmgObj ? player.stats.damageDealtToObjectives * 100 / teamStats.dmgObj : 0).toFixed(1) + '%',
dmgTaken: +(player.stats.totalDamageTaken * 100 / teamStats.dmgTaken).toFixed(1) + '%',
}
stats.kp = teamStats.kills === 0 ? '0%' : +((stats.kills + stats.assists) * 100 / teamStats.kills).toFixed(1) + '%'
} else {
const totalKills = match.participants.reduce((prev, current) => {
if (current.teamId !== player.teamId) {
return prev
}
return prev + current.stats.kills
}, 0)
stats.criticalStrike = player.stats.largestCriticalStrike
stats.killingSpree = player.stats.largestKillingSpree
stats.doubleKills = player.stats.doubleKills
stats.tripleKills = player.stats.tripleKills
stats.quadraKills = player.stats.quadraKills
stats.pentaKills = player.stats.pentaKills
stats.heal = player.stats.totalHeal
stats.towers = player.stats.turretKills
stats.longestLiving = player.stats.longestTimeSpentLiving
stats.kp = totalKills === 0 ? 0 : +((stats.kills + stats.assists) * 100 / totalKills).toFixed(1)
}
let primaryRune: string | null = null
let secondaryRune: string | null = null
if (player.stats.perkPrimaryStyle) {
({ primaryRune, secondaryRune } = this.getPerksImages(player.stats.perk0, player.stats.perkSubStyle))
}
const items: (Item | null)[] = []
for (let i = 0; i < 6; i++) {
const id = player.stats['item' + i]
if (id === 0) {
items.push(null)
continue
}
const item = this.items.find(i => i.id === id)
// TODO: get deleted item from old patch CDragon JSON instead of null
if (!item) {
items.push(null)
continue
}
const itemUrl = item.iconPath.split('/assets/')[1].toLowerCase()
items.push({
image: `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/${itemUrl}`,
name: item.name,
description: item.description,
price: item.priceTotal,
})
}
const firstSum = player.spell1Id
const secondSum = player.spell2Id
const playerData: ParticipantDetails = {
name,
summonerId: identity!.player.summonerId,
champion,
role,
primaryRune,
secondaryRune,
level,
items,
firstSum,
secondSum,
stats,
}
if (detailed) {
playerData.percentStats = percentStats!
}
playerData.perks = this.getFullPerks(player.stats)
return playerData
}
public getFullPerks (stats: ParticipantStatsDto) {
const perks: Perks = {
primaryStyle: stats.perkPrimaryStyle,
secondaryStyle: stats.perkSubStyle,
selected: [],
}
for (let i = 0; i < 6; i++) {
perks.selected.push(stats[`perk${i}`])
}
for (let i = 0; i < 3; i++) {
perks.selected.push(stats[`statPerk${i}`])
}
return perks
}
/**
* Return the icons of the primary rune and secondary category
* @param perk0 primary perks id
* @param perkSubStyle secondary perks category
*/
public getPerksImages (perk0: number, perkSubStyle: number) {
const firstRune = this.perks.find(p => p.id === perk0)
const firstRuneUrl = firstRune!.iconPath.split('/assets/')[1].toLowerCase()
const primaryRune = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/${firstRuneUrl}`
const secondRuneStyle = this.perkstyles.find(p => p.id === perkSubStyle)
const secondRuneStyleUrl = secondRuneStyle ? secondRuneStyle.iconPath.split('/assets/')[1].toLowerCase() : null
const secondaryRune = secondRuneStyleUrl ?
`https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/${secondRuneStyleUrl}`
: ''
return { primaryRune, secondaryRune }
}
/**
* Return the lane of the summoner according to timeline
* @param timeline from Riot Api
* @param gamemode of the match to check if a role is needed
*/
public getRoleName (timeline: ParticipantTimelineDto, gamemode: number) {
if (!queuesWithRole.includes(gamemode)) {
return 'NONE'
}
if (timeline.lane === 'BOTTOM' && timeline.role.includes('SUPPORT')) {
return 'SUPPORT'
}
return timeline.lane
}
/**
* Return the 5 roles of a team based on champions
* @param team 5 champions + smite from a team
*/
public getTeamRoles (team: PlayerRole[]) {
const teamJunglers = team.filter(p => p.jungle && !p.support)
const jungle = teamJunglers.length === 1 ? teamJunglers[0].champion : undefined
const teamSupports = team.filter(p => p.support && !p.jungle)
const support = teamSupports.length === 1 ? teamSupports[0].champion : undefined
return RoleIdentificationService.getRoles(this.championRoles, team.map(p => p.champion), jungle, support)
}
/**
* Update roles for a team if Riot's ones are badly identified
* @param team 5 players data of the team
* @param champs 5 champions + smite from the team
* @param playerData data of the searched player, only for basic matches
*/
public updateTeamRoles (
team: ParticipantBasic[] | ParticipantDetails[],
champs: PlayerRole[],
playerData?: ParticipantDetails
) {
// const actualRoles = [...new Set(team.map(p => p.role))]
// if (actualRoles.length === 5) {
// return
// }
const identifiedChamps = this.getTeamRoles(champs)
for (const summoner of team) {
summoner.role = Object.entries(identifiedChamps).find(([, champion]) => summoner.champion.id === champion)![0]
if (playerData && summoner.champion.id === playerData.champion.id) {
playerData.role = summoner.role
}
}
}
/**
*
* @param match from Riot Api
* @param allyTeam 5 players of the first team
* @param enemyTeam 5 players of the second team
* @param allyTeamId team id of the searched player, only for basic matches
* @param playerData data of the searched player, only for basic matches
*/
public getMatchRoles (
match: MatchDto,
allyTeam: ParticipantBasic[] | ParticipantDetails[],
enemyTeam: ParticipantBasic[] | ParticipantDetails[],
allyTeamId = 100,
playerData?: ParticipantDetails
) {
if (!this.championRoles || !queuesWithRole.includes(match.queueId)) {
return
}
let allyChamps: PlayerRole[] = []
let enemyChamps: PlayerRole[] = []
match.participants.map(p => {
const items = [p.stats.item0, p.stats.item1, p.stats.item2, p.stats.item3, p.stats.item4, p.stats.item5]
const playerRole = {
champion: p.championId,
jungle: p.spell1Id === 11 || p.spell2Id === 11,
support: supportItems.some(suppItem => items.includes(suppItem)),
}
p.teamId === allyTeamId ? allyChamps.push(playerRole) : enemyChamps.push(playerRole)
})
this.updateTeamRoles(allyTeam, allyChamps, playerData)
this.updateTeamRoles(enemyTeam, enemyChamps)
allyTeam.sort(this.sortTeamByRole)
enemyTeam.sort(this.sortTeamByRole)
}
/**
* Get Summoner Spell Data from CDragon
* @param id of the summonerSpell
*/
public getSummonerSpell (id: number): SummonerSpell | null {
if (id === 0) {
return null
}
const spell = this.summonerSpells.find(s => s.id === id)
const spellName = spell!.iconPath.split('/assets/')[1].toLowerCase()
return {
name: spell!.name,
description: spell!.description,
icon: `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/${spellName}`,
}
}
}

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class DetailedMatchValidator {
constructor (private ctx: HttpContextContract) {
}
constructor(protected ctx: HttpContextContract) {}
/**
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
/*
* Define 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
@ -25,20 +24,9 @@ export default class DetailedMatchValidator {
* ```
*/
public schema = schema.create({
gameId: schema.number(),
region: schema.string(),
matchId: 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
@ -48,6 +36,7 @@ export default class DetailedMatchValidator {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages = {}
}

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class MatchesIndexValidator {
constructor (private ctx: HttpContextContract) {
}
constructor(protected ctx: HttpContextContract) {}
/**
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
/*
* Define 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
@ -26,24 +25,11 @@ export default class MatchesIndexValidator {
*/
public schema = schema.create({
puuid: schema.string(),
accountId: schema.string(),
region: schema.string(),
gameIds: schema.array().members(
schema.number()
),
matchIds: schema.array().members(schema.string()),
season: schema.number.optional(),
})
/**
* 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
@ -53,6 +39,7 @@ export default class MatchesIndexValidator {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages = {}
}

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { rules, schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerBasicValidator {
constructor (private ctx: HttpContextContract) {
}
constructor(protected ctx: HttpContextContract) {}
/**
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
/*
* Define 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
@ -25,22 +24,10 @@ export default class SummonerBasicValidator {
* ```
*/
public schema = schema.create({
summoner: schema.string({}, [
rules.regex(/^[0-9\p{L} _\.]+$/u),
]),
summoner: schema.string({}, [rules.regex(/^[0-9\p{L} _\.]+$/u)]),
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
@ -50,6 +37,7 @@ export default class SummonerBasicValidator {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages = {}
}

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerChampionValidator {
constructor (private ctx: HttpContextContract) {
}
constructor(protected ctx: HttpContextContract) {}
/**
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
/*
* Define 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
@ -30,16 +29,6 @@ export default class SummonerChampionValidator {
season: schema.number.optional(),
})
/**
* 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
@ -49,6 +38,7 @@ export default class SummonerChampionValidator {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages = {}
}

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerLiveValidator {
constructor (private ctx: HttpContextContract) {
}
constructor(protected ctx: HttpContextContract) {}
/**
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
/*
* Define 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
@ -29,16 +28,6 @@ export default class SummonerLiveValidator {
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
@ -48,6 +37,7 @@ export default class SummonerLiveValidator {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages = {}
}

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerOverviewValidator {
constructor (private ctx: HttpContextContract) {
}
constructor(protected ctx: HttpContextContract) {}
/**
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
/*
* Define 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
@ -26,21 +25,10 @@ export default class SummonerOverviewValidator {
*/
public schema = schema.create({
puuid: schema.string(),
accountId: schema.string(),
region: schema.string(),
season: schema.number.optional(),
})
/**
* 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
@ -50,6 +38,7 @@ export default class SummonerOverviewValidator {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages = {}
}

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerRecordValidator {
constructor (private ctx: HttpContextContract) {
}
constructor(protected ctx: HttpContextContract) {}
/**
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
/*
* Define 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
@ -29,16 +28,6 @@ export default class SummonerRecordValidator {
season: schema.number.optional(),
})
/**
* 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
@ -48,6 +37,7 @@ export default class SummonerRecordValidator {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages = {}
}

View file

@ -1,4 +1,63 @@
import { ParticipantBasic, ParticipantDetails } from './Models/Match'
/**
* All League of Legends regions used in Riot API
*/
export enum LeagueRegion {
BRAZIL = 'br1',
EUROPE_NORTHEAST = 'eun1',
EUROPE_WEST = 'euw1',
KOREA = 'kr',
LATIN_AMERICA_NORTH = 'la1',
LATIN_AMERICA_SOUTH = 'la2',
NORTH_AMERICA = 'na1',
OCEANIA = 'oc1',
RUSSIA = 'ru',
TURKEY = 'tr1',
JAPAN = 'jp1',
}
/**
* New regions used in Riot API >= v5
*/
export enum RiotRegion {
AMERICAS = 'americas',
ASIA = 'asia',
EUROPE = 'europe',
}
/**
* Map old Riot API regions to new ones
* @param region : old region
* @returns new region name
*/
export function getRiotRegion(region: string): RiotRegion {
switch (
region as LeagueRegion // TODO: remove cast when region is typed to "Region" everywhere instead of string
) {
case LeagueRegion.NORTH_AMERICA:
case LeagueRegion.BRAZIL:
case LeagueRegion.LATIN_AMERICA_NORTH:
case LeagueRegion.LATIN_AMERICA_SOUTH:
case LeagueRegion.OCEANIA:
return RiotRegion.AMERICAS
case LeagueRegion.KOREA:
case LeagueRegion.JAPAN:
return RiotRegion.ASIA
case LeagueRegion.EUROPE_NORTHEAST:
case LeagueRegion.EUROPE_WEST:
case LeagueRegion.TURKEY:
case LeagueRegion.RUSSIA:
return RiotRegion.EUROPE
}
}
/**
* Interface to help define a player's role
*/
export interface PlayerRole {
champion: number
jungle?: boolean
support?: boolean
}
/**
* League of Legends queues with defined role for each summoner
@ -13,8 +72,13 @@ export const queuesWithRole = [
]
/**
* League of Legends seasons timestamps
*/
* League of Legends tutorial queues
*/
export const tutorialQueues = [2000, 2010, 2020]
/**
* League of Legends seasons timestamps
*/
export const seasons = {
0: 9,
1578628800000: 10,
@ -23,28 +87,47 @@ export const seasons = {
}
/**
* League of Legends all support item ids
*/
* League of Legends all support item ids
*/
export const supportItems = [3850, 3851, 3853, 3854, 3855, 3857, 3858, 3859, 3860, 3862, 3863, 3864]
/**
* Get season number for a match
* @param timestamp
*/
export function getSeasonNumber (timestamp: number): number {
const arrSeasons = Object.keys(seasons).map(k => Number(k))
export function getSeasonNumber(timestamp: number): number {
const arrSeasons = Object.keys(seasons).map((k) => Number(k))
arrSeasons.push(timestamp)
arrSeasons.sort()
const indexSeason = arrSeasons.indexOf(timestamp) - 1
return seasons[arrSeasons[indexSeason]]
}
/**
* Return current League of Legends season number
*/
export function getCurrentSeason(): number {
const lastTimestamp = Object.keys(seasons).pop()!
return seasons[lastTimestamp]
}
interface SortableByRole {
role: string
}
/**
* Sort array of Players by roles according to a specific order
* @param a first player
* @param b second player
*/
export function sortTeamByRole (a: ParticipantBasic | ParticipantDetails, b: ParticipantBasic | ParticipantDetails) {
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
export function sortTeamByRole<T extends SortableByRole>(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<TValue>(value: TValue | null | undefined): value is TValue {
if (value === null || value === undefined) return false
const testDummy: TValue = value
return true
}

View file

@ -0,0 +1,66 @@
import { BaseCommand, args } from '@adonisjs/core/build/standalone'
import MatchV4Service from 'App/Services/MatchV4Service'
import SummonerService from 'App/Services/SummonerService'
export default class LoadV4Matches extends BaseCommand {
/**
* Command name is used to run the command
*/
public static commandName = 'load:v4'
/**
* Command description is displayed in the "help" output
*/
public static description = 'Load matches for a given Summoner from the old Match-V4 endpoint'
@args.string({ description: 'Summoner name to seach' })
public summoner: string
@args.string({ description: 'League region of the summoner' })
public region: string
public static settings = {
/**
* Set the following value to true, if you want to load the application
* before running the command
*/
loadApp: true,
/**
* Set the following value to true, if you want this command to keep running until
* you manually decide to exit the process
*/
stayAlive: false,
}
public async run() {
this.logger.info(`Trying to find ${this.summoner} from ${this.region}`)
// ACCOUNT
const account = await SummonerService.getAccount(this.summoner, this.region)
if (account) {
this.logger.success('League account found.')
} else {
return this.logger.error('League account not found.')
}
// MATCHLIST
const matchListIds = await MatchV4Service.updateMatchList(account, this.region)
if (matchListIds.length) {
this.logger.success(`${matchListIds.length} matches in the matchlist.`)
} else {
return this.logger.error('Matchlist empty.')
}
// MATCHES
const chunkSize = 10
let savedMatches = 0
for (let i = 0; i < matchListIds.length; i += chunkSize) {
const chunk = matchListIds.slice(i, i + chunkSize)
savedMatches += await MatchV4Service.getMatches(this.region, chunk)
this.logger.info(`${savedMatches} matches saved.`)
}
this.logger.success(`${savedMatches} matches saved for summoner ${this.summoner}.`)
}
}

View file

@ -112,7 +112,7 @@ export const http: ServerConfig = {
/*
|--------------------------------------------------------------------------
| Force content negotiation to JSON
| Force Content Negotiation
|--------------------------------------------------------------------------
|
| The internals of the framework relies on the content negotiation to
@ -121,9 +121,9 @@ export const http: ServerConfig = {
| However, it is a very common these days that API servers always wants to
| make response in JSON regardless of the existence of the `Accept` header.
|
| By setting `forceContentNegotiationToJSON = true`, you negotiate with the
| server in advance to always return JSON without relying on the client
| to set the header explicitly.
| By setting `forceContentNegotiationTo = 'application/json'`, you negotiate
| with the server in advance to always return JSON without relying on the
| client to set the header explicitly.
|
*/
forceContentNegotiationTo: 'application/json',

60
server/config/database.ts Normal file
View file

@ -0,0 +1,60 @@
/**
* Config source: https://git.io/JesV9
*
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/
import pg from 'pg'
import Env from '@ioc:Adonis/Core/Env'
import { DatabaseConfig } from '@ioc:Adonis/Lucid/Database'
const databaseConfig: DatabaseConfig = {
/*
|--------------------------------------------------------------------------
| Connection
|--------------------------------------------------------------------------
|
| The primary connection for making database queries across the application
| You can use any key from the `connections` object defined in this same
| file.
|
*/
connection: Env.get('DB_CONNECTION'),
connections: {
/*
|--------------------------------------------------------------------------
| PostgreSQL config
|--------------------------------------------------------------------------
|
| Configuration for PostgreSQL database. Make sure to install the driver
| from npm when using this connection
|
| npm i pg
|
*/
pg: {
client: 'pg',
connection: {
host: Env.get('PG_HOST'),
port: Env.get('PG_PORT'),
user: Env.get('PG_USER'),
password: Env.get('PG_PASSWORD', ''),
database: Env.get('PG_DB_NAME'),
},
migrations: {
naturalSort: true,
},
healthCheck: true,
debug: false,
},
},
}
// Set bigint as number instead of string in postgres
pg.types.setTypeParser(20, (value) => {
return parseInt(value)
})
export default databaseConfig

148
server/config/drive.ts Normal file
View file

@ -0,0 +1,148 @@
/**
* Config source: https://git.io/JBt3o
*
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/
import Env from '@ioc:Adonis/Core/Env'
import { DriveConfig } from '@ioc:Adonis/Core/Drive'
import Application from '@ioc:Adonis/Core/Application'
/*
|--------------------------------------------------------------------------
| Drive Config
|--------------------------------------------------------------------------
|
| The `DriveConfig` relies on the `DisksList` interface which is
| defined inside the `contracts` directory.
|
*/
const driveConfig: DriveConfig = {
/*
|--------------------------------------------------------------------------
| Default disk
|--------------------------------------------------------------------------
|
| The default disk to use for managing file uploads. The value is driven by
| the `DRIVE_DISK` environment variable.
|
*/
disk: Env.get('DRIVE_DISK'),
disks: {
/*
|--------------------------------------------------------------------------
| Local
|--------------------------------------------------------------------------
|
| Uses the local file system to manage files. Make sure to turn off serving
| files when not using this disk.
|
*/
local: {
driver: 'local',
visibility: 'public',
/*
|--------------------------------------------------------------------------
| Storage root - Local driver only
|--------------------------------------------------------------------------
|
| Define an absolute path to the storage directory from where to read the
| files.
|
*/
root: Application.tmpPath('uploads'),
/*
|--------------------------------------------------------------------------
| Serve files - Local driver only
|--------------------------------------------------------------------------
|
| When this is set to true, AdonisJS will configure a files server to serve
| files from the disk root. This is done to mimic the behavior of cloud
| storage services that has inbuilt capabilities to serve files.
|
*/
serveFiles: true,
/*
|--------------------------------------------------------------------------
| Base path - Local driver only
|--------------------------------------------------------------------------
|
| Base path is always required when "serveFiles = true". Also make sure
| the `basePath` is unique across all the disks using "local" driver and
| you are not registering routes with this prefix.
|
*/
basePath: '/uploads',
},
/*
|--------------------------------------------------------------------------
| S3 Driver
|--------------------------------------------------------------------------
|
| Uses the S3 cloud storage to manage files. Make sure to install the s3
| drive separately when using it.
|
|**************************************************************************
| npm i @adonisjs/drive-s3
|**************************************************************************
|
*/
// s3: {
// driver: 's3',
// visibility: 'public',
// key: Env.get('S3_KEY'),
// secret: Env.get('S3_SECRET'),
// region: Env.get('S3_REGION'),
// bucket: Env.get('S3_BUCKET'),
// endpoint: Env.get('S3_ENDPOINT'),
// },
/*
|--------------------------------------------------------------------------
| GCS Driver
|--------------------------------------------------------------------------
|
| Uses the Google cloud storage to manage files. Make sure to install the GCS
| drive separately when using it.
|
|**************************************************************************
| npm i @adonisjs/drive-gcs
|**************************************************************************
|
*/
// gcs: {
// driver: 'gcs',
// visibility: 'public',
// keyFilename: Env.get('GCS_KEY_FILENAME'),
// bucket: Env.get('GCS_BUCKET'),
/*
|--------------------------------------------------------------------------
| Uniform ACL - Google cloud storage only
|--------------------------------------------------------------------------
|
| When using the Uniform ACL on the bucket, the "visibility" option is
| ignored. Since, the files ACL is managed by the google bucket policies
| directly.
|
|**************************************************************************
| Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access
|**************************************************************************
|
| The following option just informs drive whether your bucket is using uniform
| ACL or not. The actual setting needs to be toggled within the Google cloud
| console.
|
*/
// usingUniformAcl: false
// },
},
}
export default driveConfig

View file

@ -1,14 +0,0 @@
import { MongodbConfig } from '@ioc:Mongodb/Database'
import Env from '@ioc:Adonis/Core/Env'
const config: MongodbConfig = {
default: 'mongodb',
connections: {
mongodb: {
url: Env.get('MONGODB_URL') as string,
database: Env.get('MONGODB_DATABASE') as string,
},
},
}
export default config

View file

@ -41,6 +41,7 @@ const redisConfig: RedisConfig = {
password: Env.get('REDIS_PASSWORD', ''),
db: 0,
keyPrefix: '',
healthCheck: true,
},
},
}

23
server/contracts/drive.ts Normal file
View file

@ -0,0 +1,23 @@
/**
* Contract source: https://git.io/JBt3I
*
* Feel free to let us know via PR, if you find something broken in this contract
* file.
*/
declare module '@ioc:Adonis/Core/Drive' {
interface DisksList {
local: {
config: LocalDriverConfig
implementation: LocalDriverContract
}
// s3: {
// config: S3DriverConfig
// implementation: S3DriverContract
// }
// gcs: {
// config: GcsDriverConfig
// implementation: GcsDriverContract
// }
}
}

View file

@ -25,6 +25,5 @@ declare module '@ioc:Adonis/Core/Event' {
| an instance of the the UserModel only.
|
*/
interface EventsList {
}
interface EventsList {}
}

View file

@ -6,16 +6,14 @@
*/
declare module '@ioc:Adonis/Core/Hash' {
import { HashDrivers } from '@ioc:Adonis/Core/Hash'
interface HashersList {
bcrypt: {
config: BcryptConfig,
implementation: BcryptContract,
},
config: BcryptConfig
implementation: BcryptContract
}
argon: {
config: ArgonConfig,
implementation: ArgonContract,
},
config: ArgonConfig
implementation: ArgonContract
}
}
}

View file

@ -7,6 +7,6 @@
declare module '@ioc:Adonis/Addons/Redis' {
interface RedisConnectionsList {
local: RedisConnectionConfig,
local: RedisConnectionConfig
}
}

View file

@ -0,0 +1 @@
// import Factory from '@ioc:Adonis/Lucid/Factory'

View file

@ -0,0 +1,26 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
export default class Matches extends BaseSchema {
protected tableName = 'matches'
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.string('id', 15).primary()
table.bigInteger('game_id').notNullable()
table.specificType('map', 'smallint').notNullable()
table.specificType('gamemode', 'smallint').notNullable().index()
table.bigInteger('date').notNullable()
table.string('region', 4).notNullable()
table.specificType('result', 'smallint').notNullable()
table.float('season').notNullable().index()
table.specificType('game_duration', 'smallint').unsigned().notNullable()
})
// this.schema.alterTable
}
public async down() {
this.schema.dropTable(this.tableName)
}
}

View file

@ -0,0 +1,74 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
export default class MatchPlayers extends BaseSchema {
protected tableName = 'match_players'
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('match_id', 15).notNullable().index()
table.specificType('participant_id', 'smallint').notNullable()
table.string('summoner_id', 63).notNullable()
table.string('summoner_puuid', 78).notNullable().index()
table.string('summoner_name', 16).notNullable()
table.specificType('win', 'smallint').notNullable()
table.specificType('loss', 'smallint').notNullable()
table.specificType('remake', 'smallint').notNullable()
table.specificType('team', 'smallint').notNullable().index()
table.specificType('team_position', 'smallint').notNullable().index()
table.specificType('kills', 'smallint').unsigned().notNullable()
table.specificType('deaths', 'smallint').unsigned().notNullable()
table.specificType('assists', 'smallint').unsigned().notNullable()
table.float('kda').notNullable()
table.float('kp').notNullable()
table.specificType('champ_level', 'smallint').notNullable()
table.specificType('champion_id', 'smallint').notNullable().index()
table.specificType('champion_role', 'smallint').notNullable()
table.specificType('double_kills', 'smallint').notNullable()
table.specificType('triple_kills', 'smallint').notNullable()
table.specificType('quadra_kills', 'smallint').notNullable()
table.specificType('penta_kills', 'smallint').notNullable()
table.specificType('baron_kills', 'smallint').notNullable()
table.specificType('dragon_kills', 'smallint').notNullable()
table.specificType('turret_kills', 'smallint').notNullable()
table.specificType('vision_score', 'smallint').notNullable()
table.integer('gold').notNullable()
table.integer('summoner1_id').notNullable()
table.integer('summoner2_id').notNullable()
table.integer('item0').notNullable()
table.integer('item1').notNullable()
table.integer('item2').notNullable()
table.integer('item3').notNullable()
table.integer('item4').notNullable()
table.integer('item5').notNullable()
table.integer('item6').notNullable()
table.integer('damage_dealt_objectives').notNullable()
table.integer('damage_dealt_champions').notNullable()
table.integer('damage_taken').notNullable()
table.integer('heal').notNullable()
table.specificType('minions', 'smallint').notNullable()
table.specificType('critical_strike', 'smallint').unsigned().notNullable()
table.specificType('killing_spree', 'smallint').unsigned().notNullable()
table.specificType('time_spent_living', 'smallint').unsigned().notNullable()
table.integer('perks_primary_style').notNullable()
table.integer('perks_secondary_style').notNullable()
table.specificType('perks_selected', 'INT[]').notNullable()
})
}
public async down() {
this.schema.dropTable(this.tableName)
}
}

View file

@ -0,0 +1,21 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
export default class Summoners extends BaseSchema {
protected tableName = 'summoners'
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.string('puuid', 78).primary()
/**
* Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
*/
table.timestamp('created_at', { useTz: true })
table.timestamp('updated_at', { useTz: true })
})
}
public async down() {
this.schema.dropTable(this.tableName)
}
}

View file

@ -0,0 +1,26 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
export default class SummonerNames extends BaseSchema {
protected tableName = 'summoner_names'
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('summoner_puuid', 78).notNullable().index()
table.string('name', 16).notNullable().index()
/**
* Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
*/
table.timestamp('created_at', { useTz: true })
})
this.schema.alterTable(this.tableName, (table) => {
table.unique(['summoner_puuid', 'name'])
})
}
public async down() {
this.schema.dropTable(this.tableName)
}
}

View file

@ -0,0 +1,21 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
export default class SummonerMatchLists extends BaseSchema {
protected tableName = 'summoner_matchlist'
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('summoner_puuid', 78).notNullable().index()
table.string('match_id', 15).notNullable().index()
})
this.schema.alterTable(this.tableName, (table) => {
table.unique(['summoner_puuid', 'match_id'])
})
}
public async down() {
this.schema.dropTable(this.tableName)
}
}

View file

@ -0,0 +1,28 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
export default class MatchTeams extends BaseSchema {
protected tableName = 'match_teams'
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('match_id', 15).index()
table.specificType('color', 'smallint').notNullable().index() // 100 ou 200
table.string('result', 6) // Win - Remake - Fail
table.specificType('barons', 'smallint').notNullable()
table.specificType('dragons', 'smallint').notNullable()
table.specificType('inhibitors', 'smallint').notNullable()
table.specificType('rift_heralds', 'smallint').notNullable()
table.specificType('towers', 'smallint').notNullable()
table.specificType('bans', 'smallint[]').nullable()
table.specificType('ban_orders', 'smallint[]').nullable()
})
}
public async down() {
this.schema.dropTable(this.tableName)
}
}

Some files were not shown because too many files have changed in this diff Show more