Lint and format
Some checks failed
pipeline / lint-and-format (push) Failing after 56s
pipeline / build-and-push-images (push) Has been skipped

This commit is contained in:
2026-01-21 00:59:23 +01:00
parent 353baa6267
commit 3fc52205f6
53 changed files with 8505 additions and 2048 deletions

View File

@@ -1,44 +1,44 @@
<script setup lang="ts">
</script>
<script setup lang="ts"></script>
<template>
<Head>
<Title>About</Title>
</Head>
<Head>
<Title>About</Title>
</Head>
<div class="about-main-content">
<div class="about-main-content">
<NavBar :tierlist-list="true" />
<div style="width: fit-content; margin: auto;">
<Logo />
<div style="margin-top: 20px;">
<h1>About</h1>
<h3 style="margin-top: 20px;">BuildPath: a tool for League of Legends champions runes and build paths.</h3>
<h3 style="margin-top: 10px;">Copyright (C) Valentin Haudiquet (@vhaudiquet)</h3>
<h3 style="margin-top: 20px;">Acknowledgments:</h3>
<h3>- Sarah Emery, for the feedback on the designs and code</h3>
<h3>- Martin Andrieux, for the nice algorithms :)</h3>
<h3>- Paul Chaurand, for the feedback on the league data organization</h3>
<h3>- Nathan Mérillon, for the tierlists ideas</h3>
<h3>- Jean-Baptiste Döderlein, for the feedback on the mobile design</h3>
<h3 style="margin-top: 20px;">Libraries used:</h3>
<h3>Vue.JS, Nuxt.JS, Chart.JS, svg-dom-arrows</h3>
<h2 style="font-size: 1rem; font-weight: 200; margin-top: 25px; max-width: 800px;">
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, Inc.
</h2>
</div>
</div>
<div style="width: fit-content; margin: auto">
<Logo />
<div style="margin-top: 20px">
<h1>About</h1>
<h3 style="margin-top: 20px">
BuildPath: a tool for League of Legends champions runes and build paths.
</h3>
<h3 style="margin-top: 10px">Copyright (C) Valentin Haudiquet (@vhaudiquet)</h3>
<h3 style="margin-top: 20px">Acknowledgments:</h3>
<h3>- Sarah Emery, for the feedback on the designs and code</h3>
<h3>- Martin Andrieux, for the nice algorithms :)</h3>
<h3>- Paul Chaurand, for the feedback on the league data organization</h3>
<h3>- Nathan Mérillon, for the tierlists ideas</h3>
<h3>- Jean-Baptiste Döderlein, for the feedback on the mobile design</h3>
<h3 style="margin-top: 20px">Libraries used:</h3>
<h3>Vue.JS, Nuxt.JS, Chart.JS, svg-dom-arrows</h3>
<h2 style="font-size: 1rem; font-weight: 200; margin-top: 25px; max-width: 800px">
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, Inc.
</h2>
</div>
</div>
</div>
</template>
<style scoped>
.about-main-content {
display: flex;
margin-top: 30px;
display: flex;
margin-top: 30px;
}
</style>

View File

