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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]
|
||||
Reference in New Issue
Block a user