246 lines
9.0 KiB
TypeScript
246 lines
9.0 KiB
TypeScript
function sameArrays(array1 : Array<any>, array2 : Array<any>) {
|
|
if(array1.length != array2.length) return false;
|
|
for(let e of array1) {
|
|
if(!array2.includes(e)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
import { ItemTree, treeInit, treeMerge, treeCutBranches, treeSort } from "./item_tree";
|
|
const itemDict = new Map()
|
|
|
|
async function itemList() {
|
|
const response = await fetch("https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/items.json")
|
|
const list = await response.json()
|
|
return list
|
|
}
|
|
|
|
function arrayRemovePercentage(array: Array<{count:number}>, totalGames:number, percentage: number) {
|
|
let toRemove : Array<{count:number}> = []
|
|
for(let item of array) {
|
|
if((item.count/totalGames) < 0.05) {
|
|
toRemove.push(item)
|
|
}
|
|
}
|
|
for(let tr of toRemove) {
|
|
array.splice(array.indexOf(tr), 1)
|
|
}
|
|
}
|
|
|
|
type Rune = {
|
|
count: number
|
|
primaryStyle: number
|
|
secondaryStyle: number
|
|
selections: Array<number>
|
|
pickrate?: number
|
|
};
|
|
type Builds = {
|
|
tree: ItemTree
|
|
start: Array<{data: number, count: number}>
|
|
bootsFirst: number
|
|
boots: Array<{data: number, count: number}>
|
|
lateGame: Array<{data: number, count: number}>
|
|
}
|
|
type Champion = {
|
|
id: Number
|
|
name: String
|
|
alias: String
|
|
}
|
|
|
|
async function championInfos(client, patch: number, champion: Champion) {
|
|
const championId = champion.id
|
|
const database = client.db("matches");
|
|
const matches = database.collection(patch)
|
|
const allMatches = matches.find()
|
|
|
|
let winningMatches = 0;
|
|
let losingMatches = 0;
|
|
let totalMatches = 0;
|
|
const runes : Array<Rune> = [];
|
|
const builds : Builds = {tree:treeInit(), start: [], bootsFirst: 0, boots: [], lateGame: []}
|
|
for await (let match of allMatches) {
|
|
totalMatches += 1;
|
|
let participantIndex = 0;
|
|
for(let participant of match.info.participants) {
|
|
participantIndex += 1
|
|
if(participant.championId != championId) continue;
|
|
|
|
// Winrate
|
|
if(participant.win)
|
|
winningMatches += 1;
|
|
else
|
|
losingMatches += 1;
|
|
|
|
// Runes
|
|
const primaryStyle = participant.perks.styles[0].style
|
|
const secondaryStyle = participant.perks.styles[1].style
|
|
const selections : Array<number> = []
|
|
for(let style of participant.perks.styles) {
|
|
for(let perk of style.selections) {
|
|
selections.push(perk.perk)
|
|
}
|
|
}
|
|
const gameRunes : Rune = {count:1, primaryStyle: primaryStyle, secondaryStyle: secondaryStyle, selections: selections};
|
|
let addRunes = true;
|
|
for(let rune of runes) {
|
|
if(rune.primaryStyle == gameRunes.primaryStyle
|
|
&& rune.secondaryStyle == gameRunes.secondaryStyle
|
|
&& sameArrays(rune.selections, gameRunes.selections)) {
|
|
rune.count++; addRunes = false; break;
|
|
}
|
|
}
|
|
if(addRunes) runes.push(gameRunes)
|
|
|
|
// Items
|
|
const items : Array<number> = []
|
|
for(let frame of match.timeline.info.frames) {
|
|
for(let event of frame.events) {
|
|
if(event.participantId != participantIndex) continue;
|
|
if(event.type == "ITEM_UNDO") {
|
|
if(items.length > 0 && items[items.length - 1] == event.beforeId) {
|
|
items.pop()
|
|
}
|
|
continue;
|
|
}
|
|
if(event.type != "ITEM_PURCHASED") continue;
|
|
|
|
let itemInfo = itemDict.get(event.itemId)
|
|
|
|
// Handle boots differently
|
|
if(itemInfo.categories.includes("Boots")){
|
|
if(itemInfo.to.length == 0 || event.itemId == 3006) {
|
|
// Check for bootsFirst
|
|
if(items.length < 2) {
|
|
builds.bootsFirst += 1
|
|
}
|
|
|
|
// Add to boots
|
|
const already = builds.boots.find((x) => x.data == event.itemId)
|
|
if(already == undefined) builds.boots.push({count:1, data:event.itemId})
|
|
else already.count += 1
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Check if item should be included
|
|
if(itemInfo.categories.includes("Consumable")) continue;
|
|
if(itemInfo.categories.includes("Trinket")) continue;
|
|
|
|
// Ignore Cull as not-first item
|
|
if(event.itemId == 1083 && items.length >= 1) continue;
|
|
|
|
// Ignore non-final items, except when first item bought
|
|
if(itemInfo.to.length != 0 && items.length >= 1) continue;
|
|
|
|
items.push(event.itemId)
|
|
}
|
|
}
|
|
|
|
// Core items
|
|
treeMerge(builds.tree, items.slice(1, 4))
|
|
|
|
// Start items
|
|
if(items.length >= 1) {
|
|
const already = builds.start.find((x) => x.data == items[0])
|
|
if(already == undefined) builds.start.push({count:1, data:items[0]})
|
|
else already.count += 1
|
|
}
|
|
|
|
// Late game items
|
|
for(let item of items.slice(4)) {
|
|
const already = builds.lateGame.find((x) => x.data == item)
|
|
if(already == undefined) builds.lateGame.push({count:1, data:item})
|
|
else already.count += 1
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Filter runes to keep 3 most played
|
|
const maxes = [0, 0, 0] // 24, 2, 1 -> 18 > 1 -> 24, 2, 2 -> 18 > 2 -> 24, 24, 2
|
|
const maxRunes : Array<Rune | null> = [null, null, null]
|
|
for(let rune of runes) {
|
|
let maxcount = 2;
|
|
if(rune.count <= maxes[maxcount]) continue;
|
|
|
|
while(maxcount > 0 && rune.count > maxes[maxcount]) {
|
|
maxes[maxcount] = maxes[maxcount - 1];
|
|
maxRunes[maxcount] = maxRunes[maxcount - 1];
|
|
maxcount--;
|
|
}
|
|
|
|
rune.pickrate = rune.count / (winningMatches + losingMatches)
|
|
if(rune.count <= maxes[maxcount]) maxcount++;
|
|
maxes[maxcount] = rune.count
|
|
maxRunes[maxcount] = rune
|
|
}
|
|
|
|
// Cut item tree branches to keep only 4 branches every time and with percentage threshold
|
|
builds.tree.count = (winningMatches + losingMatches)
|
|
treeCutBranches(builds.tree, 4, 0.05)
|
|
treeSort(builds.tree)
|
|
|
|
// Cut item start, to only 4 and with percentage threshold
|
|
while(builds.start.length > 4) {
|
|
let leastUsedItem = builds.start.reduce((a, b) => Math.min(a.count, b.count) == a.count ? a : b, {data:undefined, count: +Infinity})
|
|
builds.start.splice(builds.start.indexOf(leastUsedItem), 1)
|
|
}
|
|
arrayRemovePercentage(builds.start, (winningMatches + losingMatches), 0.05)
|
|
builds.start.sort((a, b) => b.count - a.count)
|
|
|
|
// Remove boots that are not within percentage threshold
|
|
arrayRemovePercentage(builds.boots, (winningMatches + losingMatches), 0.05)
|
|
|
|
builds.boots.sort((a, b) => b.count - a.count)
|
|
builds.bootsFirst /= (winningMatches + losingMatches)
|
|
builds.lateGame.sort((a, b) => b.count - a.count)
|
|
|
|
return {name: champion.name,
|
|
alias: champion.alias.toLowerCase(),
|
|
id: championId,
|
|
winrate:winningMatches / (winningMatches + losingMatches),
|
|
gameCount:(winningMatches + losingMatches),
|
|
pickrate:(winningMatches + losingMatches)/totalMatches,
|
|
runes: maxRunes.filter((x) => x != null),
|
|
builds: builds
|
|
};
|
|
}
|
|
|
|
async function makeChampionStat(client, patch : number, champion : Champion) {
|
|
const championInfo = await championInfos(client, patch, champion)
|
|
const database = client.db("champions")
|
|
const collection = database.collection(patch)
|
|
await collection.updateOne({id: championInfo.id}, {$set: championInfo}, { upsert: true })
|
|
}
|
|
|
|
async function championList() {
|
|
const response = await fetch("https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json");
|
|
const list = await response.json()
|
|
return list.slice(1)
|
|
}
|
|
|
|
async function makeChampionsStats(client, patch : number) {
|
|
var globalItems = await itemList()
|
|
for(let item of globalItems) {
|
|
itemDict.set(item.id, item)
|
|
}
|
|
|
|
const list = await championList()
|
|
console.log("Generating stats for " + list.length + " champions")
|
|
let i = 0;
|
|
for(let champion of list) {
|
|
console.log("Entry " + i + "/" + list.length + " (" + champion.name + ")...")
|
|
await makeChampionStat(client, patch, champion)
|
|
i += 1
|
|
}
|
|
|
|
const database = client.db("champions")
|
|
const collection = database.collection(patch)
|
|
await collection.createIndex({id:1})
|
|
await collection.createIndex({alias:1})
|
|
}
|
|
|
|
export default {makeChampionsStats}
|