Kingdra Pokemon Teambuilder
Full-Stack Pokemon Teambuilder - June 2024
Role
- Full-Stack Development
- UI + UX Design
Team
- Solo
Timeline
- Ongoing
Overview
Kingdra is a full-stack web application that seeks to solve a problem I encountered while organizing a Pokemon draft league for my friends (think fantasy football, but with Pokemon). There aren't many tools out there that allow you to create and manage Pokemon teams, and those that do are often poorly designed and lack the ability to set moves and stats unless you're using Pokemon Showdown.
I also wanted to include a way to group in casual and competitive play in the same place. If casual players find that they are starting to develop an interest in competitive play, they can easily move their teams over to a competitive format just by toggling the app's mode.
The app features infinite scrolling, real-time stat calculations, user authentication, team management, and full import/export functionality with Pokemon Showdown. It also relies on multiple APIs to function, namely the PokeAPI and the Smogon API.
Context
The goal going in was to figure out Next.js and build my first real full-stack app. It addressed a need I saw in the Pokemon community, sure, but it was also partially me wanting to prove to myself that I could figure out how to put together a web app up and down the stack. Initially, I just used it as a specialized drafting tool for my friends. It was even formatted around our draft league format, making it pretty hyperspecialized to begin with.
As the project grew, I started to get feedback from my friends that they could see this app skeleton being used as a general purpose tool for Pokemon players. At that point, development really took off.
Development
This project was my first time really diving into a meta-framework (Next.js), and it shows. I had to figure out what API routes even were and how they worked. There was also a lot of initial trial and error in fetching data properly from all of the different APIs, since I struggled with staggering fetches and posts in sequential order. Using a NoSQL database (MongoDB) helped a lot, but I still needed to figure out how to handle data properly with React hooks, which was something I wasn't super familiar with at the time.
Initially, I had an idea to implement draggable Pokemon cards. You could take Pokemon from the infinite scroll and drag them into a team slot. Sadly, this created a UX problem. If you're far down on the page, it's a pain to drag a Pokemon all the way back to the top! That idea was scrapped, but it was the basis for the grid-based selection system we have now.
Another goal was to implement stat calculations for each Pokemon. In order to make this tool useful in the competitive scene, I had to make sure EVs and IVs could fluidly adjust the final stat totals. The stat calculation system implements the exact formulas used in the Pokemon games, factoring in the nature multiplier when it is relevant:
const calculateStatTotal = () => {
if (id === 0) {
//Is HP, calculate differently
return (
Math.floor(
((2 * baseValue + iv + Math.floor(ev / 4) + 100) * level) / level
) + 10
); // 110 = Level 100 + 10
} else {
return Math.floor(
(Math.floor(((2 * baseValue + iv + Math.floor(ev / 4)) * level) / 100) +
5) *
getNatureMultiplier()
);
}
};This function would run every time that the EVs or IVs were changed to recalculate the stat total. The fluidity of these state changes made me grateful to be using React, as these kinds of dynamic state changes are significantly harder to do in vanilla JavaScript.
There were also built in automatic speed calculations for the speed stat depending on whether the Pokemon was holding a Choice Scarf or not. This is a feature that my friends and I found to be sorely missing from other teambuilder apps, so I decided to bundle it in as a little tooltip under the speed stat. Here's how it's implemented under the hood:
const speedCalcs = (
<div>
<ul className='gap-1 p-2'>
<li className='flex items-center gap-2'>
<p className='text-gray-600'>
+1 Stage / Scarfed: <strong>{calculateStatTotal() * 1.5}</strong>
</p>
</li>
<li className='flex items-center gap-2'>
<p className='text-gray-600'>
+2 Stages: <strong>{calculateStatTotal() * 2}</strong>
</p>
</li>
<li className='flex items-center gap-2'>
<p className='text-gray-600'>
+3 Stages: <strong>{calculateStatTotal() * 2.5}</strong>
</p>
</li>
</ul>
</div>
);One of the hallmark features is the import and export system. Instead of trying to fight the dominance of Pokemon Showdown in the scene, I decided to just leverage its format to allow users to import and export from the platform at will. There was a lot of data cleanup work that went into pushing this data into JSON files. Here's a snippet from some of the stat parsing.
rest.forEach((line) => {
if (line.startsWith('EVs:')) {
const evs = line.substring(4).split('/');
evs.forEach((stat) => {
const [value, name] = stat.trim().split(' ');
const index = ['HP', 'Atk', 'Def', 'SpA', 'SpD', 'Spe'].indexOf(name);
if (index !== -1) {
ev[index] = parseInt(value);
}
});
} else if (line.startsWith('IVs:')) {
const ivs = line.substring(4).split('/');
ivs.forEach((stat) => {
const [value, name] = stat.trim().split(' ');
const index = ['HP', 'Atk', 'Def', 'SpA', 'SpD', 'Spe'].indexOf(name);
if (index !== -1) {
iv[index] = parseInt(value);
}
});
} else if (line.startsWith('Tera Type:')) {
tera_type = line.split(':')[1].trim();
} else if (line.endsWith('Nature')) {
nature = line.split(' ')[0];
} else if (line.startsWith('-')) {
const moveIndex = moves.findIndex((move) => move === '');
if (moveIndex !== -1) {
moves[moveIndex] = line.substring(1).trim();
}
}
});Though there is still work to be done, I'm proud of where this project ended up, considering it was a solo project. It was a great learning experience to learn how to leverage multiple APIs in a single app to build out a cohesive experience. Hope you find it useful!