// ==UserScript==
// @name Twitch Poké Ball Helper (Enhanced UI – Browse & Advanced)
// @namespace http://tampermonkey.net/
// @version 5.17
// @description Twitch Poké Ball Helper with a three-column grid for Catch/Shop plus two distinct lookup tabs: a visually rich Browse tab and a detailed Advanced tab featuring a full-width Pokémon info card with Pokédex entry and evolution chain. All styled with advanced UI techniques and a unified Roboto font.
// @author
// @match https://www.twitch.tv/*
// @icon https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png
// @grant none
// ==/UserScript==
(function () {
'use strict';
class PokeballHelper {
constructor() {
// Define balls for the Catch tab
this.catchBalls = {
check: { command: '!pokecheck', tooltip: 'Poke Check', image: 'https://cdn.discordapp.com/attachments/1095453488684744786/1343838706724896848/5c2d24739a206a1df3d19e60c801c494.png?ex=67bebad2&is=67bd6952&hm=6a86c6c6e6cc0e095accb89a7883ebb0b9c63d894600e4be6d29e0eadca4643b&' },
poke: { command: '!pokecatch pokeball', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png' },
great: { command: '!pokecatch greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png' },
ultra: { command: '!pokecatch ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png' },
master: { command: '!pokecatch masterball', tooltip: 'Master Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/master_ball.png' },
premier: { command: '!pokecatch premierball', tooltip: 'Premier Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/premier_ball.png' },
cherish: { command: '!pokecatch cherishball', tooltip: 'Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cherish_ball.png' },
greatCherish: { command: '!pokecatch greatcherishball', tooltip: 'Great Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_cherish_ball.png' },
ultraCherish: { command: '!pokecatch ultracherishball', tooltip: 'Ultra Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_cherish_ball.png' },
heavy: { command: '!pokecatch heavyball', tooltip: 'Heavy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heavy_ball.png' },
feather: { command: '!pokecatch featherball', tooltip: 'Feather Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/feather_ball.png' },
timer: { command: '!pokecatch timerball', tooltip: 'Timer Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/timer_ball.png' },
quick: { command: '!pokecatch quickball', tooltip: 'Quick Ball', image: 'https://www.shareicon.net/data/512x512/2016/12/13/863562_quick_512x512.png' },
nest: { command: '!pokecatch nestball', tooltip: 'Nest Ball', image: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/nest-ball.png' },
fast: { command: '!pokecatch fastball', tooltip: 'Fast Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fast_ball.png' },
heal: { command: '!pokecatch healball', tooltip: 'Heal Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heal_ball.png' },
repeat: { command: '!pokecatch repeatball', tooltip: 'Repeat Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/repeat_ball.png' },
friend: { command: '!pokecatch friendball', tooltip: 'Friend Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/friend_ball.png' },
frozen: { command: '!pokecatch frozenball', tooltip: 'Frozen Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/frozen_ball.png' },
night: { command: '!pokecatch nightball', tooltip: 'Night Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/night_ball.png' },
phantom: { command: '!pokecatch phantomball', tooltip: 'Phantom Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/phantom_ball.png' },
cipher: { command: '!pokecatch cipherball', tooltip: 'Cipher Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cipher_ball.png' },
magnet: { command: '!pokecatch magnetball', tooltip: 'Magnet Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/magnet_ball.png' },
net: { command: '!pokecatch netball', tooltip: 'Net Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/net_ball.png' },
luxury: { command: '!pokecatch luxuryball', tooltip: 'Luxury Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/luxury_ball.png' },
stone: { command: '!pokecatch stoneball', tooltip: 'Stone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/stone_ball.png' },
level: { command: '!pokecatch levelball', tooltip: 'Level Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/level_ball.png' },
clone: { command: '!pokecatch cloneball', tooltip: 'Clone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/clone_ball.png' },
sun: { command: '!pokecatch sunball', tooltip: 'Sun Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/sun_ball.png' },
fantasy: { command: '!pokecatch fantasyball', tooltip: 'Fantasy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fantasy_ball.png' },
mach: { command: '!pokecatch machball', tooltip: 'Mach Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/mach_ball.png' },
dive: { command: '!pokecatch diveball', tooltip: 'Dive Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/dive_ball.png' }
};
// Define balls for the Shop tab
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' }
};
// Default tab is Catch; preload Pokémon list for Browse tab
this.currentTab = 'catch';
this.pokemonList = null;
// Drag functionality properties
this.isDragging = false;
this.startX = 0;
this.startY = 0;
this.containerStartLeft = 0;
this.containerStartTop = 0;
this.wasDragging = false; // Flag to differentiate click vs drag
// Bind drag methods
this.dragStart = this.dragStart.bind(this);
this.drag = this.drag.bind(this);
this.dragEnd = this.dragEnd.bind(this);
this.init();
}
init() {
this.setupStyles();
this.waitForChat().then(() => {
this.createInterface();
this.addEventListeners();
this.renderGrid();
});
}
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) {
// Record starting positions
this.startX = e.clientX;
this.startY = e.clientY;
this.containerStartLeft = this.container.offsetLeft;
this.containerStartTop = this.container.offsetTop;
this.isDragging = false;
// Remove any transition during drag for instant response
this.container.style.transition = 'none';
// Add mousemove and mouseup listeners on document
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.dragEnd);
}
drag(e) {
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;
// Assuming your chat window has a container with a known selector (e.g., '.chat-window')
const chatWindow = document.querySelector('.chat-window');
if (chatWindow) {
const chatRect = chatWindow.getBoundingClientRect();
const ballRect = this.container.getBoundingClientRect();
// Clamp newX between the chat's left and right boundaries
newX = Math.max(chatRect.left, Math.min(newX, chatRect.right - ballRect.width));
// Clamp newY between the chat's top and bottom boundaries
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;
// Save new position
const left = this.container.offsetLeft;
const top = this.container.offsetTop;
localStorage.setItem('pballPosition', JSON.stringify({ x: left, y: top }));
}
// Optionally restore transition styles if needed
this.container.style.transition = '';
}
setupStyles() {
const style = document.createElement('style');
style.textContent = `
/* Import Roboto font for a modern look */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
:root {
--background-dark: #18181b;
--background-darker: #2e2e35;
--card-background: #1f1f26;
--border-color: #3e3e45;
--highlight-color: #76c7c0;
--highlight-gradient: linear-gradient(90deg, var(--highlight-color), #4db6ac);
--text-light: #ffffff;
--text-muted: #ccc;
--font-family: 'Roboto', sans-serif;
}
/* Global resets & accessibility */
.pball-container, .pball-container * {
font-family: var(--font-family);
box-sizing: border-box;
}
.pball-container {
position: fixed;
right: 12px;
bottom: 95px; /* Positions above chat input */
z-index: 10000;
cursor: move;
user-select: none;
-webkit-user-select: none;
will-change: left, top;
}
/* Main button styling */
.pball-button {
cursor: pointer;
width: 50px;
height: 50px;
border-radius: 50%;
border: 2px solid var(--border-color);
background: var(--background-dark);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.pball-button:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
}
/* Panel with glassmorphic effect and smooth slide-in */
.pball-panel {
position: absolute;
bottom: calc(100% + 10px);
right: 0;
width: 320px;
background: var(--background-dark);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-effect);
opacity: 0;
visibility: hidden;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s;
pointer-events: none;
}
.pball-panel.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
pointer-events: auto;
}
/* Tabs styling */
.pball-tabs {
display: flex;
background: var(--background-darker);
border-bottom: 1px solid var(--border-color);
}
.pball-tab {
flex: 1;
padding: 10px;
text-align: center;
font-size: 16px;
cursor: pointer;
color: var(--text-muted);
transition: background 0.2s ease, color 0.2s ease;
}
.pball-tab.active,
.pball-tab:hover {
background: var(--border-color);
color: var(--text-light);
}
/* Search input area */
.pball-search-container {
position: relative;
margin: 12px;
}
.pball-search {
width: 100%;
padding: 8px 36px 8px 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--background-dark);
color: var(--text-light);
font-size: 15px;
outline: none;
transition: border-color 0.2s ease;
}
.pball-search:focus {
border-color: var(--highlight-color);
}
.pball-search::placeholder {
color: var(--text-muted);
}
.pball-clear-btn {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: var(--text-muted);
font-size: 18px;
cursor: pointer;
display: none;
}
/* Grid layout for content */
.pball-grid {
padding: 12px;
display: grid;
gap: 12px;
max-height: 300px;
overflow-y: auto;
}
.pball-grid.ball-items {
grid-template-columns: repeat(3, 1fr);
}
.pball-grid.search-results {
grid-template-columns: 1fr;
}
.pball-grid::-webkit-scrollbar {
width: 8px;
}
.pball-grid::-webkit-scrollbar-track {
background: var(--background-dark);
border-radius: 8px;
}
.pball-grid::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 8px;
}
.pball-grid::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Catch & Shop items: clean circular icons with transparent background */
.pball-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 50%;
padding: 8px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
}
.pball-item:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.pball-item img {
width: 36px;
height: 36px;
}
.pball-label {
margin-top: 6px;
font-size: 13px;
color: var(--text-light);
text-align: center;
}
/* Browse Tab: Pokémon Tiles */
.browse-tile {
display: flex;
flex-direction: column;
align-items: center;
background: var(--background-darker);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 12px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
}
.browse-tile:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.browse-tile img {
width: 64px;
height: 64px;
margin-bottom: 6px;
}
.tile-label {
font-size: 14px;
font-weight: 500;
color: var(--text-light);
text-transform: capitalize;
}
/* Advanced Tab: Pokémon Info Card */
.poke-card {
width: 100%;
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
color: var(--text-light);
display: flex;
flex-direction: column;
gap: 20px;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.poke-card-header {
display: flex;
align-items: center;
gap: 16px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 12px;
}
.poke-image {
width: 100px;
height: 100px;
border-radius: 12px;
background: var(--background-dark);
object-fit: contain;
}
.poke-title {
font-size: 32px;
font-weight: 700;
margin: 0;
}
.section {
border-top: 1px solid var(--border-color);
padding-top: 12px;
}
.section h3 {
margin: 0 0 8px;
font-size: 20px;
font-weight: 700;
color: var(--text-light);
border-bottom: 1px solid var(--border-color);
padding-bottom: 4px;
}
/* Stats Grid in Advanced Card */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
}
.stat {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 14px;
margin-bottom: 4px;
color: #ddd;
}
.stat-bar {
width: 100%;
background: var(--background-dark);
border: 1px solid var(--border-color);
border-radius: 6px;
height: 18px;
overflow: hidden;
position: relative;
}
.stat-fill {
background: var(--highlight-gradient);
height: 100%;
width: 0;
transition: width 0.5s ease;
border-radius: 6px;
position: relative;
}
.stat-value {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
font-weight: bold;
color: var(--text-light);
}
/* Moves Section */
.moves-section {
max-height: 160px;
overflow-y: auto;
font-size: 15px;
color: var(--text-muted);
padding-right: 6px;
}
.moves-section ul {
list-style: none;
padding: 0;
margin: 0;
}
.moves-section li {
margin-bottom: 4px;
}
/* Type Damage Relations Section */
.type-relations {
display: flex;
flex-direction: column;
gap: 8px;
}
.type-box {
background: var(--background-dark);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 6px;
font-size: 13px;
color: var(--text-light);
transition: transform 0.2s ease;
}
.type-box:hover {
transform: scale(1.02);
}
.type-box strong {
display: block;
margin-bottom: 4px;
font-size: 14px;
}
// Add to the CSS section:
.pball-item img.dragging {
opacity: 0.5;
transform: scale(0.8);
transition: all 0.2s ease;
filter: drop-shadow(0 0 4px rgba(118, 199, 192, 0.5));
}
/* Spinner */
.spinner {
margin: 24px auto;
border: 4px solid var(--border-color);
border-top: 4px solid var(--highlight-color);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Media Query for smaller screens */
@media (max-width: 400px) {
.pball-panel {
width: 280px;
}
.poke-title {
font-size: 26px;
}
.pball-search {
font-size: 14px;
}
}
`;
document.head.appendChild(style);
}
async waitForChat() {
return new Promise((resolve) => {
if (document.querySelector('[data-test-selector="chat-input"]')) {
return resolve();
}
const observer = new MutationObserver(() => {
if (document.querySelector('[data-test-selector="chat-input"]')) {
observer.disconnect();
resolve();
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
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);
}
createMainButton() {
const button = document.createElement('img');
button.className = 'pball-button';
button.src = this.catchBalls.poke.image;
return button;
}
createPanel() {
const panel = document.createElement('div');
panel.className = 'pball-panel';
const tabsContainer = document.createElement('div');
tabsContainer.className = 'pball-tabs';
// Tab: Catch
const catchTab = document.createElement('div');
catchTab.className = 'pball-tab active';
catchTab.textContent = 'Catch';
catchTab.dataset.tab = 'catch';
tabsContainer.appendChild(catchTab);
// Tab: Shop
const shopTab = document.createElement('div');
shopTab.className = 'pball-tab';
shopTab.textContent = 'Shop';
shopTab.dataset.tab = 'shop';
tabsContainer.appendChild(shopTab);
// Tab: Browse
const browseTab = document.createElement('div');
browseTab.className = 'pball-tab';
browseTab.textContent = 'Browse';
browseTab.dataset.tab = 'browse';
tabsContainer.appendChild(browseTab);
// Tab: Advanced
const advancedTab = document.createElement('div');
advancedTab.className = 'pball-tab';
advancedTab.textContent = 'Advanced';
advancedTab.dataset.tab = 'advanced';
tabsContainer.appendChild(advancedTab);
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.className = 'pball-clear-btn';
this.clearBtn.textContent = '×';
this.clearBtn.setAttribute('aria-label', 'Clear Search');
searchContainer.append(this.searchInput, this.clearBtn);
this.gridContainer = document.createElement('div');
this.gridContainer.className = 'pball-grid';
panel.append(tabsContainer, searchContainer, this.gridContainer);
return panel;
}
renderGrid() {
if (this.currentTab === 'advanced') {
this.gridContainer.classList.remove('ball-items');
this.gridContainer.classList.add('search-results');
this.renderAdvancedInstruction();
} else if (this.currentTab === 'browse') {
this.gridContainer.classList.remove('ball-items');
this.gridContainer.classList.add('search-results');
this.renderBrowse();
} else {
this.gridContainer.classList.remove('search-results');
this.gridContainer.classList.add('ball-items');
this.gridContainer.innerHTML = '';
const balls = this.currentTab === 'catch' ? this.catchBalls : this.shopBalls;
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);
this.gridContainer.appendChild(item);
});
this.filterGrid();
}
}
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=151')
.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 = '';
const query = this.searchInput.value.trim().toLowerCase();
const filtered = this.pokemonList.filter(poke => poke.name.includes(query));
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.appendChild(img);
tile.appendChild(label);
// Clicking a tile switches to Advanced tab and loads details.
tile.addEventListener('click', (e) => {
e.stopPropagation();
this.panel.classList.add('active');
this.changeTab('advanced');
this.searchInput.value = poke.name;
this.searchAdvancedPokemon(poke.name);
});
this.gridContainer.appendChild(tile);
});
if (filtered.length === 0) {
this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">No Pokémon match your search.</div>`;
}
}
addEventListeners() {
// Start drag on mousedown for the main button
this.button.addEventListener('mousedown', this.dragStart);
// Modified click event: only toggle panel if not dragging
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');
}
});
// In the addEventListeners method, update the dragstart handler:
this.panel.addEventListener('dragstart', (e) => {
const ballImg = e.target.closest('.pball-item img');
if (ballImg) {
e.dataTransfer.setData('text/plain', ballImg.dataset.ballType);
// Create a temporary drag image
const dragImg = new Image();
dragImg.src = ballImg.src;
dragImg.style.width = '36px';
dragImg.style.height = '36px';
// Position off-screen to render
dragImg.style.position = 'absolute';
dragImg.style.left = '-9999px';
document.body.appendChild(dragImg);
// Set custom drag image centered under cursor
e.dataTransfer.setDragImage(dragImg, 18, 18);
// Cleanup after drag starts
setTimeout(() => document.body.removeChild(dragImg), 0);
// Add visual feedback to original image
ballImg.classList.add('dragging');
// Remove class on drag end
const onDragEnd = () => {
ballImg.classList.remove('dragging');
document.removeEventListener('dragend', onDragEnd);
};
document.addEventListener('dragend', onDragEnd);
}
});
const chatInput = this.getChatInput();
if (chatInput) {
chatInput.addEventListener('dragover', (e) => e.preventDefault());
chatInput.addEventListener('drop', (e) => {
e.preventDefault();
const ballType = e.dataTransfer.getData('text/plain');
if (ballType) {
this.selectAllAndReplace(chatInput, ballType);
}
});
}
const tabs = this.panel.querySelectorAll('.pball-tab');
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
e.stopPropagation();
this.changeTab(tab.dataset.tab);
});
});
this.searchInput.addEventListener('input', () => {
if (this.currentTab !== 'advanced') {
this.filterGrid();
if (this.currentTab === 'browse') {
this.renderBrowseGrid();
}
}
this.clearBtn.style.display = this.searchInput.value.trim() ? 'block' : 'none';
});
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());
}
});
}
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 === 'browse') {
this.searchInput.placeholder = 'Filter Pokémon...';
} 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 = ''; // Reset the input field to empty
this.triggerInputEvent(chatInput); // Ensure Twitch detects the reset
}
}
insertText(text) {
document.execCommand('insertText', false, text);
}
triggerInputEvent(element) {
element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
}
// Advanced Lookup Methods
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>`;
});
}
displayAdvancedPokemonData(data, speciesData, evoData) {
this.gridContainer.innerHTML = '';
const card = document.createElement('div');
card.className = 'poke-card';
// Build card sections (back button removed)
card.appendChild(this.createCardHeader(data));
card.appendChild(this.createBasicInfoSection(data));
card.appendChild(this.createAbilitiesSection(data));
card.appendChild(this.createStatsSection(data));
card.appendChild(this.createTypesSection(data));
card.appendChild(this.createDamageRelationsSection(data));
card.appendChild(this.createMovesSection(data));
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));
}
// Advanced sections: Pokédex Entry and Evolution Chain
card.appendChild(this.createPokedexEntrySection(speciesData));
card.appendChild(this.createEvolutionChainSection(evoData));
this.gridContainer.appendChild(card);
}
// Modular Pokémon Card Sections
createCardHeader(data) {
const header = document.createElement('header');
header.className = 'poke-card-header';
const img = document.createElement('img');
img.className = 'poke-image';
img.src = (data.sprites.other && data.sprites.other['official-artwork'] &&
data.sprites.other['official-artwork'].front_default)
|| data.sprites.front_default || '';
header.appendChild(img);
const title = document.createElement('h2');
title.className = 'poke-title';
title.textContent = `${data.name.charAt(0).toUpperCase() + data.name.slice(1)} (ID: ${data.id})`;
header.appendChild(title);
return header;
}
createBasicInfoSection(data) {
const section = document.createElement('div');
section.className = 'section';
const totalStats = data.stats.reduce((sum, stat) => sum + stat.base_stat, 0);
section.innerHTML = `
<h3>Basic Info</h3>
<p><strong>Total Stats:</strong> ${totalStats}</p>
<p><strong>Height:</strong> ${data.height}</p>
<p><strong>Weight:</strong> ${data.weight}</p>
<p><strong>Base Exp:</strong> ${data.base_experience}</p>
<p><strong>Species:</strong> ${data.species.name}</p>
`;
return section;
}
createAbilitiesSection(data) {
const section = document.createElement('div');
section.className = 'section';
section.innerHTML = `<h3>Abilities</h3>`;
const ul = document.createElement('ul');
data.abilities.forEach(a => {
const li = document.createElement('li');
li.textContent = `${a.ability.name}${a.is_hidden ? ' (Hidden)' : ''}`;
ul.appendChild(li);
});
section.appendChild(ul);
return section;
}
createStatsSection(data) {
const section = document.createElement('div');
section.className = 'section';
section.innerHTML = `<h3>Stats</h3>`;
const statsGrid = document.createElement('div');
statsGrid.className = 'stats-grid';
data.stats.forEach(stat => {
const statDiv = document.createElement('div');
statDiv.className = 'stat';
const label = document.createElement('div');
label.className = 'stat-label';
label.textContent = `${stat.stat.name.toUpperCase()}: ${stat.base_stat}`;
statDiv.appendChild(label);
const bar = document.createElement('div');
bar.className = 'stat-bar';
const fill = document.createElement('div');
fill.className = 'stat-fill';
const percentage = Math.min(100, (stat.base_stat / 255) * 100);
fill.style.width = `${percentage}%`;
const statValue = document.createElement('span');
statValue.className = 'stat-value';
statValue.textContent = stat.base_stat;
fill.appendChild(statValue);
bar.appendChild(fill);
statDiv.appendChild(bar);
statsGrid.appendChild(statDiv);
});
section.appendChild(statsGrid);
return section;
}
createTypesSection(data) {
const section = document.createElement('div');
section.className = 'section';
section.innerHTML = `<h3>Types</h3>`;
const ul = document.createElement('ul');
data.types.forEach(typeInfo => {
const li = document.createElement('li');
li.textContent = typeInfo.type.name;
ul.appendChild(li);
});
section.appendChild(ul);
return section;
}
createDamageRelationsSection(data) {
const section = document.createElement('div');
section.className = 'section';
section.innerHTML = `<h3>Type Damage Relations</h3>`;
const container = document.createElement('div');
container.className = 'type-relations';
data.types.forEach(typeInfo => {
const typeBox = document.createElement('div');
typeBox.className = 'type-box';
typeBox.innerHTML = `<strong>${typeInfo.type.name.toUpperCase()}</strong>`;
fetch(typeInfo.type.url)
.then(res => res.json())
.then(typeData => {
const strengths = typeData.damage_relations.double_damage_to.map(d => d.name).join(', ') || "None";
const weaknesses = typeData.damage_relations.double_damage_from.map(d => d.name).join(', ') || "None";
const details = document.createElement('div');
details.innerHTML = `<p><strong>Strengths:</strong> ${strengths}</p><p><strong>Weaknesses:</strong> ${weaknesses}</p>`;
typeBox.appendChild(details);
})
.catch(() => {
const errMsg = document.createElement('div');
errMsg.textContent = "Error loading type data";
typeBox.appendChild(errMsg);
});
container.appendChild(typeBox);
});
section.appendChild(container);
return section;
}
createMovesSection(data) {
const section = document.createElement('div');
section.className = 'section moves-section';
section.innerHTML = `<h3>Moves</h3>`;
const ul = document.createElement('ul');
data.moves.forEach(moveInfo => {
const li = document.createElement('li');
li.textContent = moveInfo.move.name;
ul.appendChild(li);
});
section.appendChild(ul);
return section;
}
createHeldItemsSection(data) {
const section = document.createElement('div');
section.className = 'section';
section.innerHTML = `<h3>Held Items</h3>`;
const ul = document.createElement('ul');
data.held_items.forEach(itemInfo => {
const li = document.createElement('li');
li.textContent = itemInfo.item.name;
ul.appendChild(li);
});
section.appendChild(ul);
return section;
}
createFormsSection(data) {
const section = document.createElement('div');
section.className = 'section';
section.innerHTML = `<h3>Forms</h3>`;
const ul = document.createElement('ul');
data.forms.forEach(form => {
const li = document.createElement('li');
li.textContent = form.name;
ul.appendChild(li);
});
section.appendChild(ul);
return section;
}
// Advanced Sections
createPokedexEntrySection(speciesData) {
const section = document.createElement('div');
section.className = 'section';
section.innerHTML = `<h3>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 para = document.createElement('p');
para.textContent = flavorText;
section.appendChild(para);
return section;
}
createEvolutionChainSection(evoData) {
const section = document.createElement('div');
section.className = 'section';
section.innerHTML = `<h3>Evolution Chain</h3>`;
const chainText = this.getEvolutionChain(evoData.chain);
const para = document.createElement('p');
para.textContent = chainText;
section.appendChild(para);
return section;
}
getEvolutionChain(chain) {
let result = chain.species.name;
if (chain.evolves_to && chain.evolves_to.length > 0) {
result += " → " + chain.evolves_to.map(subChain => this.getEvolutionChain(subChain)).join(" / ");
}
return result;
}
}
new PokeballHelper();
})();