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,41 +1,49 @@
<script lang="ts" setup>
defineProps<{
title: string,
bootsFirst?: number,
sizePerc?: number
title: string
bootsFirst?: number
sizePerc?: number
}>()
</script>
<template>
<div :style="(sizePerc != undefined && sizePerc != null) ? 'max-height: ' + (sizePerc * 600) + 'px;' : ''" class="item-box">
<div style="display:flex; flex-direction: column; justify-content: center; align-items: center;">
<h2 class="item-box-title">{{ title }}</h2>
<h5 v-if="bootsFirst != undefined && bootsFirst != null"
style="margin: auto;">({{ (bootsFirst * 100).toFixed(2) }}%)</h5>
<div
:style="
sizePerc != undefined && sizePerc != null ? 'max-height: ' + sizePerc * 600 + 'px;' : ''
"
class="item-box"
>
<div
style="display: flex; flex-direction: column; justify-content: center; align-items: center"
>
<h2 class="item-box-title">{{ title }}</h2>
<h5 v-if="bootsFirst != undefined && bootsFirst != null" style="margin: auto">
({{ (bootsFirst * 100).toFixed(2) }}%)
</h5>
</div>
<slot/>
</div>
<slot />
</div>
</template>
<style scoped>
.item-box {
border: 1px solid var(--color-on-surface);
border-radius: 8px;
border: 1px solid var(--color-on-surface);
border-radius: 8px;
margin: 10px;
margin: 10px;
width: fit-content;
height: 600px;
width: fit-content;
height: 600px;
}
.item-box-title {
font-variant: small-caps;
text-align: center;
margin: 10px;
font-variant: small-caps;
text-align: center;
margin: 10px;
}
@media only screen and (max-width: 1000px) {
.item-box {
width: 95%;
height: fit-content;
}
.item-box {
width: 95%;
height: fit-content;
}
}
</style>
</style>

View File

@@ -1,112 +1,128 @@
<script setup lang="ts">
import svgdomarrows from 'svg-dom-arrows'
defineProps<{
tree: ItemTree,
parentCount?: number
tree: ItemTree
parentCount?: number
}>()
const emit = defineEmits<{
mount: [end: Element],
refresh: []
mount: [end: Element]
refresh: []
}>()
const {data : items} : ItemResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/items.json")
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)
for (const item of items.value) {
itemMap.set(item.id, item)
}
import svgdomarrows from 'svg-dom-arrows';
const start : Ref<Element | null> = useTemplateRef("start")
const arrows : Array<svgdomarrows.LinePath> = []
const start: Ref<Element | null> = useTemplateRef('start')
const arrows: Array<svgdomarrows.LinePath> = []
onMounted(() => {
refreshArrows()
emit('mount', start.value!!)
refreshArrows()
emit('mount', start.value!)
})
onBeforeUpdate(() => {
for(let arrow of arrows) {
arrow.release()
}
arrows.splice(0, arrows.length)
for (const arrow of arrows) {
arrow.release()
}
arrows.splice(0, arrows.length)
})
onUpdated(() => {
refreshArrows()
emit('mount', start.value!!)
refreshArrows()
emit('mount', start.value!)
})
onUnmounted(() => {
for(let arrow of arrows) {
arrow.release()
}
for (const arrow of arrows) {
arrow.release()
}
})
function drawArrow(start : Element, end : Element) {
// console.log("drawArrow(", start, ", ", end, ")")
if(start == null || end == null) return;
function drawArrow(start: Element, end: Element) {
// console.log("drawArrow(", start, ", ", end, ")")
if (start == null || end == null) return
const arrow = new svgdomarrows.LinePath({
start: {
element: start,
position: {
top: 0.5,
left: 1
}
},
end: {
element: end,
position: {
top: 0.5,
left: 0
}
},
style: 'stroke:var(--color-on-surface);stroke-width:3;fill:transparent;',
appendTo: document.body
})
arrows.push(arrow)
const arrow = new svgdomarrows.LinePath({
start: {
element: start,
position: {
top: 0.5,
left: 1
}
},
end: {
element: end,
position: {
top: 0.5,
left: 0
}
},
style: 'stroke:var(--color-on-surface);stroke-width:3;fill:transparent;',
appendTo: document.body
})
arrows.push(arrow)
}
function refreshArrows() {
for(let arrow of arrows) {
arrow.redraw()
}
for (const arrow of arrows) {
arrow.redraw()
}
}
// Redraw arrows on window resize
addEventListener('resize', (_) => {
refreshArrows()
addEventListener('resize', _ => {
refreshArrows()
})
addEventListener("scroll", (_) => {
refreshArrows()
addEventListener('scroll', _ => {
refreshArrows()
})
function handleSubtreeMount(end : Element) {
drawArrow(start.value!!, end)
refreshArrows()
emit('refresh')
function handleSubtreeMount(end: Element) {
drawArrow(start.value!, end)
refreshArrows()
emit('refresh')
}
function handleRefresh() {
refreshArrows()
emit('refresh')
refreshArrows()
emit('refresh')
}
</script>
<template>
<div style="display: flex; align-items: center;">
<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 / parentCount!!) * 100).toFixed(0) }}%</h3>
</div>
<div style="margin-left: 30px;">
<div style="width: fit-content; height: fit-content;" v-for="child in tree.children">
<ItemTree @refresh="handleRefresh" @mount="(end) => handleSubtreeMount(end)" :tree="child" :parent-count="tree.count" />
</div>
</div>
<div style="display: flex; align-items: center">
<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 / parentCount!!) * 100).toFixed(0) }}%
</h3>
</div>
<div style="margin-left: 30px">
<div v-for="child in tree.children" style="width: fit-content; height: fit-content">
<ItemTree
:tree="child"
:parent-count="tree.count"
@refresh="handleRefresh"
@mount="end => handleSubtreeMount(end)"
/>
</div>
</div>
</div>
</template>

