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. Shoutout doubleupmafia @doubleupmolly @doubleuplowlow219 @doubleupap @doubleupeazy @musiclov3r1435

目前为 2025-03-01 提交的版本。查看 最新版本

// ==UserScript==
// @name         Twitch Poké Ball Helper (Enhanced UI – Browse & Advanced)
// @namespace    http://tampermonkey.net/
// @version      11
// @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. Shoutout doubleupmafia @doubleupmolly @doubleuplowlow219 @doubleupap @doubleupeazy @musiclov3r1435
// @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() {
      this.catchBalls = {
        dollars: { command: '$', tooltip: 'Poke Dollars', image: 'https://i.postimg.cc/T20dR1qH/f547e065261b657c49d5702826b0deca.png' },
        check: { command: '!pokecheck', tooltip: 'Poke Check', image: 'https://i.postimg.cc/0N7vhyyn/ea9752334aa08543e2f148c0a903719e.png' },
        poke: { command: '!pokecatch', 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://poketwitch.bframework.de/static/twitchextension/items/ball/quick_ball.png' },
        nest: { command: '!pokecatch nestball', tooltip: 'Nest Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/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' }
      };

      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' }
      };

      this.currentTab = 'catch';
      this.allPokemonList = null;

      this.isDragging = false;
      this.startX = 0;
      this.startY = 0;
      this.containerStartLeft = 0;
      this.containerStartTop = 0;
      this.wasDragging = false;

      this.dragStart = this.dragStart.bind(this);
      this.drag = this.drag.bind(this);
      this.dragEnd = this.dragEnd.bind(this);

      this.init();
    }

// Add this line in the init method to initialize search buttons
init() {
  this.setupStyles();
  this.waitForChat().then(() => {
    this.createInterface();
    this.createTimerElement();
    this.addEventListeners();
    this.renderGrid();
    this.initSearchButtons(); // Initialize search buttons
    this.updateSpawnTimer();
  });
}

handleSearch(query) {
  switch(this.currentTab) {
    case 'advanced':
      this.searchAdvancedPokemon(query);
      break;
    case 'browse':
      this.renderBrowseGrid();
      break;
    default:
      this.filterGrid();
  }
}

