feat: first back recording and display (#12)
All checks were successful
pipeline / lint-and-format (push) Successful in 4m35s
pipeline / build-and-push-images (push) Successful in 1m39s

Record first backs, group them by item sets and show the most popular ones, with gold and %, in the frontend.
This commit is contained in:
2026-04-28 20:10:20 +02:00
parent 7712abe3f0
commit db2ca353c5
6 changed files with 452 additions and 6 deletions

View File

@@ -12,6 +12,13 @@ import {
treeDeriveTags
} from './item_tree'
import { PLATFORM_KEYS } from './platform'
import {
initItemDict as initFirstBackItemDict,
extractFirstBackFromMatch,
groupFirstBacksByItemSet,
FirstBackData,
FirstBackGroup
} from './first_back'
import { Match, Timeline, Participant, Frame } from './api'
@@ -65,6 +72,9 @@ type Build = {
suppItems: Array<{ data: number; count: number }>
boots: Array<{ data: number; count: number }>
pickrate?: number
// First back data (collected during processing, grouped in finalize)
firstBacksRaw?: FirstBackData[]
firstBacks?: FirstBackGroup[]
}
type BuildWithStartItems = {
@@ -78,6 +88,8 @@ type BuildWithStartItems = {
suppItems: Array<{ data: number; count: number }>
boots: Array<{ data: number; count: number }>
pickrate?: number
firstBacksRaw?: FirstBackData[]
firstBacks?: FirstBackGroup[]
}
type Builds = Build[]
@@ -225,7 +237,7 @@ function handleMatchBuilds(
participantIndex: number,
builds: Builds,
platform?: string
) {
): Build {
const timeline: Timeline = match.timeline
// Find or create the build for this participant's rune configuration
@@ -316,6 +328,8 @@ function handleMatchBuilds(
if (items.length > 0) {
treeMerge(build.items, items)
}
return build
}
function handleMatch(match: Match, champions: Map<number, ChampionData>, platform?: string) {
@@ -411,7 +425,16 @@ function handleMatch(match: Match, champions: Map<number, ChampionData>, platfor
}
// Items and runes (builds)
handleMatchBuilds(match, participant, participantIndex, lane.builds, platform)
const build = handleMatchBuilds(match, participant, participantIndex, lane.builds, platform)
// First back data - store at build level
const firstBackData = extractFirstBackFromMatch(match, participantIndex)
if (firstBackData) {
if (!build.firstBacksRaw) {
build.firstBacksRaw = []
}
build.firstBacksRaw.push(firstBackData)
}
}
}
@@ -473,7 +496,8 @@ function splitMergeOnStarterItem(build: Build, championName: string): BuildWithS
startItems,
suppItems: build.suppItems,
boots: build.boots,
pickrate: build.pickrate
pickrate: build.pickrate,
firstBacksRaw: build.firstBacksRaw
}
]
} else {
@@ -489,7 +513,8 @@ function splitMergeOnStarterItem(build: Build, championName: string): BuildWithS
count: c.count,
startItems: [{ data: c.data!, count: c.count }],
suppItems: build.suppItems,
boots: build.boots
boots: build.boots,
firstBacksRaw: build.firstBacksRaw
})
c.data = undefined
}
@@ -573,6 +598,14 @@ function mergeBuildsForRunes(builds: BuildWithStartItems[]): BuildWithStartItems
const runes = Array.from(runesMap.values())
runes.sort((a, b) => b.count - a.count)
// Merge first backs raw data
const firstBacksRaw: FirstBackData[] = []
for (const build of allSimilarBuilds) {
if (build.firstBacksRaw) {
firstBacksRaw.push(...build.firstBacksRaw)
}
}
merged.push({
runeKeystone: runes[0].selections[0],
runes: runes,
@@ -581,7 +614,8 @@ function mergeBuildsForRunes(builds: BuildWithStartItems[]): BuildWithStartItems
count: totalCount,
startItems: mergeItemCounts(allSimilarBuilds, b => b.startItems),
suppItems: mergeItemCounts(allSimilarBuilds, b => b.suppItems),
boots: mergeItemCounts(allSimilarBuilds, b => b.boots)
boots: mergeItemCounts(allSimilarBuilds, b => b.boots),
firstBacksRaw: firstBacksRaw.length > 0 ? firstBacksRaw : undefined
})
}
@@ -659,6 +693,19 @@ async function finalizeChampionStats(champion: ChampionData, totalMatches: numbe
// all along.
lane.builds = mergeBuildsForRunes(lane.builds as BuildWithStartItems[])
cleanupLaneBuilds(lane)
// Process first backs at build level - group by item set
for (const build of lane.builds) {
if (build.firstBacksRaw && build.firstBacksRaw.length > 0) {
build.firstBacks = groupFirstBacksByItemSet(build.firstBacksRaw)
// Keep only top 7 groups
if (build.firstBacks!.length > 7) {
build.firstBacks = build.firstBacks!.slice(0, 7)
}
// Clean up raw data to save space
delete build.firstBacksRaw
}
}
}
for (const lane of champion.lanes) {
@@ -723,6 +770,9 @@ async function makeChampionsStats(client: MongoClient, patch: string, platforms:
itemDict.set(item.id, item)
}
// Initialize first back item dictionary
await initFirstBackItemDict()
const list = await championList()
console.log('Generating stats for ' + list.length + ' champions')