Multiple changes

- 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
This commit is contained in:
2026-03-06 23:33:02 +01:00
parent 930cbf5a18
commit 271c2b26d8
14 changed files with 684 additions and 373 deletions

View File

@@ -41,8 +41,8 @@ const championDescription = computed(() => championData.value?.title || '')
<div style="display: flex; width: fit-content">
<div class="champion-title-img-container">
<NuxtImg
width="160"
height="160"
width="100"
height="100"
class="champion-title-img"
:src="
CDRAGON_BASE +
@@ -54,13 +54,15 @@ const championDescription = computed(() => championData.value?.title || '')
</div>
<div id="ct-info-container">
<h1>{{ championName }}</h1>
<h3 id="ct-desc">{{ championDescription }}</h3>
<h1 style="font-size: 1.5rem">{{ championName }}</h1>
<h3 id="ct-desc" style="font-size: 1rem">{{ championDescription }}</h3>
<div id="ct-basic-stat-container">
<h2 class="ct-basic-stat">{{ winrate }}% win.</h2>
<h2 class="ct-basic-stat ct-basic-stat-margin">{{ pickrate }}% pick.</h2>
<h2 class="ct-basic-stat">{{ gameCount }} games</h2>
<h2 style="font-size: 1.2rem" class="ct-basic-stat">{{ winrate }}% win.</h2>
<h2 style="font-size: 1.2rem" class="ct-basic-stat ct-basic-stat-margin">
{{ pickrate }}% pick.
</h2>
<h2 style="font-size: 1.2rem" class="ct-basic-stat">{{ gameCount }} games</h2>
</div>
</div>
</div>
@@ -68,15 +70,15 @@ const championDescription = computed(() => championData.value?.title || '')
<style>
.champion-title-img-container {
width: 160px;
height: 160px;
width: 100px;
height: 100px;
overflow: hidden;
border: 1px solid var(--color-on-surface);
}
.champion-title-img {
width: 160px;
height: 160px;
width: 100px;
height: 100px;
transform: translate(4px, 4px) scale(1.2, 1.2);
user-select: none;
@@ -93,7 +95,7 @@ const championDescription = computed(() => championData.value?.title || '')
margin-top: 5px;
}
#ct-basic-stat-container {
margin-top: 30px;
margin-top: 16px;
display: flex;
}

View File