// Update the initSearchButtons method
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';

    // Enter button
    const enterButton = Object.assign(document.createElement('button'), {
      className: 'search-enter-button',
      innerHTML: '✔',
      title: 'Search (Enter)',
      onclick: () => this.handleSearch(input.value.trim())
    });

    // Clear button (persistent)
    const clearButton = Object.assign(document.createElement('button'), {
      className: 'pball-clear-btn',
      innerHTML: '×',
      title: 'Clear search',
      style: 'display: none;', // Start hidden
      onclick: () => {
        input.value = '';
        input.focus();
        this.handleSearch('');
        clearButton.style.display = 'none';
      }
    });

    // Input events
    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;
  });
}

    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 = `
    /* Theme Variables */
/*--------------------------------------------------
  Revamped UI CSS
--------------------------------------------------*/

/*--------------------------------------------------
  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 Palette */
  --color-primary: #32cd32;
  --color-secondary: #4db6ac;
  --color-accent: #ffe135;
  --color-danger: #ff4040;
  --color-dark: #18181b;
  --color-darker: #2e2e35;
  --color-card: #1f1f26;
  --color-border: #3e3e45;
  --color-glass: rgba(255, 255, 255, 0.1);
  --gradient-accent: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
  --gradient-bg: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);

  /* Typography */
  --font-base: 'Roboto', sans-serif;
  --font-led: 'Segment7Standard', monospace;
  --font-label: 'Press Start 2P', cursive;

  /* UI Sizing */
  --border-radius-small: 4px;
  --border-radius-medium: 12px;
  --border-radius-large: 16px;

  /* Effects */
  --transition-fast: 0.2s ease;
  --transition-medium: 0.3s ease;
  --box-shadow-light: 0 2px 8px rgba(0, 0, 0, 0.3);
  --box-shadow-heavy: 0 4px 16px rgba(0, 0, 0, 0.5);
  --backdrop-blur: blur(10px);
}

/* Global Reset & Base */
*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: var(--font-base);
}

/* Custom Scrollbar */
::-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: #555;
}

/*--------------------------------------------------
  Main Container & Interactive Elements
--------------------------------------------------*/
.pball-container {
  position: fixed;
  bottom: 90px;
  right: 20px;
  z-index: 10000;
  pointer-events: none;
  transform: scale(1);
  transform-origin: top right;
  width: fit-content;
  height: fit-content;
}

/* Enable interactions for children */
.pball-container > * {
  pointer-events: auto;
}

/* Main Action Button */
.pball-button {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: var(--gradient-accent);
  border: 2px solid var(--color-border);
  box-shadow: var(--box-shadow-light);
  cursor: pointer;
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
  display: flex;
  align-items: center;
  justify-content: center;
}
.pball-button:hover {
  transform: scale(1.2);
  box-shadow: var(--box-shadow-heavy);
}

/*--------------------------------------------------
  Floating Panel with Glassmorphism
--------------------------------------------------*/
.pball-panel {
  position: absolute;
  bottom: calc(100% + 12px);
  right: 0;
  width: 340px;
  background: rgba(30, 30, 35, 0.8);
  backdrop-filter: var(--backdrop-blur);
  border-radius: var(--border-radius-large);
  border: 1px solid var(--color-glass);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  opacity: 0;
  visibility: hidden;
  transform: translateY(20px);
  transition: opacity var(--transition-medium), transform var(--transition-medium), visibility var(--transition-medium);
}
.pball-panel.active {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

/*--------------------------------------------------
  Tab Navigation
--------------------------------------------------*/
.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 {
  flex: 1;
  padding: 12px;
  text-align: center;
  font-size: 15px;
  cursor: pointer;
  color: var(--color-border);
  transition: background var(--transition-fast), color var(--transition-fast);
}
.pball-tab:hover,
.pball-tab.active {
  background: var(--gradient-accent);
  color: var(--color-dark);
}

/*--------------------------------------------------
  Search Component
--------------------------------------------------*/
.pball-search-container {
  position: relative;
  margin: 16px;
  background: var(--color-card);
  border-radius: var(--border-radius-medium);
  border: 1px solid var(--color-border);
  overflow: hidden;
}
.pball-search {
  width: 100%;
  padding: 10px 14px;
  padding-right: 70px;
  border: none;
  background: transparent;
  color: var(--color-border);
  font-size: 15px;
  outline: none;
}
.search-buttons {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  gap: 6px;
}
.search-enter-button,
.pball-clear-btn {
  width: 30px;
  height: 30px;
  border: none;
  border-radius: var(--border-radius-small);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--transition-fast);
  cursor: pointer;
}
.search-enter-button {
  background: var(--color-primary);
  color: #000;
}
.pball-clear-btn {
  background: var(--color-danger);
  color: #fff;
  display: none;
}
.pball-search:not(:placeholder-shown) ~ .search-buttons .pball-clear-btn {
  display: flex;
}

/*--------------------------------------------------
  Grid Layouts & Item Cards
--------------------------------------------------*/
.pball-grid {
  padding: 16px;
  display: grid;
  gap: 16px;
  max-height: 320px;
  overflow-y: auto;
}
.pball-grid.ball-items {
  grid-template-columns: repeat(3, 1fr);
}
.pball-grid.search-results {
  grid-template-columns: 1fr;
}

/* Item Card */
.pball-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: 50%;
  padding: 10px;
  transition: transform var(--transition-fast);
}
.pball-item img {
  width: 40px;
  height: 40px;
  transition: transform var(--transition-fast);
  cursor: grab;
}
.pball-item img:hover {
  transform: scale(1.2);
}
.pball-item img.dragging {
  opacity: 0.7;
  transform: scale(0.9);
  filter: drop-shadow(0 0 6px var(--color-primary));
}
.pball-item .pball-label {
  margin-top: 8px;
  font-size: 14px;
  color: var(--color-border);
  text-align: center;
}

/*--------------------------------------------------
  Browse Tiles & Advanced Cards
--------------------------------------------------*/
.browse-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 16px;
  padding: 20px;
}
.browse-tile {
  background: var(--color-darker);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius-medium);
  padding: 16px;
  text-align: center;
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
  cursor: pointer;
}
.browse-tile:hover {
  transform: translateY(-4px);
  box-shadow: var(--box-shadow-light);
}
.browse-tile img {
  width: 72px;
  height: 72px;
  margin-bottom: 8px;
}
.browse-tile .tile-label {
  font-size: 15px;
  font-weight: 600;
  color: var(--color-border);
  text-transform: capitalize;
}

/* Advanced Card Components */
.poke-card {
  padding: 20px;
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
  background: var(--color-card);
  border-radius: var(--border-radius-large);
  box-shadow: var(--box-shadow-light);
}
.poke-card .section-title {
  font-size: 22px;
  font-weight: 700;
  color: var(--color-border);
  margin-bottom: 12px;
}
.poke-card .stats-grid {
  font-size: 22px;
  font-weight: 700;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 16px;
}

/* Evolution Chain */
.evolution-chain {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
  padding: 12px;
}
.evolution-chain .evolution-item {
  background: var(--color-darker);
  border-radius: var(--border-radius-small);
  padding: 8px;
  text-align: center;
  transition: transform var(--transition-fast);
}
.evolution-chain .evolution-item:hover {
  transform: translateY(-2px);
}
.evolution-chain .evolution-item img {
  width: 68px;
  height: 68px;
  margin-bottom: 6px;
}
.evolution-chain .evolution-item p {
  font-size: 13px;
  color: var(--color-border);
  margin: 0;
}

/* Moves Section */
.moves-section {
  max-height: 350px;
  overflow-y: auto;
  padding: 12px;
}

/*--------------------------------------------------
  Timer Container & Components
--------------------------------------------------*/
.spawn-timer {
  position: absolute;
  bottom: calc(100% - 5.8rem);
  right: 6.5rem;
  padding: 1rem 1.2rem;
  backdrop-filter: var(--backdrop-blur);
  border-radius: var(--border-radius-large);
  box-shadow: var(--box-shadow-light);
  display: flex;
  align-items: center;
  gap: 1rem;
  transform: perspective(500px) rotateX(5deg);
  transition: transform var(--transition-medium);
  z-index: 10001;
}
.spawn-timer:hover {
  transform: perspective(500px) rotateX(0deg);
}
.timer-header {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
}
.timer-label {
  font-family: var(--font-label);
  font-size: 0.8rem;
  color: var(--color-accent);
  text-transform: uppercase;
  letter-spacing: 0.1rem;
  padding: 0.4rem 0.8rem;
  border-radius: var(--border-radius-small);
  border: 1px solid rgba(58, 90, 109, 0.4);
  background: rgba(255, 255, 255, 0.05);
  text-shadow: 0 0.1rem 0.2rem rgba(0, 0, 0, 0.4);
  transition: background var(--transition-medium);
}
.timer-label:hover {
  background: rgba(255, 255, 255, 0.15);
}
.countdown-display {
  font-family: var(--font-led);
  font-size: 2.2rem;
  font-weight: bold;
  color: var(--color-primary);
  padding: 0.5rem 0.9rem;
  border-radius: var(--border-radius-small);
  letter-spacing: 0.15rem;
  min-width: 7.5rem;
  text-align: center;
  background: linear-gradient(135deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.05));
  box-shadow: 0 0.4rem 0.8rem rgba(0, 255, 55, 0.4),
              inset 0 0.3rem 0.6rem rgba(0, 255, 55, 0.3);
  transition: transform var(--transition-medium), box-shadow var(--transition-medium);
  position: relative;
}
.countdown-display:hover {
  transform: scale(1.05);
  box-shadow: 0 0.6rem 1.2rem rgba(0, 255, 55, 0.6),
              inset 0 0.4rem 0.8rem rgba(0, 255, 55, 0.4);
}
.countdown-display::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle, rgba(0, 255, 55, 0.2), transparent 70%);
  border-radius: inherit;
  z-index: -1;
  transition: opacity var(--transition-medium);
}
.countdown-display.low-time {
  color: var(--color-danger);
  text-shadow: 0 0.5rem 0.75rem rgba(255, 64, 64, 0.8),
               0 0.3rem 0.5rem rgba(255, 64, 64, 0.9);
  animation: led-pulse 0.6s ease-in-out infinite, emergency-glow 1.2s ease-in-out infinite;
}
@keyframes led-pulse {
  0%, 100% { text-shadow: 0 0.5rem 0.75rem rgba(255, 64, 64, 0.8); }
  50% { text-shadow: 0 0.8rem 1rem rgba(255, 64, 64, 1); }
}
@keyframes emergency-glow {
  0%, 100% { box-shadow: 0 0.4rem 0.8rem rgba(255, 64, 64, 0.3); }
  50% { box-shadow: 0 0.8rem 1.6rem rgba(255, 64, 64, 0.6); }
}

/*--------------------------------------------------
  Spinner Utility
--------------------------------------------------*/
.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;
}
@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/*--------------------------------------------------
  Responsive Adjustments
--------------------------------------------------*/
@media (max-width: 768px) {
  .poke-card {
    padding: 16px;
    gap: 16px;
  }
  .section-title {
    font-size: 18px;
  }
  .stats-radar-chart canvas {
    max-width: 100%;
  }
}
@media (max-width: 600px) {
  .spawn-timer {
    gap: 0.75rem;
    padding: 0.75rem 1rem;
  }
  .timer-label {
    font-size: 0.65rem;
    padding: 0.3rem 0.6rem;
  }
  .countdown-display {
    font-size: 1.8rem;
    min-width: 4.5rem;
  }
}

  `;
  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.draggable = false;
      button.className = 'pball-button';
      button.src = this.catchBalls.poke.image;
      return button;
    }

    createPanel() {
      const panel = document.createElement('div');
      panel.className = 'pball-panel';
      panel.draggable = false;

      const tabsContainer = document.createElement('div');
      tabsContainer.className = 'pball-tabs';

      const catchTab = document.createElement('div');
      catchTab.className = 'pball-tab active';
      catchTab.textContent = 'Catch';
      catchTab.dataset.tab = 'catch';
      tabsContainer.appendChild(catchTab);

      const shopTab = document.createElement('div');
      shopTab.className = 'pball-tab';
      shopTab.textContent = 'Shop';
      shopTab.dataset.tab = 'shop';
      tabsContainer.appendChild(shopTab);

      const browseTab = document.createElement('div');
      browseTab.className = 'pball-tab';
      browseTab.textContent = 'Browse';
      browseTab.dataset.tab = 'browse';
      tabsContainer.appendChild(browseTab);

      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.gridContainer = document.createElement('div');
      this.gridContainer.className = 'pball-grid';

      panel.append(tabsContainer, searchContainer, this.gridContainer);
      return panel;
    }