@@ -2,99 +2,112 @@
const route = useRoute()
const championAlias = route.params.alias as string
const { data : championData } : {data : Ref<ChampionData>} = await useFetch("/api/champion/" + championAlias.toLowerCase())
const { data: championData }: { data: Ref<ChampionData> } = await useFetch(
'/api/champion/' + championAlias.toLowerCase()
)
const championId = championData.value.id
// Prefetch home page for faster navigation
useHead({
link: [
{ rel: 'prefetch', href: '/' }
]
link: [{ rel: 'prefetch', href: '/' }]
})
defineOgImageComponent('Champion', {
title: championData.value.name,
id: championId,
winrate: championData.value.winrate,
pickrate: championData.value.pickrate,
gameCount: championData.value.gameCount,
title: championData.value.name,
id: championId,
winrate: championData.value.winrate,
pickrate: championData.value.pickrate,
gameCount: championData.value.gameCount
})
useSeoMeta({
title: championData.value.name,
description: 'Build path and runes for ' + championData.value.name + ' on BuildPath'
title: championData.value.name,
description: 'Build path and runes for ' + championData.value.name + ' on BuildPath'
})
const laneState = ref(0)
const state = ref("runes")
const state = ref('runes')
const lane = ref(championData.value.lanes[laneState.value])
function updateState(newState : string, newLane : number) {
state.value = newState
laneState.value = newLane
lane.value = championData.value.lanes[laneState.value]
function updateState(newState: string, newLane: number) {
state.value = newState
laneState.value = newLane
lane.value = championData.value.lanes[laneState.value]
}
</script>
<template>
<Head>
<Title>{{ championData.name }}</Title>
</Head>
<Head>
<Title>{{ championData.name }}</Title>
</Head>
<div id="alias-content-wrapper">
<NavBar :champion-name="championData.name"
:champion-lanes="championData.lanes"
@state-change="updateState"/>
<div id="alias-content-wrapper">
<NavBar
:champion-name="championData.name"
:champion-lanes="championData.lanes"
@state-change="updateState"
/>
<div id="champion-content">
<ChampionTitle id="champion-title" v-if="championData.gameCount > 0"
:champion-id="championId" :winrate="lane.winrate"
:pickrate="lane.pickrate" :game-count="lane.count" />
<RuneSelector v-if="state == 'runes' && championData.gameCount > 0"
style="margin: auto; margin-top: 40px;"
:runes="lane.runes!!" />
<ItemViewer v-if="state == 'items' && championData.gameCount > 0"
style="margin:auto; margin-top: 40px;"
:builds="lane.builds!!" />
<ItemTree v-if="state == 'alternatives' && championData.gameCount > 0"
style="margin: auto; margin-top: 40px; width: fit-content;"
:tree="lane.builds!!.tree" />
<h2 v-if="championData.gameCount == 0"
style="margin: auto; margin-top: 20px; width: fit-content;">
Sorry, there is no data for this champion :(
</h2>
</div>
<ChampionTitle
v-if="championData.gameCount > 0"
id="champion-title"
:champion-id="championId"
:winrate="lane.winrate"
:pickrate="lane.pickrate"
:game-count="lane.count"
/>
<RuneSelector
v-if="state == 'runes' && championData.gameCount > 0"
style="margin: auto; margin-top: 40px"
:runes="lane.runes!!"
/>
<ItemViewer
v-if="state == 'items' && championData.gameCount > 0"
style="margin: auto; margin-top: 40px"
:builds="lane.builds!!"
/>
<ItemTree
v-if="state == 'alternatives' && championData.gameCount > 0"
style="margin: auto; margin-top: 40px; width: fit-content"
:tree="lane.builds!!.tree"
/>
<h2
v-if="championData.gameCount == 0"
style="margin: auto; margin-top: 20px; width: fit-content"
>
Sorry, there is no data for this champion :(
</h2>
</div>
</div>
</template>
<style>
#alias-content-wrapper {
display: flex;
/* min-height: 100vh; */
align-items: stretch;
width: 100%;
display: flex;
/* min-height: 100vh; */
align-items: stretch;
width: 100%;
overflow: hidden;
overflow: hidden;
}
#champion-content {
margin-top: 64px;
margin-left: 39px;
width: 100%;
margin-top: 64px;
margin-left: 39px;
width: 100%;
}
@media only screen and (max-width: 650px) {
#champion-content {
margin: auto;
margin-top: 10px;
}
#champion-title {
margin:auto;
}
#champion-content {
margin: auto;
margin-top: 10px;
}
#champion-title {
margin: auto;
}
}
@media only screen and (max-width: 1200px) {
#alias-content-wrapper {
flex-direction: column;
padding-bottom: 120px;
margin-top: 20px;
}
#alias-content-wrapper {
flex-direction: column;
padding-bottom: 120px;
margin-top: 20px;
}
}
</style>

View File