View File

@@ -1,64 +1,72 @@
<script setup lang="ts">
import { isEmpty, deepClone } from '~/utils/helpers';
import { isEmpty, deepClone } from '~/utils/helpers'
const props = defineProps<{
builds: Builds;
loading?: boolean;
error?: boolean;
}>();
builds: Builds
loading?: boolean
error?: boolean
}>()
// Constants
const ITEMS_API_URL = CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/items.json";
const ITEMS_API_URL = CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/items.json'
// State
const { data: items, pending: loadingItems, error: itemsError } = await useFetch(ITEMS_API_URL);
const itemMap = ref<Map<number, any>>(new Map());
const { data: items, pending: loadingItems, error: itemsError } = await useFetch(ITEMS_API_URL)
const itemMap = ref<Map<number, any>>(new Map())
// Initialize item map
watch(items, (newItems) => {
try {
const itemsData = newItems || [];
if (Array.isArray(itemsData)) {
const map = new Map<number, any>();
for (const item of itemsData) {
if (item?.id) {
map.set(item.id, item);
watch(
items,
newItems => {
try {
const itemsData = newItems || []
if (Array.isArray(itemsData)) {
const map = new Map<number, any>()
for (const item of itemsData) {
if (item?.id) {
map.set(item.id, item)
}
}
itemMap.value = map
}
itemMap.value = map;
} catch (error) {
console.error('Error initializing item map:', error)
}
} catch (error) {
console.error('Error initializing item map:', error);
}
}, { immediate: true });
},
{ immediate: true }
)
// Builds management
const builds = ref<Builds>(deepClone(props.builds));
const builds = ref<Builds>(deepClone(props.builds))
watch(() => props.builds, (newBuilds) => {
builds.value = deepClone(newBuilds);
trimBuilds(builds.value);
trimLateGameItems(builds.value);
}, { deep: true });
watch(
() => props.builds,
newBuilds => {
builds.value = deepClone(newBuilds)
trimBuilds(builds.value)
trimLateGameItems(builds.value)
},
{ deep: true }
)
// Initialize with trimmed builds
onMounted(() => {
trimBuilds(builds.value);
trimLateGameItems(builds.value);
});
trimBuilds(builds.value)
trimLateGameItems(builds.value)
})
/**
* Trim builds tree to show only primary build paths
*/
function trimBuilds(builds: Builds): void {
if (!builds?.tree?.children) return;
if (!builds?.tree?.children) return
// Keep only the first child (primary build path)
builds.tree.children.splice(1, builds.tree.children.length - 1);
builds.tree.children.splice(1, builds.tree.children.length - 1)
// For the primary path, keep only the first child of the first child
if (builds.tree.children[0]?.children) {
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1);
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1)
}
}
@@ -66,161 +74,206 @@ function trimBuilds(builds: Builds): void {
* Remove items from lateGame that are already in the build tree
*/
function trimLateGameItems(builds: Builds): void {
if (!builds?.tree || isEmpty(builds.lateGame)) return;
if (!builds?.tree || isEmpty(builds.lateGame)) return
function trimLateGameItemsFromTree(tree: ItemTree): void {
const foundIndex = builds.lateGame.findIndex((x) => x.data === tree.data);
const foundIndex = builds.lateGame.findIndex(x => x.data === tree.data)
if (foundIndex !== -1) {
builds.lateGame.splice(foundIndex, 1);
builds.lateGame.splice(foundIndex, 1)
}
for (const child of tree.children || []) {
trimLateGameItemsFromTree(child);
trimLateGameItemsFromTree(child)
}
}
trimLateGameItemsFromTree(builds.tree);
trimLateGameItemsFromTree(builds.tree)
}
/**
* Get item data safely
*/
function getItemData(itemId: number): any {
return itemMap.value.get(itemId) || { iconPath: '' };
return itemMap.value.get(itemId) || { iconPath: '' }
}
/**
* Calculate percentage for item display
*/
function getItemPercentage(item: { count: number }, total: number): string {
if (total <= 0) return '0%';
return ((item.count / total) * 100).toFixed(0) + '%';
if (total <= 0) return '0%'
return ((item.count / total) * 100).toFixed(0) + '%'
}
// Error and loading states
const hasError = computed(() => itemsError.value || props.error);
const isLoading = computed(() => loadingItems.value || props.loading);
const hasError = computed(() => itemsError.value || props.error)
const isLoading = computed(() => loadingItems.value || props.loading)
</script>
<template>
<div id="iv-container">
<div>
<!-- Start items -->
<ItemBox title="start" v-if="builds.suppItems == undefined || builds.suppItems == null">
<div class="iv-items-container">
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.start" >
<NuxtImg v-if="item.data != null && item.data != undefined"
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/builds.tree.count * 100).toFixed(0) }}%</h3>
</div>
</div>
</ItemBox>
<!-- Supp items -->
<ItemBox v-if="builds.suppItems != undefined && builds.suppItems != null" title="supp">
<div class="iv-items-container">
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.suppItems" >
<NuxtImg v-if="item.data != null && item.data != undefined"
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/builds.tree.count * 100).toFixed(0) }}%</h3>
</div>
</div>
</ItemBox>
<div id="iv-container">
<div>
<!-- Start items -->
<ItemBox v-if="builds.suppItems == undefined || builds.suppItems == null" title="start">
<div class="iv-items-container">
<div v-for="item in builds.start" style="margin-left: 5px; margin-right: 5px">
<NuxtImg
v-if="item.data != null && item.data != undefined"
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 / builds.tree.count) * 100).toFixed(0) }}%
</h3>
</div>
</div>
</ItemBox>
<!-- Supp items -->
<ItemBox v-if="builds.suppItems != undefined && builds.suppItems != null" title="supp">
<div class="iv-items-container">
<div v-for="item in builds.suppItems" style="margin-left: 5px; margin-right: 5px">
<NuxtImg
v-if="item.data != null && item.data != undefined"
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 / builds.tree.count) * 100).toFixed(0) }}%
</h3>
</div>
</div>
</ItemBox>
</div>
<!-- Boots first : when champion rush boots -->
<ItemBox v-if="builds.bootsFirst > 0.5" title="boots rush" :boots-first="builds.bootsFirst">
<div class="iv-items-container">
<div v-for="item in builds.boots" style="margin-left: 5px; margin-right: 5px">
<NuxtImg
v-if="item.data != null && item.data != undefined"
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 / builds.tree.count) * 100).toFixed(0) }}%
</h3>
</div>
</div>
</ItemBox>
<!-- Core items -->
<ItemBox title="core">
<ItemTree style="margin: auto; width: fit-content" :tree="builds.tree" />
</ItemBox>
<!-- Boots -->
<ItemBox v-if="builds.bootsFirst <= 0.5" title="boots">
<div class="iv-items-container">
<div v-for="item in builds.boots.slice(0, 4)" style="margin-left: 5px; margin-right: 5px">
<NuxtImg
v-if="item.data != null && item.data != undefined"
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 / builds.tree.count) * 100).toFixed(0) }}%
</h3>
</div>
</div>
</ItemBox>
<!-- Late game items -->
<ItemBox title="late game">
<div id="iv-late-game-container">
<div class="iv-items-container">
<div
v-for="item in builds.lateGame.slice(0, 4)"
style="margin-left: 5px; margin-right: 5px"
>
<NuxtImg
v-if="item.data != null && item.data != undefined"
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 / builds.tree.count) * 100).toFixed(0) }}%
</h3>
</div>
</div>
<!-- Boots first : when champion rush boots -->
<ItemBox v-if="builds.bootsFirst > 0.5" title="boots rush" :bootsFirst="builds.bootsFirst">
<div class="iv-items-container">
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.boots" >
<NuxtImg v-if="item.data != null && item.data != undefined"
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/builds.tree.count * 100).toFixed(0) }}%</h3>
</div>
</div>
</ItemBox>
<!-- Core items -->
<ItemBox title="core">
<ItemTree style="margin:auto; width: fit-content;" :tree="builds.tree" />
</ItemBox>
<!-- Boots -->
<ItemBox v-if="builds.bootsFirst <= 0.5" title="boots">
<div class="iv-items-container">
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.boots.slice(0, 4)" >
<NuxtImg v-if="item.data != null && item.data != undefined"
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/builds.tree.count * 100).toFixed(0) }}%</h3>
</div>
</div>
</ItemBox>
<!-- Late game items -->
<ItemBox title="late game">
<div id="iv-late-game-container">
<div class="iv-items-container">
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.lateGame.slice(0, 4)" >
<NuxtImg v-if="item.data != null && item.data != undefined"
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/builds.tree.count * 100).toFixed(0) }}%</h3>
</div>
</div>
<div class="iv-items-container" v-if="builds.lateGame.length > 4">
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.lateGame.slice(4, 8)" >
<NuxtImg v-if="item.data != null && item.data != undefined"
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/builds.tree.count * 100).toFixed(0) }}%</h3>
</div>
</div>
</div>
</ItemBox>
</div>
<div v-if="builds.lateGame.length > 4" class="iv-items-container">
<div
v-for="item in builds.lateGame.slice(4, 8)"
style="margin-left: 5px; margin-right: 5px"
>
<NuxtImg
v-if="item.data != null && item.data != undefined"
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 / builds.tree.count) * 100).toFixed(0) }}%
</h3>
</div>
</div>
</div>
</ItemBox>
</div>
</template>
<style>
#iv-container {
display: flex;
width: fit-content;
height: fit-content;
display: flex;
width: fit-content;
height: fit-content;
}
.iv-items-container {
display: flex;
flex-direction: column;
width: fit-content;
height: fit-content;
margin:auto;
display: flex;
flex-direction: column;
width: fit-content;
height: fit-content;
margin: auto;
}
.item-img {
border: 1px solid var(--color-on-surface);
margin: 10px;
border: 1px solid var(--color-on-surface);
margin: 10px;
}
#iv-late-game-container {
display: flex;
display: flex;
}
@media only screen and (max-width: 1000px) {
#iv-container {
flex-direction: column;
width: 100%;
}
.iv-items-container {
flex-direction: row;
}
.item-img {
width: 48px;
height: 48px;
}
#iv-late-game-container {
flex-direction: column;
}
#iv-container {
flex-direction: column;
width: 100%;
}
.iv-items-container {
flex-direction: row;
}
.item-img {
width: 48px;
height: 48px;
}
#iv-late-game-container {
flex-direction: column;
}
}
</style>