createTimerElement() {
  this.timerContainer = document.createElement('div');
  this.timerContainer.className = 'spawn-timer';
  this.timerContainer.innerHTML = `
    <div class="timer-header">
      <div class="countdown-display">88:88</div>
    </div>
  `;
  this.container.appendChild(this.timerContainer);
  this.initTimer();
}

initTimer() {
  this.backendUrl = 'https://poketwitch.bframework.de/info/events/last_spawn/';
  this.countdownElement = this.timerContainer.querySelector('.countdown-display');
  this.remainingSeconds = 0;
  this.tickInterval = null;
  this.isFetching = false;

  // Start countdown mechanism
  this.startTick();

  // Fetch the initial timer value
  this.fetchTimer();

  // Allow manual refresh on click if not already fetching
  this.timerContainer.addEventListener('click', () => {
    if (!this.isFetching) {
      this.fetchTimer();
    }
  });
}

startTick() {
  if (this.tickInterval) clearInterval(this.tickInterval);

  this.tickInterval = setInterval(() => {
    if (this.remainingSeconds > 0) {
      this.remainingSeconds--;
      this.updateDisplay(this.remainingSeconds);
    } else {
      this.updateDisplay(0);
      this.fetchTimer(); // Fetch API only when timer hits 00:00
    }
  }, 1000);
}

