Lint and format
This commit is contained in:
@@ -8,7 +8,56 @@ permissions:
|
|||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
lint-and-format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: https://gitea.com/actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies for frontend
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Lint frontend
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Check formatting for frontend
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npm run format:check
|
||||||
|
|
||||||
|
- name: Install dependencies for match_collector
|
||||||
|
working-directory: ./match_collector
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Lint match_collector
|
||||||
|
working-directory: ./match_collector
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Check formatting for match_collector
|
||||||
|
working-directory: ./match_collector
|
||||||
|
run: npm run format:check
|
||||||
|
|
||||||
|
- name: Install dependencies for patch_detector
|
||||||
|
working-directory: ./patch_detector
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Lint patch_detector
|
||||||
|
working-directory: ./patch_detector
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Check formatting for patch_detector
|
||||||
|
working-directory: ./patch_detector
|
||||||
|
run: npm run format:check
|
||||||
|
|
||||||
build-and-push-images:
|
build-and-push-images:
|
||||||
|
needs: lint-and-format
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
11
frontend/.prettierrc
Normal file
11
frontend/.prettierrc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"vueIndentScriptAndStyle": false
|
||||||
|
}
|
||||||
@@ -1,2 +1 @@
|
|||||||
# BuildPath - Nuxt
|
# BuildPath - Nuxt
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ useSeoMeta({
|
|||||||
title: 'BuildPath',
|
title: 'BuildPath',
|
||||||
ogTitle: 'BuildPath',
|
ogTitle: 'BuildPath',
|
||||||
description: 'BuildPath: a tool for League of Legends champions runes and build paths.',
|
description: 'BuildPath: a tool for League of Legends champions runes and build paths.',
|
||||||
ogDescription: 'BuildPath: a tool for League of Legends champions runes and build paths.',
|
ogDescription: 'BuildPath: a tool for League of Legends champions runes and build paths.'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-surface: #312E2C;
|
--color-surface: #312e2c;
|
||||||
--color-on-surface: #B7B8E1;
|
--color-on-surface: #b7b8e1;
|
||||||
--color-surface-darker: #1f1d1c;
|
--color-surface-darker: #1f1d1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Font setting */
|
/* Font setting */
|
||||||
h1,h2,h3,h4,h5,h6,p,a,input[type=text] {
|
h1,
|
||||||
font-family: "Inter", sans-serif;
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
a,
|
||||||
|
input[type='text'] {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
font-optical-sizing: auto;
|
font-optical-sizing: auto;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -17,7 +25,14 @@ h1,h2,h3,h4,h5,h6,p,a,input[type=text] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Default margins to none */
|
/* Default margins to none */
|
||||||
h1,h2,h3,h4,h5,h6,p,a {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
a {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,116 +1,130 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { debounce, isEmpty } from '~/utils/helpers';
|
import { debounce, isEmpty } from '~/utils/helpers'
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const CDRAGON_CHAMPIONS_URL = CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json";
|
const CDRAGON_CHAMPIONS_URL =
|
||||||
const CHAMPIONS_API_URL = "/api/champions";
|
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
|
||||||
|
const CHAMPIONS_API_URL = '/api/champions'
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const { data: championsData, pending: loadingChampions, error: championsError } = useFetch(CDRAGON_CHAMPIONS_URL, {
|
const {
|
||||||
|
data: championsData,
|
||||||
|
pending: loadingChampions,
|
||||||
|
error: championsError
|
||||||
|
} = useFetch(CDRAGON_CHAMPIONS_URL, {
|
||||||
key: 'champions-data',
|
key: 'champions-data',
|
||||||
lazy: false,
|
lazy: false,
|
||||||
server: false // Disable server-side fetching to avoid hydration issues
|
server: false // Disable server-side fetching to avoid hydration issues
|
||||||
});
|
})
|
||||||
const { data: championsLanes, pending: loadingLanes, error: lanesError } = useFetch(CHAMPIONS_API_URL, {
|
const {
|
||||||
|
data: championsLanes,
|
||||||
|
pending: loadingLanes,
|
||||||
|
error: lanesError
|
||||||
|
} = useFetch(CHAMPIONS_API_URL, {
|
||||||
key: 'champions-lanes',
|
key: 'champions-lanes',
|
||||||
lazy: false,
|
lazy: false,
|
||||||
server: false // Disable server-side fetching to avoid hydration issues
|
server: false // Disable server-side fetching to avoid hydration issues
|
||||||
});
|
})
|
||||||
|
|
||||||
// Data processing
|
// Data processing
|
||||||
const champions = computed(() => {
|
const champions = computed(() => {
|
||||||
if (!championsData.value || !Array.isArray(championsData.value)) return [];
|
if (!championsData.value || !Array.isArray(championsData.value)) return []
|
||||||
|
|
||||||
return championsData.value.slice(1)
|
return championsData.value
|
||||||
.filter((champion: any) => !champion.name.includes("Doom Bot"))
|
.slice(1)
|
||||||
.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
.filter((champion: any) => !champion.name.includes('Doom Bot'))
|
||||||
});
|
.sort((a: any, b: any) => a.name.localeCompare(b.name))
|
||||||
|
})
|
||||||
|
|
||||||
const lanesMap = computed(() => {
|
const lanesMap = computed(() => {
|
||||||
const map = new Map<string, LaneData[]>();
|
const map = new Map<string, LaneData[]>()
|
||||||
if (championsLanes.value) {
|
if (championsLanes.value) {
|
||||||
for (const champion of championsLanes.value as ChampionData[]) {
|
for (const champion of championsLanes.value as ChampionData[]) {
|
||||||
map.set(champion.alias.toLowerCase(), champion.lanes);
|
map.set(champion.alias.toLowerCase(), champion.lanes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map
|
||||||
});
|
})
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const filteredChampions = ref<ChampionSummary[]>([]);
|
const filteredChampions = ref<ChampionSummary[]>([])
|
||||||
const searchText = ref("");
|
const searchText = ref('')
|
||||||
const searchBar = useTemplateRef("searchBar");
|
const searchBar = useTemplateRef('searchBar')
|
||||||
|
|
||||||
// Lane filtering
|
// Lane filtering
|
||||||
function filterToLane(filter: number): string {
|
function filterToLane(filter: number): string {
|
||||||
const laneMap = ["TOP", "JUNGLE", "MIDDLE", "BOTTOM", "UTILITY"];
|
const laneMap = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
|
||||||
return laneMap[filter] || "";
|
return laneMap[filter] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterChampionsByLane(laneFilter: number): void {
|
function filterChampionsByLane(laneFilter: number): void {
|
||||||
if (laneFilter === -1) {
|
if (laneFilter === -1) {
|
||||||
filteredChampions.value = [...champions.value];
|
filteredChampions.value = [...champions.value]
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const laneName = filterToLane(laneFilter);
|
const laneName = filterToLane(laneFilter)
|
||||||
filteredChampions.value = champions.value.filter((champion: any) => {
|
filteredChampions.value = champions.value.filter((champion: any) => {
|
||||||
const championLanes = lanesMap.value.get(champion.alias.toLowerCase());
|
const championLanes = lanesMap.value.get(champion.alias.toLowerCase())
|
||||||
if (!championLanes) return false;
|
if (!championLanes) return false
|
||||||
|
|
||||||
return championLanes.some(lane => lane.data === laneName);
|
return championLanes.some(lane => lane.data === laneName)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search functionality
|
// Search functionality
|
||||||
const debouncedSearch = debounce((searchTerm: string) => {
|
const debouncedSearch = debounce((searchTerm: string) => {
|
||||||
if (isEmpty(searchTerm)) {
|
if (isEmpty(searchTerm)) {
|
||||||
filteredChampions.value = [...champions.value];
|
filteredChampions.value = [...champions.value]
|
||||||
} else {
|
} else {
|
||||||
filteredChampions.value = champions.value.filter((champion: any) =>
|
filteredChampions.value = champions.value.filter((champion: any) =>
|
||||||
champion.name.toLowerCase().includes(searchTerm.toLowerCase())
|
champion.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300)
|
||||||
|
|
||||||
// Watchers
|
// Watchers
|
||||||
watch(searchBar, (newS, oldS) => {
|
watch(searchBar, (newS, oldS) => {
|
||||||
searchBar.value?.focus();
|
searchBar.value?.focus()
|
||||||
});
|
})
|
||||||
|
|
||||||
watch(searchText, (newTerm) => {
|
watch(searchText, newTerm => {
|
||||||
debouncedSearch(newTerm);
|
debouncedSearch(newTerm)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Watch for changes in champions data and update filtered champions
|
// Watch for changes in champions data and update filtered champions
|
||||||
watch(champions, (newChampions) => {
|
watch(
|
||||||
filteredChampions.value = [...newChampions];
|
champions,
|
||||||
}, { immediate: true });
|
newChampions => {
|
||||||
|
filteredChampions.value = [...newChampions]
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
async function navigateToChampion(championAlias: string): Promise<void> {
|
async function navigateToChampion(championAlias: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await navigateTo(`/champion/${championAlias.toLowerCase()}`);
|
await navigateTo(`/champion/${championAlias.toLowerCase()}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Navigation error:', error);
|
console.error('Navigation error:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize filtered champions
|
// Initialize filtered champions
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
filteredChampions.value = [...champions.value];
|
filteredChampions.value = [...champions.value]
|
||||||
});
|
})
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
const hasErrors = computed(() => championsError.value || lanesError.value);
|
const hasErrors = computed(() => championsError.value || lanesError.value)
|
||||||
const isLoading = computed(() => loadingChampions.value || loadingLanes.value);
|
const isLoading = computed(() => loadingChampions.value || loadingLanes.value)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Loading state -->
|
<!-- Loading state -->
|
||||||
<div v-if="isLoading" class="loading-state">
|
<div v-if="isLoading" class="loading-state">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"/>
|
||||||
<p>Loading champions...</p>
|
<p>Loading champions...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -122,18 +136,17 @@ const isLoading = computed(() => loadingChampions.value || loadingLanes.value);
|
|||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="search-lanefilter-container">
|
<div class="search-lanefilter-container">
|
||||||
<LaneFilter
|
<LaneFilter id="cs-lanefilter" @filter-change="filterChampionsByLane" />
|
||||||
id="cs-lanefilter"
|
|
||||||
@filter-change="filterChampionsByLane"
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
@keyup.enter="() => filteredChampions.length > 0 && navigateToChampion(filteredChampions[0].alias)"
|
|
||||||
v-model="searchText"
|
|
||||||
ref="searchBar"
|
ref="searchBar"
|
||||||
|
v-model="searchText"
|
||||||
class="search-bar"
|
class="search-bar"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search a champion"
|
placeholder="Search a champion"
|
||||||
/>
|
@keyup.enter="
|
||||||
|
() => filteredChampions.length > 0 && navigateToChampion(filteredChampions[0].alias)
|
||||||
|
"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
@@ -146,7 +159,7 @@ const isLoading = computed(() => loadingChampions.value || loadingLanes.value);
|
|||||||
v-for="champion in filteredChampions"
|
v-for="champion in filteredChampions"
|
||||||
:key="champion.id"
|
:key="champion.id"
|
||||||
:to="'/champion/' + champion.alias.toLowerCase()"
|
:to="'/champion/' + champion.alias.toLowerCase()"
|
||||||
style="width: fit-content; height: fit-content;"
|
style="width: fit-content; height: fit-content"
|
||||||
>
|
>
|
||||||
<div class="cs-champion-img-container">
|
<div class="cs-champion-img-container">
|
||||||
<NuxtImg
|
<NuxtImg
|
||||||
@@ -164,7 +177,9 @@ const isLoading = computed(() => loadingChampions.value || loadingLanes.value);
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Loading and error states */
|
/* Loading and error states */
|
||||||
.loading-state, .error-state, .empty-state {
|
.loading-state,
|
||||||
|
.error-state,
|
||||||
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
color: var(--color-on-surface);
|
color: var(--color-on-surface);
|
||||||
@@ -188,8 +203,12 @@ const isLoading = computed(() => loadingChampions.value || loadingLanes.value);
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-state p {
|
.loading-state p {
|
||||||
@@ -198,7 +217,8 @@ const isLoading = computed(() => loadingChampions.value || loadingLanes.value);
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@@ -257,7 +277,9 @@ const isLoading = computed(() => loadingChampions.value || loadingLanes.value);
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.cs-champion-img-container {
|
.cs-champion-img-container {
|
||||||
overflow: hidden; width: 120px; height: 120px;
|
overflow: hidden;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
border: 1px solid var(--color-surface);
|
border: 1px solid var(--color-surface);
|
||||||
}
|
}
|
||||||
.cs-champion-img-container:hover {
|
.cs-champion-img-container:hover {
|
||||||
|
|||||||
@@ -1,26 +1,50 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
championId: number,
|
championId: number
|
||||||
winrate: number,
|
winrate: number
|
||||||
pickrate: number,
|
pickrate: number
|
||||||
gameCount: number
|
gameCount: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const winrate = ref((props.winrate * 100).toFixed(2))
|
const winrate = ref((props.winrate * 100).toFixed(2))
|
||||||
watch(() => props.winrate, () => {winrate.value = (props.winrate * 100).toFixed(2)})
|
watch(
|
||||||
|
() => props.winrate,
|
||||||
|
() => {
|
||||||
|
winrate.value = (props.winrate * 100).toFixed(2)
|
||||||
|
}
|
||||||
|
)
|
||||||
const pickrate = ref((props.pickrate * 100).toFixed(2))
|
const pickrate = ref((props.pickrate * 100).toFixed(2))
|
||||||
watch(() => props.pickrate, () => {pickrate.value = (props.pickrate * 100).toFixed(2)})
|
watch(
|
||||||
|
() => props.pickrate,
|
||||||
|
() => {
|
||||||
|
pickrate.value = (props.pickrate * 100).toFixed(2)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const { data: championData } : ChampionResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champions/" + props.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 championName = championData.value.name
|
||||||
const championDescription = championData.value.title
|
const championDescription = championData.value.title
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="display: flex; width: fit-content;">
|
<div style="display: flex; width: fit-content">
|
||||||
|
|
||||||
<div class="champion-title-img-container">
|
<div class="champion-title-img-container">
|
||||||
<NuxtImg width="160" height="160" class="champion-title-img" :src="CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/' + championId + '.png'"/>
|
<NuxtImg
|
||||||
|
width="160"
|
||||||
|
height="160"
|
||||||
|
class="champion-title-img"
|
||||||
|
:src="
|
||||||
|
CDRAGON_BASE +
|
||||||
|
'plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/' +
|
||||||
|
championId +
|
||||||
|
'.png'
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="ct-info-container">
|
<div id="ct-info-container">
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LANE_IMAGES, LANE_IMAGES_HOVER, LANE_IMAGES_SELECTED, POSITIONS_STR } from '~/utils/cdragon';
|
import {
|
||||||
|
LANE_IMAGES,
|
||||||
|
LANE_IMAGES_HOVER,
|
||||||
|
LANE_IMAGES_SELECTED,
|
||||||
|
POSITIONS_STR
|
||||||
|
} from '~/utils/cdragon'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
filterChange: [value: number]
|
filterChange: [value: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const laneImgs = Array(5).fill(ref("")).map((_, index) => ref(LANE_IMAGES[index]))
|
const laneImgs = Array(5)
|
||||||
|
.fill(ref(''))
|
||||||
|
.map((_, index) => ref(LANE_IMAGES[index]))
|
||||||
const laneFilter = ref(-1)
|
const laneFilter = ref(-1)
|
||||||
|
|
||||||
function selectLaneFilter(index: number) {
|
function selectLaneFilter(index: number) {
|
||||||
@@ -15,9 +22,9 @@ function selectLaneFilter(index: number) {
|
|||||||
|
|
||||||
// This is a deselection.
|
// This is a deselection.
|
||||||
if (laneFilter.value == index) {
|
if (laneFilter.value == index) {
|
||||||
laneFilter.value = -1;
|
laneFilter.value = -1
|
||||||
emit('filterChange', laneFilter.value)
|
emit('filterChange', laneFilter.value)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,24 +35,25 @@ function selectLaneFilter(index: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseOut(laneImg: Ref<string>, index: number) {
|
function handleMouseOut(laneImg: Ref<string>, index: number) {
|
||||||
if(laneImg.value == LANE_IMAGES_HOVER[index])
|
if (laneImg.value == LANE_IMAGES_HOVER[index]) laneImg.value = LANE_IMAGES[index]
|
||||||
laneImg.value = LANE_IMAGES[index]
|
|
||||||
}
|
}
|
||||||
function handleHover(laneImg: Ref<string>, index: number) {
|
function handleHover(laneImg: Ref<string>, index: number) {
|
||||||
if(laneImg.value == LANE_IMAGES[index])
|
if (laneImg.value == LANE_IMAGES[index]) laneImg.value = LANE_IMAGES_HOVER[index]
|
||||||
laneImg.value = LANE_IMAGES_HOVER[index]
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="width: fit-content;">
|
<div style="width: fit-content">
|
||||||
<NuxtImg v-for="(laneImg, index) in laneImgs"
|
<NuxtImg
|
||||||
|
v-for="(laneImg, index) in laneImgs"
|
||||||
format="webp"
|
format="webp"
|
||||||
:alt="POSITIONS_STR[index]"
|
:alt="POSITIONS_STR[index]"
|
||||||
class="lane-img" :src="laneImg.value"
|
class="lane-img"
|
||||||
|
:src="laneImg.value"
|
||||||
@mouseout="handleMouseOut(laneImg, index)"
|
@mouseout="handleMouseOut(laneImg, index)"
|
||||||
@mouseover="handleHover(laneImg, index)"
|
@mouseover="handleHover(laneImg, index)"
|
||||||
@click="selectLaneFilter(index)"/>
|
@click="selectLaneFilter(index)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
defineProps<{
|
||||||
imgWidth?: String,
|
imgWidth?: string
|
||||||
fontSize?: String
|
fontSize?: string
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="width: fit-content; max-width: 100%; overflow: hidden;">
|
<div style="width: fit-content; max-width: 100%; overflow: hidden">
|
||||||
<NuxtLink style="display: flex; width: fit-content; text-decoration: none;" to="/">
|
<NuxtLink style="display: flex; width: fit-content; text-decoration: none" to="/">
|
||||||
<NuxtImg id="logo-img" alt="BuildPath"
|
<NuxtImg
|
||||||
|
id="logo-img"
|
||||||
|
alt="BuildPath"
|
||||||
format="webp"
|
format="webp"
|
||||||
:width="imgWidth == null ? '120' : Number(imgWidth)"
|
:width="imgWidth == null ? '120' : Number(imgWidth)"
|
||||||
src="/buildpath-high-resolution-logo-transparent.png" />
|
src="/buildpath-high-resolution-logo-transparent.png"
|
||||||
<h1 :style="'font-size: ' + (fontSize == null ? '5.0rem' : fontSize) + ';'" id="logo-text">BuildPath</h1>
|
/>
|
||||||
|
<h1 id="logo-text" :style="'font-size: ' + (fontSize == null ? '5.0rem' : fontSize) + ';'">
|
||||||
|
BuildPath
|
||||||
|
</h1>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string,
|
title: string
|
||||||
id: number,
|
id: number
|
||||||
winrate: number,
|
winrate: number
|
||||||
pickrate: number,
|
pickrate: number
|
||||||
gameCount: number
|
gameCount: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -23,12 +22,22 @@ defineProps<{
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-[200px]">
|
<div class="text-[200px]">
|
||||||
<!-- Champion image -->
|
<!-- Champion image -->
|
||||||
<div class="my-auto ml-10"
|
<div
|
||||||
style="overflow: hidden; width: 220px; height: 220px; border: 1px solid #B7B8E1;">
|
class="my-auto ml-10"
|
||||||
<NuxtImg width="216px" height="216px"
|
style="overflow: hidden; width: 220px; height: 220px; border: 1px solid #b7b8e1"
|
||||||
|
>
|
||||||
|
<NuxtImg
|
||||||
|
width="216px"
|
||||||
|
height="216px"
|
||||||
class="object-cover"
|
class="object-cover"
|
||||||
style="transform: translate(4px, 4px) scale(1.2, 1.2); width: 216px; height: 216px;"
|
style="transform: translate(4px, 4px) scale(1.2, 1.2); width: 216px; height: 216px"
|
||||||
:src="CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/' + id + '.png'" />
|
:src="
|
||||||
|
CDRAGON_BASE +
|
||||||
|
'plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/' +
|
||||||
|
id +
|
||||||
|
'.png'
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,27 +47,21 @@ defineProps<{
|
|||||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#888888" d="m4.67 28l6.39-12l7.3 6.49a2 2 0 0 0 1.7.47a2 2 0 0 0 1.42-1.07L27 10.9l-1.82-.9l-5.49 11l-7.3-6.49a2 2 0 0 0-1.68-.51a2 2 0 0 0-1.42 1L4 25V2H2v26a2 2 0 0 0 2 2h26v-2Z" /></svg> -->
|
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#888888" d="m4.67 28l6.39-12l7.3 6.49a2 2 0 0 0 1.7.47a2 2 0 0 0 1.42-1.07L27 10.9l-1.82-.9l-5.49 11l-7.3-6.49a2 2 0 0 0-1.68-.51a2 2 0 0 0-1.42 1L4 25V2H2v26a2 2 0 0 0 2 2h26v-2Z" /></svg> -->
|
||||||
<div class="pl-2">
|
<div class="pl-2">
|
||||||
<div class="text-[#B7B8E1]">{{ (winrate * 100).toFixed(2) }}%</div>
|
<div class="text-[#B7B8E1]">{{ (winrate * 100).toFixed(2) }}%</div>
|
||||||
<div class="text-lg text-[#B7B8E1]">
|
<div class="text-lg text-[#B7B8E1]">Winrate</div>
|
||||||
Winrate
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row pr-10">
|
<div class="flex flex-row pr-10">
|
||||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#888888" d="m16 6.52l2.76 5.58l.46 1l1 .15l6.16.89l-4.38 4.3l-.75.73l.18 1l1.05 6.13l-5.51-2.89L16 23l-.93.49l-5.51 2.85l1-6.13l.18-1l-.74-.77l-4.42-4.35l6.16-.89l1-.15l.46-1L16 6.52M16 2l-4.55 9.22l-10.17 1.47l7.36 7.18L6.9 30l9.1-4.78L25.1 30l-1.74-10.13l7.36-7.17l-10.17-1.48Z" /></svg> -->
|
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#888888" d="m16 6.52l2.76 5.58l.46 1l1 .15l6.16.89l-4.38 4.3l-.75.73l.18 1l1.05 6.13l-5.51-2.89L16 23l-.93.49l-5.51 2.85l1-6.13l.18-1l-.74-.77l-4.42-4.35l6.16-.89l1-.15l.46-1L16 6.52M16 2l-4.55 9.22l-10.17 1.47l7.36 7.18L6.9 30l9.1-4.78L25.1 30l-1.74-10.13l7.36-7.17l-10.17-1.48Z" /></svg> -->
|
||||||
<div class="pl-2">
|
<div class="pl-2">
|
||||||
<div class="text-[#B7B8E1]">{{ (pickrate * 100).toFixed(2) }}%</div>
|
<div class="text-[#B7B8E1]">{{ (pickrate * 100).toFixed(2) }}%</div>
|
||||||
<div class="text-lg text-[#B7B8E1]">
|
<div class="text-lg text-[#B7B8E1]">Pickrate</div>
|
||||||
Pickrate
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row pr-10">
|
<div class="flex flex-row pr-10">
|
||||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#888888" d="M22.45 6a5.47 5.47 0 0 1 3.91 1.64a5.7 5.7 0 0 1 0 8L16 26.13L5.64 15.64a5.7 5.7 0 0 1 0-8a5.48 5.48 0 0 1 7.82 0l2.54 2.6l2.53-2.58A5.44 5.44 0 0 1 22.45 6m0-2a7.47 7.47 0 0 0-5.34 2.24L16 7.36l-1.11-1.12a7.49 7.49 0 0 0-10.68 0a7.72 7.72 0 0 0 0 10.82L16 29l11.79-11.94a7.72 7.72 0 0 0 0-10.82A7.49 7.49 0 0 0 22.45 4Z" /></svg> -->
|
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#888888" d="M22.45 6a5.47 5.47 0 0 1 3.91 1.64a5.7 5.7 0 0 1 0 8L16 26.13L5.64 15.64a5.7 5.7 0 0 1 0-8a5.48 5.48 0 0 1 7.82 0l2.54 2.6l2.53-2.58A5.44 5.44 0 0 1 22.45 6m0-2a7.47 7.47 0 0 0-5.34 2.24L16 7.36l-1.11-1.12a7.49 7.49 0 0 0-10.68 0a7.72 7.72 0 0 0 0 10.82L16 29l11.79-11.94a7.72 7.72 0 0 0 0-10.82A7.49 7.49 0 0 0 22.45 4Z" /></svg> -->
|
||||||
<div class="pl-2">
|
<div class="pl-2">
|
||||||
<div class="text-[#B7B8E1]">{{ gameCount }}</div>
|
<div class="text-[#B7B8E1]">{{ gameCount }}</div>
|
||||||
<div class="text-lg text-[#B7B8E1]">
|
<div class="text-lg text-[#B7B8E1]">Games</div>
|
||||||
Games
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,4 +89,3 @@ defineProps<{
|
|||||||
<div class="absolute bottom-0 w-full h-8 bg-[#B7B8E1]" />
|
<div class="absolute bottom-0 w-full h-8 bg-[#B7B8E1]" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string,
|
title: string
|
||||||
bootsFirst?: number,
|
bootsFirst?: number
|
||||||
sizePerc?: number
|
sizePerc?: number
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :style="(sizePerc != undefined && sizePerc != null) ? 'max-height: ' + (sizePerc * 600) + 'px;' : ''" class="item-box">
|
<div
|
||||||
<div style="display:flex; flex-direction: column; justify-content: center; align-items: center;">
|
: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>
|
<h2 class="item-box-title">{{ title }}</h2>
|
||||||
<h5 v-if="bootsFirst != undefined && bootsFirst != null"
|
<h5 v-if="bootsFirst != undefined && bootsFirst != null" style="margin: auto">
|
||||||
style="margin: auto;">({{ (bootsFirst * 100).toFixed(2) }}%)</h5>
|
({{ (bootsFirst * 100).toFixed(2) }}%)
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import svgdomarrows from 'svg-dom-arrows'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
tree: ItemTree,
|
tree: ItemTree
|
||||||
parentCount?: number
|
parentCount?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
mount: [end: Element],
|
mount: [end: Element]
|
||||||
refresh: []
|
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())
|
const itemMap = reactive(new Map())
|
||||||
for(let item of items.value) {
|
for (const item of items.value) {
|
||||||
itemMap.set(item.id, item)
|
itemMap.set(item.id, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
import svgdomarrows from 'svg-dom-arrows';
|
const start: Ref<Element | null> = useTemplateRef('start')
|
||||||
|
|
||||||
const start : Ref<Element | null> = useTemplateRef("start")
|
|
||||||
const arrows: Array<svgdomarrows.LinePath> = []
|
const arrows: Array<svgdomarrows.LinePath> = []
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshArrows()
|
refreshArrows()
|
||||||
emit('mount', start.value!!)
|
emit('mount', start.value!)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUpdate(() => {
|
onBeforeUpdate(() => {
|
||||||
for(let arrow of arrows) {
|
for (const arrow of arrows) {
|
||||||
arrow.release()
|
arrow.release()
|
||||||
}
|
}
|
||||||
arrows.splice(0, arrows.length)
|
arrows.splice(0, arrows.length)
|
||||||
@@ -34,18 +36,18 @@ onBeforeUpdate(() => {
|
|||||||
|
|
||||||
onUpdated(() => {
|
onUpdated(() => {
|
||||||
refreshArrows()
|
refreshArrows()
|
||||||
emit('mount', start.value!!)
|
emit('mount', start.value!)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
for(let arrow of arrows) {
|
for (const arrow of arrows) {
|
||||||
arrow.release()
|
arrow.release()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function drawArrow(start: Element, end: Element) {
|
function drawArrow(start: Element, end: Element) {
|
||||||
// console.log("drawArrow(", start, ", ", end, ")")
|
// console.log("drawArrow(", start, ", ", end, ")")
|
||||||
if(start == null || end == null) return;
|
if (start == null || end == null) return
|
||||||
|
|
||||||
const arrow = new svgdomarrows.LinePath({
|
const arrow = new svgdomarrows.LinePath({
|
||||||
start: {
|
start: {
|
||||||
@@ -69,21 +71,21 @@ function drawArrow(start : Element, end : Element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshArrows() {
|
function refreshArrows() {
|
||||||
for(let arrow of arrows) {
|
for (const arrow of arrows) {
|
||||||
arrow.redraw()
|
arrow.redraw()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redraw arrows on window resize
|
// Redraw arrows on window resize
|
||||||
addEventListener('resize', (_) => {
|
addEventListener('resize', _ => {
|
||||||
refreshArrows()
|
refreshArrows()
|
||||||
})
|
})
|
||||||
addEventListener("scroll", (_) => {
|
addEventListener('scroll', _ => {
|
||||||
refreshArrows()
|
refreshArrows()
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleSubtreeMount(end: Element) {
|
function handleSubtreeMount(end: Element) {
|
||||||
drawArrow(start.value!!, end)
|
drawArrow(start.value!, end)
|
||||||
refreshArrows()
|
refreshArrows()
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
@@ -94,18 +96,32 @@ function handleRefresh() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center">
|
||||||
|
<div
|
||||||
<div v-if="tree.data != undefined && tree.data != null" style="width: fit-content; height: fit-content;">
|
v-if="tree.data != undefined && tree.data != null"
|
||||||
<img ref="start" class="item-img" width="64" height="64"
|
style="width: fit-content; height: fit-content"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
ref="start"
|
||||||
|
class="item-img"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
:alt="tree.data.toString()"
|
:alt="tree.data.toString()"
|
||||||
:src="CDRAGON_BASE + mapPath(itemMap.get(tree.data).iconPath)" />
|
: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>
|
>
|
||||||
|
<h3 style="width: fit-content; margin: auto; margin-bottom: 10px">
|
||||||
|
{{ ((tree.count / parentCount!!) * 100).toFixed(0) }}%
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-left: 30px;">
|
<div style="margin-left: 30px">
|
||||||
<div style="width: fit-content; height: fit-content;" v-for="child in tree.children">
|
<div v-for="child in tree.children" style="width: fit-content; height: fit-content">
|
||||||
<ItemTree @refresh="handleRefresh" @mount="(end) => handleSubtreeMount(end)" :tree="child" :parent-count="tree.count" />
|
<ItemTree
|
||||||
|
:tree="child"
|
||||||
|
:parent-count="tree.count"
|
||||||
|
@refresh="handleRefresh"
|
||||||
|
@mount="end => handleSubtreeMount(end)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,64 +1,72 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { isEmpty, deepClone } from '~/utils/helpers';
|
import { isEmpty, deepClone } from '~/utils/helpers'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
builds: Builds;
|
builds: Builds
|
||||||
loading?: boolean;
|
loading?: boolean
|
||||||
error?: boolean;
|
error?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
// Constants
|
// 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
|
// State
|
||||||
const { data: items, pending: loadingItems, error: itemsError } = await useFetch(ITEMS_API_URL);
|
const { data: items, pending: loadingItems, error: itemsError } = await useFetch(ITEMS_API_URL)
|
||||||
const itemMap = ref<Map<number, any>>(new Map());
|
const itemMap = ref<Map<number, any>>(new Map())
|
||||||
|
|
||||||
// Initialize item map
|
// Initialize item map
|
||||||
watch(items, (newItems) => {
|
watch(
|
||||||
|
items,
|
||||||
|
newItems => {
|
||||||
try {
|
try {
|
||||||
const itemsData = newItems || [];
|
const itemsData = newItems || []
|
||||||
if (Array.isArray(itemsData)) {
|
if (Array.isArray(itemsData)) {
|
||||||
const map = new Map<number, any>();
|
const map = new Map<number, any>()
|
||||||
for (const item of itemsData) {
|
for (const item of itemsData) {
|
||||||
if (item?.id) {
|
if (item?.id) {
|
||||||
map.set(item.id, item);
|
map.set(item.id, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemMap.value = map;
|
itemMap.value = map
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing item map:', error);
|
console.error('Error initializing item map:', error)
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
// Builds management
|
// Builds management
|
||||||
const builds = ref<Builds>(deepClone(props.builds));
|
const builds = ref<Builds>(deepClone(props.builds))
|
||||||
|
|
||||||
watch(() => props.builds, (newBuilds) => {
|
watch(
|
||||||
builds.value = deepClone(newBuilds);
|
() => props.builds,
|
||||||
trimBuilds(builds.value);
|
newBuilds => {
|
||||||
trimLateGameItems(builds.value);
|
builds.value = deepClone(newBuilds)
|
||||||
}, { deep: true });
|
trimBuilds(builds.value)
|
||||||
|
trimLateGameItems(builds.value)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
// Initialize with trimmed builds
|
// Initialize with trimmed builds
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
trimBuilds(builds.value);
|
trimBuilds(builds.value)
|
||||||
trimLateGameItems(builds.value);
|
trimLateGameItems(builds.value)
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trim builds tree to show only primary build paths
|
* Trim builds tree to show only primary build paths
|
||||||
*/
|
*/
|
||||||
function trimBuilds(builds: Builds): void {
|
function trimBuilds(builds: Builds): void {
|
||||||
if (!builds?.tree?.children) return;
|
if (!builds?.tree?.children) return
|
||||||
|
|
||||||
// Keep only the first child (primary build path)
|
// 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
|
// For the primary path, keep only the first child of the first child
|
||||||
if (builds.tree.children[0]?.children) {
|
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,95 +74,122 @@ function trimBuilds(builds: Builds): void {
|
|||||||
* Remove items from lateGame that are already in the build tree
|
* Remove items from lateGame that are already in the build tree
|
||||||
*/
|
*/
|
||||||
function trimLateGameItems(builds: Builds): void {
|
function trimLateGameItems(builds: Builds): void {
|
||||||
if (!builds?.tree || isEmpty(builds.lateGame)) return;
|
if (!builds?.tree || isEmpty(builds.lateGame)) return
|
||||||
|
|
||||||
function trimLateGameItemsFromTree(tree: ItemTree): void {
|
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) {
|
if (foundIndex !== -1) {
|
||||||
builds.lateGame.splice(foundIndex, 1);
|
builds.lateGame.splice(foundIndex, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const child of tree.children || []) {
|
for (const child of tree.children || []) {
|
||||||
trimLateGameItemsFromTree(child);
|
trimLateGameItemsFromTree(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trimLateGameItemsFromTree(builds.tree);
|
trimLateGameItemsFromTree(builds.tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get item data safely
|
* Get item data safely
|
||||||
*/
|
*/
|
||||||
function getItemData(itemId: number): any {
|
function getItemData(itemId: number): any {
|
||||||
return itemMap.value.get(itemId) || { iconPath: '' };
|
return itemMap.value.get(itemId) || { iconPath: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate percentage for item display
|
* Calculate percentage for item display
|
||||||
*/
|
*/
|
||||||
function getItemPercentage(item: { count: number }, total: number): string {
|
function getItemPercentage(item: { count: number }, total: number): string {
|
||||||
if (total <= 0) return '0%';
|
if (total <= 0) return '0%'
|
||||||
return ((item.count / total) * 100).toFixed(0) + '%';
|
return ((item.count / total) * 100).toFixed(0) + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error and loading states
|
// Error and loading states
|
||||||
const hasError = computed(() => itemsError.value || props.error);
|
const hasError = computed(() => itemsError.value || props.error)
|
||||||
const isLoading = computed(() => loadingItems.value || props.loading);
|
const isLoading = computed(() => loadingItems.value || props.loading)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="iv-container">
|
<div id="iv-container">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<!-- Start items -->
|
<!-- Start items -->
|
||||||
<ItemBox title="start" v-if="builds.suppItems == undefined || builds.suppItems == null">
|
<ItemBox v-if="builds.suppItems == undefined || builds.suppItems == null" title="start">
|
||||||
<div class="iv-items-container">
|
<div class="iv-items-container">
|
||||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.start" >
|
<div v-for="item in builds.start" style="margin-left: 5px; margin-right: 5px">
|
||||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
<NuxtImg
|
||||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
v-if="item.data != null && item.data != undefined"
|
||||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
class="item-img"
|
||||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
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>
|
</div>
|
||||||
</ItemBox>
|
</ItemBox>
|
||||||
<!-- Supp items -->
|
<!-- Supp items -->
|
||||||
<ItemBox v-if="builds.suppItems != undefined && builds.suppItems != null" title="supp">
|
<ItemBox v-if="builds.suppItems != undefined && builds.suppItems != null" title="supp">
|
||||||
<div class="iv-items-container">
|
<div class="iv-items-container">
|
||||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.suppItems" >
|
<div v-for="item in builds.suppItems" style="margin-left: 5px; margin-right: 5px">
|
||||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
<NuxtImg
|
||||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
v-if="item.data != null && item.data != undefined"
|
||||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
class="item-img"
|
||||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
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>
|
</div>
|
||||||
</ItemBox>
|
</ItemBox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Boots first : when champion rush boots -->
|
<!-- Boots first : when champion rush boots -->
|
||||||
<ItemBox v-if="builds.bootsFirst > 0.5" title="boots rush" :bootsFirst="builds.bootsFirst">
|
<ItemBox v-if="builds.bootsFirst > 0.5" title="boots rush" :boots-first="builds.bootsFirst">
|
||||||
<div class="iv-items-container">
|
<div class="iv-items-container">
|
||||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.boots" >
|
<div v-for="item in builds.boots" style="margin-left: 5px; margin-right: 5px">
|
||||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
<NuxtImg
|
||||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
v-if="item.data != null && item.data != undefined"
|
||||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
class="item-img"
|
||||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
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>
|
</div>
|
||||||
</ItemBox>
|
</ItemBox>
|
||||||
|
|
||||||
<!-- Core items -->
|
<!-- Core items -->
|
||||||
<ItemBox title="core">
|
<ItemBox title="core">
|
||||||
<ItemTree style="margin:auto; width: fit-content;" :tree="builds.tree" />
|
<ItemTree style="margin: auto; width: fit-content" :tree="builds.tree" />
|
||||||
</ItemBox>
|
</ItemBox>
|
||||||
|
|
||||||
<!-- Boots -->
|
<!-- Boots -->
|
||||||
<ItemBox v-if="builds.bootsFirst <= 0.5" title="boots">
|
<ItemBox v-if="builds.bootsFirst <= 0.5" title="boots">
|
||||||
<div class="iv-items-container">
|
<div class="iv-items-container">
|
||||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.boots.slice(0, 4)" >
|
<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"
|
<NuxtImg
|
||||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
v-if="item.data != null && item.data != undefined"
|
||||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data).iconPath)" />
|
class="item-img"
|
||||||
<h3 style="width: fit-content; margin:auto; margin-bottom: 10px;">{{ (item.count/builds.tree.count * 100).toFixed(0) }}%</h3>
|
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>
|
</div>
|
||||||
</ItemBox>
|
</ItemBox>
|
||||||
@@ -162,25 +197,43 @@ const isLoading = computed(() => loadingItems.value || props.loading);
|
|||||||
<!-- Late game items -->
|
<!-- Late game items -->
|
||||||
<ItemBox title="late game">
|
<ItemBox title="late game">
|
||||||
<div id="iv-late-game-container">
|
<div id="iv-late-game-container">
|
||||||
|
|
||||||
<div class="iv-items-container">
|
<div class="iv-items-container">
|
||||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.lateGame.slice(0, 4)" >
|
<div
|
||||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
v-for="item in builds.lateGame.slice(0, 4)"
|
||||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
style="margin-left: 5px; margin-right: 5px"
|
||||||
: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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="iv-items-container" v-if="builds.lateGame.length > 4">
|
<div v-if="builds.lateGame.length > 4" class="iv-items-container">
|
||||||
<div style="margin-left: 5px; margin-right: 5px;" v-for="item in builds.lateGame.slice(4, 8)" >
|
<div
|
||||||
<NuxtImg v-if="item.data != null && item.data != undefined"
|
v-for="item in builds.lateGame.slice(4, 8)"
|
||||||
class="item-img" width="64" height="64" :alt="item.data.toString()"
|
style="margin-left: 5px; margin-right: 5px"
|
||||||
: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>
|
<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>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</ItemBox>
|
</ItemBox>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,16 +7,19 @@ defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
stateChange: [state: string, lane: number]
|
stateChange: [state: string, lane: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LazyNavSideBar :champion-name="championName"
|
<LazyNavSideBar
|
||||||
|
:champion-name="championName"
|
||||||
:champion-lanes="championLanes"
|
:champion-lanes="championLanes"
|
||||||
:tierlist-list="tierlistList"
|
:tierlist-list="tierlistList"
|
||||||
@state-change="(s, l) => emit('stateChange', s, l)"/>
|
@state-change="(s, l) => emit('stateChange', s, l)"
|
||||||
<LazyNavBottomBar :champion-name="championName"
|
/>
|
||||||
|
<LazyNavBottomBar
|
||||||
|
:champion-name="championName"
|
||||||
:champion-lanes="championLanes"
|
:champion-lanes="championLanes"
|
||||||
:tierlist-list="tierlistList"
|
:tierlist-list="tierlistList"
|
||||||
@state-change="(s, l) => emit('stateChange', s, l)"/>
|
@state-change="(s, l) => emit('stateChange', s, l)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon';
|
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
championName?: string
|
championName?: string
|
||||||
@@ -10,18 +10,18 @@ const emit = defineEmits<{
|
|||||||
stateChange: [state: string, lane: number]
|
stateChange: [state: string, lane: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const state = ref("runes")
|
const state = ref('runes')
|
||||||
const laneState = ref(0)
|
const laneState = ref(0)
|
||||||
|
|
||||||
function handleStateChange(newState: string, newLane: number) {
|
function handleStateChange(newState: string, newLane: number) {
|
||||||
state.value = newState;
|
state.value = newState
|
||||||
laneState.value = newLane;
|
laneState.value = newLane
|
||||||
emit('stateChange', newState, newLane)
|
emit('stateChange', newState, newLane)
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const selected = ref("");
|
const selected = ref('')
|
||||||
if(route.path.startsWith("/tierlist/")) {
|
if (route.path.startsWith('/tierlist/')) {
|
||||||
const lane = route.params.lane as string
|
const lane = route.params.lane as string
|
||||||
selected.value = lane
|
selected.value = lane
|
||||||
}
|
}
|
||||||
@@ -29,40 +29,74 @@ if(route.path.startsWith("/tierlist/")) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="navbar-container">
|
<div class="navbar-container">
|
||||||
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
style="display: flex; width: fit-content; text-decoration: none; align-items: center; margin-left: 10px;"
|
style="
|
||||||
|
display: flex;
|
||||||
|
width: fit-content;
|
||||||
|
text-decoration: none;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
"
|
||||||
to="/"
|
to="/"
|
||||||
prefetch
|
prefetch
|
||||||
>
|
>
|
||||||
<NuxtImg format="webp" id="navbar-logo-img"
|
<NuxtImg
|
||||||
src="/buildpath-high-resolution-logo-transparent.png" />
|
id="navbar-logo-img"
|
||||||
|
format="webp"
|
||||||
|
src="/buildpath-high-resolution-logo-transparent.png"
|
||||||
|
/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<div v-for="(lane, i) in championLanes" style="display: flex; align-items: center; margin-left: 20px;">
|
<div
|
||||||
<NuxtImg format="webp" width="40" height="40"
|
v-for="(lane, i) in championLanes"
|
||||||
:src="LANE_IMAGES[lanePositionToIndex(lane.data)]" />
|
style="display: flex; align-items: center; margin-left: 20px"
|
||||||
|
>
|
||||||
|
<NuxtImg
|
||||||
|
format="webp"
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
:src="LANE_IMAGES[lanePositionToIndex(lane.data)]"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h2 :class="'navbar-link ' + (state == 'runes' && laneState == i ? 'navbar-link-selected' : '')"
|
<h2
|
||||||
@click="handleStateChange('runes', i)" >
|
:class="
|
||||||
|
'navbar-link ' + (state == 'runes' && laneState == i ? 'navbar-link-selected' : '')
|
||||||
|
"
|
||||||
|
@click="handleStateChange('runes', i)"
|
||||||
|
>
|
||||||
Runes
|
Runes
|
||||||
</h2>
|
</h2>
|
||||||
<h2 :class="'navbar-link ' + (state == 'items' && laneState == i ? 'navbar-link-selected' : '')"
|
<h2
|
||||||
@click="handleStateChange('items', i)" >
|
:class="
|
||||||
|
'navbar-link ' + (state == 'items' && laneState == i ? 'navbar-link-selected' : '')
|
||||||
|
"
|
||||||
|
@click="handleStateChange('items', i)"
|
||||||
|
>
|
||||||
Items
|
Items
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="tierlistList == true" style="padding-left: 20px;">
|
<div v-if="tierlistList == true" style="padding-left: 20px">
|
||||||
<h2 style="padding-left: 0px; font-size: 1.4rem; margin-top: 15px;">Tierlist</h2>
|
<h2 style="padding-left: 0px; font-size: 1.4rem; margin-top: 15px">Tierlist</h2>
|
||||||
<div style="display: flex;">
|
<div style="display: flex">
|
||||||
<NuxtLink style="margin-top: 5px; margin-bottom: 5px;" v-for="(pos, i) in POSITIONS" :to="'/tierlist/' + pos">
|
<NuxtLink
|
||||||
<div :class="selected == pos ? 'navbar-link-selected' : ''"
|
v-for="(pos, i) in POSITIONS"
|
||||||
class="navbar-link" style="display: flex; align-items: center;">
|
style="margin-top: 5px; margin-bottom: 5px"
|
||||||
<NuxtImg format="webp"
|
:to="'/tierlist/' + pos"
|
||||||
width="30" height="30"
|
>
|
||||||
:src="LANE_IMAGES[i]" :alt="POSITIONS_STR[i]" />
|
<div
|
||||||
|
:class="selected == pos ? 'navbar-link-selected' : ''"
|
||||||
|
class="navbar-link"
|
||||||
|
style="display: flex; align-items: center"
|
||||||
|
>
|
||||||
|
<NuxtImg
|
||||||
|
format="webp"
|
||||||
|
width="30"
|
||||||
|
height="30"
|
||||||
|
:src="LANE_IMAGES[i]"
|
||||||
|
:alt="POSITIONS_STR[i]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +112,7 @@ if(route.path.startsWith("/tierlist/")) {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
background-color: #2B2826;
|
background-color: #2b2826;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon';
|
import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
championName?: string
|
championName?: string
|
||||||
@@ -10,21 +10,22 @@ const emit = defineEmits<{
|
|||||||
stateChange: [state: string, lane: number]
|
stateChange: [state: string, lane: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const state = ref("runes")
|
const state = ref('runes')
|
||||||
const laneState = ref(0)
|
const laneState = ref(0)
|
||||||
|
|
||||||
function handleStateChange(newState: string, newLane: number) {
|
function handleStateChange(newState: string, newLane: number) {
|
||||||
state.value = newState;
|
state.value = newState
|
||||||
laneState.value = newLane;
|
laneState.value = newLane
|
||||||
emit('stateChange', newState, newLane)
|
emit('stateChange', newState, newLane)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {data: stats}: {data: Ref<{patch: number, count: number}>} = await useFetch("/api/stats")
|
const { data: stats }: { data: Ref<{ patch: number; count: number }> } =
|
||||||
|
await useFetch('/api/stats')
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const selected = ref("");
|
const selected = ref('')
|
||||||
if(route.path.startsWith("/tierlist/")) {
|
if (route.path.startsWith('/tierlist/')) {
|
||||||
const lane = route.params.lane as string
|
const lane = route.params.lane as string
|
||||||
selected.value = lane
|
selected.value = lane
|
||||||
}
|
}
|
||||||
@@ -32,62 +33,107 @@ if(route.path.startsWith("/tierlist/")) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- To make content have a 300px margin -->
|
<!-- To make content have a 300px margin -->
|
||||||
<div class="sidebar-margin"></div>
|
<div class="sidebar-margin"/>
|
||||||
|
|
||||||
<div class="sidebar-container">
|
<div class="sidebar-container">
|
||||||
<Logo font-size="2.6rem" img-width="60" style="padding-left: 15px; padding-right: 15px; margin-top: 30px;"/>
|
<Logo
|
||||||
|
font-size="2.6rem"
|
||||||
|
img-width="60"
|
||||||
|
style="padding-left: 15px; padding-right: 15px; margin-top: 30px"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-for="(lane, i) in championLanes">
|
<div v-for="(lane, i) in championLanes">
|
||||||
|
<div
|
||||||
<div style="display: flex; align-items: center; margin-top: 30px; padding-right: 10px; overflow: hidden;">
|
style="
|
||||||
<h1 style="font-size: 2.4rem; padding-left: 20px;">{{ championName }}</h1>
|
display: flex;
|
||||||
<NuxtImg format="webp" style="margin-left: 10px;"
|
align-items: center;
|
||||||
width="40" height="40"
|
margin-top: 30px;
|
||||||
:src="LANE_IMAGES[lanePositionToIndex(lane.data)]" />
|
padding-right: 10px;
|
||||||
<h2 v-if="championName != null && championName != undefined && championName.length < 8"
|
overflow: hidden;
|
||||||
style="margin-left: 5px; font-size: 1.8rem; font-weight: 200;">
|
"
|
||||||
|
>
|
||||||
|
<h1 style="font-size: 2.4rem; padding-left: 20px">{{ championName }}</h1>
|
||||||
|
<NuxtImg
|
||||||
|
format="webp"
|
||||||
|
style="margin-left: 10px"
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
:src="LANE_IMAGES[lanePositionToIndex(lane.data)]"
|
||||||
|
/>
|
||||||
|
<h2
|
||||||
|
v-if="championName != null && championName != undefined && championName.length < 8"
|
||||||
|
style="margin-left: 5px; font-size: 1.8rem; font-weight: 200"
|
||||||
|
>
|
||||||
{{ POSITIONS_STR[lanePositionToIndex(lane.data.toLowerCase())] }}
|
{{ POSITIONS_STR[lanePositionToIndex(lane.data.toLowerCase())] }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 :class="'sidebar-link ' + (state == 'runes' && laneState == i ? 'sidebar-link-selected' : '')"
|
<h2
|
||||||
|
:class="
|
||||||
|
'sidebar-link ' + (state == 'runes' && laneState == i ? 'sidebar-link-selected' : '')
|
||||||
|
"
|
||||||
|
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px"
|
||||||
@click="handleStateChange('runes', i)"
|
@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' : '')"
|
Runes
|
||||||
|
</h2>
|
||||||
|
<h2
|
||||||
|
:class="
|
||||||
|
'sidebar-link ' + (state == 'items' && laneState == i ? 'sidebar-link-selected' : '')
|
||||||
|
"
|
||||||
|
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px"
|
||||||
@click="handleStateChange('items', i)"
|
@click="handleStateChange('items', i)"
|
||||||
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px;">Items</h2>
|
>
|
||||||
|
Items
|
||||||
|
</h2>
|
||||||
|
|
||||||
<h2 :class="'sidebar-link ' + (state == 'alternatives' && laneState == i ? 'sidebar-link-selected' : '')"
|
<h2
|
||||||
|
:class="
|
||||||
|
'sidebar-link ' +
|
||||||
|
(state == 'alternatives' && laneState == i ? 'sidebar-link-selected' : '')
|
||||||
|
"
|
||||||
|
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px"
|
||||||
@click="handleStateChange('alternatives', i)"
|
@click="handleStateChange('alternatives', i)"
|
||||||
style="margin-top: 10px; font-size: 1.9rem; padding-left: 35px;">Alternatives</h2>
|
>
|
||||||
|
Alternatives
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="tierlistList == true" style="margin-top: 30px;">
|
<div v-if="tierlistList == true" style="margin-top: 30px">
|
||||||
<h2 style="padding-left: 20px; font-size: 2.4rem; margin-bottom: 10px;">Tierlist</h2>
|
<h2 style="padding-left: 20px; font-size: 2.4rem; margin-bottom: 10px">Tierlist</h2>
|
||||||
<NuxtLink style="margin-top: 5px; margin-bottom: 5px;" v-for="(pos, i) in POSITIONS" :to="'/tierlist/' + pos">
|
<NuxtLink
|
||||||
<div :class="selected == pos ? 'sidebar-link-selected' : ''"
|
v-for="(pos, i) in POSITIONS"
|
||||||
class="sidebar-link" style="padding-left: 35px; display: flex; align-items: center;">
|
style="margin-top: 5px; margin-bottom: 5px"
|
||||||
<NuxtImg format="webp"
|
:to="'/tierlist/' + pos"
|
||||||
width="40" height="40"
|
>
|
||||||
:src="LANE_IMAGES[i]" :alt="POSITIONS_STR[i]" />
|
<div
|
||||||
<h3 style="font-size: 2.1rem; font-weight: 200; margin-left: 10px;">{{ POSITIONS_STR[i] }}</h3>
|
:class="selected == pos ? 'sidebar-link-selected' : ''"
|
||||||
|
class="sidebar-link"
|
||||||
|
style="padding-left: 35px; display: flex; align-items: center"
|
||||||
|
>
|
||||||
|
<NuxtImg
|
||||||
|
format="webp"
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
:src="LANE_IMAGES[i]"
|
||||||
|
:alt="POSITIONS_STR[i]"
|
||||||
|
/>
|
||||||
|
<h3 style="font-size: 2.1rem; font-weight: 200; margin-left: 10px">
|
||||||
|
{{ POSITIONS_STR[i] }}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="position: absolute; bottom: 0; margin-bottom: 10px; padding-left: 10px;">
|
<div style="position: absolute; bottom: 0; margin-bottom: 10px; padding-left: 10px">
|
||||||
<h3 style="font-size: 23px; font-weight: 200;">
|
<h3 style="font-size: 23px; font-weight: 200">Patch {{ stats.patch }}</h3>
|
||||||
Patch {{ stats.patch }}
|
<h3 style="font-size: 23px; font-weight: 200">{{ stats.count }} games</h3>
|
||||||
</h3>
|
|
||||||
<h3 style="font-size: 23px; font-weight: 200;">
|
|
||||||
{{ stats.count }} games
|
|
||||||
</h3>
|
|
||||||
<NuxtLink to="/about"><h3>About</h3></NuxtLink>
|
<NuxtLink to="/about"><h3>About</h3></NuxtLink>
|
||||||
<h2 style="font-size: 12px; font-weight: 200; margin-top: 5px;">
|
<h2 style="font-size: 12px; font-weight: 200; margin-top: 5px">
|
||||||
BuildPath isn't endorsed by Riot Games and doesn't reflect the views or opinions of
|
BuildPath isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot
|
||||||
Riot Games or anyone officially involved in producing or managing Riot Games properties.
|
Games or anyone officially involved in producing or managing Riot Games properties. Riot
|
||||||
Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc.
|
Games, and all associated properties are trademarks or registered trademarks of Riot Games,
|
||||||
|
Inc.
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +141,7 @@ if(route.path.startsWith("/tierlist/")) {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
background-color: #2B2826;
|
background-color: #2b2826;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -5,55 +5,92 @@ const props = defineProps<{
|
|||||||
selectionIds: Array<number>
|
selectionIds: Array<number>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const primaryStyle : Ref<PerkStyle> = ref({id:0, name:"", iconPath:"", slots:[]})
|
const primaryStyle: Ref<PerkStyle> = ref({ id: 0, name: '', iconPath: '', slots: [] })
|
||||||
const secondaryStyle : Ref<PerkStyle> = ref({id:0, name:"", iconPath:"", slots:[]})
|
const secondaryStyle: Ref<PerkStyle> = ref({ id: 0, name: '', iconPath: '', slots: [] })
|
||||||
|
|
||||||
let { data: perks_data } : PerksResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perks.json")
|
const { data: perks_data }: PerksResponse = await useFetch(
|
||||||
|
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json'
|
||||||
|
)
|
||||||
const perks = reactive(new Map())
|
const perks = reactive(new Map())
|
||||||
for(let perk of perks_data.value) {
|
for (const perk of perks_data.value) {
|
||||||
perks.set(perk.id, perk)
|
perks.set(perk.id, perk)
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data: stylesData } : PerkStylesResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json")
|
const { data: stylesData }: PerkStylesResponse = await useFetch(
|
||||||
watch(() => props.primaryStyleId, async (newP, oldP) => {refreshStyles()})
|
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json'
|
||||||
watch(() => props.secondaryStyleId, async (newP, oldP) => {refreshStyles()})
|
)
|
||||||
|
watch(
|
||||||
|
() => props.primaryStyleId,
|
||||||
|
async (newP, oldP) => {
|
||||||
|
refreshStyles()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => props.secondaryStyleId,
|
||||||
|
async (newP, oldP) => {
|
||||||
|
refreshStyles()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function refreshStyles() {
|
function refreshStyles() {
|
||||||
for(let style of stylesData.value.styles) {
|
for (const style of stylesData.value.styles) {
|
||||||
if(style.id == (props.primaryStyleId)) {
|
if (style.id == props.primaryStyleId) {
|
||||||
primaryStyle.value = style
|
primaryStyle.value = style
|
||||||
}
|
}
|
||||||
if(style.id == (props.secondaryStyleId)) {
|
if (style.id == props.secondaryStyleId) {
|
||||||
secondaryStyle.value = style
|
secondaryStyle.value = style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshStyles()
|
refreshStyles()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="display: flex;">
|
<div style="display: flex">
|
||||||
<div class="rune-holder">
|
<div class="rune-holder">
|
||||||
<div class="rune-slot"><NuxtImg class="rune-style-img" style="margin: auto;" :src="CDRAGON_BASE + mapPath(primaryStyle.iconPath)" /></div>
|
<div class="rune-slot">
|
||||||
<div class="rune-slot" v-for="slot in primaryStyle.slots.slice(0, 1)">
|
<NuxtImg
|
||||||
<NuxtImg width="48" v-for="perk in slot.perks" :class="'rune-img rune-keystone ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
|
class="rune-style-img"
|
||||||
|
style="margin: auto"
|
||||||
|
:src="CDRAGON_BASE + mapPath(primaryStyle.iconPath)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="rune-slot" v-for="slot in primaryStyle.slots.slice(1, 4)">
|
<div v-for="slot in primaryStyle.slots.slice(0, 1)" class="rune-slot">
|
||||||
<NuxtImg width="48" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
|
<NuxtImg
|
||||||
|
v-for="perk in slot.perks"
|
||||||
|
width="48"
|
||||||
|
:class="
|
||||||
|
'rune-img rune-keystone ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')
|
||||||
|
"
|
||||||
|
:src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-for="slot in primaryStyle.slots.slice(1, 4)" class="rune-slot">
|
||||||
|
<NuxtImg
|
||||||
|
v-for="perk in slot.perks"
|
||||||
|
width="48"
|
||||||
|
:class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')"
|
||||||
|
:src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rune-spacer-bar"></div>
|
<div class="rune-spacer-bar"/>
|
||||||
<div class="rune-holder" style="align-content: end">
|
<div class="rune-holder" style="align-content: end">
|
||||||
<div class="rune-slot"><img style="margin: auto;" :src="CDRAGON_BASE + mapPath(secondaryStyle.iconPath)" /></div>
|
<div class="rune-slot">
|
||||||
<div class="rune-slot" v-for="slot in secondaryStyle.slots.slice(1, 4)">
|
<img style="margin: auto" :src="CDRAGON_BASE + mapPath(secondaryStyle.iconPath)" >
|
||||||
<NuxtImg width="48" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
|
</div>
|
||||||
|
<div v-for="slot in secondaryStyle.slots.slice(1, 4)" class="rune-slot">
|
||||||
|
<NuxtImg
|
||||||
|
v-for="perk in slot.perks"
|
||||||
|
width="48"
|
||||||
|
:class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')"
|
||||||
|
:src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="rune-slot mini" v-for="slot in primaryStyle.slots.slice(4, 7)">
|
<!-- <div class="rune-slot mini" v-for="slot in primaryStyle.slots.slice(4, 7)">
|
||||||
<img width="32" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
|
<img width="32" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
|
||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
runes: Array<{count: number,
|
runes: Array<{
|
||||||
primaryStyle: number,
|
count: number
|
||||||
secondaryStyle: number,
|
primaryStyle: number
|
||||||
selections: Array<number>,
|
secondaryStyle: number
|
||||||
pickrate: number}>
|
selections: Array<number>
|
||||||
|
pickrate: number
|
||||||
|
}>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const currentlySelectedPage = ref(0)
|
const currentlySelectedPage = ref(0)
|
||||||
@@ -12,28 +14,35 @@ const primaryStyles : Ref<Array<PerkStyle>> = ref(Array(props.runes.length))
|
|||||||
const secondaryStyles: 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 keystoneIds: Ref<Array<number>> = ref(Array(props.runes.length))
|
||||||
|
|
||||||
let { data: perks_data } : PerksResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perks.json")
|
const { data: perks_data }: PerksResponse = await useFetch(
|
||||||
|
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json'
|
||||||
|
)
|
||||||
const perks = reactive(new Map())
|
const perks = reactive(new Map())
|
||||||
for(let perk of perks_data.value) {
|
for (const perk of perks_data.value) {
|
||||||
perks.set(perk.id, perk)
|
perks.set(perk.id, perk)
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data: stylesData } : PerkStylesResponse = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json")
|
const { data: stylesData }: PerkStylesResponse = await useFetch(
|
||||||
watch(() => props.runes, (newRunes, oldRunes) => {
|
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json'
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => props.runes,
|
||||||
|
(newRunes, oldRunes) => {
|
||||||
currentlySelectedPage.value = 0
|
currentlySelectedPage.value = 0
|
||||||
primaryStyles.value = Array(props.runes.length)
|
primaryStyles.value = Array(props.runes.length)
|
||||||
secondaryStyles.value = Array(props.runes.length)
|
secondaryStyles.value = Array(props.runes.length)
|
||||||
keystoneIds.value = Array(props.runes.length)
|
keystoneIds.value = Array(props.runes.length)
|
||||||
|
|
||||||
refreshStylesKeystones()
|
refreshStylesKeystones()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function refreshStylesKeystones() {
|
function refreshStylesKeystones() {
|
||||||
for(let style of stylesData.value.styles) {
|
for (const style of stylesData.value.styles) {
|
||||||
for(let rune of props.runes) {
|
for (const rune of props.runes) {
|
||||||
if (style.id == rune.primaryStyle) {
|
if (style.id == rune.primaryStyle) {
|
||||||
primaryStyles.value[props.runes.indexOf(rune)] = style
|
primaryStyles.value[props.runes.indexOf(rune)] = style
|
||||||
for(let perk of style.slots[0].perks) {
|
for (const perk of style.slots[0].perks) {
|
||||||
if (rune.selections.includes(perk)) {
|
if (rune.selections.includes(perk)) {
|
||||||
keystoneIds.value[props.runes.indexOf(rune)] = perk
|
keystoneIds.value[props.runes.indexOf(rune)] = perk
|
||||||
}
|
}
|
||||||
@@ -51,26 +60,44 @@ refreshStylesKeystones()
|
|||||||
function runeSelect(index: number) {
|
function runeSelect(index: number) {
|
||||||
currentlySelectedPage.value = index
|
currentlySelectedPage.value = index
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="width: fit-content;">
|
<div style="width: fit-content">
|
||||||
<RunePage v-if="runes[currentlySelectedPage] != undefined && runes[currentlySelectedPage] != null"
|
<RunePage
|
||||||
style="margin:auto; width: fit-content;"
|
v-if="runes[currentlySelectedPage] != undefined && runes[currentlySelectedPage] != null"
|
||||||
:primaryStyleId="runes[currentlySelectedPage].primaryStyle"
|
style="margin: auto; width: fit-content"
|
||||||
:secondaryStyleId="runes[currentlySelectedPage].secondaryStyle"
|
:primary-style-id="runes[currentlySelectedPage].primaryStyle"
|
||||||
:selectionIds="runes[currentlySelectedPage].selections" />
|
:secondary-style-id="runes[currentlySelectedPage].secondaryStyle"
|
||||||
<div style="display: flex; margin-top: 20px; justify-content: center;">
|
:selection-ids="runes[currentlySelectedPage].selections"
|
||||||
|
/>
|
||||||
|
<div style="display: flex; margin-top: 20px; justify-content: center">
|
||||||
<div v-for="(_, i) in runes" @click="runeSelect(i)">
|
<div v-for="(_, i) in runes" @click="runeSelect(i)">
|
||||||
<div :class="'rune-selector-entry ' + (i == currentlySelectedPage ? 'rune-selector-entry-selected' : '')">
|
<div
|
||||||
|
:class="
|
||||||
|
'rune-selector-entry ' +
|
||||||
|
(i == currentlySelectedPage ? 'rune-selector-entry-selected' : '')
|
||||||
|
"
|
||||||
|
>
|
||||||
<div class="rs-styles-container">
|
<div class="rs-styles-container">
|
||||||
<NuxtImg class="rs-style-img" v-if="primaryStyles[i] != null && primaryStyles[i] != undefined"
|
<NuxtImg
|
||||||
style="margin: auto;" :src="CDRAGON_BASE + mapPath(primaryStyles[i].iconPath)" />
|
v-if="primaryStyles[i] != null && primaryStyles[i] != undefined"
|
||||||
<NuxtImg class="rs-style-img" v-if="keystoneIds[i] != null && keystoneIds[i] != undefined"
|
class="rs-style-img"
|
||||||
width="34" :src="CDRAGON_BASE + ( mapPath(perks.get(keystoneIds[i]).iconPath))"/>
|
style="margin: auto"
|
||||||
<NuxtImg class="rs-style-img" v-if="secondaryStyles[i] != null && secondaryStyles[i] != undefined"
|
:src="CDRAGON_BASE + mapPath(primaryStyles[i].iconPath)"
|
||||||
style="margin: auto;" :src="CDRAGON_BASE + mapPath(secondaryStyles[i].iconPath)" />
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
v-if="keystoneIds[i] != null && keystoneIds[i] != undefined"
|
||||||
|
class="rs-style-img"
|
||||||
|
width="34"
|
||||||
|
:src="CDRAGON_BASE + mapPath(perks.get(keystoneIds[i]).iconPath)"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
v-if="secondaryStyles[i] != null && secondaryStyles[i] != undefined"
|
||||||
|
class="rs-style-img"
|
||||||
|
style="margin: auto"
|
||||||
|
:src="CDRAGON_BASE + mapPath(secondaryStyles[i].iconPath)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="rs-pickrate">{{ (runes[i].pickrate * 100).toFixed(2) }}% pick.</h3>
|
<h3 class="rs-pickrate">{{ (runes[i].pickrate * 100).toFixed(2) }}% pick.</h3>
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, plugins, scales } from 'chart.js'
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
plugins,
|
||||||
|
scales
|
||||||
|
} from 'chart.js'
|
||||||
import { Bar } from 'vue-chartjs'
|
import { Bar } from 'vue-chartjs'
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: Array<{title:string, data: Array<{lane: LaneData, champion: Champion}>}>
|
data: Array<{ title: string; data: Array<{ lane: LaneData; champion: Champion }> }>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const labels: Array<string> = []
|
const labels: Array<string> = []
|
||||||
@@ -14,13 +24,13 @@ const pickrates: Array<number> = []
|
|||||||
const images: Array<string> = []
|
const images: Array<string> = []
|
||||||
const backgroundColors: Array<string> = []
|
const backgroundColors: Array<string> = []
|
||||||
const CHAMPION_CUT_THRESHOLD = 32
|
const CHAMPION_CUT_THRESHOLD = 32
|
||||||
const TIER_COLORS = ["#ff7f7e", "#ffbf7f", "#ffdf80", "#feff7f", "#beff7f", "#7eff80"]
|
const TIER_COLORS = ['#ff7f7e', '#ffbf7f', '#ffdf80', '#feff7f', '#beff7f', '#7eff80']
|
||||||
|
|
||||||
let count = 0
|
let count = 0
|
||||||
let colorIndex = 0
|
let colorIndex = 0
|
||||||
for(let tier of props.data) {
|
for (const tier of props.data) {
|
||||||
for(let {champion: champion, lane: lane} of tier.data) {
|
for (const { champion: champion, lane: lane } of tier.data) {
|
||||||
if(count > CHAMPION_CUT_THRESHOLD) break;
|
if (count > CHAMPION_CUT_THRESHOLD) break
|
||||||
|
|
||||||
labels.push(champion.name)
|
labels.push(champion.name)
|
||||||
pickrates.push(lane.pickrate * 100)
|
pickrates.push(lane.pickrate * 100)
|
||||||
@@ -32,7 +42,6 @@ for(let tier of props.data) {
|
|||||||
colorIndex++
|
colorIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const chartData = ref({
|
const chartData = ref({
|
||||||
labels: labels,
|
labels: labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
@@ -40,9 +49,9 @@ const chartData = ref({
|
|||||||
label: 'Pickrate',
|
label: 'Pickrate',
|
||||||
backgroundColor: backgroundColors,
|
backgroundColor: backgroundColors,
|
||||||
barPercentage: 1.0,
|
barPercentage: 1.0,
|
||||||
data: pickrates,
|
data: pickrates
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
})
|
})
|
||||||
const chartOptions = ref({
|
const chartOptions = ref({
|
||||||
responsive: true,
|
responsive: true,
|
||||||
@@ -50,7 +59,7 @@ const chartOptions = ref({
|
|||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (() => "")
|
callback: () => ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -60,19 +69,21 @@ const chartOptions = ref({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const chartPlugins = [{
|
const chartPlugins = [
|
||||||
id: "image-draw",
|
{
|
||||||
afterDraw: ((chart: any) => {
|
id: 'image-draw',
|
||||||
|
afterDraw: (chart: any) => {
|
||||||
const ctx: CanvasRenderingContext2D = chart.ctx
|
const ctx: CanvasRenderingContext2D = chart.ctx
|
||||||
var xAxis = chart.scales.x;
|
const xAxis = chart.scales.x
|
||||||
xAxis.ticks.forEach((value: any, index: number) => {
|
xAxis.ticks.forEach((value: any, index: number) => {
|
||||||
var x = xAxis.getPixelForTick(index)
|
const x = xAxis.getPixelForTick(index)
|
||||||
var image = new Image()
|
const image = new Image()
|
||||||
image.src = images[index]
|
image.src = images[index]
|
||||||
ctx.drawImage(image, x - 14, xAxis.bottom - 28, 28, 28)
|
ctx.drawImage(image, x - 14, xAxis.bottom - 28, 28, 28)
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string
|
title: string
|
||||||
tier: Array<{champion: Champion, lane: LaneData}>
|
tier: Array<{ champion: Champion; lane: LaneData }>
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="display: flex;">
|
<div style="display: flex">
|
||||||
<h2 class="tierlist-tier-title">{{ title }}</h2>
|
<h2 class="tierlist-tier-title">{{ title }}</h2>
|
||||||
<div class="tierlist-tier-container">
|
<div class="tierlist-tier-container">
|
||||||
<NuxtLink v-for="{champion: champion} in tier" :to="'/champion/' + champion.alias.toLowerCase()">
|
<NuxtLink
|
||||||
|
v-for="{ champion: champion } in tier"
|
||||||
|
:to="'/champion/' + champion.alias.toLowerCase()"
|
||||||
|
>
|
||||||
<div class="champion-img-container">
|
<div class="champion-img-container">
|
||||||
<NuxtImg class="champion-img" :src="CDRAGON_BASE + mapPath(champion.squarePortraitPath)" :alt="champion.name"/>
|
<NuxtImg
|
||||||
|
class="champion-img"
|
||||||
|
:src="CDRAGON_BASE + mapPath(champion.squarePortraitPath)"
|
||||||
|
:alt="champion.name"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +50,9 @@ defineProps<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.champion-img-container {
|
.champion-img-container {
|
||||||
overflow: hidden; width: 120px; height: 120px;
|
overflow: hidden;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
border: 1px solid var(--color-surface);
|
border: 1px solid var(--color-surface);
|
||||||
}
|
}
|
||||||
.champion-img-container:hover {
|
.champion-img-container:hover {
|
||||||
|
|||||||
@@ -9,16 +9,26 @@ const props = defineProps({
|
|||||||
<Title>{{ props.error.statusCode }} - BuildPath</Title>
|
<Title>{{ props.error.statusCode }} - BuildPath</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Logo style="margin: auto; margin-top: 64px; margin-bottom: 64px;" />
|
<Logo style="margin: auto; margin-top: 64px; margin-bottom: 64px" />
|
||||||
<div style="margin:auto; width: fit-content; margin-top: 64px;">
|
<div style="margin: auto; width: fit-content; margin-top: 64px">
|
||||||
<h1>{{ props.error.statusCode }} Error</h1>
|
<h1>{{ props.error.statusCode }} Error</h1>
|
||||||
<h2>Something went wrong, sorry :(</h2>
|
<h2>Something went wrong, sorry :(</h2>
|
||||||
<div style="margin-top: 64px;">
|
<div style="margin-top: 64px">
|
||||||
<h3 v-if="props.error.statusMessage != null
|
<h3
|
||||||
&& props.error.statusMessage != undefined
|
v-if="
|
||||||
&& props.error.statusMessage != ''">Error message: {{ props.error.statusMessage }}</h3>
|
props.error.statusMessage != null &&
|
||||||
<h3 v-if="props.error.data != null && props.error.data != undefined">Error data: {{ props.error.data }}</h3>
|
props.error.statusMessage != undefined &&
|
||||||
<h3 v-if="props.error.cause != null && props.error.cause != undefined">Error cause: {{ props.error.cause }}</h3>
|
props.error.statusMessage != ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Error message: {{ props.error.statusMessage }}
|
||||||
|
</h3>
|
||||||
|
<h3 v-if="props.error.data != null && props.error.data != undefined">
|
||||||
|
Error data: {{ props.error.data }}
|
||||||
|
</h3>
|
||||||
|
<h3 v-if="props.error.cause != null && props.error.cause != undefined">
|
||||||
|
Error cause: {{ props.error.cause }}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
6
frontend/eslint.config.mjs
Normal file
6
frontend/eslint.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// @ts-check
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt(
|
||||||
|
// Your custom configs here
|
||||||
|
)
|
||||||
@@ -13,35 +13,31 @@ export default defineNuxtConfig({
|
|||||||
url: 'https://buildpath.win',
|
url: 'https://buildpath.win',
|
||||||
name: 'BuildPath',
|
name: 'BuildPath',
|
||||||
description: 'BuildPath: a tool for League of Legends champions runes and build paths.',
|
description: 'BuildPath: a tool for League of Legends champions runes and build paths.',
|
||||||
defaultLocale: 'en', // not needed if you have @nuxtjs/i18n installed
|
defaultLocale: 'en' // not needed if you have @nuxtjs/i18n installed
|
||||||
},
|
},
|
||||||
sitemap: {
|
sitemap: {
|
||||||
sources: [
|
sources: ['/api/routemap']
|
||||||
'/api/routemap'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: 'en',
|
lang: 'en'
|
||||||
},
|
},
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/png', sizes: '96x96', href: '/favicon-96x96.png' },
|
{ rel: 'icon', type: 'image/png', sizes: '96x96', href: '/favicon-96x96.png' },
|
||||||
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
||||||
{ rel: 'shortcut icon', href: '/favicon.ico' },
|
{ rel: 'shortcut icon', href: '/favicon.ico' },
|
||||||
{ rel: 'apple-touch-icon', sizes: "180x180", href: '/apple-touch-icon.png' },
|
{ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' },
|
||||||
{ rel: 'manifest', href: '/site.webmanifest' },
|
{ rel: 'manifest', href: '/site.webmanifest' }
|
||||||
],
|
|
||||||
meta: [
|
|
||||||
{name: "apple-mobile-web-app-title", content:"BuildPath"},
|
|
||||||
],
|
],
|
||||||
|
meta: [{ name: 'apple-mobile-web-app-title', content: 'BuildPath' }],
|
||||||
charset: 'utf-8',
|
charset: 'utf-8',
|
||||||
viewport: 'width=device-width, initial-scale=1'
|
viewport: 'width=device-width, initial-scale=1'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: ['@nuxt/image', '@nuxt/fonts', '@nuxtjs/seo', 'nuxt-umami'],
|
modules: ['@nuxt/image', '@nuxt/fonts', '@nuxtjs/seo', 'nuxt-umami', '@nuxt/eslint'],
|
||||||
|
|
||||||
umami: {
|
umami: {
|
||||||
id: '98ef53ef-5fe1-4e29-a35e-56dc1283c212',
|
id: '98ef53ef-5fe1-4e29-a35e-56dc1283c212',
|
||||||
@@ -49,12 +45,12 @@ export default defineNuxtConfig({
|
|||||||
autoTrack: true,
|
autoTrack: true,
|
||||||
domains: ['buildpath.win'],
|
domains: ['buildpath.win'],
|
||||||
ignoreLocalhost: true,
|
ignoreLocalhost: true,
|
||||||
enabled: true,
|
enabled: true
|
||||||
},
|
},
|
||||||
|
|
||||||
fonts: {
|
fonts: {
|
||||||
defaults: {
|
defaults: {
|
||||||
weights: [200, 400],
|
weights: [200, 400]
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
3008
frontend/package-lock.json
generated
3008
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,14 @@
|
|||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint --fix .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nuxt/eslint": "^1.12.1",
|
||||||
"@nuxt/fonts": "^0.11.3",
|
"@nuxt/fonts": "^0.11.3",
|
||||||
"@nuxt/image": "^1.10.0",
|
"@nuxt/image": "^1.10.0",
|
||||||
"@nuxtjs/seo": "^3.0.3",
|
"@nuxtjs/seo": "^3.0.3",
|
||||||
@@ -24,7 +29,15 @@
|
|||||||
"vue-router": "latest"
|
"vue-router": "latest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
||||||
|
"@typescript-eslint/parser": "^8.53.1",
|
||||||
"@vue/compiler-sfc": "^3.5.13",
|
"@vue/compiler-sfc": "^3.5.13",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-nuxt": "^4.0.0",
|
||||||
|
"eslint-plugin-vue": "^10.7.0",
|
||||||
|
"prettier": "^3.8.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.10"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -10,29 +8,31 @@
|
|||||||
<div class="about-main-content">
|
<div class="about-main-content">
|
||||||
<NavBar :tierlist-list="true" />
|
<NavBar :tierlist-list="true" />
|
||||||
|
|
||||||
<div style="width: fit-content; margin: auto;">
|
<div style="width: fit-content; margin: auto">
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px">
|
||||||
<h1>About</h1>
|
<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: 20px">
|
||||||
<h3 style="margin-top: 10px;">Copyright (C) Valentin Haudiquet (@vhaudiquet)</h3>
|
BuildPath: a tool for League of Legends champions runes and build paths.
|
||||||
<h3 style="margin-top: 20px;">Acknowledgments:</h3>
|
</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>- Sarah Emery, for the feedback on the designs and code</h3>
|
||||||
<h3>- Martin Andrieux, for the nice algorithms :)</h3>
|
<h3>- Martin Andrieux, for the nice algorithms :)</h3>
|
||||||
<h3>- Paul Chaurand, for the feedback on the league data organization</h3>
|
<h3>- Paul Chaurand, for the feedback on the league data organization</h3>
|
||||||
<h3>- Nathan Mérillon, for the tierlists ideas</h3>
|
<h3>- Nathan Mérillon, for the tierlists ideas</h3>
|
||||||
<h3>- Jean-Baptiste Döderlein, for the feedback on the mobile design</h3>
|
<h3>- Jean-Baptiste Döderlein, for the feedback on the mobile design</h3>
|
||||||
<h3 style="margin-top: 20px;">Libraries used:</h3>
|
<h3 style="margin-top: 20px">Libraries used:</h3>
|
||||||
<h3>Vue.JS, Nuxt.JS, Chart.JS, svg-dom-arrows</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;">
|
<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
|
BuildPath isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot
|
||||||
Riot Games or anyone officially involved in producing or managing Riot Games properties.
|
Games or anyone officially involved in producing or managing Riot Games properties. Riot
|
||||||
Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc.
|
Games, and all associated properties are trademarks or registered trademarks of Riot
|
||||||
|
Games, Inc.
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const championAlias = route.params.alias as string
|
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
|
const championId = championData.value.id
|
||||||
|
|
||||||
// Prefetch home page for faster navigation
|
// Prefetch home page for faster navigation
|
||||||
useHead({
|
useHead({
|
||||||
link: [
|
link: [{ rel: 'prefetch', href: '/' }]
|
||||||
{ rel: 'prefetch', href: '/' }
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defineOgImageComponent('Champion', {
|
defineOgImageComponent('Champion', {
|
||||||
@@ -17,7 +17,7 @@ defineOgImageComponent('Champion', {
|
|||||||
id: championId,
|
id: championId,
|
||||||
winrate: championData.value.winrate,
|
winrate: championData.value.winrate,
|
||||||
pickrate: championData.value.pickrate,
|
pickrate: championData.value.pickrate,
|
||||||
gameCount: championData.value.gameCount,
|
gameCount: championData.value.gameCount
|
||||||
})
|
})
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: championData.value.name,
|
title: championData.value.name,
|
||||||
@@ -25,7 +25,7 @@ useSeoMeta({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const laneState = ref(0)
|
const laneState = ref(0)
|
||||||
const state = ref("runes")
|
const state = ref('runes')
|
||||||
const lane = ref(championData.value.lanes[laneState.value])
|
const lane = ref(championData.value.lanes[laneState.value])
|
||||||
function updateState(newState: string, newLane: number) {
|
function updateState(newState: string, newLane: number) {
|
||||||
state.value = newState
|
state.value = newState
|
||||||
@@ -40,30 +40,43 @@ function updateState(newState : string, newLane : number) {
|
|||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<div id="alias-content-wrapper">
|
<div id="alias-content-wrapper">
|
||||||
|
<NavBar
|
||||||
<NavBar :champion-name="championData.name"
|
:champion-name="championData.name"
|
||||||
:champion-lanes="championData.lanes"
|
:champion-lanes="championData.lanes"
|
||||||
@state-change="updateState"/>
|
@state-change="updateState"
|
||||||
|
/>
|
||||||
|
|
||||||
<div id="champion-content">
|
<div id="champion-content">
|
||||||
<ChampionTitle id="champion-title" v-if="championData.gameCount > 0"
|
<ChampionTitle
|
||||||
:champion-id="championId" :winrate="lane.winrate"
|
v-if="championData.gameCount > 0"
|
||||||
:pickrate="lane.pickrate" :game-count="lane.count" />
|
id="champion-title"
|
||||||
<RuneSelector v-if="state == 'runes' && championData.gameCount > 0"
|
:champion-id="championId"
|
||||||
style="margin: auto; margin-top: 40px;"
|
:winrate="lane.winrate"
|
||||||
:runes="lane.runes!!" />
|
:pickrate="lane.pickrate"
|
||||||
<ItemViewer v-if="state == 'items' && championData.gameCount > 0"
|
:game-count="lane.count"
|
||||||
style="margin:auto; margin-top: 40px;"
|
/>
|
||||||
:builds="lane.builds!!" />
|
<RuneSelector
|
||||||
<ItemTree v-if="state == 'alternatives' && championData.gameCount > 0"
|
v-if="state == 'runes' && championData.gameCount > 0"
|
||||||
style="margin: auto; margin-top: 40px; width: fit-content;"
|
style="margin: auto; margin-top: 40px"
|
||||||
:tree="lane.builds!!.tree" />
|
:runes="lane.runes!!"
|
||||||
<h2 v-if="championData.gameCount == 0"
|
/>
|
||||||
style="margin: auto; margin-top: 20px; width: fit-content;">
|
<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 :(
|
Sorry, there is no data for this champion :(
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { POSITIONS, LANE_IMAGES, POSITIONS_STR } from '~/utils/cdragon';
|
import { POSITIONS, LANE_IMAGES, POSITIONS_STR } from '~/utils/cdragon'
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -12,7 +11,6 @@ import { POSITIONS, LANE_IMAGES, POSITIONS_STR } from '~/utils/cdragon';
|
|||||||
<NavBar :tierlist-list="true" />
|
<NavBar :tierlist-list="true" />
|
||||||
|
|
||||||
<ChampionSelector class="index-champion-selector" />
|
<ChampionSelector class="index-champion-selector" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -59,5 +57,4 @@ import { POSITIONS, LANE_IMAGES, POSITIONS_STR } from '~/utils/cdragon';
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<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 route = useRoute()
|
||||||
const lane = route.params.lane as string
|
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 { data: championsLanes }: { data: Ref<Array<ChampionData>> } =
|
||||||
|
await useFetch('/api/champions')
|
||||||
const infoMap: Map<string, ChampionData> = new Map()
|
const infoMap: Map<string, ChampionData> = new Map()
|
||||||
for(let champion of championsLanes.value) {
|
for (const champion of championsLanes.value) {
|
||||||
infoMap.set(champion.alias, champion)
|
infoMap.set(champion.alias, champion)
|
||||||
}
|
}
|
||||||
|
|
||||||
const champions = championsData.value.slice(1).filter((champion) => {
|
const champions = championsData.value.slice(1).filter(champion => {
|
||||||
const championData: ChampionData | undefined = infoMap.get(champion.alias.toLowerCase())
|
const championData: ChampionData | undefined = infoMap.get(champion.alias.toLowerCase())
|
||||||
if(championData == undefined) return false;
|
if (championData == undefined) return false
|
||||||
|
|
||||||
const lanes = championData.lanes
|
const lanes = championData.lanes
|
||||||
return lanes.reduce((acc : boolean, current : {data:string, count:number}) =>
|
return lanes.reduce(
|
||||||
acc || (current.data.toLowerCase() == lane.toLowerCase()), false)
|
(acc: boolean, current: { data: string; count: number }) =>
|
||||||
|
acc || current.data.toLowerCase() == lane.toLowerCase(),
|
||||||
|
false
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const allChampions = champions.map((x) => {
|
const allChampions = champions
|
||||||
const championData : ChampionData = infoMap.get(x.alias.toLowerCase())!!
|
.map(x => {
|
||||||
|
const championData: ChampionData = infoMap.get(x.alias.toLowerCase())!
|
||||||
|
|
||||||
let currentLane = championData.lanes[0]
|
let currentLane = championData.lanes[0]
|
||||||
for(let championLane of championData.lanes) {
|
for (const championLane of championData.lanes) {
|
||||||
if (championLane.data.toLowerCase() == lane.toLowerCase()) {
|
if (championLane.data.toLowerCase() == lane.toLowerCase()) {
|
||||||
currentLane = championLane
|
currentLane = championLane
|
||||||
break
|
break
|
||||||
@@ -33,27 +40,33 @@ const allChampions = champions.map((x) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { lane: currentLane, champion: x }
|
return { lane: currentLane, champion: x }
|
||||||
}).sort((a, b) => b.lane.pickrate - a.lane.pickrate)
|
})
|
||||||
|
.sort((a, b) => b.lane.pickrate - a.lane.pickrate)
|
||||||
|
|
||||||
const p_min = Math.min(...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))
|
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)
|
allChampions.sort((a, b) => b.lane.pickrate - a.lane.pickrate)
|
||||||
function tierFromScaledPickrate(min: number, max: number) {
|
function tierFromScaledPickrate(min: number, max: number) {
|
||||||
return (allChampions as Array<{lane: LaneData, champion: Champion, scaledPickrate: number}>)
|
return (
|
||||||
.filter(({scaledPickrate: scaledPickrate}) => {
|
allChampions as Array<{ lane: LaneData; champion: Champion; scaledPickrate: number }>
|
||||||
|
).filter(({ scaledPickrate: scaledPickrate }) => {
|
||||||
return scaledPickrate > min && scaledPickrate <= max
|
return scaledPickrate > min && scaledPickrate <= max
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const tiers: Array<{title:string, data: Array<{lane: LaneData, champion: Champion}>}> = []
|
const tiers: Array<{ title: string; data: Array<{ lane: LaneData; champion: Champion }> }> = []
|
||||||
tiers.push({title: "S", data: tierFromScaledPickrate(0.9, 1)})
|
tiers.push({ title: 'S', data: tierFromScaledPickrate(0.9, 1) })
|
||||||
tiers.push({title: "A", data: tierFromScaledPickrate(0.7, 0.9)})
|
tiers.push({ title: 'A', data: tierFromScaledPickrate(0.7, 0.9) })
|
||||||
tiers.push({title: "B", data: tierFromScaledPickrate(0.5, 0.7)})
|
tiers.push({ title: 'B', data: tierFromScaledPickrate(0.5, 0.7) })
|
||||||
tiers.push({title: "C", data: tierFromScaledPickrate(0.3, 0.5)})
|
tiers.push({ title: 'C', data: tierFromScaledPickrate(0.3, 0.5) })
|
||||||
tiers.push({title: "D", data: tierFromScaledPickrate(0.1, 0.3)})
|
tiers.push({ title: 'D', data: tierFromScaledPickrate(0.1, 0.3) })
|
||||||
tiers.push({title: "F", data: tierFromScaledPickrate(0, 0.1)})
|
tiers.push({ title: 'F', data: tierFromScaledPickrate(0, 0.1) })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -61,26 +74,37 @@ tiers.push({title: "F", data: tierFromScaledPickrate(0, 0.1)})
|
|||||||
<Title>Tierlist for {{ POSITIONS_STR[lanePositionToIndex(lane)] }}</Title>
|
<Title>Tierlist for {{ POSITIONS_STR[lanePositionToIndex(lane)] }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<div style="display: flex; min-height: 100vh; align-items: stretch; width: 100%;">
|
<div style="display: flex; min-height: 100vh; align-items: stretch; width: 100%">
|
||||||
<NavBar :tierlist-list="true" />
|
<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
|
||||||
<div style="margin-left: 0px; margin-top: 20px; display: flex; margin-bottom: 30px; align-items: center">
|
style="
|
||||||
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300;">Tierlist for</h1>
|
margin-left: 0px;
|
||||||
<NuxtImg format="webp" style="margin-left: 10px;"
|
margin-top: 20px;
|
||||||
width="50" height="50"
|
display: flex;
|
||||||
:src="LANE_IMAGES[lanePositionToIndex(lane)]" />
|
margin-bottom: 30px;
|
||||||
<h1 style="margin-left: 10px; font-size: 2.8rem; font-weight: 300;">{{ POSITIONS_STR[lanePositionToIndex(lane)] }}</h1>
|
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>
|
||||||
|
|
||||||
<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" />
|
<TierlistChart id="chart" :data="tiers" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -88,7 +112,7 @@ tiers.push({title: "F", data: tierFromScaledPickrate(0, 0.1)})
|
|||||||
margin-left: 100px;
|
margin-left: 100px;
|
||||||
margin-right: 100px;
|
margin-right: 100px;
|
||||||
margin-bottom: 100px;
|
margin-bottom: 100px;
|
||||||
margin-top: 40px
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 450px) {
|
@media only screen and (max-width: 450px) {
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { MongoClient } from 'mongodb';
|
import type { MongoClient } from 'mongodb'
|
||||||
import { connectToDatabase, fetchLatestPatch } from '../../utils/mongo'
|
import { connectToDatabase, fetchLatestPatch } from '../../utils/mongo'
|
||||||
|
|
||||||
async function championInfos(client: MongoClient, patch: string, championAlias: string) {
|
async function championInfos(client: MongoClient, patch: string, championAlias: string) {
|
||||||
const database = client.db("champions");
|
const database = client.db('champions')
|
||||||
const collection = database.collection(patch);
|
const collection = database.collection(patch)
|
||||||
const query = { alias:championAlias };
|
const query = { alias: championAlias }
|
||||||
const championInfo = (await collection.findOne(query)) as unknown as ChampionData;
|
const championInfo = (await collection.findOne(query)) as unknown as ChampionData
|
||||||
return championInfo
|
return championInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const championAlias = (getRouterParam(event, "alias") as string).toLowerCase()
|
const championAlias = (getRouterParam(event, 'alias') as string).toLowerCase()
|
||||||
const client = await connectToDatabase();
|
const client = await connectToDatabase()
|
||||||
const latestPatch = await fetchLatestPatch(client);
|
const latestPatch = await fetchLatestPatch(client)
|
||||||
const data = await championInfos(client, latestPatch, championAlias);
|
const data = await championInfos(client, latestPatch, championAlias)
|
||||||
await client.close()
|
await client.close()
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { MongoClient } from 'mongodb';
|
import type { MongoClient } from 'mongodb'
|
||||||
import { connectToDatabase, fetchLatestPatch } from '../utils/mongo'
|
import { connectToDatabase, fetchLatestPatch } from '../utils/mongo'
|
||||||
|
|
||||||
async function champions(client: MongoClient, patch: string) {
|
async function champions(client: MongoClient, patch: string) {
|
||||||
const database = client.db("champions");
|
const database = client.db('champions')
|
||||||
const collection = database.collection(patch);
|
const collection = database.collection(patch)
|
||||||
const data : Array<ChampionData> = (await collection.find().toArray()) as unknown as Array<ChampionData>
|
const data: Array<ChampionData> = (await collection
|
||||||
data.map((x) => {
|
.find()
|
||||||
|
.toArray()) as unknown as Array<ChampionData>
|
||||||
|
data.map(x => {
|
||||||
if (x.lanes != undefined && x.lanes != null) {
|
if (x.lanes != undefined && x.lanes != null) {
|
||||||
for(let lane of x.lanes) {
|
for (const lane of x.lanes) {
|
||||||
delete lane.builds
|
delete lane.builds
|
||||||
delete lane.runes
|
delete lane.runes
|
||||||
}
|
}
|
||||||
@@ -16,11 +18,11 @@ async function champions(client: MongoClient, patch: string) {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (_) => {
|
export default defineEventHandler(async _ => {
|
||||||
const client = await connectToDatabase();
|
const client = await connectToDatabase()
|
||||||
const latestPatch = await fetchLatestPatch(client);
|
const latestPatch = await fetchLatestPatch(client)
|
||||||
|
|
||||||
const data = await champions(client, latestPatch);
|
const data = await champions(client, latestPatch)
|
||||||
|
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { CDRAGON_BASE } from "~/utils/cdragon";
|
import { CDRAGON_BASE } from '~/utils/cdragon'
|
||||||
|
|
||||||
async function championRoutes() {
|
async function championRoutes() {
|
||||||
const championsData : Array<Champion> = await
|
const championsData: Array<Champion> = await (
|
||||||
(await fetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json")).json()
|
await fetch(
|
||||||
|
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
|
||||||
|
)
|
||||||
|
).json()
|
||||||
|
|
||||||
let routes : Array<string> = []
|
const routes: Array<string> = []
|
||||||
for(let champion of championsData) {
|
for (const champion of championsData) {
|
||||||
routes.push("/champion/" + champion.alias.toLowerCase())
|
routes.push('/champion/' + champion.alias.toLowerCase())
|
||||||
}
|
}
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (_) => {
|
export default defineEventHandler(async _ => {
|
||||||
const data = await championRoutes();
|
const data = await championRoutes()
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { MongoClient } from 'mongodb';
|
import type { MongoClient } from 'mongodb'
|
||||||
import { connectToDatabase, fetchLatestPatch } from '../utils/mongo'
|
import { connectToDatabase, fetchLatestPatch } from '../utils/mongo'
|
||||||
|
|
||||||
async function fetchGameCount(client: MongoClient, patch: string) {
|
async function fetchGameCount(client: MongoClient, patch: string) {
|
||||||
const database = client.db("matches");
|
const database = client.db('matches')
|
||||||
const matches = database.collection(patch);
|
const matches = database.collection(patch)
|
||||||
const count = await matches.countDocuments()
|
const count = await matches.countDocuments()
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (_) => {
|
export default defineEventHandler(async _ => {
|
||||||
const client = await connectToDatabase();
|
const client = await connectToDatabase()
|
||||||
const latestPatch = await fetchLatestPatch(client);
|
const latestPatch = await fetchLatestPatch(client)
|
||||||
const gameCount = await fetchGameCount(client, latestPatch)
|
const gameCount = await fetchGameCount(client, latestPatch)
|
||||||
|
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { MongoClient } from 'mongodb'
|
|||||||
async function connectToDatabase() {
|
async function connectToDatabase() {
|
||||||
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
|
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
|
||||||
let uri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}`
|
let uri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}`
|
||||||
if(process.env.MONGO_URI != undefined && process.env.MONGO_URI != null && process.env.MONGO_URI != "") {
|
if (
|
||||||
|
process.env.MONGO_URI != undefined &&
|
||||||
|
process.env.MONGO_URI != null &&
|
||||||
|
process.env.MONGO_URI != ''
|
||||||
|
) {
|
||||||
uri = process.env.MONGO_URI
|
uri = process.env.MONGO_URI
|
||||||
}
|
}
|
||||||
const client = new MongoClient(uri)
|
const client = new MongoClient(uri)
|
||||||
@@ -12,10 +16,10 @@ async function connectToDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLatestPatch(client: MongoClient) {
|
async function fetchLatestPatch(client: MongoClient) {
|
||||||
const database = client.db("patches");
|
const database = client.db('patches')
|
||||||
const patches = database.collection("patches");
|
const patches = database.collection('patches')
|
||||||
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
||||||
return latestPatch!!.patch as string
|
return latestPatch!.patch as string
|
||||||
}
|
}
|
||||||
|
|
||||||
export { connectToDatabase, fetchLatestPatch }
|
export { connectToDatabase, fetchLatestPatch }
|
||||||
|
|||||||
@@ -3,72 +3,71 @@ declare global {
|
|||||||
* Represents an item in the build tree
|
* Represents an item in the build tree
|
||||||
*/
|
*/
|
||||||
interface ItemTree {
|
interface ItemTree {
|
||||||
count: number;
|
count: number
|
||||||
data: number;
|
data: number
|
||||||
children: ItemTree[];
|
children: ItemTree[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents champion build information
|
* Represents champion build information
|
||||||
*/
|
*/
|
||||||
interface Builds {
|
interface Builds {
|
||||||
start: Array<{count: number, data: number}>;
|
start: Array<{ count: number; data: number }>
|
||||||
tree: ItemTree;
|
tree: ItemTree
|
||||||
bootsFirst: number;
|
bootsFirst: number
|
||||||
boots: Array<{count: number, data: number}>;
|
boots: Array<{ count: number; data: number }>
|
||||||
lateGame: Array<{count: number, data: number}>;
|
lateGame: Array<{ count: number; data: number }>
|
||||||
suppItems?: Array<{count: number, data: number}>;
|
suppItems?: Array<{ count: number; data: number }>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a rune configuration
|
* Represents a rune configuration
|
||||||
*/
|
*/
|
||||||
interface Rune {
|
interface Rune {
|
||||||
count: number;
|
count: number
|
||||||
primaryStyle: number;
|
primaryStyle: number
|
||||||
secondaryStyle: number;
|
secondaryStyle: number
|
||||||
selections: number[];
|
selections: number[]
|
||||||
pickrate: number;
|
pickrate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents lane-specific champion data
|
* Represents lane-specific champion data
|
||||||
*/
|
*/
|
||||||
interface LaneData {
|
interface LaneData {
|
||||||
data: string;
|
data: string
|
||||||
count: number;
|
count: number
|
||||||
winningMatches: number;
|
winningMatches: number
|
||||||
losingMatches: number;
|
losingMatches: number
|
||||||
winrate: number;
|
winrate: number
|
||||||
pickrate: number;
|
pickrate: number
|
||||||
runes?: Rune[];
|
runes?: Rune[]
|
||||||
builds?: Builds;
|
builds?: Builds
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents complete champion data
|
* Represents complete champion data
|
||||||
*/
|
*/
|
||||||
interface ChampionData {
|
interface ChampionData {
|
||||||
id: number;
|
id: number
|
||||||
name: string;
|
name: string
|
||||||
alias: string;
|
alias: string
|
||||||
gameCount: number;
|
gameCount: number
|
||||||
winrate: number;
|
winrate: number
|
||||||
pickrate: number;
|
pickrate: number
|
||||||
lanes: LaneData[];
|
lanes: LaneData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Champion summary from CDragon
|
* Champion summary from CDragon
|
||||||
*/
|
*/
|
||||||
interface ChampionSummary {
|
interface ChampionSummary {
|
||||||
id: number;
|
id: number
|
||||||
name: string;
|
name: string
|
||||||
alias: string;
|
alias: string
|
||||||
squarePortraitPath: string;
|
squarePortraitPath: string
|
||||||
// Add other relevant fields as needed
|
// Add other relevant fields as needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {}
|
||||||
|
|||||||
@@ -1,35 +1,55 @@
|
|||||||
const CDRAGON_BASE = "https://raw.communitydragon.org/latest/"
|
const CDRAGON_BASE = 'https://raw.communitydragon.org/latest/'
|
||||||
|
|
||||||
/* Lanes */
|
/* Lanes */
|
||||||
const POSITIONS = ["top", "jungle", "middle", "bottom", "utility"]
|
const POSITIONS = ['top', 'jungle', 'middle', 'bottom', 'utility']
|
||||||
const POSITIONS_STR = ["top", "jungle", "mid", "bot", "support"]
|
const POSITIONS_STR = ['top', 'jungle', 'mid', 'bot', 'support']
|
||||||
const LANE_IMAGES = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + ".png")
|
const LANE_IMAGES = Array(5)
|
||||||
const LANE_IMAGES_HOVER = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + "-hover.png")
|
.fill('')
|
||||||
const LANE_IMAGES_SELECTED = Array(5).fill("").map((_, index) => "/img/lanes/icon-position-" + POSITIONS[index] + "-blue.png")
|
.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) {
|
function laneIndexToPosition(index: number) {
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0: return "top"
|
case 0:
|
||||||
case 1: return "jungle"
|
return 'top'
|
||||||
case 2: return "middle"
|
case 1:
|
||||||
case 3: return "bottom"
|
return 'jungle'
|
||||||
case 4: return "utility"
|
case 2:
|
||||||
|
return 'middle'
|
||||||
|
case 3:
|
||||||
|
return 'bottom'
|
||||||
|
case 4:
|
||||||
|
return 'utility'
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
function lanePositionToIndex(position: string) {
|
function lanePositionToIndex(position: string) {
|
||||||
const p = position.toLowerCase()
|
const p = position.toLowerCase()
|
||||||
for (let i = 0; i < POSITIONS.length; i++) {
|
for (let i = 0; i < POSITIONS.length; i++) {
|
||||||
if(p == POSITIONS[i]) return i;
|
if (p == POSITIONS[i]) return i
|
||||||
}
|
}
|
||||||
return -1;
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapPath(assetPath: string) {
|
function mapPath(assetPath: string) {
|
||||||
if(assetPath === undefined || assetPath === null) return ""
|
if (assetPath === undefined || assetPath === null) return ''
|
||||||
return assetPath.toLowerCase().replace("/lol-game-data/assets/", "plugins/rcp-be-lol-game-data/global/default/")
|
return assetPath
|
||||||
|
.toLowerCase()
|
||||||
|
.replace('/lol-game-data/assets/', 'plugins/rcp-be-lol-game-data/global/default/')
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
mapPath, CDRAGON_BASE, laneIndexToPosition, lanePositionToIndex,
|
mapPath,
|
||||||
POSITIONS, LANE_IMAGES, LANE_IMAGES_HOVER, LANE_IMAGES_SELECTED, POSITIONS_STR
|
CDRAGON_BASE,
|
||||||
|
laneIndexToPosition,
|
||||||
|
lanePositionToIndex,
|
||||||
|
POSITIONS,
|
||||||
|
LANE_IMAGES,
|
||||||
|
LANE_IMAGES_HOVER,
|
||||||
|
LANE_IMAGES_SELECTED,
|
||||||
|
POSITIONS_STR
|
||||||
}
|
}
|
||||||
@@ -8,15 +8,18 @@
|
|||||||
* @param wait Time in milliseconds to wait before calling the function
|
* @param wait Time in milliseconds to wait before calling the function
|
||||||
* @returns Debounced function
|
* @returns Debounced function
|
||||||
*/
|
*/
|
||||||
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
return function (...args: Parameters<T>): void {
|
return function (...args: Parameters<T>): void {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => func(...args), wait)
|
||||||
}
|
}
|
||||||
timeout = setTimeout(() => func(...args), wait);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,10 +30,10 @@ export function debounce<T extends (...args: any[]) => any>(func: T, wait: numbe
|
|||||||
*/
|
*/
|
||||||
export function safeJsonParse<T>(data: string, defaultValue: T): T {
|
export function safeJsonParse<T>(data: string, defaultValue: T): T {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(data) as T;
|
return JSON.parse(data) as T
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('JSON parse error:', error);
|
console.error('JSON parse error:', error)
|
||||||
return defaultValue;
|
return defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +44,7 @@ export function safeJsonParse<T>(data: string, defaultValue: T): T {
|
|||||||
* @returns Formatted percentage string
|
* @returns Formatted percentage string
|
||||||
*/
|
*/
|
||||||
export function formatPercentage(value: number, decimals: number = 0): string {
|
export function formatPercentage(value: number, decimals: number = 0): string {
|
||||||
return (value * 100).toFixed(decimals) + '%';
|
return (value * 100).toFixed(decimals) + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,8 +53,8 @@ export function formatPercentage(value: number, decimals: number = 0): string {
|
|||||||
* @returns Capitalized string
|
* @returns Capitalized string
|
||||||
*/
|
*/
|
||||||
export function capitalize(str: string): string {
|
export function capitalize(str: string): string {
|
||||||
if (!str) return '';
|
if (!str) return ''
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,13 +64,13 @@ export function capitalize(str: string): string {
|
|||||||
*/
|
*/
|
||||||
export function getLaneName(position: string): string {
|
export function getLaneName(position: string): string {
|
||||||
const laneMap: Record<string, string> = {
|
const laneMap: Record<string, string> = {
|
||||||
'top': 'Top',
|
top: 'Top',
|
||||||
'jungle': 'Jungle',
|
jungle: 'Jungle',
|
||||||
'middle': 'Middle',
|
middle: 'Middle',
|
||||||
'bottom': 'Bottom',
|
bottom: 'Bottom',
|
||||||
'utility': 'Support'
|
utility: 'Support'
|
||||||
};
|
}
|
||||||
return laneMap[position.toLowerCase()] || position;
|
return laneMap[position.toLowerCase()] || position
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +79,7 @@ export function getLaneName(position: string): string {
|
|||||||
* @returns Full image URL
|
* @returns Full image URL
|
||||||
*/
|
*/
|
||||||
export function getChampionImageUrl(championAlias: string): string {
|
export function getChampionImageUrl(championAlias: string): string {
|
||||||
return `/img/champions/${championAlias.toLowerCase()}.png`;
|
return `/img/champions/${championAlias.toLowerCase()}.png`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,7 +88,7 @@ export function getChampionImageUrl(championAlias: string): string {
|
|||||||
* @returns Full item image URL
|
* @returns Full item image URL
|
||||||
*/
|
*/
|
||||||
export function getItemImageUrl(itemId: number): string {
|
export function getItemImageUrl(itemId: number): string {
|
||||||
return `${CDRAGON_BASE}plugins/rcp-be-lol-game-data/global/default/v1/items/${itemId}.png`;
|
return `${CDRAGON_BASE}plugins/rcp-be-lol-game-data/global/default/v1/items/${itemId}.png`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,7 +97,7 @@ export function getItemImageUrl(itemId: number): string {
|
|||||||
* @returns Full rune image URL
|
* @returns Full rune image URL
|
||||||
*/
|
*/
|
||||||
export function getRuneImageUrl(runeId: number): string {
|
export function getRuneImageUrl(runeId: number): string {
|
||||||
return `${CDRAGON_BASE}plugins/rcp-be-lol-game-data/global/default/v1/perks/${runeId}.png`;
|
return `${CDRAGON_BASE}plugins/rcp-be-lol-game-data/global/default/v1/perks/${runeId}.png`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,11 +107,11 @@ export function getRuneImageUrl(runeId: number): string {
|
|||||||
*/
|
*/
|
||||||
export function formatLargeNumber(num: number): string {
|
export function formatLargeNumber(num: number): string {
|
||||||
if (num >= 1000000) {
|
if (num >= 1000000) {
|
||||||
return (num / 1000000).toFixed(1) + 'M';
|
return (num / 1000000).toFixed(1) + 'M'
|
||||||
} else if (num >= 1000) {
|
} else if (num >= 1000) {
|
||||||
return (num / 1000).toFixed(1) + 'K';
|
return (num / 1000).toFixed(1) + 'K'
|
||||||
}
|
}
|
||||||
return num.toString();
|
return num.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,7 +120,7 @@ export function formatLargeNumber(num: number): string {
|
|||||||
* @returns Cloned object
|
* @returns Cloned object
|
||||||
*/
|
*/
|
||||||
export function deepClone<T>(obj: T): T {
|
export function deepClone<T>(obj: T): T {
|
||||||
return JSON.parse(JSON.stringify(obj));
|
return JSON.parse(JSON.stringify(obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,11 +129,11 @@ export function deepClone<T>(obj: T): T {
|
|||||||
* @returns True if value is empty
|
* @returns True if value is empty
|
||||||
*/
|
*/
|
||||||
export function isEmpty(value: any): boolean {
|
export function isEmpty(value: any): boolean {
|
||||||
if (value === null || value === undefined) return true;
|
if (value === null || value === undefined) return true
|
||||||
if (typeof value === 'string' && value.trim() === '') return true;
|
if (typeof value === 'string' && value.trim() === '') return true
|
||||||
if (Array.isArray(value) && value.length === 0) return true;
|
if (Array.isArray(value) && value.length === 0) return true
|
||||||
if (typeof value === 'object' && Object.keys(value).length === 0) return true;
|
if (typeof value === 'object' && Object.keys(value).length === 0) return true
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -139,9 +142,9 @@ export function isEmpty(value: any): boolean {
|
|||||||
* @returns CSS color class
|
* @returns CSS color class
|
||||||
*/
|
*/
|
||||||
export function getWinrateColor(winrate: number): string {
|
export function getWinrateColor(winrate: number): string {
|
||||||
if (winrate > 0.55) return 'text-green-500';
|
if (winrate > 0.55) return 'text-green-500'
|
||||||
if (winrate < 0.45) return 'text-red-500';
|
if (winrate < 0.45) return 'text-red-500'
|
||||||
return 'text-yellow-500';
|
return 'text-yellow-500'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,15 +153,15 @@ export function getWinrateColor(winrate: number): string {
|
|||||||
* @returns Formatted duration string
|
* @returns Formatted duration string
|
||||||
*/
|
*/
|
||||||
export function formatDuration(ms: number): string {
|
export function formatDuration(ms: number): string {
|
||||||
const seconds = Math.floor(ms / 1000);
|
const seconds = Math.floor(ms / 1000)
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60)
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60)
|
||||||
|
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
return `${hours}h ${minutes % 60}m`;
|
return `${hours}h ${minutes % 60}m`
|
||||||
} else if (minutes > 0) {
|
} else if (minutes > 0) {
|
||||||
return `${minutes}m ${seconds % 60}s`;
|
return `${minutes}m ${seconds % 60}s`
|
||||||
} else {
|
} else {
|
||||||
return `${seconds}s`;
|
return `${seconds}s`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
match_collector/.prettierrc
Normal file
10
match_collector/.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
@@ -1,29 +1,35 @@
|
|||||||
function sameArrays(array1: Array<any>, array2: Array<any>) {
|
function sameArrays(array1: Array<any>, array2: Array<any>) {
|
||||||
if(array1.length != array2.length) return false;
|
if (array1.length != array2.length) return false
|
||||||
for(let e of array1) {
|
for (const e of array1) {
|
||||||
if(!array2.includes(e)) return false;
|
if (!array2.includes(e)) return false
|
||||||
}
|
}
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from 'mongodb'
|
||||||
import { ItemTree, treeInit, treeMerge, treeCutBranches, treeSort } from "./item_tree";
|
import { ItemTree, treeInit, treeMerge, treeCutBranches, treeSort } from './item_tree'
|
||||||
const itemDict = new Map()
|
const itemDict = new Map()
|
||||||
|
|
||||||
async function itemList() {
|
async function itemList() {
|
||||||
const response = await fetch("https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/items.json")
|
const response = await fetch(
|
||||||
|
'https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/items.json'
|
||||||
|
)
|
||||||
const list = await response.json()
|
const list = await response.json()
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayRemovePercentage(array: Array<{count:number}>, totalGames:number, percentage: number) {
|
function arrayRemovePercentage(
|
||||||
let toRemove : Array<{count:number}> = []
|
array: Array<{ count: number }>,
|
||||||
for(let item of array) {
|
totalGames: number,
|
||||||
if((item.count/totalGames) < percentage) {
|
percentage: number
|
||||||
|
) {
|
||||||
|
const toRemove: Array<{ count: number }> = []
|
||||||
|
for (const item of array) {
|
||||||
|
if (item.count / totalGames < percentage) {
|
||||||
toRemove.push(item)
|
toRemove.push(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(let tr of toRemove) {
|
for (const tr of toRemove) {
|
||||||
array.splice(array.indexOf(tr), 1)
|
array.splice(array.indexOf(tr), 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,19 +40,19 @@ type Rune = {
|
|||||||
secondaryStyle: number
|
secondaryStyle: number
|
||||||
selections: Array<number>
|
selections: Array<number>
|
||||||
pickrate?: number
|
pickrate?: number
|
||||||
};
|
}
|
||||||
type Builds = {
|
type Builds = {
|
||||||
tree: ItemTree
|
tree: ItemTree
|
||||||
start: Array<{data: number, count: number}>
|
start: Array<{ data: number; count: number }>
|
||||||
bootsFirst: number
|
bootsFirst: number
|
||||||
boots: Array<{data: number, count: number}>
|
boots: Array<{ data: number; count: number }>
|
||||||
lateGame: Array<{data: number, count: number}>
|
lateGame: Array<{ data: number; count: number }>
|
||||||
suppItems?: Array<{data: number, count: number}>
|
suppItems?: Array<{ data: number; count: number }>
|
||||||
}
|
}
|
||||||
type Champion = {
|
type Champion = {
|
||||||
id: Number
|
id: number
|
||||||
name: String
|
name: string
|
||||||
alias: String
|
alias: string
|
||||||
}
|
}
|
||||||
type LaneData = {
|
type LaneData = {
|
||||||
data: string
|
data: string
|
||||||
@@ -69,18 +75,27 @@ function handleParticipantRunes(participant, runes: Array<Rune>) {
|
|||||||
const primaryStyle = participant.perks.styles[0].style
|
const primaryStyle = participant.perks.styles[0].style
|
||||||
const secondaryStyle = participant.perks.styles[1].style
|
const secondaryStyle = participant.perks.styles[1].style
|
||||||
const selections: Array<number> = []
|
const selections: Array<number> = []
|
||||||
for(let style of participant.perks.styles) {
|
for (const style of participant.perks.styles) {
|
||||||
for(let perk of style.selections) {
|
for (const perk of style.selections) {
|
||||||
selections.push(perk.perk)
|
selections.push(perk.perk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const gameRunes : Rune = {count:1, primaryStyle: primaryStyle, secondaryStyle: secondaryStyle, selections: selections};
|
const gameRunes: Rune = {
|
||||||
let addRunes = true;
|
count: 1,
|
||||||
for(let rune of runes) {
|
primaryStyle: primaryStyle,
|
||||||
if(rune.primaryStyle == gameRunes.primaryStyle
|
secondaryStyle: secondaryStyle,
|
||||||
&& rune.secondaryStyle == gameRunes.secondaryStyle
|
selections: selections
|
||||||
&& sameArrays(rune.selections, gameRunes.selections)) {
|
}
|
||||||
rune.count++; addRunes = false; break;
|
let addRunes = true
|
||||||
|
for (const rune of runes) {
|
||||||
|
if (
|
||||||
|
rune.primaryStyle == gameRunes.primaryStyle &&
|
||||||
|
rune.secondaryStyle == gameRunes.secondaryStyle &&
|
||||||
|
sameArrays(rune.selections, gameRunes.selections)
|
||||||
|
) {
|
||||||
|
rune.count++
|
||||||
|
addRunes = false
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (addRunes) runes.push(gameRunes)
|
if (addRunes) runes.push(gameRunes)
|
||||||
@@ -88,45 +103,49 @@ function handleParticipantRunes(participant, runes: Array<Rune>) {
|
|||||||
|
|
||||||
function handleMatchItems(timeline, participant: any, participantIndex: number, builds: Builds) {
|
function handleMatchItems(timeline, participant: any, participantIndex: number, builds: Builds) {
|
||||||
const items: Array<number> = []
|
const items: Array<number> = []
|
||||||
for(let frame of timeline.info.frames) {
|
for (const frame of timeline.info.frames) {
|
||||||
for(let event of frame.events) {
|
for (const event of frame.events) {
|
||||||
if(event.participantId != participantIndex) continue;
|
if (event.participantId != participantIndex) continue
|
||||||
if(event.type == "ITEM_UNDO") {
|
if (event.type == 'ITEM_UNDO') {
|
||||||
if (items.length > 0 && items[items.length - 1] == event.beforeId) {
|
if (items.length > 0 && items[items.length - 1] == event.beforeId) {
|
||||||
items.pop()
|
items.pop()
|
||||||
}
|
}
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemInfo = itemDict.get(event.itemId)
|
const itemInfo = itemDict.get(event.itemId)
|
||||||
// Handle bounty of worlds destroy as upgrade
|
// Handle bounty of worlds destroy as upgrade
|
||||||
if(event.type == "ITEM_DESTROYED") {
|
if (event.type == 'ITEM_DESTROYED') {
|
||||||
if (event.itemId == 3867) {
|
if (event.itemId == 3867) {
|
||||||
let suppItem : number = itemInfo.to.find((x:number) =>
|
const suppItem: number = itemInfo.to.find(
|
||||||
x == participant.item0
|
(x: number) =>
|
||||||
|| x == participant.item1
|
x == participant.item0 ||
|
||||||
|| x == participant.item2
|
x == participant.item1 ||
|
||||||
|| x == participant.item3
|
x == participant.item2 ||
|
||||||
|| x == participant.item4
|
x == participant.item3 ||
|
||||||
|| x == participant.item5
|
x == participant.item4 ||
|
||||||
|| x == participant.item6 )
|
x == participant.item5 ||
|
||||||
|
x == participant.item6
|
||||||
|
)
|
||||||
if (suppItem != undefined) {
|
if (suppItem != undefined) {
|
||||||
const already = builds.suppItems.find((x) => x.data == suppItem)
|
const already = builds.suppItems.find(x => x.data == suppItem)
|
||||||
if (already == undefined) builds.suppItems.push({ count: 1, data: suppItem })
|
if (already == undefined) builds.suppItems.push({ count: 1, data: suppItem })
|
||||||
else already.count += 1
|
else already.count += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(event.type != "ITEM_PURCHASED") continue;
|
if (event.type != 'ITEM_PURCHASED') continue
|
||||||
|
|
||||||
// Handle boots upgrades
|
// Handle boots upgrades
|
||||||
if(itemInfo.requiredBuffCurrencyName == "Feats_NoxianBootPurchaseBuff"
|
if (
|
||||||
|| itemInfo.requiredBuffCurrencyName == "Feats_SpecialQuestBootBuff") {
|
itemInfo.requiredBuffCurrencyName == 'Feats_NoxianBootPurchaseBuff' ||
|
||||||
continue;
|
itemInfo.requiredBuffCurrencyName == 'Feats_SpecialQuestBootBuff'
|
||||||
|
) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle boots differently
|
// Handle boots differently
|
||||||
if(itemInfo.categories.includes("Boots")){
|
if (itemInfo.categories.includes('Boots')) {
|
||||||
if (itemInfo.to.length == 0 || (itemInfo.to[0] >= 3171 && itemInfo.to[0] <= 3176)) {
|
if (itemInfo.to.length == 0 || (itemInfo.to[0] >= 3171 && itemInfo.to[0] <= 3176)) {
|
||||||
// Check for bootsFirst
|
// Check for bootsFirst
|
||||||
if (items.length < 2) {
|
if (items.length < 2) {
|
||||||
@@ -134,26 +153,26 @@ function handleMatchItems(timeline, participant: any, participantIndex : number,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add to boots
|
// Add to boots
|
||||||
const already = builds.boots.find((x) => x.data == event.itemId)
|
const already = builds.boots.find(x => x.data == event.itemId)
|
||||||
if (already == undefined) builds.boots.push({ count: 1, data: event.itemId })
|
if (already == undefined) builds.boots.push({ count: 1, data: event.itemId })
|
||||||
else already.count += 1
|
else already.count += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if item should be included
|
// Check if item should be included
|
||||||
if(itemInfo.categories.includes("Consumable")) continue;
|
if (itemInfo.categories.includes('Consumable')) continue
|
||||||
if(itemInfo.categories.includes("Trinket")) continue;
|
if (itemInfo.categories.includes('Trinket')) continue
|
||||||
|
|
||||||
// Ignore zephyr
|
// Ignore zephyr
|
||||||
if(event.itemId == 3172) continue;
|
if (event.itemId == 3172) continue
|
||||||
|
|
||||||
// Ignore Cull as not-first item
|
// Ignore Cull as not-first item
|
||||||
if(event.itemId == 1083 && items.length >= 1) continue;
|
if (event.itemId == 1083 && items.length >= 1) continue
|
||||||
|
|
||||||
// Ignore non-final items, except when first item bought
|
// Ignore non-final items, except when first item bought
|
||||||
if(itemInfo.to.length != 0 && items.length >= 1) continue;
|
if (itemInfo.to.length != 0 && items.length >= 1) continue
|
||||||
|
|
||||||
items.push(event.itemId)
|
items.push(event.itemId)
|
||||||
}
|
}
|
||||||
@@ -164,43 +183,57 @@ function handleMatchItems(timeline, participant: any, participantIndex : number,
|
|||||||
|
|
||||||
// Start items
|
// Start items
|
||||||
if (items.length >= 1) {
|
if (items.length >= 1) {
|
||||||
const already = builds.start.find((x) => x.data == items[0])
|
const already = builds.start.find(x => x.data == items[0])
|
||||||
if (already == undefined) builds.start.push({ count: 1, data: items[0] })
|
if (already == undefined) builds.start.push({ count: 1, data: items[0] })
|
||||||
else already.count += 1
|
else already.count += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Late game items
|
// Late game items
|
||||||
for(let item of items.slice(3)) {
|
for (const item of items.slice(3)) {
|
||||||
const already = builds.lateGame.find((x) => x.data == item)
|
const already = builds.lateGame.find(x => x.data == item)
|
||||||
if (already == undefined) builds.lateGame.push({ count: 1, data: item })
|
if (already == undefined) builds.lateGame.push({ count: 1, data: item })
|
||||||
else already.count += 1
|
else already.count += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMatch(match: any, champions: Map<number, ChampionData>) {
|
function handleMatch(match: any, champions: Map<number, ChampionData>) {
|
||||||
let participantIndex = 0;
|
let participantIndex = 0
|
||||||
for(let participant of match.info.participants) {
|
for (const participant of match.info.participants) {
|
||||||
participantIndex += 1
|
participantIndex += 1
|
||||||
const championId = participant.championId
|
const championId = participant.championId
|
||||||
const champion = champions.get(championId)
|
const champion = champions.get(championId)
|
||||||
|
|
||||||
// Lanes
|
// Lanes
|
||||||
let lane = champion.lanes.find((x) => x.data == participant.teamPosition)
|
let lane = champion.lanes.find(x => x.data == participant.teamPosition)
|
||||||
if (lane == undefined) {
|
if (lane == undefined) {
|
||||||
const builds : Builds = {tree:treeInit(), start: [], bootsFirst: 0, boots: [], lateGame: [], suppItems: []}
|
const builds: Builds = {
|
||||||
lane = {count:1, data: participant.teamPosition, runes:[], builds:builds, winningMatches: 0, losingMatches: 0, winrate: 0, pickrate: 0}
|
tree: treeInit(),
|
||||||
champion.lanes.push(lane)
|
start: [],
|
||||||
|
bootsFirst: 0,
|
||||||
|
boots: [],
|
||||||
|
lateGame: [],
|
||||||
|
suppItems: []
|
||||||
}
|
}
|
||||||
else lane.count += 1
|
lane = {
|
||||||
|
count: 1,
|
||||||
|
data: participant.teamPosition,
|
||||||
|
runes: [],
|
||||||
|
builds: builds,
|
||||||
|
winningMatches: 0,
|
||||||
|
losingMatches: 0,
|
||||||
|
winrate: 0,
|
||||||
|
pickrate: 0
|
||||||
|
}
|
||||||
|
champion.lanes.push(lane)
|
||||||
|
} else lane.count += 1
|
||||||
|
|
||||||
// Winrate
|
// Winrate
|
||||||
if (participant.win) {
|
if (participant.win) {
|
||||||
champion.winningMatches++;
|
champion.winningMatches++
|
||||||
lane.winningMatches++;
|
lane.winningMatches++
|
||||||
}
|
} else {
|
||||||
else {
|
champion.losingMatches++
|
||||||
champion.losingMatches++;
|
lane.losingMatches++
|
||||||
lane.losingMatches++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runes
|
// Runes
|
||||||
@@ -211,16 +244,22 @@ function handleMatch(match: any, champions : Map<number, ChampionData>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMatchList(client: MongoClient, patch: string, champions: Map<number, ChampionData>) {
|
async function handleMatchList(
|
||||||
const database = client.db("matches");
|
client: MongoClient,
|
||||||
|
patch: string,
|
||||||
|
champions: Map<number, ChampionData>
|
||||||
|
) {
|
||||||
|
const database = client.db('matches')
|
||||||
const matches = database.collection(patch)
|
const matches = database.collection(patch)
|
||||||
const allMatches = matches.find()
|
const allMatches = matches.find()
|
||||||
const totalMatches: number = await matches.countDocuments()
|
const totalMatches: number = await matches.countDocuments()
|
||||||
|
|
||||||
let currentMatch = 0;
|
let currentMatch = 0
|
||||||
for await (let match of allMatches) {
|
for await (const match of allMatches) {
|
||||||
process.stdout.write("\rComputing champion stats, game entry " + currentMatch + "/" + totalMatches + " ... ")
|
process.stdout.write(
|
||||||
currentMatch += 1;
|
'\rComputing champion stats, game entry ' + currentMatch + '/' + totalMatches + ' ... '
|
||||||
|
)
|
||||||
|
currentMatch += 1
|
||||||
handleMatch(match, champions)
|
handleMatch(match, champions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,36 +267,33 @@ async function handleMatchList(client: MongoClient, patch: string, champions: Ma
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function finalizeChampionStats(champion: ChampionData, totalMatches: number) {
|
async function finalizeChampionStats(champion: ChampionData, totalMatches: number) {
|
||||||
let totalChampionMatches = champion.winningMatches + champion.losingMatches;
|
const totalChampionMatches = champion.winningMatches + champion.losingMatches
|
||||||
|
|
||||||
arrayRemovePercentage(champion.lanes, totalChampionMatches, 0.2)
|
arrayRemovePercentage(champion.lanes, totalChampionMatches, 0.2)
|
||||||
champion.lanes.sort((a, b) => b.count - a.count)
|
champion.lanes.sort((a, b) => b.count - a.count)
|
||||||
|
|
||||||
// Filter runes to keep 3 most played
|
// Filter runes to keep 3 most played
|
||||||
for(let lane of champion.lanes) {
|
for (const lane of champion.lanes) {
|
||||||
const runes = lane.runes
|
const runes = lane.runes
|
||||||
|
|
||||||
runes.sort((a, b) => b.count - a.count)
|
runes.sort((a, b) => b.count - a.count)
|
||||||
if(runes.length > 3)
|
if (runes.length > 3) runes.splice(3, runes.length - 3)
|
||||||
runes.splice(3, runes.length - 3)
|
|
||||||
// Compute runes pickrate
|
// Compute runes pickrate
|
||||||
for(let rune of runes)
|
for (const rune of runes) rune.pickrate = rune.count / lane.count
|
||||||
rune.pickrate = rune.count / lane.count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for(let lane of champion.lanes) {
|
for (const lane of champion.lanes) {
|
||||||
const builds = lane.builds
|
const builds = lane.builds
|
||||||
|
|
||||||
// Cut item tree branches to keep only 4 branches every time and with percentage threshold
|
// Cut item tree branches to keep only 4 branches every time and with percentage threshold
|
||||||
builds.tree.count = lane.count;
|
builds.tree.count = lane.count
|
||||||
treeCutBranches(builds.tree, 4, 0.05)
|
treeCutBranches(builds.tree, 4, 0.05)
|
||||||
treeSort(builds.tree)
|
treeSort(builds.tree)
|
||||||
|
|
||||||
// Cut item start, to only 4 and with percentage threshold
|
// Cut item start, to only 4 and with percentage threshold
|
||||||
arrayRemovePercentage(builds.start, lane.count, 0.05)
|
arrayRemovePercentage(builds.start, lane.count, 0.05)
|
||||||
builds.start.sort((a, b) => b.count - a.count)
|
builds.start.sort((a, b) => b.count - a.count)
|
||||||
if(builds.start.length > 4)
|
if (builds.start.length > 4) builds.start.splice(4, builds.start.length - 4)
|
||||||
builds.start.splice(4, builds.start.length - 4)
|
|
||||||
|
|
||||||
// Remove boots that are not within percentage threshold
|
// Remove boots that are not within percentage threshold
|
||||||
arrayRemovePercentage(builds.boots, lane.count, 0.05)
|
arrayRemovePercentage(builds.boots, lane.count, 0.05)
|
||||||
@@ -268,8 +304,7 @@ async function finalizeChampionStats(champion: ChampionData, totalMatches: numbe
|
|||||||
// Cut supp items below 2 and percentage threshold
|
// Cut supp items below 2 and percentage threshold
|
||||||
arrayRemovePercentage(builds.suppItems, lane.count, 0.05)
|
arrayRemovePercentage(builds.suppItems, lane.count, 0.05)
|
||||||
builds.suppItems.sort((a, b) => b.count - a.count)
|
builds.suppItems.sort((a, b) => b.count - a.count)
|
||||||
if(builds.suppItems.length > 2)
|
if (builds.suppItems.length > 2) builds.suppItems.splice(2, builds.suppItems.length - 2)
|
||||||
builds.suppItems.splice(2, builds.suppItems.length - 2)
|
|
||||||
|
|
||||||
// Delete supp items if empty
|
// Delete supp items if empty
|
||||||
if (builds.suppItems.length == 0) delete builds.suppItems
|
if (builds.suppItems.length == 0) delete builds.suppItems
|
||||||
@@ -277,39 +312,42 @@ async function finalizeChampionStats(champion: ChampionData, totalMatches: numbe
|
|||||||
builds.lateGame.sort((a, b) => b.count - a.count)
|
builds.lateGame.sort((a, b) => b.count - a.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
for(let lane of champion.lanes) {
|
for (const lane of champion.lanes) {
|
||||||
lane.winrate = lane.winningMatches / lane.count
|
lane.winrate = lane.winningMatches / lane.count
|
||||||
lane.pickrate = lane.count / totalMatches
|
lane.pickrate = lane.count / totalMatches
|
||||||
}
|
}
|
||||||
|
|
||||||
return {name: champion.champion.name,
|
return {
|
||||||
|
name: champion.champion.name,
|
||||||
alias: champion.champion.alias.toLowerCase(),
|
alias: champion.champion.alias.toLowerCase(),
|
||||||
id: champion.champion.id,
|
id: champion.champion.id,
|
||||||
lanes: champion.lanes,
|
lanes: champion.lanes,
|
||||||
winrate: champion.winningMatches / totalChampionMatches,
|
winrate: champion.winningMatches / totalChampionMatches,
|
||||||
gameCount: totalChampionMatches,
|
gameCount: totalChampionMatches,
|
||||||
pickrate: totalChampionMatches/totalMatches,
|
pickrate: totalChampionMatches / totalMatches
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function championList() {
|
async function championList() {
|
||||||
const response = await fetch("https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json");
|
const response = await fetch(
|
||||||
|
'https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
|
||||||
|
)
|
||||||
const list = await response.json()
|
const list = await response.json()
|
||||||
return list.slice(1)
|
return list.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function makeChampionsStats(client: MongoClient, patch: string) {
|
async function makeChampionsStats(client: MongoClient, patch: string) {
|
||||||
var globalItems = await itemList()
|
const globalItems = await itemList()
|
||||||
for(let item of globalItems) {
|
for (const item of globalItems) {
|
||||||
itemDict.set(item.id, item)
|
itemDict.set(item.id, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = await championList()
|
const list = await championList()
|
||||||
console.log("Generating stats for " + list.length + " champions")
|
console.log('Generating stats for ' + list.length + ' champions')
|
||||||
|
|
||||||
// Pre-generate list of champions
|
// Pre-generate list of champions
|
||||||
const champions: Map<number, ChampionData> = new Map()
|
const champions: Map<number, ChampionData> = new Map()
|
||||||
for(let champion of list) {
|
for (const champion of list) {
|
||||||
champions.set(champion.id, {
|
champions.set(champion.id, {
|
||||||
champion: { id: champion.id, name: champion.name, alias: champion.alias },
|
champion: { id: champion.id, name: champion.name, alias: champion.alias },
|
||||||
winningMatches: 0,
|
winningMatches: 0,
|
||||||
@@ -322,9 +360,9 @@ async function makeChampionsStats(client: MongoClient, patch : string) {
|
|||||||
const totalMatches = await handleMatchList(client, patch, champions)
|
const totalMatches = await handleMatchList(client, patch, champions)
|
||||||
|
|
||||||
// Finalize and save stats for every champion
|
// Finalize and save stats for every champion
|
||||||
const database = client.db("champions")
|
const database = client.db('champions')
|
||||||
const collection = database.collection(patch)
|
const collection = database.collection(patch)
|
||||||
for(let champion of list) {
|
for (const champion of list) {
|
||||||
const championInfo = await finalizeChampionStats(champions.get(champion.id), totalMatches)
|
const championInfo = await finalizeChampionStats(champions.get(champion.id), totalMatches)
|
||||||
await collection.updateOne({ id: champion.id }, { $set: championInfo }, { upsert: true })
|
await collection.updateOne({ id: champion.id }, { $set: championInfo }, { upsert: true })
|
||||||
}
|
}
|
||||||
|
|||||||
16
match_collector/eslint.config.js
Normal file
16
match_collector/eslint.config.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'eslint/config'
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import prettier from 'eslint-config-prettier'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
prettier,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
semi: 'off',
|
||||||
|
'prefer-const': 'error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
@@ -1,65 +1,67 @@
|
|||||||
const base = "https://euw1.api.riotgames.com"
|
const base = 'https://euw1.api.riotgames.com'
|
||||||
const api_key = process.env.RIOT_API_KEY
|
const api_key = process.env.RIOT_API_KEY
|
||||||
const sleep_minutes = 12
|
const sleep_minutes = 12
|
||||||
|
|
||||||
import { MongoClient } from 'mongodb'
|
import { MongoClient } from 'mongodb'
|
||||||
|
|
||||||
import champion_stat from "./champion_stat"
|
import champion_stat from './champion_stat'
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Check if we're in development mode with pre-loaded data
|
// Check if we're in development mode with pre-loaded data
|
||||||
if (process.env.NODE_ENV === 'development' && process.env.USE_IMPORTED_DATA === 'true') {
|
if (process.env.NODE_ENV === 'development' && process.env.USE_IMPORTED_DATA === 'true') {
|
||||||
console.log("MatchCollector - Development mode with pre-loaded data");
|
console.log('MatchCollector - Development mode with pre-loaded data')
|
||||||
await runWithPreloadedData();
|
await runWithPreloadedData()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original production mode
|
// Original production mode
|
||||||
console.log("MatchCollector - Hello !");
|
console.log('MatchCollector - Hello !')
|
||||||
const client = await connectToDatabase();
|
const client = await connectToDatabase()
|
||||||
const [latestPatch, latestPatchTime] = await fetchLatestPatchDate(client);
|
const [latestPatch, latestPatchTime] = await fetchLatestPatchDate(client)
|
||||||
console.log("Connected to database, latest patch " + latestPatch + " was epoch: " + latestPatchTime)
|
console.log(
|
||||||
|
'Connected to database, latest patch ' + latestPatch + ' was epoch: ' + latestPatchTime
|
||||||
|
)
|
||||||
|
|
||||||
const alreadySeenGameList = await alreadySeenGames(client, latestPatch);
|
const alreadySeenGameList = await alreadySeenGames(client, latestPatch)
|
||||||
console.log("We already have " + alreadySeenGameList.length + " matches for this patch !")
|
console.log('We already have ' + alreadySeenGameList.length + ' matches for this patch !')
|
||||||
|
|
||||||
console.log("Using RIOT_API_KEY: " + api_key)
|
console.log('Using RIOT_API_KEY: ' + api_key)
|
||||||
if(api_key != null && api_key != undefined && api_key != "") {
|
if (api_key != null && api_key != undefined && api_key != '') {
|
||||||
const challengerLeague = await fetchChallengerLeague();
|
const challengerLeague = await fetchChallengerLeague()
|
||||||
console.log("ChallengerLeague: got " + challengerLeague.entries.length + " entries");
|
console.log('ChallengerLeague: got ' + challengerLeague.entries.length + ' entries')
|
||||||
|
|
||||||
const gameList = [];
|
const gameList = []
|
||||||
let i = 0;
|
let i = 0
|
||||||
for(let challenger of challengerLeague.entries) {
|
for (const challenger of challengerLeague.entries) {
|
||||||
console.log("Entry " + i + "/" + challengerLeague.entries.length + " ...")
|
console.log('Entry ' + i + '/' + challengerLeague.entries.length + ' ...')
|
||||||
const puuid = challenger.puuid;
|
const puuid = challenger.puuid
|
||||||
const challengerGameList = await summonerGameList(puuid, latestPatchTime);
|
const challengerGameList = await summonerGameList(puuid, latestPatchTime)
|
||||||
for(let game of challengerGameList) {
|
for (const game of challengerGameList) {
|
||||||
if (!gameList.includes(game) && !alreadySeenGameList.includes(game)) {
|
if (!gameList.includes(game) && !alreadySeenGameList.includes(game)) {
|
||||||
gameList.push(game)
|
gameList.push(game)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i++;
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Games: got " + gameList.length + " entries");
|
console.log('Games: got ' + gameList.length + ' entries')
|
||||||
i = 0;
|
i = 0
|
||||||
for(let game of gameList) {
|
for (const game of gameList) {
|
||||||
console.log("Entry " + i + "/" + gameList.length + " ...")
|
console.log('Entry ' + i + '/' + gameList.length + ' ...')
|
||||||
const gameMatch = await match(game)
|
const gameMatch = await match(game)
|
||||||
const gameTimeline = await matchTimeline(game)
|
const gameTimeline = await matchTimeline(game)
|
||||||
gameMatch.timeline = gameTimeline
|
gameMatch.timeline = gameTimeline
|
||||||
await saveMatch(client, gameMatch, latestPatch)
|
await saveMatch(client, gameMatch, latestPatch)
|
||||||
i++;
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Generating stats...");
|
console.log('Generating stats...')
|
||||||
await champion_stat.makeChampionsStats(client, latestPatch)
|
await champion_stat.makeChampionsStats(client, latestPatch)
|
||||||
|
|
||||||
console.log("All done. Closing client.");
|
console.log('All done. Closing client.')
|
||||||
await client.close()
|
await client.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,15 +77,27 @@ async function handleRateLimit(url : URL) : Promise<Response> {
|
|||||||
|
|
||||||
function handleError(response: Response) {
|
function handleError(response: Response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.log("Error during fetch(" + response.url + "): STATUS " + response.status + " (" + response.statusText + ")");
|
console.log(
|
||||||
process.exit(1);
|
'Error during fetch(' +
|
||||||
|
response.url +
|
||||||
|
'): STATUS ' +
|
||||||
|
response.status +
|
||||||
|
' (' +
|
||||||
|
response.statusText +
|
||||||
|
')'
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connectToDatabase() {
|
async function connectToDatabase() {
|
||||||
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
|
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
|
||||||
let uri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}`
|
let uri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}`
|
||||||
if(process.env.MONGO_URI != undefined && process.env.MONGO_URI != null && process.env.MONGO_URI != "") {
|
if (
|
||||||
|
process.env.MONGO_URI != undefined &&
|
||||||
|
process.env.MONGO_URI != null &&
|
||||||
|
process.env.MONGO_URI != ''
|
||||||
|
) {
|
||||||
uri = process.env.MONGO_URI
|
uri = process.env.MONGO_URI
|
||||||
}
|
}
|
||||||
const client = new MongoClient(uri)
|
const client = new MongoClient(uri)
|
||||||
@@ -92,71 +106,71 @@ async function connectToDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLatestPatchDate(client) {
|
async function fetchLatestPatchDate(client) {
|
||||||
const database = client.db("patches");
|
const database = client.db('patches')
|
||||||
const patches = database.collection("patches");
|
const patches = database.collection('patches')
|
||||||
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
||||||
return [latestPatch.patch, Math.floor(latestPatch.date.valueOf() / 1000)]
|
return [latestPatch.patch, Math.floor(latestPatch.date.valueOf() / 1000)]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchChallengerLeague() {
|
async function fetchChallengerLeague() {
|
||||||
const queue = "RANKED_SOLO_5x5"
|
const queue = 'RANKED_SOLO_5x5'
|
||||||
const endpoint = `/lol/league/v4/challengerleagues/by-queue/${queue}`
|
const endpoint = `/lol/league/v4/challengerleagues/by-queue/${queue}`
|
||||||
const url = `${base}${endpoint}?api_key=${api_key}`
|
const url = `${base}${endpoint}?api_key=${api_key}`
|
||||||
|
|
||||||
const challengerLeagueResponse = await handleRateLimit(new URL(url));
|
const challengerLeagueResponse = await handleRateLimit(new URL(url))
|
||||||
|
|
||||||
handleError(challengerLeagueResponse)
|
handleError(challengerLeagueResponse)
|
||||||
|
|
||||||
const challengerLeague = await challengerLeagueResponse.json();
|
const challengerLeague = await challengerLeagueResponse.json()
|
||||||
return challengerLeague;
|
return challengerLeague
|
||||||
}
|
}
|
||||||
|
|
||||||
async function summonerGameList(puuid, startTime) {
|
async function summonerGameList(puuid, startTime) {
|
||||||
const base = "https://europe.api.riotgames.com"
|
const base = 'https://europe.api.riotgames.com'
|
||||||
const endpoint = `/lol/match/v5/matches/by-puuid/${puuid}/ids`;
|
const endpoint = `/lol/match/v5/matches/by-puuid/${puuid}/ids`
|
||||||
const url = `${base}${endpoint}?queue=420&type=ranked&startTime=${startTime}&api_key=${api_key}`
|
const url = `${base}${endpoint}?queue=420&type=ranked&startTime=${startTime}&api_key=${api_key}`
|
||||||
|
|
||||||
const gameListResponse = await handleRateLimit(new URL(url));
|
const gameListResponse = await handleRateLimit(new URL(url))
|
||||||
handleError(gameListResponse)
|
handleError(gameListResponse)
|
||||||
const gameList = await gameListResponse.json();
|
const gameList = await gameListResponse.json()
|
||||||
|
|
||||||
return gameList;
|
return gameList
|
||||||
}
|
}
|
||||||
|
|
||||||
async function match(matchId) {
|
async function match(matchId) {
|
||||||
const base = "https://europe.api.riotgames.com"
|
const base = 'https://europe.api.riotgames.com'
|
||||||
const endpoint = `/lol/match/v5/matches/${matchId}`
|
const endpoint = `/lol/match/v5/matches/${matchId}`
|
||||||
const url = `${base}${endpoint}?api_key=${api_key}`
|
const url = `${base}${endpoint}?api_key=${api_key}`
|
||||||
|
|
||||||
const matchResponse = await handleRateLimit(new URL(url))
|
const matchResponse = await handleRateLimit(new URL(url))
|
||||||
handleError(matchResponse)
|
handleError(matchResponse)
|
||||||
const match = await matchResponse.json();
|
const match = await matchResponse.json()
|
||||||
|
|
||||||
return match;
|
return match
|
||||||
}
|
}
|
||||||
|
|
||||||
async function matchTimeline(matchId) {
|
async function matchTimeline(matchId) {
|
||||||
const base = "https://europe.api.riotgames.com"
|
const base = 'https://europe.api.riotgames.com'
|
||||||
const endpoint = `/lol/match/v5/matches/${matchId}/timeline`
|
const endpoint = `/lol/match/v5/matches/${matchId}/timeline`
|
||||||
const url = `${base}${endpoint}?api_key=${api_key}`
|
const url = `${base}${endpoint}?api_key=${api_key}`
|
||||||
|
|
||||||
const timelineResponse = await handleRateLimit(new URL(url))
|
const timelineResponse = await handleRateLimit(new URL(url))
|
||||||
handleError(timelineResponse)
|
handleError(timelineResponse)
|
||||||
const timeline = await timelineResponse.json();
|
const timeline = await timelineResponse.json()
|
||||||
|
|
||||||
return timeline
|
return timeline
|
||||||
}
|
}
|
||||||
|
|
||||||
async function alreadySeenGames(client, latestPatch) {
|
async function alreadySeenGames(client, latestPatch) {
|
||||||
const database = client.db("matches")
|
const database = client.db('matches')
|
||||||
const matches = database.collection(latestPatch)
|
const matches = database.collection(latestPatch)
|
||||||
|
|
||||||
const alreadySeen = await matches.distinct("metadata.matchId")
|
const alreadySeen = await matches.distinct('metadata.matchId')
|
||||||
return alreadySeen
|
return alreadySeen
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveMatch(client, match, latestPatch) {
|
async function saveMatch(client, match, latestPatch) {
|
||||||
const database = client.db("matches")
|
const database = client.db('matches')
|
||||||
const matches = database.collection(latestPatch)
|
const matches = database.collection(latestPatch)
|
||||||
await matches.insertOne(match)
|
await matches.insertOne(match)
|
||||||
}
|
}
|
||||||
@@ -165,43 +179,40 @@ async function saveMatch(client, match, latestPatch) {
|
|||||||
* Development mode function that generates stats from pre-loaded data
|
* Development mode function that generates stats from pre-loaded data
|
||||||
*/
|
*/
|
||||||
async function runWithPreloadedData() {
|
async function runWithPreloadedData() {
|
||||||
console.log("Using pre-loaded match data for development");
|
console.log('Using pre-loaded match data for development')
|
||||||
|
|
||||||
const client = await connectToDatabase();
|
const client = await connectToDatabase()
|
||||||
try {
|
try {
|
||||||
const [latestPatch] = await fetchLatestPatchDate(client);
|
const [latestPatch] = await fetchLatestPatchDate(client)
|
||||||
console.log(`Latest patch: ${latestPatch}`);
|
console.log(`Latest patch: ${latestPatch}`)
|
||||||
|
|
||||||
// Check if we have matches for this patch
|
// Check if we have matches for this patch
|
||||||
const matchesDb = client.db("matches");
|
const matchesDb = client.db('matches')
|
||||||
const collections = await matchesDb.listCollections().toArray();
|
const collections = await matchesDb.listCollections().toArray()
|
||||||
const patchCollections = collections
|
const patchCollections = collections.map(c => c.name).filter(name => name === latestPatch)
|
||||||
.map(c => c.name)
|
|
||||||
.filter(name => name === latestPatch);
|
|
||||||
|
|
||||||
if (patchCollections.length === 0) {
|
if (patchCollections.length === 0) {
|
||||||
console.error(`❌ No match data found for patch ${latestPatch}`);
|
console.error(`❌ No match data found for patch ${latestPatch}`)
|
||||||
console.log("💡 Please run the data import script first:");
|
console.log('💡 Please run the data import script first:')
|
||||||
console.log(" node dev/scripts/setup-db.js");
|
console.log(' node dev/scripts/setup-db.js')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Found ${patchCollections.length} match collection(s)`);
|
console.log(`Found ${patchCollections.length} match collection(s)`)
|
||||||
|
|
||||||
// Generate stats for each patch with data
|
// Generate stats for each patch with data
|
||||||
for (const patch of patchCollections) {
|
for (const patch of patchCollections) {
|
||||||
console.log(`Generating stats for patch ${patch}...`);
|
console.log(`Generating stats for patch ${patch}...`)
|
||||||
await champion_stat.makeChampionsStats(client, patch);
|
await champion_stat.makeChampionsStats(client, patch)
|
||||||
console.log(`Stats generated for patch ${patch}`);
|
console.log(`Stats generated for patch ${patch}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🎉 All stats generated successfully!");
|
console.log('🎉 All stats generated successfully!')
|
||||||
console.log("🚀 Your development database is ready for frontend testing!");
|
console.log('🚀 Your development database is ready for frontend testing!')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Error in development mode:", error);
|
console.error('❌ Error in development mode:', error)
|
||||||
throw error;
|
throw error
|
||||||
} finally {
|
} finally {
|
||||||
await client.close();
|
await client.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ type ItemTree = {
|
|||||||
data: any
|
data: any
|
||||||
count: number
|
count: number
|
||||||
children: Array<ItemTree>
|
children: Array<ItemTree>
|
||||||
};
|
}
|
||||||
|
|
||||||
function treeInit(): ItemTree {
|
function treeInit(): ItemTree {
|
||||||
return { data: undefined, count: 0, children: [] }
|
return { data: undefined, count: 0, children: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function treeNode(data: number, count: number): ItemTree {
|
function treeNode(data: number, count: number): ItemTree {
|
||||||
return { data: data, count: count, children: [] }
|
return { data: data, count: count, children: [] }
|
||||||
}
|
}
|
||||||
@@ -17,16 +16,16 @@ function treeNode(data : number, count : number) : ItemTree {
|
|||||||
* Merge a node with an item tree
|
* Merge a node with an item tree
|
||||||
*/
|
*/
|
||||||
function nodeMerge(itemtree: ItemTree, node: ItemTree) {
|
function nodeMerge(itemtree: ItemTree, node: ItemTree) {
|
||||||
const item = node.data;
|
const item = node.data
|
||||||
const count = node.count;
|
const count = node.count
|
||||||
let next : ItemTree | null = null;
|
let next: ItemTree | null = null
|
||||||
|
|
||||||
// Try to find an existing node in this tree level with same item
|
// Try to find an existing node in this tree level with same item
|
||||||
for(let node of itemtree.children) {
|
for (const node of itemtree.children) {
|
||||||
if (node.data == item) {
|
if (node.data == item) {
|
||||||
node.count += 1;
|
node.count += 1
|
||||||
next = node;
|
next = node
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,16 +35,16 @@ function nodeMerge(itemtree : ItemTree, node : ItemTree) {
|
|||||||
itemtree.children.push(next)
|
itemtree.children.push(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
return next;
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Merge a full build path with an existing item tree
|
* Merge a full build path with an existing item tree
|
||||||
*/
|
*/
|
||||||
function treeMerge(itemtree: ItemTree, items: Array<number>) {
|
function treeMerge(itemtree: ItemTree, items: Array<number>) {
|
||||||
let current = itemtree;
|
let current = itemtree
|
||||||
|
|
||||||
for(let item of items) {
|
for (const item of items) {
|
||||||
current = nodeMerge(current, { data: item, count: 1, children: [] })
|
current = nodeMerge(current, { data: item, count: 1, children: [] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,27 +52,30 @@ function treeMerge(itemtree : ItemTree, items : Array<number>) {
|
|||||||
function treeCutBranches(itemtree: ItemTree, thresholdCount: number, thresholdPerc: number) {
|
function treeCutBranches(itemtree: ItemTree, thresholdCount: number, thresholdPerc: number) {
|
||||||
// Remove branches that are above threshold count
|
// Remove branches that are above threshold count
|
||||||
while (itemtree.children.length > thresholdCount) {
|
while (itemtree.children.length > thresholdCount) {
|
||||||
let leastUsedBranch = itemtree.children.reduce((a, b) => Math.min(a.count, b.count) == a.count ? a : b, {data:undefined, count: +Infinity, children: []})
|
const leastUsedBranch = itemtree.children.reduce(
|
||||||
|
(a, b) => (Math.min(a.count, b.count) == a.count ? a : b),
|
||||||
|
{ data: undefined, count: +Infinity, children: [] }
|
||||||
|
)
|
||||||
itemtree.children.splice(itemtree.children.indexOf(leastUsedBranch), 1)
|
itemtree.children.splice(itemtree.children.indexOf(leastUsedBranch), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove branches that are of too low usage
|
// Remove branches that are of too low usage
|
||||||
let toRemove : Array<ItemTree> = []
|
const toRemove: Array<ItemTree> = []
|
||||||
for(let child of itemtree.children) {
|
for (const child of itemtree.children) {
|
||||||
if((child.count/itemtree.count) < thresholdPerc) {
|
if (child.count / itemtree.count < thresholdPerc) {
|
||||||
toRemove.push(child)
|
toRemove.push(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(let tr of toRemove) {
|
for (const tr of toRemove) {
|
||||||
itemtree.children.splice(itemtree.children.indexOf(tr), 1)
|
itemtree.children.splice(itemtree.children.indexOf(tr), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
itemtree.children.map((x) => treeCutBranches(x, thresholdCount, thresholdPerc))
|
itemtree.children.map(x => treeCutBranches(x, thresholdCount, thresholdPerc))
|
||||||
}
|
}
|
||||||
|
|
||||||
function treeMergeTree(itemtree1: ItemTree, itemtree2: ItemTree) {
|
function treeMergeTree(itemtree1: ItemTree, itemtree2: ItemTree) {
|
||||||
for(let child of itemtree2.children) {
|
for (const child of itemtree2.children) {
|
||||||
let node = nodeMerge(itemtree1, child)
|
const node = nodeMerge(itemtree1, child)
|
||||||
treeMergeTree(node, child)
|
treeMergeTree(node, child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +83,7 @@ function treeMergeTree(itemtree1: ItemTree, itemtree2: ItemTree) {
|
|||||||
function treeSort(itemtree: ItemTree) {
|
function treeSort(itemtree: ItemTree) {
|
||||||
itemtree.children.sort((a, b) => b.count - a.count)
|
itemtree.children.sort((a, b) => b.count - a.count)
|
||||||
|
|
||||||
for(let item of itemtree.children) {
|
for (const item of itemtree.children) {
|
||||||
treeSort(item)
|
treeSort(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1430
match_collector/package-lock.json
generated
1430
match_collector/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,11 @@
|
|||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint --fix .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@@ -13,7 +17,15 @@
|
|||||||
"mongodb": "^6.10.0"
|
"mongodb": "^6.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.2",
|
||||||
"@types/node": "^22.9.1",
|
"@types/node": "^22.9.1",
|
||||||
"tsx": "^4.19.2"
|
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
||||||
|
"@typescript-eslint/parser": "^8.53.1",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"prettier": "^3.8.0",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.53.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
10
patch_detector/.prettierrc
Normal file
10
patch_detector/.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
16
patch_detector/eslint.config.js
Normal file
16
patch_detector/eslint.config.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'eslint/config'
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import prettier from 'eslint-config-prettier'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
prettier,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
semi: 'off',
|
||||||
|
'prefer-const': 'error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from 'mongodb'
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ async function main() {
|
|||||||
const client = await connectToDatabase()
|
const client = await connectToDatabase()
|
||||||
const newPatch = await fetchLatestPatch()
|
const newPatch = await fetchLatestPatch()
|
||||||
|
|
||||||
console.log("Latest patch is: " + newPatch)
|
console.log('Latest patch is: ' + newPatch)
|
||||||
|
|
||||||
const newDate = new Date()
|
const newDate = new Date()
|
||||||
if (!(await compareLatestSavedPatch(client, newPatch, newDate))) {
|
if (!(await compareLatestSavedPatch(client, newPatch, newDate))) {
|
||||||
@@ -16,19 +16,22 @@ async function main() {
|
|||||||
await client.close()
|
await client.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function fetchLatestPatch() {
|
async function fetchLatestPatch() {
|
||||||
const url = "https://ddragon.leagueoflegends.com/api/versions.json"
|
const url = 'https://ddragon.leagueoflegends.com/api/versions.json'
|
||||||
const patchDataResponse = await fetch(url);
|
const patchDataResponse = await fetch(url)
|
||||||
const patchData = await patchDataResponse.json();
|
const patchData = await patchDataResponse.json()
|
||||||
const patch : string = patchData[0];
|
const patch: string = patchData[0]
|
||||||
return patch;
|
return patch
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connectToDatabase() {
|
async function connectToDatabase() {
|
||||||
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
|
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
|
||||||
let uri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}`
|
let uri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}`
|
||||||
if(process.env.MONGO_URI != undefined && process.env.MONGO_URI != null && process.env.MONGO_URI != "") {
|
if (
|
||||||
|
process.env.MONGO_URI != undefined &&
|
||||||
|
process.env.MONGO_URI != null &&
|
||||||
|
process.env.MONGO_URI != ''
|
||||||
|
) {
|
||||||
uri = process.env.MONGO_URI
|
uri = process.env.MONGO_URI
|
||||||
}
|
}
|
||||||
const client = new MongoClient(uri)
|
const client = new MongoClient(uri)
|
||||||
@@ -37,14 +40,14 @@ async function connectToDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function compareLatestSavedPatch(client: MongoClient, newPatch: string, newDate: Date) {
|
async function compareLatestSavedPatch(client: MongoClient, newPatch: string, newDate: Date) {
|
||||||
const database = client.db("patches")
|
const database = client.db('patches')
|
||||||
const patches = database.collection("patches")
|
const patches = database.collection('patches')
|
||||||
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
||||||
|
|
||||||
if (latestPatch == null) {
|
if (latestPatch == null) {
|
||||||
console.log("No previous patch recorded in database.")
|
console.log('No previous patch recorded in database.')
|
||||||
} else {
|
} else {
|
||||||
console.log("Latest patch in database is: " + latestPatch.patch)
|
console.log('Latest patch in database is: ' + latestPatch.patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (latestPatch == null || latestPatch.patch != newPatch) {
|
if (latestPatch == null || latestPatch.patch != newPatch) {
|
||||||
@@ -55,6 +58,4 @@ async function compareLatestSavedPatch(client: MongoClient, newPatch : string, n
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadAssets() {
|
async function downloadAssets() {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
1430
patch_detector/package-lock.json
generated
1430
patch_detector/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,13 @@
|
|||||||
"name": "patch_detector",
|
"name": "patch_detector",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"type": "commonjs",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint --fix .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@@ -13,7 +17,15 @@
|
|||||||
"mongodb": "^6.10.0"
|
"mongodb": "^6.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.2",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"tsx": "^4.19.2"
|
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
||||||
|
"@typescript-eslint/parser": "^8.53.1",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"prettier": "^3.8.0",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.53.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user