@@ -1,63 +1,60 @@
<script setup lang="ts">
import { POSITIONS, LANE_IMAGES, POSITIONS_STR } from '~/utils/cdragon';
import { POSITIONS, LANE_IMAGES, POSITIONS_STR } from '~/utils/cdragon'
</script>
<template>
<Head>
<Title>Home</Title>
</Head>
<Head>
<Title>Home</Title>
</Head>
<div class="index-main-content">
<div class="index-main-content">
<NavBar :tierlist-list="true" />
<ChampionSelector class="index-champion-selector"/>
</div>
<ChampionSelector class="index-champion-selector" />
</div>
</template>
<style>
.index-main-content {
display: flex;
margin-top: 50px;
display: flex;
margin-top: 50px;
}
.index-champion-selector {
width: 80%;
width: 80%;
}
.index-tierlist-info-container {
margin: auto;
margin-top: 0px;
width: fit-content;
margin: auto;
margin-top: 0px;
width: fit-content;
}
#index-tierlists {
margin: auto;
margin-top: 10px;
width: fit-content;
margin: auto;
margin-top: 10px;
width: fit-content;
}
@media only screen and (max-width: 1240px) {
.index-main-content {
flex-direction: column;
margin-top: 5px;
}
.index-champion-selector {
width: 100%;
}
.index-tierlist-info-container {
display: flex;
margin-bottom: 50px;
}
.index-main-content {
flex-direction: column;
margin-top: 5px;
}
.index-champion-selector {
width: 100%;
}
.index-tierlist-info-container {
display: flex;
margin-bottom: 50px;
}
}
@media only screen and (max-width: 500px) {
#index-statp {
display: none;
}
.index-tierlist-info-container {
margin-bottom: 10px;
margin-left: 10px;
#index-statp {
display: none;
}
.index-tierlist-info-container {
margin-bottom: 10px;
margin-left: 10px;
display: none;
}
display: none;
}
}
</style>

View File

