Lane-dependant stats (fix #5)
All checks were successful
pipeline / build-and-push-images (push) Successful in 23s
pipeline / deploy (push) Successful in 7s

This commit is contained in:
2024-11-29 18:26:17 +01:00
parent 5b7262877d
commit d8443efd7e
12 changed files with 233 additions and 107 deletions

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
const props = defineProps<{
championId: string,
championId: number,
winrate: number,
pickrate: number,
gameCount: number
}>()
const championId = Number(props.championId)
const winrate = ref((props.winrate * 100).toFixed(2))
watch(() => props.winrate, () => {winrate.value = (props.winrate * 100).toFixed(2)})
const pickrate = ref((props.pickrate * 100).toFixed(2))
watch(() => props.pickrate, () => {pickrate.value = (props.pickrate * 100).toFixed(2)})
const winrate = (props.winrate * 100).toFixed(2)
const pickrate = (props.pickrate * 100).toFixed(2)
const gameCount = props.gameCount
const { data: championData } : ChampionResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champions/" + championId + ".json")
const { data: championData } : ChampionResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champions/" + props.championId + ".json")
const championName = championData.value.name
const championDescription = championData.value.title
</script>

View File

@@ -2,7 +2,6 @@
const props = defineProps<{
builds: Builds
}>()
const builds = props.builds
const {data : items} : ItemResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/items.json")
const itemMap = reactive(new Map())
@@ -10,9 +9,15 @@ for(let item of items.value) {
itemMap.set(item.id, item)
}
builds.tree.children.splice(1, builds.tree.children.length - 1)
if(builds.tree.children[0] != null && builds.tree.children[0] != undefined)
watch(() => props.builds, () => trimBuilds(props.builds))
trimBuilds(props.builds)
function trimBuilds(builds : Builds) {
builds.tree.children.splice(1, builds.tree.children.length - 1)
if(builds.tree.children[0] != null && builds.tree.children[0] != undefined)
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1)
}
</script>
<template>

View File

@@ -1,13 +1,10 @@
<script setup lang="ts">
import { LANE_IMAGES, LANE_IMAGES_HOVER, LANE_IMAGES_SELECTED } from '~/utils/cdragon';
const emit = defineEmits<{
filterChange: [value: number]
}>()
const POSITIONS = ["top", "jungle", "middle", "bottom", "utility"]
const LANE_IMAGES = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + ".png")
const LANE_IMAGES_HOVER = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + "-hover.png")
const LANE_IMAGES_SELECTED = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + "-blue.png")
const laneImgs = Array(5).fill(ref("")).map((_, index) => ref(LANE_IMAGES[index]))
const laneFilter = ref(-1)

View File

