您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
当前为
// ==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(); })();