@@ -1,23 +1,17 @@
<script setup lang="ts">
import { CDRAGON_BASE, mapPath } from '~/utils/cdragon'
interface SummonerSpellData {
id: number
count: number
pickrate: number
}
const { summonerSpellMap } = useSummonerSpellMap()
const props = defineProps<{
spells: Array<SummonerSpellData>
summonerSpellMap: Map<number, SummonerSpell>
defineProps<{
summonerSpells: Array<{ id: number; count: number; pickrate: number }>
}>()
</script>
<template>
<div class="summoner-spells-section">
<h3 class="section-title">Summoner Spells</h3>
<div class="summoner-spells-row">
<div v-for="(spell, i) in props.spells.slice(0, 2)" :key="i" class="summoner-spell-item">
<div v-for="(spell, i) in summonerSpells" :key="i" class="summoner-spell-item">
<NuxtImg
v-if="summonerSpellMap.get(spell.id)"
class="summoner-spell-img"
@@ -33,27 +27,21 @@ const props = defineProps<{
<style scoped>
.summoner-spells-section {
display: flex;
flex-direction: column;
}
.section-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 10px;
color: #4a9eff;
text-transform: uppercase;
letter-spacing: 0.5px;
flex-direction: row;
}
.summoner-spells-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.summoner-spell-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
gap: 2px;
}
.summoner-spell-img {
@@ -72,21 +60,17 @@ const props = defineProps<{
}
.spell-pickrate {
font-size: 0.7rem;
font-size: 0.65rem;
color: var(--color-on-surface);
opacity: 0.7;
}
/* Responsive: Mobile */
@media only screen and (max-width: 900px) {
.section-title {
font-size: 0.9rem;
}
.summoner-spell-img,
.summoner-spell-placeholder {
width: 36px;
height: 36px;
width: 32px;
height: 32px;
}
}
</style>

View File

@@ -7,12 +7,18 @@ const props = defineProps<{
keystore: Map<number, Perk>
itemMap: Map<number, Item>
pickrate: number
selected: boolean
index: number
}>()
const emit = defineEmits<{
select: [index: number]
}>()
</script>
<template>
<div class="build-variant-selector">
<div :class="['build-variant-card', { selected: true }]">
<div :class="['build-variant-card', { selected }]" @click="emit('select', index)">
<div class="variant-content">
<!-- Keystone -->
<NuxtImg

View File

@@ -9,23 +9,22 @@ interface RuneBuild {
pickrate: number
}
interface PerkStyle {
id: number
iconPath: string
}
const props = defineProps<{
runes: Array<RuneBuild>
primaryStyles: Array<PerkStyle>
secondaryStyles: Array<PerkStyle>
keystoneIds: Array<number>
perks: Map<number, Perk>
selectedIndex: number
perkStyles: Map<number, PerkStyle>
}>()
const selectedIndex = ref(0)
const emit = defineEmits<{
select: [index: number]
}>()
function select(index: number) {
emit('select', index)
selectedIndex.value = index
}
</script>
<template>
@@ -33,24 +32,24 @@ const emit = defineEmits<{
<div
v-for="(rune, index) in props.runes"
:key="index"
:class="['compact-rune-option', { active: index === props.selectedIndex }]"
@click="emit('select', index)"
:class="['compact-rune-option', { active: index === selectedIndex }]"
@click="select(index)"
>
<div class="compact-rune-content">
<NuxtImg
v-if="primaryStyles[index]"
v-if="runes[index].primaryStyle"
class="compact-rune-img"
:src="CDRAGON_BASE + mapPath(primaryStyles[index].iconPath)"
:src="CDRAGON_BASE + mapPath(perkStyles.get(runes[index].primaryStyle)?.iconPath!)"
/>
<NuxtImg
v-if="keystoneIds[index] && props.perks.get(keystoneIds[index])"
v-if="perks.get(runes[index].selections[0])"
class="compact-rune-img"
:src="CDRAGON_BASE + mapPath(props.perks.get(keystoneIds[index])!.iconPath)"
:src="CDRAGON_BASE + mapPath(perks.get(runes[index].selections[0])!.iconPath)"
/>
<NuxtImg
v-if="secondaryStyles[index]"
v-if="runes[index].secondaryStyle"
class="compact-rune-img"
:src="CDRAGON_BASE + mapPath(secondaryStyles[index].iconPath)"
:src="CDRAGON_BASE + mapPath(perkStyles.get(runes[index].secondaryStyle)?.iconPath!)"
/>
</div>
<span class="compact-rune-pickrate">{{ (rune.pickrate * 100).toFixed(1) }}%</span>

View File

@@ -1,21 +1,11 @@
<script setup lang="ts">
import { getHighestPickrateBuildIndex, getFirstCoreItems } from '~/utils/buildHelpers'
import { MOCK_SUMMONER_SPELLS } from '~/utils/mockData'
import { getCoreItems, getLateGameItems } from '~/utils/buildHelpers'
import BuildVariantSelector from '~/components/build/BuildVariantSelector.vue'
import SummonerSpells from '~/components/build/SummonerSpells.vue'
import CompactRuneSelector from '~/components/build/CompactRuneSelector.vue'
import ItemRow from '~/components/build/ItemRow.vue'
const props = defineProps<{
runes: Array<{
count: number
primaryStyle: number
secondaryStyle: number
selections: Array<number>
pickrate: number
}>
builds: Builds
summonerSpells?: Array<{ id: number; count: number; pickrate: number }>
}>()
// State
@@ -23,76 +13,75 @@ const currentlySelectedBuild = ref(0)
// Use composables for data fetching
const { itemMap } = useItemMap()
const { summonerSpellMap } = useSummonerSpellMap()
// Use composable for rune styles
const { perks, primaryStyles, secondaryStyles, keystoneIds } = useRuneStyles(toRef(props, 'runes'))
const { perks, perkStyles } = useRuneStyles()
// Use composable for builds management
const { builds } = useBuilds(toRef(props, 'builds'))
const currentBuild = computed(() => builds.value[currentlySelectedBuild.value])
// Summoner spells data - use提供的 or fall back to mock
const displaySummonerSpells = computed(() =>
props.summonerSpells && props.summonerSpells.length > 0
? props.summonerSpells
: MOCK_SUMMONER_SPELLS
)
// Late game items for current build
const lateGameItems = computed(() => {
if (!currentBuild.value) return []
return getLateGameItems(currentBuild.value).slice(0, 6)
})
// Computed properties using utility functions
const firstCoreItems = computed(() => getFirstCoreItems(props.runes, builds.value))
const currentlySelectedRunes = ref(0)
const highestPickrateBuildIndex = computed(() => getHighestPickrateBuildIndex(props.runes))
// Reset selected build when runes change
// Reset selected build when variant changes
watch(
() => props.runes,
() => currentBuild,
() => {
currentlySelectedBuild.value = 0
currentlySelectedRunes.value = 0
}
)
function selectRune(index: number): void {
currentlySelectedRunes.value = index
}
function selectBuild(index: number): void {
currentlySelectedBuild.value = index
}
</script>
<template>
<div class="build-viewer">
<!-- Global Build Variant Selector - Single variant with highest pickrate -->
<BuildVariantSelector
:keystone-id="keystoneIds[highestPickrateBuildIndex]"
:item-id="firstCoreItems[highestPickrateBuildIndex]"
:keystore="perks"
:item-map="itemMap"
:pickrate="1"
/>
<div v-if="currentBuild" class="build-viewer">
<div style="display: flex">
<BuildVariantSelector
v-for="(build, i) in builds"
:key="i"
:keystone-id="build.runeKeystone"
:item-id="build.items.children[0].data"
:keystore="perks"
:item-map="itemMap"
:pickrate="build.pickrate"
:selected="currentBuild == build"
:index="i"
@select="selectBuild"
/>
</div>
<!-- Main Build Content -->
<div class="build-content">
<!-- Left Column: Summoner Spells + Runes -->
<div class="build-left-column">
<!-- Summoner Spells -->
<SummonerSpells :spells="displaySummonerSpells" :summoner-spell-map="summonerSpellMap" />
<!-- Rune Page -->
<div class="rune-section">
<h3 class="section-title">Runes</h3>
<div class="rune-page-wrapper">
<RunePage
v-if="runes[currentlySelectedBuild]"
:primary-style-id="runes[currentlySelectedBuild].primaryStyle"
:secondary-style-id="runes[currentlySelectedBuild].secondaryStyle"
:selection-ids="runes[currentlySelectedBuild].selections"
v-if="currentBuild.runes"
:primary-style-id="currentBuild.runes[currentlySelectedRunes].primaryStyle"
:secondary-style-id="currentBuild.runes[currentlySelectedRunes].secondaryStyle"
:selection-ids="currentBuild.runes[currentlySelectedRunes].selections"
/>
</div>
<!-- Compact Rune Selector -->
<CompactRuneSelector
:runes="runes"
:primary-styles="primaryStyles"
:secondary-styles="secondaryStyles"
:keystone-ids="keystoneIds"
:runes="currentBuild.runes"
:perks="perks"
:perk-styles="perkStyles"
:selected-index="currentlySelectedBuild"
@select="selectRune"
/>
@@ -105,46 +94,47 @@ function selectRune(index: number): void {
<!-- Start/Support + Boots Container -->
<div class="item-row-group">
<!-- Start Items -->
<!-- Start Item (root of the tree) -->
<ItemRow
v-if="!builds.suppItems"
v-if="currentBuild.startItems && currentBuild.startItems.length > 0"
label="Start"
:items="builds.start"
:items="currentBuild.startItems"
:item-map="itemMap"
:total-count="builds.tree.count"
:total-count="currentBuild.count"
/>
<!-- Support Items -->
<ItemRow
v-if="builds.suppItems"
v-if="currentBuild.suppItems && currentBuild.suppItems.length > 0"
label="Support"
:items="builds.suppItems"
:items="currentBuild.suppItems"
:item-map="itemMap"
:total-count="builds.tree.count"
:total-count="currentBuild.count"
/>
<!-- Boots (regular or rush) -->
<ItemRow
:label="builds.bootsFirst > 0.5 ? 'Boots Rush' : 'Boots'"
:items="builds.boots.slice(0, 2)"
:label="currentBuild.bootsFirst > 0.5 ? 'Boots Rush' : 'Boots'"
:items="currentBuild.boots.slice(0, 2)"
:item-map="itemMap"
:total-count="builds.tree.count"
:total-count="currentBuild.count"
:max-items="2"
/>
</div>
<!-- Core Items Tree -->
<div class="item-row">
<!-- Core Items Tree (children of start item) -->
<div v-if="currentBuild.items?.children?.length" class="item-row">
<span class="item-row-label">Core</span>
<ItemTree :tree="builds.tree" />
<ItemTree :tree="getCoreItems(currentBuild)" />
</div>
<!-- Late Game -->
<ItemRow
v-if="lateGameItems.length > 0"
label="Late Game"
:items="builds.lateGame.slice(0, 6)"
:items="lateGameItems"
:item-map="itemMap"
:total-count="builds.tree.count"
:total-count="currentBuild.count"
:max-items="6"
/>
</div>

View File

@@ -137,7 +137,7 @@ if (route.path.startsWith('/tierlist/')) {
<h3 style="font-size: 18px; font-weight: 200; opacity: 0.5">Loading stats...</h3>
</template>
<NuxtLink to="/about"><h3>About</h3></NuxtLink>
<h2 style="font-size: 10px; font-weight: 200; margin-top: 3px">
<h2 style="font-size: 10px; font-weight: 200; margin-top: 3px; margin-right: 10px">
BuildPath isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot
Games or anyone officially involved in producing or managing Riot Games properties. Riot
Games, and all associated properties are trademarks or registered trademarks of Riot Games,

View File

@@ -1,32 +1,19 @@
/**
* Composable for managing build data with automatic trimming
* Handles deep cloning and tree manipulation
* Composable for managing build data
*/
import { deepClone } from '~/utils/helpers'
import { trimBuilds, trimLateGameItems } from '~/utils/buildHelpers'
export const useBuilds = (buildsProp: Ref<Builds>) => {
const builds = ref<Builds>(deepClone(buildsProp.value))
function trimBuildData(): void {
trimBuilds(builds.value)
trimLateGameItems(builds.value)
}
// Watch for changes and rebuild
watch(
() => buildsProp.value,
newBuilds => {
builds.value = deepClone(newBuilds)
trimBuildData()
},
{ deep: true }
)
// Initial trim on mount
onMounted(() => {
trimBuildData()
})
return { builds }
}

View File

@@ -2,23 +2,10 @@
* Composable for fetching and managing rune styles and keystones
* Transforms rune data into format needed for display components
*/
export const useRuneStyles = (
runes: Ref<
Array<{
count: number
primaryStyle: number
secondaryStyle: number
selections: Array<number>
pickrate: number
}>
>
) => {
const primaryStyles = ref<Array<PerkStyle>>(Array(runes.value.length))
const secondaryStyles = ref<Array<PerkStyle>>(Array(runes.value.length))
const keystoneIds = ref<Array<number>>(Array(runes.value.length))
export const useRuneStyles = () => {
const { data: perksData } = useFetch('/api/cdragon/perks')
const { data: stylesData } = useFetch('/api/cdragon/perkstyles')
console.log(stylesData.value)
const perks = reactive(new Map<number, Perk>())
watch(
@@ -36,62 +23,24 @@ export const useRuneStyles = (
{ immediate: true }
)
function refreshStylesKeystones(): void {
if (!stylesData.value?.styles) return
primaryStyles.value = Array(runes.value.length)
secondaryStyles.value = Array(runes.value.length)
keystoneIds.value = Array(runes.value.length)
for (const style of stylesData.value.styles) {
for (const rune of runes.value) {
const runeIndex = runes.value.indexOf(rune)
if (style.id === rune.primaryStyle) {
primaryStyles.value[runeIndex] = style
// Find keystone from first slot
if (style.slots?.[0]?.perks) {
for (const perk of style.slots[0].perks) {
if (rune.selections.includes(perk)) {
keystoneIds.value[runeIndex] = perk
break
}
}
const perkStyles = reactive(new Map<number, PerkStyle>())
watch(
stylesData,
newPerkStyles => {
if (Array.isArray(newPerkStyles?.styles)) {
perkStyles.clear()
for (const perkStyle of newPerkStyles.styles) {
if (perkStyle?.id) {
perkStyles.set(perkStyle.id, perkStyle)
}
}
if (style.id === rune.secondaryStyle) {
secondaryStyles.value[runeIndex] = style
}
}
}
}
// Refresh when styles data loads or runes change
watch(
[stylesData, runes],
() => {
refreshStylesKeystones()
},
{ immediate: true }
)
// Reset when runes array changes
watch(
() => runes.value.length,
() => {
primaryStyles.value = Array(runes.value.length)
secondaryStyles.value = Array(runes.value.length)
keystoneIds.value = Array(runes.value.length)
refreshStylesKeystones()
}
)
return {
perks,
primaryStyles,
secondaryStyles,
keystoneIds
perkStyles
}
}

View File

@@ -10,6 +10,13 @@ const error = ref<string | null>(null)
const laneState = ref(0)
const state = ref('build')
// Data fetching
const { itemMap } = useItemMap()
const { perks } = useRuneStyles()
// State for selected variant in alternatives tab
const selectedAltVariant = ref(0)
// Use useAsyncData with client-side fetching for faster initial page load
const {
data: championData,
@@ -156,28 +163,57 @@ function fetchChampionData() {
/>
<div id="champion-content">
<ChampionTitle
v-if="championData.gameCount > 0 && lane"
id="champion-title"
:champion-id="championId"
:winrate="lane.winrate || 0"
:pickrate="lane.pickrate || 0"
:game-count="lane.count || 0"
/>
<div class="champion-header">
<ChampionTitle
v-if="championData.gameCount > 0 && lane"
id="champion-title"
:champion-id="championId"
:winrate="lane.winrate || 0"
:pickrate="lane.pickrate || 0"
:game-count="lane.count || 0"
/>
<SummonerSpells v-if="lane" :summoner-spells="lane.summonerSpells" />
</div>
<ClientOnly>
<LazyBuildViewer
v-if="state == 'build' && championData.gameCount > 0 && lane?.runes && lane?.builds"
v-if="state == 'build' && championData.gameCount > 0 && lane?.builds"
style="margin: auto; margin-top: 40px"
:runes="lane.runes"
:builds="lane.builds"
/>
</ClientOnly>
<ClientOnly>
<LazyItemTree
v-if="state == 'alternatives' && championData.gameCount > 0 && lane?.builds?.tree"
style="margin: auto; margin-top: 40px; width: fit-content"
:tree="lane.builds.tree"
/>
<div
v-if="state == 'alternatives' && championData.gameCount > 0 && lane && lane.builds"
style="
margin: auto;
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
"
>
<div style="display: flex">
<LazyBuildVariantSelector
v-for="(build, i) in lane.builds"
:key="i"
:keystone-id="build.runeKeystone"
:item-id="build.items.children[0].data"
:keystore="perks"
:item-map="itemMap"
:pickrate="build.pickrate"
:selected="selectedAltVariant == i"
:index="i"
@select="selectedAltVariant = i"
/>
</div>
<LazyItemTree
v-if="lane.builds[selectedAltVariant]?.items"
style="width: fit-content"
:tree="lane.builds[selectedAltVariant].items"
/>
</div>
</ClientOnly>
<ClientOnly>
<LazyMatchupSection
@@ -292,6 +328,15 @@ function fetchChampionData() {
background-color: #45a049;
}
/* Champion header layout */
.champion-header {
display: flex;
align-items: center;
margin: auto;
width: fit-content;
gap: 40px;
}
@media only screen and (max-width: 650px) {
#champion-content {
margin: auto;
@@ -300,7 +345,18 @@ function fetchChampionData() {
#champion-title {
margin: auto;
}
.champion-header {
flex-direction: column;
gap: 20px;
}
.layout-selector {
flex-wrap: wrap;
justify-content: center;
}
}
@media only screen and (max-width: 1200px) {
#alias-content-wrapper {
flex-direction: column;

View File

@@ -9,17 +9,25 @@ declare global {
}
/**
* Represents champion build information
* Represents a complete build with runes and items
*/
interface Builds {
start: Array<{ count: number; data: number }>
tree: ItemTree
interface Build {
runeKeystone: number
runes: Rune[]
items: ItemTree
bootsFirst: number
count: number
boots: Array<{ count: number; data: number }>
lateGame: Array<{ count: number; data: number }>
suppItems?: Array<{ count: number; data: number }>
suppItems: Array<{ count: number; data: number }>
startItems: Array<{ count: number; data: number }>
pickrate: number
}
/**
* Represents champion build information (array of builds)
*/
type Builds = Array<Build>
/**
* Represents a rune configuration
*/
@@ -52,8 +60,8 @@ declare global {
losingMatches: number
winrate: number
pickrate: number
runes?: Rune[]
builds?: Builds
summonerSpells: Array<{ id: number; count: number; pickrate: number }>
matchups?: MatchupData[]
}

View File

@@ -1,64 +1,89 @@
import { isEmpty } from './helpers'
/**
* Trims the build tree to only show the first path
* Removes alternate build paths to keep the UI clean
* Gets all late game items from the item tree (items beyond first level)
* Returns a flat array of unique items with their counts
*/
export function trimBuilds(builds: Builds): void {
if (!builds?.tree?.children) return
export function getLateGameItems(build: Build): Array<{ data: number; count: number }> {
const lateGameItems: Array<{ data: number; count: number }> = []
const itemCounts = new Map<number, number>()
// Keep only the first child (primary build path)
builds.tree.children.splice(1, builds.tree.children.length - 1)
// Collect late items
function collectLateItems(tree: ItemTree, depth: number = 0): void {
if (depth >= 3 && tree.data !== undefined && tree.count > 0) {
const existing = itemCounts.get(tree.data) || 0
itemCounts.set(tree.data, existing + tree.count)
}
// Also trim grandchildren to first path only
if (builds.tree.children[0]?.children) {
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1)
for (const child of tree.children) {
collectLateItems(child, depth + 1)
}
}
collectLateItems(build.items)
// Convert map to array
for (const [data, count] of itemCounts.entries()) {
lateGameItems.push({ data, count })
}
lateGameItems.sort((a, b) => b.count - a.count)
console.log(lateGameItems)
// Sort by count descending
return lateGameItems.filter(item => !treeToArray(getCoreItems(build)).includes(item.data))
}
function treeToArray(tree: ItemTree): Array<number> {
const arr: Array<number> = []
if (tree.data != null) arr.push(tree.data)
for (const child of tree.children) arr.push(...treeToArray(child))
return arr
}
/**
* Removes late game items that appear in the core build tree
* Prevents duplicate items from being shown
* Creates a deep copy of an ItemTree trimmed to a maximum depth
* @param tree - The item tree to copy and trim
* @param maxDepth - The maximum depth to keep (inclusive)
* @param currentDepth - The current depth during recursion
* @returns A new ItemTree with children trimmed beyond maxDepth
*/
export function trimLateGameItems(builds: Builds): void {
if (!builds?.tree || isEmpty(builds.lateGame)) return
function trimTreeDepth(tree: ItemTree, maxDepth: number, currentDepth: number = 0): ItemTree {
const trimmedTree: ItemTree = {
count: tree.count,
data: tree.data,
children: []
}
const coreItemIds = new Set<number>()
// Collect all item IDs from the tree
function collectItemIds(tree: ItemTree): void {
if (tree.data !== undefined) {
coreItemIds.add(tree.data)
}
// If we haven't reached maxDepth, include children
if (currentDepth < maxDepth) {
for (const child of tree.children || []) {
collectItemIds(child)
trimmedTree.children.push(trimTreeDepth(child, maxDepth, currentDepth + 1))
}
}
collectItemIds(builds.tree)
// Remove late game items that appear in core
builds.lateGame = builds.lateGame.filter(item => !coreItemIds.has(item.data))
return trimmedTree
}
/**
* Gets the index of the build with the highest pickrate
*/
export function getHighestPickrateBuildIndex(runes: Array<{ pickrate: number }>): number {
if (runes.length === 0) return 0
function trimTreeChildrensAtDepth(tree: ItemTree, maxChildren: number, depth: number) {
if (depth == 0) {
if (tree.children.length > maxChildren) {
tree.children.splice(maxChildren, tree.children.length - maxChildren)
}
return
}
return runes.reduce(
(maxIdx, rune, idx, arr) => (rune.pickrate > arr[maxIdx].pickrate ? idx : maxIdx),
0
)
for (const c of tree.children) {
trimTreeChildrensAtDepth(c, maxChildren, depth - 1)
}
}
/**
* Gets the first core item for each build variant
*/
export function getFirstCoreItems(runes: unknown[], builds: Builds): number[] {
return runes.map(() => {
const tree = builds?.tree
return tree?.children?.[0]?.data ?? tree?.data ?? 0
})
export function getCoreItems(build: Build): ItemTree {
const tree = trimTreeDepth(build.items, 3)
trimTreeChildrensAtDepth(tree, 1, 0)
trimTreeChildrensAtDepth(tree, 1, 1)
trimTreeChildrensAtDepth(tree, 3, 2)
return tree
}

View File

@@ -1,11 +0,0 @@
/**
* Mock data for development and fallback scenarios
* Used when API data is not available
*/
export const MOCK_SUMMONER_SPELLS = [
{ id: 4, count: 1000, pickrate: 0.45 }, // Flash
{ id: 7, count: 800, pickrate: 0.35 }, // Heal
{ id: 14, count: 600, pickrate: 0.15 }, // Ignite
{ id: 3, count: 200, pickrate: 0.05 } // Exhaust
]