// ==UserScript==
// @name Twitch Pokemon Community Game Helper - NO TIMER
// @namespace http://tampermonkey.net/
// @version 19
// @description Twitch PokéBall Drag and Drop to chat! !pokecatch <balltype> Pokemoncommunitygame Timer and Spawn Helper. Pokemon stats and more!
// @match https://www.twitch.tv/*
// @icon https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png
// @grant none
// ==/UserScript==
(function () {
'use strict';
class PokeballHelper {
constructor() {
this.catchBalls = {
dollars: { command: '$', tooltip: 'Poke Dollars', image: 'https://i.postimg.cc/T20dR1qH/f547e065261b657c49d5702826b0deca.png', quantity: 0 },
check: { command: '!pokecheck', tooltip: 'Poke Check', image: 'https://i.postimg.cc/0N7vhyyn/ea9752334aa08543e2f148c0a903719e.png', quantity: 0 },
poke: { command: '!pokecatch', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png', quantity: 0 },
great: { command: '!pokecatch greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png', quantity: 0 },
ultra: { command: '!pokecatch ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png', quantity: 0 },
premier: { command: '!pokecatch premierball', tooltip: 'Premier Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/premier_ball.png', quantity: 0 },
basic: { command: '!pokecatch basicball', tooltip: 'Basic Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/basic_ball.png', quantity: 0 },
heavy: { command: '!pokecatch heavyball', tooltip: 'Heavy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heavy_ball.png', quantity: 0 },
feather: { command: '!pokecatch featherball', tooltip: 'Feather Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/feather_ball.png', quantity: 0 },
timer: { command: '!pokecatch timerball', tooltip: 'Timer Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/timer_ball.png', quantity: 0 },
quick: { command: '!pokecatch quickball', tooltip: 'Quick Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/quick_ball.png', quantity: 0 },
nest: { command: '!pokecatch nestball', tooltip: 'Nest Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/nest_ball.png', quantity: 0 },
fast: { command: '!pokecatch fastball', tooltip: 'Fast Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fast_ball.png', quantity: 0 },
heal: { command: '!pokecatch healball', tooltip: 'Heal Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heal_ball.png', quantity: 0 },
repeat: { command: '!pokecatch repeatball', tooltip: 'Repeat Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/repeat_ball.png', quantity: 0 },
friend: { command: '!pokecatch friendball', tooltip: 'Friend Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/friend_ball.png', quantity: 0 },
frozen: { command: '!pokecatch frozenball', tooltip: 'Frozen Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/frozen_ball.png', quantity: 0 },
night: { command: '!pokecatch nightball', tooltip: 'Night Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/night_ball.png', quantity: 0 },
phantom: { command: '!pokecatch phantomball', tooltip: 'Phantom Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/phantom_ball.png', quantity: 0 },
cipher: { command: '!pokecatch cipherball', tooltip: 'Cipher Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cipher_ball.png', quantity: 0 },
magnet: { command: '!pokecatch magnetball', tooltip: 'Magnet Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/magnet_ball.png', quantity: 0 },
net: { command: '!pokecatch netball', tooltip: 'Net Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/net_ball.png', quantity: 0 },
luxury: { command: '!pokecatch luxuryball', tooltip: 'Luxury Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/luxury_ball.png', quantity: 0 },
stone: { command: '!pokecatch stoneball', tooltip: 'Stone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/stone_ball.png', quantity: 0 },
level: { command: '!pokecatch levelball', tooltip: 'Level Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/level_ball.png', quantity: 0 },
clone: { command: '!pokecatch cloneball', tooltip: 'Clone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/clone_ball.png', quantity: 0 },
sun: { command: '!pokecatch sunball', tooltip: 'Sun Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/sun_ball.png', quantity: 0 },
fantasy: { command: '!pokecatch fantasyball', tooltip: 'Fantasy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fantasy_ball.png', quantity: 0 },
mach: { command: '!pokecatch machball', tooltip: 'Mach Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/mach_ball.png', quantity: 0 },
geo: { command: '!pokecatch geoball', tooltip: 'Geo Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/geo_ball.png', quantity: 0 },
dive: { command: '!pokecatch diveball', tooltip: 'Dive Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/dive_ball.png', quantity: 0 },
master: { command: '!pokecatch masterball', tooltip: 'Master Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/master_ball.png', quantity: 0 },
cherish: { command: '!pokecatch cherishball', tooltip: 'Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cherish_ball.png', quantity: 0 },
greatCherish: { command: '!pokecatch greatcherishball', tooltip: 'Great Cherish', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_cherish_ball.png', quantity: 0 },
ultraCherish: { command: '!pokecatch ultracherishball', tooltip: 'Ultra Cherish', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_cherish_ball.png', quantity: 0 }
};
this.shopBalls = {
pokeball: { command: '!pokeshop pokeball', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png' },
great: { command: '!pokeshop greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png' },
ultra: { command: '!pokeshop ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png' },
gift: { command: '!pokegift', tooltip: 'Poke Gift', image: 'https://i.postimg.cc/CxNNP2Zz/Pngtree-gift-box-3d-illustration-6508903.png' },
v5: { command: ' 5', tooltip: '5', image: 'https://i.postimg.cc/wM8LT5tC/pngaaa-com-3588470.png' },
v10: { command: ' 10', tooltip: '10', image: 'https://i.postimg.cc/NjJXrv97/pngaaa-com-2133853.png' },
v25: { command: ' 25', tooltip: '25', image: 'https://i.postimg.cc/wTFySYV8/pngaaa-com-1433934.png' },
v50: { command: ' 50', tooltip: '50', image: 'https://i.postimg.cc/bNqSCw19/pngaaa-com-973335.png' }
};
this.currentTab = 'catch';
this.allPokemonList = null;
this.isDragging = false;
this.startX = 0;
this.startY = 0;
this.containerStartLeft = 0;
this.containerStartTop = 0;
this.wasDragging = false;
// Bind drag methods.
this.dragStart = this.dragStart.bind(this);
this.drag = this.drag.bind(this);
this.dragEnd = this.dragEnd.bind(this);
// Initialize UI and observers.
this.init();
this.gridContainer = document.getElementById('grid-container');
this.tooltip = null;
this.initIntersectionObserver();
this.initInventoryObserver();
}
initIntersectionObserver() {
const lazyImages = document.querySelectorAll('img.lazy');
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.addEventListener('load', () => {
img.classList.add('fade-in', 'visible');
});
obs.unobserve(img);
}
});
});
lazyImages.forEach(img => observer.observe(img));
}
init() {
this.setupStyles();
this.waitForChat().then(() => {
this.createInterface();
// Removed timer element creation.
this.addEventListeners();
this.renderGrid();
this.initSearchButtons();
this.imageUpdateStarted = false;
// Removed spawn timer update.
this.updateInventoryFromDOM();
});
}
handleSearch(query) {
switch (this.currentTab) {
case 'advanced':
this.searchAdvancedPokemon(query);
break;
case 'browse':
this.renderBrowseGrid();
break;
default:
this.filterGrid();
}
}
initSearchButtons() {
document.querySelectorAll('.pball-search-container').forEach(container => {
const input = container.querySelector('.pball-search');
if (!input || container.dataset.initialized) return;
const btnContainer = document.createElement('div');
btnContainer.className = 'search-buttons';
const enterButton = Object.assign(document.createElement('button'), {
className: 'search-enter-button',
innerHTML: '✔',
title: 'Search (Enter)',
onclick: () => this.handleSearch(input.value.trim())
});
const clearButton = Object.assign(document.createElement('button'), {
className: 'pball-clear-btn',
innerHTML: '×',
title: 'Clear search',
style: 'display: none;',
onclick: () => {
input.value = '';
input.focus();
this.handleSearch('');
clearButton.style.display = 'none';
}
});
input.addEventListener('input', () => {
clearButton.style.display = input.value ? 'flex' : 'none';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') enterButton.click();
});
btnContainer.append(enterButton, clearButton);
container.append(btnContainer);
container.dataset.initialized = true;
});
}
createInterface() {
let inventoryDisplay = document.getElementById('inventory-display');
if (!inventoryDisplay) {
inventoryDisplay = document.createElement('div');
inventoryDisplay.id = 'inventory-display';
Object.assign(inventoryDisplay.style, {
position: 'fixed',
top: '10px',
right: '10px',
backgroundColor: '#fff',
border: '1px solid #ccc',
padding: '10px',
zIndex: '9999'
});
document.body.appendChild(inventoryDisplay);
}
inventoryDisplay.innerHTML = `
<h3>Inventory</h3>
<div id="dollars-display">Poke Dollars: 0</div>
<ul id="balls-list"></ul>
`;
}
loadPosition() {
const savedPos = localStorage.getItem('pballPosition');
if (savedPos) {
const { x, y } = JSON.parse(savedPos);
this.container.style.left = `${x}px`;
this.container.style.top = `${y}px`;
}
}
dragStart(e) {
e.preventDefault();
this.wasDragging = false;
const startX = e.clientX;
const startY = e.clientY;
const rect = this.container.getBoundingClientRect();
const origLeft = rect.left;
const origTop = rect.top;
const onMouseMove = (moveEvent) => {
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
this.wasDragging = true;
}
this.container.style.left = `${origLeft + deltaX}px`;
this.container.style.top = `${origTop + deltaY}px`;
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
const ballImg = e.target.closest('.pball-item img');
if (ballImg) {
ballImg.style.cursor = 'grab';
}
};
const ballImg = e.target.closest('.pball-item img');
if (ballImg) {
ballImg.style.cursor = 'grabbing';
}
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}
drag(e) {
this.container.classList.remove('dragging');
e.preventDefault();
const dx = e.clientX - this.startX;
const dy = e.clientY - this.startY;
if (!this.isDragging && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
this.isDragging = true;
}
if (this.isDragging) {
let newX = this.containerStartLeft + dx;
let newY = this.containerStartTop + dy;
const chatWindow = document.querySelector('.chat-window');
if (chatWindow) {
const chatRect = chatWindow.getBoundingClientRect();
const ballRect = this.container.getBoundingClientRect();
newX = Math.max(chatRect.left, Math.min(newX, chatRect.right - ballRect.width));
newY = Math.max(chatRect.top, Math.min(newY, chatRect.bottom - ballRect.height));
}
requestAnimationFrame(() => {
this.container.style.left = `${newX}px`;
this.container.style.top = `${newY}px`;
});
}
}
dragEnd(e) {
document.removeEventListener('mousemove', this.drag);
document.removeEventListener('mouseup', this.dragEnd);
if (this.isDragging) {
this.wasDragging = true;
const left = this.container.offsetLeft;
const top = this.container.offsetTop;
localStorage.setItem('pballPosition', JSON.stringify({ x: left, y: top }));
}
this.container.style.transition = '';
}
setupStyles() {
const style = document.createElement('style');
style.textContent = `
/* ============================================
Ultra Stunning UI & Theme – Dark Transparent with Soft White Illuminations
============================================ */
/*--------------------------------------------------
Import Fonts
--------------------------------------------------*/
@import url('https://fonts.googleapis.com/css2?family=Segment7Standard&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
/*--------------------------------------------------
Global Variables & Base Styles
--------------------------------------------------*/
:root {
--color-primary: rgba(255, 255, 255, 0.8);
--color-secondary: rgba(255, 255, 255, 0.8);
--color-accent: rgba(255, 255, 255, 0.6);
--color-dark: #1c1c1e;
--color-darker: #141414;
--color-card: rgba(20, 20, 20, 0.8);
--color-border: rgba(255, 255, 255, 0.1);
--color-glass: rgba(255, 255, 255, 0.05);
--gradient-accent: linear-gradient(90deg, rgba(255,255,255,0.5), rgba(255,255,255,0.2));
--gradient-bg: radial-gradient(circle at top left, rgba(10,10,10,1), rgba(0,0,0,1));
--color-text: #fefefe;
--font-base: 'Roboto', sans-serif;
--font-led: 'Segment7Standard', monospace;
--font-label: 'Press Start 2P', cursive;
--font-size-base: clamp(0.9rem, 1vw + 0.8rem, 1.1rem);
--border-radius-small: 4px;
--border-radius-medium: 12px;
--border-radius-large: 16px;
--spacing-small: 8px;
--spacing-medium: 16px;
--spacing-large: 24px;
--transition-fast: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--transition-medium: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--box-shadow-light: 0 2px 12px rgba(0, 0, 0, 0.4);
--box-shadow-heavy: 0 4px 20px rgba(0, 0, 0, 0.6);
--backdrop-blur: blur(10px);
--neon-glow: drop-shadow(0 0 8px rgba(255,255,255,0.7)) drop-shadow(0 0 8px rgba(255,255,255,0.7));
--soft-glow: drop-shadow(0 0 10px rgba(255,255,255,0.8));
--breakpoint-md: 768px;
--breakpoint-sm: 600px;
}
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: var(--font-base);
transition: background var(--transition-fast), color var(--transition-fast);
}
html {
scroll-behavior: smooth;
}
body {
background: var(--gradient-bg);
color: var(--color-text);
font-size: var(--font-size-base);
line-height: 1.5;
}
/*--------------------------------------------------
Scrollbar Styles
--------------------------------------------------*/
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-darker);
border-radius: var(--border-radius-medium);
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: var(--border-radius-medium);
border: 1px solid var(--color-dark);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.3);
}
* {
scrollbar-width: thin;
scrollbar-color: var(--color-border) var(--color-darker);
}
/*--------------------------------------------------
Main UI Components
--------------------------------------------------*/
.pball-container {
position: fixed;
bottom: calc(var(--spacing-large) + 50px);
right: var(--spacing-medium);
z-index: 10000;
pointer-events: none;
transform: scale(1);
transform-origin: top right;
width: fit-content;
height: fit-content;
}
.pball-container > * {
pointer-events: auto;
}
.pball-button {
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
cursor: pointer;
transition: transform var(--transition-fast), filter var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.pball-button img {
width: 48px;
height: 48px;
object-fit: contain;
transition: transform var(--transition-fast), filter var(--transition-fast);
}
.pball-button:hover,
.pball-button:focus-visible {
transform: scale(1.25) rotate(3deg);
filter: var(--soft-glow);
outline: none;
}
.pball-button:hover img,
.pball-button:focus-visible img {
transform: scale(1.1);
}
/*--------------------------------------------------
Panel & Tab System
--------------------------------------------------*/
.pball-panel {
position: absolute;
bottom: calc(100% + var(--spacing-small));
right: 0;
width: 340px;
height: 500px;
min-width: 300px;
min-height: 300px;
overflow: auto;
background: var(--color-card);
backdrop-filter: var(--backdrop-blur);
border-radius: var(--border-radius-large);
border: 1px solid var(--color-border);
box-shadow: var(--box-shadow-heavy);
opacity: 0;
visibility: hidden;
transform: translateY(var(--spacing-small));
transition: opacity var(--transition-medium), transform var(--transition-medium), visibility var(--transition-medium);
}
.pball-panel.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.pball-tabs {
display: flex;
background: var(--color-darker);
border-bottom: 1px solid var(--color-border);
border-top-left-radius: var(--border-radius-large);
border-top-right-radius: var(--border-radius-large);
overflow: hidden;
}
.pball-tab {
position: relative;
flex: 1;
padding: var(--spacing-small);
text-align: center;
font-size: 15px;
cursor: pointer;
color: var(--color-text);
transition: background var(--transition-fast), color var(--transition-fast);
}
.pball-tab.active::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
width: 60%;
height: 3px;
background: var(--color-primary);
box-shadow: 0 0 12px var(--color-primary);
border-radius: 2px;
transform: translateX(-50%);
}
/*--------------------------------------------------
Search & Input Components
--------------------------------------------------*/
.pball-search-container {
position: relative;
margin: var(--spacing-medium);
background: var(--color-card);
backdrop-filter: var(--backdrop-blur);
border-radius: var(--border-radius-medium);
border: 1px solid var(--color-border);
overflow: hidden;
}
.pball-search {
width: 100%;
padding: calc(var(--spacing-small) + 2px) var(--spacing-medium);
padding-right: 70px;
border: none;
background: transparent;
color: var(--color-text);
font-size: 15px;
outline: none;
}
.search-buttons {
position: absolute;
right: var(--spacing-small);
top: 50%;
transform: translateY(-50%);
display: flex;
gap: var(--spacing-small);
}
/*--------------------------------------------------
Grid Layouts & Item Cards
--------------------------------------------------*/
.pball-grid {
padding: var(--spacing-medium);
display: grid;
gap: var(--spacing-medium);
max-height: 320px;
overflow-y: auto;
}
.pball-panel.shop .pball-grid,
.pball-panel.catch-shop .pball-grid {
grid-template-columns: repeat(3, minmax(80px, 1fr));
justify-content: center;
}
.pball-grid.ball-items {
grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));
justify-items: center;
}
/*--------------------------------------------------
Item Cards & Catch Tab Overrides
--------------------------------------------------*/
.pball-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 50%;
padding: var(--spacing-small);
transition: transform var(--transition-fast);
}
.pball-item img {
width: 40px;
height: 40px;
transition: transform var(--transition-fast);
cursor: grab;
}
.pball-item img:hover,
.pball-item img:focus-visible {
transform: scale(1.2);
outline: none;
}
.pball-item img:active,
.pball-item img.grabbing {
filter: var(--neon-glow);
animation: neonPulse 0.26s infinite alternate;
}
@keyframes neonPulse {
0% {
filter: drop-shadow(0 0 8px rgba(255,255,255,0.7)) drop-shadow(0 0 8px rgba(255,255,255,0.7));
}
100% {
filter: drop-shadow(0 0 12px rgba(255,255,255,0.7)) drop-shadow(0 0 12px rgba(255,255,255,0.7));
}
}
.pball-item .pball-label {
margin-top: var(--spacing-small);
font-size: 14px;
color: var(--color-text);
text-align: center;
}
.catch-shop .pball-item {
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.catch-shop .pball-item img {
margin-right: var(--spacing-small);
}
.catch-shop .pball-item .pball-label {
margin-top: 0;
text-align: left;
}
/*--------------------------------------------------
Pokémon Card Styles - Transparent Version
--------------------------------------------------*/
.pokemon-card {
background: transparent !important;
border-radius: 12px;
padding: 10px;
text-align: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: hidden;
cursor: pointer;
}
.pokemon-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 16px rgba(255,255,255,0.5);
}
.dex-number {
position: absolute;
top: 6px;
left: 8px;
font-weight: bold;
background: rgba(0, 0, 0, 0.4);
padding: 4px 8px;
border-radius: 8px;
color: #fff;
}
.pokemon-image {
max-width: 100px;
max-height: 100px;
object-fit: contain;
transition: transform var(--transition-fast);
background: transparent !important;
}
.pokemon-card:hover .pokemon-image,
.pokemon-card:hover .pball-label,
.pball-item:hover img {
filter: drop-shadow(0px 0px 10px rgba(255,255,255,0.8));
transition: filter var(--transition-fast);
}
/*--------------------------------------------------
Utility Classes
--------------------------------------------------*/
.spinner {
margin: 1.5rem auto;
border: 4px solid var(--color-border);
border-top: 4px solid var(--color-primary);
border-radius: 50%;
width: 2.8rem;
height: 2.8rem;
animation: spin 1s linear infinite;
}
.animate-fadeIn {
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.5s forwards;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
/*--------------------------------------------------
Responsive Adjustments for Auto-Alignment
--------------------------------------------------*/
@media (max-width: var(--breakpoint-md)) {
.pball-panel {
width: 90%;
height: auto;
bottom: var(--spacing-medium);
right: var(--spacing-medium);
}
}
`;
document.head.appendChild(style);
}
// -----------------------------
// Helper: Debounce Function
// -----------------------------
debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// -----------------------------
// Wait for Chat Input to be Available
// -----------------------------
async waitForChat() {
return new Promise((resolve) => {
const chatSelector = '[data-test-selector="chat-input"]';
if (document.querySelector(chatSelector)) {
return resolve();
}
const observer = new MutationObserver((mutations, obs) => {
if (document.querySelector(chatSelector)) {
obs.disconnect();
resolve();
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
// -----------------------------
// Create Interface Elements
// -----------------------------
createInterface() {
this.container = document.createElement('div');
this.container.className = 'pball-container';
this.button = this.createMainButton();
this.panel = this.createPanel();
this.container.append(this.button, this.panel);
document.body.appendChild(this.container);
}
// -----------------------------
// Create Main Button (Static)
// -----------------------------
createMainButton() {
const button = document.createElement('div');
button.className = 'pball-button';
const icon = document.createElement('img');
// Use the static pokeball image
icon.src = this.catchBalls.poke.image;
icon.style.width = '46px';
icon.style.height = '46px';
icon.style.filter = 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))';
button.appendChild(icon);
this.buttonIcon = icon;
return button;
}
// -----------------------------
// Create Panel with Tabs, Search, and Grid
// -----------------------------
createPanel() {
const panel = document.createElement('div');
panel.className = 'pball-panel';
panel.draggable = false;
const tabsContainer = document.createElement('div');
tabsContainer.className = 'pball-tabs';
const createTab = (name, isActive = false) => {
const tab = document.createElement('div');
tab.className = `pball-tab${isActive ? ' active' : ''}`;
tab.textContent = name;
tab.dataset.tab = name.toLowerCase();
tab.addEventListener('click', () => {
this.switchTab(name.toLowerCase());
});
return tab;
};
tabsContainer.appendChild(createTab('Catch', true));
tabsContainer.appendChild(createTab('Shop'));
tabsContainer.appendChild(createTab('Pokemon'));
tabsContainer.appendChild(createTab('Moves'));
tabsContainer.appendChild(createTab('Advanced'));
const searchContainer = document.createElement('div');
searchContainer.className = 'pball-search-container';
this.searchInput = document.createElement('input');
this.searchInput.type = 'text';
this.searchInput.className = 'pball-search';
this.searchInput.placeholder = 'Search...';
this.searchInput.setAttribute('aria-label', 'Search Pokémon');
this.clearBtn = document.createElement('button');
this.clearBtn.setAttribute('aria-label', 'Clear Search');
searchContainer.append(this.searchInput, this.clearBtn);
this.gridContainer = document.createElement('div');
this.gridContainer.className = 'pball-grid';
this.tabContainers = {};
['catch', 'shop', 'pokemon', 'moves', 'advanced'].forEach(tabName => {
const container = document.createElement('div');
container.className = 'tab-content';
container.dataset.tab = tabName;
container.style.display = tabName === 'catch' ? 'block' : 'none';
this.tabContainers[tabName] = container;
this.gridContainer.appendChild(container);
});
const footer = document.createElement('div');
Object.assign(footer.style, {
position: 'absolute',
bottom: '0',
left: '0',
width: '100%',
padding: '4px 12px',
borderTop: '1px solid rgba(255,255,255,0.1)',
fontSize: '15px',
color: '#666',
textAlign: 'center'
});
const message = document.createTextNode('Like the extension? ');
const cashTag = document.createElement('span');
cashTag.textContent = '$yeetsquadcuz';
Object.assign(cashTag.style, {
color: '#888',
fontWeight: '500',
cursor: 'pointer',
transition: 'color 0.2s ease',
display: 'inline-flex',
alignItems: 'center'
});
cashTag.onmouseenter = () => cashTag.style.color = '#aaa';
cashTag.onmouseleave = () => cashTag.style.color = '#888';
const cashLogo = document.createElement('img');
cashLogo.src = 'https://i.postimg.cc/qq9LWcjm/pngegg.png';
Object.assign(cashLogo.style, {
width: '14px',
height: '14px',
marginLeft: '4px'
});
cashTag.appendChild(cashLogo);
cashTag.addEventListener('click', () => {
const audio = new Audio('https://www.myinstants.com/media/sounds/yeet.mp3');
audio.volume = 0.1;
audio.play();
});
footer.appendChild(message);
footer.appendChild(cashTag);
panel.append(tabsContainer, searchContainer, this.gridContainer, footer);
return panel;
}
renderGrid() {
// Cache control elements if they exist
const movesControls = document.getElementById('moves-controls');
const pokemonControls = document.getElementById('pokemon-controls');
// Only remove controls if needed
if (movesControls && this.currentTab !== 'moves') {
movesControls.remove();
}
if (pokemonControls && this.currentTab !== 'pokemon') {
pokemonControls.remove();
}
// Clear existing classes to avoid duplication
this.gridContainer.classList.remove('ball-items', 'search-results');
if (this.currentTab === 'advanced') {
this.gridContainer.classList.add('search-results');
this.renderAdvancedInstruction();
} else if (this.currentTab === 'pokemon') {
this.gridContainer.classList.add('search-results');
this.renderPokemon();
} else if (this.currentTab === 'moves') {
this.gridContainer.classList.add('search-results');
this.renderMoves();
} else {
// For "catch" and "shop" tabs
this.gridContainer.classList.add('ball-items');
// Clear out the container
this.gridContainer.innerHTML = '';
const balls = this.currentTab === 'catch' ? this.catchBalls : this.shopBalls;
const fragment = document.createDocumentFragment();
Object.entries(balls).forEach(([key, ball]) => {
const item = document.createElement('div');
item.className = 'pball-item';
item.dataset.label = ball.tooltip.toLowerCase();
const img = document.createElement('img');
img.src = ball.image;
img.dataset.ballType = ball.command;
img.draggable = true;
const label = document.createElement('div');
label.className = 'pball-label';
label.textContent = ball.tooltip;
item.append(img, label);
fragment.appendChild(item);
});
this.gridContainer.appendChild(fragment);
this.filterGrid();
}
}
renderMoves() {
// Clear the grid container.
this.gridContainer.innerHTML = '';
// Render the custom controls panel above the grid.
this.renderMovesControls();
// Try to load from localStorage first.
const cachedMoves = localStorage.getItem('movesList');
if (cachedMoves) {
this.movesList = JSON.parse(cachedMoves);
this.renderMovesGrid();
} else if (!this.movesList) {
this.gridContainer.innerHTML += '<div class="spinner"></div>';
fetch('https://pokeapi.co/api/v2/move?limit=1000')
.then(response => response.json())
.then(data => {
this.movesList = data.results;
// Cache the moves list in localStorage.
localStorage.setItem('movesList', JSON.stringify(data.results));
this.renderMovesGrid();
})
.catch(err => {
this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading moves list</div>`;
});
} else {
// Use the already fetched moves list.
this.renderMovesGrid();
}
}
renderMovesControls() {
let controlsContainer = document.getElementById('moves-controls');
if (!controlsContainer) {
controlsContainer = document.createElement('div');
controlsContainer.id = 'moves-controls';
Object.assign(controlsContainer.style, {
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'center', // Changed to center
alignItems: 'center',
marginBottom: '10px',
backgroundColor: '#202020',
color: '#fff',
padding: '3px',
borderRadius: '5px',
gap: '4px',
overflowX: 'auto',
width: '100%' // Ensure full width for proper centering
});
this.gridContainer.parentNode.insertBefore(controlsContainer, this.gridContainer);
}
controlsContainer.innerHTML = '';
// --- Filter Buttons ---
const filterOptions = ['All', 'Physical', 'Special', 'Status'];
const filterGroup = document.createElement('div');
filterGroup.style.display = 'flex';
filterGroup.style.gap = '4px';
filterGroup.style.margin = '0';
filterOptions.forEach(option => {
const btn = document.createElement('button');
btn.textContent = option;
btn.dataset.filter = option.toLowerCase();
Object.assign(btn.style, {
backgroundColor: '#202020',
color: '#fff',
border: '1px solid #666',
borderRadius: '3px',
padding: '2px 4px',
cursor: 'pointer',
fontSize: '10px',
height: '24px',
transition: 'background-color 0.2s ease',
whiteSpace: 'nowrap'
});
btn.addEventListener('click', () => {
Array.from(filterGroup.children).forEach(child => {
child.style.backgroundColor = '#202020';
});
btn.style.backgroundColor = '#444';
this.selectedDamageFilter = btn.dataset.filter;
this.renderMovesGrid();
});
filterGroup.appendChild(btn);
});
controlsContainer.appendChild(filterGroup);
// --- Sort Dropdown ---
const sortContainer = document.createElement('div');
sortContainer.style.display = 'flex';
sortContainer.style.alignItems= 'center';
sortContainer.style.gap = '3px';
const sortLabel = document.createElement('label');
sortLabel.textContent = 'Sort:';
Object.assign(sortLabel.style, {
fontSize: '10px',
color: '#fff',
whiteSpace: 'nowrap',
marginLeft: '8px' // Added spacing between filter buttons and sort
});
const sortSelect = document.createElement('select');
sortSelect.id = 'moves-sort';
Object.assign(sortSelect.style, {
backgroundColor: '#202020',
color: '#fff',
border: '1px solid #666',
borderRadius: '3px',
padding: '2px',
fontSize: '10px',
height: '24px',
cursor: 'pointer',
minWidth: '72px'
});
sortSelect.innerHTML = `
<option value="name-asc">A–Z</option>
<option value="name-desc">Z–A</option>
`;
sortSelect.addEventListener('change', () => {
this.renderMovesGrid();
});
sortContainer.appendChild(sortLabel);
sortContainer.appendChild(sortSelect);
controlsContainer.appendChild(sortContainer);
}
// Render the moves grid using the custom controls (all moves displayed, with search highlighting)
renderMovesGrid() {
this.gridContainer.innerHTML = '';
this.gridContainer.classList.add('browse-container');
const query = this.searchInput.value.trim().toLowerCase();
// Get the selected damage filter from the buttons (default to 'all' if not set)
const damageFilter = this.selectedDamageFilter || 'all';
const sortOption = document.getElementById('moves-sort')?.value || 'name-asc';
// Filter moves by search query.
let filtered = this.movesList.filter(move => move.name.includes(query));
// Further filter by damage class if not set to 'all'
if (damageFilter !== 'all') {
filtered = filtered.filter(move => {
if (this.moveDetailCache && this.moveDetailCache.has(move.url)) {
const moveData = this.moveDetailCache.get(move.url);
return moveData.damage_class.name === damageFilter;
}
// Include moves without loaded details by default.
return true;
});
}
// Sort moves alphabetically.
if (sortOption === 'name-asc') {
filtered.sort((a, b) => a.name.localeCompare(b.name));
} else if (sortOption === 'name-desc') {
filtered.sort((a, b) => b.name.localeCompare(a.name));
}
// Build the grid using a document fragment.
const fragment = document.createDocumentFragment();
filtered.forEach(move => {
const tile = document.createElement('div');
tile.className = 'browse-tile';
tile.dataset.label = move.name.toLowerCase();
// Card container for move info.
const content = document.createElement('div');
content.className = 'move-card';
Object.assign(content.style, {
padding: '12px',
borderRadius: '12px',
background: 'var(--background-darker)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
cursor: 'pointer',
transition: 'box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out'
});
// Hover glow effect.
content.addEventListener('mouseenter', () => {
content.style.boxShadow = '0 0 20px rgba(255, 255, 255, 0.6)';
content.style.transform = 'scale(1.05)';
});
content.addEventListener('mouseleave', () => {
content.style.boxShadow = '0 0 0px rgba(255, 255, 255)';
content.style.transform = 'scale(1)';
});
// Highlight matching search text in move title.
let titleText = move.name;
if (query) {
const regex = new RegExp(`(${query})`, 'gi');
titleText = move.name.replace(regex, '<mark>$1</mark>');
}
const title = document.createElement('div');
title.className = 'move-title';
Object.assign(title.style, {
fontSize: '28px',
fontWeight: 'bold',
marginBottom: '8px',
color: 'var(--text-glow)',
textShadow: '0 0 5px rgba(255, 255, 255, 0.8)',
textAlign: 'center'
});
title.innerHTML = titleText;
// Container for badges.
const badgeContainer = document.createElement('div');
badgeContainer.className = 'badge-container';
Object.assign(badgeContainer.style, {
display: 'flex',
gap: '8px'
});
content.append(title, badgeContainer);
tile.appendChild(content);
// Click event to switch to advanced view.
tile.addEventListener('click', (e) => {
e.stopPropagation();
this.panel.classList.add('active');
this.changeTab('advanced');
this.searchInput.value = move.name;
this.searchAdvancedMove(move.name);
});
// Fetch move details for badges using caching.
if (this.moveDetailCache && this.moveDetailCache.has(move.url)) {
const moveData = this.moveDetailCache.get(move.url);
const typeBadge = this.createBadge(moveData.type.name, this.getTypeColor(moveData.type.name));
const damageColor = moveData.damage_class.name === 'physical' ? '#F08030' :
moveData.damage_class.name === 'special' ? '#6890F0' : '#A8A878';
const damageBadge = this.createBadge(moveData.damage_class.name, damageColor);
badgeContainer.append(typeBadge, damageBadge);
} else {
fetch(move.url)
.then(response => response.json())
.then(moveData => {
if (!this.moveDetailCache) this.moveDetailCache = new Map();
this.moveDetailCache.set(move.url, moveData);
const typeBadge = this.createBadge(moveData.type.name, this.getTypeColor(moveData.type.name));
const damageColor = moveData.damage_class.name === 'physical' ? '#F08030' :
moveData.damage_class.name === 'special' ? '#6890F0' : '#A8A878';
const damageBadge = this.createBadge(moveData.damage_class.name, damageColor);
badgeContainer.append(typeBadge, damageBadge);
})
.catch(err => {
console.error('Error fetching move details for grid tile:', err);
});
}
fragment.appendChild(tile);
});
this.gridContainer.appendChild(fragment);
if (filtered.length === 0) {
this.gridContainer.innerHTML = '<div style="padding:12px; color: var(--text-light); text-align: center;">No moves match your search.</div>';
}
}
// -----------------------------
// Advanced Move Details Section
// -----------------------------
async searchAdvancedMove(moveName) {
try {
const normalizedMove = moveName.trim().toLowerCase();
const response = await fetch(`https://pokeapi.co/api/v2/move/${normalizedMove}`);
if (!response.ok) throw new Error('Move not found');
const moveData = await response.json();
this.displayAdvancedMoveData(moveData);
} catch (error) {
console.error("Error fetching move details:", error);
this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading move details</div>`;
}
}
displayAdvancedPokemonData(data, speciesData, evoData) {
this.gridContainer.innerHTML = '';
const card = document.createElement('div');
card.className = 'poke-card animate-fadeIn';
card.style.maxWidth = '100%';
card.style.overflowX = 'hidden';
card.style.display = 'flex';
card.style.flexDirection = 'column';
card.style.gap = '16px';
// Assemble the card sections with animations
card.append(
this.createCardHeader(data),
this.createBasicInfoSection(data),
this.createStatsRadarChart(data),
this.createAbilitiesSection(data),
// Remove old type relations grid
// Instead, add our new advanced analysis section:
this.createAdvancedBattleAnalysisTab(data),
this.createPokedexEntrySection(speciesData),
this.createEvolutionVisualization(evoData.chain),
this.createMovesSection(data)
);
// Append optional sections
if (data.held_items && data.held_items.length > 0) {
card.appendChild(this.createHeldItemsSection(data));
}
if (data.forms && data.forms.length > 0) {
card.appendChild(this.createFormsSection(data));
}
this.gridContainer.appendChild(card);
this.initIntersectionObserver(); // Reinitialize for new lazy images
// Trigger an animation frame for smoother entrance
requestAnimationFrame(() => {
card.classList.add('visible');
});
}
displayAdvancedMoveData(moveData) {
this.gridContainer.innerHTML = '';
const card = document.createElement('div');
card.className = 'poke-card animate-fadeIn';
card.style.maxWidth = '100%';
card.style.overflowX = 'hidden';
card.style.display = 'flex';
card.style.flexDirection = 'column';
card.style.gap = '16px';
card.append(
this.createMoveCardHeader(moveData),
this.createMoveBasicInfoSection(moveData),
this.createMoveStatsChart(moveData),
this.createMoveDescriptionSection(moveData)
);
this.gridContainer.appendChild(card);
requestAnimationFrame(() => card.classList.add('visible'));
}
// -----------------------------
// Helper: Create a Badge Element
// -----------------------------
createBadge(text, backgroundColor) {
const badge = document.createElement('span');
badge.className = 'type-badge';
badge.textContent = text.toUpperCase();
badge.style.background = backgroundColor;
badge.style.padding = '3px 3px';
badge.style.borderRadius = '5px';
badge.style.fontSize = '10px';
badge.style.fontWeight = '700';
badge.style.color = '#fff';
badge.style.textShadow = '0 1px 2px rgba(0,0,0,0.3)';
return badge;
}
// -----------------------------
// Header: Move Title, Badges, and Numeric Details
// -----------------------------
createMoveCardHeader(moveData) {
const header = document.createElement('header');
header.className = 'poke-card-header animate-slideDown';
header.setAttribute('role', 'banner');
const title = document.createElement('h1');
title.className = 'poke-title';
title.style.fontSize = '32px';
title.style.margin = '0 0 8px';
title.textContent = moveData.name.charAt(0).toUpperCase() + moveData.name.slice(1);
const badgeContainer = document.createElement('div');
badgeContainer.style.display = 'flex';
badgeContainer.style.gap = '8px';
badgeContainer.style.marginBottom = '16px';
const typeBadge = this.createBadge(moveData.type.name, this.getTypeColor(moveData.type.name));
const damageColor = moveData.damage_class.name === 'physical'
? '#F08030'
: moveData.damage_class.name === 'special'
? '#6890F0'
: '#A8A878';
const damageBadge = this.createBadge(moveData.damage_class.name, damageColor);
badgeContainer.append(typeBadge, damageBadge);
const details = document.createElement('div');
details.style.display = 'grid';
details.style.gridTemplateColumns = 'repeat(4, auto)';
details.style.gap = '16px';
details.innerHTML = `
<div class="detail-item">
<span class="detail-label">POWER</span>
<span class="detail-value">${moveData.power !== null ? moveData.power : '—'}</span>
</div>
<div class="detail-item">
<span class="detail-label">ACC</span>
<span class="detail-value">${moveData.accuracy !== null ? moveData.accuracy : '—'}</span>
</div>
<div class="detail-item">
<span class="detail-label">PP</span>
<span class="detail-value">${moveData.pp}</span>
</div>
<div class="detail-item">
<span class="detail-label">PRI</span>
<span class="detail-value">${moveData.priority}</span>
</div>
`;
header.append(title, badgeContainer, details);
return header;
}
// -----------------------------
// Basic Info Section: Additional Move Details
// -----------------------------
createMoveBasicInfoSection(moveData) {
const section = document.createElement('section');
section.className = 'info-grid animate-fadeInUp';
section.innerHTML = `
<h3 class="section-title">BASIC INFO</h3>
<div class="metric">
<span class="label">CATEGORY</span>
<span class="value">${moveData.damage_class.name.toUpperCase()}</span>
</div>
<div class="metric">
<span class="label">TARGET</span>
<span class="value">${moveData.target.name.replace(/-/g, ' ').toUpperCase()}</span>
</div>
<div class="metric">
<span class="label">EFFECT CHANCE</span>
<span class="value">${moveData.effect_chance !== null ? moveData.effect_chance + '%' : '—'}</span>
</div>
`;
return section;
}
// -----------------------------
// Statistics Chart Section: Visualize Move Data with Chart.js
// -----------------------------
createMoveStatsChart(moveData) {
const section = document.createElement('section');
section.className = 'stats-chart-card animate-fadeInUp';
section.innerHTML = `<h3 class="section-title">STATISTICS</h3>`;
const chartContainer = document.createElement('div');
chartContainer.className = 'chart-container';
chartContainer.style.position = 'relative';
chartContainer.style.height = '220px';
chartContainer.style.width = '100%';
chartContainer.style.margin = '16px 0';
const canvas = document.createElement('canvas');
canvas.setAttribute('aria-label', 'Move statistics chart');
canvas.style.touchAction = 'none';
canvas.style.width = '100%';
canvas.style.height = '100%';
const renderChart = () => {
const ctx = canvas.getContext('2d');
const labels = [];
const dataValues = [];
if (moveData.power !== null) {
labels.push('Power');
dataValues.push(moveData.power);
}
if (moveData.accuracy !== null) {
labels.push('Accuracy');
dataValues.push(moveData.accuracy);
}
labels.push('PP');
dataValues.push(moveData.pp);
labels.push('Priority');
dataValues.push(moveData.priority);
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: moveData.name.toUpperCase(),
data: dataValues,
backgroundColor: [
'rgba(145,70,255,0.6)',
'rgba(245,25,255,0.6)',
'rgba(255,159,64,0.6)',
'rgba(255,64,64,0.6)'
],
borderColor: [
'rgba(145,70,255,1)',
'rgba(245,25,255,1)',
'rgba(255,159,64,1)',
'rgba(255,64,64,1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true }
},
plugins: {
legend: { display: false }
}
}
});
};
if (typeof Chart === 'undefined') {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
script.onload = renderChart;
script.onerror = () => {
console.error("Failed to load Chart.js");
section.innerHTML += `<div style="padding:12px; color: var(--text-light);">Failed to load chart library.</div>`;
};
document.head.appendChild(script);
} else {
renderChart();
}
chartContainer.appendChild(canvas);
section.appendChild(chartContainer);
return section;
}
// -----------------------------
// Move Description Section: Display Move Effect Text
// -----------------------------
createMoveDescriptionSection(moveData) {
const section = document.createElement('section');
section.className = 'move-description-section animate-fadeInUp';
section.innerHTML = `<h3 class="section-title">MOVE DESCRIPTION</h3>`;
const effectEntry = moveData.effect_entries.find(e => e.language.name === 'en');
const effectText = effectEntry
? effectEntry.effect.replace(/\n|\f/g, ' ')
: 'No description available.';
const descContainer = document.createElement('div');
descContainer.className = 'move-description';
descContainer.style.background = 'var(--background-darker)';
descContainer.style.padding = '16px';
descContainer.style.borderRadius = '8px';
descContainer.style.fontSize = '14px';
descContainer.style.lineHeight = '1.5';
descContainer.style.color = 'var(--text-muted)';
descContainer.textContent = effectText;
section.appendChild(descContainer);
return section;
}
// -----------------------------
// Instruction & Browse Sections
// -----------------------------
renderAdvancedInstruction() {
this.gridContainer.innerHTML = '';
const info = document.createElement('div');
info.style.padding = '12px';
info.style.textAlign = 'center';
info.style.color = 'var(--text-light)';
info.textContent = 'Enter a Pokémon name and press Enter for detailed info.';
this.gridContainer.appendChild(info);
}
renderBrowse() {
this.gridContainer.innerHTML = '';
if (!this.pokemonList) {
this.gridContainer.innerHTML = '<div class="spinner"></div>';
fetch('https://pokeapi.co/api/v2/pokemon?limit=20000')
.then(response => response.json())
.then(data => {
this.pokemonList = data.results;
this.renderBrowseGrid();
})
.catch(err => {
this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading Pokémon list</div>`;
});
} else {
this.renderBrowseGrid();
}
}
renderBrowseGrid() {
this.gridContainer.innerHTML = '';
this.gridContainer.classList.add('browse-container');
const query = this.searchInput.value.trim().toLowerCase();
const filtered = this.pokemonList.filter(poke => poke.name.includes(query));
const fragment = document.createDocumentFragment();
filtered.forEach(poke => {
const tile = document.createElement('div');
tile.className = 'browse-tile';
tile.dataset.label = poke.name.toLowerCase();
const idMatch = poke.url.match(/\/pokemon\/(\d+)\//);
const id = idMatch ? idMatch[1] : '';
const img = document.createElement('img');
img.src = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`;
const label = document.createElement('div');
label.className = 'tile-label';
label.textContent = poke.name;
tile.append(img, label);
tile.addEventListener('click', (e) => {
e.stopPropagation();
this.panel.classList.add('active');
this.changeTab('advanced');
this.searchInput.value = poke.name;
this.searchAdvancedPokemon(poke.name);
});
fragment.appendChild(tile);
});
this.gridContainer.appendChild(fragment);
if (filtered.length === 0) {
this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">No Pokémon match your search.</div>`;
}
}
// -----------------------------
// Render Pokémon with Sorting & Filtering via Dropdowns (with BST)
// -----------------------------
renderPokemon() {
this.gridContainer.innerHTML = '';
// Render the custom controls panel for Pokémon filters and sort.
this.renderPokemonControls();
if (!this.pokemonList) {
this.gridContainer.innerHTML = '<div class="spinner"></div>';
fetch('https://pokeapi.co/api/v2/pokemon?limit=20000')
.then(response => response.json())
.then(data => {
this.pokemonList = data.results;
this.renderPokemonGrid();
})
.catch(err => {
this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading Pokémon list</div>`;
});
} else {
this.renderPokemonGrid();
}
}
renderPokemonControls() {
let controlsContainer = document.getElementById('pokemon-controls');
if (!controlsContainer) {
controlsContainer = document.createElement('div');
controlsContainer.id = 'pokemon-controls';
Object.assign(controlsContainer.style, {
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
justifyContent: 'flex-start',
alignItems: 'center',
gap: '2px', // Slightly increased from 3px
marginBottom: '5px',
backgroundColor: '#202020',
color: '#fff',
padding: '1.8px', // Slightly increased from 2px
borderRadius: '5px',
overflowX: 'auto'
});
this.gridContainer.parentNode.insertBefore(controlsContainer, this.gridContainer);
}
controlsContainer.innerHTML = '';
// Updated helper function with slight size increases
const createControl = (labelText, id, optionsHTML, onChange) => {
const container = document.createElement('div');
Object.assign(container.style, {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '1.2px', // Slightly increased from 1px
flexShrink: '0',
minWidth: '50px', // Increased from 48px
maxWidth: '76px' // Increased from 72px
});
const label = document.createElement('div');
label.textContent = labelText;
Object.assign(label.style, {
fontSize: '11px', // Increased from 10px
textAlign: 'center',
whiteSpace: 'nowrap',
padding: '0 2px'
});
const select = document.createElement('select');
select.id = id;
Object.assign(select.style, {
backgroundColor: '#202020',
color: '#fff',
border: '1px solid #666',
borderRadius: '3px',
padding: '2px', // Increased from 1px
fontSize: '10px', // Increased from 9px
width: '92%',
height: '22px', // Increased from 22px
cursor: 'pointer'
});
select.innerHTML = optionsHTML;
select.addEventListener('change', onChange);
container.appendChild(label);
container.appendChild(select);
return container;
};
// Create all controls
const typeControl = createControl('Type', 'pokemon-type-filter', `
<option value="all">All</option>
<option value="normal">Normal</option>
<option value="fire">Fire</option>
<option value="water">Water</option>
<option value="grass">Grass</option>
<option value="electric">Electric</option>
<option value="ice">Ice</option>
<option value="fighting">Fighting</option>
<option value="poison">Poison</option>
<option value="ground">Ground</option>
<option value="flying">Flying</option>
<option value="psychic">Psychic</option>
<option value="bug">Bug</option>
<option value="rock">Rock</option>
<option value="ghost">Ghost</option>
<option value="dragon">Dragon</option>
<option value="dark">Dark</option>
<option value="steel">Steel</option>
<option value="fairy">Fairy</option>
`, () => {
this.selectedTypeFilter = document.getElementById('pokemon-type-filter').value;
this.renderPokemonGrid();
});
const weightControl = createControl('Weight', 'pokemon-weight-filter', `
<option value="all">All</option>
<option value="light"><30kg</option>
<option value="medium">30-70kg</option>
<option value="heavy">>70kg</option>
`, () => {
this.selectedWeightFilter = document.getElementById('pokemon-weight-filter').value;
this.renderPokemonGrid();
});
const heightControl = createControl('Height', 'pokemon-height-filter', `
<option value="all">All</option>
<option value="short"><1m</option>
<option value="medium">1-2m</option>
<option value="tall">>2m</option>
`, () => {
this.selectedHeightFilter = document.getElementById('pokemon-height-filter').value;
this.renderPokemonGrid();
});
const bstControl = createControl('BST', 'pokemon-bst-filter', `
<option value="all">All</option>
<option value="low"><400</option>
<option value="medium">400-600</option>
<option value="high">>600</option>
`, () => {
this.selectedBSTFilter = document.getElementById('pokemon-bst-filter').value;
this.renderPokemonGrid();
});
const sortControl = createControl('Sort', 'pokemon-sort', `
<option value="name-asc">Name A-Z</option>
<option value="name-desc">Name Z-A</option>
<option value="dex-asc">Dex Ascending</option>
<option value="dex-desc">Dex Descending</option>
<option value="bst-asc">BST Ascending</option>
<option value="bst-desc">BST Descending</option>
`, () => {
this.renderPokemonGrid();
});
// Append controls together
controlsContainer.appendChild(typeControl);
controlsContainer.appendChild(weightControl);
controlsContainer.appendChild(heightControl);
controlsContainer.appendChild(bstControl);
controlsContainer.appendChild(sortControl);
}
/**
* This function should be called when switching tabs to ensure
* the sorting controls are removed and re-rendered correctly.
*/
switchTab(tabName) {
console.log(`Switching to tab: ${tabName}`);
// Clear and rebuild controls
this.renderPokemonControls();
this.renderPokemonGrid(); // Re-render grid to reflect any new filters
}
// Render the Pokémon grid with search, filtering (Type, Weight, Height, BST), and sorting.
renderPokemonGrid() {
this.gridContainer.innerHTML = '';
this.gridContainer.classList.add('browse-container');
const query = this.searchInput.value.trim().toLowerCase();
let filtered = this.pokemonList.filter(poke => poke.name.includes(query));
// Filter by Type if selected.
if (this.selectedTypeFilter && this.selectedTypeFilter !== 'all') {
filtered = filtered.filter(poke => {
if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
const pokeData = this.pokemonDetailCache.get(poke.url);
return pokeData.types.some(typeInfo => typeInfo.type.name === this.selectedTypeFilter);
}
return true;
});
}
// Filter by Weight if selected.
if (this.selectedWeightFilter && this.selectedWeightFilter !== 'all') {
filtered = filtered.filter(poke => {
if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
const pokeData = this.pokemonDetailCache.get(poke.url);
const weightKg = pokeData.weight / 10;
if (this.selectedWeightFilter === 'light') {
return weightKg < 30;
} else if (this.selectedWeightFilter === 'medium') {
return weightKg >= 30 && weightKg <= 70;
} else if (this.selectedWeightFilter === 'heavy') {
return weightKg > 70;
}
}
return true;
});
}
// Filter by Height if selected.
if (this.selectedHeightFilter && this.selectedHeightFilter !== 'all') {
filtered = filtered.filter(poke => {
if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
const pokeData = this.pokemonDetailCache.get(poke.url);
const heightM = pokeData.height / 10;
if (this.selectedHeightFilter === 'short') {
return heightM < 1;
} else if (this.selectedHeightFilter === 'medium') {
return heightM >= 1 && heightM <= 2;
} else if (this.selectedHeightFilter === 'tall') {
return heightM > 2;
}
}
return true;
});
}
// Filter by BST if selected.
if (this.selectedBSTFilter && this.selectedBSTFilter !== 'all') {
filtered = filtered.filter(poke => {
if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
const pokeData = this.pokemonDetailCache.get(poke.url);
const totalStats = pokeData.stats.reduce((sum, stat) => sum + stat.base_stat, 0);
if (this.selectedBSTFilter === 'low') {
return totalStats < 400;
} else if (this.selectedBSTFilter === 'medium') {
return totalStats >= 400 && totalStats <= 600;
} else if (this.selectedBSTFilter === 'high') {
return totalStats > 600;
}
}
return true;
});
}
// Sort the filtered Pokémon.
const sortOption = document.getElementById('pokemon-sort')?.value || 'name-asc';
if (sortOption === 'name-asc') {
filtered.sort((a, b) => a.name.localeCompare(b.name));
} else if (sortOption === 'name-desc') {
filtered.sort((a, b) => b.name.localeCompare(a.name));
} else if (sortOption === 'dex-asc') {
filtered.sort((a, b) => {
const idA = parseInt(a.url.match(/\/pokemon\/(\d+)\//)[1]);
const idB = parseInt(b.url.match(/\/pokemon\/(\d+)\//)[1]);
return idA - idB;
});
} else if (sortOption === 'dex-desc') {
filtered.sort((a, b) => {
const idA = parseInt(a.url.match(/\/pokemon\/(\d+)\//)[1]);
const idB = parseInt(b.url.match(/\/pokemon\/(\d+)\//)[1]);
return idB - idA;
});
} else if (sortOption === 'bst-asc') {
filtered.sort((a, b) => {
const bstA = (this.pokemonDetailCache && this.pokemonDetailCache.has(a.url))
? this.pokemonDetailCache.get(a.url).stats.reduce((sum, stat) => sum + stat.base_stat, 0)
: 0;
const bstB = (this.pokemonDetailCache && this.pokemonDetailCache.has(b.url))
? this.pokemonDetailCache.get(b.url).stats.reduce((sum, stat) => sum + stat.base_stat, 0)
: 0;
return bstA - bstB;
});
} else if (sortOption === 'bst-desc') {
filtered.sort((a, b) => {
const bstA = (this.pokemonDetailCache && this.pokemonDetailCache.has(a.url))
? this.pokemonDetailCache.get(a.url).stats.reduce((sum, stat) => sum + stat.base_stat, 0)
: 0;
const bstB = (this.pokemonDetailCache && this.pokemonDetailCache.has(b.url))
? this.pokemonDetailCache.get(b.url).stats.reduce((sum, stat) => sum + stat.base_stat, 0)
: 0;
return bstB - bstA;
});
}
// Build the grid as before.
const fragment = document.createDocumentFragment();
filtered.forEach(poke => {
const tile = document.createElement('div');
tile.className = 'pokemon-card';
// Hover effects.
tile.addEventListener('mouseenter', () => {
tile.style.boxShadow = '0 0 15px rgba(255, 255, 255, 0.6)';
tile.style.transform = 'scale(1.05)';
});
tile.addEventListener('mouseleave', () => {
tile.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.1)';
tile.style.transform = 'scale(1)';
});
// Dex Number (Top Left)
const dexNumber = document.createElement('div');
dexNumber.className = 'dex-number';
tile.appendChild(dexNumber);
// Image Container
const imageContainer = document.createElement('div');
imageContainer.className = 'pokemon-image-container';
tile.appendChild(imageContainer);
// Pokémon Image with lazy loading.
const img = document.createElement('img');
img.className = 'pokemon-image';
img.loading = 'lazy';
imageContainer.appendChild(img);
// Extract Pokémon ID from URL.
const idMatch = poke.url.match(/\/pokemon\/(\d+)\//);
const id = idMatch ? idMatch[1] : '';
// Try several image sources with fallbacks.
const imageSources = [
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/showdown/${id}.gif`,
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`,
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/${id}.svg`,
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`,
`https://via.placeholder.com/150x150?text=No+Image`
];
let imgIndex = 0;
function loadNextImage() {
if (imgIndex >= imageSources.length) return;
img.src = imageSources[imgIndex];
img.onerror = () => {
imgIndex++;
loadNextImage();
};
}
loadNextImage();
// Pokémon Name
const nameLabel = document.createElement('div');
nameLabel.className = 'pokemon-name';
nameLabel.textContent = poke.name;
tile.appendChild(nameLabel);
// Type Badges (Centered)
const typesContainer = document.createElement('div');
typesContainer.className = 'pokemon-types';
tile.appendChild(typesContainer);
// Basic Info container (for height, weight, BST, etc.)
const basicInfoContainer = document.createElement('div');
basicInfoContainer.className = 'pokemon-info';
basicInfoContainer.textContent = 'Loading info...';
tile.appendChild(basicInfoContainer);
// Function to update the tile with fetched Pokémon details.
const updatePokemonDetails = (detail) => {
dexNumber.textContent = `#${detail.id}`;
typesContainer.innerHTML = '';
detail.types.forEach(typeInfo => {
const badge = this.createBadge(
typeInfo.type.name,
this.getTypeColor(typeInfo.type.name)
);
typesContainer.appendChild(badge);
});
const height = (detail.height / 10).toFixed(1);
const weight = (detail.weight / 10).toFixed(1);
const baseExp = detail.base_experience;
const totalStats = detail.stats.reduce((sum, stat) => sum + stat.base_stat, 0);
basicInfoContainer.innerHTML = `
<span> ${height}m | ${weight}kg </span>
<span> Exp: ${baseExp} | BST: ${totalStats} </span>
`;
};
// Fetch details if not already cached.
if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
const cachedDetail = this.pokemonDetailCache.get(poke.url);
updatePokemonDetails(cachedDetail);
} else {
fetch(poke.url)
.then(response => response.json())
.then(detail => {
if (!this.pokemonDetailCache) this.pokemonDetailCache = new Map();
this.pokemonDetailCache.set(poke.url, detail);
updatePokemonDetails(detail);
})
.catch(err => {
console.error("Error fetching Pokémon detail:", err);
basicInfoContainer.textContent = 'Info not available';
});
}
// Click event for showing detailed view.
tile.addEventListener('click', (e) => {
e.stopPropagation();
this.panel.classList.add('active');
this.changeTab('advanced');
this.searchInput.value = poke.name;
this.searchAdvancedPokemon(poke.name);
});
fragment.appendChild(tile);
});
this.gridContainer.appendChild(fragment);
if (filtered.length === 0) {
this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">No Pokémon match your search.</div>`;
}
}
// -----------------------------
// Add Global Event Listeners
// -----------------------------
addEventListeners() {
this.button.addEventListener('mousedown', this.dragStart);
this.button.addEventListener('click', (e) => {
if (this.wasDragging) {
this.wasDragging = false;
return;
}
e.stopPropagation();
this.panel.classList.toggle('active');
if (this.panel.classList.contains('active')) {
this.searchInput.focus();
}
});
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) {
this.panel.classList.remove('active');
}
});
this.panel.addEventListener('dragstart', (e) => {
const ballImg = e.target.closest('.pball-item img');
if (ballImg) {
e.dataTransfer.setData('text/plain', ballImg.dataset.ballType);
const dragImg = new Image();
dragImg.src = ballImg.src;
dragImg.style.width = '36px';
dragImg.style.height = '36px';
dragImg.style.position = 'absolute';
dragImg.style.left = '-9999px';
document.body.appendChild(dragImg);
e.dataTransfer.setDragImage(dragImg, 18, 18);
setTimeout(() => document.body.removeChild(dragImg), 0);
ballImg.classList.add('dragging');
const onDragEnd = () => {
ballImg.classList.remove('dragging');
document.removeEventListener('dragend', onDragEnd);
};
document.addEventListener('dragend', onDragEnd);
}
});
const chatInput = document.querySelector('#chatInput');
if (chatInput) {
chatInput.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
chatInput.addEventListener('drop', (e) => {
e.preventDefault();
const ballType = e.dataTransfer.getData('text/plain');
if (ballType) {
chatInput.value += ` ${ballType}`;
}
});
}
const tabs = this.panel.querySelectorAll('.pball-tab');
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
e.stopPropagation();
this.changeTab(tab.dataset.tab);
});
});
// Use debounced handler for the search input
const debouncedSearch = this.debounce(() => {
if (this.currentTab !== 'advanced') {
this.filterGrid();
if (this.currentTab === 'pokemon') {
this.renderPokemonGrid();
}
}
this.clearBtn.style.display = this.searchInput.value.trim() ? 'block' : 'none';
}, 300);
this.searchInput.addEventListener('input', debouncedSearch);
this.clearBtn.addEventListener('click', () => {
this.searchInput.value = '';
this.clearBtn.style.display = 'none';
if (this.currentTab !== 'advanced') {
this.filterGrid();
if (this.currentTab === 'browse') {
this.renderBrowseGrid();
}
}
});
this.searchInput.addEventListener('keydown', (e) => {
if (this.currentTab === 'advanced' && e.key === 'Enter') {
this.searchAdvancedPokemon(this.searchInput.value.trim());
}
});
}
// 2. Update changeTab() to handle the new "moves" tab
changeTab(tabName) {
this.currentTab = tabName;
const tabs = this.panel.querySelectorAll('.pball-tab');
tabs.forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
if (tabName === 'advanced') {
this.searchInput.placeholder = 'Enter Pokémon name for detailed info...';
} else if (tabName === 'pokemon') {
this.searchInput.placeholder = 'Filter Pokémon...';
} else if (tabName === 'moves') {
this.searchInput.placeholder = 'Filter moves...';
} else {
this.searchInput.placeholder = 'Search...';
}
this.searchInput.value = '';
this.clearBtn.style.display = 'none';
this.renderGrid();
}
filterGrid() {
const query = this.searchInput.value.trim().toLowerCase();
const items = this.gridContainer.querySelectorAll('.pball-item, .browse-tile');
items.forEach(item => {
if (!query || item.dataset.label.includes(query)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
}
getChatInput() {
return document.querySelector('[data-a-target="chat-input"]');
}
insertCommand(ballType) {
const chatInput = this.getChatInput();
if (!chatInput) return;
chatInput.focus();
this.clearChatInput();
this.insertText(ballType);
this.triggerInputEvent(chatInput);
}
clearChatInput() {
const chatInput = this.getChatInput();
if (chatInput) {
chatInput.value = '';
this.triggerInputEvent(chatInput);
}
}
insertText(text) {
document.execCommand('insertText', false, text);
}
triggerInputEvent(element) {
element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
}
searchAdvancedPokemon(name) {
if (!name) return;
this.gridContainer.innerHTML = '<div class="spinner"></div>';
fetch(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`)
.then(response => {
if (!response.ok) { throw new Error("Pokémon not found"); }
return response.json();
})
.then(data => {
return fetch(data.species.url)
.then(res => {
if (!res.ok) { throw new Error("Species data not found"); }
return res.json().then(speciesData => ({ data, speciesData }));
});
})
.then(({ data, speciesData }) => {
return fetch(speciesData.evolution_chain.url)
.then(res => {
if (!res.ok) { throw new Error("Evolution chain not found"); }
return res.json().then(evoData => ({ data, speciesData, evoData }));
});
})
.then(({ data, speciesData, evoData }) => {
this.displayAdvancedPokemonData(data, speciesData, evoData);
})
.catch(err => {
this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">${err.message}</div>`;
});
}
createAdvancedBattleAnalysisTab(data) {
const container = document.createElement('section');
container.className = 'advanced-battle-analysis animate-fadeInUp';
container.style.padding = '1rem';
container.style.borderTop = '1px solid var(--color-border)';
// Section title
const title = document.createElement('h3');
title.className = 'section-title';
title.textContent = 'Comprehensive Type Interactions';
container.appendChild(title);
// Grid container for type cards
const grid = document.createElement('div');
grid.style.display = 'grid';
grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(250px, 1fr))';
grid.style.gap = '16px';
// Create an interactive card for each type
data.types.forEach(typeObj => {
const typeCard = document.createElement('div');
typeCard.className = 'advanced-type-card';
typeCard.style.border = '1px solid var(--color-border)';
typeCard.style.borderRadius = '8px';
typeCard.style.background = 'var(--color-card)';
typeCard.style.boxShadow = 'var(--box-shadow-light)';
typeCard.style.overflow = 'hidden';
typeCard.style.display = 'flex';
typeCard.style.flexDirection = 'column';
// Card header with type name and color
const header = document.createElement('div');
header.textContent = typeObj.type.name.toUpperCase();
header.style.background = this.getTypeColor(typeObj.type.name);
header.style.color = '#fff';
header.style.padding = '8px';
header.style.fontWeight = 'bold';
header.style.textAlign = 'center';
typeCard.appendChild(header);
// Tab header container for "Offense" and "Defense"
const tabHeaderContainer = document.createElement('div');
tabHeaderContainer.style.display = 'flex';
// Both buttons now use the card's background
const offenseTab = document.createElement('button');
offenseTab.textContent = 'Offense';
offenseTab.style.flex = '1';
offenseTab.style.padding = '8px';
offenseTab.style.border = 'none';
offenseTab.style.cursor = 'pointer';
offenseTab.style.background = 'var(--color-card)';
offenseTab.style.fontWeight = 'bold';
offenseTab.style.transition = 'border-bottom 0.2s ease';
const defenseTab = document.createElement('button');
defenseTab.textContent = 'Defense';
defenseTab.style.flex = '1';
defenseTab.style.padding = '8px';
defenseTab.style.border = 'none';
defenseTab.style.cursor = 'pointer';
defenseTab.style.background = 'var(--color-card)';
defenseTab.style.fontWeight = 'bold';
defenseTab.style.transition = 'border-bottom 0.2s ease';
tabHeaderContainer.append(offenseTab, defenseTab);
typeCard.appendChild(tabHeaderContainer);
// Create content containers for each tab
const offenseContent = document.createElement('div');
offenseContent.style.padding = '8px';
offenseContent.style.display = 'block';
const defenseContent = document.createElement('div');
defenseContent.style.padding = '8px';
defenseContent.style.display = 'none';
typeCard.appendChild(offenseContent);
typeCard.appendChild(defenseContent);
// Fetch the type data and build the interactive details
fetch(typeObj.type.url)
.then(res => res.json())
.then(typeData => {
// --- Offense details ---
const offenseSections = [
{ label: 'Double Damage To', data: typeData.damage_relations.double_damage_to },
{ label: 'Half Damage To', data: typeData.damage_relations.half_damage_to },
{ label: 'No Damage To', data: typeData.damage_relations.no_damage_to }
];
offenseSections.forEach(section => {
const sectionTitle = document.createElement('h4');
sectionTitle.textContent = section.label;
sectionTitle.style.marginBottom = '4px';
sectionTitle.style.fontSize = '0.9rem';
offenseContent.appendChild(sectionTitle);
const sectionContent = document.createElement('div');
sectionContent.style.display = 'flex';
sectionContent.style.flexWrap = 'wrap';
sectionContent.style.gap = '4px';
if (section.data.length > 0) {
section.data.forEach(item => {
const badge = document.createElement('span');
badge.textContent = item.name.toUpperCase();
badge.style.background = this.getTypeColor(item.name);
badge.style.color = '#fff';
badge.style.padding = '2px 6px';
badge.style.borderRadius = '4px';
badge.style.fontSize = '0.8rem';
sectionContent.appendChild(badge);
});
} else {
const noData = document.createElement('span');
noData.textContent = 'None';
noData.style.fontSize = '0.8rem';
sectionContent.appendChild(noData);
}
offenseContent.appendChild(sectionContent);
});
// --- Defense details ---
const defenseSections = [
{ label: 'Double Damage From', data: typeData.damage_relations.double_damage_from },
{ label: 'Half Damage From', data: typeData.damage_relations.half_damage_from },
{ label: 'No Damage From', data: typeData.damage_relations.no_damage_from }
];
defenseSections.forEach(section => {
const sectionTitle = document.createElement('h4');
sectionTitle.textContent = section.label;
sectionTitle.style.marginBottom = '4px';
sectionTitle.style.fontSize = '0.9rem';
defenseContent.appendChild(sectionTitle);
const sectionContent = document.createElement('div');
sectionContent.style.display = 'flex';
sectionContent.style.flexWrap = 'wrap';
sectionContent.style.gap = '4px';
if (section.data.length > 0) {
section.data.forEach(item => {
const badge = document.createElement('span');
badge.textContent = item.name.toUpperCase();
badge.style.background = this.getTypeColor(item.name);
badge.style.color = '#fff';
badge.style.padding = '2px 6px';
badge.style.borderRadius = '4px';
badge.style.fontSize = '0.8rem';
sectionContent.appendChild(badge);
});
} else {
const noData = document.createElement('span');
noData.textContent = 'None';
noData.style.fontSize = '0.8rem';
sectionContent.appendChild(noData);
}
defenseContent.appendChild(sectionContent);
});
})
.catch(err => {
console.error('Error fetching type interactions:', err);
offenseContent.textContent = 'Unable to load data.';
defenseContent.textContent = 'Unable to load data.';
});
// --- Tab switching functionality ---
// Instead of changing the background, we use a bottom border to indicate the active tab.
offenseTab.addEventListener('click', () => {
offenseTab.style.borderBottom = '2px solid var(--color-primary)';
defenseTab.style.borderBottom = 'none';
offenseContent.style.display = 'block';
defenseContent.style.display = 'none';
});
defenseTab.addEventListener('click', () => {
defenseTab.style.borderBottom = '2px solid var(--color-primary)';
offenseTab.style.borderBottom = 'none';
offenseContent.style.display = 'none';
defenseContent.style.display = 'block';
});
// Set initial active state for offense tab
offenseTab.style.borderBottom = '2px solid var(--color-primary)';
grid.appendChild(typeCard);
});
container.appendChild(grid);
return container;
}
/**
* Helper function to get a color based on the Pokémon type.
* @param {string} typeName
* @returns {string} The color string.
*/
getTypeColor(typeName) {
const typeColors = {
normal: '#A8A878',
fire: '#F08030',
water: '#6890F0',
electric: '#F8D030',
grass: '#78C850',
ice: '#98D8D8',
fighting: '#C03028',
poison: '#A040A0',
ground: '#E0C068',
flying: '#A890F0',
psychic: '#F85888',
bug: '#A8B820',
rock: '#B8A038',
ghost: '#705898',
dragon: '#7038F8',
dark: '#705848',
steel: '#B8B8D0',
fairy: '#EE99AC'
};
return typeColors[typeName] || '#68A090';
}
// CARD HEADER WITH IMAGE, NAME, AND DETAILS
createCardHeader(data) {
const header = document.createElement('header');
header.className = 'poke-card-header animate-slideDown';
header.setAttribute('role', 'banner');
// Image container with lazy loading and type badges
const imgContainer = document.createElement('div');
imgContainer.style.position = 'relative';
const img = document.createElement('img');
img.className = 'poke-image lazy';
img.style.width = '90px';
img.style.height = '90px';
img.style.borderRadius = '16px';
img.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
img.dataset.src =
data.sprites.other?.['official-artwork']?.front_default ||
data.sprites.front_default;
img.src = ''; // initially empty—loaded via IntersectionObserver
// Type badges positioned over the image
const typeBadges = document.createElement('div');
typeBadges.style.display = 'flex';
typeBadges.style.gap = '4px';
typeBadges.style.position = 'absolute';
typeBadges.style.bottom = '60px';
typeBadges.style.left = '80%';
typeBadges.style.transform = 'translateX(-50%)';
data.types.forEach(type => {
const badge = document.createElement('span');
badge.className = 'type-badge';
badge.textContent = type.type.name.toUpperCase();
badge.style.background = this.getTypeColor(type.type.name);
badge.style.padding = '3px 3px';
badge.style.borderRadius = '5px';
badge.style.fontSize = '12px';
badge.style.fontWeight = '700';
badge.style.color = '#fff';
badge.style.textShadow = '0 1px 2px rgba(0,0,0,0.3)';
typeBadges.appendChild(badge);
});
imgContainer.append(img, typeBadges);
// Title and details section
const titleSection = document.createElement('div');
const title = document.createElement('h1');
title.className = 'poke-title';
title.style.fontSize = '32px';
title.style.margin = '0 0 8px';
title.textContent =
data.name.charAt(0).toUpperCase() + data.name.slice(1);
const details = document.createElement('div');
details.style.display = 'grid';
details.style.gridTemplateColumns = 'repeat(3, auto)';
details.style.gap = '16px';
details.innerHTML = `
<div class="detail-item">
<span class="detail-label">ID</span>
<span class="detail-value">#${data.id
.toString()
.padStart(3, '0')}</span>
</div>
<div class="detail-item">
<span class="detail-label">EXP</span>
<span class="detail-value">${data.base_experience}</span>
</div>
<div class="detail-item">
<span class="detail-label">SPECIES</span>
<span class="detail-value">${data.species.name}</span>
</div>
`;
titleSection.append(title, details);
header.append(imgContainer, titleSection);
return header;
}
// POKÉDEX ENTRY SECTION
createPokedexEntrySection(speciesData) {
const section = document.createElement('section');
section.className = 'pokedex-entry-section animate-fadeInUp';
section.innerHTML = `<h3 class="section-title">POKÉDEX ENTRY</h3>`;
const entry = speciesData.flavor_text_entries.find(
e => e.language.name === 'en'
);
const flavorText = entry
? entry.flavor_text.replace(/\f|\n/g, ' ')
: 'No entry available.';
const entryContainer = document.createElement('div');
entryContainer.className = 'pokedex-entry';
entryContainer.style.background = 'var(--background-darker)';
entryContainer.style.padding = '16px';
entryContainer.style.borderRadius = '8px';
entryContainer.style.fontSize = '14px';
entryContainer.style.lineHeight = '1.5';
entryContainer.style.color = 'var(--text-muted)';
entryContainer.textContent = flavorText;
section.appendChild(entryContainer);
return section;
}
// BASIC PHYSICAL INFORMATION SECTION
createBasicInfoSection(data) {
const section = document.createElement('section');
section.className = 'info-grid animate-fadeInUp';
section.innerHTML = `
<h3 class="section-title">PHYSICAL TRAITS</h3>
<div class="metric">
<i class="icon-height"></i>
<span class="label">Height</span>
<span class="value">${data.height / 10}m</span>
</div>
<div class="metric">
<i class="icon-weight"></i>
<span class="label">Weight</span>
<span class="value">${data.weight / 10}kg</span>
</div>
<div class="metric">
<i class="icon-stats"></i>
<span class="label">Total Stats</span>
<span class="value">${data.stats.reduce(
(sum, s) => sum + s.base_stat,
0
)}</span>
</div>
`;
return section;
}
// ABILITIES SECTION WITH TOOLTIP (using async/await)
createAbilitiesSection(data) {
const section = document.createElement('section');
section.className = 'abilities-section animate-fadeInUp';
section.innerHTML = `<h3 class="section-title">ABILITIES</h3>`;
const abilitiesGrid = document.createElement('div');
abilitiesGrid.className = 'abilities-grid';
abilitiesGrid.style.display = 'grid';
abilitiesGrid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(140px, 1fr))';
abilitiesGrid.style.gap = '12px';
data.abilities.forEach(ability => {
const abilityCard = document.createElement('div');
abilityCard.className = 'ability-card';
abilityCard.style.background = 'var(--background-darker)';
abilityCard.style.padding = '12px';
abilityCard.style.borderRadius = '8px';
abilityCard.style.textAlign = 'center';
abilityCard.style.position = 'relative';
abilityCard.style.cursor = 'pointer';
const abilityName = document.createElement('div');
abilityName.textContent = ability.ability.name.replace(/-/g, ' ');
abilityName.style.fontWeight = '500';
abilityName.style.textTransform = 'capitalize';
if (ability.is_hidden) {
const hiddenBadge = document.createElement('div');
hiddenBadge.textContent = 'Hidden';
hiddenBadge.style.position = 'absolute';
hiddenBadge.style.top = '4px';
hiddenBadge.style.right = '4px';
hiddenBadge.style.background = '#FF6B6B';
hiddenBadge.style.color = '#FFF';
hiddenBadge.style.fontSize = '10px';
hiddenBadge.style.padding = '2px 6px';
hiddenBadge.style.borderRadius = '12px';
abilityCard.appendChild(hiddenBadge);
}
abilityCard.appendChild(abilityName);
abilitiesGrid.appendChild(abilityCard);
// Use async/await for fetching ability descriptions
abilityCard.addEventListener('mouseenter', async () => {
try {
const res = await fetch(ability.ability.url);
const abilityData = await res.json();
const description =
abilityData.effect_entries.find(e => e.language.name === 'en')?.effect ||
'No description available.';
this.showTooltip(abilityCard, description);
} catch (err) {
console.error('Error fetching ability data:', err);
}
});
abilityCard.addEventListener('mouseleave', () => {
this.hideTooltip();
});
});
section.appendChild(abilitiesGrid);
return section;
}
// Show tooltip with smooth fade-in/out transitions using a solid background color
showTooltip(element, text) {
if (this.tooltip) this.tooltip.remove();
this.tooltip = document.createElement('div');
this.tooltip.className = 'tooltip animate-fadeIn';
this.tooltip.textContent = text;
const rect = element.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const tooltipHeight = 100; // estimated height
let topPosition = rect.bottom + 8;
if (topPosition + tooltipHeight > viewportHeight) {
topPosition = rect.top - tooltipHeight - 8;
}
Object.assign(this.tooltip.style, {
background: 'var(--color-card)', // Updated to a solid color
color: 'var(--text-light)',
borderRadius: '6px',
padding: '8px 12px',
position: 'fixed',
top: `${topPosition}px`,
left: `${rect.left}px`,
maxWidth: '240px',
zIndex: '10000',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
pointerEvents: 'none',
opacity: '0',
transition: 'opacity 0.3s ease'
});
document.body.appendChild(this.tooltip);
requestAnimationFrame(() => {
this.tooltip.style.opacity = '1';
});
}
hideTooltip() {
if (this.tooltip) {
this.tooltip.style.opacity = '0';
setTimeout(() => {
if (this.tooltip) {
this.tooltip.remove();
this.tooltip = null;
}
}, 300);
}
}
// MOVES SECTION WITH SEARCH AND DETAILED TOOLTIP
createMovesSection(data) {
const section = document.createElement('section');
section.className = 'moves-section animate-fadeInUp';
section.innerHTML = `<h3 class="section-title">MOVES</h3>`;
// Search container (same as before)
const searchContainer = document.createElement('div');
searchContainer.className = 'pball-search-container';
const searchInput = document.createElement('input');
searchInput.className = 'pball-search';
searchInput.placeholder = 'Search moves...';
searchInput.setAttribute('aria-label', 'Search moves');
const searchButtons = document.createElement('div');
searchButtons.className = 'search-buttons';
const enterButton = document.createElement('button');
enterButton.className = 'search-enter-button';
enterButton.textContent = '✔';
const clearButton = document.createElement('button');
clearButton.className = 'pball-clear-btn';
clearButton.textContent = 'X';
let searchTimeout;
const handleSearch = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const query = searchInput.value.trim().toLowerCase();
Array.from(movesList.children).forEach(move => {
move.style.display = move.textContent.toLowerCase().includes(query)
? 'block'
: 'none';
});
}, 300);
};
searchInput.addEventListener('input', handleSearch);
enterButton.addEventListener('click', handleSearch);
clearButton.addEventListener('click', () => {
searchInput.value = '';
handleSearch();
});
searchButtons.append(enterButton, clearButton);
searchContainer.append(searchInput, searchButtons);
section.append(searchContainer);
// Moves list container with virtual scroll behavior
const movesList = document.createElement('div');
movesList.className = 'moves-list';
movesList.style.maxHeight = '200px';
movesList.style.overflowY = 'auto';
movesList.style.display = 'grid';
movesList.style.gap = '8px';
data.moves.forEach(move => {
const moveItem = document.createElement('div');
moveItem.className = 'move-item';
moveItem.textContent = move.move.name.replace(/-/g, ' ');
moveItem.style.padding = '8px 12px';
moveItem.style.background = 'var(--background-darker)';
moveItem.style.borderRadius = '6px';
moveItem.style.textTransform = 'capitalize';
moveItem.style.cursor = 'pointer';
// Fetch and show move details on hover
moveItem.addEventListener('mouseenter', async () => {
try {
const res = await fetch(move.move.url);
const moveData = await res.json();
const effectEntry = moveData.effect_entries.find(
e => e.language.name === 'en'
);
const effectText = effectEntry
? effectEntry.short_effect.replace(/\n|\f/g, ' ')
: 'No description available.';
// Format move details
const details = `
Name: ${moveData.name.replace(/-/g, ' ')}
Type: ${moveData.type.name.toUpperCase()}
Category: ${moveData.damage_class.name.toUpperCase()}
Power: ${moveData.power || '—'}
Accuracy: ${moveData.accuracy || '—'}
PP: ${moveData.pp}
Priority: ${moveData.priority}
Effect: ${effectText}
`;
this.showTooltip(moveItem, details);
} catch (error) {
console.error('Error fetching move data:', error);
}
});
moveItem.addEventListener('mouseleave', () => {
this.hideTooltip();
});
movesList.appendChild(moveItem);
});
section.appendChild(movesList);
return section;
}
// HELD ITEMS SECTION
createHeldItemsSection(data) {
const section = document.createElement('section');
section.className = 'held-items-section animate-fadeInUp';
section.innerHTML = `<h3 class="section-title">HELD ITEMS</h3>`;
const itemsGrid = document.createElement('div');
itemsGrid.className = 'items-grid';
itemsGrid.style.display = 'grid';
itemsGrid.style.gridTemplateColumns =
'repeat(auto-fit, minmax(120px, 1fr))';
itemsGrid.style.gap = '12px';
data.held_items.forEach(item => {
const itemCard = document.createElement('div');
itemCard.className = 'item-card';
itemCard.textContent = item.item.name.replace(/-/g, ' ');
itemCard.style.padding = '12px';
itemCard.style.background = 'var(--background-darker)';
itemCard.style.borderRadius = '8px';
itemCard.style.textAlign = 'center';
itemCard.style.textTransform = 'capitalize';
itemsGrid.appendChild(itemCard);
});
section.appendChild(itemsGrid);
return section;
}
// FORMS SECTION
createFormsSection(data) {
const section = document.createElement('section');
section.className = 'forms-section animate-fadeInUp';
section.innerHTML = `<h3 class="section-title">FORMS</h3>`;
const formsGrid = document.createElement('div');
formsGrid.className = 'forms-grid';
formsGrid.style.display = 'grid';
formsGrid.style.gridTemplateColumns =
'repeat(auto-fit, minmax(120px, 1fr))';
formsGrid.style.gap = '12px';
data.forms.forEach(form => {
const formCard = document.createElement('div');
formCard.className = 'form-card';
formCard.textContent = form.name.replace(/-/g, ' ');
formCard.style.padding = '12px';
formCard.style.background = 'var(--background-darker)';
formCard.style.borderRadius = '8px';
formCard.style.textAlign = 'center';
formCard.style.textTransform = 'capitalize';
formsGrid.appendChild(formCard);
});
section.appendChild(formsGrid);
return section;
}
// STATS RADAR CHART SECTION (with Chart.js and animated rendering)
createStatsRadarChart(data) {
const section = document.createElement('section');
section.className = 'stats-radar-card animate-fadeInUp';
section.innerHTML = `
<div class="stats-header">
<h3 class="section-title">Stat Distribution</h3>
</span>
</div>
</div>
</div>
`;
const chartContainer = document.createElement('div');
chartContainer.className = 'radar-container';
chartContainer.style.position = 'relative';
chartContainer.style.height = 'clamp(280px, 35vh, 400px)';
chartContainer.style.margin = '16px 0';
const canvas = document.createElement('canvas');
canvas.setAttribute('aria-label', 'Pokémon stat radar chart');
canvas.style.touchAction = 'none';
const typeColor = this.getTypeColor(data.types[0].type.name);
const gradient = {
light: this.hexToRgba(typeColor, 0.3),
dark: this.hexToRgba(typeColor, 0.1)
};
if (!window.Chart) {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
script.onload = () => this.drawEnhancedRadar(canvas, data, gradient);
script.onerror = () => this.showChartError(chartContainer);
document.head.appendChild(script);
} else {
this.drawEnhancedRadar(canvas, data, gradient);
}
chartContainer.appendChild(canvas);
section.appendChild(chartContainer);
return section;
}
drawEnhancedRadar(canvas, data, gradient) {
try {
const ctx = canvas.getContext('2d');
const stats = data.stats.map(s => s.base_stat);
const labels = data.stats.map(s => ({
full: s.stat.name.replace(/-/g, ' '),
short: this.getStatAbbreviation(s.stat.name)
}));
const chartGradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
chartGradient.addColorStop(0, gradient.light);
chartGradient.addColorStop(1, gradient.dark);
new Chart(ctx, {
type: 'radar',
data: {
labels: labels.map(l => l.short),
datasets: [
{
data: stats,
backgroundColor: chartGradient,
borderColor: this.hexToRgba(gradient.light, 0.8),
borderWidth: 1.8,
pointBackgroundColor: '#ffffff',
pointBorderColor: gradient.light,
pointHoverRadius: 8,
pointRadius: 4,
pointHitRadius: 12,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeOutQuint'
},
scales: {
r: {
beginAtZero: true,
max: Math.ceil(Math.max(...stats) / 10) * 10 + 10,
ticks: {
display: false,
count: 5,
z: 1
},
grid: {
color: 'rgba(255, 255, 255, 0.12)',
circular: true,
lineWidth: 0.8
},
pointLabels: {
color: '#ffffff',
font: {
size: 13,
weight: '500'
},
callback: (value, index) => [`${value}`, stats[index]],
padding: 18
},
angleLines: {
color: 'rgba(255, 255, 255, 0.08)',
lineWidth: 0.8
}
}
},
plugins: {
legend: { display: false },
tooltip: {
enabled: true,
intersect: false,
callbacks: {
title: items => labels[items[0].dataIndex].full,
label: context => `Base Stat: ${context.raw}`
},
bodyFont: { size: 13 },
titleFont: { size: 12 },
padding: 14,
backgroundColor: 'rgba(28, 28, 34, 0.96)',
borderColor: 'rgba(255, 255, 255, 0.12)',
borderWidth: 1,
cornerRadius: 8,
boxShadow: '0 4px 12px rgba(0,0,0,0.24)'
},
annotation: {
annotations: {
avgLine: {
type: 'line',
borderColor: 'rgba(255, 255, 255, 0.2)',
borderWidth: 1,
borderDash: [4, 4],
scaleID: 'r',
value: stats.reduce((a, b) => a + b, 0) / stats.length
}
}
}
},
onHover: (event, elements) => {
canvas.style.cursor = elements.length ? 'pointer' : 'default';
}
}
});
} catch (error) {
this.showChartError(canvas.parentElement);
}
}
// Utility to convert HEX to RGBA
hexToRgba(hex, alpha = 1) {
const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
return `rgba(${r},${g},${b},${alpha})`;
}
// Abbreviate stat names
getStatAbbreviation(statName) {
const abbreviations = {
hp: 'HP',
attack: 'ATK',
defense: 'DEF',
'special-attack': 'SP.ATK',
'special-defense': 'SP.DEF',
speed: 'SPD'
};
return abbreviations[statName] || statName.slice(0, 3).toUpperCase();
}
// Display error if Chart.js fails
showChartError(container) {
container.innerHTML = `
<div class="chart-error">
<svg class="error-icon" viewBox="0 0 24 24" width="48" height="48">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
<div class="error-message">
<h4>Chart Unavailable</h4>
<p>Failed to load stat visualization</p>
</div>
</div>
`;
}
// TYPE INTERACTIONS SECTION
createTypeRelationsGrid(data) {
const section = document.createElement('section');
section.className = 'type-relations-grid animate-fadeInUp';
section.innerHTML = `<h3 class="section-title">TYPE INTERACTIONS</h3>`;
const grid = document.createElement('div');
grid.style.display = 'grid';
grid.style.gridTemplateColumns =
'repeat(auto-fit, minmax(160px, 1fr))';
grid.style.gap = '12px';
data.types.forEach(type => {
const typeCard = document.createElement('div');
typeCard.className = 'type-card';
typeCard.innerHTML = `
<div class="type-header">${type.type.name.toUpperCase()}</div>
<div class="damage-relations">
<div class="strengths">
<h4>STRONG VS</h4>
<div class="types-list"></div>
</div>
<div class="weaknesses">
<h4>WEAK TO</h4>
<div class="types-list"></div>
</div>
</div>
`;
const typeHeader = typeCard.querySelector('.type-header');
typeHeader.style.background = this.getTypeColor(type.type.name);
fetch(type.type.url)
.then(res => res.json())
.then(typeData => {
const strengths = typeData.damage_relations.double_damage_to;
const weaknesses = typeData.damage_relations.double_damage_from;
strengths.forEach(t => {
const badge = this.createTypeBadge(t.name);
typeCard.querySelector('.strengths .types-list').appendChild(badge);
});
weaknesses.forEach(t => {
const badge = this.createTypeBadge(t.name);
typeCard.querySelector('.weaknesses .types-list').appendChild(badge);
});
});
grid.appendChild(typeCard);
});
section.appendChild(grid);
return section;
}
// Create a small type badge element
createTypeBadge(typeName) {
const badge = document.createElement('span');
badge.className = 'type-badge small';
badge.textContent = typeName.toUpperCase();
badge.style.background = this.getTypeColor(typeName);
badge.style.padding = '2px 8px';
badge.style.borderRadius = '12px';
badge.style.fontSize = '10px';
return badge;
}
// Get a color based on the Pokémon type
getTypeColor(typeName) {
const typeColors = {
normal: '#A8A878',
fire: '#F08030',
water: '#6890F0',
electric: '#F8D030',
grass: '#78C850',
ice: '#98D8D8',
fighting: '#C03028',
poison: '#A040A0',
ground: '#E0C068',
flying: '#A890F0',
psychic: '#F85888',
bug: '#A8B820',
rock: '#B8A038',
ghost: '#705898',
dragon: '#7038F8',
dark: '#705848',
steel: '#B8B8D0',
fairy: '#EE99AC'
};
return typeColors[typeName] || '#68A090';
}
// EVOLUTION CHAIN VISUALIZATION WITH MULTIPLE IMAGE SOURCES & FALLBACK HANDLING
createEvolutionVisualization(chain) {
const section = document.createElement('section');
section.className = 'evolution-chain animate-fadeInUp';
section.innerHTML = `<h3 class="section-title">EVOLUTION LINE</h3>`;
const stages = this.parseEvolutionChain(chain);
const container = document.createElement('div');
container.style.display = 'flex';
container.style.justifyContent = 'center';
container.style.gap = '0px';
container.style.padding = '16px 0';
stages.forEach((stage, index) => {
const stageDiv = document.createElement('div');
stageDiv.style.display = 'flex';
stageDiv.style.flexDirection = 'column';
stageDiv.style.alignItems = 'center';
stageDiv.style.gap = '8px';
if (index > 0) {
const arrow = document.createElement('div');
arrow.textContent = '→';
arrow.style.fontSize = '24px';
arrow.style.opacity = '0.6';
container.appendChild(arrow);
}
const sprite = document.createElement('img');
sprite.className = 'lazy';
sprite.alt = stage.name;
sprite.style.width = '64px';
sprite.style.height = '64px';
// Initially hidden until the image loads
sprite.style.opacity = '0';
// Attach onload event to reveal the image once it loads
sprite.onload = () => {
sprite.style.opacity = '1';
};
// Set multiple image sources and fallback handling
const imageSources = [
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/showdown/${stage.id}.gif`, // Animated
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${stage.id}.png`, // Official
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/${stage.id}.svg`, // SVG
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${stage.id}.png`, // Default
`https://via.placeholder.com/150x150?text=No+Image` // Placeholder
];
let imgIndex = 0;
function loadNextImage() {
if (imgIndex >= imageSources.length) return;
sprite.src = imageSources[imgIndex++];
// If the current image fails to load, try the next one
sprite.onerror = loadNextImage;
}
loadNextImage();
const name = document.createElement('div');
name.textContent = stage.name;
name.style.fontWeight = '500';
stageDiv.append(sprite, name);
container.appendChild(stageDiv);
});
section.appendChild(container);
// If you have a lazy loading observer, you may reinitialize it here:
// this.initIntersectionObserver();
return section;
}
// Recursively parse the evolution chain into an array of stages
parseEvolutionChain(chain, result = []) {
const id = chain.species.url.split('/').slice(-2, -1)[0];
result.push({ name: chain.species.name, id });
if (chain.evolves_to.length > 0) {
chain.evolves_to.forEach(e => this.parseEvolutionChain(e, result));
}
return result.filter((v, i, a) => a.findIndex(t => t.id === v.id) === i);
}
}
new PokeballHelper();
})();