updateDisplay(seconds) {
  if (isNaN(seconds) || seconds < 0) {
    this.countdownElement.textContent = '--:--';
    this.countdownElement.classList.remove('low-time');
    return;
  }
  const mins = Math.floor(seconds / 60);
  const secs = seconds % 60;
  this.countdownElement.textContent =
    `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;

  this.countdownElement.classList.toggle('low-time', seconds <= 30);
}

fetchTimer() {
  if (this.isFetching) return;
  this.isFetching = true;

  fetch(this.backendUrl)
    .then(response => {
      if (!response.ok) throw new Error('Network error');
      return response.json();
    })
    .then(data => {
      const newTime = parseInt(data.next_spawn, 10);
      if (isNaN(newTime)) throw new Error('Invalid timer data');

      // Reset countdown
      this.remainingSeconds = newTime;
      this.updateDisplay(this.remainingSeconds);
    })
    .catch(error => {
      console.error('Timer error:', error);
      this.remainingSeconds = -1;
      this.updateDisplay(NaN);
    })
    .finally(() => {
      this.isFetching = false;
    });
}


    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=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));

      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);
        });

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

      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 = '';
        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>`;
        });
    }

displayAdvancedPokemonData(data, speciesData, evoData) {
  this.gridContainer.innerHTML = '';
  const card = document.createElement('div');
  card.className = 'poke-card';

  // Constrain the card width to fit within the pop-up
  card.style.maxWidth = '100%'; // Ensure it doesn't exceed the pop-up width
  card.style.overflowX = 'hidden'; // Prevent horizontal scrolling

  // Header Section
  const header = this.createCardHeader(data);
  card.appendChild(header);

  // Single Column Layout for Advanced Tab
  card.style.display = 'flex';
  card.style.flexDirection = 'column';
  card.style.gap = '16px'; // Reduced gap for compact layout

  // Add sections in a single column
  card.append(
    this.createBasicInfoSection(data),
    this.createStatsRadarChart(data),
    this.createAbilitiesSection(data),
    this.createTypeRelationsGrid(data),
    this.createPokedexEntrySection(speciesData),
    this.createEvolutionVisualization(evoData.chain),
    this.createMovesSection(data)
  );

  // Add Held Items and Forms sections if they exist
  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);
}

