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-02-27 提交的版本。查看 最新版本

// ==UserScript==
// @name         Twitch Poké Ball Helper (Enhanced UI – Browse & Advanced)
// @namespace    http://tampermonkey.net/
// @version      9
// @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() {
      // Define balls for the Catch tab
      this.catchBalls = {
        dollars: { command: '$', tooltip: 'Poke Dollars', image: 'https://cdn.discordapp.com/attachments/1095453488684744786/1344388523750457354/pngwing.com_13.png?ex=67c0bae1&is=67bf6961&hm=ef378cc914ec3d785094e9a21690c377fbc3d5187ee243ae0d9d21b522ece867&' },
        check: { command: '!pokecheck', tooltip: 'Poke Check', image: 'https://cdn.discordapp.com/attachments/1095453488684744786/1344383577323995168/pngwing.com_2.png?ex=67c0b646&is=67bf64c6&hm=71bdf7b7a547df375849fa3874370067476fad556422dd16df77d6beba710a90&' },
        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' }
      };

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

    this.currentTab = 'catch';
    this.allPokemonList = 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();// Creates the container with both button and timer
      this.createTimerElement();// Creates the timer element inside the container
      this.addEventListeners();
      this.renderGrid();
      this.updateSpawnTimer();
    });
  }

  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;

  // Remove fixed positioning so left/top can be used
  this.container.style.right = '';
  this.container.style.bottom = '';

  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);
    // Reset the cursor style after drag completes
    const ballImg = e.target.closest('.pball-item img');
    if (ballImg) {
      ballImg.style.cursor = 'grab';
    }
  };

  // Update the dragged element's cursor style during dragging
  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'); // Remove dragging class

    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;

      // Optionally, clamp newX and newY within a chat window if it exists
      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;
      // 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;
      --glass-effect: rgba(255, 255, 255, 0.1); /* Adjust as needed */
    }

    /* Global resets & accessibility */
    .pball-container, .pball-container * {
      font-family: var(--font-family);
      box-sizing: border-box;
    }

    /* Draggable container that holds the button and timer.
       - Restrict its dimensions to fit its children.
       - Disable pointer events on the container so that only designated children receive them.
    */
    .pball-container {
      position: fixed;
      right: 12px;
      bottom: 95px;  /* Positions above chat input */
      z-index: 10000;
      /* Remove default cursor so that only the interactive elements show a pointer */
      cursor: default;
      user-select: none;
      -webkit-user-select: none;
      will-change: left, top;
      transform: scale(1);
      transform-origin: top left;
      /* Limit the container's hit area */
      width: fit-content;
      height: fit-content;
      pointer-events: none;
    }
    /* Ensure only the button (and active panel) accept mouse events */
    .pball-container > .pball-button,
    .pball-panel.active {
      pointer-events: auto;
    }



    /* Button styling (draggable) */
    .my-button {
      position: relative;
      cursor: pointer;
    }

    .spawn-timer {
      position: absolute;
      left: calc(100% - 65px);
      bottom: -33px;
      background: transparent;
      border: none;
      padding: 8px 12px;
      color: var(--text-light);
      white-space: nowrap;
      z-index: 10001;
      pointer-events: none;
      text-align: center;
    }

    .timer-header {
      display: block;
      margin-bottom: 2px;
      position: relative;
      z-index: 1;
    }

    .timer-label {
      color: #fff;
      font-size: 16px;
      font-weight: bold;
      text-shadow: 0 0 5px #000;
      display: block;
    }

    .countdown-display {
      font-size: 22px !important;
      color: #fdc331 !important;
      margin: 0;
      line-height: 1;
      text-shadow:
        0 0 5px #000,
        0 0 5px #000,
        0 0 5px #000,
        0 0 5px #000,
        0 0 5px #000,
        0 0 5px #000,
        0 0 5px #000,
        0 0 5px #000,
        0 0 5px #000,
        0 0 5px #000 !important;
      font-family: 'Arial Black', sans-serif;
      letter-spacing: -2px;
    }

    /* 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.2);
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
    }

    /* Panel with glassmorphic effect and smooth slide-in */
    .pball-panel {
      cursor: default;
      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: none;
      cursor: arrow;
    }
    .pball-item:hover {
      transform: none;
      box-shadow: none;
    }
    .pball-item img {
  pointer-events: auto;
  user-select: none;
  cursor: grab;
      width: 36px;
      height: 36px;
      transition: transform 0.2s ease;
    }
    .pball-item:hover img {
      transform: scale(1.3);
    }
.pball-item img.dragging {
  opacity: 0.6;
  cursor: grabbing;
}
    .pball-label {
      margin-top: 6px;
      font-size: 13px;
      color: var(--text-light);
      text-align: center;
    }

    .spawn-timer,
    .timer-header,
    .countdown-display {
      pointer-events: none;
    }

    /* Browse Tab: Pokémon Grid */
    .browse-container {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
      gap: 12px;
      padding: 16px;
    }

    /* 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;
      text-align: center;
    }
    .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%;
      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: 26px;
      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);
    }
/* Custom Scrollbar for Moves Section */
.moves-section {
  max-height: 160px;
  overflow-y: auto;
  font-size: 15px;
  color: var(--text-muted);
  padding-right: 6px;

  /* Ensures smooth scrolling */
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb, #888) var(--scrollbar-track, #222);
}

/* Webkit Browsers (Chrome, Edge, Safari) */
.moves-section::-webkit-scrollbar {
  width: 6px; /* Same width as other sections */
}

.moves-section::-webkit-scrollbar-track {
  background: var(--scrollbar-track, #222); /* Darker background */
  border-radius: 3px;
}

.moves-section::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb, #888); /* Scrollbar thumb color */
  border-radius: 3px;
}

.moves-section::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover, #666); /* Slightly darker on hover */
}


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

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

    /* Modified browse tile cursor */
    .browse-tile {
      cursor: arrow;
      user-select: none;
    }

    /* Media Query fix for small screens */
    @media (max-width: 400px) {
      .pball-container {
        transform-origin: bottom right;
      }
    }
  `;
  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; // Add this line
      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; // Add this line

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

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

startTimerLoop() {
  const backendUrl = 'https://poketwitch.bframework.de/info/events/last_spawn/';
  const countdownElement = document.querySelector('.countdown-display');

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

  let tickTimeout; // To store the current tick timeout ID

  // Start the tick loop given an initial remaining time
  const startTick = (remaining) => {
    const tick = () => {
      if (remaining >= 0) {
        updateDisplay(remaining);
        remaining--;
        tickTimeout = setTimeout(tick, 1000);
      }
    };
    tick();
  };

  // Fetch new timer data and restart the tick loop
  const fetchAndRestartTick = () => {
    // Clear any existing tick loop
    if (tickTimeout) clearTimeout(tickTimeout);

    fetch(backendUrl)
      .then(response => {
        if (!response.ok) throw new Error('Network error');
        return response.json();
      })
      .then(data => {
        let remaining = parseInt(data.next_spawn, 10);
        if (isNaN(remaining)) {
          throw new Error('Invalid timer data');
        }
        startTick(remaining);
      })
      .catch(error => {
        console.error('Timer error:', error);
        updateDisplay(NaN);
      });
  };

  // Start immediately
  fetchAndRestartTick();

// Refresh the timer every 10 seconds (10000 ms)
setInterval(fetchAndRestartTick, 60000);
}


    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'); // Ensure grid layout

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

    // 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 = document.querySelector('#chatInput'); // Change to your input selector

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

QingJ © 2025

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