mirror of
https://github.com/vkaelin/LeagueStats.git
synced 2026-03-25 12:57:28 +00:00
Merge branch 'V2'
This commit is contained in:
commit
11fb369051
114 changed files with 31057 additions and 9074 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
.vscode
|
||||
.DS_STORE
|
||||
1
LeagueStatsSQL.diagram
Normal file
1
LeagueStatsSQL.diagram
Normal file
File diff suppressed because one or more lines are too long
13
README.md
13
README.md
|
|
@ -3,7 +3,6 @@
|
|||
[](https://app.netlify.com/sites/leaguestats-gg/deploys)
|
||||
<a href="https://discord.gg/RjBzjfk"><img src="https://img.shields.io/badge/Discord-join%20chat-738bd7.svg" alt="LeagueStats.gg official Discord"></a>
|
||||
|
||||
|
||||
The goal of [leaguestats.gg](https://leaguestats.gg) is to provide global complete data for all League of Legends summoners.
|
||||
Here is an [example](https://leaguestats.gg/summoner/euw/SammyWinchester) of stats for some summoner.
|
||||
|
||||
|
|
@ -18,11 +17,15 @@ Here is an [example](https://leaguestats.gg/summoner/euw/SammyWinchester) of sta
|
|||
## Installation
|
||||
|
||||
Development environment requirements :
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 12.0.0
|
||||
- [MongoDB](https://www.mongodb.com/download-center/community) >= 4.4
|
||||
- [PostgreSQL](https://www.postgresql.org/download/)
|
||||
- [Redis](https://redis.io/download)
|
||||
|
||||
You can use the `docker-compose.yml` file to quickly setup Postgre and Redis in development.
|
||||
|
||||
Setting up your development environment on your local machine :
|
||||
|
||||
```bash
|
||||
> git clone https://github.com/vkaelin/LeagueStats.git
|
||||
> cd leaguestats/client
|
||||
|
|
@ -33,11 +36,13 @@ Setting up your development environment on your local machine :
|
|||
> cd leaguestats/server
|
||||
> npm install
|
||||
> cp .env.example .env # edit the values
|
||||
> node ace mongodb:migration:run # your MongoDB installation needs to by a Replica Set and not a Standalone
|
||||
> node ace migration:run
|
||||
```
|
||||
|
||||
## Useful commands
|
||||
|
||||
Running the app :
|
||||
|
||||
```bash
|
||||
> cd client
|
||||
> npm run dev
|
||||
|
|
@ -49,6 +54,7 @@ Running the app :
|
|||
```
|
||||
|
||||
Deploying the app :
|
||||
|
||||
```bash
|
||||
> cd client
|
||||
> npm run build
|
||||
|
|
@ -77,7 +83,6 @@ Adapt — remix, transform, and build upon the material
|
|||
|
||||
### Under the following terms:
|
||||
|
||||
|
||||
NonCommercial — You may not use the material for commercial purposes.
|
||||
|
||||
ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
|
||||
|
|
|
|||
21512
client/package-lock.json
generated
21512
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
|
@ -5,6 +5,7 @@
|
|||
:data="allyTeam"
|
||||
:all-players="[...allyTeam.players, ...enemyTeam.players]"
|
||||
:ally-team="true"
|
||||
:ranks-loaded="data.ranksLoaded"
|
||||
/>
|
||||
|
||||
<div class="flex items-start justify-between px-3 py-2">
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
:data="enemyTeam"
|
||||
:all-players="[...allyTeam.players, ...enemyTeam.players]"
|
||||
:ally-team="false"
|
||||
:ranks-loaded="data.ranksLoaded"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="data.status === 'loading' && detailsOpen">
|
||||
|
|
|
|||
|
|
@ -91,29 +91,29 @@
|
|||
<div
|
||||
:style="{
|
||||
backgroundImage: `url(${
|
||||
player.firstSum ? player.firstSum.icon : null
|
||||
player.summonerSpell1 ? player.summonerSpell1.icon : null
|
||||
})`,
|
||||
}"
|
||||
:class="{ 'cursor-pointer': player.firstSum }"
|
||||
:class="{ 'cursor-pointer': player.summonerSpell1 }"
|
||||
class="w-4 h-4 bg-center bg-cover rounded-md bg-blue-1000"
|
||||
></div>
|
||||
</template>
|
||||
<template v-if="player.firstSum" #default>
|
||||
<template v-if="player.summonerSpell1" #default>
|
||||
<div
|
||||
class="flex max-w-sm p-2 text-xs text-left text-white select-none"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
backgroundImage: `url('${player.firstSum.icon}')`,
|
||||
backgroundImage: `url('${player.summonerSpell1.icon}')`,
|
||||
}"
|
||||
class="flex-shrink-0 w-12 h-12 ml-1 bg-center bg-cover rounded-md bg-blue-1000"
|
||||
></div>
|
||||
<div class="ml-2 leading-tight">
|
||||
<div class="text-base leading-none">
|
||||
{{ player.firstSum.name }}
|
||||
{{ player.summonerSpell1.name }}
|
||||
</div>
|
||||
<div class="mt-1 font-light text-blue-200">
|
||||
{{ player.firstSum.description }}
|
||||
{{ player.summonerSpell1.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -124,29 +124,29 @@
|
|||
<div
|
||||
:style="{
|
||||
backgroundImage: `url(${
|
||||
player.secondSum ? player.secondSum.icon : null
|
||||
player.summonerSpell2 ? player.summonerSpell2.icon : null
|
||||
})`,
|
||||
}"
|
||||
:class="{ 'cursor-pointer': player.secondSum }"
|
||||
:class="{ 'cursor-pointer': player.summonerSpell2 }"
|
||||
class="w-4 h-4 bg-center bg-cover rounded-md bg-blue-1000"
|
||||
></div>
|
||||
</template>
|
||||
<template v-if="player.secondSum" #default>
|
||||
<template v-if="player.summonerSpell2" #default>
|
||||
<div
|
||||
class="flex max-w-sm p-2 text-xs text-left text-white select-none"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
backgroundImage: `url('${player.secondSum.icon}')`,
|
||||
backgroundImage: `url('${player.summonerSpell2.icon}')`,
|
||||
}"
|
||||
class="flex-shrink-0 w-12 h-12 ml-1 bg-center bg-cover rounded-md bg-blue-1000"
|
||||
></div>
|
||||
<div class="ml-2 leading-tight">
|
||||
<div class="text-base leading-none">
|
||||
{{ player.secondSum.name }}
|
||||
{{ player.summonerSpell2.name }}
|
||||
</div>
|
||||
<div class="mt-1 font-light text-blue-200">
|
||||
{{ player.secondSum.description }}
|
||||
{{ player.summonerSpell2.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -195,7 +195,7 @@
|
|||
class="flex flex-col items-start justify-center ml-1 leading-none"
|
||||
>
|
||||
<router-link
|
||||
v-if="player.firstSum"
|
||||
v-if="player.summonerSpell1"
|
||||
:to="{
|
||||
name: 'summoner',
|
||||
params: { region: $route.params.region, name: player.name },
|
||||
|
|
@ -228,7 +228,7 @@
|
|||
{{ player.rank.shortName }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="player.rank === undefined">
|
||||
<div v-else-if="!ranksLoaded">
|
||||
<DotsLoader width="30px" dot-width="10px" />
|
||||
</div>
|
||||
<div v-else class="w-5 h-5">
|
||||
|
|
@ -350,6 +350,10 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
ranksLoaded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -35,13 +35,23 @@
|
|||
></div>
|
||||
<div class="flex flex-col justify-around ml-2">
|
||||
<div
|
||||
:style="{backgroundImage: `url(${data.firstSum})`}"
|
||||
v-if="data.summonerSpell1"
|
||||
:style="{backgroundImage: `url(${data.summonerSpell1.icon})`}"
|
||||
class="w-6 h-6 bg-center bg-cover rounded-md bg-blue-1000"
|
||||
></div>
|
||||
<div
|
||||
:style="{backgroundImage: `url(${data.secondSum})`}"
|
||||
v-else
|
||||
class="w-6 h-6 rounded-md bg-blue-1000"
|
||||
></div>
|
||||
<div
|
||||
v-if="data.summonerSpell2"
|
||||
:style="{backgroundImage: `url(${data.summonerSpell2.icon})`}"
|
||||
class="w-6 h-6 bg-center bg-cover rounded-md bg-blue-1000"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="w-6 h-6 rounded-md bg-blue-1000"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-around ml-1">
|
||||
<div
|
||||
|
|
@ -156,7 +166,7 @@
|
|||
<svg class="w-5 h-5 text-blue-200">
|
||||
<use xlink:href="#stopwatch" />
|
||||
</svg>
|
||||
<div class="text-lg font-medium text-teal-400">{{ data.time|secToTime }}</div>
|
||||
<div class="text-lg font-medium text-teal-400">{{ (data.time)|secToTime }}</div>
|
||||
<Tooltip>
|
||||
<template #trigger>
|
||||
<div class="text-xs font-medium text-white">{{ data.date }}</div>
|
||||
|
|
@ -175,7 +185,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</Ripple>
|
||||
<DetailedMatch :data="getMatchDetails(data.gameId) || {}" :details-open="showDetails" />
|
||||
<DetailedMatch :data="getMatchDetails(data.matchId) || {}" :details-open="showDetails" />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
|
|
@ -223,8 +233,8 @@ export default {
|
|||
displayDetails() {
|
||||
this.showDetails = !this.showDetails
|
||||
|
||||
if (!this.getMatchDetails(this.data.gameId)) {
|
||||
this.matchDetails(this.data.gameId)
|
||||
if (!this.getMatchDetails(this.data.matchId)) {
|
||||
this.matchDetails(this.data.matchId)
|
||||
}
|
||||
},
|
||||
isSummonerProfile(account_id) {
|
||||
|
|
|
|||
|
|
@ -24,17 +24,17 @@
|
|||
>
|
||||
<td class="py-1 pl-2 rounded-l-lg">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
<div
|
||||
@click="selectRunes(player)"
|
||||
:class="{ 'cursor-pointer': player.perks }"
|
||||
class="flex flex-col items-center runes"
|
||||
>
|
||||
<div
|
||||
:style="{backgroundImage: `url('${player.runes.primaryRune}')`}"
|
||||
:style="{backgroundImage: `url('${getPrimarRune(player.perks)}')`}"
|
||||
class="w-6 h-6 bg-center bg-cover"
|
||||
></div>
|
||||
<div
|
||||
:style="{backgroundImage: `url('${player.runes.secondaryRune}')`}"
|
||||
:style="{backgroundImage: `url('${getSecondaryRune(player.perks)}')`}"
|
||||
class="w-3 h-3 mt-1 bg-center bg-cover"
|
||||
></div>
|
||||
</div>
|
||||
|
|
@ -72,7 +72,11 @@
|
|||
:class="[player.summonerId === account.id ? 'text-yellow-500' : 'hover:text-blue-200']"
|
||||
class="font-semibold"
|
||||
>{{ player.summonerName }}</router-link>
|
||||
<div class="text-xs">Level {{ player.level }}</div>
|
||||
<div
|
||||
:class="[ally ? 'text-teal-300 ' : 'text-red-400 ']"
|
||||
class="text-xs"
|
||||
>{{ player.champion.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -185,7 +189,7 @@
|
|||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
import { getSummonerLink } from '@/helpers/summoner.js'
|
||||
import { getSummonerLink, getPrimarRune, getSecondaryRune } from '@/helpers/summoner.js'
|
||||
import { ContentLoader } from 'vue-content-loader'
|
||||
|
||||
export default {
|
||||
|
|
@ -264,17 +268,13 @@ export default {
|
|||
}
|
||||
},
|
||||
selectRunes(player) {
|
||||
if(!player.perks) {
|
||||
if(!player.perks)
|
||||
return
|
||||
}
|
||||
|
||||
this.displayOrHideRunes({
|
||||
primaryStyle: player.perks.perkStyle,
|
||||
secondaryStyle: player.perks.perkSubStyle,
|
||||
selected: player.perks.perkIds
|
||||
})
|
||||
this.displayOrHideRunes(player.perks)
|
||||
},
|
||||
getSummonerLink,
|
||||
getPrimarRune,
|
||||
getSecondaryRune,
|
||||
...mapActions('cdragon', ['displayOrHideRunes']),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<ul class="mt-1 text-gray-100">
|
||||
<li
|
||||
v-for="mate in mates.slice(0, maxMates)"
|
||||
:key="mate._id"
|
||||
:key="mate.name"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<router-link
|
||||
|
|
|
|||
|
|
@ -185,12 +185,12 @@
|
|||
</div>
|
||||
<ul class="mt-1 text-gray-100">
|
||||
<li
|
||||
v-for="(championClass, index) in championClasses"
|
||||
v-for="(championClass, index) in stats.class"
|
||||
:key="index"
|
||||
:class="{'bg-blue-760': index % 2 !== 0}"
|
||||
class="flex items-center justify-between px-4 py-1 leading-tight"
|
||||
>
|
||||
<div class="w-2/4 text-left capitalize">{{ championClass._id }}</div>
|
||||
<div class="w-2/4 text-left capitalize">{{ championClass.id }}</div>
|
||||
<div
|
||||
:class="calculateWinrate(championClass.wins, championClass.count).color"
|
||||
class="w-1/4"
|
||||
|
|
@ -241,16 +241,12 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
championClasses() {
|
||||
const classes = [...this.stats.class]
|
||||
return classes.sort((a, b) => b.count - a.count)
|
||||
},
|
||||
mostPlayedRole() {
|
||||
return Math.max(...this.stats.role.map(r => r.count), 0)
|
||||
},
|
||||
globalStatsKeys() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { _id, wins, losses, count, time, kp, ...rest } = this.stats.global
|
||||
const { id, wins, losses, count, time, kp, ...rest } = this.stats.global
|
||||
return rest
|
||||
},
|
||||
...mapState({
|
||||
|
|
@ -270,10 +266,9 @@ export default {
|
|||
leagueStatsByType(typeName) {
|
||||
return this.stats.league
|
||||
.map(l => {
|
||||
return { ...l, ...gameModes[l._id] }
|
||||
return { ...l, ...gameModes[l.id] }
|
||||
})
|
||||
.filter(l => l.type === typeName)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
},
|
||||
roundedRoleLosses(win, count) {
|
||||
return win === count ? 'rounded-full' : 'rounded-b-full'
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Tooltip from '@/components/Common/Tooltip.vue'
|
||||
|
||||
export default {
|
||||
|
|
@ -64,15 +65,6 @@ export default {
|
|||
Tooltip,
|
||||
},
|
||||
|
||||
props: {
|
||||
matches: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
gridDays: [],
|
||||
|
|
@ -86,8 +78,14 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
recentActivity: state => state.summoner.basic.recentActivity
|
||||
}),
|
||||
},
|
||||
|
||||
watch: {
|
||||
matches() {
|
||||
recentActivity() {
|
||||
this.fillGrid()
|
||||
}
|
||||
},
|
||||
|
|
@ -118,16 +116,15 @@ export default {
|
|||
},
|
||||
fillGrid() {
|
||||
// Add all the matches made by the summoner
|
||||
for (const key in this.matches) {
|
||||
const match = this.matches[key]
|
||||
const matchTime = new Date(match.timestamp)
|
||||
for (const match of this.recentActivity) {
|
||||
const matchTime = new Date(match.day)
|
||||
const formattedTime = matchTime.toLocaleString(undefined, this.options)
|
||||
|
||||
const dayOfTheMatch = this.gridDays.filter(
|
||||
e => e.date === formattedTime
|
||||
)
|
||||
if (dayOfTheMatch.length > 0) {
|
||||
dayOfTheMatch[0].matches++
|
||||
dayOfTheMatch[0].matches = match.count
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:style="{
|
||||
backgroundImage:
|
||||
`${hover ? gradientHover : gradient},
|
||||
url('https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-splashes/${record.champion.id}/${record.champion.id}000.jpg')`
|
||||
url('https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-splashes/${record.champion_id}/${record.champion_id}000.jpg')`
|
||||
}"
|
||||
:class="borderColor"
|
||||
class="relative w-full p-4 mx-2 mt-6 leading-none bg-center bg-cover border rounded-lg record-card"
|
||||
|
|
@ -21,18 +21,18 @@
|
|||
<span :class="textColor" class="ml-0">{{ title }}</span>
|
||||
</div>
|
||||
<img
|
||||
:src="`https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/${record.champion.id}.png`"
|
||||
:src="`https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/${record.champion_id}.png`"
|
||||
:class="[{'opacity-0 scale-125': hover}, borderColor]"
|
||||
class="block w-16 h-16 mx-auto mt-10 transition duration-500 ease-in transform border-2 rounded-full"
|
||||
alt="Champion Played"
|
||||
/>
|
||||
<div :style="{textShadow: `-2px 1px 6px ${color}`}" class="mt-6 text-4xl">{{ record[property] }}</div>
|
||||
<div :style="{textShadow: `-2px 1px 6px ${color}`}" class="mt-6 text-4xl">{{ record.amount }}</div>
|
||||
|
||||
<div class="text-sm">
|
||||
<div class="mt-6">
|
||||
<span
|
||||
:class="record.result === 'Win' ? 'text-green-400' : 'text-red-400'"
|
||||
>{{ record.result === 'Win' ? 'Won' : 'Lost' }}</span>
|
||||
:class="record.result ? 'text-green-400' : 'text-red-400'"
|
||||
>{{ record.result ? 'Won' : 'Lost' }}</span>
|
||||
<span class="ml-1 font-semibold">{{ timeDifference(record.date) }}</span>
|
||||
</div>
|
||||
<div class="mt-2 text-gray-500">
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mt-6 text-xs font-light text-right text-gray-200 opacity-25">
|
||||
<span v-if="hover">match {{ record.gameId }}</span>
|
||||
<span v-if="hover">{{ record.id }}</span>
|
||||
<span v-else>{{ gameModes[record.gamemode].name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -65,10 +65,6 @@ export default {
|
|||
type: String,
|
||||
required: true
|
||||
},
|
||||
property: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
record: {
|
||||
type: Object,
|
||||
required: true
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function secToTime(seconds) {
|
|||
* Sort an array of players by role
|
||||
*/
|
||||
export function sortTeamByRole(a, b) {
|
||||
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
|
||||
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
|
||||
return sortingArr.indexOf(a.role) - sortingArr.indexOf(b.role)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,37 @@
|
|||
import { secToTime, timeDifference } from '@/helpers/functions.js'
|
||||
import { createCDragonAssetUrl, secToTime, timeDifference } from '@/helpers/functions.js'
|
||||
import { maps, gameModes } from '@/data/data.js'
|
||||
import summonerSpells from '@/data/summonerSpells.json'
|
||||
import store from '@/store'
|
||||
|
||||
const leaguesNumbers = { 'I': 1, 'II': 2, 'III': 3, 'IV': 4 }
|
||||
|
||||
/**
|
||||
* Get the url of the of the player primary rune
|
||||
* @param {Object} perks : from the API
|
||||
*/
|
||||
export function getPrimarRune(perks) {
|
||||
const primaryRune = perks.selected.length ? store.state.cdragon.runes.perks[perks.selected[0]] : null
|
||||
return primaryRune ? createCDragonAssetUrl(primaryRune.icon) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of the of the player secondary rune
|
||||
* @param {Object} perks : from the API
|
||||
*/
|
||||
export function getSecondaryRune(perks) {
|
||||
const secondaryRune = store.state.cdragon.runes.perkstyles[perks.secondaryStyle]
|
||||
return secondaryRune ? createCDragonAssetUrl(secondaryRune.icon) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the infos about a list of matches built with the Riot API data
|
||||
* @param {Object} RiotData : all data from the Riot API
|
||||
*/
|
||||
export function createMatchData(matches) {
|
||||
for (const match of matches) {
|
||||
match.firstSum = getSummonerLink(match.firstSum)
|
||||
match.secondSum = getSummonerLink(match.secondSum)
|
||||
// Runes
|
||||
match.primaryRune = getPrimarRune(match.perks)
|
||||
match.secondaryRune = getSecondaryRune(match.perks)
|
||||
|
||||
const date = new Date(match.date)
|
||||
const dateOptions = { day: '2-digit', month: '2-digit', year: 'numeric' }
|
||||
|
|
@ -62,21 +82,22 @@ export function createBasicSummonerData(RiotData) {
|
|||
|
||||
/**
|
||||
* Return the formatted records of a summoner
|
||||
* @param {Object} records : raw records from the database stats
|
||||
* @param {Object} recordsDto : raw records from the database stats
|
||||
*/
|
||||
export function createRecordsData(records) {
|
||||
records.maxTime.time = secToTime(records.maxTime.time)
|
||||
records.maxGold.gold = records.maxGold.gold.toLocaleString()
|
||||
records.maxDmgTaken.dmgTaken = records.maxDmgTaken.dmgTaken.toLocaleString()
|
||||
records.maxDmgChamp.dmgChamp = records.maxDmgChamp.dmgChamp.toLocaleString()
|
||||
records.maxDmgObj.dmgObj = records.maxDmgObj.dmgObj.toLocaleString()
|
||||
records.maxKp.kp = `${records.maxKp.kp}%`
|
||||
export function createRecordsData(recordsDto) {
|
||||
const records = recordsDto.reduce((acc, record) => {
|
||||
acc[record.what] = record
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// New record fields
|
||||
if (records.maxLiving) {
|
||||
records.maxLiving.longestLiving = secToTime(records.maxLiving.longestLiving)
|
||||
records.maxHeal.heal = records.maxHeal.heal.toLocaleString()
|
||||
}
|
||||
records.game_duration.amount = secToTime(records.game_duration.amount)
|
||||
records.gold.amount = records.gold.amount.toLocaleString()
|
||||
records.damage_taken.amount = records.damage_taken.amount.toLocaleString()
|
||||
records.damage_dealt_champions.amount = records.damage_dealt_champions.amount.toLocaleString()
|
||||
records.damage_dealt_objectives.amount = records.damage_dealt_objectives.amount.toLocaleString()
|
||||
records.kp.amount = `${records.kp.amount}%`
|
||||
records.time_spent_living.amount = secToTime(records.time_spent_living.amount)
|
||||
records.heal.amount = records.heal.amount.toLocaleString()
|
||||
|
||||
return records
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<RecentActivity :matches="basic.matchList" />
|
||||
<RecentActivity />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default new Vuex.Store({
|
|||
'tr': 'tr1',
|
||||
'ru': 'ru'
|
||||
},
|
||||
roles: ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
|
||||
roles: ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
|
||||
},
|
||||
strict: debug
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,47 +8,70 @@ export const state = {
|
|||
}
|
||||
|
||||
export const mutations = {
|
||||
MATCH_LOADING(state, gameId) {
|
||||
const alreadyIn = state.matches.find(m => m.gameId === gameId)
|
||||
MATCH_LOADING(state, matchId) {
|
||||
const alreadyIn = state.matches.find(m => m.matchId === matchId)
|
||||
if (!alreadyIn) {
|
||||
state.matches.push({ gameId: gameId, status: 'loading' })
|
||||
state.matches.push({ matchId, status: 'loading' })
|
||||
}
|
||||
},
|
||||
MATCH_FOUND(state, matchDetails) {
|
||||
MATCH_FOUND(state, {matchDetails, ranksLoaded }) {
|
||||
matchDetails.status = 'loaded'
|
||||
matchDetails.ranksLoaded = ranksLoaded
|
||||
|
||||
// Set SoloQ as rank for now
|
||||
if(ranksLoaded) {
|
||||
for (const player of matchDetails.blueTeam.players) {
|
||||
player.rank = player.rank && player.rank[420]
|
||||
}
|
||||
for (const player of matchDetails.redTeam.players) {
|
||||
player.rank = player.rank && player.rank[420]
|
||||
}
|
||||
}
|
||||
|
||||
const index = state.matches.findIndex(m => m.gameId === matchDetails.gameId)
|
||||
Vue.set(state.matches, index, matchDetails)
|
||||
},
|
||||
MATCH_RANKS_FOUND(state, { gameId, blueTeam, redTeam }) {
|
||||
MATCH_RANKS_FOUND(state, { gameId, ranksByPlayer }) {
|
||||
const match = state.matches.find(m => m.gameId === gameId)
|
||||
match.blueTeam.players = blueTeam
|
||||
match.redTeam.players = redTeam
|
||||
|
||||
for (const player of match.blueTeam.players) {
|
||||
const ranks = ranksByPlayer[player.id]
|
||||
if(!ranks) continue
|
||||
Vue.set(player, 'rank', ranks[420])
|
||||
}
|
||||
|
||||
for (const player of match.redTeam.players) {
|
||||
const ranks = ranksByPlayer[player.id]
|
||||
if(!ranks) continue
|
||||
Vue.set(player, 'rank', ranks[420])
|
||||
}
|
||||
|
||||
match.ranksLoaded = true
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async matchDetails({ commit, rootState }, gameId) {
|
||||
commit('MATCH_LOADING', gameId)
|
||||
const region = rootState.regionsList[rootState.settings.region]
|
||||
console.log('MATCH DETAILS STORE', gameId, region)
|
||||
async matchDetails({ commit }, matchId) {
|
||||
commit('MATCH_LOADING', matchId)
|
||||
console.log('MATCH DETAILS STORE', matchId)
|
||||
|
||||
const resp = await axios(({ url: 'match/details', data: { gameId, region }, method: 'POST' })).catch(() => { })
|
||||
const resp = await axios(({ url: 'match/details', data: { matchId }, method: 'POST' })).catch(() => { })
|
||||
console.log('--- DETAILS INFOS ---')
|
||||
console.log(resp.data)
|
||||
commit('MATCH_FOUND', resp.data.matchDetails)
|
||||
const {matchDetails, ranksLoaded} = resp.data
|
||||
commit('MATCH_FOUND', {matchDetails, ranksLoaded })
|
||||
|
||||
// If the ranks of the players are not yet known
|
||||
if (resp.data.matchDetails.blueTeam.players[0].rank === undefined) {
|
||||
const ranks = await axios(({ url: 'match/details/ranks', data: { gameId, region }, method: 'POST' })).catch(() => { })
|
||||
if (!ranksLoaded) {
|
||||
const ranks = await axios(({ url: 'match/details/ranks', data: { matchId }, method: 'POST' })).catch(() => { })
|
||||
if (!ranks) return
|
||||
console.log('--- RANK OF MATCH DETAILS ---')
|
||||
console.log(ranks.data)
|
||||
commit('MATCH_RANKS_FOUND', { gameId, ...ranks.data })
|
||||
commit('MATCH_RANKS_FOUND', { matchId, ranksByPlayer: ranks.data })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
getMatchDetails: state => gameId => state.matches.find(m => m.gameId === gameId),
|
||||
getMatchDetails: state => matchId => state.matches.find(m => m.matchId === matchId),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const state = {
|
|||
currentSeason: null,
|
||||
matchList: [],
|
||||
ranked: {},
|
||||
recentActivity: [],
|
||||
seasons: [],
|
||||
status: '',
|
||||
},
|
||||
|
|
@ -24,7 +25,7 @@ export const state = {
|
|||
championsLoaded: false
|
||||
},
|
||||
records: {
|
||||
list: [],
|
||||
list: {},
|
||||
recordsLoaded: false
|
||||
},
|
||||
live: {
|
||||
|
|
@ -66,18 +67,21 @@ export const mutations = {
|
|||
state.overview.matchesLoading = true
|
||||
},
|
||||
MATCHES_FOUND(state, { newMatches, stats }) {
|
||||
state.basic.recentActivity = stats.recentActivity
|
||||
state.overview.matchesLoading = false
|
||||
state.overview.matches = [...state.overview.matches, ...newMatches]
|
||||
state.overview.matchIndex += newMatches.length
|
||||
state.overview.matchIndex += 10
|
||||
state.overview.stats = stats
|
||||
state.champions.championsLoaded = false
|
||||
state.records.recordsLoaded = false
|
||||
},
|
||||
OVERVIEW_FOUND(state, infos) {
|
||||
state.basic.recentActivity = infos.stats.recentActivity
|
||||
state.overview.matches = infos.matches
|
||||
state.overview.matchIndex = infos.matches.length
|
||||
state.overview.stats = infos.stats
|
||||
state.overview.loaded = true
|
||||
state.records.recordsLoaded = false
|
||||
},
|
||||
RECORDS_FOUND(state, { records }) {
|
||||
state.records.list = records
|
||||
|
|
@ -87,6 +91,7 @@ export const mutations = {
|
|||
state.basic.account = infos.account
|
||||
state.basic.matchList = infos.matchList
|
||||
state.basic.ranked = infos.ranked
|
||||
state.basic.recentActivity = infos.recentActivity
|
||||
state.basic.seasons = infos.seasons.sort((a, b) => b - a)
|
||||
state.basic.status = 'found'
|
||||
state.live.match = infos.current
|
||||
|
|
@ -178,17 +183,15 @@ export const actions = {
|
|||
async moreMatches({ commit, getters, rootState }) {
|
||||
commit('MATCHES_LOADING')
|
||||
|
||||
const gameIds = getters.filteredMatchList
|
||||
const matchIds = getters.filteredMatchList
|
||||
.slice(state.overview.matchIndex, state.overview.matchIndex + 10)
|
||||
.map(({ gameId }) => gameId)
|
||||
|
||||
const resp = await axios(({
|
||||
url: 'match',
|
||||
data: {
|
||||
puuid: state.basic.account.puuid,
|
||||
accountId: state.basic.account.accountId,
|
||||
region: rootState.regionsList[rootState.settings.region],
|
||||
gameIds
|
||||
matchIds
|
||||
},
|
||||
method: 'POST'
|
||||
})).catch(() => { })
|
||||
|
|
@ -216,7 +219,7 @@ export const actions = {
|
|||
const resp = await axios(({ url: 'summoner/records', data: { puuid: state.basic.account.puuid }, method: 'POST' })).catch(() => { })
|
||||
console.log('---RECORDS---')
|
||||
console.log(resp.data)
|
||||
const records = resp.data ? createRecordsData(resp.data) : {}
|
||||
const records = resp.data.length ? createRecordsData(resp.data) : {}
|
||||
|
||||
commit('RECORDS_FOUND', { records })
|
||||
},
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ export default {
|
|||
|
||||
created() {
|
||||
this.fetchData()
|
||||
|
||||
this.getRunes()
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -67,6 +69,7 @@ export default {
|
|||
this.liveMatchRequest()
|
||||
}
|
||||
},
|
||||
...mapActions('cdragon', ['getRunes']),
|
||||
...mapActions('summoner', ['liveMatchRequest']),
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div key="records">
|
||||
<template v-if="!recordsLoaded || (recordsLoaded && records.maxKda)">
|
||||
<template v-if="!recordsLoaded || (recordsLoaded && records.assists)">
|
||||
<div
|
||||
class="relative pl-6 text-2xl text-blue-200 border-b-2 border-blue-800 category blue-900"
|
||||
>Basics</div>
|
||||
|
|
@ -10,48 +10,42 @@
|
|||
color="#63b3ed"
|
||||
text-color="text-blue-400"
|
||||
border-color="border-blue-400"
|
||||
property="kda"
|
||||
:record="records.maxKda"
|
||||
:record="records.kda"
|
||||
title="KDA"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#68D391"
|
||||
text-color="text-green-400"
|
||||
border-color="border-green-400"
|
||||
property="kills"
|
||||
:record="records.maxKills"
|
||||
:record="records.kills"
|
||||
title="Kills"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#9F7AEA"
|
||||
text-color="text-purple-500"
|
||||
border-color="border-purple-500"
|
||||
property="assists"
|
||||
:record="records.maxAssists"
|
||||
:record="records.assists"
|
||||
title="Assists"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#F56565"
|
||||
text-color="text-red-500"
|
||||
border-color="border-red-500"
|
||||
property="deaths"
|
||||
:record="records.maxDeaths"
|
||||
:record="records.deaths"
|
||||
title="Deaths"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#D69E2E"
|
||||
text-color="text-yellow-600"
|
||||
border-color="border-yellow-600"
|
||||
property="gold"
|
||||
:record="records.maxGold"
|
||||
:record="records.gold"
|
||||
title="Gold earned"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#81E6D9"
|
||||
text-color="text-teal-300"
|
||||
border-color="border-teal-300"
|
||||
property="minions"
|
||||
:record="records.maxMinions"
|
||||
:record="records.minions"
|
||||
title="Minions killed"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -81,24 +75,21 @@
|
|||
color="#FC8181"
|
||||
text-color="text-red-400"
|
||||
border-color="border-red-400"
|
||||
property="dmgChamp"
|
||||
:record="records.maxDmgChamp"
|
||||
:record="records.damage_dealt_champions"
|
||||
title="Damage champions"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#D69E2E"
|
||||
text-color="text-yellow-400"
|
||||
border-color="border-yellow-400"
|
||||
property="dmgObj"
|
||||
:record="records.maxDmgObj"
|
||||
:record="records.damage_dealt_objectives"
|
||||
title="Damage objectives"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#FC8181"
|
||||
text-color="text-red-400"
|
||||
border-color="border-red-400"
|
||||
property="dmgTaken"
|
||||
:record="records.maxDmgTaken"
|
||||
:record="records.damage_taken"
|
||||
title="Damage taken"
|
||||
/>
|
||||
<RecordCard
|
||||
|
|
@ -106,24 +97,21 @@
|
|||
color="#D69E2E"
|
||||
text-color="text-yellow-400"
|
||||
border-color="border-yellow-400"
|
||||
property="towers"
|
||||
:record="records.maxTowers"
|
||||
:record="records.turret_kills"
|
||||
title="Towers"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#68D391"
|
||||
text-color="text-green-400"
|
||||
border-color="border-green-400"
|
||||
property="kp"
|
||||
:record="records.maxKp"
|
||||
:record="records.kp"
|
||||
title="Kill participation"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#D69E2E"
|
||||
text-color="text-yellow-400"
|
||||
border-color="border-yellow-400"
|
||||
property="vision"
|
||||
:record="records.maxVision"
|
||||
:record="records.vision_score"
|
||||
title="Vision score"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -153,35 +141,28 @@
|
|||
color="#4299E1"
|
||||
text-color="text-blue-500"
|
||||
border-color="border-blue-500"
|
||||
property="time"
|
||||
:record="records.maxTime"
|
||||
:record="records.game_duration"
|
||||
title="Longest game"
|
||||
/>
|
||||
<RecordCard
|
||||
v-if="records.maxLiving"
|
||||
color="#4299E1"
|
||||
text-color="text-blue-500"
|
||||
border-color="border-blue-500"
|
||||
property="longestLiving"
|
||||
:record="records.maxLiving"
|
||||
:record="records.time_spent_living"
|
||||
title="Longest living"
|
||||
/>
|
||||
<RecordCard
|
||||
v-if="records.maxCriticalStrike"
|
||||
color="#D69E2E"
|
||||
text-color="text-yellow-400"
|
||||
border-color="border-yellow-400"
|
||||
property="criticalStrike"
|
||||
:record="records.maxCriticalStrike"
|
||||
:record="records.critical_strike"
|
||||
title="Critical Strike"
|
||||
/>
|
||||
<RecordCard
|
||||
v-if="records.maxHeal"
|
||||
color="#68D391"
|
||||
text-color="text-green-400"
|
||||
border-color="border-green-400"
|
||||
property="heal"
|
||||
:record="records.maxHeal"
|
||||
:record="records.heal"
|
||||
title="Heal"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -204,47 +185,42 @@
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="records.maxDouble" class="relative pl-6 mt-3 text-2xl text-blue-200 border-b-2 border-blue-800 category">Multi kills</div>
|
||||
<div v-if="records.maxDouble" class="flex flex-wrap -mx-2">
|
||||
<div class="relative pl-6 mt-3 text-2xl text-blue-200 border-b-2 border-blue-800 category">Multi kills</div>
|
||||
<div class="flex flex-wrap -mx-2">
|
||||
<template v-if="recordsLoaded">
|
||||
<RecordCard
|
||||
color="#FEFCBF"
|
||||
text-color="text-yellow-200"
|
||||
border-color="border-yellow-200"
|
||||
property="doubleKills"
|
||||
:record="records.maxDouble"
|
||||
:record="records.double_kills"
|
||||
title="Double kills"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#F6E05E"
|
||||
text-color="text-yellow-400"
|
||||
border-color="border-yellow-400"
|
||||
property="tripleKills"
|
||||
:record="records.maxTriple"
|
||||
:record="records.triple_kills"
|
||||
title="Triple kills"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#D69E2E"
|
||||
text-color="text-yellow-600"
|
||||
border-color="border-yellow-600"
|
||||
property="quadraKills"
|
||||
:record="records.maxQuadra"
|
||||
:record="records.quadra_kills"
|
||||
title="Quadra kills"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#F56565"
|
||||
text-color="text-red-500"
|
||||
border-color="border-red-500"
|
||||
property="pentaKills"
|
||||
:record="records.maxPenta"
|
||||
:record="records.penta_kills"
|
||||
title="Penta kills"
|
||||
/>
|
||||
<RecordCard
|
||||
color="#63b3ed"
|
||||
text-color="text-blue-400"
|
||||
border-color="border-blue-400"
|
||||
property="killingSpree"
|
||||
:record="records.maxKillingSpree"
|
||||
:record="records.killing_spree"
|
||||
title="Killing Spree"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -268,7 +244,7 @@
|
|||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="recordsLoaded && !records.maxKda">
|
||||
<template v-if="recordsLoaded && !records.assists">
|
||||
<div class="flex flex-col items-center mt-4">
|
||||
<div>No records have been found.</div>
|
||||
<div>😕</div>
|
||||
|
|
|
|||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal 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:
|
||||
|
|
@ -2,28 +2,36 @@
|
|||
"typescript": true,
|
||||
"commands": [
|
||||
"./commands",
|
||||
"@adonisjs/core/build/commands",
|
||||
"@zakodium/adonis-mongodb/lib/commands"
|
||||
"@adonisjs/core/build/commands/index.js",
|
||||
"@adonisjs/repl/build/commands",
|
||||
"@adonisjs/lucid/build/commands"
|
||||
],
|
||||
"exceptionHandlerNamespace": "App/Exceptions/Handler",
|
||||
"aliases": {
|
||||
"App": "app",
|
||||
"Contracts": "contracts",
|
||||
"Config": "config",
|
||||
"Database": "database"
|
||||
"Database": "database",
|
||||
"Contracts": "contracts"
|
||||
},
|
||||
"preloads": [
|
||||
"./start/routes",
|
||||
"./start/kernel"
|
||||
"./start/kernel",
|
||||
{
|
||||
"file": "./start/events",
|
||||
"environment": [
|
||||
"console",
|
||||
"repl",
|
||||
"web"
|
||||
]
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
"./providers/AppProvider",
|
||||
"@adonisjs/core",
|
||||
"@zakodium/adonis-mongodb",
|
||||
"@adonisjs/lucid",
|
||||
"@adonisjs/redis"
|
||||
],
|
||||
"metaFiles": [
|
||||
".env",
|
||||
".adonisrc.json"
|
||||
"aceProviders": [
|
||||
"@adonisjs/repl"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,18 @@ PORT=3333
|
|||
HOST=0.0.0.0
|
||||
NODE_ENV=development
|
||||
APP_KEY=
|
||||
DRIVE_DISK=local
|
||||
|
||||
MONGODB_URL=mongodb://localhost:27017
|
||||
MONGODB_DATABASE=leaguestats
|
||||
DB_CONNECTION=pg
|
||||
PG_HOST=localhost
|
||||
PG_PORT=5432
|
||||
PG_USER=
|
||||
PG_PASSWORD=
|
||||
PG_DB_NAME=
|
||||
|
||||
REDIS_CONNECTION=local
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
API_KEY=RGAPI-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
RIOT_API_KEY=
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
{
|
||||
"extends": [
|
||||
"plugin:adonis/typescriptApp"
|
||||
],
|
||||
"extends": ["plugin:adonis/typescriptApp", "prettier"],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 130
|
||||
}
|
||||
]
|
||||
"prettier/prettier": ["error"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
server/.prettierignore
Normal file
1
server/.prettierignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
build
|
||||
11
server/.prettierrc
Normal file
11
server/.prettierrc
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"quoteProps": "consistent",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"printWidth": 100,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
|
|
@ -3,17 +3,14 @@
|
|||
| Ace Commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is the entry point for running ace commands. For typescript
|
||||
| projects, the ace commands will fallback to the compiled code and
|
||||
| hence this file has to be executable by node directly.
|
||||
| This file is the entry point for running ace commands.
|
||||
|
|
||||
*/
|
||||
|
||||
require('reflect-metadata')
|
||||
require('source-map-support').install({ handleUncaughtExceptions: false })
|
||||
|
||||
const { Ignitor } = require('@adonisjs/core/build/src/Ignitor')
|
||||
const { Ignitor } = require('@adonisjs/core/build/standalone')
|
||||
new Ignitor(__dirname)
|
||||
.ace()
|
||||
.handle(process.argv.slice(2))
|
||||
.catch(console.error)
|
||||
|
|
|
|||
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import Redis from '@ioc:Adonis/Addons/Redis'
|
||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import RuneSerializer from 'App/Serializers/RuneSerializer'
|
||||
import Jax from 'App/Services/Jax'
|
||||
import RuneTransformer from 'App/Transformers/RuneTransformer'
|
||||
|
||||
export default class CDragonController {
|
||||
public async runes ({ response }: HttpContextContract) {
|
||||
public async runes({ response }: HttpContextContract) {
|
||||
const cacheUrl = 'cdragon-runes'
|
||||
|
||||
const requestCached = await Redis.get(cacheUrl)
|
||||
|
|
@ -16,8 +16,8 @@ export default class CDragonController {
|
|||
const perkstyles = await Jax.CDragon.perkstyles()
|
||||
|
||||
const runesData = {
|
||||
perks: RuneTransformer.transformPerks(perks),
|
||||
perkstyles: RuneTransformer.transformStyles(perkstyles.styles),
|
||||
perks: RuneSerializer.serializePerks(perks),
|
||||
perkstyles: RuneSerializer.serializeStyles(perkstyles.styles),
|
||||
}
|
||||
|
||||
await Redis.set(cacheUrl, JSON.stringify(runesData), 'EX', 36000)
|
||||
|
|
|
|||
|
|
@ -1,51 +1,23 @@
|
|||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import DetailedMatch, { DetailedMatchModel } from 'App/Models/DetailedMatch'
|
||||
import { ParticipantDetails } from 'App/Models/Match'
|
||||
import Summoner from 'App/Models/Summoner'
|
||||
import Jax from 'App/Services/Jax'
|
||||
import Match from 'App/Models/Match'
|
||||
import MatchPlayerRankParser from 'App/Parsers/MatchPlayerRankParser'
|
||||
import DetailedMatchSerializer from 'App/Serializers/DetailedMatchSerializer'
|
||||
import MatchPlayerRankSerializer from 'App/Serializers/MatchPlayerRankSerializer'
|
||||
import MatchService from 'App/Services/MatchService'
|
||||
import StatsService from 'App/Services/StatsService'
|
||||
import SummonerService from 'App/Services/SummonerService'
|
||||
import DetailedMatchTransformer from 'App/Transformers/DetailedMatchTransformer'
|
||||
import DetailedMatchValidator from 'App/Validators/DetailedMatchValidator'
|
||||
import MatchesIndexValidator from 'App/Validators/MatchesIndexValidator'
|
||||
|
||||
export default class MatchesController {
|
||||
/**
|
||||
* Get the soloQ rank of all the players of the team
|
||||
* @param summoner all the data of the summoner
|
||||
* @param region of the match
|
||||
* POST - Return data from matches searched by matchIds
|
||||
* @param ctx
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public async index ({ request, response }: HttpContextContract) {
|
||||
console.log('More Matches Request')
|
||||
const { puuid, accountId, region, gameIds, season } = await request.validate(MatchesIndexValidator)
|
||||
|
||||
const summonerDB = await Summoner.findOne({ puuid })
|
||||
if (!summonerDB) {
|
||||
return response.json(null)
|
||||
}
|
||||
const matches = await MatchService.getMatches(puuid, accountId, region, gameIds, summonerDB)
|
||||
|
||||
await summonerDB.save()
|
||||
public async index({ request, response }: HttpContextContract) {
|
||||
const { puuid, region, matchIds, season } = await request.validate(MatchesIndexValidator)
|
||||
const matches = await MatchService.getMatches(region, matchIds, puuid)
|
||||
|
||||
const stats = await StatsService.getSummonerStats(puuid, season)
|
||||
|
||||
return response.json({
|
||||
matches,
|
||||
stats,
|
||||
|
|
@ -54,57 +26,42 @@ export default class MatchesController {
|
|||
|
||||
/**
|
||||
* 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')
|
||||
const { gameId, region } = await request.validate(DetailedMatchValidator)
|
||||
const { matchId } = await request.validate(DetailedMatchValidator)
|
||||
|
||||
let matchDetails: DetailedMatchModel
|
||||
const alreadySaved = await DetailedMatch.findOne({ gameId, region })
|
||||
const match = await Match.query()
|
||||
.where('id', matchId)
|
||||
.preload('teams')
|
||||
.preload('players', (playersQuery) => {
|
||||
playersQuery.preload('ranks')
|
||||
})
|
||||
.firstOrFail()
|
||||
|
||||
if (alreadySaved) {
|
||||
console.log('MATCH DETAILS ALREADY SAVED')
|
||||
matchDetails = alreadySaved
|
||||
} else {
|
||||
const match = await Jax.Match.get(gameId, region)
|
||||
matchDetails = await DetailedMatchTransformer.transform(match)
|
||||
await DetailedMatch.create(matchDetails)
|
||||
}
|
||||
const { match: matchDetails, ranksLoaded } = DetailedMatchSerializer.serializeOneMatch(match)
|
||||
|
||||
console.timeEnd('MatchDetails')
|
||||
|
||||
return response.json({
|
||||
matchDetails,
|
||||
ranksLoaded,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')
|
||||
const { gameId, region } = await request.validate(DetailedMatchValidator)
|
||||
const { matchId } = await request.validate(DetailedMatchValidator)
|
||||
const match = await Match.query().where('id', matchId).preload('players').firstOrFail()
|
||||
const parsedRanks = await MatchPlayerRankParser.parse(match)
|
||||
const serializedRanks = MatchPlayerRankSerializer.serialize(parsedRanks)
|
||||
|
||||
let matchDetails = await DetailedMatch.findOne({ gameId, region })
|
||||
|
||||
if (!matchDetails) {
|
||||
return response.json(null)
|
||||
}
|
||||
|
||||
const requestsBlue = matchDetails.blueTeam.players.map(p => this.getPlayerRank(p, region))
|
||||
matchDetails.blueTeam.players = await Promise.all(requestsBlue)
|
||||
|
||||
const requestsRed = matchDetails.redTeam.players.map(p => this.getPlayerRank(p, region))
|
||||
matchDetails.redTeam.players = await Promise.all(requestsRed)
|
||||
|
||||
matchDetails.save()
|
||||
console.timeEnd('Ranks')
|
||||
|
||||
return response.json({
|
||||
blueTeam: matchDetails.blueTeam.players,
|
||||
redTeam: matchDetails.redTeam.players,
|
||||
})
|
||||
return response.json(serializedRanks)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import { getCurrentSeason } from 'App/helpers'
|
||||
import Summoner from 'App/Models/Summoner'
|
||||
import MatchRepository from 'App/Repositories/MatchRepository'
|
||||
import BasicMatchSerializer from 'App/Serializers/BasicMatchSerializer'
|
||||
import LiveMatchSerializer from 'App/Serializers/LiveMatchSerializer'
|
||||
import Jax from 'App/Services/Jax'
|
||||
import MatchService from 'App/Services/MatchService'
|
||||
import StatsService from 'App/Services/StatsService'
|
||||
import SummonerService from 'App/Services/SummonerService'
|
||||
import LiveMatchTransformer from 'App/Transformers/LiveMatchTransformer'
|
||||
import SummonerBasicValidator from 'App/Validators/SummonerBasicValidator'
|
||||
import SummonerChampionValidator from 'App/Validators/SummonerChampionValidator'
|
||||
import SummonerLiveValidator from 'App/Validators/SummonerLiveValidator'
|
||||
|
|
@ -13,21 +15,8 @@ import SummonerOverviewValidator from 'App/Validators/SummonerOverviewValidator'
|
|||
import SummonerRecordValidator from 'App/Validators/SummonerRecordValidator'
|
||||
|
||||
export default class SummonersController {
|
||||
/**
|
||||
* Get all played seasons for a summoner
|
||||
* @param puuid of the summoner
|
||||
*/
|
||||
private async getSeasons (puuid: string): Promise<number[]> {
|
||||
const seasons = await MatchRepository.seasons(puuid)
|
||||
return seasons.length ? seasons.map(s => s._id) : [10]
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: get basic summoner data
|
||||
* @param ctx
|
||||
*/
|
||||
public async basic ({ request, response }: HttpContextContract) {
|
||||
console.time('all')
|
||||
public async basic({ request, response }: HttpContextContract) {
|
||||
console.time('BASIC_REQUEST')
|
||||
const { summoner, region } = await request.validate(SummonerBasicValidator)
|
||||
const finalJSON: any = {}
|
||||
|
||||
|
|
@ -37,24 +26,20 @@ export default class SummonersController {
|
|||
if (!account) {
|
||||
return response.json(null)
|
||||
}
|
||||
account.region = region
|
||||
finalJSON.account = account
|
||||
|
||||
// Summoner in DB
|
||||
let summonerDB = await Summoner.findOne({ puuid: account.puuid })
|
||||
if (!summonerDB) {
|
||||
summonerDB = await Summoner.create({ puuid: account.puuid })
|
||||
}
|
||||
const summonerDB = await Summoner.firstOrCreate({ puuid: account.puuid })
|
||||
|
||||
// Summoner names
|
||||
finalJSON.account.names = SummonerService.getAllSummonerNames(account, summonerDB)
|
||||
finalJSON.account.names = await SummonerService.getAllSummonerNames(account, summonerDB)
|
||||
|
||||
// MATCH LIST
|
||||
await MatchService.updateMatchList(account, summonerDB)
|
||||
finalJSON.matchList = summonerDB.matchList
|
||||
finalJSON.matchList = await MatchService.updateMatchList(account, region, summonerDB)
|
||||
|
||||
// All seasons the summoner has played
|
||||
finalJSON.seasons = await this.getSeasons(account.puuid)
|
||||
// TODO: check if there is a way to do that with V5...
|
||||
finalJSON.seasons = [getCurrentSeason()]
|
||||
|
||||
// CURRENT GAME
|
||||
const currentGame = await Jax.Spectator.summonerID(account.id, region)
|
||||
|
|
@ -62,98 +47,102 @@ export default class SummonersController {
|
|||
finalJSON.current = currentGame
|
||||
|
||||
// RANKED STATS
|
||||
finalJSON.ranked = await SummonerService.getRanked(account, region)
|
||||
finalJSON.ranked = await SummonerService.getRanked(account.id, region)
|
||||
|
||||
// SAVE IN DB
|
||||
await summonerDB.save()
|
||||
} catch (error) {
|
||||
console.log('username not found')
|
||||
console.log(error)
|
||||
// RECENT ACTIVITY
|
||||
finalJSON.recentActivity = await StatsService.getRecentActivity(account.puuid)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
console.timeEnd('BASIC_REQUEST')
|
||||
return response.json(null)
|
||||
}
|
||||
|
||||
console.timeEnd('all')
|
||||
console.timeEnd('BASIC_REQUEST')
|
||||
return response.json(finalJSON)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: get overview view summoner data
|
||||
* @param ctx
|
||||
*/
|
||||
public async overview ({ request, response }: HttpContextContract) {
|
||||
console.time('overview')
|
||||
const { puuid, accountId, region, season } = await request.validate(SummonerOverviewValidator)
|
||||
public async overview({ request, response }: HttpContextContract) {
|
||||
console.time('OVERVIEW_REQUEST')
|
||||
const { puuid, region, season } = await request.validate(SummonerOverviewValidator)
|
||||
const finalJSON: any = {}
|
||||
|
||||
// Summoner in DB
|
||||
let summonerDB = await Summoner.findOne({ puuid: puuid })
|
||||
if (!summonerDB) {
|
||||
summonerDB = await Summoner.create({ puuid: puuid })
|
||||
}
|
||||
const summonerDB = await Summoner.firstOrCreate({ puuid: puuid })
|
||||
|
||||
// MATCHES BASIC
|
||||
const gameIds = summonerDB.matchList!.slice(0)
|
||||
.filter(m => {
|
||||
return season ? m.seasonMatch === season : true
|
||||
})
|
||||
.slice(0, 10)
|
||||
.map(({ gameId }) => gameId)
|
||||
finalJSON.matchesDetails = await MatchService.getMatches(puuid, accountId, region, gameIds, summonerDB)
|
||||
const matchlist = await summonerDB
|
||||
.related('matchList')
|
||||
.query()
|
||||
.select('matchId')
|
||||
.orderBy('matchId', 'desc')
|
||||
.limit(10)
|
||||
const matchIds = matchlist.map((m) => m.matchId)
|
||||
|
||||
finalJSON.matchesDetails = await MatchService.getMatches(region, matchIds, puuid)
|
||||
|
||||
// STATS
|
||||
console.time('STATS')
|
||||
finalJSON.stats = await StatsService.getSummonerStats(puuid, season)
|
||||
console.timeEnd('STATS')
|
||||
|
||||
// SAVE IN DB
|
||||
await summonerDB.save()
|
||||
|
||||
console.timeEnd('overview')
|
||||
console.timeEnd('OVERVIEW_REQUEST')
|
||||
return response.json(finalJSON)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')
|
||||
const { puuid, queue, season } = await request.validate(SummonerChampionValidator)
|
||||
const championStats = await MatchRepository.championCompleteStats(puuid, queue, season)
|
||||
const championStatsSerialized = championStats.map((champion) => {
|
||||
return {
|
||||
...champion,
|
||||
champion: BasicMatchSerializer.getChampion(champion.id),
|
||||
}
|
||||
})
|
||||
console.timeEnd('championsRequest')
|
||||
return response.json(championStats)
|
||||
return response.json(championStatsSerialized)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: get records view summoner data
|
||||
* @param ctx
|
||||
* @param ctx
|
||||
*/
|
||||
public async records ({ request, response }: HttpContextContract) {
|
||||
public async records({ request, response }: HttpContextContract) {
|
||||
console.time('recordsRequest')
|
||||
const { puuid, season } = await request.validate(SummonerRecordValidator)
|
||||
const records = await MatchRepository.records(puuid, season)
|
||||
const records = await MatchRepository.records(puuid)
|
||||
const recordsSerialized = records.map((record) => {
|
||||
return {
|
||||
...record,
|
||||
what: record.what.split('.')[1],
|
||||
champion: BasicMatchSerializer.getChampion(record.champion_id),
|
||||
}
|
||||
})
|
||||
console.timeEnd('recordsRequest')
|
||||
return response.json(records)
|
||||
return response.json(recordsSerialized)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - Return live match detail
|
||||
* @param ctx
|
||||
* @param ctx
|
||||
*/
|
||||
public async liveMatchDetails ({ request, response }: HttpContextContract) {
|
||||
public async liveMatchDetails({ request, response }: HttpContextContract) {
|
||||
console.time('liveMatchDetails')
|
||||
const { id, region } = await request.validate(SummonerLiveValidator)
|
||||
|
||||
// CURRENT GAME
|
||||
let currentGame = await Jax.Spectator.summonerID(id, region)
|
||||
const currentGame = await Jax.Spectator.summonerID(id, region)
|
||||
|
||||
if (!currentGame) {
|
||||
return response.json(null)
|
||||
}
|
||||
|
||||
currentGame = await LiveMatchTransformer.transform(currentGame, { region })
|
||||
const currentGameSerialized = await LiveMatchSerializer.serializeOneMatch(currentGame, region)
|
||||
console.timeEnd('liveMatchDetails')
|
||||
|
||||
return response.json(currentGame)
|
||||
return response.json(currentGameSerialized)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import Logger from '@ioc:Adonis/Core/Logger'
|
|||
import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler'
|
||||
|
||||
export default class ExceptionHandler extends HttpExceptionHandler {
|
||||
constructor () {
|
||||
constructor() {
|
||||
super(Logger)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,134 +1,40 @@
|
|||
import { Model } from '@ioc:Mongodb/Model'
|
||||
import { BaseModel, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm'
|
||||
import MatchPlayer from './MatchPlayer'
|
||||
import MatchTeam from './MatchTeam'
|
||||
|
||||
export interface MatchModel extends ParticipantDetails {
|
||||
account_id: string,
|
||||
summoner_puuid: string,
|
||||
gameId: number,
|
||||
result: string,
|
||||
allyTeam: ParticipantBasic[],
|
||||
enemyTeam: ParticipantBasic[],
|
||||
map: number,
|
||||
gamemode: number,
|
||||
date: number,
|
||||
region: string,
|
||||
season: number,
|
||||
time: number,
|
||||
newMatch?: boolean,
|
||||
}
|
||||
export default class Match extends BaseModel {
|
||||
public static selfAssignPrimaryKey = true
|
||||
|
||||
export interface ParticipantDetails {
|
||||
name: string,
|
||||
summonerId: string,
|
||||
champion: Champion,
|
||||
role: string,
|
||||
primaryRune: string | null,
|
||||
secondaryRune: string | null,
|
||||
level: number,
|
||||
items: (Item | null)[],
|
||||
firstSum: SummonerSpell | number | null,
|
||||
secondSum: SummonerSpell | number | null,
|
||||
stats: Stats,
|
||||
percentStats?: PercentStats
|
||||
rank?: Rank | null,
|
||||
perks?: Perks
|
||||
}
|
||||
@column({ isPrimary: true })
|
||||
public id: string
|
||||
|
||||
export interface Champion<T = number, U = string> {
|
||||
id: number | T,
|
||||
name: string | U,
|
||||
alias?: string,
|
||||
roles?: string[],
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface SummonerSpell {
|
||||
name: string,
|
||||
description: string,
|
||||
icon: string
|
||||
}
|
||||
|
||||
export interface Rank {
|
||||
tier: string,
|
||||
shortName: string | number
|
||||
}
|
||||
|
||||
export interface Perks {
|
||||
primaryStyle: number;
|
||||
secondaryStyle: number;
|
||||
selected: number[];
|
||||
}
|
||||
|
||||
export interface ParticipantBasic {
|
||||
account_id: string,
|
||||
name: string,
|
||||
role: string,
|
||||
champion: Champion
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
image: string,
|
||||
name: string,
|
||||
description: string,
|
||||
price: number
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
kills: number,
|
||||
deaths: number,
|
||||
assists: number,
|
||||
minions: number,
|
||||
vision: number,
|
||||
gold: number,
|
||||
dmgChamp: number,
|
||||
dmgObj: number,
|
||||
dmgTaken: number,
|
||||
kda: number | string,
|
||||
realKda: number,
|
||||
criticalStrike?: number,
|
||||
killingSpree?: number,
|
||||
doubleKills?: number,
|
||||
tripleKills?: number,
|
||||
quadraKills?: number,
|
||||
pentaKills?: number,
|
||||
heal?: number,
|
||||
towers?: number,
|
||||
longestLiving?: number,
|
||||
kp: number | string,
|
||||
}
|
||||
|
||||
export interface PercentStats {
|
||||
minions: number,
|
||||
vision: number,
|
||||
gold: string,
|
||||
dmgChamp: string,
|
||||
dmgObj: string,
|
||||
dmgTaken: string,
|
||||
}
|
||||
|
||||
export default class Match extends Model implements MatchModel {
|
||||
public static collectionName = 'matches'
|
||||
|
||||
public account_id: string
|
||||
public summoner_puuid: string
|
||||
@column()
|
||||
public gameId: number
|
||||
public result: string
|
||||
public allyTeam: ParticipantBasic[]
|
||||
public enemyTeam: ParticipantBasic[]
|
||||
|
||||
@column()
|
||||
public map: number
|
||||
|
||||
@column()
|
||||
public gamemode: number
|
||||
|
||||
@column()
|
||||
public date: number
|
||||
|
||||
@column()
|
||||
public region: string
|
||||
|
||||
@column()
|
||||
public result: number
|
||||
|
||||
@column()
|
||||
public season: number
|
||||
public time: number
|
||||
public name: string
|
||||
public summonerId: string
|
||||
public champion: Champion
|
||||
public role: string
|
||||
public primaryRune: string
|
||||
public secondaryRune: string
|
||||
public level: number
|
||||
public items: Item[]
|
||||
public firstSum: number
|
||||
public secondSum: number
|
||||
public stats: Stats
|
||||
|
||||
@column()
|
||||
public gameDuration: number
|
||||
|
||||
@hasMany(() => MatchTeam)
|
||||
public teams: HasMany<typeof MatchTeam>
|
||||
|
||||
@hasMany(() => MatchPlayer)
|
||||
public players: HasMany<typeof MatchPlayer>
|
||||
}
|
||||
|
|
|
|||
165
server/app/Models/MatchPlayer.ts
Normal file
165
server/app/Models/MatchPlayer.ts
Normal 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[]
|
||||
}
|
||||
38
server/app/Models/MatchPlayerRank.ts
Normal file
38
server/app/Models/MatchPlayerRank.ts
Normal 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
|
||||
}
|
||||
40
server/app/Models/MatchTeam.ts
Normal file
40
server/app/Models/MatchTeam.ts
Normal 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[]
|
||||
}
|
||||
|
|
@ -1,21 +1,36 @@
|
|||
import { Model } from '@ioc:Mongodb/Model'
|
||||
import { MatchReferenceDto } from 'App/Services/Jax/src/Endpoints/MatchlistEndpoint'
|
||||
import { DateTime } from 'luxon'
|
||||
import { BaseModel, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm'
|
||||
import SummonerMatchlist from './SummonerMatchlist'
|
||||
import SummonerName from './SummonerName'
|
||||
import MatchPlayer from './MatchPlayer'
|
||||
|
||||
export interface SummonerModel {
|
||||
puuid: string,
|
||||
matchList?: MatchReferenceDto[],
|
||||
names?: SummonerNames[]
|
||||
}
|
||||
|
||||
interface SummonerNames {
|
||||
name: string,
|
||||
date: Date
|
||||
}
|
||||
|
||||
export default class Summoner extends Model implements SummonerModel {
|
||||
public static collectionName = 'summoners'
|
||||
export default class Summoner extends BaseModel {
|
||||
public static selfAssignPrimaryKey = true
|
||||
|
||||
@column({ isPrimary: true })
|
||||
public puuid: string
|
||||
public matchList?: MatchReferenceDto[]
|
||||
public names?: SummonerNames[]
|
||||
|
||||
@column.dateTime({ autoCreate: true })
|
||||
public createdAt: DateTime
|
||||
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
public updatedAt: DateTime
|
||||
|
||||
@hasMany(() => SummonerMatchlist, {
|
||||
localKey: 'puuid',
|
||||
foreignKey: 'summonerPuuid',
|
||||
})
|
||||
public matchList: HasMany<typeof SummonerMatchlist>
|
||||
|
||||
@hasMany(() => MatchPlayer, {
|
||||
localKey: 'puuid',
|
||||
foreignKey: 'summonerPuuid',
|
||||
})
|
||||
public matches: HasMany<typeof MatchPlayer>
|
||||
|
||||
@hasMany(() => SummonerName, {
|
||||
localKey: 'puuid',
|
||||
foreignKey: 'summonerPuuid',
|
||||
})
|
||||
public names: HasMany<typeof SummonerName>
|
||||
}
|
||||
|
|
|
|||
21
server/app/Models/SummonerMatchlist.ts
Normal file
21
server/app/Models/SummonerMatchlist.ts
Normal 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>
|
||||
}
|
||||
23
server/app/Models/SummonerName.ts
Normal file
23
server/app/Models/SummonerName.ts
Normal 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
|
||||
}
|
||||
160
server/app/Parsers/MatchParser.ts
Normal file
160
server/app/Parsers/MatchParser.ts
Normal 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()
|
||||
43
server/app/Parsers/MatchPlayerRankParser.ts
Normal file
43
server/app/Parsers/MatchPlayerRankParser.ts
Normal 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()
|
||||
246
server/app/Parsers/MatchV4Parser.ts
Normal file
246
server/app/Parsers/MatchV4Parser.ts
Normal 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()
|
||||
27
server/app/Parsers/ParsedType.ts
Normal file
27
server/app/Parsers/ParsedType.ts
Normal 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
|
||||
}
|
||||
|
|
@ -1,337 +1,257 @@
|
|||
import mongodb from '@ioc:Mongodb/Database'
|
||||
import { Collection } from 'mongodb'
|
||||
import Database from '@ioc:Adonis/Lucid/Database'
|
||||
|
||||
class MatchRepository {
|
||||
private collection: Collection
|
||||
private readonly JOIN_MATCHES = 'INNER JOIN matches ON matches.id = match_players.match_id'
|
||||
private readonly JOIN_TEAMS =
|
||||
'INNER JOIN match_teams ON match_players.match_id = match_teams.match_id AND match_players.team = match_teams.color'
|
||||
private readonly JOIN_ALL = `${this.JOIN_MATCHES} ${this.JOIN_TEAMS}`
|
||||
|
||||
constructor () {
|
||||
this.getCollection()
|
||||
private readonly GLOBAL_FILTERS = `
|
||||
match_players.summoner_puuid = :puuid
|
||||
AND match_players.remake = 0
|
||||
AND matches.gamemode NOT IN (800, 810, 820, 830, 840, 850, 2000, 2010, 2020)
|
||||
`
|
||||
|
||||
public async recentActivity(puuid: string) {
|
||||
const query = `
|
||||
SELECT
|
||||
to_timestamp(matches.date/1000)::date as day,
|
||||
COUNT(match_players.id) as count
|
||||
FROM
|
||||
match_players
|
||||
${this.JOIN_MATCHES}
|
||||
WHERE
|
||||
match_players.summoner_puuid = :puuid
|
||||
GROUP BY
|
||||
day
|
||||
ORDER BY
|
||||
day
|
||||
`
|
||||
const { rows } = await Database.rawQuery(query, { puuid })
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic matchParams used in a lot of requests
|
||||
* @param puuid of the summoner
|
||||
*/
|
||||
private matchParams (puuid: string, season?: number) {
|
||||
return {
|
||||
summoner_puuid: puuid,
|
||||
result: { $not: { $eq: 'Remake' } },
|
||||
gamemode: { $nin: [800, 810, 820, 830, 840, 850, 2000, 2010, 2020] },
|
||||
season: season ? season : { $exists: true },
|
||||
}
|
||||
public async globalStats(puuid: string) {
|
||||
const query = `
|
||||
SELECT
|
||||
SUM(match_players.kills) as kills,
|
||||
SUM(match_players.deaths) as deaths,
|
||||
SUM(match_players.assists) as assists,
|
||||
SUM(match_players.minions) as minions,
|
||||
SUM(matches.game_duration) as time,
|
||||
SUM(match_players.vision_score) as vision,
|
||||
COUNT(match_players.id) as count,
|
||||
AVG(match_players.kp) as kp,
|
||||
SUM(match_players.win) as wins,
|
||||
SUM(match_players.loss) as losses
|
||||
FROM
|
||||
match_players
|
||||
${this.JOIN_MATCHES}
|
||||
WHERE
|
||||
${this.GLOBAL_FILTERS}
|
||||
LIMIT
|
||||
1
|
||||
`
|
||||
const { rows } = await Database.rawQuery(query, { puuid })
|
||||
return rows[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the aggregate mongo query
|
||||
* @param puuid
|
||||
* @param matchParams
|
||||
* @param intermediateSteps
|
||||
* @param groupId
|
||||
* @param groupParams
|
||||
* @param finalSteps
|
||||
*/
|
||||
private async aggregate (
|
||||
puuid: string,
|
||||
matchParams: object,
|
||||
intermediateSteps: any[],
|
||||
groupId: any,
|
||||
groupParams: object,
|
||||
finalSteps: any[],
|
||||
season?: number,
|
||||
) {
|
||||
return this.collection.aggregate([
|
||||
{
|
||||
$match: {
|
||||
...this.matchParams(puuid, season),
|
||||
...matchParams,
|
||||
},
|
||||
},
|
||||
...intermediateSteps,
|
||||
{
|
||||
$group: {
|
||||
_id: groupId,
|
||||
count: { $sum: 1 },
|
||||
wins: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ['$result', 'Win'] }, 1, 0],
|
||||
},
|
||||
},
|
||||
losses: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ['$result', 'Fail'] }, 1, 0],
|
||||
},
|
||||
},
|
||||
...groupParams,
|
||||
},
|
||||
},
|
||||
...finalSteps,
|
||||
]).toArray()
|
||||
public async gamemodeStats(puuid: string) {
|
||||
const query = `
|
||||
SELECT
|
||||
matches.gamemode as id,
|
||||
COUNT(match_players.id) as count,
|
||||
SUM(match_players.win) as wins,
|
||||
SUM(match_players.loss) as losses
|
||||
FROM
|
||||
match_players
|
||||
${this.JOIN_MATCHES}
|
||||
WHERE
|
||||
${this.GLOBAL_FILTERS}
|
||||
GROUP BY
|
||||
matches.gamemode
|
||||
ORDER BY
|
||||
count DESC
|
||||
`
|
||||
const { rows } = await Database.rawQuery(query, { puuid })
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MongoDB matches collection
|
||||
*/
|
||||
public async getCollection () {
|
||||
if (!this.collection) {
|
||||
this.collection = await mongodb.connection().collection('matches')
|
||||
}
|
||||
public async roleStats(puuid: string) {
|
||||
const query = `
|
||||
SELECT
|
||||
match_players.team_position as role,
|
||||
COUNT(match_players.id) as count,
|
||||
SUM(match_players.win) as wins,
|
||||
SUM(match_players.loss) as losses
|
||||
FROM
|
||||
match_players
|
||||
${this.JOIN_MATCHES}
|
||||
WHERE
|
||||
${this.GLOBAL_FILTERS}
|
||||
AND match_players.team_position != 0
|
||||
GROUP BY
|
||||
role
|
||||
`
|
||||
const { rows } = await Database.rawQuery(query, { puuid })
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Summoner's statistics for the N most played champions
|
||||
* @param puuid of the summoner
|
||||
* @param limit number of champions to fetch
|
||||
* @param season
|
||||
*/
|
||||
public async championStats (puuid: string, limit = 5, season?: number) {
|
||||
const groupParams = {
|
||||
champion: { $first: '$champion' },
|
||||
kills: { $sum: '$stats.kills' },
|
||||
deaths: { $sum: '$stats.deaths' },
|
||||
assists: { $sum: '$stats.assists' },
|
||||
}
|
||||
const finalSteps = [
|
||||
{ $sort: { 'count': -1, 'champion.name': 1 } },
|
||||
{ $limit: limit },
|
||||
public async championStats(puuid: string, limit: number) {
|
||||
const query = `
|
||||
SELECT
|
||||
match_players.champion_id as id,
|
||||
SUM(match_players.assists) as assists,
|
||||
SUM(match_players.deaths) as deaths,
|
||||
SUM(match_players.kills) as kills,
|
||||
COUNT(match_players.id) as count,
|
||||
SUM(match_players.win) as wins,
|
||||
SUM(match_players.loss) as losses
|
||||
FROM
|
||||
match_players
|
||||
${this.JOIN_MATCHES}
|
||||
WHERE
|
||||
${this.GLOBAL_FILTERS}
|
||||
GROUP BY
|
||||
match_players.champion_id
|
||||
ORDER BY
|
||||
count DESC, match_players.champion_id
|
||||
LIMIT
|
||||
:limit
|
||||
`
|
||||
const { rows } = await Database.rawQuery(query, { puuid, limit })
|
||||
return rows
|
||||
}
|
||||
|
||||
public async championClassStats(puuid: string) {
|
||||
const query = `
|
||||
SELECT
|
||||
match_players.champion_role as id,
|
||||
COUNT(match_players.id) as count,
|
||||
SUM(match_players.win) as wins,
|
||||
SUM(match_players.loss) as losses
|
||||
FROM
|
||||
match_players
|
||||
${this.JOIN_MATCHES}
|
||||
WHERE
|
||||
${this.GLOBAL_FILTERS}
|
||||
GROUP BY
|
||||
match_players.champion_role
|
||||
ORDER BY
|
||||
count DESC
|
||||
`
|
||||
const { rows } = await Database.rawQuery(query, { puuid })
|
||||
return rows
|
||||
}
|
||||
|
||||
public async championCompleteStats(puuid: string, queue?: number, season?: number) {
|
||||
const query = `
|
||||
SELECT
|
||||
match_players.champion_id as id,
|
||||
SUM(match_players.assists) as assists,
|
||||
SUM(match_players.deaths) as deaths,
|
||||
SUM(match_players.kills) as kills,
|
||||
COUNT(match_players.id) as count,
|
||||
SUM(match_players.win) as wins,
|
||||
SUM(match_players.loss) as losses,
|
||||
AVG(matches.game_duration)::int as "gameLength",
|
||||
AVG(match_players.minions)::int as minions,
|
||||
AVG(match_players.gold)::int as gold,
|
||||
AVG(match_players.damage_dealt_champions)::int as "dmgChamp",
|
||||
AVG(match_players.damage_taken)::int as "dmgTaken",
|
||||
AVG(match_players.kp) as kp,
|
||||
MAX(matches.date) as date
|
||||
FROM
|
||||
match_players
|
||||
${this.JOIN_MATCHES}
|
||||
WHERE
|
||||
${this.GLOBAL_FILTERS}
|
||||
GROUP BY
|
||||
match_players.champion_id
|
||||
ORDER BY
|
||||
count DESC, match_players.champion_id
|
||||
`
|
||||
const { rows } = await Database.rawQuery(query, { puuid })
|
||||
return rows
|
||||
}
|
||||
|
||||
public async mates(puuid: string) {
|
||||
const query = `
|
||||
SELECT
|
||||
(array_agg(mates.summoner_name ORDER BY mates.match_id DESC))[1] as name,
|
||||
COUNT(match_players.id) as count,
|
||||
SUM(match_players.win) as wins,
|
||||
SUM(match_players.loss) as losses
|
||||
FROM
|
||||
match_players
|
||||
${this.JOIN_ALL}
|
||||
INNER JOIN match_players as mates ON match_players.match_id = mates.match_id AND match_players.team = mates.team
|
||||
WHERE
|
||||
${this.GLOBAL_FILTERS}
|
||||
GROUP BY
|
||||
mates.summoner_puuid
|
||||
ORDER BY
|
||||
count DESC, wins DESC
|
||||
LIMIT
|
||||
15
|
||||
`
|
||||
const { rows } = await Database.rawQuery(query, { puuid })
|
||||
|
||||
// Remove the Summoner himself + unique game mates
|
||||
return rows.splice(1).filter((row) => row.count > 1)
|
||||
}
|
||||
|
||||
public async records(puuid: string) {
|
||||
const fields = [
|
||||
'match_players.kills',
|
||||
'match_players.deaths',
|
||||
'match_players.assists',
|
||||
'match_players.gold',
|
||||
'matches.game_duration',
|
||||
'match_players.minions',
|
||||
'match_players.kda',
|
||||
'match_players.damage_taken',
|
||||
'match_players.damage_dealt_champions',
|
||||
'match_players.damage_dealt_objectives',
|
||||
'match_players.kp',
|
||||
'match_players.vision_score',
|
||||
'match_players.critical_strike',
|
||||
'match_players.time_spent_living',
|
||||
'match_players.heal',
|
||||
'match_players.turret_kills',
|
||||
'match_players.killing_spree',
|
||||
'match_players.double_kills',
|
||||
'match_players.triple_kills',
|
||||
'match_players.quadra_kills',
|
||||
'match_players.penta_kills',
|
||||
]
|
||||
return this.aggregate(puuid, {}, [], '$champion.id', groupParams, finalSteps, season)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Summoner's statistics for all played champion classes
|
||||
* @param puuid of the summoner
|
||||
* @param season
|
||||
*/
|
||||
public async championClassStats (puuid: string, season?: number) {
|
||||
const groupId = { '$arrayElemAt': ['$champion.roles', 0] }
|
||||
return this.aggregate(puuid, {}, [], groupId, {}, [], season)
|
||||
}
|
||||
const query = fields
|
||||
.map((field) => {
|
||||
return `
|
||||
(SELECT
|
||||
'${field}' AS what,
|
||||
${field} AS amount,
|
||||
match_players.win as result,
|
||||
matches.id,
|
||||
matches.date,
|
||||
matches.gamemode,
|
||||
match_players.champion_id
|
||||
FROM
|
||||
match_players
|
||||
${this.JOIN_MATCHES}
|
||||
WHERE
|
||||
${this.GLOBAL_FILTERS}
|
||||
ORDER BY
|
||||
${field} DESC, matches.id
|
||||
LIMIT
|
||||
1)
|
||||
`
|
||||
})
|
||||
.join('UNION ALL ')
|
||||
|
||||
/**
|
||||
* Get Summoner's complete statistics for the all played champs
|
||||
* @param puuid of the summoner
|
||||
* @param queue of the matches to fetch, if not set: get all matches
|
||||
* @param season of the matches to fetch, if not set: get all seasons
|
||||
*/
|
||||
public async championCompleteStats (puuid: string, queue?: number, season?: number) {
|
||||
const matchParams = queue ? { gamemode: { $eq: Number(queue) } } : {}
|
||||
const groupParams = {
|
||||
time: { $sum: '$time' },
|
||||
gameLength: { $avg: '$time' },
|
||||
date: { $max: '$date' },
|
||||
champion: { $first: '$champion' },
|
||||
kills: { $sum: '$stats.kills' },
|
||||
deaths: { $sum: '$stats.deaths' },
|
||||
assists: { $sum: '$stats.assists' },
|
||||
minions: { $avg: '$stats.minions' },
|
||||
gold: { $avg: '$stats.gold' },
|
||||
dmgChamp: { $avg: '$stats.dmgChamp' },
|
||||
dmgTaken: { $avg: '$stats.dmgTaken' },
|
||||
kp: { $avg: '$stats.kp' },
|
||||
}
|
||||
const finalSteps = [
|
||||
{ $sort: { 'count': -1, 'champion.name': 1 } },
|
||||
]
|
||||
return this.aggregate(puuid, matchParams, [], '$champion.id', groupParams, finalSteps, season)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Summoner's statistics for all played modes
|
||||
* @param puuid of the summoner
|
||||
* @param season
|
||||
*/
|
||||
public async gamemodeStats (puuid: string, season?: number) {
|
||||
return this.aggregate(puuid, {}, [], '$gamemode', {}, [], season)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global Summoner's statistics
|
||||
* @param puuid of the summoner
|
||||
* @param season
|
||||
*/
|
||||
public async globalStats (puuid: string, season?: number) {
|
||||
const groupParams = {
|
||||
time: { $sum: '$time' },
|
||||
kills: { $sum: '$stats.kills' },
|
||||
deaths: { $sum: '$stats.deaths' },
|
||||
assists: { $sum: '$stats.assists' },
|
||||
minions: { $sum: '$stats.minions' },
|
||||
vision: { $sum: '$stats.vision' },
|
||||
kp: { $avg: '$stats.kp' },
|
||||
}
|
||||
return this.aggregate(puuid, {}, [], null, groupParams, [], season)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Summoner's all records
|
||||
* @param puuid of the summoner
|
||||
* @param season of the matches to fetch, if null get all seasons
|
||||
*/
|
||||
public async records (puuid: string, season?: number) {
|
||||
const records = await this.collection.aggregate([
|
||||
{
|
||||
$match: {
|
||||
...this.matchParams(puuid, season),
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
maxKills: { $max: '$stats.kills' },
|
||||
maxDeaths: { $max: '$stats.deaths' },
|
||||
maxAssists: { $max: '$stats.assists' },
|
||||
maxGold: { $max: '$stats.gold' },
|
||||
maxTime: { $max: '$time' },
|
||||
maxMinions: { $max: '$stats.minions' },
|
||||
maxKda: { $max: '$stats.realKda' },
|
||||
maxDmgTaken: { $max: '$stats.dmgTaken' },
|
||||
maxDmgChamp: { $max: '$stats.dmgChamp' },
|
||||
maxDmgObj: { $max: '$stats.dmgObj' },
|
||||
maxKp: { $max: '$stats.kp' },
|
||||
maxVision: { $max: '$stats.vision' },
|
||||
maxCriticalStrike: { $max: '$stats.criticalStrike' },
|
||||
maxLiving: { $max: '$stats.longestLiving' },
|
||||
maxHeal: { $max: '$stats.heal' },
|
||||
maxTowers: { $max: '$stats.towers' },
|
||||
maxKillingSpree: { $max: '$stats.killingSpree' },
|
||||
maxDouble: { $max: '$stats.doubleKills' },
|
||||
maxTriple: { $max: '$stats.tripleKills' },
|
||||
maxQuadra: { $max: '$stats.quadraKills' },
|
||||
maxPenta: { $max: '$stats.pentaKills' },
|
||||
docs: {
|
||||
'$push': {
|
||||
'champion': '$champion',
|
||||
'gameId': '$gameId',
|
||||
'kills': '$stats.kills',
|
||||
'deaths': '$stats.deaths',
|
||||
'assists': '$stats.assists',
|
||||
'gold': '$stats.gold',
|
||||
'time': '$time',
|
||||
'minions': '$stats.minions',
|
||||
'kda': '$stats.realKda',
|
||||
'dmgTaken': '$stats.dmgTaken',
|
||||
'dmgChamp': '$stats.dmgChamp',
|
||||
'dmgObj': '$stats.dmgObj',
|
||||
'kp': '$stats.kp',
|
||||
'vision': '$stats.vision',
|
||||
'criticalStrike': '$stats.criticalStrike',
|
||||
'longestLiving': '$stats.longestLiving',
|
||||
'heal': '$stats.heal',
|
||||
'towers': '$stats.towers',
|
||||
'killingSpree': '$stats.killingSpree',
|
||||
'doubleKills': '$stats.doubleKills',
|
||||
'tripleKills': '$stats.tripleKills',
|
||||
'quadraKills': '$stats.quadraKills',
|
||||
'pentaKills': '$stats.pentaKills',
|
||||
'result': '$result',
|
||||
'date': '$date',
|
||||
'gamemode': '$gamemode',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
/* eslint-disable max-len */
|
||||
maxKills: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.kills', '$maxKills'] } } }, 0] },
|
||||
maxDeaths: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.deaths', '$maxDeaths'] } } }, 0] },
|
||||
maxAssists: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.assists', '$maxAssists'] } } }, 0] },
|
||||
maxGold: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.gold', '$maxGold'] } } }, 0] },
|
||||
maxTime: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.time', '$maxTime'] } } }, 0] },
|
||||
maxMinions: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.minions', '$maxMinions'] } } }, 0] },
|
||||
maxKda: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.kda', '$maxKda'] } } }, 0] },
|
||||
maxDmgTaken: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.dmgTaken', '$maxDmgTaken'] } } }, 0] },
|
||||
maxDmgChamp: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.dmgChamp', '$maxDmgChamp'] } } }, 0] },
|
||||
maxDmgObj: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.dmgObj', '$maxDmgObj'] } } }, 0] },
|
||||
maxKp: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.kp', '$maxKp'] } } }, 0] },
|
||||
maxVision: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.vision', '$maxVision'] } } }, 0] },
|
||||
maxCriticalStrike: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.criticalStrike', '$maxCriticalStrike'] } } }, 0] },
|
||||
maxLiving: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.longestLiving', '$maxLiving'] } } }, 0] },
|
||||
maxHeal: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.heal', '$maxHeal'] } } }, 0] },
|
||||
maxTowers: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.towers', '$maxTowers'] } } }, 0] },
|
||||
maxKillingSpree: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.killingSpree', '$maxKillingSpree'] } } }, 0] },
|
||||
maxDouble: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.doubleKills', '$maxDouble'] } } }, 0] },
|
||||
maxTriple: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.tripleKills', '$maxTriple'] } } }, 0] },
|
||||
maxQuadra: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.quadraKills', '$maxQuadra'] } } }, 0] },
|
||||
maxPenta: { $arrayElemAt: [{ $filter: { input: '$docs', cond: { $eq: ['$$this.pentaKills', '$maxPenta'] } } }, 0] },
|
||||
},
|
||||
},
|
||||
]).toArray()
|
||||
|
||||
return records[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Summoner's statistics for the 5 differnt roles
|
||||
* @param puuid of the summoner
|
||||
* @param season
|
||||
*/
|
||||
public async roleStats (puuid: string, season?: number) {
|
||||
const matchParams = {
|
||||
role: { $not: { $eq: 'NONE' } },
|
||||
}
|
||||
const finalSteps = [
|
||||
{
|
||||
$project: {
|
||||
role: '$_id',
|
||||
count: '$count',
|
||||
wins: '$wins',
|
||||
losses: '$losses',
|
||||
},
|
||||
},
|
||||
]
|
||||
return this.aggregate(puuid, matchParams, [], '$role', {}, finalSteps, season)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Summoner's played seasons
|
||||
* @param puuid of the summoner
|
||||
*/
|
||||
public async seasons (puuid: string) {
|
||||
return this.collection.aggregate([
|
||||
{
|
||||
$match: {
|
||||
...this.matchParams(puuid),
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: { _id: '$season' },
|
||||
},
|
||||
]).toArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Summoner's mates list
|
||||
* @param puuid of the summoner
|
||||
* @param season
|
||||
*/
|
||||
public async mates (puuid: string, season?: number) {
|
||||
const intermediateSteps = [
|
||||
{ $sort: { 'gameId': -1 } },
|
||||
{ $unwind: '$allyTeam' },
|
||||
]
|
||||
const groupParams = {
|
||||
account_id: { $first: '$account_id' },
|
||||
name: { $first: '$allyTeam.name' },
|
||||
mateId: { $first: '$allyTeam.account_id' },
|
||||
}
|
||||
const finalSteps = [
|
||||
{
|
||||
'$addFields': {
|
||||
'idEq': { '$eq': ['$mateId', '$account_id'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
'idEq': false,
|
||||
'count': { $gte: 2 },
|
||||
},
|
||||
},
|
||||
{ $sort: { 'count': -1, 'name': 1 } },
|
||||
{ $limit: 15 },
|
||||
]
|
||||
return this.aggregate(puuid, {}, intermediateSteps, '$allyTeam.account_id', groupParams, finalSteps, season)
|
||||
const { rows } = await Database.rawQuery(query, { puuid })
|
||||
return rows
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
80
server/app/Serializers/BasicMatchSerializer.ts
Normal file
80
server/app/Serializers/BasicMatchSerializer.ts
Normal 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()
|
||||
153
server/app/Serializers/DetailedMatchSerializer.ts
Normal file
153
server/app/Serializers/DetailedMatchSerializer.ts
Normal 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()
|
||||
81
server/app/Serializers/LiveMatchSerializer.ts
Normal file
81
server/app/Serializers/LiveMatchSerializer.ts
Normal 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()
|
||||
20
server/app/Serializers/MatchPlayerRankSerializer.ts
Normal file
20
server/app/Serializers/MatchPlayerRankSerializer.ts
Normal 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()
|
||||
135
server/app/Serializers/MatchSerializer.ts
Normal file
135
server/app/Serializers/MatchSerializer.ts
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { PerkDTO, PerkStyleDTO } from 'App/Services/Jax/src/Endpoints/CDragonEndpoint'
|
||||
|
||||
class RuneTransformer {
|
||||
public transformPerks (perks: PerkDTO[]) {
|
||||
class RuneSerializer {
|
||||
public serializePerks(perks: PerkDTO[]) {
|
||||
return perks.reduce((acc, perk) => {
|
||||
acc[perk.id] = {
|
||||
name: perk.name,
|
||||
|
|
@ -12,18 +12,16 @@ class RuneTransformer {
|
|||
}, {})
|
||||
}
|
||||
|
||||
public transformStyles (styles: PerkStyleDTO[]) {
|
||||
public serializeStyles(styles: PerkStyleDTO[]) {
|
||||
return styles.reduce((acc, style) => {
|
||||
acc[style.id] = {
|
||||
name: style.name,
|
||||
icon: style.iconPath,
|
||||
slots: style.slots
|
||||
.filter(s => s.type !== 'kStatMod')
|
||||
.map(s => s.perks),
|
||||
slots: style.slots.filter((s) => s.type !== 'kStatMod').map((s) => s.perks),
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
export default new RuneTransformer()
|
||||
export default new RuneSerializer()
|
||||
211
server/app/Serializers/SerializedTypes.ts
Normal file
211
server/app/Serializers/SerializedTypes.ts
Normal 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
|
||||
}
|
||||
64
server/app/Services/CDragonService.ts
Normal file
64
server/app/Services/CDragonService.ts
Normal 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()
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import Env from '@ioc:Adonis/Core/Env'
|
||||
|
||||
export interface JaxConfig {
|
||||
key: string,
|
||||
region: string,
|
||||
key: string
|
||||
region: string
|
||||
requestOptions: JaxConfigRequestOptions
|
||||
}
|
||||
|
||||
export interface JaxConfigRequestOptions {
|
||||
retriesBeforeAbort: number,
|
||||
delayBeforeRetry: number,
|
||||
retriesBeforeAbort: number
|
||||
delayBeforeRetry: number
|
||||
}
|
||||
|
||||
export const JAX_CONFIG: JaxConfig = {
|
||||
key: Env.get('API_KEY') as string,
|
||||
key: Env.get('RIOT_API_KEY') as string,
|
||||
region: 'euw1',
|
||||
requestOptions: {
|
||||
retriesBeforeAbort: 3,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default class CDragonRequest {
|
|||
private retries: number
|
||||
private sleep: { (ms: number): Promise<void>; <T>(ms: number, value: T): Promise<T> }
|
||||
|
||||
constructor (config: JaxConfig, endpoint: string, cacheTime: number) {
|
||||
constructor(config: JaxConfig, endpoint: string, cacheTime: number) {
|
||||
this.config = config
|
||||
this.endpoint = endpoint
|
||||
this.cacheTime = cacheTime
|
||||
|
|
@ -26,7 +26,7 @@ export default class CDragonRequest {
|
|||
// https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json
|
||||
// https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/summoner-spells.json
|
||||
|
||||
public async execute () {
|
||||
public async execute() {
|
||||
const url = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/${this.endpoint}`
|
||||
|
||||
const requestCached = await Redis.get(url)
|
||||
|
|
|
|||
|
|
@ -2,103 +2,103 @@ import { JaxConfig } from '../../JaxConfig'
|
|||
import CDragonRequest from '../CDragonRequest'
|
||||
|
||||
export interface ChampionDTO {
|
||||
id: number,
|
||||
name: string,
|
||||
alias: string,
|
||||
squarePortraitPath: string,
|
||||
id: number
|
||||
name: string
|
||||
alias: string
|
||||
squarePortraitPath: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export interface ItemDTO {
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
active: boolean,
|
||||
inStore: boolean,
|
||||
from: number[],
|
||||
to: number[],
|
||||
categories: string[],
|
||||
mapStringIdInclusions: string[],
|
||||
maxStacks: number,
|
||||
modeNameInclusions: string[],
|
||||
requiredChampion: string,
|
||||
requiredAlly: string,
|
||||
requiredBuffCurrencyName: string,
|
||||
requiredBuffCurrencyCost: number,
|
||||
specialRecipe: number,
|
||||
isEnchantment: boolean,
|
||||
price: number,
|
||||
priceTotal: number,
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
active: boolean
|
||||
inStore: boolean
|
||||
from: number[]
|
||||
to: number[]
|
||||
categories: string[]
|
||||
mapStringIdInclusions: string[]
|
||||
maxStacks: number
|
||||
modeNameInclusions: string[]
|
||||
requiredChampion: string
|
||||
requiredAlly: string
|
||||
requiredBuffCurrencyName: string
|
||||
requiredBuffCurrencyCost: number
|
||||
specialRecipe: number
|
||||
isEnchantment: boolean
|
||||
price: number
|
||||
priceTotal: number
|
||||
iconPath: string
|
||||
}
|
||||
|
||||
export interface PerkDTO {
|
||||
id: number,
|
||||
name: string,
|
||||
majorChangePatchVersion: string,
|
||||
tooltip: string,
|
||||
shortDesc: string,
|
||||
longDesc: string,
|
||||
iconPath: string,
|
||||
id: number
|
||||
name: string
|
||||
majorChangePatchVersion: string
|
||||
tooltip: string
|
||||
shortDesc: string
|
||||
longDesc: string
|
||||
iconPath: string
|
||||
endOfGameStatDescs: string[]
|
||||
}
|
||||
|
||||
export interface PerkStyleResponse {
|
||||
schemaVersion: string,
|
||||
schemaVersion: string
|
||||
styles: PerkStyleDTO[]
|
||||
}
|
||||
|
||||
export interface PerkStyleDTO {
|
||||
id: number,
|
||||
name: string,
|
||||
tooltip: string,
|
||||
iconPath: string,
|
||||
assetMap: { [key: string]: string },
|
||||
isAdvanced: boolean,
|
||||
allowedSubStyles: number[],
|
||||
subStyleBonus: { styleId: number, perkId: number }[],
|
||||
slots: { type: string, slotLabel: string, perks: number[] }[],
|
||||
defaultPageName: string,
|
||||
defaultSubStyle: number,
|
||||
defaultPerks: number[],
|
||||
defaultPerksWhenSplashed: number[],
|
||||
defaultStatModsPerSubStyle: { id: string, perks: number[] }[]
|
||||
id: number
|
||||
name: string
|
||||
tooltip: string
|
||||
iconPath: string
|
||||
assetMap: { [key: string]: string }
|
||||
isAdvanced: boolean
|
||||
allowedSubStyles: number[]
|
||||
subStyleBonus: { styleId: number; perkId: number }[]
|
||||
slots: { type: string; slotLabel: string; perks: number[] }[]
|
||||
defaultPageName: string
|
||||
defaultSubStyle: number
|
||||
defaultPerks: number[]
|
||||
defaultPerksWhenSplashed: number[]
|
||||
defaultStatModsPerSubStyle: { id: string; perks: number[] }[]
|
||||
}
|
||||
|
||||
export interface SummonerSpellDTO {
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
summonerLevel: number,
|
||||
cooldown: number,
|
||||
gameModes: string[],
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
summonerLevel: number
|
||||
cooldown: number
|
||||
gameModes: string[]
|
||||
iconPath: string
|
||||
}
|
||||
|
||||
export default class CDragonEndpoint {
|
||||
private config: JaxConfig
|
||||
|
||||
constructor (config: JaxConfig) {
|
||||
constructor(config: JaxConfig) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
public async champions (): Promise<ChampionDTO[]> {
|
||||
public async champions(): Promise<ChampionDTO[]> {
|
||||
return new CDragonRequest(this.config, 'champion-summary.json', 36000).execute()
|
||||
}
|
||||
|
||||
public async items (): Promise<ItemDTO[]> {
|
||||
public async items(): Promise<ItemDTO[]> {
|
||||
return new CDragonRequest(this.config, 'items.json', 36000).execute()
|
||||
}
|
||||
|
||||
public async perks (): Promise<PerkDTO[]> {
|
||||
public async perks(): Promise<PerkDTO[]> {
|
||||
return new CDragonRequest(this.config, 'perks.json', 36000).execute()
|
||||
}
|
||||
|
||||
public async perkstyles (): Promise<PerkStyleResponse> {
|
||||
public async perkstyles(): Promise<PerkStyleResponse> {
|
||||
return new CDragonRequest(this.config, 'perkstyles.json', 36000).execute()
|
||||
}
|
||||
|
||||
public async summonerSpells (): Promise<SummonerSpellDTO[]> {
|
||||
public async summonerSpells(): Promise<SummonerSpellDTO[]> {
|
||||
return new CDragonRequest(this.config, 'summoner-spells.json', 36000).execute()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,26 +4,26 @@ import { JaxConfig } from '../../JaxConfig'
|
|||
import JaxRequest from '../JaxRequest'
|
||||
|
||||
export interface LeagueEntryDTO {
|
||||
leagueId: string;
|
||||
queueType: string;
|
||||
tier: string;
|
||||
rank: string;
|
||||
summonerId: string;
|
||||
summonerName: string;
|
||||
leaguePoints: number;
|
||||
wins: number;
|
||||
losses: number;
|
||||
veteran: boolean;
|
||||
inactive: boolean;
|
||||
freshBlood: boolean;
|
||||
hotStreak: boolean;
|
||||
leagueId: string
|
||||
queueType: string
|
||||
tier: string
|
||||
rank: string
|
||||
summonerId: string
|
||||
summonerName: string
|
||||
leaguePoints: number
|
||||
wins: number
|
||||
losses: number
|
||||
veteran: boolean
|
||||
inactive: boolean
|
||||
freshBlood: boolean
|
||||
hotStreak: boolean
|
||||
miniSeries?: MiniSeriesDTO
|
||||
}
|
||||
|
||||
interface MiniSeriesDTO {
|
||||
losses: number,
|
||||
progress: string,
|
||||
target: number,
|
||||
losses: number
|
||||
progress: string
|
||||
target: number
|
||||
wins: number
|
||||
}
|
||||
|
||||
|
|
@ -31,12 +31,12 @@ export default class LeagueEndpoint {
|
|||
private config: JaxConfig
|
||||
private limiter: RiotRateLimiter
|
||||
|
||||
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
this.config = config
|
||||
this.limiter = limiter
|
||||
}
|
||||
|
||||
public summonerID (summonerID: string, region: string): Promise<LeagueEntryDTO[]> {
|
||||
public summonerID(summonerID: string, region: string): Promise<LeagueEntryDTO[]> {
|
||||
return new JaxRequest(
|
||||
region,
|
||||
this.config,
|
||||
|
|
|
|||
|
|
@ -1,231 +1,216 @@
|
|||
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
|
||||
import { getRiotRegion } from 'App/helpers'
|
||||
import RiotRateLimiter from 'riot-ratelimiter'
|
||||
import { JaxConfig } from '../../JaxConfig'
|
||||
import JaxRequest from '../JaxRequest'
|
||||
|
||||
export interface MatchDto {
|
||||
gameId: number,
|
||||
participantIdentities: ParticipantIdentityDto[],
|
||||
queueId: number,
|
||||
gameType: string,
|
||||
gameDuration: number,
|
||||
teams: TeamStatsDto[],
|
||||
metadata: MetadataDto
|
||||
info: InfoDto
|
||||
}
|
||||
|
||||
export interface MetadataDto {
|
||||
dataVersion: string
|
||||
matchId: string
|
||||
participants: string[]
|
||||
}
|
||||
|
||||
export interface InfoDto {
|
||||
gameCreation: number
|
||||
gameDuration: number
|
||||
gameId: number
|
||||
gameMode: string
|
||||
gameName: string
|
||||
gameStartTimestamp: number
|
||||
gameType: string
|
||||
gameVersion: string
|
||||
mapId: number
|
||||
participants: ParticipantDto[]
|
||||
platformId: string
|
||||
gameCreation: number,
|
||||
seasonId: number,
|
||||
gameVersion: string,
|
||||
mapId: number,
|
||||
gameMode: string,
|
||||
participants: ParticipantDto[],
|
||||
}
|
||||
|
||||
export interface ParticipantIdentityDto {
|
||||
participantId: number,
|
||||
player: PlayerDto
|
||||
}
|
||||
|
||||
export interface PlayerDto {
|
||||
profileIcon: number,
|
||||
accountId: string,
|
||||
matchHistoryUri: string,
|
||||
currentAccountId: string,
|
||||
currentPlatformId: string,
|
||||
summonerName: string,
|
||||
summonerId: string,
|
||||
platformId: string,
|
||||
}
|
||||
|
||||
export interface TeamStatsDto {
|
||||
towerKills: number,
|
||||
riftHeraldKills: number,
|
||||
firstBlood: boolean,
|
||||
inhibitorKills: number,
|
||||
bans: TeamBansDto[],
|
||||
firstBaron: boolean,
|
||||
firstDragon: boolean,
|
||||
dominionVictoryScore: number,
|
||||
dragonKills: number,
|
||||
baronKills: number,
|
||||
firstInhibitor: boolean
|
||||
firstTower: boolean
|
||||
vilemawKills: number,
|
||||
firstRiftHerald: boolean
|
||||
teamId: number, // 100 for blue side. 200 for red side.
|
||||
win: string
|
||||
}
|
||||
|
||||
export interface TeamBansDto {
|
||||
championId: number,
|
||||
pickTurn: number,
|
||||
queueId: number
|
||||
teams: TeamDto[]
|
||||
tournamentCode?: string
|
||||
}
|
||||
|
||||
export interface ParticipantDto {
|
||||
participantId: number,
|
||||
championId: number,
|
||||
runes: RuneDto[],
|
||||
stats: ParticipantStatsDto,
|
||||
teamId: number,
|
||||
timeline: ParticipantTimelineDto,
|
||||
spell1Id: number,
|
||||
spell2Id: number,
|
||||
highestAchievedSeasonTier?:
|
||||
'CHALLENGER' | 'MASTER' | 'DIAMOND' | 'PLATINUM' | 'GOLD' | 'SILVER' | 'BRONZE' | 'UNRANKED',
|
||||
masteries: MasteryDto[]
|
||||
}
|
||||
|
||||
export interface RuneDto {
|
||||
runeId: number,
|
||||
rank: number,
|
||||
}
|
||||
|
||||
export interface ParticipantStatsDto {
|
||||
item0: number,
|
||||
item2: number,
|
||||
totalUnitsHealed: number,
|
||||
item1: number,
|
||||
largestMultiKill: number,
|
||||
goldEarned: number,
|
||||
firstInhibitorKill: boolean,
|
||||
physicalDamageTaken: number,
|
||||
nodeNeutralizeAssist: number,
|
||||
totalPlayerScore: number,
|
||||
champLevel: number,
|
||||
damageDealtToObjectives: number,
|
||||
totalDamageTaken: number,
|
||||
neutralMinionsKilled: number,
|
||||
deaths: number,
|
||||
tripleKills: number,
|
||||
magicDamageDealtToChampions: number,
|
||||
wardsKilled: number,
|
||||
pentaKills: number,
|
||||
damageSelfMitigated: number,
|
||||
largestCriticalStrike: number,
|
||||
nodeNeutralize: number,
|
||||
totalTimeCrowdControlDealt: number,
|
||||
firstTowerKill: boolean
|
||||
magicDamageDealt: number,
|
||||
totalScoreRank: number,
|
||||
nodeCapture: number,
|
||||
wardsPlaced: number,
|
||||
totalDamageDealt: number,
|
||||
timeCCingOthers: number,
|
||||
magicalDamageTaken: number,
|
||||
largestKillingSpree: number,
|
||||
totalDamageDealtToChampions: number,
|
||||
physicalDamageDealtToChampions: number,
|
||||
neutralMinionsKilledTeamJungle: number,
|
||||
totalMinionsKilled: number,
|
||||
firstInhibitorAssist: boolean
|
||||
visionWardsBoughtInGame: number,
|
||||
objectivePlayerScore: number,
|
||||
kills: number,
|
||||
firstTowerAssist: boolean
|
||||
combatPlayerScore: number,
|
||||
inhibitorKills: number,
|
||||
turretKills: number,
|
||||
participantId: number,
|
||||
trueDamageTaken: number,
|
||||
assists: number
|
||||
baronKills: number
|
||||
bountyLevel: number
|
||||
champExperience: number
|
||||
champLevel: number
|
||||
championId: number
|
||||
championName: string
|
||||
championTransform: ChampionTransformDto
|
||||
consumablesPurchased: number
|
||||
damageDealtToObjectives: number
|
||||
damageDealtToTurrets: number
|
||||
damageSelfMitigated: number
|
||||
deaths: number
|
||||
detectorWardsPlaced: number
|
||||
doubleKills: number
|
||||
dragonKills: number
|
||||
firstBloodAssist: boolean
|
||||
nodeCaptureAssist: number,
|
||||
assists: number,
|
||||
teamObjective: number,
|
||||
altarsNeutralized: number,
|
||||
goldSpent: number,
|
||||
damageDealtToTurrets: number,
|
||||
altarsCaptured: number,
|
||||
win: boolean,
|
||||
totalHeal: number,
|
||||
unrealKills: number,
|
||||
visionScore: number,
|
||||
physicalDamageDealt: number,
|
||||
firstBloodKill: boolean,
|
||||
longestTimeSpentLiving: number,
|
||||
killingSprees: number,
|
||||
sightWardsBoughtInGame: number,
|
||||
trueDamageDealtToChampions: number,
|
||||
neutralMinionsKilledEnemyJungle: number,
|
||||
doubleKills: number,
|
||||
trueDamageDealt: number,
|
||||
quadraKills: number,
|
||||
item4: number,
|
||||
item3: number,
|
||||
item6: number,
|
||||
item5: number,
|
||||
playerScore0: number,
|
||||
playerScore1: number,
|
||||
playerScore2: number,
|
||||
playerScore3: number,
|
||||
playerScore4: number,
|
||||
playerScore5: number,
|
||||
playerScore6: number,
|
||||
playerScore7: number,
|
||||
playerScore8: number,
|
||||
playerScore9: number,
|
||||
perk0: number,
|
||||
perk0Var1: number,
|
||||
perk0Var2: number,
|
||||
perk0Var3: number,
|
||||
perk1: number,
|
||||
perk1Var1: number,
|
||||
perk1Var2: number,
|
||||
perk1Var3: number,
|
||||
perk2: number,
|
||||
perk2Var1: number,
|
||||
perk2Var2: number,
|
||||
perk2Var3: number,
|
||||
perk3: number,
|
||||
perk3Var1: number,
|
||||
perk3Var2: number,
|
||||
perk3Var3: number,
|
||||
perk4: number,
|
||||
perk4Var1: number,
|
||||
perk4Var2: number,
|
||||
perk4Var3: number,
|
||||
perk5: number,
|
||||
perk5Var1: number,
|
||||
perk5Var2: number,
|
||||
perk5Var3: number,
|
||||
perkPrimaryStyle: number,
|
||||
perkSubStyle: number,
|
||||
statPerk0: number,
|
||||
statPerk1: number,
|
||||
statPerk2: number,
|
||||
firstBloodKill: boolean
|
||||
firstTowerAssist: boolean
|
||||
firstTowerKill: boolean
|
||||
gameEndedInEarlySurrender: boolean
|
||||
gameEndedInSurrender: boolean
|
||||
goldEarned: number
|
||||
goldSpent: number
|
||||
individualPosition: 'Invalid' | TeamPositionDto // TODO
|
||||
inhibitorKills: number
|
||||
item0: number
|
||||
item1: number
|
||||
item2: number
|
||||
item3: number
|
||||
item4: number
|
||||
item5: number
|
||||
item6: number
|
||||
itemsPurchased: number
|
||||
killingSprees: number
|
||||
kills: number
|
||||
lane: LaneDto // TODO
|
||||
largestCriticalStrike: number
|
||||
largestKillingSpree: number
|
||||
largestMultiKill: number
|
||||
longestTimeSpentLiving: number
|
||||
magicDamageDealt: number
|
||||
magicDamageDealtToChampions: number
|
||||
magicDamageTaken: number
|
||||
neutralMinionsKilled: number
|
||||
nexusKills: number
|
||||
objectivesStolen: number
|
||||
objectivesStolenAssists: number
|
||||
participantId: number
|
||||
pentaKills: number
|
||||
perks: PerksDto
|
||||
physicalDamageDealt: number
|
||||
physicalDamageDealtToChampions: number
|
||||
physicalDamageTaken: number
|
||||
profileIcon: number
|
||||
puuid: string
|
||||
quadraKills: number
|
||||
riotIdName: string
|
||||
riotIdTagline: string
|
||||
role: RoleDto // TODO
|
||||
sightWardsBoughtInGame: number
|
||||
spell1Casts: number
|
||||
spell2Casts: number
|
||||
spell3Casts: number
|
||||
spell4Casts: number
|
||||
summoner1Casts: number
|
||||
summoner1Id: number
|
||||
summoner2Casts: number
|
||||
summoner2Id: number
|
||||
summonerId: string
|
||||
summonerLevel: number
|
||||
summonerName: string
|
||||
teamEarlySurrendered: boolean
|
||||
teamId: number
|
||||
teamPosition: TeamPositionDto // TODO
|
||||
timeCCingOthers: number
|
||||
timePlayed: number
|
||||
totalDamageDealt: number
|
||||
totalDamageDealtToChampions: number
|
||||
totalDamageShieldedOnTeammates: number
|
||||
totalDamageTaken: number
|
||||
totalHeal: number
|
||||
totalHealsOnTeammates: number
|
||||
totalMinionsKilled: number
|
||||
totalTimeCCDealt: number
|
||||
totalTimeSpentDead: number
|
||||
totalUnitsHealed: number
|
||||
tripleKills: number
|
||||
trueDamageDealt: number
|
||||
trueDamageDealtToChampions: number
|
||||
trueDamageTaken: number
|
||||
turretKills: number
|
||||
unrealKills: number
|
||||
visionScore: number
|
||||
visionWardsBoughtInGame: number
|
||||
wardsKilled: number
|
||||
wardsPlaced: number
|
||||
win: boolean
|
||||
}
|
||||
|
||||
export interface ParticipantTimelineDto {
|
||||
participantId: number,
|
||||
csDiffPerMinDeltas: { [index: string]: number },
|
||||
damageTakenPerMinDeltas: { [index: string]: number },
|
||||
role: 'DUO' | 'NONE' | 'SOLO' | 'DUO_CARRY' | 'DUO_SUPPORT',
|
||||
damageTakenDiffPerMinDeltas: { [index: string]: number },
|
||||
xpPerMinDeltas: { [index: string]: number },
|
||||
xpDiffPerMinDeltas: { [index: string]: number },
|
||||
lane: 'MID' | 'MIDDLE' | 'TOP' | 'JUNGLE' | 'BOT' | 'BOTTOM',
|
||||
creepsPerMinDeltas: { [index: string]: number },
|
||||
goldPerMinDeltas: { [index: string]: number },
|
||||
export enum ChampionTransformDto {
|
||||
None,
|
||||
Slayer,
|
||||
Assasin,
|
||||
}
|
||||
|
||||
export interface MasteryDto {
|
||||
rank: number,
|
||||
masteryId: number,
|
||||
export type LaneDto = 'TOP' | 'JUNGLE' | 'MIDDLE' | 'BOTTOM'
|
||||
|
||||
export interface PerksDto {
|
||||
statPerks: PerkStatsDto
|
||||
styles: PerkStyleDto[]
|
||||
}
|
||||
|
||||
export interface PerkStatsDto {
|
||||
defense: number
|
||||
flex: number
|
||||
offense: number
|
||||
}
|
||||
|
||||
export interface PerkStyleDto {
|
||||
description: 'primaryStyle' | 'subStyle'
|
||||
selections: PerkStyleSelectionDto[]
|
||||
style: number
|
||||
}
|
||||
|
||||
export interface PerkStyleSelectionDto {
|
||||
perk: number
|
||||
var1: number
|
||||
var2: number
|
||||
var3: number
|
||||
}
|
||||
|
||||
export type RoleDto = 'NONE' | 'DUO' | 'SOLO' | 'CARRY' | 'SUPPORT'
|
||||
|
||||
export type TeamPositionDto = 'TOP' | 'JUNGLE' | 'MIDDLE' | 'BOTTOM' | 'UTILITY'
|
||||
|
||||
export interface TeamDto {
|
||||
bans: BanDto[]
|
||||
objectives: ObjectivesDto
|
||||
teamId: number
|
||||
win: boolean
|
||||
}
|
||||
|
||||
export interface BanDto {
|
||||
championId: number
|
||||
pickTurn: number
|
||||
}
|
||||
|
||||
export interface ObjectivesDto {
|
||||
baron: ObjectiveDto
|
||||
champion: ObjectiveDto
|
||||
dragon: ObjectiveDto
|
||||
inhibitor: ObjectiveDto
|
||||
riftHerald: ObjectiveDto
|
||||
tower: ObjectiveDto
|
||||
}
|
||||
|
||||
export interface ObjectiveDto {
|
||||
first: boolean
|
||||
kills: number
|
||||
}
|
||||
|
||||
export default class MatchEndpoint {
|
||||
private config: JaxConfig
|
||||
private limiter: RiotRateLimiter
|
||||
|
||||
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
this.config = config
|
||||
this.limiter = limiter
|
||||
|
||||
this.get = this.get.bind(this)
|
||||
}
|
||||
|
||||
public get (matchID: number, region: string): Promise<MatchDto> {
|
||||
public get(matchID: string, region: string): Promise<MatchDto> {
|
||||
return new JaxRequest(
|
||||
region,
|
||||
getRiotRegion(region),
|
||||
this.config,
|
||||
`match/v4/matches/${matchID}`,
|
||||
`match/v5/matches/${matchID}`,
|
||||
this.limiter,
|
||||
1500
|
||||
).execute()
|
||||
|
|
|
|||
240
server/app/Services/Jax/src/Endpoints/MatchV4Endpoint.ts
Normal file
240
server/app/Services/Jax/src/Endpoints/MatchV4Endpoint.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,52 @@
|
|||
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
|
||||
import { getRiotRegion } from 'App/helpers'
|
||||
import RiotRateLimiter from 'riot-ratelimiter'
|
||||
import { JaxConfig } from '../../JaxConfig'
|
||||
import JaxRequest from '../JaxRequest'
|
||||
|
||||
export interface MatchlistDto {
|
||||
startIndex: number,
|
||||
totalGames: number,
|
||||
endIndex: number,
|
||||
matches: MatchReferenceDto[]
|
||||
}
|
||||
// export interface MatchlistDto {
|
||||
// startIndex: number,
|
||||
// totalGames: number,
|
||||
// endIndex: number,
|
||||
// matches: MatchReferenceDto[]
|
||||
// }
|
||||
|
||||
export interface MatchReferenceDto {
|
||||
gameId: number,
|
||||
role: string,
|
||||
season: number,
|
||||
platformId: string,
|
||||
champion: number,
|
||||
queue: number,
|
||||
lane: string,
|
||||
timestamp: number,
|
||||
seasonMatch?: number
|
||||
}
|
||||
// export interface MatchReferenceDto {
|
||||
// gameId: number,
|
||||
// role: string,
|
||||
// season: number,
|
||||
// platformId: string,
|
||||
// champion: number,
|
||||
// queue: number,
|
||||
// lane: string,
|
||||
// timestamp: number,
|
||||
// seasonMatch?: number
|
||||
// }
|
||||
|
||||
/**
|
||||
*
|
||||
* ===============================================
|
||||
* V5
|
||||
* ===============================================
|
||||
*
|
||||
*/
|
||||
|
||||
export type MatchlistDto = string[]
|
||||
|
||||
export default class MatchlistEndpoint {
|
||||
private config: JaxConfig
|
||||
private limiter: RiotRateLimiter
|
||||
|
||||
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
this.config = config
|
||||
this.limiter = limiter
|
||||
}
|
||||
|
||||
public accountID (accountID: string, region: string, beginIndex = 0): Promise<MatchlistDto> {
|
||||
public puuid(puuid: string, region: string, beginIndex = 0, count = 100): Promise<MatchlistDto> {
|
||||
return new JaxRequest(
|
||||
region,
|
||||
getRiotRegion(region),
|
||||
this.config,
|
||||
`match/v4/matchlists/by-account/${accountID}?beginIndex=${beginIndex}`,
|
||||
`match/v5/matches/by-puuid/${puuid}/ids?start=${beginIndex}&count=${count}`,
|
||||
this.limiter,
|
||||
0
|
||||
).execute()
|
||||
|
|
|
|||
43
server/app/Services/Jax/src/Endpoints/MatchlistV4Endpoint.ts
Normal file
43
server/app/Services/Jax/src/Endpoints/MatchlistV4Endpoint.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +1,53 @@
|
|||
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
|
||||
import RiotRateLimiter from 'riot-ratelimiter'
|
||||
import { LeagueEntriesByQueue } from 'App/Services/SummonerService'
|
||||
import { JaxConfig } from '../../JaxConfig'
|
||||
import JaxRequest from '../JaxRequest'
|
||||
|
||||
export interface CurrentGameInfo {
|
||||
gameId: number,
|
||||
export interface CurrentGameInfoDTO {
|
||||
gameId: number
|
||||
gameType: string
|
||||
gameStartTime: number,
|
||||
mapId: number,
|
||||
gameLength: number,
|
||||
platformId: string,
|
||||
gameMode: string,
|
||||
bannedChampions: BannedChampion[],
|
||||
gameQueueConfigId: number,
|
||||
observers: Observer,
|
||||
participants: CurrentGameParticipant[],
|
||||
gameStartTime: number
|
||||
mapId: number
|
||||
gameLength: number
|
||||
platformId: string
|
||||
gameMode: string
|
||||
bannedChampions: BannedChampionDTO[]
|
||||
gameQueueConfigId: number
|
||||
observers: ObserverDTO
|
||||
participants: CurrentGameParticipantDTO[]
|
||||
}
|
||||
|
||||
export interface BannedChampion {
|
||||
pickTurn: number,
|
||||
championId: number,
|
||||
teamId: number,
|
||||
export interface BannedChampionDTO {
|
||||
pickTurn: number
|
||||
championId: number
|
||||
teamId: number
|
||||
}
|
||||
|
||||
export interface Observer {
|
||||
encryptionKey: string,
|
||||
export interface ObserverDTO {
|
||||
encryptionKey: string
|
||||
}
|
||||
|
||||
export interface CurrentGameParticipant {
|
||||
championId: number,
|
||||
perks: Perks,
|
||||
profileIconId: number,
|
||||
bot: boolean,
|
||||
teamId: number,
|
||||
summonerName: string,
|
||||
summonerId: string,
|
||||
spell1Id: number,
|
||||
spell2Id: number,
|
||||
gameCustomizationObjects: GameCustomizationObject[],
|
||||
// Custom types from here
|
||||
role?: string,
|
||||
runes?: { primaryRune: string, secondaryRune: string } | {}
|
||||
level?: number,
|
||||
rank?: LeagueEntriesByQueue
|
||||
export interface CurrentGameParticipantDTO {
|
||||
championId: number
|
||||
perks: PerksDTO
|
||||
profileIconId: number
|
||||
bot: boolean
|
||||
teamId: number
|
||||
summonerName: string
|
||||
summonerId: string
|
||||
spell1Id: number
|
||||
spell2Id: number
|
||||
gameCustomizationObjects: GameCustomizationObjectDTO[]
|
||||
}
|
||||
|
||||
export interface Perks {
|
||||
export interface PerksDTO {
|
||||
perkIds: number[]
|
||||
perkStyle: number,
|
||||
perkStyle: number
|
||||
perkSubStyle: number
|
||||
}
|
||||
|
||||
export interface GameCustomizationObject {
|
||||
category: string,
|
||||
export interface GameCustomizationObjectDTO {
|
||||
category: string
|
||||
content: string
|
||||
}
|
||||
|
||||
|
|
@ -61,12 +55,12 @@ export default class SpectatorEndpoint {
|
|||
private config: JaxConfig
|
||||
private limiter: RiotRateLimiter
|
||||
|
||||
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
this.config = config
|
||||
this.limiter = limiter
|
||||
}
|
||||
|
||||
public summonerID (summonerID: string, region: string) {
|
||||
public summonerID(summonerID: string, region: string): Promise<CurrentGameInfoDTO | undefined> {
|
||||
return new JaxRequest(
|
||||
region,
|
||||
this.config,
|
||||
|
|
|
|||
|
|
@ -4,26 +4,35 @@ import { JaxConfig } from '../../JaxConfig'
|
|||
import JaxRequest from '../JaxRequest'
|
||||
|
||||
export interface SummonerDTO {
|
||||
accountId: string,
|
||||
profileIconId: number,
|
||||
revisionDate: number,
|
||||
name: string,
|
||||
id: string,
|
||||
puuid: string,
|
||||
summonerLevel: number,
|
||||
region?: string
|
||||
accountId: string
|
||||
profileIconId: number
|
||||
revisionDate: number
|
||||
name: string
|
||||
id: string
|
||||
puuid: string
|
||||
summonerLevel: number
|
||||
}
|
||||
|
||||
export default class SummonerEndpoint {
|
||||
private config: JaxConfig
|
||||
private limiter: RiotRateLimiter
|
||||
|
||||
constructor (config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
constructor(config: JaxConfig, limiter: RiotRateLimiter) {
|
||||
this.config = config
|
||||
this.limiter = limiter
|
||||
}
|
||||
|
||||
public summonerId (summonerId: string, region: string): Promise<SummonerDTO> {
|
||||
public accountId(accountId: string, region: string): Promise<SummonerDTO> {
|
||||
return new JaxRequest(
|
||||
region,
|
||||
this.config,
|
||||
`summoner/v4/summoners/by-account/${accountId}`,
|
||||
this.limiter,
|
||||
36000
|
||||
).execute()
|
||||
}
|
||||
|
||||
public summonerId(summonerId: string, region: string): Promise<SummonerDTO> {
|
||||
return new JaxRequest(
|
||||
region,
|
||||
this.config,
|
||||
|
|
@ -33,7 +42,7 @@ export default class SummonerEndpoint {
|
|||
).execute()
|
||||
}
|
||||
|
||||
public summonerName (summonerName: string, region: string): Promise<SummonerDTO> {
|
||||
public summonerName(summonerName: string, region: string): Promise<SummonerDTO> {
|
||||
return new JaxRequest(
|
||||
region,
|
||||
this.config,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { JaxConfig } from '../JaxConfig'
|
|||
// import { RiotRateLimiter } from '@fightmegg/riot-rate-limiter'
|
||||
import RiotRateLimiter from 'riot-ratelimiter'
|
||||
import { STRATEGY } from 'riot-ratelimiter/dist/RateLimiter'
|
||||
import MatchV4Endpoint from './Endpoints/MatchV4Endpoint'
|
||||
import MatchlistV4Endpoint from './Endpoints/MatchlistV4Endpoint'
|
||||
|
||||
export default class Jax {
|
||||
public key: string
|
||||
|
|
@ -15,12 +17,14 @@ export default class Jax {
|
|||
public config: JaxConfig
|
||||
public League: LeagueEndpoint
|
||||
public Match: MatchEndpoint
|
||||
public MatchV4: MatchV4Endpoint
|
||||
public Matchlist: MatchlistEndpoint
|
||||
public MatchlistV4: MatchlistV4Endpoint
|
||||
public Spectator: SpectatorEndpoint
|
||||
public Summoner: SummonerEndpoint
|
||||
public CDragon: CDragonEndpoint
|
||||
|
||||
constructor (config:JaxConfig) {
|
||||
constructor(config: JaxConfig) {
|
||||
this.key = config.key
|
||||
// this.limiter = new RiotRateLimiter({
|
||||
// debug: true,
|
||||
|
|
@ -34,7 +38,9 @@ export default class Jax {
|
|||
|
||||
this.League = new LeagueEndpoint(this.config, this.limiter)
|
||||
this.Match = new MatchEndpoint(this.config, this.limiter)
|
||||
this.MatchV4 = new MatchV4Endpoint(this.config, this.limiter)
|
||||
this.Matchlist = new MatchlistEndpoint(this.config, this.limiter)
|
||||
this.MatchlistV4 = new MatchlistV4Endpoint(this.config, this.limiter)
|
||||
this.Spectator = new SpectatorEndpoint(this.config, this.limiter)
|
||||
this.Summoner = new SummonerEndpoint(this.config, this.limiter)
|
||||
this.CDragon = new CDragonEndpoint(this.config)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,13 @@ export default class JaxRequest {
|
|||
private retries: number
|
||||
private sleep: { (ms: number): Promise<void>; <T>(ms: number, value: T): Promise<T> }
|
||||
|
||||
constructor (region: string, config: JaxConfig, endpoint: string, limiter: RiotRateLimiter, cacheTime: number) {
|
||||
constructor(
|
||||
region: string,
|
||||
config: JaxConfig,
|
||||
endpoint: string,
|
||||
limiter: RiotRateLimiter,
|
||||
cacheTime: number
|
||||
) {
|
||||
this.region = region
|
||||
this.config = config
|
||||
this.endpoint = endpoint
|
||||
|
|
@ -25,7 +31,7 @@ export default class JaxRequest {
|
|||
this.sleep = promisify(setTimeout)
|
||||
}
|
||||
|
||||
public async execute () {
|
||||
public async execute() {
|
||||
const url = `https://${this.region}.api.riotgames.com/lol/${this.endpoint}`
|
||||
|
||||
// Redis cache
|
||||
|
|
@ -37,16 +43,7 @@ export default class JaxRequest {
|
|||
}
|
||||
|
||||
try {
|
||||
// const resp = await this.limiter.execute({
|
||||
// url,
|
||||
// options: {
|
||||
// headers: {
|
||||
// 'X-Riot-Token': this.config.key,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
const resp:any = await this.limiter.executing({
|
||||
const resp: any = await this.limiter.executing({
|
||||
url,
|
||||
token: this.config.key,
|
||||
resolveWithFullResponse: false,
|
||||
|
|
@ -56,17 +53,28 @@ export default class JaxRequest {
|
|||
await Redis.setex(url, this.cacheTime, resp)
|
||||
}
|
||||
return JSON.parse(resp)
|
||||
} catch ({ statusCode , ...rest }) {
|
||||
} catch ({ statusCode, ...rest }) {
|
||||
this.retries--
|
||||
|
||||
if (statusCode !== 500 && statusCode !== 503 && statusCode !== 504) { //
|
||||
console.log('JAX ERROR')
|
||||
console.log(rest?.cause?.code)
|
||||
|
||||
if (
|
||||
statusCode !== 500 &&
|
||||
statusCode !== 503 &&
|
||||
statusCode !== 504 &&
|
||||
rest?.cause?.code !== 'ETIMEDOUT'
|
||||
) {
|
||||
//
|
||||
// Don't log 404 when summoner isn't playing or the summoner doesn't exist
|
||||
// Or if summoner has no MatchList
|
||||
if (!this.endpoint.includes('spectator/v4/active-games/by-summoner') &&
|
||||
if (
|
||||
!this.endpoint.includes('spectator/v4/active-games/by-summoner') &&
|
||||
!this.endpoint.includes('summoner/v4/summoners/by-name') &&
|
||||
!this.endpoint.includes('match/v4/matchlists/by-account')
|
||||
) {
|
||||
Logger.error(`JaxRequest Error ${statusCode}: `, rest)
|
||||
Logger.error(`URL ${url}: `)
|
||||
// Logger.error(`JaxRequest Error ${statusCode}: `, rest)
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,142 +1,138 @@
|
|||
import Jax from './Jax'
|
||||
import Logger from '@ioc:Adonis/Core/Logger'
|
||||
import { getSeasonNumber } from 'App/helpers'
|
||||
import { MatchReferenceDto } from './Jax/src/Endpoints/MatchlistEndpoint'
|
||||
import { MatchlistDto } from './Jax/src/Endpoints/MatchlistEndpoint'
|
||||
import { SummonerDTO } from './Jax/src/Endpoints/SummonerEndpoint'
|
||||
import { SummonerModel } from 'App/Models/Summoner'
|
||||
import Match, { MatchModel } from 'App/Models/Match'
|
||||
import BasicMatchTransformer from 'App/Transformers/BasicMatchTransformer'
|
||||
import Summoner from 'App/Models/Summoner'
|
||||
import Database from '@ioc:Adonis/Lucid/Database'
|
||||
import MatchParser from 'App/Parsers/MatchParser'
|
||||
import BasicMatchSerializer from 'App/Serializers/BasicMatchSerializer'
|
||||
import { SerializedMatch } from 'App/Serializers/SerializedTypes'
|
||||
import Match from 'App/Models/Match'
|
||||
import { notEmpty, tutorialQueues } from 'App/helpers'
|
||||
|
||||
class MatchService {
|
||||
/**
|
||||
* Add 100 matches at a time to MatchList until the stopFetching condition is true
|
||||
* @param account of the summoner
|
||||
* @param region of the summoner
|
||||
* @param stopFetching condition to stop fetching the MatchList
|
||||
*/
|
||||
private async _fetchMatchListUntil (account: SummonerDTO, stopFetching: any) {
|
||||
let matchList: MatchReferenceDto[] = []
|
||||
private async _fetchMatchListUntil(account: SummonerDTO, region: string, stopFetching: any) {
|
||||
let matchList: MatchlistDto = []
|
||||
let alreadyIn = false
|
||||
let index = 0
|
||||
do {
|
||||
let newMatchList = await Jax.Matchlist.accountID(account.accountId, account.region as string, index)
|
||||
const newMatchList = await Jax.Matchlist.puuid(account.puuid, region, index)
|
||||
// Error while fetching Riot API
|
||||
if (!newMatchList) {
|
||||
matchList = matchList.map(m => {
|
||||
m.seasonMatch = getSeasonNumber(m.timestamp)
|
||||
return m
|
||||
})
|
||||
return matchList
|
||||
}
|
||||
matchList = [...matchList, ...newMatchList.matches]
|
||||
alreadyIn = newMatchList.matches.length === 0 || stopFetching(newMatchList.matches)
|
||||
matchList = [...matchList, ...newMatchList]
|
||||
alreadyIn = newMatchList.length === 0 || stopFetching(newMatchList)
|
||||
// If the match is made in another region : we stop fetching
|
||||
if (matchList[matchList.length - 1].platformId.toLowerCase() !== account.region) {
|
||||
if (matchList[matchList.length - 1].split('_')[0].toLowerCase() !== region.toLowerCase()) {
|
||||
alreadyIn = true
|
||||
}
|
||||
index += 100
|
||||
} while (!alreadyIn)
|
||||
|
||||
// Remove matches from MatchList made in another region and tutorial games
|
||||
const tutorialModes = [2000, 2010, 2020]
|
||||
matchList = matchList
|
||||
.filter(m => {
|
||||
const sameRegion = m.platformId.toLowerCase() === account.region
|
||||
const notATutorialGame = !tutorialModes.includes(m.queue)
|
||||
|
||||
return sameRegion && notATutorialGame
|
||||
})
|
||||
.map(m => {
|
||||
m.seasonMatch = getSeasonNumber(m.timestamp)
|
||||
return m
|
||||
})
|
||||
|
||||
return matchList
|
||||
}
|
||||
/**
|
||||
* Update the full MatchList of the summoner (min. 4 months)
|
||||
* @param account of the summoner
|
||||
* @param summonerDB summoner in the database
|
||||
* Update the full MatchList of the summoner
|
||||
*/
|
||||
public async updateMatchList (account: SummonerDTO, summonerDB: SummonerModel) {
|
||||
public async updateMatchList(
|
||||
account: SummonerDTO,
|
||||
region: string,
|
||||
summonerDB: Summoner
|
||||
): Promise<MatchlistDto> {
|
||||
console.time('matchList')
|
||||
|
||||
// Summoner has already been searched : we already have a MatchList and we need to update it
|
||||
if (summonerDB.matchList) {
|
||||
// Get MatchList
|
||||
const matchList = await this._fetchMatchListUntil(account, (newMatchList: MatchReferenceDto[]) => {
|
||||
return summonerDB.matchList!.some(m => m.gameId === newMatchList[newMatchList.length - 1].gameId)
|
||||
})
|
||||
// Update Summoner's MatchList
|
||||
for (const match of matchList.reverse()) {
|
||||
if (!summonerDB.matchList.some(m => m.gameId === match.gameId)) {
|
||||
summonerDB.matchList.unshift(match)
|
||||
}
|
||||
const currentMatchList = await summonerDB.related('matchList').query().orderBy('matchId', 'asc')
|
||||
const currentMatchListIds = currentMatchList.map((m) => m.matchId)
|
||||
|
||||
const newMatchList = await this._fetchMatchListUntil(
|
||||
account,
|
||||
region,
|
||||
(newMatchList: MatchlistDto) => {
|
||||
return currentMatchListIds.some((id) => id === newMatchList[newMatchList.length - 1])
|
||||
}
|
||||
)
|
||||
|
||||
const matchListToSave: MatchlistDto = []
|
||||
for (const matchId of newMatchList.reverse()) {
|
||||
if (!currentMatchListIds.some((id) => id === matchId)) {
|
||||
matchListToSave.push(matchId)
|
||||
currentMatchListIds.push(matchId)
|
||||
}
|
||||
} else { // First search of the Summoner
|
||||
const today = Date.now()
|
||||
// Get MatchList
|
||||
const matchList = await this._fetchMatchListUntil(account, (newMatchList: MatchReferenceDto[]) => {
|
||||
return (newMatchList.length !== 100 || today - newMatchList[newMatchList.length - 1].timestamp > 10368000000)
|
||||
})
|
||||
// Create Summoner's MatchList in Database
|
||||
summonerDB.matchList = matchList
|
||||
}
|
||||
|
||||
// If there is new matchIds to save in database
|
||||
if (matchListToSave.length) {
|
||||
await Database.table('summoner_matchlist').multiInsert(
|
||||
matchListToSave.map((id) => ({
|
||||
match_id: id,
|
||||
summoner_puuid: summonerDB.puuid,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
console.timeEnd('matchList')
|
||||
return currentMatchListIds.reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of matches for a specific Summoner
|
||||
* @param puuid
|
||||
* @param accountId
|
||||
* @param region
|
||||
* @param gameIds
|
||||
* @param summonerDB
|
||||
*/
|
||||
public async getMatches (puuid: string, accountId: string, region: string, gameIds: number[], summonerDB: SummonerModel) {
|
||||
public async getMatches(
|
||||
region: string,
|
||||
matchIds: string[],
|
||||
puuid: string
|
||||
): Promise<SerializedMatch[]> {
|
||||
console.time('getMatches')
|
||||
|
||||
let matchesDetails: MatchModel[] = []
|
||||
const matchesToGetFromRiot: number[] = []
|
||||
for (let i = 0; i < gameIds.length; ++i) {
|
||||
const matchSaved = await Match.findOne({
|
||||
summoner_puuid: puuid,
|
||||
gameId: gameIds[i],
|
||||
})
|
||||
const matches: SerializedMatch[] = []
|
||||
const matchesToGetFromRiot: MatchlistDto = []
|
||||
for (let i = 0; i < matchIds.length; ++i) {
|
||||
const matchSaved = await Match.query()
|
||||
.where('id', matchIds[i])
|
||||
.preload('teams')
|
||||
.preload('players')
|
||||
.first()
|
||||
|
||||
if (matchSaved) {
|
||||
matchesDetails.push(matchSaved)
|
||||
// TODO: Serialize match from DB + put it in Redis + push it in "matches"
|
||||
matches.push(BasicMatchSerializer.serializeOneMatch(matchSaved, puuid))
|
||||
} else {
|
||||
matchesToGetFromRiot.push(gameIds[i])
|
||||
matchesToGetFromRiot.push(matchIds[i])
|
||||
}
|
||||
}
|
||||
|
||||
const requests = matchesToGetFromRiot.map(gameId => Jax.Match.get(gameId, region))
|
||||
let matchesFromApi = await Promise.all(requests)
|
||||
const requests = matchesToGetFromRiot.map((gameId) => Jax.Match.get(gameId, region))
|
||||
const matchesFromApi = await Promise.all(requests)
|
||||
|
||||
/* If we have to store some matches in the db */
|
||||
if (matchesFromApi.length !== 0) {
|
||||
// Try to see why matches are sometimes undefined
|
||||
matchesFromApi.filter(m => {
|
||||
if (m === undefined) {
|
||||
Logger.info(`Match undefined, summoner: ${summonerDB.puuid}`, m)
|
||||
}
|
||||
})
|
||||
// Remove bugged matches from the Riot API + tutorial games
|
||||
const filteredMatches = matchesFromApi
|
||||
.filter(notEmpty)
|
||||
.filter(
|
||||
(m) =>
|
||||
!tutorialQueues.includes(m.info.queueId) &&
|
||||
m.info.teams.length > 0 &&
|
||||
m.info.participants.length > 0
|
||||
)
|
||||
|
||||
// Transform raw matches data
|
||||
const transformedMatches = await BasicMatchTransformer.transform(matchesFromApi, { puuid, accountId })
|
||||
const parsedMatches: any = await MatchParser.parse(filteredMatches)
|
||||
|
||||
/* Save all matches from Riot Api in db */
|
||||
for (const match of transformedMatches) {
|
||||
await Match.create(match)
|
||||
match.newMatch = true
|
||||
}
|
||||
matchesDetails = [...matchesDetails, ...transformedMatches]
|
||||
// TODO: Serialize match from DB + put it in Redis + push it in "matches"
|
||||
const serializedMatches = BasicMatchSerializer.serialize(parsedMatches, puuid, true)
|
||||
matches.push(...serializedMatches)
|
||||
}
|
||||
|
||||
/* Sort matches */
|
||||
matchesDetails.sort((a, b) => (a.date < b.date) ? 1 : -1)
|
||||
// Todo: check if we need to sort here
|
||||
matches.sort((a, b) => (a.date < b.date ? 1 : -1))
|
||||
console.timeEnd('getMatches')
|
||||
|
||||
return matchesDetails
|
||||
return matches
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
72
server/app/Services/MatchV4Service.ts
Normal file
72
server/app/Services/MatchV4Service.ts
Normal 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()
|
||||
|
|
@ -3,28 +3,20 @@ import got from 'got/dist/source'
|
|||
|
||||
export interface ChampionsPlayRate {
|
||||
[champion: string]: {
|
||||
TOP: number,
|
||||
JUNGLE: number,
|
||||
MIDDLE: number,
|
||||
BOTTOM: number,
|
||||
UTILITY: number,
|
||||
TOP: number
|
||||
JUNGLE: number
|
||||
MIDDLE: number
|
||||
BOTTOM: number
|
||||
UTILITY: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface FinalRoleComposition {
|
||||
TOP?: number,
|
||||
JUNGLE?: number,
|
||||
MIDDLE?: number,
|
||||
BOTTOM?: number,
|
||||
SUPPORT?: number,
|
||||
}
|
||||
|
||||
export interface RoleComposition {
|
||||
TOP?: number,
|
||||
JUNGLE?: number,
|
||||
MIDDLE?: number,
|
||||
BOTTOM?: number,
|
||||
UTILITY?: number,
|
||||
TOP?: number
|
||||
JUNGLE?: number
|
||||
MIDDLE?: number
|
||||
BOTTOM?: number
|
||||
UTILITY?: number
|
||||
}
|
||||
|
||||
export interface ChampionComposition {
|
||||
|
|
@ -32,22 +24,22 @@ export interface ChampionComposition {
|
|||
}
|
||||
|
||||
export interface ApiRoleResponse {
|
||||
data: { [key: string]: ChampionInitialRates };
|
||||
patch: string;
|
||||
data: { [key: string]: ChampionInitialRates }
|
||||
patch: string
|
||||
}
|
||||
|
||||
export interface ChampionInitialRates {
|
||||
MIDDLE?: RoleRate;
|
||||
UTILITY?: RoleRate;
|
||||
JUNGLE?: RoleRate;
|
||||
TOP?: RoleRate;
|
||||
BOTTOM?: RoleRate;
|
||||
MIDDLE?: RoleRate
|
||||
UTILITY?: RoleRate
|
||||
JUNGLE?: RoleRate
|
||||
TOP?: RoleRate
|
||||
BOTTOM?: RoleRate
|
||||
}
|
||||
|
||||
export interface RoleRate {
|
||||
playRate: number;
|
||||
winRate: number;
|
||||
banRate: number;
|
||||
playRate: number
|
||||
winRate: number
|
||||
banRate: number
|
||||
}
|
||||
|
||||
export interface ChampionsRates {
|
||||
|
|
@ -55,7 +47,7 @@ export interface ChampionsRates {
|
|||
}
|
||||
|
||||
class RoleIdentificationService {
|
||||
private _getPermutations (array: number[]) {
|
||||
private _getPermutations(array: number[]) {
|
||||
const result: number[][] = []
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
|
|
@ -72,13 +64,15 @@ class RoleIdentificationService {
|
|||
return result
|
||||
}
|
||||
|
||||
private _calculateMetric (championPositions: ChampionsRates, bestPositions: RoleComposition) {
|
||||
return Object.entries(bestPositions).reduce((agg, [position, champion]) => {
|
||||
return agg + (championPositions[champion][position] || 0)
|
||||
}, 0) / Object.keys(bestPositions).length
|
||||
private _calculateMetric(championPositions: ChampionsRates, bestPositions: RoleComposition) {
|
||||
return (
|
||||
Object.entries(bestPositions).reduce((agg, [position, champion]) => {
|
||||
return agg + (championPositions[champion][position] || 0)
|
||||
}, 0) / Object.keys(bestPositions).length
|
||||
)
|
||||
}
|
||||
|
||||
private _getPositions (
|
||||
private _getPositions(
|
||||
championPositions: ChampionsRates,
|
||||
composition: number[],
|
||||
top?: number,
|
||||
|
|
@ -89,11 +83,11 @@ class RoleIdentificationService {
|
|||
) {
|
||||
// Set the initial guess to be the champion in the composition, order doesn't matter
|
||||
let bestPositions: RoleComposition = {
|
||||
'TOP': composition[0],
|
||||
'JUNGLE': composition[1],
|
||||
'MIDDLE': composition[2],
|
||||
'BOTTOM': composition[3],
|
||||
'UTILITY': composition[4],
|
||||
TOP: composition[0],
|
||||
JUNGLE: composition[1],
|
||||
MIDDLE: composition[2],
|
||||
BOTTOM: composition[3],
|
||||
UTILITY: composition[4],
|
||||
}
|
||||
|
||||
let bestMetric = this._calculateMetric(championPositions, bestPositions)
|
||||
|
|
@ -102,19 +96,23 @@ class RoleIdentificationService {
|
|||
|
||||
// Figure out which champions and positions we need to fill
|
||||
const knownChampions = [top, jungle, middle, adc, support].filter(Boolean)
|
||||
const unknownChampions = composition.filter(champ => !knownChampions.includes(champ))
|
||||
const unknownChampions = composition.filter((champ) => !knownChampions.includes(champ))
|
||||
const unknownPositions = Object.entries({
|
||||
'TOP': top, 'JUNGLE': jungle, 'MIDDLE': middle, 'BOTTOM': adc, 'UTILITY': support,
|
||||
TOP: top,
|
||||
JUNGLE: jungle,
|
||||
MIDDLE: middle,
|
||||
BOTTOM: adc,
|
||||
UTILITY: support,
|
||||
})
|
||||
.filter(pos => !pos[1])
|
||||
.map(pos => pos[0])
|
||||
.filter((pos) => !pos[1])
|
||||
.map((pos) => pos[0])
|
||||
|
||||
const testComposition: RoleComposition = {
|
||||
'TOP': top,
|
||||
'JUNGLE': jungle,
|
||||
'MIDDLE': middle,
|
||||
'BOTTOM': adc,
|
||||
'UTILITY': support,
|
||||
TOP: top,
|
||||
JUNGLE: jungle,
|
||||
MIDDLE: middle,
|
||||
BOTTOM: adc,
|
||||
UTILITY: support,
|
||||
}
|
||||
|
||||
// Iterate over the positions we need to fill and record how well each composition "performs"
|
||||
|
|
@ -163,7 +161,7 @@ class RoleIdentificationService {
|
|||
/**
|
||||
* Get the CDN data of the champion playrates by role
|
||||
*/
|
||||
public async pullData (): Promise<ChampionsPlayRate> {
|
||||
public async pullData(): Promise<ChampionsPlayRate> {
|
||||
const url = 'http://cdn.merakianalytics.com/riot/lol/resources/latest/en-US/championrates.json'
|
||||
|
||||
// Check if cached
|
||||
|
|
@ -199,17 +197,17 @@ class RoleIdentificationService {
|
|||
|
||||
/**
|
||||
* Get roles for the 5 players of a team
|
||||
* @param championPositions
|
||||
* @param composition
|
||||
* @param championPositions
|
||||
* @param composition
|
||||
* @param jungle
|
||||
* @param support
|
||||
*/
|
||||
public getRoles (
|
||||
public getRoles(
|
||||
championPositions: ChampionsRates,
|
||||
composition: number[],
|
||||
jungle?: number,
|
||||
support?: number
|
||||
): FinalRoleComposition {
|
||||
): RoleComposition {
|
||||
// Set composition champion playrate to 0% if not present in the json data
|
||||
for (const compChamp of composition) {
|
||||
if (championPositions[compChamp]) {
|
||||
|
|
@ -239,10 +237,19 @@ class RoleIdentificationService {
|
|||
}
|
||||
|
||||
while (Object.keys(identified).length < composition.length - 1) {
|
||||
let { bestPositions, bestMetric: metric, secondBestPositions: sbp } =
|
||||
this._getPositions(championPositions, composition,
|
||||
identified.TOP, identified.JUNGLE, identified.MIDDLE, identified.BOTTOM, identified.UTILITY
|
||||
)
|
||||
let {
|
||||
bestPositions,
|
||||
bestMetric: metric,
|
||||
secondBestPositions: sbp,
|
||||
} = this._getPositions(
|
||||
championPositions,
|
||||
composition,
|
||||
identified.TOP,
|
||||
identified.JUNGLE,
|
||||
identified.MIDDLE,
|
||||
identified.BOTTOM,
|
||||
identified.UTILITY
|
||||
)
|
||||
|
||||
positions = bestPositions
|
||||
|
||||
|
|
@ -261,7 +268,11 @@ class RoleIdentificationService {
|
|||
// Done! Grab the results.
|
||||
const positionsWithMetric = {}
|
||||
for (const [position, champion] of Object.entries(positions)) {
|
||||
if (Object.keys(identified).includes(position) || champion === jungle || champion === support) {
|
||||
if (
|
||||
Object.keys(identified).includes(position) ||
|
||||
champion === jungle ||
|
||||
champion === support
|
||||
) {
|
||||
continue
|
||||
}
|
||||
positionsWithMetric[position] = {
|
||||
|
|
@ -285,13 +296,7 @@ class RoleIdentificationService {
|
|||
identified[best[0]] = best[1]
|
||||
}
|
||||
|
||||
// Rename UTILITY to SUPPORT
|
||||
const {
|
||||
UTILITY: SUPPORT,
|
||||
...rest
|
||||
} = positions
|
||||
|
||||
return { ...rest, SUPPORT }
|
||||
return positions
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,20 +1,28 @@
|
|||
import MatchRepository from 'App/Repositories/MatchRepository'
|
||||
import { sortTeamByRole } from 'App/helpers'
|
||||
import { ChampionRoles, TeamPosition } from 'App/Parsers/ParsedType'
|
||||
import MatchRepository from 'App/Repositories/MatchRepository'
|
||||
import BasicMatchSerializer from 'App/Serializers/BasicMatchSerializer'
|
||||
|
||||
class StatsService {
|
||||
public async getSummonerStats (puuid: string, season?: number) {
|
||||
public async getRecentActivity(puuid: string) {
|
||||
return MatchRepository.recentActivity(puuid)
|
||||
}
|
||||
public async getSummonerStats(puuid: string, season?: number) {
|
||||
console.time('GLOBAL')
|
||||
const globalStats = await MatchRepository.globalStats(puuid, season)
|
||||
const globalStats = await MatchRepository.globalStats(puuid)
|
||||
console.timeEnd('GLOBAL')
|
||||
console.time('GAMEMODE')
|
||||
const gamemodeStats = await MatchRepository.gamemodeStats(puuid, season)
|
||||
const gamemodeStats = await MatchRepository.gamemodeStats(puuid)
|
||||
console.timeEnd('GAMEMODE')
|
||||
console.time('ROLE')
|
||||
const roleStats = await MatchRepository.roleStats(puuid, season)
|
||||
const roleStats = await MatchRepository.roleStats(puuid)
|
||||
// Check if all roles are in the array
|
||||
const roles = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
|
||||
const roles = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
|
||||
for (const role of roles) {
|
||||
if (!roleStats.find(r => r.role === role)) {
|
||||
const findedRole = roleStats.find((r) => TeamPosition[r.role] === role)
|
||||
if (findedRole) {
|
||||
findedRole.role = TeamPosition[findedRole.role]
|
||||
} else {
|
||||
roleStats.push({
|
||||
count: 0,
|
||||
losses: 0,
|
||||
|
|
@ -25,22 +33,33 @@ class StatsService {
|
|||
}
|
||||
console.timeEnd('ROLE')
|
||||
console.time('CHAMPION')
|
||||
const championStats = await MatchRepository.championStats(puuid, 5, season)
|
||||
const championStats = await MatchRepository.championStats(puuid, 5)
|
||||
for (const champ of championStats) {
|
||||
champ.champion = BasicMatchSerializer.getChampion(champ.id)
|
||||
}
|
||||
console.timeEnd('CHAMPION')
|
||||
console.time('CHAMPION-CLASS')
|
||||
const championClassStats = await MatchRepository.championClassStats(puuid, season)
|
||||
const championClassStats = await MatchRepository.championClassStats(puuid)
|
||||
for (const champ of championClassStats) {
|
||||
champ.id = ChampionRoles[champ.id]
|
||||
}
|
||||
console.timeEnd('CHAMPION-CLASS')
|
||||
console.time('MATES')
|
||||
const mates = await MatchRepository.mates(puuid, season)
|
||||
const mates = await MatchRepository.mates(puuid)
|
||||
console.timeEnd('MATES')
|
||||
|
||||
console.time('RECENT_ACTIVITY')
|
||||
const recentActivity = await MatchRepository.recentActivity(puuid)
|
||||
console.timeEnd('RECENT_ACTIVITY')
|
||||
|
||||
return {
|
||||
global: globalStats[0],
|
||||
global: globalStats,
|
||||
league: gamemodeStats,
|
||||
role: roleStats.sort(sortTeamByRole),
|
||||
champion: championStats,
|
||||
class: championClassStats,
|
||||
mates,
|
||||
champion: championStats,
|
||||
recentActivity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,48 @@
|
|||
import Jax from './Jax'
|
||||
import { SummonerDTO } from 'App/Services/Jax/src/Endpoints/SummonerEndpoint'
|
||||
import { LeagueEntryDTO } from './Jax/src/Endpoints/LeagueEndpoint'
|
||||
import { SummonerModel } from 'App/Models/Summoner'
|
||||
import Summoner from 'App/Models/Summoner'
|
||||
import { PlayerRankParsed } from 'App/Parsers/ParsedType'
|
||||
import MatchPlayerRank from 'App/Models/MatchPlayerRank'
|
||||
|
||||
export interface LeagueEntriesByQueue {
|
||||
soloQ?: LeagueEntryByQueue,
|
||||
soloQ?: LeagueEntryByQueue
|
||||
flex5v5?: LeagueEntryByQueue
|
||||
}
|
||||
|
||||
export interface LeagueEntryByQueue extends LeagueEntryDTO {
|
||||
fullRank: string,
|
||||
winrate: string,
|
||||
fullRank: string
|
||||
winrate: string
|
||||
shortName: string | number
|
||||
}
|
||||
|
||||
class SummonerService {
|
||||
private uniqueLeagues = ['CHALLENGER', 'GRANDMASTER', 'MASTER']
|
||||
private leaguesNumbers = { 'I': 1, 'II': 2, 'III': 3, 'IV': 4 }
|
||||
private readonly uniqueLeagues = ['CHALLENGER', 'GRANDMASTER', 'MASTER']
|
||||
public readonly leaguesNumbers = { I: 1, II: 2, III: 3, IV: 4 }
|
||||
|
||||
public getRankedShortName(rank: PlayerRankParsed | MatchPlayerRank) {
|
||||
return this.uniqueLeagues.includes(rank.tier) ? rank.lp : rank.tier[0] + rank.rank
|
||||
}
|
||||
|
||||
public getWinrate(wins: number, losses: number) {
|
||||
return +((wins * 100) / (wins + losses)).toFixed(1) + '%'
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to transform League Data from the Riot API
|
||||
* @param league raw data of the league from Riot API
|
||||
*/
|
||||
private getleagueData (league?: LeagueEntryDTO): LeagueEntryByQueue | null {
|
||||
private getleagueData(league?: LeagueEntryDTO): LeagueEntryByQueue | null {
|
||||
if (!league) {
|
||||
return null
|
||||
}
|
||||
const fullRank = this.uniqueLeagues.includes(league.tier) ? league.tier : `${league.tier} ${league.rank}`
|
||||
const winrate = +(league.wins * 100 / (league.wins + league.losses)).toFixed(1) + '%'
|
||||
const shortName = this.uniqueLeagues.includes(league.tier) ?
|
||||
league.leaguePoints :
|
||||
league.tier[0] + this.leaguesNumbers[league.rank]
|
||||
const fullRank = this.uniqueLeagues.includes(league.tier)
|
||||
? league.tier
|
||||
: `${league.tier} ${league.rank}`
|
||||
const winrate = this.getWinrate(league.wins, league.losses)
|
||||
const shortName = this.uniqueLeagues.includes(league.tier)
|
||||
? league.leaguePoints
|
||||
: league.tier[0] + this.leaguesNumbers[league.rank]
|
||||
|
||||
return {
|
||||
...league,
|
||||
|
|
@ -42,10 +54,10 @@ class SummonerService {
|
|||
|
||||
/**
|
||||
* Get account infos for a searched summoner name
|
||||
* @param summonerName
|
||||
* @param region
|
||||
* @param summonerName
|
||||
* @param region
|
||||
*/
|
||||
public async getAccount (summonerName: string, region: string) {
|
||||
public async getAccount(summonerName: string, region: string) {
|
||||
const name = summonerName.toLowerCase()
|
||||
const account = await Jax.Summoner.summonerName(name, region)
|
||||
return account
|
||||
|
|
@ -56,34 +68,28 @@ class SummonerService {
|
|||
* @param account of the summoner
|
||||
* @param summonerDB summoner in the database
|
||||
*/
|
||||
public getAllSummonerNames (account: SummonerDTO, summonerDB: SummonerModel) {
|
||||
const names = summonerDB.names ? summonerDB.names : []
|
||||
|
||||
if (!names.find(n => n.name === account.name)) {
|
||||
names.push({
|
||||
name: account.name,
|
||||
date: new Date(),
|
||||
})
|
||||
summonerDB.names = names
|
||||
}
|
||||
|
||||
return names
|
||||
public async getAllSummonerNames(account: SummonerDTO, summonerDB: Summoner) {
|
||||
await summonerDB.related('names').firstOrCreate({
|
||||
name: account.name,
|
||||
})
|
||||
return summonerDB.related('names').query().select('name', 'created_at')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ranked data for a specific Summoner
|
||||
* @param account
|
||||
* @param region
|
||||
* @param region
|
||||
*/
|
||||
public async getRanked (account: SummonerDTO, region: string): Promise<LeagueEntriesByQueue> {
|
||||
const ranked = await Jax.League.summonerID(account.id, region)
|
||||
const result:LeagueEntriesByQueue = {}
|
||||
public async getRanked(summonerId: string, region: string): Promise<LeagueEntriesByQueue> {
|
||||
const ranked = await Jax.League.summonerID(summonerId, region)
|
||||
const result: LeagueEntriesByQueue = {}
|
||||
|
||||
if (ranked && ranked.length) {
|
||||
result.soloQ = this.getleagueData(ranked.find(e => e.queueType === 'RANKED_SOLO_5x5')) || undefined
|
||||
result.flex5v5 = this.getleagueData(ranked.find(e => e.queueType === 'RANKED_FLEX_SR')) || undefined
|
||||
result.soloQ =
|
||||
this.getleagueData(ranked.find((e) => e.queueType === 'RANKED_SOLO_5x5')) || undefined
|
||||
result.flex5v5 =
|
||||
this.getleagueData(ranked.find((e) => e.queueType === 'RANKED_FLEX_SR')) || undefined
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import { schema } from '@ioc:Adonis/Core/Validator'
|
||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
|
||||
export default class DetailedMatchValidator {
|
||||
constructor (private ctx: HttpContextContract) {
|
||||
}
|
||||
constructor(protected ctx: HttpContextContract) {}
|
||||
|
||||
/**
|
||||
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
/*
|
||||
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
*
|
||||
* For example:
|
||||
* 1. The username must be of data type string. But then also, it should
|
||||
|
|
@ -25,20 +24,9 @@ export default class DetailedMatchValidator {
|
|||
* ```
|
||||
*/
|
||||
public schema = schema.create({
|
||||
gameId: schema.number(),
|
||||
region: schema.string(),
|
||||
matchId: schema.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The `schema` first gets compiled to a reusable function and then that compiled
|
||||
* function validates the data at runtime.
|
||||
*
|
||||
* Since, compiling the schema is an expensive operation, you must always cache it by
|
||||
* defining a unique cache key. The simplest way is to use the current request route
|
||||
* key, which is a combination of the route pattern and HTTP method.
|
||||
*/
|
||||
public cacheKey = this.ctx.routeKey
|
||||
|
||||
/**
|
||||
* Custom messages for validation failures. You can make use of dot notation `(.)`
|
||||
* for targeting nested fields and array expressions `(*)` for targeting all
|
||||
|
|
@ -48,6 +36,7 @@ export default class DetailedMatchValidator {
|
|||
* 'profile.username.required': 'Username is required',
|
||||
* 'scores.*.number': 'Define scores as valid numbers'
|
||||
* }
|
||||
*/
|
||||
*
|
||||
*/
|
||||
public messages = {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import { schema } from '@ioc:Adonis/Core/Validator'
|
||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
|
||||
export default class MatchesIndexValidator {
|
||||
constructor (private ctx: HttpContextContract) {
|
||||
}
|
||||
constructor(protected ctx: HttpContextContract) {}
|
||||
|
||||
/**
|
||||
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
/*
|
||||
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
*
|
||||
* For example:
|
||||
* 1. The username must be of data type string. But then also, it should
|
||||
|
|
@ -26,24 +25,11 @@ export default class MatchesIndexValidator {
|
|||
*/
|
||||
public schema = schema.create({
|
||||
puuid: schema.string(),
|
||||
accountId: schema.string(),
|
||||
region: schema.string(),
|
||||
gameIds: schema.array().members(
|
||||
schema.number()
|
||||
),
|
||||
matchIds: schema.array().members(schema.string()),
|
||||
season: schema.number.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The `schema` first gets compiled to a reusable function and then that compiled
|
||||
* function validates the data at runtime.
|
||||
*
|
||||
* Since, compiling the schema is an expensive operation, you must always cache it by
|
||||
* defining a unique cache key. The simplest way is to use the current request route
|
||||
* key, which is a combination of the route pattern and HTTP method.
|
||||
*/
|
||||
public cacheKey = this.ctx.routeKey
|
||||
|
||||
/**
|
||||
* Custom messages for validation failures. You can make use of dot notation `(.)`
|
||||
* for targeting nested fields and array expressions `(*)` for targeting all
|
||||
|
|
@ -53,6 +39,7 @@ export default class MatchesIndexValidator {
|
|||
* 'profile.username.required': 'Username is required',
|
||||
* 'scores.*.number': 'Define scores as valid numbers'
|
||||
* }
|
||||
*/
|
||||
*
|
||||
*/
|
||||
public messages = {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import { rules, schema } from '@ioc:Adonis/Core/Validator'
|
||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
|
||||
export default class SummonerBasicValidator {
|
||||
constructor (private ctx: HttpContextContract) {
|
||||
}
|
||||
constructor(protected ctx: HttpContextContract) {}
|
||||
|
||||
/**
|
||||
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
/*
|
||||
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
*
|
||||
* For example:
|
||||
* 1. The username must be of data type string. But then also, it should
|
||||
|
|
@ -25,22 +24,10 @@ export default class SummonerBasicValidator {
|
|||
* ```
|
||||
*/
|
||||
public schema = schema.create({
|
||||
summoner: schema.string({}, [
|
||||
rules.regex(/^[0-9\p{L} _\.]+$/u),
|
||||
]),
|
||||
summoner: schema.string({}, [rules.regex(/^[0-9\p{L} _\.]+$/u)]),
|
||||
region: schema.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The `schema` first gets compiled to a reusable function and then that compiled
|
||||
* function validates the data at runtime.
|
||||
*
|
||||
* Since, compiling the schema is an expensive operation, you must always cache it by
|
||||
* defining a unique cache key. The simplest way is to use the current request route
|
||||
* key, which is a combination of the route pattern and HTTP method.
|
||||
*/
|
||||
public cacheKey = this.ctx.routeKey
|
||||
|
||||
/**
|
||||
* Custom messages for validation failures. You can make use of dot notation `(.)`
|
||||
* for targeting nested fields and array expressions `(*)` for targeting all
|
||||
|
|
@ -50,6 +37,7 @@ export default class SummonerBasicValidator {
|
|||
* 'profile.username.required': 'Username is required',
|
||||
* 'scores.*.number': 'Define scores as valid numbers'
|
||||
* }
|
||||
*/
|
||||
*
|
||||
*/
|
||||
public messages = {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import { schema } from '@ioc:Adonis/Core/Validator'
|
||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
|
||||
export default class SummonerChampionValidator {
|
||||
constructor (private ctx: HttpContextContract) {
|
||||
}
|
||||
constructor(protected ctx: HttpContextContract) {}
|
||||
|
||||
/**
|
||||
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
/*
|
||||
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
*
|
||||
* For example:
|
||||
* 1. The username must be of data type string. But then also, it should
|
||||
|
|
@ -30,16 +29,6 @@ export default class SummonerChampionValidator {
|
|||
season: schema.number.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The `schema` first gets compiled to a reusable function and then that compiled
|
||||
* function validates the data at runtime.
|
||||
*
|
||||
* Since, compiling the schema is an expensive operation, you must always cache it by
|
||||
* defining a unique cache key. The simplest way is to use the current request route
|
||||
* key, which is a combination of the route pattern and HTTP method.
|
||||
*/
|
||||
public cacheKey = this.ctx.routeKey
|
||||
|
||||
/**
|
||||
* Custom messages for validation failures. You can make use of dot notation `(.)`
|
||||
* for targeting nested fields and array expressions `(*)` for targeting all
|
||||
|
|
@ -49,6 +38,7 @@ export default class SummonerChampionValidator {
|
|||
* 'profile.username.required': 'Username is required',
|
||||
* 'scores.*.number': 'Define scores as valid numbers'
|
||||
* }
|
||||
*/
|
||||
*
|
||||
*/
|
||||
public messages = {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import { schema } from '@ioc:Adonis/Core/Validator'
|
||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
|
||||
export default class SummonerLiveValidator {
|
||||
constructor (private ctx: HttpContextContract) {
|
||||
}
|
||||
constructor(protected ctx: HttpContextContract) {}
|
||||
|
||||
/**
|
||||
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
/*
|
||||
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
*
|
||||
* For example:
|
||||
* 1. The username must be of data type string. But then also, it should
|
||||
|
|
@ -29,16 +28,6 @@ export default class SummonerLiveValidator {
|
|||
region: schema.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The `schema` first gets compiled to a reusable function and then that compiled
|
||||
* function validates the data at runtime.
|
||||
*
|
||||
* Since, compiling the schema is an expensive operation, you must always cache it by
|
||||
* defining a unique cache key. The simplest way is to use the current request route
|
||||
* key, which is a combination of the route pattern and HTTP method.
|
||||
*/
|
||||
public cacheKey = this.ctx.routeKey
|
||||
|
||||
/**
|
||||
* Custom messages for validation failures. You can make use of dot notation `(.)`
|
||||
* for targeting nested fields and array expressions `(*)` for targeting all
|
||||
|
|
@ -48,6 +37,7 @@ export default class SummonerLiveValidator {
|
|||
* 'profile.username.required': 'Username is required',
|
||||
* 'scores.*.number': 'Define scores as valid numbers'
|
||||
* }
|
||||
*/
|
||||
*
|
||||
*/
|
||||
public messages = {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import { schema } from '@ioc:Adonis/Core/Validator'
|
||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
|
||||
export default class SummonerOverviewValidator {
|
||||
constructor (private ctx: HttpContextContract) {
|
||||
}
|
||||
constructor(protected ctx: HttpContextContract) {}
|
||||
|
||||
/**
|
||||
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
/*
|
||||
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
*
|
||||
* For example:
|
||||
* 1. The username must be of data type string. But then also, it should
|
||||
|
|
@ -26,21 +25,10 @@ export default class SummonerOverviewValidator {
|
|||
*/
|
||||
public schema = schema.create({
|
||||
puuid: schema.string(),
|
||||
accountId: schema.string(),
|
||||
region: schema.string(),
|
||||
season: schema.number.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The `schema` first gets compiled to a reusable function and then that compiled
|
||||
* function validates the data at runtime.
|
||||
*
|
||||
* Since, compiling the schema is an expensive operation, you must always cache it by
|
||||
* defining a unique cache key. The simplest way is to use the current request route
|
||||
* key, which is a combination of the route pattern and HTTP method.
|
||||
*/
|
||||
public cacheKey = this.ctx.routeKey
|
||||
|
||||
/**
|
||||
* Custom messages for validation failures. You can make use of dot notation `(.)`
|
||||
* for targeting nested fields and array expressions `(*)` for targeting all
|
||||
|
|
@ -50,6 +38,7 @@ export default class SummonerOverviewValidator {
|
|||
* 'profile.username.required': 'Username is required',
|
||||
* 'scores.*.number': 'Define scores as valid numbers'
|
||||
* }
|
||||
*/
|
||||
*
|
||||
*/
|
||||
public messages = {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
import { schema } from '@ioc:Adonis/Core/Validator'
|
||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||
|
||||
export default class SummonerRecordValidator {
|
||||
constructor (private ctx: HttpContextContract) {
|
||||
}
|
||||
constructor(protected ctx: HttpContextContract) {}
|
||||
|
||||
/**
|
||||
* Defining a schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
/*
|
||||
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
|
||||
*
|
||||
* For example:
|
||||
* 1. The username must be of data type string. But then also, it should
|
||||
|
|
@ -29,16 +28,6 @@ export default class SummonerRecordValidator {
|
|||
season: schema.number.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The `schema` first gets compiled to a reusable function and then that compiled
|
||||
* function validates the data at runtime.
|
||||
*
|
||||
* Since, compiling the schema is an expensive operation, you must always cache it by
|
||||
* defining a unique cache key. The simplest way is to use the current request route
|
||||
* key, which is a combination of the route pattern and HTTP method.
|
||||
*/
|
||||
public cacheKey = this.ctx.routeKey
|
||||
|
||||
/**
|
||||
* Custom messages for validation failures. You can make use of dot notation `(.)`
|
||||
* for targeting nested fields and array expressions `(*)` for targeting all
|
||||
|
|
@ -48,6 +37,7 @@ export default class SummonerRecordValidator {
|
|||
* 'profile.username.required': 'Username is required',
|
||||
* 'scores.*.number': 'Define scores as valid numbers'
|
||||
* }
|
||||
*/
|
||||
*
|
||||
*/
|
||||
public messages = {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,63 @@
|
|||
import { ParticipantBasic, ParticipantDetails } from './Models/Match'
|
||||
/**
|
||||
* All League of Legends regions used in Riot API
|
||||
*/
|
||||
export enum LeagueRegion {
|
||||
BRAZIL = 'br1',
|
||||
EUROPE_NORTHEAST = 'eun1',
|
||||
EUROPE_WEST = 'euw1',
|
||||
KOREA = 'kr',
|
||||
LATIN_AMERICA_NORTH = 'la1',
|
||||
LATIN_AMERICA_SOUTH = 'la2',
|
||||
NORTH_AMERICA = 'na1',
|
||||
OCEANIA = 'oc1',
|
||||
RUSSIA = 'ru',
|
||||
TURKEY = 'tr1',
|
||||
JAPAN = 'jp1',
|
||||
}
|
||||
|
||||
/**
|
||||
* New regions used in Riot API >= v5
|
||||
*/
|
||||
export enum RiotRegion {
|
||||
AMERICAS = 'americas',
|
||||
ASIA = 'asia',
|
||||
EUROPE = 'europe',
|
||||
}
|
||||
|
||||
/**
|
||||
* Map old Riot API regions to new ones
|
||||
* @param region : old region
|
||||
* @returns new region name
|
||||
*/
|
||||
export function getRiotRegion(region: string): RiotRegion {
|
||||
switch (
|
||||
region as LeagueRegion // TODO: remove cast when region is typed to "Region" everywhere instead of string
|
||||
) {
|
||||
case LeagueRegion.NORTH_AMERICA:
|
||||
case LeagueRegion.BRAZIL:
|
||||
case LeagueRegion.LATIN_AMERICA_NORTH:
|
||||
case LeagueRegion.LATIN_AMERICA_SOUTH:
|
||||
case LeagueRegion.OCEANIA:
|
||||
return RiotRegion.AMERICAS
|
||||
case LeagueRegion.KOREA:
|
||||
case LeagueRegion.JAPAN:
|
||||
return RiotRegion.ASIA
|
||||
case LeagueRegion.EUROPE_NORTHEAST:
|
||||
case LeagueRegion.EUROPE_WEST:
|
||||
case LeagueRegion.TURKEY:
|
||||
case LeagueRegion.RUSSIA:
|
||||
return RiotRegion.EUROPE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to help define a player's role
|
||||
*/
|
||||
export interface PlayerRole {
|
||||
champion: number
|
||||
jungle?: boolean
|
||||
support?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* League of Legends queues with defined role for each summoner
|
||||
|
|
@ -13,8 +72,13 @@ export const queuesWithRole = [
|
|||
]
|
||||
|
||||
/**
|
||||
* League of Legends seasons timestamps
|
||||
*/
|
||||
* League of Legends tutorial queues
|
||||
*/
|
||||
export const tutorialQueues = [2000, 2010, 2020]
|
||||
|
||||
/**
|
||||
* League of Legends seasons timestamps
|
||||
*/
|
||||
export const seasons = {
|
||||
0: 9,
|
||||
1578628800000: 10,
|
||||
|
|
@ -23,28 +87,47 @@ export const seasons = {
|
|||
}
|
||||
|
||||
/**
|
||||
* League of Legends all support item ids
|
||||
*/
|
||||
* League of Legends all support item ids
|
||||
*/
|
||||
export const supportItems = [3850, 3851, 3853, 3854, 3855, 3857, 3858, 3859, 3860, 3862, 3863, 3864]
|
||||
|
||||
/**
|
||||
* Get season number for a match
|
||||
* @param timestamp
|
||||
* @param timestamp
|
||||
*/
|
||||
export function getSeasonNumber (timestamp: number): number {
|
||||
const arrSeasons = Object.keys(seasons).map(k => Number(k))
|
||||
export function getSeasonNumber(timestamp: number): number {
|
||||
const arrSeasons = Object.keys(seasons).map((k) => Number(k))
|
||||
arrSeasons.push(timestamp)
|
||||
arrSeasons.sort()
|
||||
const indexSeason = arrSeasons.indexOf(timestamp) - 1
|
||||
return seasons[arrSeasons[indexSeason]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current League of Legends season number
|
||||
*/
|
||||
export function getCurrentSeason(): number {
|
||||
const lastTimestamp = Object.keys(seasons).pop()!
|
||||
return seasons[lastTimestamp]
|
||||
}
|
||||
|
||||
interface SortableByRole {
|
||||
role: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort array of Players by roles according to a specific order
|
||||
* @param a first player
|
||||
* @param b second player
|
||||
*/
|
||||
export function sortTeamByRole (a: ParticipantBasic | ParticipantDetails, b: ParticipantBasic | ParticipantDetails) {
|
||||
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT']
|
||||
export function sortTeamByRole<T extends SortableByRole>(a: T, b: T) {
|
||||
const sortingArr = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
|
||||
return sortingArr.indexOf(a.role) - sortingArr.indexOf(b.role)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/46700791/9188650
|
||||
export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
|
||||
if (value === null || value === undefined) return false
|
||||
const testDummy: TValue = value
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
66
server/commands/LoadV4Matches.ts
Normal file
66
server/commands/LoadV4Matches.ts
Normal 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}.`)
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ export const http: ServerConfig = {
|
|||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Force content negotiation to JSON
|
||||
| Force Content Negotiation
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The internals of the framework relies on the content negotiation to
|
||||
|
|
@ -121,9 +121,9 @@ export const http: ServerConfig = {
|
|||
| However, it is a very common these days that API servers always wants to
|
||||
| make response in JSON regardless of the existence of the `Accept` header.
|
||||
|
|
||||
| By setting `forceContentNegotiationToJSON = true`, you negotiate with the
|
||||
| server in advance to always return JSON without relying on the client
|
||||
| to set the header explicitly.
|
||||
| By setting `forceContentNegotiationTo = 'application/json'`, you negotiate
|
||||
| with the server in advance to always return JSON without relying on the
|
||||
| client to set the header explicitly.
|
||||
|
|
||||
*/
|
||||
forceContentNegotiationTo: 'application/json',
|
||||
|
|
|
|||
|
|
@ -9,25 +9,25 @@ import { BodyParserConfig } from '@ioc:Adonis/Core/BodyParser'
|
|||
|
||||
const bodyParserConfig: BodyParserConfig = {
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| White listed methods
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| HTTP methods for which body parsing must be performed. It is a good practice
|
||||
| to avoid body parsing for `GET` requests.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| White listed methods
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| HTTP methods for which body parsing must be performed. It is a good practice
|
||||
| to avoid body parsing for `GET` requests.
|
||||
|
|
||||
*/
|
||||
whitelistedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JSON parser settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The settings for the JSON parser. The types defines the request content
|
||||
| types which gets processed by the JSON parser.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| JSON parser settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The settings for the JSON parser. The types defines the request content
|
||||
| types which gets processed by the JSON parser.
|
||||
|
|
||||
*/
|
||||
json: {
|
||||
encoding: 'utf-8',
|
||||
limit: '1mb',
|
||||
|
|
@ -41,44 +41,44 @@ const bodyParserConfig: BodyParserConfig = {
|
|||
},
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form parser settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The settings for the `application/x-www-form-urlencoded` parser. The types
|
||||
| defines the request content types which gets processed by the form parser.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Form parser settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The settings for the `application/x-www-form-urlencoded` parser. The types
|
||||
| defines the request content types which gets processed by the form parser.
|
||||
|
|
||||
*/
|
||||
form: {
|
||||
encoding: 'utf-8',
|
||||
limit: '1mb',
|
||||
queryString: {},
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Convert empty strings to null
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Convert empty form fields to null. HTML forms results in field string
|
||||
| value when the field is left blank. This option normalizes all the blank
|
||||
| field values to "null"
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Convert empty strings to null
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Convert empty form fields to null. HTML forms results in field string
|
||||
| value when the field is left blank. This option normalizes all the blank
|
||||
| field values to "null"
|
||||
|
|
||||
*/
|
||||
convertEmptyStringsToNull: true,
|
||||
|
||||
types: ['application/x-www-form-urlencoded'],
|
||||
},
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Raw body parser settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Raw body just reads the request body stream as a plain text, which you
|
||||
| can process by hand. This must be used when request body type is not
|
||||
| supported by the body parser.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Raw body parser settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Raw body just reads the request body stream as a plain text, which you
|
||||
| can process by hand. This must be used when request body type is not
|
||||
| supported by the body parser.
|
||||
|
|
||||
*/
|
||||
raw: {
|
||||
encoding: 'utf-8',
|
||||
limit: '1mb',
|
||||
|
|
@ -87,117 +87,117 @@ const bodyParserConfig: BodyParserConfig = {
|
|||
},
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Multipart parser settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The settings for the `multipart/form-data` parser. The types defines the
|
||||
| request content types which gets processed by the form parser.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Multipart parser settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The settings for the `multipart/form-data` parser. The types defines the
|
||||
| request content types which gets processed by the form parser.
|
||||
|
|
||||
*/
|
||||
multipart: {
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auto process
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The auto process option will process uploaded files and writes them to
|
||||
| the `tmp` folder. You can turn it off and then manually use the stream
|
||||
| to pipe stream to a different destination.
|
||||
|
|
||||
| It is recommended to keep `autoProcess=true`. Unless you are processing bigger
|
||||
| file sizes.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Auto process
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The auto process option will process uploaded files and writes them to
|
||||
| the `tmp` folder. You can turn it off and then manually use the stream
|
||||
| to pipe stream to a different destination.
|
||||
|
|
||||
| It is recommended to keep `autoProcess=true`. Unless you are processing bigger
|
||||
| file sizes.
|
||||
|
|
||||
*/
|
||||
autoProcess: true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Files to be processed manually
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can turn off `autoProcess` for certain routes by defining
|
||||
| routes inside the following array.
|
||||
|
|
||||
| NOTE: Make sure the route pattern starts with a leading slash.
|
||||
|
|
||||
| Correct
|
||||
| ```js
|
||||
| /projects/:id/file
|
||||
| ```
|
||||
|
|
||||
| Incorrect
|
||||
| ```js
|
||||
| projects/:id/file
|
||||
| ```
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Files to be processed manually
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can turn off `autoProcess` for certain routes by defining
|
||||
| routes inside the following array.
|
||||
|
|
||||
| NOTE: Make sure the route pattern starts with a leading slash.
|
||||
|
|
||||
| Correct
|
||||
| ```js
|
||||
| /projects/:id/file
|
||||
| ```
|
||||
|
|
||||
| Incorrect
|
||||
| ```js
|
||||
| projects/:id/file
|
||||
| ```
|
||||
*/
|
||||
processManually: [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Temporary file name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When auto processing is on. We will use this method to compute the temporary
|
||||
| file name. AdonisJs will compute a unique `tmpPath` for you automatically,
|
||||
| However, you can also define your own custom method.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Temporary file name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When auto processing is on. We will use this method to compute the temporary
|
||||
| file name. AdonisJs will compute a unique `tmpPath` for you automatically,
|
||||
| However, you can also define your own custom method.
|
||||
|
|
||||
*/
|
||||
// tmpFileName () {
|
||||
// },
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encoding
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Request body encoding
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Encoding
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Request body encoding
|
||||
|
|
||||
*/
|
||||
encoding: 'utf-8',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Convert empty strings to null
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Convert empty form fields to null. HTML forms results in field string
|
||||
| value when the field is left blank. This option normalizes all the blank
|
||||
| field values to "null"
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Convert empty strings to null
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Convert empty form fields to null. HTML forms results in field string
|
||||
| value when the field is left blank. This option normalizes all the blank
|
||||
| field values to "null"
|
||||
|
|
||||
*/
|
||||
convertEmptyStringsToNull: true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Max Fields
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The maximum number of fields allowed in the request body. The field includes
|
||||
| text inputs and files both.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Max Fields
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The maximum number of fields allowed in the request body. The field includes
|
||||
| text inputs and files both.
|
||||
|
|
||||
*/
|
||||
maxFields: 1000,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Request body limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The total limit to the multipart body. This includes all request files
|
||||
| and fields data.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Request body limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The total limit to the multipart body. This includes all request files
|
||||
| and fields data.
|
||||
|
|
||||
*/
|
||||
limit: '20mb',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The types that will be considered and parsed as multipart body.
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The types that will be considered and parsed as multipart body.
|
||||
|
|
||||
*/
|
||||
types: ['multipart/form-data'],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
60
server/config/database.ts
Normal file
60
server/config/database.ts
Normal 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
148
server/config/drive.ts
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -41,6 +41,7 @@ const redisConfig: RedisConfig = {
|
|||
password: Env.get('REDIS_PASSWORD', ''),
|
||||
db: 0,
|
||||
keyPrefix: '',
|
||||
healthCheck: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
23
server/contracts/drive.ts
Normal file
23
server/contracts/drive.ts
Normal 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
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,5 @@ declare module '@ioc:Adonis/Core/Event' {
|
|||
| an instance of the the UserModel only.
|
||||
|
|
||||
*/
|
||||
interface EventsList {
|
||||
}
|
||||
interface EventsList {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,14 @@
|
|||
*/
|
||||
|
||||
declare module '@ioc:Adonis/Core/Hash' {
|
||||
import { HashDrivers } from '@ioc:Adonis/Core/Hash'
|
||||
|
||||
interface HashersList {
|
||||
bcrypt: {
|
||||
config: BcryptConfig,
|
||||
implementation: BcryptContract,
|
||||
},
|
||||
config: BcryptConfig
|
||||
implementation: BcryptContract
|
||||
}
|
||||
argon: {
|
||||
config: ArgonConfig,
|
||||
implementation: ArgonContract,
|
||||
},
|
||||
config: ArgonConfig
|
||||
implementation: ArgonContract
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@
|
|||
|
||||
declare module '@ioc:Adonis/Addons/Redis' {
|
||||
interface RedisConnectionsList {
|
||||
local: RedisConnectionConfig,
|
||||
local: RedisConnectionConfig
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
server/database/factories/index.ts
Normal file
1
server/database/factories/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
// import Factory from '@ioc:Adonis/Lucid/Factory'
|
||||
26
server/database/migrations/1631392754960_matches.ts
Normal file
26
server/database/migrations/1631392754960_matches.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
74
server/database/migrations/1631392766690_match_players.ts
Normal file
74
server/database/migrations/1631392766690_match_players.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
21
server/database/migrations/1631392773430_summoners.ts
Normal file
21
server/database/migrations/1631392773430_summoners.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
26
server/database/migrations/1631392778620_summoner_names.ts
Normal file
26
server/database/migrations/1631392778620_summoner_names.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue