Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ff7c42f
Use encodeURI to generate urls
KevinLu Nov 8, 2020
b69e5bf
Implement summonerIcon
KevinLu Nov 8, 2020
0e22777
Add toast and error state for summonerIcon
KevinLu Nov 8, 2020
de4eb5e
Add region
KevinLu Nov 8, 2020
cbabfd9
Merge branch 'getdata' into homepage
zalsaigh Nov 8, 2020
294eb64
Merge branch 'getdata' into homepage
KevinLu Nov 8, 2020
4995258
Remove summonericon feature
KevinLu Nov 8, 2020
e763362
Changed around the error statuses of some messages. Also added champi…
zalsaigh Nov 8, 2020
dcc09e7
Merge branch 'homepage' of https://github.com/KevinLu/tlhacks into ho…
zalsaigh Nov 8, 2020
b55daef
batch championList
LeMichael88 Nov 8, 2020
7a47cff
Only get summoner icon when you click find comps
KevinLu Nov 8, 2020
d1f7644
Merge branch 'getdata' into homepage
KevinLu Nov 8, 2020
2c239ea
Update api.js
LeMichael88 Nov 8, 2020
6bea546
Merge branch 'getdata' into homepage
KevinLu Nov 8, 2020
e3d0348
It works
KevinLu Nov 8, 2020
028b678
Made some bugfixes + Implemented MonkeyKing Workaround
zalsaigh Nov 8, 2020
f45c45b
Final push
KevinLu Nov 8, 2020
7200c4a
Update README.md
KevinLu Nov 8, 2020
92e2ee8
Update README.md
NolanDSouza Nov 8, 2020
deb66e9
updated the app - revert to previous version if need it
zalsaigh Oct 7, 2021
ca02673
Update README.md
zalsaigh Oct 7, 2021
01d8e31
Added some clarifications for read me
KevinLu Oct 18, 2021
e46a6a8
Update .gitignore
KevinLu Oct 18, 2021
3e1bc6d
Merge pull request #5 from KevinLu/add-dev-keys-to-gitignore
KevinLu Oct 19, 2021
e0519d5
chore: update package-lock.json
KevinLu Oct 19, 2021
719ab9d
Merge branch 'homepage' of https://github.com/KevinLu/comp.gg into ho…
KevinLu Oct 19, 2021
1bab531
load summoner icon onBlur; use Text for summoner name after submitting
KevinLu Oct 19, 2021
ef552d5
feat: summoner card improvements
KevinLu Oct 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Keys
dev_keys.js
backend/config/dev_keys.js

# Logs
logs
*.log
Expand Down
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
# tlhacks
# COMP.GG
## ⚠ UPDATE: use branch `homepage` for latest version!
INSTRUCTIONS TO RUN COMP.GG AND REASONING FOR IT BEING LIKE THIS NOW:

1. GET YOUR OWN RIOT DEV KEY AND REPLACE THE PLACEHOLDER WITH IT [backend/config/dev_keys.js](/backend/config/dev_keys.js)!
2. Just do `npm run dev`. If this doesn't work, you're gonna need to delete node_modules from both the root directory and the
[frontend](/frontend) directory. Then, run `npm install` in **both** the root directory and the frontend directory. Should work afterwards.

I've deleted the match history part of our calculation. The reason being is that **Riot has changed its match history API massively.**
It is much more specific and detailed, and is a lot more difficult to access for our purposes (we just wanted `championPlayed` from
each match). Deleting all of this has let our code work again, but now it is less accurate as it is only based on champion mastery.

Furthermore, I updated alot of the API links to reflect the most recent patch as Riot does not host legacy patch versions on their API.

LASTLY, I'm sharing this here rather than pushing these changes to `main` because I don't want to lose that code in case we ever want to
properly fix the match history issue.

## Inspiration

We wanted to make custom-built team compositions for our League of Legends Clash team, but we didn't want to sift through a [222 minute long Mobalytics article](https://mobalytics.gg/blog/everything-you-need-to-know-about-team-comps-and-teamfighting-in-league-of-legends/).

## What it does

Our tool allows you to pick a team composition theme and analyzes your summoner profile to determine up to 3 of your top champions that suit this playstyle.

We currently use a combination of champion mastery points and recent match history to identify your best champions. Then, we sort your champions based on [AI-generated champion classes](https://towardsdatascience.com/umap-and-k-means-to-classify-characters-league-of-legends-668a788cb3c1) to generate a team composition that fits one of 5 major playstyles (engage, disengage, poke & siege, pick, and split-push).

## How we built it

Frontend: React, HTML, CSS

Backend: Express.js, Node.js, Python (protoyping)

We also used the Riot Games Developer API.

## Challenges we ran into

Writing code efficiently to ensure we didn't exceed the rate limits for API calls

Classifying champions and team compositions in a non-subjective manner.

## Accomplishments that we're proud of

Creating a long-lasting tool that anyone can use during dozens of future Clash events!

## What we learned

New technologies such as Express.js and React

## What's next for COMP.GG

Identifying in-built synergies between certain champions (e.g. Malphite/Yasuo, Xayah/Rakan)
2 changes: 1 addition & 1 deletion backend/config/dev_keys.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module.exports = {
riotAPI: 'RGAPI-ffdec1d6-9142-44e1-a371-2ec5b86d7bf5',
riotAPI: '<insert_your_dev_key_here>',
};
105 changes: 87 additions & 18 deletions backend/routes/api.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,101 @@
const express = require('express');
const router = express.Router();
const router = express();
const Axios = require('axios');
const generateRiotAPIUrl = require('../services/generateRiotAPIUrl');
const getSummonerData = require('../services/getSummonerData');
const getChampionMasteries = require('../services/getChampionMasteries');
const rankSummonerChampionMastery = require('../services/rankSummonerChampionMastery');
const championRankings = require('../services/championRankings');
const champIdToName = require('../services/champIdToName');
const findChampions = require('../services/findChampions');
const { json } = require('express');

router.get('/', (req, res) => {
router.get('/', function (req, res) {
return res.status(200).json({success: true, msg: "NICE"});
});

router.get('/riot', async (req, res) => {
const url = generateRiotAPIUrl(req.query.region, req.query.summonerName);
const response = await Axios.get(url);

if (response.data.summonerLevel > 100) {
return res.status(200).json({success: true, data: "you are big mon"});
} else {
return res.status(200).json({success: true, data: "you are trash"});
router.get('/riot', async function (req, res) {
try {
const url = generateRiotAPIUrl(req.query.region, req.query.summonerName);
const response = await Axios.get(url);
if (response.data.summonerLevel > 100) {
return res.status(200).json({success: true, data: "you are big mon"});
} else {
return res.status(200).json({success: true, data: "you are trash"});
}
} catch (error) {
return res.status(200).json({success: false, data: "bruh doesnt exist"});
}
});

router.get('/summonerIcon', async (req, res) => {

router.get('/testingChampName', async function (req, res) {
const name = await champIdToName(req.query.champId);
return res.status(200).json({success: true, data: name});
});

router.post('/championList', async function (req, res) {
try {
const summoners = req.body.summoners;
let ret = [];
for (const summoner in summoners) {
const summonerData = await getSummonerData(req.body.region, summoners[summoner]);
const accountId = summonerData.accountId;
const id = summonerData.id;
let playerRole;
if (summoner == "topSummoner") {
playerRole = "Top";
} else if (summoner == "jungleSummoner") {
playerRole = "Jungle";
} else if (summoner == "midSummoner") {
playerRole = "Middle";
} else if (summoner == "botSummoner") {
playerRole = "Bottom";
} else {
playerRole = "Support";
}
console.log(playerRole);
const data = await findChampions(req.body.region, id, req.body.myPlaystyle, playerRole);
console.log(data);
ret.push(data);
}

let real = [];
for (let i = 0; i < 3; i++) {
let item = [];
for (let j = 0; j < 5; j++) {
item.push(ret[j][i]);
}
real.push(item);
}

return res.status(200).json({success: true, data: real, old: ret});
} catch (error) {
return res.status(200).json({success: false, data: "request error"});
}
});

router.get('/championIcon', async function (req, res) {
try {
let iconURL = "http://ddragon.leagueoflegends.com/cdn/11.20.1/img/champion/" + req.query.championName + ".png";
if (req.query.championName === "Wukong") {
iconURL = "http://ddragon.leagueoflegends.com/cdn/11.20.1/img/champion/MonkeyKing.png"
}
return res.status(200).json({success: true, data: iconURL});
} catch (error) {
return res.status(200).json({success: false, data: "Summoner name does not exist."});
}
});

router.get('/summonerIcon', async function (req, res) {
const url = generateRiotAPIUrl(req.query.region, req.query.summonerName);
console.log(url);
const response = await Axios.get(url);
const iconId = response.data.profileIconId;
const iconURL = "http://ddragon.leagueoflegends.com/cdn/10.22.1/img/profileicon/" + iconId + ".png";

return res.status(200).json({success: true, data: iconURL});
try {
const response = await Axios.get(url);
const iconId = response.data.profileIconId;
const iconURL = "http://ddragon.leagueoflegends.com/cdn/11.20.1/img/profileicon/" + iconId + ".png";
return res.status(200).json({success: true, data: iconURL});
} catch (error) {
return res.status(200).json({success: false, data: "Summoner name does not exist."});
}
});

module.exports = router;
18 changes: 18 additions & 0 deletions backend/services/champIdToName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const Axios = require('axios');

const champIdToName = async (champId) => {
const response = await Axios.get('http://ddragon.leagueoflegends.com/cdn/11.20.1/data/en_US/champion.json');
const champData = response.data.data;

for (const key in champData) {
if (champId == champData[key]['key']) {
if (champData[key]['id'] == "MonkeyKing") {
return "Wukong"
}
return champData[key]['id']
}
}
return "error";
}

module.exports = champIdToName
8 changes: 8 additions & 0 deletions backend/services/championRankings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const rankSummonerChampionMastery = require('./rankSummonerChampionMastery');

const championRankings = async (region, id) => {
const rankingsMastery = await rankSummonerChampionMastery(region, id);
return rankingsMastery;
}

module.exports = championRankings
136 changes: 136 additions & 0 deletions backend/services/findChampions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
const championRankings = require('./championRankings');
const champIdToName = require('./champIdToName');

const ENGAGE_CHAMPIONS = {
'Top': ["Vladimir", "Ryze", "Sion", "Teemo", "Chogath", "DrMundo", "Cassiopeia", "Heimerdinger", "Nasus", "Gragas",
"Mordekaiser", "Akali", "Kennen", "Volibear", "Gnar", "Sylas", "Neeko", "Singed", "Karma", "Malphite",
"Maokai", "Rumble", "Poppy", "Shen", "Lulu", "Ornn", "Wukong", "Sett"],
'Jungle': ["Nunu","Amumu","Rammus","Trundle","JarvanIV","Skarner","Poppy","Gragas","Shen","Volibear","Sejuani",
"Zac","Ivern","Sett","Olaf","XinZhao","Warwick","Twitch","Shaco","Elise","Wukong","LeeSin","Rengar",
"RekSai","Fiddlesticks","Morgana","Evelynn","Karthus","Nidalee","Taliyah","Ekko","Sylas","Lillia"],
'Middle': ["Tristana","Tryndamere","Irelia","Nocturne","Renekton","Garen","Talon","Graves","Jayce","Diana","Yasuo",
"Camille","Lucian","Zed","Yone","Sett","Annie","LeBlanc","Vladimir","Kassadin","Swain","Katarina",
"Rumble", "Pantheon","Akali","Kennen","Fizz","Zoe","Taliyah","Ekko","Qiyana","Sylas","Pyke","Galio",
"TwistedFate", "Morgana","Zilean","Karma","Malphite","Lux","Xerath","Ahri","Lulu","Lissandra","Syndra",
"AurelionSol", "Velkoz","Neeko"],
'Bottom': ["Ashe","Veigar","Swain","Heimerdinger","Ezreal","Varus","Syndra","Senna","Twitch","Vayne","Karthus",
"Cassiopeia","KogMaw","Tristana","Draven","Kaisa","Jinx","Lucian","Kalista","Xayah","Sivir",
"MissFortune","Caitlyn","Ziggs","Yasuo","Jhin","Aphelios"],
'Support': ["Maokai","Poppy","Pantheon","Shen","Senna","Pyke","Sett","Galio","Alistar","Blitzcrank","Leona",
"Nautilus","Braum","TahmKench","Thresh"]
}

const DISENGAGE_CHAMPIONS = {
'Top': ["Singed","Karma","Malphite","Maokai","Rumble","Poppy","Shen","Lulu","Ornn"],
'Jungle': ["Nunu","Amumu","Rammus","Trundle","JarvanIV","Skarner","Poppy","Gragas","Shen","Volibear","Sejuani","Zac",
"Ivern","Sett"],
'Middle': ["Kayle","Ryze","Karthus","Chogath","Anivia","Corki","Veigar","Orianna","Cassiopeia","Heimerdinger","Malzahar",
"Viktor","Ziggs","Azir","Galio","TwistedFate","Morgana","Zilean","Karma","Malphite","Lux","Xerath","Ahri",
"Lulu","Lissandra","Syndra","AurelionSol","Velkoz","Neeko"],
'Bottom': ["Ashe","Veigar","Swain","Heimerdinger","Ezreal","Varus","Syndra","Senna","Tristana","Draven","Kaisa",
"Jinx","Lucian","Kalista","Xayah"],
'Support': ["Soraka","Morgana","Zilean","Sona","Janna","Karma","Taric","Lux","Lulu","Nami","Yuumi","Bard","Rakan"]
}

const POKE_AND_SIEGE_CHAMPIONS = {

'Top' : ["Vladimir", "Ryze", "Sion", "Teemo", "Chogath","DrMundo", "Cassiopeia", "Heimerdinger", "Nasus", "Gragas",
"Mordekaiser", "Akali", "Kennen", "Volibear", "Gnar", "Sylas", "Neeko", "Singed", "Karma", "Malphite",
"Maokai", "Rumble", "Poppy", "Shen", "Lulu", "Ornn", "Jayce"],

'Jungle' : ["Fiddlesticks", "Morgana", "Evelynn", "Karthus", "Nidalee", "Taliyah","Ekko","Sylas","Lillia"],

'Middle' : ["Kayle", "Ryze", "Karthus", "Chogath", "Anivia", "Corki", "Veigar", "Orianna", "Cassiopeia",
"Heimerdinger", "Malzahar","Viktor","Ziggs","Azir"],

'Bottom' : ["Ashe", "Veigar", "Swain", "Heimerdinger", "Ezreal", "Varus", "Syndra", "Senna", "Sivir",
"MissFortune", "Caitlyn", "Ziggs", "Yasuo", "Jhin", 'Aphelios'],

'Support' : ["Shaco", "Veigar", "Swain", "Brand", "Gragas", "Xerath", "Zyra", "Velkoz", "Neeko", "Soraka",
"Morgana", "Zilean", "Sona", "Janna", "Karma", "Taric", "Lux", "Lulu", "Nami", "Yuumi", "Bard", "Rakan"]

}

const PICK_CHAMPIONS = {
'Top': ["Olaf","Urgot","Warwick","Renekton","Wukong","Vayne","Pantheon","Garen","Riven","Rengar","Fiora","Darius",
"Camille","Kled","Aatrox","Sett","Singed","Karma","Malphite","Maokai","Rumble","Poppy","Shen","Lulu","Ornn",
"Vladimir","Ryze","Sion","Teemo","Chogath","DrMundo","Cassiopeia","Heimerdinger","Nasus","Gragas",
"Mordekaiser","Akali","Kennen","Volibear","Gnar","Sylas","Neeko"],
'Jungle': ["Olaf","XinZhao","Warwick","Twitch","Shaco","Elise","Wukong","LeeSin","Rengar","Kindred","RekSai","Nunu",
"Amumu","Rammus","Trundle","JarvanIV","Skarner","Poppy","Gragas","Shen","Volibear","Sejuani","Zac",
"Ivern","Sett","Jax","DrMundo","Nocturne","Udyr","Shyvana","Graves","Hecarim","Khazix","Kayn","Vi",
"Fiddlesticks","Morgana","Evelynn","Karthus","Nidalee","Taliyah","Ekko","Sylas","Lillia"],
'Middle': ["Tristana","Tryndamere","Irelia","Nocturne","Renekton","Garen","Talon","Graves","Jayce","Diana","Yasuo",
"Camille","Lucian","Zed","Yone","Sett","Annie","LeBlanc","Vladimir","Kassadin","Swain","Katarina",
"Rumble","Gragas","Pantheon","Akali","Kennen","Fizz","Zoe","Taliyah","Ekko","Qiyana","Sylas","Pyke",
"Galio","TwistedFate","Morgana","Zilean","Karma","Malphite","Lux","Xerath","Ahri","Lulu","Lissandra",
"Syndra","AurelionSol","Velkoz","Neeko"],
'Bottom': ["Ashe","Veigar","Swain","Heimerdinger","Ezreal","Varus","Syndra","Senna","Tristana","Draven","Kaisa",
"Jinx","Lucian","Kalista","Xayah"],
'Support': ["Maokai","Poppy","Pantheon","Shen","Senna","Pyke","Sett","Galio","Alistar","Blitzcrank","Leona",
"Nautilus","Braum","TahmKench","Thresh"]
}

const SPLITPUSH_CHAMPIONS = {

'Top' : ["Kayle", "Tryndamere", "Jax", "Irelia", "Gangplank", "Udyr", "Yorick", "Jayce", "Quinn", "Yasuo",
"Lucian", "Illaoi", "Yone", "Olaf", "Urgot", "Warwick", "Renekton", "Wukong", "Vayne", "Pantheon",
"Garen", "Riven", "Rengar", "Fiora", "Darius", "Camille", "Kled", "Aatrox", "Sett"],

'Jungle' : ["Nunu", "Amumu", "Rammus", "Trundle", "JarvanIV", "Skarner", "Poppy", "Gragas", "Shen", "Volibear",
"Sejuani", "Zac", "Ivern", "Sett", "Olaf", "XinZhao", "Warwick", "Twitch", "Shaco", "Elise", "Wukong",
"LeeSin", "Rengar", "Kindred", "RekSai"],

'Middle' : ["Tristana", "Tryndamere", "Irelia", "Nocturne", "Renekton", "Garen", "Talon", "Graves", "Jayce",
"Diana", "Yasuo", "Camille", "Lucian", "Zed", "Yone", "Sett", "Kayle", "Ryze", "Karthus", "Chogath",
"Anivia", "Corki", "Veigar", "Orianna", "Cassiopeia", "Heimerdinger", "Malzahar", "Viktor", "Ziggs",
"Azir", "Annie", "LeBlanc", "Vladimir", "Kassadin", "Swain", "Katarina", "Rumble", "Gragas", "Pantheon",
"Akali", "Kennen", "Fizz", "Zoe", "Taliyah", "Ekko", "Qiyana", "Sylas", "Pyke", "TwistedFate"],

'Bottom' : ["Ashe", "Veigar", "Swain", "Heimerdinger", "Ezreal", "Varus", "Syndra", "Senna", "Tristana", "Draven",
"Kaisa", "Jinx", "Lucian", "Kalista", "Xayah", "Sivir", "MissFortune", "Caitlyn", "Ziggs", "Yasuo",
"Jhin", "Aphelios"],

'Support' : ["Galio", "Alistar", "Blitzcrank", "Leona", "Nautilus", "Braum", "TahmKench", "Thresh", "Soraka",
"Morgana", "Zilean", "Sona", "Janna", "Karma", "Taric", "Lux", "Lulu", 'Nami', "Yuumi", "Bard",
"Rakan", "Maokai", "Poppy", "Pantheon", "Shen", "Senna", "Pyke", "Sett"]

}

const findChampions = async (region, id, championType, playerRole) => {
let data;
if (championType == "ENGAGE") {
data = ENGAGE_CHAMPIONS;
} else if (championType == "DISENGAGE") {
data = DISENGAGE_CHAMPIONS;
} else if (championType == "PICK") {
data = PICK_CHAMPIONS;
} else if (championType == "POKE_AND_SIEGE") {
data = POKE_AND_SIEGE_CHAMPIONS;
} else if (championType == "SPLITPUSH") {
data = SPLITPUSH_CHAMPIONS;
} else {
return "error";
}

let playerChampions = await championRankings(region, id);
let championsFound = 0;
let champs = [];
while(championsFound < 3) {
for (champIdAndRating of playerChampions) {
let champName = await champIdToName(String(champIdAndRating[0]));
if (data[playerRole].includes(champName) && championsFound < 3) {
championsFound += 1;
champs.push(champName);
playerChampions.pop(champIdAndRating);
continue;
}

if (playerChampions.indexOf(champIdAndRating) == playerChampions.length - 1) {
return champs;
}
}
}
}

module.exports = findChampions
18 changes: 18 additions & 0 deletions backend/services/generateChampMasteryUrl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const key = require('../config/key');

// A function that generates the Riot API url for champion masteries
const generateChampMasteryUrl = (region, id) => {
let regions = new Map();
regions.set('NA', 'NA1');
regions.set('KR', 'KR');
regions.set('EUW', 'EUW1');
regions.set('EUN', 'EUN1');
regions.set('OCE', 'OC1');
regions.set('TUR', 'TR1');
regions.set('LAN', 'LA1');
regions.set('LAS', 'LA2');

return `https://${regions.get(region)}.api.riotgames.com/lol/champion-mastery/v4/champion-masteries/by-summoner/${id}?api_key=${key.riotAPI}`;
}

module.exports = generateChampMasteryUrl
2 changes: 1 addition & 1 deletion backend/services/generateRiotAPIUrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const generateRiotAPIUrl = (region, summonerName) => {
regions.set('LAN', 'LA1');
regions.set('LAS', 'LA2');

return `https://${regions.get(region)}.api.riotgames.com/lol/summoner/v4/summoners/by-name/${summonerName}?api_key=${key.riotAPI}`;
return encodeURI(`https://${regions.get(region)}.api.riotgames.com/lol/summoner/v4/summoners/by-name/${summonerName}?api_key=${key.riotAPI}`);
}

module.exports = generateRiotAPIUrl
Loading