@@ -7,8 +7,6 @@ const props = defineProps<{
pickrate: number}>
}>()
const runes = props.runes
const currentlySelectedPage = ref(0)
const primaryStyles : Ref<Array<PerkStyle>> = ref([])
const secondaryStyles : Ref<Array<PerkStyle>> = ref([])
@@ -21,8 +19,18 @@ for(let perk of perks_data.value) {
}
let { data: stylesData } : PerkStylesResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json")
for(let style of stylesData.value.styles) {
for(let rune of runes) {
watch(() => props.runes, (newRunes, oldRunes) => {
currentlySelectedPage.value = 0
primaryStyles.value = []
secondaryStyles.value = []
keystoneIds.value = []
refreshStylesKeystones()
})
function refreshStylesKeystones() {
for(let style of stylesData.value.styles) {
for(let rune of props.runes) {
if(style.id == rune.primaryStyle) {
primaryStyles.value.push(style)
for(let perk of style.slots[0].perks) {
@@ -35,8 +43,11 @@ for(let style of stylesData.value.styles) {
secondaryStyles.value.push(style)
}
}
}
}
refreshStylesKeystones()
function runeSelect(index: number) {
currentlySelectedPage.value = index
}
@@ -45,8 +56,12 @@ function runeSelect(index: number) {
<template>
<div style="width: fit-content;">
<RunePage style="margin:auto; width: fit-content;" :primaryStyleId="runes[currentlySelectedPage].primaryStyle" :secondaryStyleId="runes[currentlySelectedPage].secondaryStyle" :selectionIds="runes[currentlySelectedPage].selections" />
<div style="display: flex; margin-top: 20px;">
<RunePage v-if="runes[currentlySelectedPage] != undefined && runes[currentlySelectedPage] != null"
style="margin:auto; width: fit-content;"
:primaryStyleId="runes[currentlySelectedPage].primaryStyle"
:secondaryStyleId="runes[currentlySelectedPage].secondaryStyle"
:selectionIds="runes[currentlySelectedPage].selections" />
<div style="display: flex; margin-top: 20px; justify-content: center;">
<div v-for="(_, i) in runes" :class="'rune-selector-entry ' + (i == currentlySelectedPage ? 'rune-selector-entry-selected' : '')" @click="runeSelect(i)">
<div style="display: flex; margin-top: 20px;">
<NuxtImg v-if="primaryStyles[i] != null && primaryStyles[i] != undefined"

View File

@@ -1,27 +1,42 @@
<script setup lang="ts">
import { LANE_IMAGES, lanePositionToIndex } from '~/utils/cdragon';
defineProps<{
championName: String
championLanes: any
}>()
const emit = defineEmits<{
stateChange: [state: String]
stateChange: [state: String, lane: number]
}>()
const state = ref("runes")
const laneState = ref(0)
function handleStateChange(newState : string) {
function handleStateChange(newState : string, newLane: number) {
state.value = newState;
emit('stateChange', newState)
laneState.value = newLane;
emit('stateChange', newState, newLane)
}
</script>
<template>
<div class="sidebar-container">
<Logo font-size="2.6rem" img-width="60" style="padding-left: 15px; padding-right: 15px; margin-top: 30px;"/>
<h1 class="sidebar-link" style="margin-top: 30px; font-size: 2.4rem; padding-left: 20px;">{{ championName }}</h1>
<h2 :class="'sidebar-link ' + (state == 'runes' ? 'sidebar-link-selected' : '')"
@click="handleStateChange('runes')" style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px;">Runes</h2>
<h2 :class="'sidebar-link ' + (state == 'items' ? 'sidebar-link-selected' : '')"
@click="handleStateChange('items')" style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px;">Items</h2>
<div v-for="(lane, i) in championLanes">
<div style="display: flex; align-items: center; margin-top: 30px;">
<h1 style="font-size: 2.4rem; padding-left: 20px;">{{ championName }}</h1>
<img style="margin-left: 10px;" width="40" height="40" :src="LANE_IMAGES[lanePositionToIndex(lane.data)]" />
<h2 style="margin-left: 5px; font-size: 1.8rem; font-weight: 200;">{{ lane.data.toLowerCase() }}</h2>
</div>
<h2 :class="'sidebar-link ' + (state == 'runes' && laneState == i ? 'sidebar-link-selected' : '')"
@click="handleStateChange('runes', i)" style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px;">Runes</h2>
<h2 :class="'sidebar-link ' + (state == 'items' && laneState == i ? 'sidebar-link-selected' : '')"
@click="handleStateChange('items', i)" style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px;">Items</h2>
</div>
</div>
</template>

View File

@@ -8,15 +8,12 @@ const emit = defineEmits<{
refresh: []
}>()
const item = props.tree
const {data : items} : ItemResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/items.json")
const itemMap = reactive(new Map())
for(let item of items.value) {
itemMap.set(item.id, item)
}
import type { TreeItem } from '#build/components';
import pkg from 'svg-dom-arrows';
const { LinePath } = pkg;
@@ -28,6 +25,18 @@ onMounted(() => {
emit('mount', start.value!!)
})
onBeforeUpdate(() => {
for(let arrow of arrows) {
arrow.release()
}
arrows.splice(0, arrows.length)
})
onUpdated(() => {
refreshArrows()
emit('mount', start.value!!)
})
onUnmounted(() => {
for(let arrow of arrows) {
arrow.release()
@@ -84,13 +93,13 @@ function handleRefresh() {
<template>
<div style="display: flex; align-items: center;">
<div v-if="item.data != undefined && item.data != null" style="width: fit-content; height: fit-content;">
<img ref="start" class="item-img" width="64" height="64" :alt="item.data.toString()" :src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ item.count }}</h3>
<div v-if="tree.data != undefined && tree.data != null" style="width: fit-content; height: fit-content;">
<img ref="start" class="item-img" width="64" height="64" :alt="tree.data.toString()" :src="CDRAGON_BASE + mapPath(itemMap.get(tree.data).iconPath)" />
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ tree.count }}</h3>
</div>
<div style="margin-left: 30px;">
<div style="width: fit-content; height: fit-content;" v-for="child in item.children">
<div style="width: fit-content; height: fit-content;" v-for="child in tree.children">
<TreeItem @refresh="handleRefresh" @mount="(end) => handleSubtreeMount(end)" :tree="child" />
</div>
</div>

View File

@@ -1,11 +1,18 @@
<script setup>
<script setup lang="ts">
const route = useRoute()
const championAlias = route.params.alias
const championAlias = route.params.alias as string
const { data : championData } = await useFetch("/api/champion/" + championAlias.toLowerCase())
const { data : championData } : {data : Ref<ChampionData>} = await useFetch("/api/champion/" + championAlias.toLowerCase())
const championId = championData.value.id
const laneState = ref(0)
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]
}
</script>
<template>
@@ -13,13 +20,15 @@ const state = ref("runes")
<Title>{{ championData.name }} - BuildPath</Title>
</Head>
<SideBar :champion-name="championData.name" @state-change="(newState) => state = newState"/>
<SideBar :champion-name="championData.name"
:champion-lanes="championData.lanes"
@state-change="updateState"/>
<!-- <div style="display: flex; width: fit-content; margin: auto; margin-left: 330px;"> -->
<div style="margin-top: 64px; margin-left: 339px;">
<ChampionTitle :champion-id="championId" :winrate="championData.winrate" :pickrate="championData.pickrate" :game-count="championData.gameCount" />
<RuneSelector v-if="state == 'runes' && championData.gameCount > 0" style="margin: auto; margin-top: 40px;" :runes="championData.runes" />
<ItemViewer v-if="state == 'items' && championData.gameCount > 0" style="margin:auto; margin-top: 40px;" :builds="championData.builds" />
<ChampionTitle 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" />
<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>
<!-- <ItemViewer v-if="championData.gameCount > 0" style="margin-top: 64px; margin-left: 64px;" :builds="championData.builds" /> -->

43
frontend/types/api.ts Normal file
View File

@@ -0,0 +1,43 @@
declare global {
type ItemTree = {
count: number
data: number
children: Array<ItemTree>
}
type Builds = {
start: Array<{count: number, data: number}>
tree: ItemTree
bootsFirst: number
boots: Array<{count: number, data: number}>
lateGame: Array<{count: number, data: number}>
}
type Rune = {
count: number
primaryStyle: number
secondaryStyle: number
selections: Array<number>
pickrate: number
}
type LaneData = {
data: string
count: number
winningMatches: number
losingMatches: number
winrate: number
pickrate: number
runes: Array<Rune>
builds: Builds
}
type ChampionData = {
id: number
name: string
alias: string
gameCount: number
winrate: number
pickrate: number
lanes: Array<LaneData>
}
}
export {};

View File

@@ -1,16 +0,0 @@
declare global {
type ItemTree = {
count: number
data: number
children: Array<ItemTree>
}
type Builds = {
start: Array<{count: number, data: number}>
tree: ItemTree
bootsFirst: number
boots: Array<{count: number, data: number}>
lateGame: Array<{count: number, data: number}>
}
}
export {};

View File

@@ -1,8 +0,0 @@
const CDRAGON_BASE = "https://raw.communitydragon.org/latest/"
function mapPath(assetPath) {
if(assetPath === undefined || assetPath === null) return ""
return assetPath.toLowerCase().replace("/lol-game-data/assets/", "plugins/rcp-be-lol-game-data/global/default/")
}
export { mapPath, CDRAGON_BASE}

31
frontend/utils/cdragon.ts Normal file
View File

@@ -0,0 +1,31 @@
const CDRAGON_BASE = "https://raw.communitydragon.org/latest/"
/* Lanes */
const POSITIONS = ["top", "jungle", "middle", "bottom", "utility"]
const LANE_IMAGES = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + ".png")
const LANE_IMAGES_HOVER = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + "-hover.png")
const LANE_IMAGES_SELECTED = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + "-blue.png")
function laneIndexToPosition(index : number) {
switch(index) {
case 0: return "top"
case 1: return "jungle"
case 2: return "middle"
case 3: return "bottom"
case 4: return "utility"
}
return null
}
function lanePositionToIndex(position : string) {
const p = position.toLowerCase()
for(let i = 0; i < POSITIONS.length; i++) {
if(p == POSITIONS[i]) return i;
}
return -1;
}
function mapPath(assetPath : string) {
if(assetPath === undefined || assetPath === null) return ""
return assetPath.toLowerCase().replace("/lol-game-data/assets/", "plugins/rcp-be-lol-game-data/global/default/")
}
export { mapPath, CDRAGON_BASE, laneIndexToPosition, lanePositionToIndex, POSITIONS, LANE_IMAGES, LANE_IMAGES_HOVER, LANE_IMAGES_SELECTED}

View File

@@ -46,6 +46,16 @@ type Champion = {
name: String
alias: String
}
type LaneData = {
data: string
count: number
winningMatches: number
losingMatches: number
winrate: number
pickrate: number
runes: Array<Rune>
builds: Builds
}
function handleParticipantRunes(participant, runes: Array<Rune>) {
const primaryStyle = participant.perks.styles[0].style
@@ -141,9 +151,7 @@ async function championInfos(client, patch: number, champion: Champion) {
let winningMatches = 0;
let losingMatches = 0;
let totalMatches = 0;
const lanes : Array<{data: string, count: number}> = [];
const runes : Array<Rune> = [];
const builds : Builds = {tree:treeInit(), start: [], bootsFirst: 0, boots: [], lateGame: []}
const lanes : Array<LaneData> = [];
for await (let match of allMatches) {
totalMatches += 1;
let participantIndex = 0;
@@ -158,16 +166,24 @@ async function championInfos(client, patch: number, champion: Champion) {
losingMatches += 1;
// Lanes
// TODO: make stats lane-dependant
const already = lanes.find((x) => x.data == participant.teamPosition)
if(already == undefined) lanes.push({count:1, data: participant.teamPosition})
else already.count += 1
let lane = lanes.find((x) => x.data == participant.teamPosition)
if(lane == undefined) {
const builds : Builds = {tree:treeInit(), start: [], bootsFirst: 0, boots: [], lateGame: []}
lane = {count:1, data: participant.teamPosition, runes:[], builds:builds, winningMatches: 0, losingMatches: 0, winrate: 0, pickrate: 0}
lanes.push(lane)
}
else lane.count += 1
if(participant.win)
lane.winningMatches += 1;
else
lane.losingMatches += 1;
// Runes
handleParticipantRunes(participant, runes)
handleParticipantRunes(participant, lane.runes)
// Items
handleMatchItems(match.timeline, participantIndex, builds)
handleMatchItems(match.timeline, participantIndex, lane.builds)
break;
}
@@ -179,31 +195,44 @@ async function championInfos(client, patch: number, champion: Champion) {
lanes.sort((a, b) => b.count - a.count)
// Filter runes to keep 3 most played
for(let lane of lanes) {
const runes = lane.runes
runes.sort((a, b) => b.count - a.count)
if(runes.length > 3)
runes.splice(3, runes.length - 3)
// Compute runes pickrate
for(let rune of runes)
rune.pickrate = rune.count / totalChampionMatches;
rune.pickrate = rune.count / lane.count;
}
for(let lane of lanes) {
const builds = lane.builds
// Cut item tree branches to keep only 4 branches every time and with percentage threshold
builds.tree.count = totalChampionMatches;
builds.tree.count = lane.count;
treeCutBranches(builds.tree, 4, 0.05)
treeSort(builds.tree)
// Cut item start, to only 4 and with percentage threshold
arrayRemovePercentage(builds.start, totalChampionMatches, 0.05)
arrayRemovePercentage(builds.start, lane.count, 0.05)
builds.start.sort((a, b) => b.count - a.count)
if(builds.start.length > 4)
builds.start.splice(4, builds.start.length - 4)
// Remove boots that are not within percentage threshold
arrayRemovePercentage(builds.boots, totalChampionMatches, 0.05)
arrayRemovePercentage(builds.boots, lane.count, 0.05)
builds.boots.sort((a, b) => b.count - a.count)
builds.bootsFirst /= (winningMatches + losingMatches)
builds.bootsFirst /= lane.count
builds.lateGame.sort((a, b) => b.count - a.count)
}
for(let lane of lanes) {
lane.winrate = lane.winningMatches / lane.count
lane.pickrate = lane.count / totalMatches
}
return {name: champion.name,
alias: champion.alias.toLowerCase(),
@@ -212,8 +241,6 @@ async function championInfos(client, patch: number, champion: Champion) {
winrate: winningMatches / totalChampionMatches,
gameCount: totalChampionMatches,
pickrate: totalChampionMatches/totalMatches,
runes: runes,
builds: builds
};
}