@@ -1,105 +1,129 @@
<script setup lang="ts">
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon';
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon'
const route = useRoute()
const lane = route.params.lane as string
const {data: championsData} : ChampionsResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json")
const { data: championsData }: ChampionsResponse = await useFetch(
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
)
const {data: championsLanes} : {data: Ref<Array<ChampionData>>} = await useFetch("/api/champions")
const infoMap : Map<string, ChampionData> = new Map()
for(let champion of championsLanes.value) {
infoMap.set(champion.alias, champion)
const { data: championsLanes }: { data: Ref<Array<ChampionData>> } =
await useFetch('/api/champions')
const infoMap: Map<string, ChampionData> = new Map()
for (const champion of championsLanes.value) {
infoMap.set(champion.alias, champion)
}
const champions = championsData.value.slice(1).filter((champion) => {
const championData : ChampionData | undefined = infoMap.get(champion.alias.toLowerCase())
if(championData == undefined) return false;
const champions = championsData.value.slice(1).filter(champion => {
const championData: ChampionData | undefined = infoMap.get(champion.alias.toLowerCase())
if (championData == undefined) return false
const lanes = championData.lanes
return lanes.reduce((acc : boolean, current : {data:string, count:number}) =>
acc || (current.data.toLowerCase() == lane.toLowerCase()), false)
const lanes = championData.lanes
return lanes.reduce(
(acc: boolean, current: { data: string; count: number }) =>
acc || current.data.toLowerCase() == lane.toLowerCase(),
false
)
})
const allChampions = champions.map((x) => {
const championData : ChampionData = infoMap.get(x.alias.toLowerCase())!!
const allChampions = champions
.map(x => {
const championData: ChampionData = infoMap.get(x.alias.toLowerCase())!
let currentLane = championData.lanes[0]
for(let championLane of championData.lanes) {
if(championLane.data.toLowerCase() == lane.toLowerCase()) {
currentLane = championLane
break
}
for (const championLane of championData.lanes) {
if (championLane.data.toLowerCase() == lane.toLowerCase()) {
currentLane = championLane
break
}
}
return {lane: currentLane, champion: x}
}).sort((a, b) => b.lane.pickrate - a.lane.pickrate)
return { lane: currentLane, champion: x }
})
.sort((a, b) => b.lane.pickrate - a.lane.pickrate)
const p_min = Math.min(...allChampions.map((x) => x.lane.pickrate))
const p_max = Math.max(...allChampions.map((x) => x.lane.pickrate))
const p_min = Math.min(...allChampions.map(x => x.lane.pickrate))
const p_max = Math.max(...allChampions.map(x => x.lane.pickrate))
allChampions.map((x) => (x as {lane: LaneData, champion: Champion, scaledPickrate: number}).scaledPickrate = (x.lane.pickrate - p_min)/(p_max - p_min))
allChampions.map(
x =>
((x as { lane: LaneData; champion: Champion; scaledPickrate: number }).scaledPickrate =
(x.lane.pickrate - p_min) / (p_max - p_min))
)
allChampions.sort((a, b) => b.lane.pickrate - a.lane.pickrate)
function tierFromScaledPickrate(min: number, max: number) {
return (allChampions as Array<{lane: LaneData, champion: Champion, scaledPickrate: number}>)
.filter(({scaledPickrate: scaledPickrate}) => {
return scaledPickrate > min && scaledPickrate <= max
})
return (
allChampions as Array<{ lane: LaneData; champion: Champion; scaledPickrate: number }>
).filter(({ scaledPickrate: scaledPickrate }) => {
return scaledPickrate > min && scaledPickrate <= max
})
}
const tiers: Array<{title:string, data: Array<{lane: LaneData, champion: Champion}>}> = []
tiers.push({title: "S", data: tierFromScaledPickrate(0.9, 1)})
tiers.push({title: "A", data: tierFromScaledPickrate(0.7, 0.9)})
tiers.push({title: "B", data: tierFromScaledPickrate(0.5, 0.7)})
tiers.push({title: "C", data: tierFromScaledPickrate(0.3, 0.5)})
tiers.push({title: "D", data: tierFromScaledPickrate(0.1, 0.3)})
tiers.push({title: "F", data: tierFromScaledPickrate(0, 0.1)})
const tiers: Array<{ title: string; data: Array<{ lane: LaneData; champion: Champion }> }> = []
tiers.push({ title: 'S', data: tierFromScaledPickrate(0.9, 1) })
tiers.push({ title: 'A', data: tierFromScaledPickrate(0.7, 0.9) })
tiers.push({ title: 'B', data: tierFromScaledPickrate(0.5, 0.7) })
tiers.push({ title: 'C', data: tierFromScaledPickrate(0.3, 0.5) })
tiers.push({ title: 'D', data: tierFromScaledPickrate(0.1, 0.3) })
tiers.push({ title: 'F', data: tierFromScaledPickrate(0, 0.1) })
</script>
<template>
<Head>
<Title>Tierlist for {{ POSITIONS_STR[lanePositionToIndex(lane)] }}</Title>
</Head>
<Head>
<Title>Tierlist for {{ POSITIONS_STR[lanePositionToIndex(lane)] }}</Title>
</Head>
<div style="display: flex; min-height: 100vh; align-items: stretch; width: 100%;">
<NavBar :tierlist-list="true" />
<div style="display: flex; min-height: 100vh; align-items: stretch; width: 100%">
<NavBar :tierlist-list="true" />
<div id="tierlist-container" style="margin-left: 10px; width: 100%; overflow-y: scroll;">
<div id="tierlist-container" style="margin-left: 10px; width: 100%; overflow-y: scroll">
<div
style="
margin-left: 0px;
margin-top: 20px;
display: flex;
margin-bottom: 30px;
align-items: center;
"
>
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300">Tierlist for</h1>
<NuxtImg
format="webp"
style="margin-left: 10px"
width="50"
height="50"
:src="LANE_IMAGES[lanePositionToIndex(lane)]"
/>
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300">
{{ POSITIONS_STR[lanePositionToIndex(lane)] }}
</h1>
</div>
<div style="margin-left: 0px; margin-top: 20px; display: flex; margin-bottom: 30px; align-items: center">
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300;">Tierlist for</h1>
<NuxtImg format="webp" style="margin-left: 10px;"
width="50" height="50"
:src="LANE_IMAGES[lanePositionToIndex(lane)]" />
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300;">{{ POSITIONS_STR[lanePositionToIndex(lane)] }}</h1>
</div>
<TierlistTier v-for="tier in tiers" :title="tier.title" :tier="tier.data" />
<TierlistTier v-for="tier in tiers" :title="tier.title" :tier="tier.data" />
<TierlistChart id="chart" :data="tiers" />
</div>
<TierlistChart id="chart" :data="tiers" />
</div>
</div>
</template>
<style scoped>
#chart {
margin-left: 100px;
margin-right: 100px;
margin-bottom: 100px;
margin-top: 40px
margin-left: 100px;
margin-right: 100px;
margin-bottom: 100px;
margin-top: 40px;
}
@media only screen and (max-width: 450px) {
#chart {
margin-left: 2px;
margin-right: 12px;
margin-bottom: 40px;
margin-top: 40px;
}
#tierlist-container {
padding-bottom: 120px;
}
#chart {
margin-left: 2px;
margin-right: 12px;
margin-bottom: 40px;
margin-top: 40px;
}
#tierlist-container {
padding-bottom: 120px;
}
}
</style>
</style>