Twitch Poké Ball Helper (Enhanced UI – Browse & Advanced)

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.

当前为 2025-02-26 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();