Lint and format
Some checks failed
pipeline / lint-and-format (push) Failing after 56s
pipeline / build-and-push-images (push) Has been skipped

This commit is contained in:
2026-01-21 00:59:23 +01:00
parent 353baa6267
commit 3fc52205f6
53 changed files with 8505 additions and 2048 deletions

View File

@@ -1,336 +1,374 @@
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;
function sameArrays(array1: Array<any>, array2: Array<any>) {
if (array1.length != array2.length) return false
for (const e of array1) {
if (!array2.includes(e)) return false
}
return true
}
import { MongoClient } from "mongodb";
import { ItemTree, treeInit, treeMerge, treeCutBranches, treeSort } from "./item_tree";
import { MongoClient } from 'mongodb'
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
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) < percentage) {
toRemove.push(item)
}
}
for(let tr of toRemove) {
array.splice(array.indexOf(tr), 1)
function arrayRemovePercentage(
array: Array<{ count: number }>,
totalGames: number,
percentage: number
) {
const toRemove: Array<{ count: number }> = []
for (const item of array) {
if (item.count / totalGames < percentage) {
toRemove.push(item)
}
}
for (const tr of toRemove) {
array.splice(array.indexOf(tr), 1)
}
}
type Rune = {
count: number
primaryStyle: number
secondaryStyle: number
selections: Array<number>
pickrate?: number
};
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}>
suppItems?: Array<{data: number, count: number}>
tree: ItemTree
start: Array<{ data: number; count: number }>
bootsFirst: number
boots: Array<{ data: number; count: number }>
lateGame: Array<{ data: number; count: number }>
suppItems?: Array<{ data: number; count: number }>
}
type Champion = {
id: Number
name: String
alias: String
id: number
name: string
alias: string
}
type LaneData = {
data: string
count: number
winningMatches: number
losingMatches: number
winrate: number
pickrate: number
runes: Array<Rune>
builds: Builds
data: string
count: number
winningMatches: number
losingMatches: number
winrate: number
pickrate: number
runes: Array<Rune>
builds: Builds
}
type ChampionData = {
champion: Champion
winningMatches: number
losingMatches: number
lanes: Array<LaneData>
champion: Champion
winningMatches: number
losingMatches: number
lanes: Array<LaneData>
}
function handleParticipantRunes(participant, runes: Array<Rune>) {
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 primaryStyle = participant.perks.styles[0].style
const secondaryStyle = participant.perks.styles[1].style
const selections: Array<number> = []
for (const style of participant.perks.styles) {
for (const 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;
}
}
const gameRunes: Rune = {
count: 1,
primaryStyle: primaryStyle,
secondaryStyle: secondaryStyle,
selections: selections
}
let addRunes = true
for (const 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)
}
if (addRunes) runes.push(gameRunes)
}
function handleMatchItems(timeline, participant: any, participantIndex : number, builds: Builds) {
const items : Array<number> = []
for(let frame of 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;
}
let itemInfo = itemDict.get(event.itemId)
// Handle bounty of worlds destroy as upgrade
if(event.type == "ITEM_DESTROYED") {
if(event.itemId == 3867) {
let suppItem : number = itemInfo.to.find((x:number) =>
x == participant.item0
|| x == participant.item1
|| x == participant.item2
|| x == participant.item3
|| x == participant.item4
|| x == participant.item5
|| x == participant.item6 )
if(suppItem != undefined) {
const already = builds.suppItems.find((x) => x.data == suppItem)
if(already == undefined) builds.suppItems.push({count:1, data: suppItem})
else already.count += 1
}
}
}
if(event.type != "ITEM_PURCHASED") continue;
// Handle boots upgrades
if(itemInfo.requiredBuffCurrencyName == "Feats_NoxianBootPurchaseBuff"
|| itemInfo.requiredBuffCurrencyName == "Feats_SpecialQuestBootBuff") {
continue;
}
// Handle boots differently
if(itemInfo.categories.includes("Boots")){
if(itemInfo.to.length == 0 || (itemInfo.to[0] >= 3171 && itemInfo.to[0] <= 3176)) {
// 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 zephyr
if(event.itemId == 3172) 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)
function handleMatchItems(timeline, participant: any, participantIndex: number, builds: Builds) {
const items: Array<number> = []
for (const frame of timeline.info.frames) {
for (const 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
}
const itemInfo = itemDict.get(event.itemId)
// Handle bounty of worlds destroy as upgrade
if (event.type == 'ITEM_DESTROYED') {
if (event.itemId == 3867) {
const suppItem: number = itemInfo.to.find(
(x: number) =>
x == participant.item0 ||
x == participant.item1 ||
x == participant.item2 ||
x == participant.item3 ||
x == participant.item4 ||
x == participant.item5 ||
x == participant.item6
)
if (suppItem != undefined) {
const already = builds.suppItems.find(x => x.data == suppItem)
if (already == undefined) builds.suppItems.push({ count: 1, data: suppItem })
else already.count += 1
}
}
}
if (event.type != 'ITEM_PURCHASED') continue
// Handle boots upgrades
if (
itemInfo.requiredBuffCurrencyName == 'Feats_NoxianBootPurchaseBuff' ||
itemInfo.requiredBuffCurrencyName == 'Feats_SpecialQuestBootBuff'
) {
continue
}
// Handle boots differently
if (itemInfo.categories.includes('Boots')) {
if (itemInfo.to.length == 0 || (itemInfo.to[0] >= 3171 && itemInfo.to[0] <= 3176)) {
// 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 zephyr
if (event.itemId == 3172) 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(3)) {
const already = builds.lateGame.find((x) => x.data == item)
if(already == undefined) builds.lateGame.push({count:1, data:item})
else already.count += 1
}
}
// 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 (const item of items.slice(3)) {
const already = builds.lateGame.find(x => x.data == item)
if (already == undefined) builds.lateGame.push({ count: 1, data: item })
else already.count += 1
}
}
function handleMatch(match: any, champions : Map<number, ChampionData>) {
let participantIndex = 0;
for(let participant of match.info.participants) {
participantIndex += 1
const championId = participant.championId
const champion = champions.get(championId)
function handleMatch(match: any, champions: Map<number, ChampionData>) {
let participantIndex = 0
for (const participant of match.info.participants) {
participantIndex += 1
const championId = participant.championId
const champion = champions.get(championId)
// Lanes
let lane = champion.lanes.find((x) => x.data == participant.teamPosition)
if(lane == undefined) {
const builds : Builds = {tree:treeInit(), start: [], bootsFirst: 0, boots: [], lateGame: [], suppItems: []}
lane = {count:1, data: participant.teamPosition, runes:[], builds:builds, winningMatches: 0, losingMatches: 0, winrate: 0, pickrate: 0}
champion.lanes.push(lane)
}
else lane.count += 1
// Lanes
let lane = champion.lanes.find(x => x.data == participant.teamPosition)
if (lane == undefined) {
const builds: Builds = {
tree: treeInit(),
start: [],
bootsFirst: 0,
boots: [],
lateGame: [],
suppItems: []
}
lane = {
count: 1,
data: participant.teamPosition,
runes: [],
builds: builds,
winningMatches: 0,
losingMatches: 0,
winrate: 0,
pickrate: 0
}
champion.lanes.push(lane)
} else lane.count += 1
// Winrate
if(participant.win) {
champion.winningMatches++;
lane.winningMatches++;
}
else {
champion.losingMatches++;
lane.losingMatches++;
}
// Runes
handleParticipantRunes(participant, lane.runes)
// Items
handleMatchItems(match.timeline, participant, participantIndex, lane.builds)
// Winrate
if (participant.win) {
champion.winningMatches++
lane.winningMatches++
} else {
champion.losingMatches++
lane.losingMatches++
}
// Runes
handleParticipantRunes(participant, lane.runes)
// Items
handleMatchItems(match.timeline, participant, participantIndex, lane.builds)
}
}
async function handleMatchList(client: MongoClient, patch: string, champions: Map<number, ChampionData>) {
const database = client.db("matches");
const matches = database.collection(patch)
const allMatches = matches.find()
const totalMatches: number = await matches.countDocuments()
async function handleMatchList(
client: MongoClient,
patch: string,
champions: Map<number, ChampionData>
) {
const database = client.db('matches')
const matches = database.collection(patch)
const allMatches = matches.find()
const totalMatches: number = await matches.countDocuments()
let currentMatch = 0;
for await (let match of allMatches) {
process.stdout.write("\rComputing champion stats, game entry " + currentMatch + "/" + totalMatches + " ... ")
currentMatch += 1;
handleMatch(match, champions)
}
return totalMatches
let currentMatch = 0
for await (const match of allMatches) {
process.stdout.write(
'\rComputing champion stats, game entry ' + currentMatch + '/' + totalMatches + ' ... '
)
currentMatch += 1
handleMatch(match, champions)
}
return totalMatches
}
async function finalizeChampionStats(champion: ChampionData, totalMatches: number) {
let totalChampionMatches = champion.winningMatches + champion.losingMatches;
const totalChampionMatches = champion.winningMatches + champion.losingMatches
arrayRemovePercentage(champion.lanes, totalChampionMatches, 0.2)
champion.lanes.sort((a, b) => b.count - a.count)
arrayRemovePercentage(champion.lanes, totalChampionMatches, 0.2)
champion.lanes.sort((a, b) => b.count - a.count)
// Filter runes to keep 3 most played
for(let lane of champion.lanes) {
const runes = lane.runes
runes.sort((a, b) => b.count - a.count)
if(runes.length > 3)
runes.splice(3, runes.length - 3)
// Compute runes pickrate
for(let rune of runes)
rune.pickrate = rune.count / lane.count;
}
// Filter runes to keep 3 most played
for (const lane of champion.lanes) {
const runes = lane.runes
for(let lane of champion.lanes) {
const builds = lane.builds
runes.sort((a, b) => b.count - a.count)
if (runes.length > 3) runes.splice(3, runes.length - 3)
// Compute runes pickrate
for (const rune of runes) rune.pickrate = rune.count / lane.count
}
// Cut item tree branches to keep only 4 branches every time and with percentage threshold
builds.tree.count = lane.count;
treeCutBranches(builds.tree, 4, 0.05)
treeSort(builds.tree)
for (const lane of champion.lanes) {
const builds = lane.builds
// Cut item start, to only 4 and with percentage threshold
arrayRemovePercentage(builds.start, lane.count, 0.05)
builds.start.sort((a, b) => b.count - a.count)
if(builds.start.length > 4)
builds.start.splice(4, builds.start.length - 4)
// Remove boots that are not within percentage threshold
arrayRemovePercentage(builds.boots, lane.count, 0.05)
builds.boots.sort((a, b) => b.count - a.count)
builds.bootsFirst /= lane.count
// Cut supp items below 2 and percentage threshold
arrayRemovePercentage(builds.suppItems, lane.count, 0.05)
builds.suppItems.sort((a, b) => b.count - a.count)
if(builds.suppItems.length > 2)
builds.suppItems.splice(2, builds.suppItems.length - 2)
// Cut item tree branches to keep only 4 branches every time and with percentage threshold
builds.tree.count = lane.count
treeCutBranches(builds.tree, 4, 0.05)
treeSort(builds.tree)
// Delete supp items if empty
if(builds.suppItems.length == 0) delete builds.suppItems
// Cut item start, to only 4 and with percentage threshold
arrayRemovePercentage(builds.start, lane.count, 0.05)
builds.start.sort((a, b) => b.count - a.count)
if (builds.start.length > 4) builds.start.splice(4, builds.start.length - 4)
builds.lateGame.sort((a, b) => b.count - a.count)
}
// Remove boots that are not within percentage threshold
arrayRemovePercentage(builds.boots, lane.count, 0.05)
builds.boots.sort((a, b) => b.count - a.count)
for(let lane of champion.lanes) {
lane.winrate = lane.winningMatches / lane.count
lane.pickrate = lane.count / totalMatches
}
builds.bootsFirst /= lane.count
return {name: champion.champion.name,
alias: champion.champion.alias.toLowerCase(),
id: champion.champion.id,
lanes: champion.lanes,
winrate: champion.winningMatches / totalChampionMatches,
gameCount: totalChampionMatches,
pickrate: totalChampionMatches/totalMatches,
};
// Cut supp items below 2 and percentage threshold
arrayRemovePercentage(builds.suppItems, lane.count, 0.05)
builds.suppItems.sort((a, b) => b.count - a.count)
if (builds.suppItems.length > 2) builds.suppItems.splice(2, builds.suppItems.length - 2)
// Delete supp items if empty
if (builds.suppItems.length == 0) delete builds.suppItems
builds.lateGame.sort((a, b) => b.count - a.count)
}
for (const lane of champion.lanes) {
lane.winrate = lane.winningMatches / lane.count
lane.pickrate = lane.count / totalMatches
}
return {
name: champion.champion.name,
alias: champion.champion.alias.toLowerCase(),
id: champion.champion.id,
lanes: champion.lanes,
winrate: champion.winningMatches / totalChampionMatches,
gameCount: totalChampionMatches,
pickrate: totalChampionMatches / totalMatches
}
}
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)
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: MongoClient, patch : string) {
var globalItems = await itemList()
for(let item of globalItems) {
itemDict.set(item.id, item)
}
async function makeChampionsStats(client: MongoClient, patch: string) {
const globalItems = await itemList()
for (const item of globalItems) {
itemDict.set(item.id, item)
}
const list = await championList()
console.log("Generating stats for " + list.length + " champions")
const list = await championList()
console.log('Generating stats for ' + list.length + ' champions')
// Pre-generate list of champions
const champions: Map<number, ChampionData> = new Map()
for(let champion of list) {
champions.set(champion.id, {
champion: {id: champion.id, name: champion.name, alias: champion.alias},
winningMatches: 0,
losingMatches: 0,
lanes: []
})
}
// Pre-generate list of champions
const champions: Map<number, ChampionData> = new Map()
for (const champion of list) {
champions.set(champion.id, {
champion: { id: champion.id, name: champion.name, alias: champion.alias },
winningMatches: 0,
losingMatches: 0,
lanes: []
})
}
// Loop through all matches to generate stats
const totalMatches = await handleMatchList(client, patch, champions)
// Loop through all matches to generate stats
const totalMatches = await handleMatchList(client, patch, champions)
// Finalize and save stats for every champion
const database = client.db("champions")
const collection = database.collection(patch)
for(let champion of list) {
const championInfo = await finalizeChampionStats(champions.get(champion.id), totalMatches)
await collection.updateOne({id: champion.id}, {$set: championInfo}, { upsert: true })
}
// Finalize and save stats for every champion
const database = client.db('champions')
const collection = database.collection(patch)
for (const champion of list) {
const championInfo = await finalizeChampionStats(champions.get(champion.id), totalMatches)
await collection.updateOne({ id: champion.id }, { $set: championInfo }, { upsert: true })
}
// Create alias-index for better key-find
await collection.createIndex({alias:1})
// Create alias-index for better key-find
await collection.createIndex({ alias: 1 })
}
export default {makeChampionsStats}
export default { makeChampionsStats }