Twitch Poké Ball Helper (Ultimate Search Tab UI – Unified Font)

Twitch Poké Ball Helper with a three-column grid for Catch/Shop and an ultra-stylized Search tab that features a full-width Pokémon info card with advanced typography (all using Roboto), animated stat bars, detailed type relations, and a refined overall look.

目前為 2025-02-25 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Twitch Poké Ball Helper (Ultimate Search Tab UI – Unified Font)
// @namespace    http://tampermonkey.net/
// @version      5.13
// @description  Twitch Poké Ball Helper with a three-column grid for Catch/Shop and an ultra-stylized Search tab that features a full-width Pokémon info card with advanced typography (all using Roboto), animated stat bars, detailed type relations, and a refined overall look.
// @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 (using !pokecatch)
      this.catchBalls = {
        check: { command: '!pokecheck', tooltip: 'Poke Check', image: 'https://cdn.discordapp.com/attachments/1095453488684744786/1343838706724896848/5c2d24739a206a1df3d19e60c801c494.png?ex=67bebad2&is=67bd6952&hm=6a86c6c6e6cc0e095accb89a7883ebb0b9c63d894600e4be6d29e0eadca4643b&' },
        poke: { command: '!pokecatch pokeball', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png' },
        great: { command: '!pokecatch greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png' },
        ultra: { command: '!pokecatch ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png' },
        master: { command: '!pokecatch masterball', tooltip: 'Master Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/master_ball.png' },
        premier: { command: '!pokecatch premierball', tooltip: 'Premier Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/premier_ball.png' },
        cherish: { command: '!pokecatch cherishball', tooltip: 'Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cherish_ball.png' },
        greatCherish: { command: '!pokecatch greatcherishball', tooltip: 'Great Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_cherish_ball.png' },
        ultraCherish: { command: '!pokecatch ultracherishball', tooltip: 'Ultra Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_cherish_ball.png' },
        heavy: { command: '!pokecatch heavyball', tooltip: 'Heavy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heavy_ball.png' },
        feather: { command: '!pokecatch featherball', tooltip: 'Feather Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/feather_ball.png' },
        timer: { command: '!pokecatch timerball', tooltip: 'Timer Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/timer_ball.png' },
        quick: { command: '!pokecatch quickball', tooltip: 'Quick Ball', image: 'https://www.shareicon.net/data/512x512/2016/12/13/863562_quick_512x512.png' },
        nest: { command: '!pokecatch nestball', tooltip: 'Nest Ball', image: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/nest-ball.png' },
        fast: { command: '!pokecatch fastball', tooltip: 'Fast Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fast_ball.png' },
        heal: { command: '!pokecatch healball', tooltip: 'Heal Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heal_ball.png' },
        repeat: { command: '!pokecatch repeatball', tooltip: 'Repeat Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/repeat_ball.png' },
        friend: { command: '!pokecatch friendball', tooltip: 'Friend Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/friend_ball.png' },
        frozen: { command: '!pokecatch frozenball', tooltip: 'Frozen Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/frozen_ball.png' },
        night: { command: '!pokecatch nightball', tooltip: 'Night Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/night_ball.png' },
        phantom: { command: '!pokecatch phantomball', tooltip: 'Phantom Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/phantom_ball.png' },
        cipher: { command: '!pokecatch cipherball', tooltip: 'Cipher Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cipher_ball.png' },
        magnet: { command: '!pokecatch magnetball', tooltip: 'Magnet Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/magnet_ball.png' },
        net: { command: '!pokecatch netball', tooltip: 'Net Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/net_ball.png' },
        luxury: { command: '!pokecatch luxuryball', tooltip: 'Luxury Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/luxury_ball.png' },
        stone: { command: '!pokecatch stoneball', tooltip: 'Stone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/stone_ball.png' },
        level: { command: '!pokecatch levelball', tooltip: 'Level Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/level_ball.png' },
        clone: { command: '!pokecatch cloneball', tooltip: 'Clone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/clone_ball.png' },
        sun: { command: '!pokecatch sunball', tooltip: 'Sun Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/sun_ball.png' },
        fantasy: { command: '!pokecatch fantasyball', tooltip: 'Fantasy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fantasy_ball.png' },
        mach: { command: '!pokecatch machball', tooltip: 'Mach Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/mach_ball.png' },
        dive: { command: '!pokecatch diveball', tooltip: 'Dive Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/dive_ball.png' }
      };

      // Define balls for the Shop tab (you can expand this list if needed)
      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' }
      };

      // Set default tab mode to 'catch'
      this.currentTab = 'catch';
      this.init();
    }

    init() {
      this.setupStyles();
      this.waitForChat().then(() => {
        this.createInterface();
        this.addEventListeners();
        this.renderGrid();
      });
    }

    setupStyles() {
      const style = document.createElement('style');
      style.textContent = `
        /* Import Roboto font for a modern look */
        @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');

        :root {
          --background-dark: #18181b;
          --background-darker: #2e2e35;
          --card-background: #1f1f26;
          --border-color: #3e3e45;
          --highlight-color: #76c7c0;
          --highlight-gradient: linear-gradient(90deg, var(--highlight-color), #4db6ac);
          --text-light: #ffffff;
          --text-muted: #ccc;
          --font-family: 'Roboto', sans-serif;
        }

        /* Global font rule for the entire widget */
        .pball-container, .pball-container * {
          font-family: var(--font-family) !important;
        }

        /* Global styles for the widget */
        .pball-container {
          position: fixed;
          bottom: 80px;
          right: 20px;
          z-index: 10000;
        }
        .pball-button {
          cursor: pointer;
          width: 50px;
          height: 50px;
          border-radius: 50%;
          border: 2px solid var(--border-color);
          box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
          transition: transform 0.2s ease, box-shadow 0.2s ease;
          background: var(--background-dark);
        }
        .pball-button:hover {
          transform: scale(1.1);
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
        }
        .pball-panel {
          position: absolute;
          bottom: calc(100% + 10px);
          right: 0;
          background: rgba(24, 24, 27, 0.97);
          width: 280px;
          border-radius: 8px;
          overflow: hidden;
          box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
          visibility: hidden;
          opacity: 0;
          transform: translateY(10px);
          transition: opacity 0.2s ease, transform 0.2s ease;
          pointer-events: none;
        }
        .pball-panel.active {
          visibility: visible;
          opacity: 1;
          transform: translateY(0);
          pointer-events: auto;
        }
        .pball-tabs {
          display: flex;
          background: var(--background-darker);
          border-bottom: 1px solid var(--border-color);
        }
        .pball-tab {
          flex: 1;
          text-align: center;
          padding: 8px;
          font-size: 16px;
          cursor: pointer;
          color: var(--text-muted);
          transition: background 0.2s ease;
        }
        .pball-tab.active,
        .pball-tab:hover {
          background: var(--border-color);
          color: var(--text-light);
        }
        .pball-search-container {
          position: relative;
          width: calc(100% - 20px);
          margin: 10px;
        }
        .pball-search {
          width: 100%;
          padding: 6px 30px 6px 8px;
          border: 1px solid var(--border-color);
          border-radius: 4px;
          background: var(--background-dark);
          color: var(--text-light);
          font-size: 15px;
          outline: none;
        }
        .pball-search::placeholder {
          color: #777;
        }
        .pball-clear-btn {
          position: absolute;
          right: 8px;
          top: 50%;
          transform: translateY(-50%);
          background: transparent;
          border: none;
          color: var(--text-muted);
          font-size: 16px;
          cursor: pointer;
          display: none;
        }
        .pball-grid {
          display: grid;
          gap: 10px;
          padding: 10px;
          max-height: 240px;
          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: 4px;
        }
        .pball-grid::-webkit-scrollbar-thumb {
          background: var(--border-color);
          border-radius: 4px;
        }
        .pball-grid::-webkit-scrollbar-thumb:hover {
          background: #4b4b56;
        }
        .moves-section::-webkit-scrollbar {
          width: 8px;
        }
        .moves-section::-webkit-scrollbar-track {
          background: var(--background-dark);
          border-radius: 4px;
        }
        .moves-section::-webkit-scrollbar-thumb {
          background: var(--border-color);
          border-radius: 4px;
        }
        .moves-section::-webkit-scrollbar-thumb:hover {
          background: #4b4b56;
        }
        .pball-item {
          display: flex;
          flex-direction: column;
          align-items: center;
          cursor: default;
          transition: transform 0.2s ease, box-shadow 0.2s ease;
        }
        .pball-item:hover {
          transform: translateY(-4px);
          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
        }
        .pball-item img {
          width: 40px;
          height: 40px;
        }
        .pball-label {
          margin-top: 4px;
          font-size: 14px;
          font-weight: 600;
          color: var(--text-light);
          text-align: center;
        }
        /* SEARCH TAB - Enhanced Pokémon Info Card */
        .poke-card {
          width: 100%;
          background: var(--card-background);
          border: 1px solid var(--border-color);
          border-radius: 8px;
          padding: 16px;
          box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
          color: var(--text-light);
          display: flex;
          flex-direction: column;
          gap: 16px;
          animation: fadeIn 0.5s ease;
          box-sizing: border-box;
        }
        @keyframes fadeIn {
          from { opacity: 0; transform: scale(0.95); }
          to { opacity: 1; transform: scale(1); }
        }
        .poke-card header {
          display: flex;
          align-items: center;
          gap: 16px;
          border-bottom: 1px solid var(--border-color);
          padding-bottom: 8px;
        }
        .poke-card header img {
          width: 100px;
          height: 100px;
          border-radius: 8px;
          background: var(--background-dark);
          object-fit: contain;
        }
        .poke-card h2 {
          margin: 0;
          font-size: 32px;
          font-weight: 700;
        }
        .poke-card p {
          margin: 4px 0;
          font-size: 16px;
        }
        .section {
          border-top: 1px solid var(--border-color);
          padding-top: 8px;
        }
        .section h3 {
          margin: 8px 0;
          font-size: 20px;
          font-weight: 700;
          color: var(--text-light);
          border-bottom: 1px solid var(--border-color);
          padding-bottom: 4px;
        }
        /* Refined Stats */
        .stat {
          display: flex;
          flex-direction: column;
          margin-bottom: 8px;
        }
        .stat-label {
          font-size: 16px;
          margin-bottom: 4px;
          color: #ddd;
        }
        .stat-bar {
          width: 100%;
          background: var(--background-dark);
          border: 1px solid var(--border-color);
          border-radius: 4px;
          height: 18px;
          overflow: hidden;
          position: relative;
        }
        .stat-fill {
          background: var(--highlight-gradient);
          height: 100%;
          width: 0;
          transition: width 0.5s ease;
          border-radius: 4px;
          position: relative;
        }
        .stat-value {
          position: absolute;
          right: 6px;
          top: 50%;
          transform: translateY(-50%);
          font-size: 14px;
          color: var(--text-light);
          font-weight: bold;
        }
        /* Moves Section */
        .moves-section {
          max-height: 150px;
          overflow-y: auto;
          font-size: 15px;
          color: var(--text-muted);
          margin-top: 8px;
          padding-right: 4px;
        }
        .moves-section ul {
          list-style: none;
          padding: 0;
          margin: 0;
        }
        .moves-section li {
          margin-bottom: 4px;
        }
        /* Type Damage Relations */
        .type-relations {
          display: flex;
          flex-direction: column;
          gap: 8px;
          margin-top: 8px;
        }
        .type-box {
          background: var(--background-dark);
          border: 1px solid var(--border-color);
          border-radius: 4px;
          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;
        }
        /* Spinner */
        .spinner {
          margin: 20px 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); }
        }
      `;
      document.head.appendChild(style);
    }

    async waitForChat() {
      return new Promise((resolve) => {
        if (document.querySelector('[data-test-selector="chat-input"]')) {
          return resolve();
        }
        const observer = new MutationObserver(() => {
          if (document.querySelector('[data-test-selector="chat-input"]')) {
            observer.disconnect();
            resolve();
          }
        });
        observer.observe(document.body, { childList: true, subtree: true });
      });
    }

    createInterface() {
      this.container = document.createElement('div');
      this.container.className = 'pball-container';
      this.button = this.createMainButton();
      this.panel = this.createPanel();
      this.container.append(this.button, this.panel);
      document.body.appendChild(this.container);
    }

    createMainButton() {
      const button = document.createElement('img');
      button.className = 'pball-button';
      button.src = this.catchBalls.poke.image;
      return button;
    }

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

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

      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 searchTab = document.createElement('div');
      searchTab.className = 'pball-tab';
      searchTab.textContent = 'Search';
      searchTab.dataset.tab = 'search';
      tabsContainer.appendChild(searchTab);

      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.clearBtn = document.createElement('button');
      this.clearBtn.className = 'pball-clear-btn';
      this.clearBtn.textContent = '×';

      searchContainer.append(this.searchInput, this.clearBtn);

      this.gridContainer = document.createElement('div');
      this.gridContainer.className = 'pball-grid';

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

    renderGrid() {
      if (this.currentTab === 'search') {
        this.gridContainer.classList.remove('ball-items');
        this.gridContainer.classList.add('search-results');
        this.renderPokemonSearch();
      } 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();
      }
    }

    renderPokemonSearch() {
      this.gridContainer.innerHTML = '';
      const info = document.createElement('div');
      info.style.padding = '10px';
      info.style.color = 'var(--text-light)';
      info.textContent = 'Enter a Pokémon name and press Enter to search.';
      this.gridContainer.appendChild(info);
    }

    addEventListeners() {
      this.button.addEventListener('click', (e) => {
        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 chatInput = this.getChatInput();
      if (chatInput) {
        chatInput.addEventListener('dragover', (e) => e.preventDefault());
        chatInput.addEventListener('drop', (e) => {
          e.preventDefault();
          const ballType = e.dataTransfer.getData('text/plain');
          this.insertCommand(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 !== 'search') {
          this.filterGrid();
        }
        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 !== 'search') {
          this.filterGrid();
        }
      });

      this.searchInput.addEventListener('keydown', (e) => {
        if (this.currentTab === 'search' && e.key === 'Enter') {
          this.searchPokemon(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);
      });
      this.searchInput.placeholder = (tabName === 'search') ? 'Search Pokémon...' : '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');
      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 selection = window.getSelection();
      const range = document.createRange();
      range.selectNodeContents(this.getChatInput());
      selection.removeAllRanges();
      selection.addRange(range);
      document.execCommand('delete');
    }

    insertText(text) {
      document.execCommand('insertText', false, text);
    }

    triggerInputEvent(element) {
      element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
    }

    searchPokemon(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 => {
          this.displayPokemonData(data);
        })
        .catch(err => {
          this.gridContainer.innerHTML = `<div style="padding:10px; color: var(--text-light);">${err.message}</div>`;
        });
    }

    displayPokemonData(data) {
      this.gridContainer.innerHTML = '';
      const card = document.createElement('div');
      card.className = 'poke-card';

      // Header
      const header = document.createElement('header');
      const img = document.createElement('img');
      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.textContent = `${data.name.charAt(0).toUpperCase() + data.name.slice(1)} (ID: ${data.id})`;
      header.appendChild(title);
      card.appendChild(header);

      // Calculate total stats
      const totalStats = data.stats.reduce((sum, stat) => sum + stat.base_stat, 0);

      // Basic Info with Total Stats added above Height
      const basicInfo = document.createElement('div');
      basicInfo.className = 'section';
      basicInfo.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>
      `;
      card.appendChild(basicInfo);

      // Abilities
      const abilitiesSection = document.createElement('div');
      abilitiesSection.className = 'section';
      abilitiesSection.innerHTML = `<h3>Abilities</h3>`;
      const abilitiesList = document.createElement('ul');
      data.abilities.forEach(a => {
        const li = document.createElement('li');
        li.textContent = `${a.ability.name}${a.is_hidden ? ' (Hidden)' : ''}`;
        abilitiesList.appendChild(li);
      });
      abilitiesSection.appendChild(abilitiesList);
      card.appendChild(abilitiesSection);

      // Stats
      const statsSection = document.createElement('div');
      statsSection.className = 'section';
      statsSection.innerHTML = `<h3>Stats</h3>`;
      data.stats.forEach(stat => {
        const statContainer = document.createElement('div');
        statContainer.className = 'stat';
        const label = document.createElement('div');
        label.className = 'stat-label';
        label.textContent = `${stat.stat.name.toUpperCase()}: ${stat.base_stat}`;
        statContainer.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);
        statContainer.appendChild(bar);
        statsSection.appendChild(statContainer);
      });
      card.appendChild(statsSection);

      // Types
      const typesSection = document.createElement('div');
      typesSection.className = 'section';
      typesSection.innerHTML = `<h3>Types</h3>`;
      const typesList = document.createElement('ul');
      data.types.forEach(typeInfo => {
        const li = document.createElement('li');
        li.textContent = typeInfo.type.name;
        typesList.appendChild(li);
      });
      typesSection.appendChild(typesList);
      card.appendChild(typesSection);

      // Type Damage Relations
      const typeRelationsSection = document.createElement('div');
      typeRelationsSection.className = 'section';
      typeRelationsSection.innerHTML = `<h3>Type Damage Relations</h3>`;
      const typeRelationsBox = document.createElement('div');
      typeRelationsBox.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);
          });
        typeRelationsBox.appendChild(typeBox);
      });
      typeRelationsSection.appendChild(typeRelationsBox);
      card.appendChild(typeRelationsSection);

      // Moves
      const movesSection = document.createElement('div');
      movesSection.className = 'section moves-section';
      movesSection.innerHTML = `<h3>Moves</h3>`;
      const movesList = document.createElement('ul');
      data.moves.forEach(moveInfo => {
        const li = document.createElement('li');
        li.textContent = moveInfo.move.name;
        movesList.appendChild(li);
      });
      movesSection.appendChild(movesList);
      card.appendChild(movesSection);

      // Held Items
      if (data.held_items.length) {
        const itemsSection = document.createElement('div');
        itemsSection.className = 'section';
        itemsSection.innerHTML = `<h3>Held Items</h3>`;
        const itemsList = document.createElement('ul');
        data.held_items.forEach(itemInfo => {
          const li = document.createElement('li');
          li.textContent = itemInfo.item.name;
          itemsList.appendChild(li);
        });
        itemsSection.appendChild(itemsList);
        card.appendChild(itemsSection);
      }

      // Forms
      if (data.forms.length) {
        const formsSection = document.createElement('div');
        formsSection.className = 'section';
        formsSection.innerHTML = `<h3>Forms</h3>`;
        const formsList = document.createElement('ul');
        data.forms.forEach(form => {
          const li = document.createElement('li');
          li.textContent = form.name;
          formsList.appendChild(li);
        });
        formsSection.appendChild(formsList);
        card.appendChild(formsSection);
      }

      this.gridContainer.innerHTML = '';
      this.gridContainer.appendChild(card);
    }
  }

  new PokeballHelper();
})();

QingJ © 2025

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