- backend: add summoner spells - backend: add build variants - backend: builds are now storing full tree with runes (keystones) - backend: build trees are split on starter items and merged on runes - frontend: computing core tree now - frontend: variant selectors
686 lines
21 KiB
TypeScript
686 lines
21 KiB
TypeScript
function sameArrays(array1: Array<number>, array2: Array<number>) {
|
|
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,
|
|
treeMergeTree,
|
|
areTreeSimilars
|
|
} 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
|
|
) {
|
|
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
|
|
}
|
|
type Build = {
|
|
runeKeystone: number
|
|
runes: Array<Rune>
|
|
items: ItemTree
|
|
bootsFirstCount: number
|
|
bootsFirst?: number
|
|
count: number
|
|
suppItems: Array<{ data: number; count: number }>
|
|
boots: Array<{ data: number; count: number }>
|
|
pickrate?: number
|
|
}
|
|
|
|
type BuildWithStartItems = {
|
|
runeKeystone: number
|
|
runes: Array<Rune>
|
|
items: ItemTree
|
|
bootsFirst?: number
|
|
bootsFirstCount: number
|
|
count: number
|
|
startItems: Array<{ data: number; count: number }>
|
|
suppItems: Array<{ data: number; count: number }>
|
|
boots: Array<{ data: number; count: number }>
|
|
pickrate?: number
|
|
}
|
|
|
|
type Builds = Build[]
|
|
type Champion = {
|
|
id: number
|
|
name: string
|
|
alias: string
|
|
}
|
|
type MatchupData = {
|
|
championId: number
|
|
winrate: number
|
|
games: number
|
|
championName: string
|
|
championAlias: string
|
|
}
|
|
|
|
type LaneData = {
|
|
data: string
|
|
count: number
|
|
winningMatches: number
|
|
losingMatches: number
|
|
winrate: number
|
|
pickrate: number
|
|
builds: Builds
|
|
matchups?: Array<MatchupData>
|
|
summonerSpells: Array<{ id: number; count: number; pickrate?: number }>
|
|
}
|
|
type ChampionData = {
|
|
champion: Champion
|
|
winningMatches: number
|
|
losingMatches: number
|
|
lanes: Array<LaneData>
|
|
}
|
|
|
|
// Helper function to create rune configuration from participant
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function createRuneConfiguration(participant: any): Rune {
|
|
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)
|
|
}
|
|
}
|
|
return {
|
|
count: 0, // Will be incremented when added to build
|
|
primaryStyle: primaryStyle,
|
|
secondaryStyle: secondaryStyle,
|
|
selections: selections
|
|
}
|
|
}
|
|
|
|
// Find or create a build for the given rune keystone
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function findOrCreateBuild(builds: Builds, participant: any): Build {
|
|
const keystone = participant.perks.styles[0].selections[0].perk
|
|
const runeConfig = createRuneConfiguration(participant)
|
|
|
|
// Try to find existing build with matching keystone
|
|
const existingBuild = builds.find(
|
|
build =>
|
|
build.runes[0].primaryStyle === runeConfig.primaryStyle && build.runeKeystone === keystone
|
|
)
|
|
|
|
if (existingBuild) {
|
|
// Check if this rune configuration already exists in the build
|
|
const existingRune = existingBuild.runes.find(rune =>
|
|
sameArrays(rune.selections, runeConfig.selections)
|
|
)
|
|
|
|
if (existingRune) {
|
|
existingRune.count++
|
|
} else {
|
|
existingBuild.runes.push({ ...runeConfig, count: 1 })
|
|
}
|
|
|
|
return existingBuild
|
|
}
|
|
|
|
// Create new build for this keystone
|
|
const newBuild: Build = {
|
|
runeKeystone: keystone,
|
|
runes: [{ ...runeConfig, count: 1 }],
|
|
items: treeInit(),
|
|
bootsFirstCount: 0,
|
|
count: 0,
|
|
suppItems: [],
|
|
boots: []
|
|
}
|
|
builds.push(newBuild)
|
|
return newBuild
|
|
}
|
|
|
|
function handleMatchBuilds(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
timeline: any,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
participant: any,
|
|
participantIndex: number,
|
|
builds: Builds
|
|
) {
|
|
// Find or create the build for this participant's rune configuration
|
|
const build = findOrCreateBuild(builds, participant)
|
|
build.count += 1
|
|
|
|
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 = build.suppItems.find(x => x.data == suppItem)
|
|
if (already == undefined) build.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) {
|
|
build.bootsFirstCount += 1
|
|
}
|
|
|
|
// Add to boots array
|
|
const already = build.boots.find(x => x.data == event.itemId)
|
|
if (already == undefined) build.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 or support role
|
|
if (itemInfo.to.length != 0 && (items.length >= 1 || participant.teamPosition === 'UTILITY'))
|
|
continue
|
|
|
|
items.push(event.itemId)
|
|
}
|
|
}
|
|
|
|
// Merge the full item path into the build's item tree
|
|
// This tree includes start item as the root, then branching paths
|
|
if (items.length > 0) {
|
|
treeMerge(build.items, items)
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
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) {
|
|
lane = {
|
|
count: 1,
|
|
data: participant.teamPosition,
|
|
builds: [],
|
|
winningMatches: 0,
|
|
losingMatches: 0,
|
|
winrate: 0,
|
|
pickrate: 0,
|
|
summonerSpells: [],
|
|
matchups: []
|
|
}
|
|
champion.lanes.push(lane)
|
|
} else lane.count += 1
|
|
|
|
// Initialize matchups if not present
|
|
if (!lane.matchups) {
|
|
lane.matchups = []
|
|
}
|
|
|
|
// Winrate
|
|
if (participant.win) {
|
|
champion.winningMatches++
|
|
lane.winningMatches++
|
|
} else {
|
|
champion.losingMatches++
|
|
lane.losingMatches++
|
|
}
|
|
|
|
// Summoner spells
|
|
let spell1 = lane.summonerSpells.find(x => x.id == participant.summoner1Id)
|
|
if (spell1 == undefined) {
|
|
spell1 = { id: participant.summoner1Id, count: 1, pickrate: undefined }
|
|
lane.summonerSpells.push(spell1)
|
|
} else spell1.count += 1
|
|
let spell2 = lane.summonerSpells.find(x => x.id == participant.summoner2Id)
|
|
if (spell2 == undefined) {
|
|
spell2 = { id: participant.summoner2Id, count: 1, pickrate: undefined }
|
|
lane.summonerSpells.push(spell2)
|
|
} else spell2.count += 1
|
|
|
|
// Track counter matchups - find opponent in same lane
|
|
const opponentTeam = participant.teamId === 100 ? 200 : 100
|
|
const opponent = match.info.participants.find(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(p: any) => p.teamId === opponentTeam && p.teamPosition === participant.teamPosition
|
|
)
|
|
|
|
if (opponent) {
|
|
const opponentChampionId = opponent.championId
|
|
|
|
// Track this matchup for current champion
|
|
const matchup = lane.matchups.find(c => c.championId === opponentChampionId)
|
|
if (matchup) {
|
|
matchup.games += 1
|
|
if (participant.win) {
|
|
matchup.winrate = (matchup.winrate * (matchup.games - 1) + 1) / matchup.games
|
|
} else {
|
|
matchup.winrate = (matchup.winrate * (matchup.games - 1)) / matchup.games
|
|
}
|
|
} else {
|
|
const opponentChampion = champions.get(opponentChampionId)
|
|
|
|
lane.matchups.push({
|
|
championId: opponentChampionId,
|
|
winrate: participant.win ? 1 : 0,
|
|
games: 1,
|
|
championName: opponentChampion.champion.name,
|
|
championAlias: opponentChampion.champion.alias
|
|
})
|
|
}
|
|
}
|
|
|
|
// Items and runes (builds)
|
|
handleMatchBuilds(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()
|
|
|
|
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
|
|
}
|
|
|
|
// Split or merge a build/buildtree on starter items
|
|
// If starter items have a rest-of-tree that is too different, we split
|
|
// into two variants.
|
|
// Otherwise, we merge into a ProcessedBuild that has a tree without starters
|
|
function splitMergeOnStarterItem(build: Build, championName: string): BuildWithStartItems[] {
|
|
if (build.items.children.length > 2) {
|
|
console.log(
|
|
`Warning: We have more than 2 starter items for champion ${championName}. Current algorithm won't work.`
|
|
)
|
|
}
|
|
|
|
if (
|
|
build.items.children.length <= 1 ||
|
|
areTreeSimilars(build.items.children[0], build.items.children[1]) >= 0.5
|
|
) {
|
|
const startItems = []
|
|
let items = build.items.children[0]
|
|
startItems.push({ data: build.items.children[0].data, count: build.items.children[0].count })
|
|
build.items.children[0].data = undefined
|
|
if (build.items.children.length > 1) {
|
|
startItems.push({ data: build.items.children[1].data, count: build.items.children[1].count })
|
|
build.items.children[1].data = undefined
|
|
items = treeMergeTree(build.items.children[0], build.items.children[1])
|
|
}
|
|
return [
|
|
{
|
|
runeKeystone: build.runeKeystone,
|
|
runes: build.runes,
|
|
items,
|
|
bootsFirstCount: build.bootsFirstCount,
|
|
count: build.count,
|
|
startItems,
|
|
suppItems: build.suppItems,
|
|
boots: build.boots,
|
|
pickrate: build.pickrate
|
|
}
|
|
]
|
|
} else {
|
|
// Trees are different. We separate into two build variants
|
|
console.log(`Warning: for champion ${championName}, start item splits build variant.`)
|
|
const builds = []
|
|
for (const c of build.items.children) {
|
|
builds.push({
|
|
runeKeystone: build.runeKeystone,
|
|
runes: build.runes,
|
|
items: c,
|
|
bootsFirstCount: build.bootsFirstCount,
|
|
count: c.count,
|
|
startItems: [{ data: c.data, count: c.count }],
|
|
suppItems: build.suppItems,
|
|
boots: build.boots
|
|
})
|
|
c.data = undefined
|
|
}
|
|
return builds
|
|
}
|
|
}
|
|
|
|
// Helper function to merge item counts with same data
|
|
function mergeItemCounts(
|
|
builds: BuildWithStartItems[],
|
|
itemsGetter: (build: BuildWithStartItems) => Array<{ data: number; count: number }>
|
|
): Array<{ data: number; count: number }> {
|
|
const countsMap = new Map<number, number>()
|
|
|
|
for (const build of builds) {
|
|
const items = itemsGetter(build)
|
|
if (!items) continue
|
|
|
|
for (const item of items) {
|
|
const existing = countsMap.get(item.data)
|
|
if (existing !== undefined) {
|
|
countsMap.set(item.data, existing + item.count)
|
|
} else {
|
|
countsMap.set(item.data, item.count)
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(countsMap.entries()).map(([data, count]) => ({ data, count }))
|
|
}
|
|
|
|
// Merge different builds that have the same items (item trees similar) but different
|
|
// runes (primary style and keystones)
|
|
function mergeBuildsForRunes(builds: BuildWithStartItems[]): BuildWithStartItems[] {
|
|
const merged: BuildWithStartItems[] = []
|
|
const processed = new Set<number>()
|
|
const sortedBuilds = [...builds].sort((a, b) => b.count - a.count)
|
|
|
|
for (let i = 0; i < sortedBuilds.length; i++) {
|
|
if (processed.has(i)) continue
|
|
|
|
const currentBuild = sortedBuilds[i]
|
|
processed.add(i)
|
|
|
|
// Find all builds with similar item trees
|
|
const similarBuildsIndices: number[] = []
|
|
for (let j = i + 1; j < sortedBuilds.length; j++) {
|
|
if (processed.has(j)) continue
|
|
|
|
const otherBuild = sortedBuilds[j]
|
|
if (areTreeSimilars(currentBuild.items, otherBuild.items) >= 0.5) {
|
|
similarBuildsIndices.push(j)
|
|
processed.add(j)
|
|
}
|
|
}
|
|
|
|
// If no similar builds found, just add the current build as-is
|
|
if (similarBuildsIndices.length === 0) {
|
|
merged.push(currentBuild)
|
|
continue
|
|
}
|
|
|
|
// Merge all similar builds
|
|
const allSimilarBuilds = [currentBuild, ...similarBuildsIndices.map(idx => sortedBuilds[idx])]
|
|
const totalCount = allSimilarBuilds.reduce((sum, b) => sum + b.count, 0)
|
|
|
|
// Merge runes - combine all unique rune configurations
|
|
const runesMap = new Map<string, Rune>()
|
|
for (const build of allSimilarBuilds) {
|
|
for (const rune of build.runes) {
|
|
const key = `${rune.primaryStyle}-${rune.selections.join('-')}`
|
|
const existing = runesMap.get(key)
|
|
if (existing) {
|
|
existing.count += rune.count
|
|
} else {
|
|
runesMap.set(key, { ...rune })
|
|
}
|
|
}
|
|
}
|
|
|
|
const runes = Array.from(runesMap.values())
|
|
runes.sort((a, b) => b.count - a.count)
|
|
|
|
merged.push({
|
|
runeKeystone: runes[0].selections[0],
|
|
runes: runes,
|
|
items: currentBuild.items,
|
|
bootsFirstCount: allSimilarBuilds.reduce((sum, b) => sum + b.bootsFirstCount, 0),
|
|
count: totalCount,
|
|
startItems: mergeItemCounts(allSimilarBuilds, b => b.startItems),
|
|
suppItems: mergeItemCounts(allSimilarBuilds, b => b.suppItems),
|
|
boots: mergeItemCounts(allSimilarBuilds, b => b.boots)
|
|
})
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
function cleanupLaneBuilds(lane: LaneData) {
|
|
// Filter builds to remove variants that are not played enough
|
|
lane.builds = lane.builds.filter(build => build.count / lane.count >= 0.05)
|
|
|
|
const builds = lane.builds
|
|
|
|
// Sort builds by count
|
|
builds.sort((a, b) => b.count - a.count)
|
|
|
|
// For each build: prune item tree, clean up boots, calculate percentages
|
|
for (const build of builds) {
|
|
// Cut item tree branches to keep only 4 branches every time and with percentage threshold
|
|
build.items.count = build.count
|
|
treeCutBranches(build.items, 4, 0.05)
|
|
treeSort(build.items)
|
|
|
|
// Remove boots that are not within percentage threshold
|
|
arrayRemovePercentage(build.boots, build.count, 0.05)
|
|
build.boots.sort((a, b) => b.count - a.count)
|
|
|
|
// Remove support items that are not within percentage threshold
|
|
arrayRemovePercentage(build.suppItems, build.count, 0.05)
|
|
build.suppItems.sort((a, b) => b.count - a.count)
|
|
|
|
// Calculate bootsFirst percentage
|
|
build.bootsFirst = build.bootsFirstCount / build.count
|
|
|
|
// Compute runes pickrate, and filter out to keep only top 3
|
|
build.runes.forEach(rune => (rune.pickrate = rune.count / build.count))
|
|
build.runes.sort((a, b) => b.count - a.count)
|
|
if (build.runes.length > 3) build.runes.splice(3, build.runes.length - 3)
|
|
|
|
build.pickrate = build.count / lane.count
|
|
}
|
|
}
|
|
|
|
async function finalizeChampionStats(champion: ChampionData, totalMatches: number) {
|
|
const totalChampionMatches = champion.winningMatches + champion.losingMatches
|
|
|
|
arrayRemovePercentage(champion.lanes, totalChampionMatches, 0.2)
|
|
champion.lanes.sort((a, b) => b.count - a.count)
|
|
|
|
for (const lane of champion.lanes) {
|
|
// Summoner spells
|
|
lane.summonerSpells.forEach(x => (x.pickrate = x.count / lane.count))
|
|
lane.summonerSpells.sort((a, b) => b.count - a.count)
|
|
lane.summonerSpells = lane.summonerSpells.filter(x => x.pickrate >= 0.05)
|
|
|
|
// Cleaning up builds
|
|
cleanupLaneBuilds(lane)
|
|
|
|
// Now, second stage: clustering and de-clustering
|
|
// First, we split the builds on starter items, to obtain a BuildWithStartItems.
|
|
if (lane.data != 'UTILITY') {
|
|
const newBuilds: BuildWithStartItems[] = []
|
|
for (const build of lane.builds) {
|
|
newBuilds.push(...splitMergeOnStarterItem(build, champion.champion.name))
|
|
}
|
|
lane.builds = newBuilds
|
|
cleanupLaneBuilds(lane)
|
|
}
|
|
|
|
// Finally, we merge the builds that are similar but have different keystones.
|
|
// Now that we split everything that needed to be split, we are sure that we don't need
|
|
// to have the data per-keystone. We can just merge them back, as it was the same build
|
|
// all along.
|
|
lane.builds = mergeBuildsForRunes(lane.builds as BuildWithStartItems[])
|
|
cleanupLaneBuilds(lane)
|
|
}
|
|
|
|
for (const lane of champion.lanes) {
|
|
lane.winrate = lane.winningMatches / lane.count
|
|
lane.pickrate = lane.count / totalMatches
|
|
}
|
|
|
|
// Sort matchups by score (games * winrate) in descending order
|
|
for (const lane of champion.lanes) {
|
|
if (lane.matchups && lane.matchups.length > 0) {
|
|
// Filter out matchups with insufficient games (minimum 5 games)
|
|
const filteredMatchups = lane.matchups.filter(m => m.games >= 5)
|
|
|
|
// Sort by score (games * (winrate - 0.5)^2) descending
|
|
filteredMatchups.sort((a, b) => {
|
|
// Handle special case of exactly 50% winrate
|
|
if (a.winrate === 0.5 && b.winrate === 0.5) {
|
|
// Both have 50% winrate, sort by games (more games first)
|
|
return b.games - a.games
|
|
}
|
|
if (a.winrate === 0.5 || b.winrate === 0.5) {
|
|
// a has 50% winrate, b doesn't - b comes first
|
|
return b.winrate - a.winrate
|
|
}
|
|
|
|
if (a.winrate > 0.5 && b.winrate < 0.5) return -1
|
|
if (a.winrate < 0.5 && b.winrate > 0.5) return 1
|
|
if (a.winrate > 0.5) {
|
|
return b.games * (b.winrate - 0.5) ** 2 - a.games * (a.winrate - 0.5) ** 2
|
|
} else {
|
|
return -1 * b.games * (0.5 - b.winrate) ** 2 - -1 * a.games * (0.5 - a.winrate) ** 2
|
|
}
|
|
})
|
|
|
|
// Limit to top matchups (or keep all if we want comprehensive data)
|
|
lane.matchups = filteredMatchups
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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')
|
|
|
|
// 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)
|
|
|
|
// 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 })
|
|
}
|
|
|
|
export default { makeChampionsStats }
|