428 lines
10 KiB
Vue
428 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import { isEmpty, deepClone } from '~/utils/helpers'
|
|
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 }> // API data when available
|
|
}>()
|
|
|
|
// State
|
|
const currentlySelectedBuild = ref(0)
|
|
|
|
// Fetch items from cached API
|
|
const { data: items } = useFetch<Array<Item>>('/api/cdragon/items', {
|
|
lazy: true,
|
|
server: false
|
|
})
|
|
const itemMap = ref<Map<number, Item>>(new Map())
|
|
|
|
watch(
|
|
items,
|
|
newItems => {
|
|
if (Array.isArray(newItems)) {
|
|
const map = new Map<number, Item>()
|
|
for (const item of newItems) {
|
|
if (item?.id) {
|
|
map.set(item.id, item)
|
|
}
|
|
}
|
|
itemMap.value = map
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
// Fetch summoner spells from cached API
|
|
const { data: summonerSpellsData } = useFetch<Array<SummonerSpell>>(
|
|
'/api/cdragon/summoner-spells',
|
|
{
|
|
lazy: true,
|
|
server: false
|
|
}
|
|
)
|
|
const summonerSpellMap = ref<Map<number, SummonerSpell>>(new Map())
|
|
|
|
watch(
|
|
summonerSpellsData,
|
|
newData => {
|
|
if (Array.isArray(newData)) {
|
|
const map = new Map<number, SummonerSpell>()
|
|
for (const spell of newData) {
|
|
if (spell?.id) {
|
|
map.set(spell.id, spell)
|
|
}
|
|
}
|
|
summonerSpellMap.value = map
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
// Mock summoner spells data if not provided by API
|
|
const mockSummonerSpells = computed(() => {
|
|
if (props.summonerSpells && props.summonerSpells.length > 0) {
|
|
return props.summonerSpells
|
|
}
|
|
// Default mock data based on common summoner spells
|
|
return [
|
|
{ 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
|
|
]
|
|
})
|
|
|
|
// Builds management
|
|
const builds = ref<Builds>(deepClone(props.builds))
|
|
|
|
watch(
|
|
() => props.builds,
|
|
newBuilds => {
|
|
builds.value = deepClone(newBuilds)
|
|
trimBuilds(builds.value)
|
|
trimLateGameItems(builds.value)
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
onMounted(() => {
|
|
trimBuilds(builds.value)
|
|
trimLateGameItems(builds.value)
|
|
})
|
|
|
|
function trimBuilds(builds: Builds): void {
|
|
if (!builds?.tree?.children) return
|
|
builds.tree.children.splice(1, builds.tree.children.length - 1)
|
|
if (builds.tree.children[0]?.children) {
|
|
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1)
|
|
}
|
|
}
|
|
|
|
function trimLateGameItems(builds: Builds): void {
|
|
if (!builds?.tree || isEmpty(builds.lateGame)) return
|
|
|
|
function trimLateGameItemsFromTree(tree: ItemTree): void {
|
|
const foundIndex = builds.lateGame.findIndex(x => x.data === tree.data)
|
|
if (foundIndex !== -1) {
|
|
builds.lateGame.splice(foundIndex, 1)
|
|
}
|
|
for (const child of tree.children || []) {
|
|
trimLateGameItemsFromTree(child)
|
|
}
|
|
}
|
|
trimLateGameItemsFromTree(builds.tree)
|
|
}
|
|
|
|
// Get first core item for build variant display
|
|
const firstCoreItems = computed(() => {
|
|
const result: number[] = []
|
|
for (let i = 0; i < props.runes.length; i++) {
|
|
const tree = builds.value?.tree
|
|
if (tree?.children?.[0]?.data) {
|
|
result.push(tree.children[0].data)
|
|
} else if (tree?.data) {
|
|
result.push(tree.data)
|
|
} else {
|
|
result.push(0)
|
|
}
|
|
}
|
|
return result
|
|
})
|
|
|
|
// Get the highest pickrate rune build
|
|
const highestPickrateBuildIndex = computed(() => {
|
|
if (props.runes.length === 0) return 0
|
|
let maxIndex = 0
|
|
let maxPickrate = props.runes[0].pickrate
|
|
for (let i = 1; i < props.runes.length; i++) {
|
|
if (props.runes[i].pickrate > maxPickrate) {
|
|
maxPickrate = props.runes[i].pickrate
|
|
maxIndex = i
|
|
}
|
|
}
|
|
return maxIndex
|
|
})
|
|
|
|
// State for compact rune selector
|
|
const selectedRuneIndex = ref(0)
|
|
|
|
watch(
|
|
() => currentlySelectedBuild,
|
|
() => {
|
|
selectedRuneIndex.value = 0
|
|
}
|
|
)
|
|
|
|
function selectRune(index: number) {
|
|
selectedRuneIndex.value = index
|
|
currentlySelectedBuild.value = index
|
|
}
|
|
|
|
// Rune styles
|
|
const primaryStyles: Ref<Array<PerkStyle>> = ref(Array(props.runes.length))
|
|
const secondaryStyles: Ref<Array<PerkStyle>> = ref(Array(props.runes.length))
|
|
const keystoneIds: Ref<Array<number>> = ref(Array(props.runes.length))
|
|
|
|
const { data: perks_data }: PerksResponse = await useFetch('/api/cdragon/perks')
|
|
const perks = reactive(new Map())
|
|
for (const perk of perks_data.value) {
|
|
perks.set(perk.id, perk)
|
|
}
|
|
|
|
const { data: stylesData }: PerkStylesResponse = await useFetch('/api/cdragon/perkstyles')
|
|
|
|
function refreshStylesKeystones() {
|
|
for (const style of stylesData.value.styles) {
|
|
for (const rune of props.runes) {
|
|
if (style.id == rune.primaryStyle) {
|
|
primaryStyles.value[props.runes.indexOf(rune)] = style
|
|
for (const perk of style.slots[0].perks) {
|
|
if (rune.selections.includes(perk)) {
|
|
keystoneIds.value[props.runes.indexOf(rune)] = perk
|
|
}
|
|
}
|
|
}
|
|
if (style.id == rune.secondaryStyle) {
|
|
secondaryStyles.value[props.runes.indexOf(rune)] = style
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => props.runes,
|
|
() => {
|
|
currentlySelectedBuild.value = 0
|
|
primaryStyles.value = Array(props.runes.length)
|
|
secondaryStyles.value = Array(props.runes.length)
|
|
keystoneIds.value = Array(props.runes.length)
|
|
refreshStylesKeystones()
|
|
}
|
|
)
|
|
|
|
refreshStylesKeystones()
|
|
</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"
|
|
/>
|
|
|
|
<!-- Main Build Content -->
|
|
<div class="build-content">
|
|
<!-- Left Column: Summoner Spells + Runes -->
|
|
<div class="build-left-column">
|
|
<!-- Summoner Spells -->
|
|
<SummonerSpells :spells="mockSummonerSpells" :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"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Compact Rune Selector -->
|
|
<CompactRuneSelector
|
|
:runes="runes"
|
|
:primary-styles="primaryStyles"
|
|
:secondary-styles="secondaryStyles"
|
|
:keystone-ids="keystoneIds"
|
|
:perks="perks"
|
|
:selected-index="currentlySelectedBuild"
|
|
@select="selectRune"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Items -->
|
|
<div class="build-right-column">
|
|
<h3 class="section-title">Items</h3>
|
|
|
|
<!-- Start/Support + Boots Container -->
|
|
<div class="item-row-group">
|
|
<!-- Start Items -->
|
|
<ItemRow
|
|
v-if="!builds.suppItems"
|
|
label="Start"
|
|
:items="builds.start"
|
|
:item-map="itemMap"
|
|
:total-count="builds.tree.count"
|
|
/>
|
|
|
|
<!-- Support Items -->
|
|
<ItemRow
|
|
v-if="builds.suppItems"
|
|
label="Support"
|
|
:items="builds.suppItems"
|
|
:item-map="itemMap"
|
|
:total-count="builds.tree.count"
|
|
/>
|
|
|
|
<!-- Boots (regular or rush) -->
|
|
<ItemRow
|
|
:label="builds.bootsFirst > 0.5 ? 'Boots Rush' : 'Boots'"
|
|
:items="builds.boots.slice(0, 2)"
|
|
:item-map="itemMap"
|
|
:total-count="builds.tree.count"
|
|
:max-items="2"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Core Items Tree -->
|
|
<div class="item-row">
|
|
<span class="item-row-label">Core</span>
|
|
<ItemTree :tree="builds.tree" />
|
|
</div>
|
|
|
|
<!-- Late Game -->
|
|
<ItemRow
|
|
label="Late Game"
|
|
:items="builds.lateGame.slice(0, 6)"
|
|
:item-map="itemMap"
|
|
:total-count="builds.tree.count"
|
|
:max-items="6"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
.build-viewer {
|
|
width: 100%;
|
|
max-width: 1100px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
/* Main Build Content */
|
|
.build-content {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 80px;
|
|
padding: 0 24px;
|
|
width: 100%;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
margin-bottom: 10px;
|
|
color: #4a9eff;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
/* Left Column: Runes + Summoner Spells */
|
|
.build-left-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 40px;
|
|
}
|
|
|
|
.rune-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.rune-page-wrapper {
|
|
margin-top: -45px;
|
|
transform: scale(0.7);
|
|
transform-origin: center;
|
|
}
|
|
|
|
/* Right Column: Items */
|
|
.build-right-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.item-row-group {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 24px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.item-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.item-row-label {
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
color: var(--color-on-surface);
|
|
opacity: 0.6;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
/* Responsive: Mobile */
|
|
@media only screen and (max-width: 900px) {
|
|
.build-content {
|
|
flex-direction: column;
|
|
gap: 40px;
|
|
align-items: center;
|
|
}
|
|
|
|
.build-left-column {
|
|
order: 1;
|
|
align-items: center;
|
|
}
|
|
|
|
.build-right-column {
|
|
order: 2;
|
|
align-items: center;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.rune-section,
|
|
.summoner-spells-section {
|
|
align-items: center;
|
|
}
|
|
|
|
.rune-page-wrapper {
|
|
transform: scale(1);
|
|
transform-origin: center center;
|
|
}
|
|
}
|
|
</style>
|