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) [![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> <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. 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. 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 ## Installation
Development environment requirements : Development environment requirements :
- [Node.js](https://nodejs.org/en/download/) >= 12.0.0 - [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) - [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 : Setting up your development environment on your local machine :
```bash ```bash
> git clone https://github.com/vkaelin/LeagueStats.git > git clone https://github.com/vkaelin/LeagueStats.git
> cd leaguestats/client > cd leaguestats/client
@ -33,11 +36,13 @@ Setting up your development environment on your local machine :
> cd leaguestats/server > cd leaguestats/server
> npm install > npm install
> cp .env.example .env # edit the values > 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 ## Useful commands
Running the app : Running the app :
```bash ```bash
> cd client > cd client
> npm run dev > npm run dev
@ -49,6 +54,7 @@ Running the app :
``` ```
Deploying the app : Deploying the app :
```bash ```bash
> cd client > cd client
> npm run build > npm run build
@ -77,7 +83,6 @@ Adapt — remix, transform, and build upon the material
### Under the following terms: ### Under the following terms:
NonCommercial — You may not use the material for commercial purposes. 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. 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" :data="allyTeam"
:all-players="[...allyTeam.players, ...enemyTeam.players]" :all-players="[...allyTeam.players, ...enemyTeam.players]"
:ally-team="true" :ally-team="true"
:ranks-loaded="data.ranksLoaded"
/> />
<div class="flex items-start justify-between px-3 py-2"> <div class="flex items-start justify-between px-3 py-2">
@ -23,6 +24,7 @@
:data="enemyTeam" :data="enemyTeam"
:all-players="[...allyTeam.players, ...enemyTeam.players]" :all-players="[...allyTeam.players, ...enemyTeam.players]"
:ally-team="false" :ally-team="false"
:ranks-loaded="data.ranksLoaded"
/> />
</div> </div>
<div v-else-if="data.status === 'loading' && detailsOpen"> <div v-else-if="data.status === 'loading' && detailsOpen">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@
:style="{ :style="{
backgroundImage: backgroundImage:
`${hover ? gradientHover : gradient}, `${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="borderColor"
class="relative w-full p-4 mx-2 mt-6 leading-none bg-center bg-cover border rounded-lg record-card" 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> <span :class="textColor" class="ml-0">{{ title }}</span>
</div> </div>
<img <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="[{'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" class="block w-16 h-16 mx-auto mt-10 transition duration-500 ease-in transform border-2 rounded-full"
alt="Champion Played" 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="text-sm">
<div class="mt-6"> <div class="mt-6">
<span <span
:class="record.result === 'Win' ? 'text-green-400' : 'text-red-400'" :class="record.result ? 'text-green-400' : 'text-red-400'"
>{{ record.result === 'Win' ? 'Won' : 'Lost' }}</span> >{{ record.result ? 'Won' : 'Lost' }}</span>
<span class="ml-1 font-semibold">{{ timeDifference(record.date) }}</span> <span class="ml-1 font-semibold">{{ timeDifference(record.date) }}</span>
</div> </div>
<div class="mt-2 text-gray-500"> <div class="mt-2 text-gray-500">
@ -41,7 +41,7 @@
</div> </div>
</div> </div>
<div class="mt-6 text-xs font-light text-right text-gray-200 opacity-25"> <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> <span v-else>{{ gameModes[record.gamemode].name }}</span>
</div> </div>
</div> </div>
@ -65,10 +65,6 @@ export default {
type: String, type: String,
required: true required: true
}, },
property: {
type: String,
required: true
},
record: { record: {
type: Object, type: Object,
required: true required: true

View file

@ -40,7 +40,7 @@ export function secToTime(seconds) {
* Sort an array of players by role * Sort an array of players by role
*/ */
export function sortTeamByRole(a, b) { 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) 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 { maps, gameModes } from '@/data/data.js'
import summonerSpells from '@/data/summonerSpells.json' import summonerSpells from '@/data/summonerSpells.json'
import store from '@/store'
const leaguesNumbers = { 'I': 1, 'II': 2, 'III': 3, 'IV': 4 } 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 * Return all the infos about a list of matches built with the Riot API data
* @param {Object} RiotData : all data from the Riot API * @param {Object} RiotData : all data from the Riot API
*/ */
export function createMatchData(matches) { export function createMatchData(matches) {
for (const match of matches) { for (const match of matches) {
match.firstSum = getSummonerLink(match.firstSum) // Runes
match.secondSum = getSummonerLink(match.secondSum) match.primaryRune = getPrimarRune(match.perks)
match.secondaryRune = getSecondaryRune(match.perks)
const date = new Date(match.date) const date = new Date(match.date)
const dateOptions = { day: '2-digit', month: '2-digit', year: 'numeric' } 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 * 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) { export function createRecordsData(recordsDto) {
records.maxTime.time = secToTime(records.maxTime.time) const records = recordsDto.reduce((acc, record) => {
records.maxGold.gold = records.maxGold.gold.toLocaleString() acc[record.what] = record
records.maxDmgTaken.dmgTaken = records.maxDmgTaken.dmgTaken.toLocaleString() return acc
records.maxDmgChamp.dmgChamp = records.maxDmgChamp.dmgChamp.toLocaleString() }, {})
records.maxDmgObj.dmgObj = records.maxDmgObj.dmgObj.toLocaleString()
records.maxKp.kp = `${records.maxKp.kp}%`
// New record fields records.game_duration.amount = secToTime(records.game_duration.amount)
if (records.maxLiving) { records.gold.amount = records.gold.amount.toLocaleString()
records.maxLiving.longestLiving = secToTime(records.maxLiving.longestLiving) records.damage_taken.amount = records.damage_taken.amount.toLocaleString()
records.maxHeal.heal = records.maxHeal.heal.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 return records
} }

View file

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

View file

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

View file

@ -8,47 +8,70 @@ export const state = {
} }
export const mutations = { export const mutations = {
MATCH_LOADING(state, gameId) { MATCH_LOADING(state, matchId) {
const alreadyIn = state.matches.find(m => m.gameId === gameId) const alreadyIn = state.matches.find(m => m.matchId === matchId)
if (!alreadyIn) { 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.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) const index = state.matches.findIndex(m => m.gameId === matchDetails.gameId)
Vue.set(state.matches, index, matchDetails) 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) 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 = { export const actions = {
async matchDetails({ commit, rootState }, gameId) { async matchDetails({ commit }, matchId) {
commit('MATCH_LOADING', gameId) commit('MATCH_LOADING', matchId)
const region = rootState.regionsList[rootState.settings.region] console.log('MATCH DETAILS STORE', matchId)
console.log('MATCH DETAILS STORE', gameId, region)
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('--- DETAILS INFOS ---')
console.log(resp.data) 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 the ranks of the players are not yet known
if (resp.data.matchDetails.blueTeam.players[0].rank === undefined) { if (!ranksLoaded) {
const ranks = await axios(({ url: 'match/details/ranks', data: { gameId, region }, method: 'POST' })).catch(() => { }) const ranks = await axios(({ url: 'match/details/ranks', data: { matchId }, method: 'POST' })).catch(() => { })
if (!ranks) return if (!ranks) return
console.log('--- RANK OF MATCH DETAILS ---') console.log('--- RANK OF MATCH DETAILS ---')
console.log(ranks.data) console.log(ranks.data)
commit('MATCH_RANKS_FOUND', { gameId, ...ranks.data }) commit('MATCH_RANKS_FOUND', { matchId, ranksByPlayer: ranks.data })
} }
} }
} }
export const getters = { 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, currentSeason: null,
matchList: [], matchList: [],
ranked: {}, ranked: {},
recentActivity: [],
seasons: [], seasons: [],
status: '', status: '',
}, },
@ -24,7 +25,7 @@ export const state = {
championsLoaded: false championsLoaded: false
}, },
records: { records: {
list: [], list: {},
recordsLoaded: false recordsLoaded: false
}, },
live: { live: {
@ -66,18 +67,21 @@ export const mutations = {
state.overview.matchesLoading = true state.overview.matchesLoading = true
}, },
MATCHES_FOUND(state, { newMatches, stats }) { MATCHES_FOUND(state, { newMatches, stats }) {
state.basic.recentActivity = stats.recentActivity
state.overview.matchesLoading = false state.overview.matchesLoading = false
state.overview.matches = [...state.overview.matches, ...newMatches] state.overview.matches = [...state.overview.matches, ...newMatches]
state.overview.matchIndex += newMatches.length state.overview.matchIndex += 10
state.overview.stats = stats state.overview.stats = stats
state.champions.championsLoaded = false state.champions.championsLoaded = false
state.records.recordsLoaded = false state.records.recordsLoaded = false
}, },
OVERVIEW_FOUND(state, infos) { OVERVIEW_FOUND(state, infos) {
state.basic.recentActivity = infos.stats.recentActivity
state.overview.matches = infos.matches state.overview.matches = infos.matches
state.overview.matchIndex = infos.matches.length state.overview.matchIndex = infos.matches.length
state.overview.stats = infos.stats state.overview.stats = infos.stats
state.overview.loaded = true state.overview.loaded = true
state.records.recordsLoaded = false
}, },
RECORDS_FOUND(state, { records }) { RECORDS_FOUND(state, { records }) {
state.records.list = records state.records.list = records
@ -87,6 +91,7 @@ export const mutations = {
state.basic.account = infos.account state.basic.account = infos.account
state.basic.matchList = infos.matchList state.basic.matchList = infos.matchList
state.basic.ranked = infos.ranked state.basic.ranked = infos.ranked
state.basic.recentActivity = infos.recentActivity
state.basic.seasons = infos.seasons.sort((a, b) => b - a) state.basic.seasons = infos.seasons.sort((a, b) => b - a)
state.basic.status = 'found' state.basic.status = 'found'
state.live.match = infos.current state.live.match = infos.current
@ -178,17 +183,15 @@ export const actions = {
async moreMatches({ commit, getters, rootState }) { async moreMatches({ commit, getters, rootState }) {
commit('MATCHES_LOADING') commit('MATCHES_LOADING')
const gameIds = getters.filteredMatchList const matchIds = getters.filteredMatchList
.slice(state.overview.matchIndex, state.overview.matchIndex + 10) .slice(state.overview.matchIndex, state.overview.matchIndex + 10)
.map(({ gameId }) => gameId)
const resp = await axios(({ const resp = await axios(({
url: 'match', url: 'match',
data: { data: {
puuid: state.basic.account.puuid, puuid: state.basic.account.puuid,
accountId: state.basic.account.accountId,
region: rootState.regionsList[rootState.settings.region], region: rootState.regionsList[rootState.settings.region],
gameIds matchIds
}, },
method: 'POST' method: 'POST'
})).catch(() => { }) })).catch(() => { })
@ -216,7 +219,7 @@ export const actions = {
const resp = await axios(({ url: 'summoner/records', data: { puuid: state.basic.account.puuid }, method: 'POST' })).catch(() => { }) const resp = await axios(({ url: 'summoner/records', data: { puuid: state.basic.account.puuid }, method: 'POST' })).catch(() => { })
console.log('---RECORDS---') console.log('---RECORDS---')
console.log(resp.data) console.log(resp.data)
const records = resp.data ? createRecordsData(resp.data) : {} const records = resp.data.length ? createRecordsData(resp.data) : {}
commit('RECORDS_FOUND', { records }) commit('RECORDS_FOUND', { records })
}, },

View file

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

View file

@ -1,6 +1,6 @@
<template> <template>
<div key="records"> <div key="records">
<template v-if="!recordsLoaded || (recordsLoaded && records.maxKda)"> <template v-if="!recordsLoaded || (recordsLoaded && records.assists)">
<div <div
class="relative pl-6 text-2xl text-blue-200 border-b-2 border-blue-800 category blue-900" class="relative pl-6 text-2xl text-blue-200 border-b-2 border-blue-800 category blue-900"
>Basics</div> >Basics</div>
@ -10,48 +10,42 @@
color="#63b3ed" color="#63b3ed"
text-color="text-blue-400" text-color="text-blue-400"
border-color="border-blue-400" border-color="border-blue-400"
property="kda" :record="records.kda"
:record="records.maxKda"
title="KDA" title="KDA"
/> />
<RecordCard <RecordCard
color="#68D391" color="#68D391"
text-color="text-green-400" text-color="text-green-400"
border-color="border-green-400" border-color="border-green-400"
property="kills" :record="records.kills"
:record="records.maxKills"
title="Kills" title="Kills"
/> />
<RecordCard <RecordCard
color="#9F7AEA" color="#9F7AEA"
text-color="text-purple-500" text-color="text-purple-500"
border-color="border-purple-500" border-color="border-purple-500"
property="assists" :record="records.assists"
:record="records.maxAssists"
title="Assists" title="Assists"
/> />
<RecordCard <RecordCard
color="#F56565" color="#F56565"
text-color="text-red-500" text-color="text-red-500"
border-color="border-red-500" border-color="border-red-500"
property="deaths" :record="records.deaths"
:record="records.maxDeaths"
title="Deaths" title="Deaths"
/> />
<RecordCard <RecordCard
color="#D69E2E" color="#D69E2E"
text-color="text-yellow-600" text-color="text-yellow-600"
border-color="border-yellow-600" border-color="border-yellow-600"
property="gold" :record="records.gold"
:record="records.maxGold"
title="Gold earned" title="Gold earned"
/> />
<RecordCard <RecordCard
color="#81E6D9" color="#81E6D9"
text-color="text-teal-300" text-color="text-teal-300"
border-color="border-teal-300" border-color="border-teal-300"
property="minions" :record="records.minions"
:record="records.maxMinions"
title="Minions killed" title="Minions killed"
/> />
</template> </template>
@ -81,24 +75,21 @@
color="#FC8181" color="#FC8181"
text-color="text-red-400" text-color="text-red-400"
border-color="border-red-400" border-color="border-red-400"
property="dmgChamp" :record="records.damage_dealt_champions"
:record="records.maxDmgChamp"
title="Damage champions" title="Damage champions"
/> />
<RecordCard <RecordCard
color="#D69E2E" color="#D69E2E"
text-color="text-yellow-400" text-color="text-yellow-400"
border-color="border-yellow-400" border-color="border-yellow-400"
property="dmgObj" :record="records.damage_dealt_objectives"
:record="records.maxDmgObj"
title="Damage objectives" title="Damage objectives"
/> />
<RecordCard <RecordCard
color="#FC8181" color="#FC8181"
text-color="text-red-400" text-color="text-red-400"
border-color="border-red-400" border-color="border-red-400"
property="dmgTaken" :record="records.damage_taken"
:record="records.maxDmgTaken"
title="Damage taken" title="Damage taken"
/> />
<RecordCard <RecordCard
@ -106,24 +97,21 @@
color="#D69E2E" color="#D69E2E"
text-color="text-yellow-400" text-color="text-yellow-400"
border-color="border-yellow-400" border-color="border-yellow-400"
property="towers" :record="records.turret_kills"
:record="records.maxTowers"
title="Towers" title="Towers"
/> />
<RecordCard <RecordCard
color="#68D391" color="#68D391"
text-color="text-green-400" text-color="text-green-400"
border-color="border-green-400" border-color="border-green-400"
property="kp" :record="records.kp"
:record="records.maxKp"
title="Kill participation" title="Kill participation"
/> />
<RecordCard <RecordCard
color="#D69E2E" color="#D69E2E"
text-color="text-yellow-400" text-color="text-yellow-400"
border-color="border-yellow-400" border-color="border-yellow-400"
property="vision" :record="records.vision_score"
:record="records.maxVision"
title="Vision score" title="Vision score"
/> />
</template> </template>
@ -153,35 +141,28 @@
color="#4299E1" color="#4299E1"
text-color="text-blue-500" text-color="text-blue-500"
border-color="border-blue-500" border-color="border-blue-500"
property="time" :record="records.game_duration"
:record="records.maxTime"
title="Longest game" title="Longest game"
/> />
<RecordCard <RecordCard
v-if="records.maxLiving"
color="#4299E1" color="#4299E1"
text-color="text-blue-500" text-color="text-blue-500"
border-color="border-blue-500" border-color="border-blue-500"
property="longestLiving" :record="records.time_spent_living"
:record="records.maxLiving"
title="Longest living" title="Longest living"
/> />
<RecordCard <RecordCard
v-if="records.maxCriticalStrike"
color="#D69E2E" color="#D69E2E"
text-color="text-yellow-400" text-color="text-yellow-400"
border-color="border-yellow-400" border-color="border-yellow-400"
property="criticalStrike" :record="records.critical_strike"
:record="records.maxCriticalStrike"
title="Critical Strike" title="Critical Strike"
/> />
<RecordCard <RecordCard
v-if="records.maxHeal"
color="#68D391" color="#68D391"
text-color="text-green-400" text-color="text-green-400"
border-color="border-green-400" border-color="border-green-400"
property="heal" :record="records.heal"
:record="records.maxHeal"
title="Heal" title="Heal"
/> />
</template> </template>
@ -204,47 +185,42 @@
</div> </div>
</template> </template>
</div> </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 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="flex flex-wrap -mx-2">
<template v-if="recordsLoaded"> <template v-if="recordsLoaded">
<RecordCard <RecordCard
color="#FEFCBF" color="#FEFCBF"
text-color="text-yellow-200" text-color="text-yellow-200"
border-color="border-yellow-200" border-color="border-yellow-200"
property="doubleKills" :record="records.double_kills"
:record="records.maxDouble"
title="Double kills" title="Double kills"
/> />
<RecordCard <RecordCard
color="#F6E05E" color="#F6E05E"
text-color="text-yellow-400" text-color="text-yellow-400"
border-color="border-yellow-400" border-color="border-yellow-400"
property="tripleKills" :record="records.triple_kills"
:record="records.maxTriple"
title="Triple kills" title="Triple kills"
/> />
<RecordCard <RecordCard
color="#D69E2E" color="#D69E2E"
text-color="text-yellow-600" text-color="text-yellow-600"
border-color="border-yellow-600" border-color="border-yellow-600"
property="quadraKills" :record="records.quadra_kills"
:record="records.maxQuadra"
title="Quadra kills" title="Quadra kills"
/> />
<RecordCard <RecordCard
color="#F56565" color="#F56565"
text-color="text-red-500" text-color="text-red-500"
border-color="border-red-500" border-color="border-red-500"
property="pentaKills" :record="records.penta_kills"
:record="records.maxPenta"
title="Penta kills" title="Penta kills"
/> />
<RecordCard <RecordCard
color="#63b3ed" color="#63b3ed"
text-color="text-blue-400" text-color="text-blue-400"
border-color="border-blue-400" border-color="border-blue-400"
property="killingSpree" :record="records.killing_spree"
:record="records.maxKillingSpree"
title="Killing Spree" title="Killing Spree"
/> />
</template> </template>
@ -268,7 +244,7 @@
</template> </template>
</div> </div>
</template> </template>
<template v-if="recordsLoaded && !records.maxKda"> <template v-if="recordsLoaded && !records.assists">
<div class="flex flex-col items-center mt-4"> <div class="flex flex-col items-center mt-4">
<div>No records have been found.</div> <div>No records have been found.</div>
<div>😕</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, "typescript": true,
"commands": [ "commands": [
"./commands", "./commands",
"@adonisjs/core/build/commands", "@adonisjs/core/build/commands/index.js",
"@zakodium/adonis-mongodb/lib/commands" "@adonisjs/repl/build/commands",
"@adonisjs/lucid/build/commands"
], ],
"exceptionHandlerNamespace": "App/Exceptions/Handler", "exceptionHandlerNamespace": "App/Exceptions/Handler",
"aliases": { "aliases": {
"App": "app", "App": "app",
"Contracts": "contracts",
"Config": "config", "Config": "config",
"Database": "database" "Database": "database",
"Contracts": "contracts"
}, },
"preloads": [ "preloads": [
"./start/routes", "./start/routes",
"./start/kernel" "./start/kernel",
{
"file": "./start/events",
"environment": [
"console",
"repl",
"web"
]
}
], ],
"providers": [ "providers": [
"./providers/AppProvider", "./providers/AppProvider",
"@adonisjs/core", "@adonisjs/core",
"@zakodium/adonis-mongodb", "@adonisjs/lucid",
"@adonisjs/redis" "@adonisjs/redis"
], ],
"metaFiles": [ "aceProviders": [
".env", "@adonisjs/repl"
".adonisrc.json"
] ]
} }

View file

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

View file

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

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 | Ace Commands
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This file is the entry point for running ace commands. For typescript | This file is the entry point for running ace commands.
| projects, the ace commands will fallback to the compiled code and
| hence this file has to be executable by node directly.
| |
*/ */
require('reflect-metadata') require('reflect-metadata')
require('source-map-support').install({ handleUncaughtExceptions: false }) 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) new Ignitor(__dirname)
.ace() .ace()
.handle(process.argv.slice(2)) .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 Redis from '@ioc:Adonis/Addons/Redis'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import RuneSerializer from 'App/Serializers/RuneSerializer'
import Jax from 'App/Services/Jax' import Jax from 'App/Services/Jax'
import RuneTransformer from 'App/Transformers/RuneTransformer'
export default class CDragonController { export default class CDragonController {
public async runes ({ response }: HttpContextContract) { public async runes({ response }: HttpContextContract) {
const cacheUrl = 'cdragon-runes' const cacheUrl = 'cdragon-runes'
const requestCached = await Redis.get(cacheUrl) const requestCached = await Redis.get(cacheUrl)
@ -16,8 +16,8 @@ export default class CDragonController {
const perkstyles = await Jax.CDragon.perkstyles() const perkstyles = await Jax.CDragon.perkstyles()
const runesData = { const runesData = {
perks: RuneTransformer.transformPerks(perks), perks: RuneSerializer.serializePerks(perks),
perkstyles: RuneTransformer.transformStyles(perkstyles.styles), perkstyles: RuneSerializer.serializeStyles(perkstyles.styles),
} }
await Redis.set(cacheUrl, JSON.stringify(runesData), 'EX', 36000) await Redis.set(cacheUrl, JSON.stringify(runesData), 'EX', 36000)

View file

@ -1,51 +1,23 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import DetailedMatch, { DetailedMatchModel } from 'App/Models/DetailedMatch' import Match from 'App/Models/Match'
import { ParticipantDetails } from 'App/Models/Match' import MatchPlayerRankParser from 'App/Parsers/MatchPlayerRankParser'
import Summoner from 'App/Models/Summoner' import DetailedMatchSerializer from 'App/Serializers/DetailedMatchSerializer'
import Jax from 'App/Services/Jax' import MatchPlayerRankSerializer from 'App/Serializers/MatchPlayerRankSerializer'
import MatchService from 'App/Services/MatchService' import MatchService from 'App/Services/MatchService'
import StatsService from 'App/Services/StatsService' import StatsService from 'App/Services/StatsService'
import SummonerService from 'App/Services/SummonerService'
import DetailedMatchTransformer from 'App/Transformers/DetailedMatchTransformer'
import DetailedMatchValidator from 'App/Validators/DetailedMatchValidator' import DetailedMatchValidator from 'App/Validators/DetailedMatchValidator'
import MatchesIndexValidator from 'App/Validators/MatchesIndexValidator' import MatchesIndexValidator from 'App/Validators/MatchesIndexValidator'
export default class MatchesController { export default class MatchesController {
/** /**
* Get the soloQ rank of all the players of the team * POST - Return data from matches searched by matchIds
* @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
* @param ctx * @param ctx
*/ */
public async index ({ request, response }: HttpContextContract) { public async index({ request, response }: HttpContextContract) {
console.log('More Matches Request') const { puuid, region, matchIds, season } = await request.validate(MatchesIndexValidator)
const { puuid, accountId, region, gameIds, season } = await request.validate(MatchesIndexValidator) const matches = await MatchService.getMatches(region, matchIds, puuid)
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()
const stats = await StatsService.getSummonerStats(puuid, season) const stats = await StatsService.getSummonerStats(puuid, season)
return response.json({ return response.json({
matches, matches,
stats, stats,
@ -56,26 +28,25 @@ export default class MatchesController {
* POST - Return details data for one specific match * POST - Return details data for one specific match
* @param ctx * @param ctx
*/ */
public async show ({ request, response }: HttpContextContract) { public async show({ request, response }: HttpContextContract) {
console.time('MatchDetails') console.time('MatchDetails')
const { gameId, region } = await request.validate(DetailedMatchValidator) const { matchId } = await request.validate(DetailedMatchValidator)
let matchDetails: DetailedMatchModel const match = await Match.query()
const alreadySaved = await DetailedMatch.findOne({ gameId, region }) .where('id', matchId)
.preload('teams')
.preload('players', (playersQuery) => {
playersQuery.preload('ranks')
})
.firstOrFail()
if (alreadySaved) { const { match: matchDetails, ranksLoaded } = DetailedMatchSerializer.serializeOneMatch(match)
console.log('MATCH DETAILS ALREADY SAVED')
matchDetails = alreadySaved
} else {
const match = await Jax.Match.get(gameId, region)
matchDetails = await DetailedMatchTransformer.transform(match)
await DetailedMatch.create(matchDetails)
}
console.timeEnd('MatchDetails') console.timeEnd('MatchDetails')
return response.json({ return response.json({
matchDetails, matchDetails,
ranksLoaded,
}) })
} }
@ -83,28 +54,14 @@ export default class MatchesController {
* POST - Return ranks of players for a specific game * POST - Return ranks of players for a specific game
* @param ctx * @param ctx
*/ */
public async showRanks ({ request, response }: HttpContextContract) { public async showRanks({ request, response }: HttpContextContract) {
console.time('Ranks') 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') console.timeEnd('Ranks')
return response.json(serializedRanks)
return response.json({
blueTeam: matchDetails.blueTeam.players,
redTeam: matchDetails.redTeam.players,
})
} }
} }

View file

@ -1,11 +1,13 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { getCurrentSeason } from 'App/helpers'
import Summoner from 'App/Models/Summoner' import Summoner from 'App/Models/Summoner'
import MatchRepository from 'App/Repositories/MatchRepository' 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 Jax from 'App/Services/Jax'
import MatchService from 'App/Services/MatchService' import MatchService from 'App/Services/MatchService'
import StatsService from 'App/Services/StatsService' import StatsService from 'App/Services/StatsService'
import SummonerService from 'App/Services/SummonerService' import SummonerService from 'App/Services/SummonerService'
import LiveMatchTransformer from 'App/Transformers/LiveMatchTransformer'
import SummonerBasicValidator from 'App/Validators/SummonerBasicValidator' import SummonerBasicValidator from 'App/Validators/SummonerBasicValidator'
import SummonerChampionValidator from 'App/Validators/SummonerChampionValidator' import SummonerChampionValidator from 'App/Validators/SummonerChampionValidator'
import SummonerLiveValidator from 'App/Validators/SummonerLiveValidator' import SummonerLiveValidator from 'App/Validators/SummonerLiveValidator'
@ -13,21 +15,8 @@ import SummonerOverviewValidator from 'App/Validators/SummonerOverviewValidator'
import SummonerRecordValidator from 'App/Validators/SummonerRecordValidator' import SummonerRecordValidator from 'App/Validators/SummonerRecordValidator'
export default class SummonersController { export default class SummonersController {
/** public async basic({ request, response }: HttpContextContract) {
* Get all played seasons for a summoner console.time('BASIC_REQUEST')
* @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')
const { summoner, region } = await request.validate(SummonerBasicValidator) const { summoner, region } = await request.validate(SummonerBasicValidator)
const finalJSON: any = {} const finalJSON: any = {}
@ -37,24 +26,20 @@ export default class SummonersController {
if (!account) { if (!account) {
return response.json(null) return response.json(null)
} }
account.region = region
finalJSON.account = account finalJSON.account = account
// Summoner in DB // Summoner in DB
let summonerDB = await Summoner.findOne({ puuid: account.puuid }) const summonerDB = await Summoner.firstOrCreate({ puuid: account.puuid })
if (!summonerDB) {
summonerDB = await Summoner.create({ puuid: account.puuid })
}
// Summoner names // Summoner names
finalJSON.account.names = SummonerService.getAllSummonerNames(account, summonerDB) finalJSON.account.names = await SummonerService.getAllSummonerNames(account, summonerDB)
// MATCH LIST // MATCH LIST
await MatchService.updateMatchList(account, summonerDB) finalJSON.matchList = await MatchService.updateMatchList(account, region, summonerDB)
finalJSON.matchList = summonerDB.matchList
// All seasons the summoner has played // 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 // CURRENT GAME
const currentGame = await Jax.Spectator.summonerID(account.id, region) const currentGame = await Jax.Spectator.summonerID(account.id, region)
@ -62,53 +47,44 @@ export default class SummonersController {
finalJSON.current = currentGame finalJSON.current = currentGame
// RANKED STATS // RANKED STATS
finalJSON.ranked = await SummonerService.getRanked(account, region) finalJSON.ranked = await SummonerService.getRanked(account.id, region)
// SAVE IN DB // RECENT ACTIVITY
await summonerDB.save() finalJSON.recentActivity = await StatsService.getRecentActivity(account.puuid)
} catch (error) { } catch (e) {
console.log('username not found') console.log(e)
console.log(error) console.timeEnd('BASIC_REQUEST')
return response.json(null) return response.json(null)
} }
console.timeEnd('all') console.timeEnd('BASIC_REQUEST')
return response.json(finalJSON) return response.json(finalJSON)
} }
/** public async overview({ request, response }: HttpContextContract) {
* POST: get overview view summoner data console.time('OVERVIEW_REQUEST')
* @param ctx const { puuid, region, season } = await request.validate(SummonerOverviewValidator)
*/
public async overview ({ request, response }: HttpContextContract) {
console.time('overview')
const { puuid, accountId, region, season } = await request.validate(SummonerOverviewValidator)
const finalJSON: any = {} const finalJSON: any = {}
// Summoner in DB // Summoner in DB
let summonerDB = await Summoner.findOne({ puuid: puuid }) const summonerDB = await Summoner.firstOrCreate({ puuid: puuid })
if (!summonerDB) {
summonerDB = await Summoner.create({ puuid: puuid })
}
// MATCHES BASIC // MATCHES BASIC
const gameIds = summonerDB.matchList!.slice(0) const matchlist = await summonerDB
.filter(m => { .related('matchList')
return season ? m.seasonMatch === season : true .query()
}) .select('matchId')
.slice(0, 10) .orderBy('matchId', 'desc')
.map(({ gameId }) => gameId) .limit(10)
finalJSON.matchesDetails = await MatchService.getMatches(puuid, accountId, region, gameIds, summonerDB) const matchIds = matchlist.map((m) => m.matchId)
finalJSON.matchesDetails = await MatchService.getMatches(region, matchIds, puuid)
// STATS
console.time('STATS') console.time('STATS')
finalJSON.stats = await StatsService.getSummonerStats(puuid, season) finalJSON.stats = await StatsService.getSummonerStats(puuid, season)
console.timeEnd('STATS') console.timeEnd('STATS')
// SAVE IN DB console.timeEnd('OVERVIEW_REQUEST')
await summonerDB.save()
console.timeEnd('overview')
return response.json(finalJSON) return response.json(finalJSON)
} }
@ -116,44 +92,57 @@ export default class SummonersController {
* POST: get champions view summoner data * POST: get champions view summoner data
* @param ctx * @param ctx
*/ */
public async champions ({ request, response }: HttpContextContract) { public async champions({ request, response }: HttpContextContract) {
console.time('championsRequest') console.time('championsRequest')
const { puuid, queue, season } = await request.validate(SummonerChampionValidator) const { puuid, queue, season } = await request.validate(SummonerChampionValidator)
const championStats = await MatchRepository.championCompleteStats(puuid, queue, season) const championStats = await MatchRepository.championCompleteStats(puuid, queue, season)
const championStatsSerialized = championStats.map((champion) => {
return {
...champion,
champion: BasicMatchSerializer.getChampion(champion.id),
}
})
console.timeEnd('championsRequest') console.timeEnd('championsRequest')
return response.json(championStats) return response.json(championStatsSerialized)
} }
/** /**
* POST: get records view summoner data * POST: get records view summoner data
* @param ctx * @param ctx
*/ */
public async records ({ request, response }: HttpContextContract) { public async records({ request, response }: HttpContextContract) {
console.time('recordsRequest') console.time('recordsRequest')
const { puuid, season } = await request.validate(SummonerRecordValidator) 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') console.timeEnd('recordsRequest')
return response.json(records) return response.json(recordsSerialized)
} }
/** /**
* POST - Return live match detail * POST - Return live match detail
* @param ctx * @param ctx
*/ */
public async liveMatchDetails ({ request, response }: HttpContextContract) { public async liveMatchDetails({ request, response }: HttpContextContract) {
console.time('liveMatchDetails') console.time('liveMatchDetails')
const { id, region } = await request.validate(SummonerLiveValidator) const { id, region } = await request.validate(SummonerLiveValidator)
// CURRENT GAME // CURRENT GAME
let currentGame = await Jax.Spectator.summonerID(id, region) const currentGame = await Jax.Spectator.summonerID(id, region)
if (!currentGame) { if (!currentGame) {
return response.json(null) return response.json(null)
} }
currentGame = await LiveMatchTransformer.transform(currentGame, { region }) const currentGameSerialized = await LiveMatchSerializer.serializeOneMatch(currentGame, region)
console.timeEnd('liveMatchDetails') 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' import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler'
export default class ExceptionHandler extends HttpExceptionHandler { export default class ExceptionHandler extends HttpExceptionHandler {
constructor () { constructor() {
super(Logger) 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 { export default class Match extends BaseModel {
account_id: string, public static selfAssignPrimaryKey = true
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 interface ParticipantDetails { @column({ isPrimary: true })
name: string, public id: 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
}
export interface Champion<T = number, U = string> { @column()
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
public gameId: number public gameId: number
public result: string
public allyTeam: ParticipantBasic[] @column()
public enemyTeam: ParticipantBasic[]
public map: number public map: number
@column()
public gamemode: number public gamemode: number
@column()
public date: number public date: number
@column()
public region: string public region: string
@column()
public result: number
@column()
public season: number public season: number
public time: number
public name: string @column()
public summonerId: string public gameDuration: number
public champion: Champion
public role: string @hasMany(() => MatchTeam)
public primaryRune: string public teams: HasMany<typeof MatchTeam>
public secondaryRune: string
public level: number @hasMany(() => MatchPlayer)
public items: Item[] public players: HasMany<typeof MatchPlayer>
public firstSum: number
public secondSum: number
public stats: Stats
} }

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 { DateTime } from 'luxon'
import { MatchReferenceDto } from 'App/Services/Jax/src/Endpoints/MatchlistEndpoint' 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 { export default class Summoner extends BaseModel {
puuid: string, public static selfAssignPrimaryKey = true
matchList?: MatchReferenceDto[],
names?: SummonerNames[]
}
interface SummonerNames {
name: string,
date: Date
}
export default class Summoner extends Model implements SummonerModel {
public static collectionName = 'summoners'
@column({ isPrimary: true })
public puuid: string 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 Database from '@ioc:Adonis/Lucid/Database'
import { Collection } from 'mongodb'
class MatchRepository { 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 () { private readonly GLOBAL_FILTERS = `
this.getCollection() 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
} }
/** public async globalStats(puuid: string) {
* Basic matchParams used in a lot of requests const query = `
* @param puuid of the summoner SELECT
*/ SUM(match_players.kills) as kills,
private matchParams (puuid: string, season?: number) { SUM(match_players.deaths) as deaths,
return { SUM(match_players.assists) as assists,
summoner_puuid: puuid, SUM(match_players.minions) as minions,
result: { $not: { $eq: 'Remake' } }, SUM(matches.game_duration) as time,
gamemode: { $nin: [800, 810, 820, 830, 840, 850, 2000, 2010, 2020] }, SUM(match_players.vision_score) as vision,
season: season ? season : { $exists: true }, 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]
} }
/** public async gamemodeStats(puuid: string) {
* Build the aggregate mongo query const query = `
* @param puuid SELECT
* @param matchParams matches.gamemode as id,
* @param intermediateSteps COUNT(match_players.id) as count,
* @param groupId SUM(match_players.win) as wins,
* @param groupParams SUM(match_players.loss) as losses
* @param finalSteps FROM
*/ match_players
private async aggregate ( ${this.JOIN_MATCHES}
puuid: string, WHERE
matchParams: object, ${this.GLOBAL_FILTERS}
intermediateSteps: any[], GROUP BY
groupId: any, matches.gamemode
groupParams: object, ORDER BY
finalSteps: any[], count DESC
season?: number, `
) { const { rows } = await Database.rawQuery(query, { puuid })
return this.collection.aggregate([ return rows
{
$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 roleStats(puuid: string) {
* Get MongoDB matches collection const query = `
*/ SELECT
public async getCollection () { match_players.team_position as role,
if (!this.collection) { COUNT(match_players.id) as count,
this.collection = await mongodb.connection().collection('matches') 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
} }
/** public async championStats(puuid: string, limit: number) {
* Get Summoner's statistics for the N most played champions const query = `
* @param puuid of the summoner SELECT
* @param limit number of champions to fetch match_players.champion_id as id,
* @param season SUM(match_players.assists) as assists,
*/ SUM(match_players.deaths) as deaths,
public async championStats (puuid: string, limit = 5, season?: number) { SUM(match_players.kills) as kills,
const groupParams = { COUNT(match_players.id) as count,
champion: { $first: '$champion' }, SUM(match_players.win) as wins,
kills: { $sum: '$stats.kills' }, SUM(match_players.loss) as losses
deaths: { $sum: '$stats.deaths' }, FROM
assists: { $sum: '$stats.assists' }, 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 } }, public async championClassStats(puuid: string) {
{ $limit: limit }, 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)
}
/** const query = fields
* Get Summoner's statistics for all played champion classes .map((field) => {
* @param puuid of the summoner return `
* @param season (SELECT
*/ '${field}' AS what,
public async championClassStats (puuid: string, season?: number) { ${field} AS amount,
const groupId = { '$arrayElemAt': ['$champion.roles', 0] } match_players.win as result,
return this.aggregate(puuid, {}, [], groupId, {}, [], season) 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 ')
/** const { rows } = await Database.rawQuery(query, { puuid })
* Get Summoner's complete statistics for the all played champs return rows
* @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)
} }
} }

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' import { PerkDTO, PerkStyleDTO } from 'App/Services/Jax/src/Endpoints/CDragonEndpoint'
class RuneTransformer { class RuneSerializer {
public transformPerks (perks: PerkDTO[]) { public serializePerks(perks: PerkDTO[]) {
return perks.reduce((acc, perk) => { return perks.reduce((acc, perk) => {
acc[perk.id] = { acc[perk.id] = {
name: perk.name, name: perk.name,
@ -12,18 +12,16 @@ class RuneTransformer {
}, {}) }, {})
} }
public transformStyles (styles: PerkStyleDTO[]) { public serializeStyles(styles: PerkStyleDTO[]) {
return styles.reduce((acc, style) => { return styles.reduce((acc, style) => {
acc[style.id] = { acc[style.id] = {
name: style.name, name: style.name,
icon: style.iconPath, icon: style.iconPath,
slots: style.slots slots: style.slots.filter((s) => s.type !== 'kStatMod').map((s) => s.perks),
.filter(s => s.type !== 'kStatMod')
.map(s => s.perks),
} }
return acc 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' import Env from '@ioc:Adonis/Core/Env'
export interface JaxConfig { export interface JaxConfig {
key: string, key: string
region: string, region: string
requestOptions: JaxConfigRequestOptions requestOptions: JaxConfigRequestOptions
} }
export interface JaxConfigRequestOptions { export interface JaxConfigRequestOptions {
retriesBeforeAbort: number, retriesBeforeAbort: number
delayBeforeRetry: number, delayBeforeRetry: number
} }
export const JAX_CONFIG: JaxConfig = { export const JAX_CONFIG: JaxConfig = {
key: Env.get('API_KEY') as string, key: Env.get('RIOT_API_KEY') as string,
region: 'euw1', region: 'euw1',
requestOptions: { requestOptions: {
retriesBeforeAbort: 3, retriesBeforeAbort: 3,

View file

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

View file

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

View file

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

View file

@ -1,231 +1,216 @@
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter' // import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
import { getRiotRegion } from 'App/helpers'
import RiotRateLimiter from 'riot-ratelimiter' import RiotRateLimiter from 'riot-ratelimiter'
import { JaxConfig } from '../../JaxConfig' import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest' import JaxRequest from '../JaxRequest'
export interface MatchDto { export interface MatchDto {
gameId: number, metadata: MetadataDto
participantIdentities: ParticipantIdentityDto[], info: InfoDto
queueId: number, }
gameType: string,
gameDuration: number, export interface MetadataDto {
teams: TeamStatsDto[], 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 platformId: string
gameCreation: number, queueId: number
seasonId: number, teams: TeamDto[]
gameVersion: string, tournamentCode?: 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,
} }
export interface ParticipantDto { export interface ParticipantDto {
participantId: number, assists: number
championId: number, baronKills: number
runes: RuneDto[], bountyLevel: number
stats: ParticipantStatsDto, champExperience: number
teamId: number, champLevel: number
timeline: ParticipantTimelineDto, championId: number
spell1Id: number, championName: string
spell2Id: number, championTransform: ChampionTransformDto
highestAchievedSeasonTier?: consumablesPurchased: number
'CHALLENGER' | 'MASTER' | 'DIAMOND' | 'PLATINUM' | 'GOLD' | 'SILVER' | 'BRONZE' | 'UNRANKED', damageDealtToObjectives: number
masteries: MasteryDto[] damageDealtToTurrets: number
} damageSelfMitigated: number
deaths: number
export interface RuneDto { detectorWardsPlaced: number
runeId: number, doubleKills: number
rank: number, dragonKills: 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,
firstBloodAssist: boolean firstBloodAssist: boolean
nodeCaptureAssist: number, firstBloodKill: boolean
assists: number, firstTowerAssist: boolean
teamObjective: number, firstTowerKill: boolean
altarsNeutralized: number, gameEndedInEarlySurrender: boolean
goldSpent: number, gameEndedInSurrender: boolean
damageDealtToTurrets: number, goldEarned: number
altarsCaptured: number, goldSpent: number
win: boolean, individualPosition: 'Invalid' | TeamPositionDto // TODO
totalHeal: number, inhibitorKills: number
unrealKills: number, item0: number
visionScore: number, item1: number
physicalDamageDealt: number, item2: number
firstBloodKill: boolean, item3: number
longestTimeSpentLiving: number, item4: number
killingSprees: number, item5: number
sightWardsBoughtInGame: number, item6: number
trueDamageDealtToChampions: number, itemsPurchased: number
neutralMinionsKilledEnemyJungle: number, killingSprees: number
doubleKills: number, kills: number
trueDamageDealt: number, lane: LaneDto // TODO
quadraKills: number, largestCriticalStrike: number
item4: number, largestKillingSpree: number
item3: number, largestMultiKill: number
item6: number, longestTimeSpentLiving: number
item5: number, magicDamageDealt: number
playerScore0: number, magicDamageDealtToChampions: number
playerScore1: number, magicDamageTaken: number
playerScore2: number, neutralMinionsKilled: number
playerScore3: number, nexusKills: number
playerScore4: number, objectivesStolen: number
playerScore5: number, objectivesStolenAssists: number
playerScore6: number, participantId: number
playerScore7: number, pentaKills: number
playerScore8: number, perks: PerksDto
playerScore9: number, physicalDamageDealt: number
perk0: number, physicalDamageDealtToChampions: number
perk0Var1: number, physicalDamageTaken: number
perk0Var2: number, profileIcon: number
perk0Var3: number, puuid: string
perk1: number, quadraKills: number
perk1Var1: number, riotIdName: string
perk1Var2: number, riotIdTagline: string
perk1Var3: number, role: RoleDto // TODO
perk2: number, sightWardsBoughtInGame: number
perk2Var1: number, spell1Casts: number
perk2Var2: number, spell2Casts: number
perk2Var3: number, spell3Casts: number
perk3: number, spell4Casts: number
perk3Var1: number, summoner1Casts: number
perk3Var2: number, summoner1Id: number
perk3Var3: number, summoner2Casts: number
perk4: number, summoner2Id: number
perk4Var1: number, summonerId: string
perk4Var2: number, summonerLevel: number
perk4Var3: number, summonerName: string
perk5: number, teamEarlySurrendered: boolean
perk5Var1: number, teamId: number
perk5Var2: number, teamPosition: TeamPositionDto // TODO
perk5Var3: number, timeCCingOthers: number
perkPrimaryStyle: number, timePlayed: number
perkSubStyle: number, totalDamageDealt: number
statPerk0: number, totalDamageDealtToChampions: number
statPerk1: number, totalDamageShieldedOnTeammates: number
statPerk2: 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 { export enum ChampionTransformDto {
participantId: number, None,
csDiffPerMinDeltas: { [index: string]: number }, Slayer,
damageTakenPerMinDeltas: { [index: string]: number }, Assasin,
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 MasteryDto { export type LaneDto = 'TOP' | 'JUNGLE' | 'MIDDLE' | 'BOTTOM'
rank: number,
masteryId: number, 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 { export default class MatchEndpoint {
private config: JaxConfig private config: JaxConfig
private limiter: RiotRateLimiter private limiter: RiotRateLimiter
constructor (config: JaxConfig, limiter: RiotRateLimiter) { constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config this.config = config
this.limiter = limiter this.limiter = limiter
this.get = this.get.bind(this) 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( return new JaxRequest(
region, getRiotRegion(region),
this.config, this.config,
`match/v4/matches/${matchID}`, `match/v5/matches/${matchID}`,
this.limiter, this.limiter,
1500 1500
).execute() ).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 { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
import { getRiotRegion } from 'App/helpers'
import RiotRateLimiter from 'riot-ratelimiter' import RiotRateLimiter from 'riot-ratelimiter'
import { JaxConfig } from '../../JaxConfig' import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest' import JaxRequest from '../JaxRequest'
export interface MatchlistDto { // export interface MatchlistDto {
startIndex: number, // startIndex: number,
totalGames: number, // totalGames: number,
endIndex: number, // endIndex: number,
matches: MatchReferenceDto[] // matches: MatchReferenceDto[]
} // }
export interface MatchReferenceDto { // export interface MatchReferenceDto {
gameId: number, // gameId: number,
role: string, // role: string,
season: number, // season: number,
platformId: string, // platformId: string,
champion: number, // champion: number,
queue: number, // queue: number,
lane: string, // lane: string,
timestamp: number, // timestamp: number,
seasonMatch?: number // seasonMatch?: number
} // }
/**
*
* ===============================================
* V5
* ===============================================
*
*/
export type MatchlistDto = string[]
export default class MatchlistEndpoint { export default class MatchlistEndpoint {
private config: JaxConfig private config: JaxConfig
private limiter: RiotRateLimiter private limiter: RiotRateLimiter
constructor (config: JaxConfig, limiter: RiotRateLimiter) { constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config this.config = config
this.limiter = limiter 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( return new JaxRequest(
region, getRiotRegion(region),
this.config, this.config,
`match/v4/matchlists/by-account/${accountID}?beginIndex=${beginIndex}`, `match/v5/matches/by-puuid/${puuid}/ids?start=${beginIndex}&count=${count}`,
this.limiter, this.limiter,
0 0
).execute() ).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 '@fightmegg/riot-rate-limiter'
import RiotRateLimiter from 'riot-ratelimiter' import RiotRateLimiter from 'riot-ratelimiter'
import { LeagueEntriesByQueue } from 'App/Services/SummonerService'
import { JaxConfig } from '../../JaxConfig' import { JaxConfig } from '../../JaxConfig'
import JaxRequest from '../JaxRequest' import JaxRequest from '../JaxRequest'
export interface CurrentGameInfo { export interface CurrentGameInfoDTO {
gameId: number, gameId: number
gameType: string gameType: string
gameStartTime: number, gameStartTime: number
mapId: number, mapId: number
gameLength: number, gameLength: number
platformId: string, platformId: string
gameMode: string, gameMode: string
bannedChampions: BannedChampion[], bannedChampions: BannedChampionDTO[]
gameQueueConfigId: number, gameQueueConfigId: number
observers: Observer, observers: ObserverDTO
participants: CurrentGameParticipant[], participants: CurrentGameParticipantDTO[]
} }
export interface BannedChampion { export interface BannedChampionDTO {
pickTurn: number, pickTurn: number
championId: number, championId: number
teamId: number, teamId: number
} }
export interface Observer { export interface ObserverDTO {
encryptionKey: string, encryptionKey: string
} }
export interface CurrentGameParticipant { export interface CurrentGameParticipantDTO {
championId: number, championId: number
perks: Perks, perks: PerksDTO
profileIconId: number, profileIconId: number
bot: boolean, bot: boolean
teamId: number, teamId: number
summonerName: string, summonerName: string
summonerId: string, summonerId: string
spell1Id: number, spell1Id: number
spell2Id: number, spell2Id: number
gameCustomizationObjects: GameCustomizationObject[], gameCustomizationObjects: GameCustomizationObjectDTO[]
// Custom types from here
role?: string,
runes?: { primaryRune: string, secondaryRune: string } | {}
level?: number,
rank?: LeagueEntriesByQueue
} }
export interface Perks { export interface PerksDTO {
perkIds: number[] perkIds: number[]
perkStyle: number, perkStyle: number
perkSubStyle: number perkSubStyle: number
} }
export interface GameCustomizationObject { export interface GameCustomizationObjectDTO {
category: string, category: string
content: string content: string
} }
@ -61,12 +55,12 @@ export default class SpectatorEndpoint {
private config: JaxConfig private config: JaxConfig
private limiter: RiotRateLimiter private limiter: RiotRateLimiter
constructor (config: JaxConfig, limiter: RiotRateLimiter) { constructor(config: JaxConfig, limiter: RiotRateLimiter) {
this.config = config this.config = config
this.limiter = limiter this.limiter = limiter
} }
public summonerID (summonerID: string, region: string) { public summonerID(summonerID: string, region: string): Promise<CurrentGameInfoDTO | undefined> {
return new JaxRequest( return new JaxRequest(
region, region,
this.config, this.config,

View file

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

View file

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

View file

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

View file

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

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

View file

@ -1,20 +1,28 @@
import MatchRepository from 'App/Repositories/MatchRepository'
import { sortTeamByRole } from 'App/helpers' 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 { 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') console.time('GLOBAL')
const globalStats = await MatchRepository.globalStats(puuid, season) const globalStats = await MatchRepository.globalStats(puuid)
console.timeEnd('GLOBAL') console.timeEnd('GLOBAL')
console.time('GAMEMODE') console.time('GAMEMODE')
const gamemodeStats = await MatchRepository.gamemodeStats(puuid, season) const gamemodeStats = await MatchRepository.gamemodeStats(puuid)
console.timeEnd('GAMEMODE') console.timeEnd('GAMEMODE')
console.time('ROLE') console.time('ROLE')
const roleStats = await MatchRepository.roleStats(puuid, season) const roleStats = await MatchRepository.roleStats(puuid)
// Check if all roles are in the array // 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) { 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({ roleStats.push({
count: 0, count: 0,
losses: 0, losses: 0,
@ -25,22 +33,33 @@ class StatsService {
} }
console.timeEnd('ROLE') console.timeEnd('ROLE')
console.time('CHAMPION') 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.timeEnd('CHAMPION')
console.time('CHAMPION-CLASS') 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.timeEnd('CHAMPION-CLASS')
console.time('MATES') console.time('MATES')
const mates = await MatchRepository.mates(puuid, season) const mates = await MatchRepository.mates(puuid)
console.timeEnd('MATES') console.timeEnd('MATES')
console.time('RECENT_ACTIVITY')
const recentActivity = await MatchRepository.recentActivity(puuid)
console.timeEnd('RECENT_ACTIVITY')
return { return {
global: globalStats[0], global: globalStats,
league: gamemodeStats, league: gamemodeStats,
role: roleStats.sort(sortTeamByRole), role: roleStats.sort(sortTeamByRole),
champion: championStats,
class: championClassStats, class: championClassStats,
mates, mates,
champion: championStats, recentActivity,
} }
} }
} }

View file

@ -1,36 +1,48 @@
import Jax from './Jax' import Jax from './Jax'
import { SummonerDTO } from 'App/Services/Jax/src/Endpoints/SummonerEndpoint' import { SummonerDTO } from 'App/Services/Jax/src/Endpoints/SummonerEndpoint'
import { LeagueEntryDTO } from './Jax/src/Endpoints/LeagueEndpoint' 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 { export interface LeagueEntriesByQueue {
soloQ?: LeagueEntryByQueue, soloQ?: LeagueEntryByQueue
flex5v5?: LeagueEntryByQueue flex5v5?: LeagueEntryByQueue
} }
export interface LeagueEntryByQueue extends LeagueEntryDTO { export interface LeagueEntryByQueue extends LeagueEntryDTO {
fullRank: string, fullRank: string
winrate: string, winrate: string
shortName: string | number shortName: string | number
} }
class SummonerService { class SummonerService {
private uniqueLeagues = ['CHALLENGER', 'GRANDMASTER', 'MASTER'] private readonly uniqueLeagues = ['CHALLENGER', 'GRANDMASTER', 'MASTER']
private leaguesNumbers = { 'I': 1, 'II': 2, 'III': 3, 'IV': 4 } 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 * Helper to transform League Data from the Riot API
* @param league raw data of the league from 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) { if (!league) {
return null return null
} }
const fullRank = this.uniqueLeagues.includes(league.tier) ? league.tier : `${league.tier} ${league.rank}` const fullRank = this.uniqueLeagues.includes(league.tier)
const winrate = +(league.wins * 100 / (league.wins + league.losses)).toFixed(1) + '%' ? league.tier
const shortName = this.uniqueLeagues.includes(league.tier) ? : `${league.tier} ${league.rank}`
league.leaguePoints : const winrate = this.getWinrate(league.wins, league.losses)
league.tier[0] + this.leaguesNumbers[league.rank] const shortName = this.uniqueLeagues.includes(league.tier)
? league.leaguePoints
: league.tier[0] + this.leaguesNumbers[league.rank]
return { return {
...league, ...league,
@ -45,7 +57,7 @@ class SummonerService {
* @param summonerName * @param summonerName
* @param region * @param region
*/ */
public async getAccount (summonerName: string, region: string) { public async getAccount(summonerName: string, region: string) {
const name = summonerName.toLowerCase() const name = summonerName.toLowerCase()
const account = await Jax.Summoner.summonerName(name, region) const account = await Jax.Summoner.summonerName(name, region)
return account return account
@ -56,18 +68,11 @@ class SummonerService {
* @param account of the summoner * @param account of the summoner
* @param summonerDB summoner in the database * @param summonerDB summoner in the database
*/ */
public getAllSummonerNames (account: SummonerDTO, summonerDB: SummonerModel) { public async getAllSummonerNames(account: SummonerDTO, summonerDB: Summoner) {
const names = summonerDB.names ? summonerDB.names : [] await summonerDB.related('names').firstOrCreate({
if (!names.find(n => n.name === account.name)) {
names.push({
name: account.name, name: account.name,
date: new Date(),
}) })
summonerDB.names = names return summonerDB.related('names').query().select('name', 'created_at')
}
return names
} }
/** /**
@ -75,15 +80,16 @@ class SummonerService {
* @param account * @param account
* @param region * @param region
*/ */
public async getRanked (account: SummonerDTO, region: string): Promise<LeagueEntriesByQueue> { public async getRanked(summonerId: string, region: string): Promise<LeagueEntriesByQueue> {
const ranked = await Jax.League.summonerID(account.id, region) const ranked = await Jax.League.summonerID(summonerId, region)
const result:LeagueEntriesByQueue = {} const result: LeagueEntriesByQueue = {}
if (ranked && ranked.length) { if (ranked && ranked.length) {
result.soloQ = this.getleagueData(ranked.find(e => e.queueType === 'RANKED_SOLO_5x5')) || undefined result.soloQ =
result.flex5v5 = this.getleagueData(ranked.find(e => e.queueType === 'RANKED_FLEX_SR')) || undefined 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 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 { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class DetailedMatchValidator { 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: * For example:
* 1. The username must be of data type string. But then also, it should * 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({ public schema = schema.create({
gameId: schema.number(), matchId: schema.string(),
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 `(.)` * Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all * for targeting nested fields and array expressions `(*)` for targeting all
@ -48,6 +36,7 @@ export default class DetailedMatchValidator {
* 'profile.username.required': 'Username is required', * 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers' * 'scores.*.number': 'Define scores as valid numbers'
* } * }
*
*/ */
public messages = {} public messages = {}
} }

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator' import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class MatchesIndexValidator { 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: * For example:
* 1. The username must be of data type string. But then also, it should * 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({ public schema = schema.create({
puuid: schema.string(), puuid: schema.string(),
accountId: schema.string(),
region: schema.string(), region: schema.string(),
gameIds: schema.array().members( matchIds: schema.array().members(schema.string()),
schema.number()
),
season: schema.number.optional(), 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 `(.)` * Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all * for targeting nested fields and array expressions `(*)` for targeting all
@ -53,6 +39,7 @@ export default class MatchesIndexValidator {
* 'profile.username.required': 'Username is required', * 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers' * 'scores.*.number': 'Define scores as valid numbers'
* } * }
*
*/ */
public messages = {} public messages = {}
} }

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { rules, schema } from '@ioc:Adonis/Core/Validator' import { rules, schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerBasicValidator { 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: * For example:
* 1. The username must be of data type string. But then also, it should * 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({ public schema = schema.create({
summoner: schema.string({}, [ summoner: schema.string({}, [rules.regex(/^[0-9\p{L} _\.]+$/u)]),
rules.regex(/^[0-9\p{L} _\.]+$/u),
]),
region: schema.string(), 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 `(.)` * Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all * for targeting nested fields and array expressions `(*)` for targeting all
@ -50,6 +37,7 @@ export default class SummonerBasicValidator {
* 'profile.username.required': 'Username is required', * 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers' * 'scores.*.number': 'Define scores as valid numbers'
* } * }
*
*/ */
public messages = {} public messages = {}
} }

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator' import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerChampionValidator { 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: * For example:
* 1. The username must be of data type string. But then also, it should * 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(), 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 `(.)` * Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all * for targeting nested fields and array expressions `(*)` for targeting all
@ -49,6 +38,7 @@ export default class SummonerChampionValidator {
* 'profile.username.required': 'Username is required', * 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers' * 'scores.*.number': 'Define scores as valid numbers'
* } * }
*
*/ */
public messages = {} public messages = {}
} }

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator' import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerLiveValidator { 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: * For example:
* 1. The username must be of data type string. But then also, it should * 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(), 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 `(.)` * Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all * for targeting nested fields and array expressions `(*)` for targeting all
@ -48,6 +37,7 @@ export default class SummonerLiveValidator {
* 'profile.username.required': 'Username is required', * 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers' * 'scores.*.number': 'Define scores as valid numbers'
* } * }
*
*/ */
public messages = {} public messages = {}
} }

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator' import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerOverviewValidator { 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: * For example:
* 1. The username must be of data type string. But then also, it should * 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({ public schema = schema.create({
puuid: schema.string(), puuid: schema.string(),
accountId: schema.string(),
region: schema.string(), region: schema.string(),
season: schema.number.optional(), 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 `(.)` * Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all * for targeting nested fields and array expressions `(*)` for targeting all
@ -50,6 +38,7 @@ export default class SummonerOverviewValidator {
* 'profile.username.required': 'Username is required', * 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers' * 'scores.*.number': 'Define scores as valid numbers'
* } * }
*
*/ */
public messages = {} public messages = {}
} }

View file

@ -1,12 +1,11 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator' import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SummonerRecordValidator { 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: * For example:
* 1. The username must be of data type string. But then also, it should * 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(), 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 `(.)` * Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all * for targeting nested fields and array expressions `(*)` for targeting all
@ -48,6 +37,7 @@ export default class SummonerRecordValidator {
* 'profile.username.required': 'Username is required', * 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers' * 'scores.*.number': 'Define scores as valid numbers'
* } * }
*
*/ */
public messages = {} 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 * 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 = { export const seasons = {
0: 9, 0: 9,
1578628800000: 10, 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] export const supportItems = [3850, 3851, 3853, 3854, 3855, 3857, 3858, 3859, 3860, 3862, 3863, 3864]
/** /**
* Get season number for a match * Get season number for a match
* @param timestamp * @param timestamp
*/ */
export function getSeasonNumber (timestamp: number): number { export function getSeasonNumber(timestamp: number): number {
const arrSeasons = Object.keys(seasons).map(k => Number(k)) const arrSeasons = Object.keys(seasons).map((k) => Number(k))
arrSeasons.push(timestamp) arrSeasons.push(timestamp)
arrSeasons.sort() arrSeasons.sort()
const indexSeason = arrSeasons.indexOf(timestamp) - 1 const indexSeason = arrSeasons.indexOf(timestamp) - 1
return seasons[arrSeasons[indexSeason]] 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 * Sort array of Players by roles according to a specific order
* @param a first player * @param a first player
* @param b second player * @param b second player
*/ */
export function sortTeamByRole (a: ParticipantBasic | ParticipantDetails, b: ParticipantBasic | ParticipantDetails) { export function sortTeamByRole<T extends SortableByRole>(a: T, b: T) {
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT'] const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
return sortingArr.indexOf(a.role) - sortingArr.indexOf(b.role) 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 | 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 | 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. | make response in JSON regardless of the existence of the `Accept` header.
| |
| By setting `forceContentNegotiationToJSON = true`, you negotiate with the | By setting `forceContentNegotiationTo = 'application/json'`, you negotiate
| server in advance to always return JSON without relying on the client | with the server in advance to always return JSON without relying on the
| to set the header explicitly. | client to set the header explicitly.
| |
*/ */
forceContentNegotiationTo: 'application/json', 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', ''), password: Env.get('REDIS_PASSWORD', ''),
db: 0, db: 0,
keyPrefix: '', 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. | an instance of the the UserModel only.
| |
*/ */
interface EventsList { interface EventsList {}
}
} }

View file

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

View file

@ -7,6 +7,6 @@
declare module '@ioc:Adonis/Addons/Redis' { declare module '@ioc:Adonis/Addons/Redis' {
interface RedisConnectionsList { 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