createCardHeader(data) {
  const header = document.createElement('header');
  header.className = 'poke-card-header';
  header.style.display = 'grid';
  header.style.gridTemplateColumns = 'auto 1fr';
  header.style.gap = '24px';
  header.style.alignItems = 'center';
  header.style.marginBottom = '24px';

  // Image with Badge
  const imgContainer = document.createElement('div');
  imgContainer.style.position = 'relative';
  const img = document.createElement('img');
  img.className = 'poke-image';
  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.src = data.sprites.other?.['official-artwork']?.front_default || data.sprites.front_default;

  // Type Badges
  const typeBadges = document.createElement('div');
  typeBadges.style.display = 'flex';
  typeBadges.style.gap = '4px';
  typeBadges.style.position = 'absolute';
  typeBadges.style.bottom = '-23px';
  typeBadges.style.left = '55%';
  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 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;
}

createPokedexEntrySection(speciesData) {
  const section = document.createElement('div');
  section.className = 'pokedex-entry-section';
  section.innerHTML = `<h3 class="section-title">POKÉDEX ENTRY</h3>`;

  // Find the English flavor text
  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.';

  // Create the entry container
  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;
}

createBasicInfoSection(data) {
  const section = document.createElement('div');
  section.className = 'info-grid';
  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;
}


createAbilitiesSection(data) {
  const section = document.createElement('div');
  section.className = 'abilities-section';
  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';

    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);

    // Add tooltip for ability description
    abilityCard.addEventListener('mouseenter', () => {
      fetch(ability.ability.url)
        .then(res => res.json())
        .then(abilityData => {
          const description = abilityData.effect_entries.find(e => e.language.name === 'en')?.effect || 'No description available.';
          this.showTooltip(abilityCard, description);
        });
    });

    abilityCard.addEventListener('mouseleave', () => {
      this.hideTooltip();
    });
  });

  section.appendChild(abilitiesGrid);
  return section;
}

showTooltip(element, text) {
  if (this.tooltip) this.tooltip.remove();

  this.tooltip = document.createElement('div');
  this.tooltip.className = 'tooltip';
  this.tooltip.textContent = text;

  // Calculate position with viewport boundary checks
  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(--background-dark)',
    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'
  });

  document.body.appendChild(this.tooltip);
}

hideTooltip() {
  if (this.tooltip) {
    this.tooltip.remove();
    this.tooltip = null;
  }
}

createMovesSection(data) {
  const section = document.createElement('div');
  section.className = 'moves-section';
  section.innerHTML = `<h3 class="section-title">MOVES</h3>`;

  // Search container with unified styling
  const searchContainer = document.createElement('div');
  searchContainer.className = 'pball-search-container';

  const searchInput = document.createElement('input');
  searchInput.className = 'pball-search';
  searchInput.placeholder = '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';

  // Event handling with debounce
  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();
  });

  // Assembly
  searchButtons.append(enterButton, clearButton);
  searchContainer.append(searchInput, searchButtons);
  section.append(searchContainer);

  // Moves list with virtual scroll
  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';
    movesList.appendChild(moveItem);
  });

  searchInput.addEventListener('input', () => {
    const query = searchInput.value.trim().toLowerCase();
    Array.from(movesList.children).forEach(move => {
      move.style.display = move.textContent.toLowerCase().includes(query) ? 'block' : 'none';
    });
  });

  section.append(searchInput, movesList);
  return section;
}

createHeldItemsSection(data) {
  const section = document.createElement('div');
  section.className = 'held-items-section';
  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;
}

createFormsSection(data) {
  const section = document.createElement('div');
  section.className = 'forms-section';
  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;
}




createStatsRadarChart(data) {
  const section = document.createElement('div');
  section.className = 'stats-radar-card';
  section.innerHTML = `
    <div class="stats-header">
      <h3 class="section-title">Stat Distribution</h3>
      <div class="stats-summary">
        <span class="total-stats">Total: ${data.stats.reduce((sum, s) => sum + s.base_stat, 0)}</span>
        <div class="type-badge" style="background: ${this.getTypeColor(data.types[0].type.name)}">
          ${data.types[0].type.name.toUpperCase()}
        </div>
      </div>
    </div>
  `;

  // Chart container with aspect ratio constraints
  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';

  // Dynamic gradient based on Pokémon type
  const typeColor = this.getTypeColor(data.types[0].type.name);
  const gradient = {
    light: this.hexToRgba(typeColor, 0.3),
    dark: this.hexToRgba(typeColor, 0.1)
  };

  // Chart.js loader with error handling
  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)
    }));

    // Create gradient fill
    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);
  }
}

// Helper methods
hexToRgba(hex, alpha = 1) {
  const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
  return `rgba(${r},${g},${b},${alpha})`;
}

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

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>
  `;
}

createTypeRelationsGrid(data) {
  const section = document.createElement('div');
  section.className = 'type-relations-grid';
  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>
    `;

          // Add this block to target the header specifically
    const typeHeader = typeCard.querySelector('.type-header');
    typeHeader.style.background = this.getTypeColor(type.type.name);

    // Async load damage relations
    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;
}

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;
}

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';
}

createEvolutionVisualization(chain) {
  const section = document.createElement('div');
  section.className = 'evolution-chain';
  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.src = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${stage.id}.png`;
    sprite.style.width = '64px';
    sprite.style.height = '64px';

    const name = document.createElement('div');
    name.textContent = stage.name;
    name.style.fontWeight = '500';

    stageDiv.append(sprite, name);
    container.appendChild(stageDiv);
  });

  section.appendChild(container);
  return section;
}

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址