您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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
当前为
// ==UserScript== // @name Twitch Poké Ball Helper (Enhanced UI – Browse & Advanced) // @namespace http://tampermonkey.net/ // @version 11 // @description Twitch Poké Ball Helper with a three-column grid for Catch/Shop plus two distinct lookup tabs: a visually rich Browse tab and a detailed Advanced tab featuring a full-width Pokémon info card with Pokédex entry and evolution chain. All styled with advanced UI techniques and a unified Roboto font. Shoutout doubleupmafia @doubleupmolly @doubleuplowlow219 @doubleupap @doubleupeazy @musiclov3r1435 // @author // @match https://www.twitch.tv/* // @icon https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png // @grant none // ==/UserScript== (function () { 'use strict'; class PokeballHelper { constructor() { this.catchBalls = { dollars: { command: '$', tooltip: 'Poke Dollars', image: 'https://i.postimg.cc/T20dR1qH/f547e065261b657c49d5702826b0deca.png' }, check: { command: '!pokecheck', tooltip: 'Poke Check', image: 'https://i.postimg.cc/0N7vhyyn/ea9752334aa08543e2f148c0a903719e.png' }, poke: { command: '!pokecatch', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png' }, great: { command: '!pokecatch greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png' }, ultra: { command: '!pokecatch ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png' }, master: { command: '!pokecatch masterball', tooltip: 'Master Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/master_ball.png' }, premier: { command: '!pokecatch premierball', tooltip: 'Premier Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/premier_ball.png' }, cherish: { command: '!pokecatch cherishball', tooltip: 'Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cherish_ball.png' }, greatCherish: { command: '!pokecatch greatcherishball', tooltip: 'Great Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_cherish_ball.png' }, ultraCherish: { command: '!pokecatch ultracherishball', tooltip: 'Ultra Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_cherish_ball.png' }, heavy: { command: '!pokecatch heavyball', tooltip: 'Heavy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heavy_ball.png' }, feather: { command: '!pokecatch featherball', tooltip: 'Feather Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/feather_ball.png' }, timer: { command: '!pokecatch timerball', tooltip: 'Timer Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/timer_ball.png' }, quick: { command: '!pokecatch quickball', tooltip: 'Quick Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/quick_ball.png' }, nest: { command: '!pokecatch nestball', tooltip: 'Nest Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/nest_ball.png' }, fast: { command: '!pokecatch fastball', tooltip: 'Fast Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fast_ball.png' }, heal: { command: '!pokecatch healball', tooltip: 'Heal Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heal_ball.png' }, repeat: { command: '!pokecatch repeatball', tooltip: 'Repeat Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/repeat_ball.png' }, friend: { command: '!pokecatch friendball', tooltip: 'Friend Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/friend_ball.png' }, frozen: { command: '!pokecatch frozenball', tooltip: 'Frozen Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/frozen_ball.png' }, night: { command: '!pokecatch nightball', tooltip: 'Night Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/night_ball.png' }, phantom: { command: '!pokecatch phantomball', tooltip: 'Phantom Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/phantom_ball.png' }, cipher: { command: '!pokecatch cipherball', tooltip: 'Cipher Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cipher_ball.png' }, magnet: { command: '!pokecatch magnetball', tooltip: 'Magnet Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/magnet_ball.png' }, net: { command: '!pokecatch netball', tooltip: 'Net Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/net_ball.png' }, luxury: { command: '!pokecatch luxuryball', tooltip: 'Luxury Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/luxury_ball.png' }, stone: { command: '!pokecatch stoneball', tooltip: 'Stone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/stone_ball.png' }, level: { command: '!pokecatch levelball', tooltip: 'Level Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/level_ball.png' }, clone: { command: '!pokecatch cloneball', tooltip: 'Clone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/clone_ball.png' }, sun: { command: '!pokecatch sunball', tooltip: 'Sun Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/sun_ball.png' }, fantasy: { command: '!pokecatch fantasyball', tooltip: 'Fantasy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fantasy_ball.png' }, mach: { command: '!pokecatch machball', tooltip: 'Mach Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/mach_ball.png' }, dive: { command: '!pokecatch diveball', tooltip: 'Dive Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/dive_ball.png' } }; this.shopBalls = { pokeball: { command: '!pokeshop pokeball', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png' }, great: { command: '!pokeshop greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png' }, ultra: { command: '!pokeshop ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png' } }; this.currentTab = 'catch'; this.allPokemonList = null; this.isDragging = false; this.startX = 0; this.startY = 0; this.containerStartLeft = 0; this.containerStartTop = 0; this.wasDragging = false; this.dragStart = this.dragStart.bind(this); this.drag = this.drag.bind(this); this.dragEnd = this.dragEnd.bind(this); this.init(); } // Add this line in the init method to initialize search buttons init() { this.setupStyles(); this.waitForChat().then(() => { this.createInterface(); this.createTimerElement(); this.addEventListeners(); this.renderGrid(); this.initSearchButtons(); // Initialize search buttons this.updateSpawnTimer(); }); } handleSearch(query) { switch(this.currentTab) { case 'advanced': this.searchAdvancedPokemon(query); break; case 'browse': this.renderBrowseGrid(); break; default: this.filterGrid(); } } // Update the initSearchButtons method initSearchButtons() { document.querySelectorAll('.pball-search-container').forEach(container => { const input = container.querySelector('.pball-search'); if (!input || container.dataset.initialized) return; const btnContainer = document.createElement('div'); btnContainer.className = 'search-buttons'; // Enter button const enterButton = Object.assign(document.createElement('button'), { className: 'search-enter-button', innerHTML: '✔', title: 'Search (Enter)', onclick: () => this.handleSearch(input.value.trim()) }); // Clear button (persistent) const clearButton = Object.assign(document.createElement('button'), { className: 'pball-clear-btn', innerHTML: '×', title: 'Clear search', style: 'display: none;', // Start hidden onclick: () => { input.value = ''; input.focus(); this.handleSearch(''); clearButton.style.display = 'none'; } }); // Input events input.addEventListener('input', () => { clearButton.style.display = input.value ? 'flex' : 'none'; }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') enterButton.click(); }); btnContainer.append(enterButton, clearButton); container.append(btnContainer); container.dataset.initialized = true; }); } loadPosition() { const savedPos = localStorage.getItem('pballPosition'); if (savedPos) { const { x, y } = JSON.parse(savedPos); this.container.style.left = `${x}px`; this.container.style.top = `${y}px`; } } dragStart(e) { e.preventDefault(); this.wasDragging = false; const startX = e.clientX; const startY = e.clientY; const rect = this.container.getBoundingClientRect(); const origLeft = rect.left; const origTop = rect.top; const onMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - startX; const deltaY = moveEvent.clientY - startY; if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { this.wasDragging = true; } this.container.style.left = `${origLeft + deltaX}px`; this.container.style.top = `${origTop + deltaY}px`; }; const onMouseUp = () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); const ballImg = e.target.closest('.pball-item img'); if (ballImg) { ballImg.style.cursor = 'grab'; } }; const ballImg = e.target.closest('.pball-item img'); if (ballImg) { ballImg.style.cursor = 'grabbing'; } window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); } drag(e) { this.container.classList.remove('dragging'); e.preventDefault(); const dx = e.clientX - this.startX; const dy = e.clientY - this.startY; if (!this.isDragging && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) { this.isDragging = true; } if (this.isDragging) { let newX = this.containerStartLeft + dx; let newY = this.containerStartTop + dy; const chatWindow = document.querySelector('.chat-window'); if (chatWindow) { const chatRect = chatWindow.getBoundingClientRect(); const ballRect = this.container.getBoundingClientRect(); newX = Math.max(chatRect.left, Math.min(newX, chatRect.right - ballRect.width)); newY = Math.max(chatRect.top, Math.min(newY, chatRect.bottom - ballRect.height)); } requestAnimationFrame(() => { this.container.style.left = `${newX}px`; this.container.style.top = `${newY}px`; }); } } dragEnd(e) { document.removeEventListener('mousemove', this.drag); document.removeEventListener('mouseup', this.dragEnd); if (this.isDragging) { this.wasDragging = true; const left = this.container.offsetLeft; const top = this.container.offsetTop; localStorage.setItem('pballPosition', JSON.stringify({ x: left, y: top })); } this.container.style.transition = ''; } setupStyles() { const style = document.createElement('style'); style.textContent = ` /* Theme Variables */ /*-------------------------------------------------- Revamped UI CSS --------------------------------------------------*/ /*-------------------------------------------------- Import Fonts --------------------------------------------------*/ @import url('https://fonts.googleapis.com/css2?family=Segment7Standard&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); /*-------------------------------------------------- Global Variables & Base Styles --------------------------------------------------*/ :root { /* Color Palette */ --color-primary: #32cd32; --color-secondary: #4db6ac; --color-accent: #ffe135; --color-danger: #ff4040; --color-dark: #18181b; --color-darker: #2e2e35; --color-card: #1f1f26; --color-border: #3e3e45; --color-glass: rgba(255, 255, 255, 0.1); --gradient-accent: linear-gradient(90deg, var(--color-primary), var(--color-secondary)); --gradient-bg: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d); /* Typography */ --font-base: 'Roboto', sans-serif; --font-led: 'Segment7Standard', monospace; --font-label: 'Press Start 2P', cursive; /* UI Sizing */ --border-radius-small: 4px; --border-radius-medium: 12px; --border-radius-large: 16px; /* Effects */ --transition-fast: 0.2s ease; --transition-medium: 0.3s ease; --box-shadow-light: 0 2px 8px rgba(0, 0, 0, 0.3); --box-shadow-heavy: 0 4px 16px rgba(0, 0, 0, 0.5); --backdrop-blur: blur(10px); } /* Global Reset & Base */ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; font-family: var(--font-base); } /* Custom Scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: var(--color-darker); border-radius: var(--border-radius-medium); } ::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: var(--border-radius-medium); border: 1px solid var(--color-dark); } ::-webkit-scrollbar-thumb:hover { background: #555; } /*-------------------------------------------------- Main Container & Interactive Elements --------------------------------------------------*/ .pball-container { position: fixed; bottom: 90px; right: 20px; z-index: 10000; pointer-events: none; transform: scale(1); transform-origin: top right; width: fit-content; height: fit-content; } /* Enable interactions for children */ .pball-container > * { pointer-events: auto; } /* Main Action Button */ .pball-button { width: 60px; height: 60px; border-radius: 50%; background: var(--gradient-accent); border: 2px solid var(--color-border); box-shadow: var(--box-shadow-light); cursor: pointer; transition: transform var(--transition-fast), box-shadow var(--transition-fast); display: flex; align-items: center; justify-content: center; } .pball-button:hover { transform: scale(1.2); box-shadow: var(--box-shadow-heavy); } /*-------------------------------------------------- Floating Panel with Glassmorphism --------------------------------------------------*/ .pball-panel { position: absolute; bottom: calc(100% + 12px); right: 0; width: 340px; background: rgba(30, 30, 35, 0.8); backdrop-filter: var(--backdrop-blur); border-radius: var(--border-radius-large); border: 1px solid var(--color-glass); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); opacity: 0; visibility: hidden; transform: translateY(20px); transition: opacity var(--transition-medium), transform var(--transition-medium), visibility var(--transition-medium); } .pball-panel.active { opacity: 1; visibility: visible; transform: translateY(0); } /*-------------------------------------------------- Tab Navigation --------------------------------------------------*/ .pball-tabs { display: flex; background: var(--color-darker); border-bottom: 1px solid var(--color-border); border-top-left-radius: var(--border-radius-large); border-top-right-radius: var(--border-radius-large); overflow: hidden; } .pball-tab { flex: 1; padding: 12px; text-align: center; font-size: 15px; cursor: pointer; color: var(--color-border); transition: background var(--transition-fast), color var(--transition-fast); } .pball-tab:hover, .pball-tab.active { background: var(--gradient-accent); color: var(--color-dark); } /*-------------------------------------------------- Search Component --------------------------------------------------*/ .pball-search-container { position: relative; margin: 16px; background: var(--color-card); border-radius: var(--border-radius-medium); border: 1px solid var(--color-border); overflow: hidden; } .pball-search { width: 100%; padding: 10px 14px; padding-right: 70px; border: none; background: transparent; color: var(--color-border); font-size: 15px; outline: none; } .search-buttons { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); display: flex; gap: 6px; } .search-enter-button, .pball-clear-btn { width: 30px; height: 30px; border: none; border-radius: var(--border-radius-small); display: flex; align-items: center; justify-content: center; transition: background var(--transition-fast); cursor: pointer; } .search-enter-button { background: var(--color-primary); color: #000; } .pball-clear-btn { background: var(--color-danger); color: #fff; display: none; } .pball-search:not(:placeholder-shown) ~ .search-buttons .pball-clear-btn { display: flex; } /*-------------------------------------------------- Grid Layouts & Item Cards --------------------------------------------------*/ .pball-grid { padding: 16px; display: grid; gap: 16px; max-height: 320px; overflow-y: auto; } .pball-grid.ball-items { grid-template-columns: repeat(3, 1fr); } .pball-grid.search-results { grid-template-columns: 1fr; } /* Item Card */ .pball-item { display: flex; flex-direction: column; align-items: center; justify-content: center; background: transparent; border: none; border-radius: 50%; padding: 10px; transition: transform var(--transition-fast); } .pball-item img { width: 40px; height: 40px; transition: transform var(--transition-fast); cursor: grab; } .pball-item img:hover { transform: scale(1.2); } .pball-item img.dragging { opacity: 0.7; transform: scale(0.9); filter: drop-shadow(0 0 6px var(--color-primary)); } .pball-item .pball-label { margin-top: 8px; font-size: 14px; color: var(--color-border); text-align: center; } /*-------------------------------------------------- Browse Tiles & Advanced Cards --------------------------------------------------*/ .browse-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 16px; padding: 20px; } .browse-tile { background: var(--color-darker); border: 1px solid var(--color-border); border-radius: var(--border-radius-medium); padding: 16px; text-align: center; transition: transform var(--transition-fast), box-shadow var(--transition-fast); cursor: pointer; } .browse-tile:hover { transform: translateY(-4px); box-shadow: var(--box-shadow-light); } .browse-tile img { width: 72px; height: 72px; margin-bottom: 8px; } .browse-tile .tile-label { font-size: 15px; font-weight: 600; color: var(--color-border); text-transform: capitalize; } /* Advanced Card Components */ .poke-card { padding: 20px; display: grid; grid-template-columns: 1fr; gap: 20px; background: var(--color-card); border-radius: var(--border-radius-large); box-shadow: var(--box-shadow-light); } .poke-card .section-title { font-size: 22px; font-weight: 700; color: var(--color-border); margin-bottom: 12px; } .poke-card .stats-grid { font-size: 22px; font-weight: 700; display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 16px; } /* Evolution Chain */ .evolution-chain { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; padding: 12px; } .evolution-chain .evolution-item { background: var(--color-darker); border-radius: var(--border-radius-small); padding: 8px; text-align: center; transition: transform var(--transition-fast); } .evolution-chain .evolution-item:hover { transform: translateY(-2px); } .evolution-chain .evolution-item img { width: 68px; height: 68px; margin-bottom: 6px; } .evolution-chain .evolution-item p { font-size: 13px; color: var(--color-border); margin: 0; } /* Moves Section */ .moves-section { max-height: 350px; overflow-y: auto; padding: 12px; } /*-------------------------------------------------- Timer Container & Components --------------------------------------------------*/ .spawn-timer { position: absolute; bottom: calc(100% - 5.8rem); right: 6.5rem; padding: 1rem 1.2rem; backdrop-filter: var(--backdrop-blur); border-radius: var(--border-radius-large); box-shadow: var(--box-shadow-light); display: flex; align-items: center; gap: 1rem; transform: perspective(500px) rotateX(5deg); transition: transform var(--transition-medium); z-index: 10001; } .spawn-timer:hover { transform: perspective(500px) rotateX(0deg); } .timer-header { width: 100%; display: flex; flex-direction: column; align-items: center; gap: 0.3rem; } .timer-label { font-family: var(--font-label); font-size: 0.8rem; color: var(--color-accent); text-transform: uppercase; letter-spacing: 0.1rem; padding: 0.4rem 0.8rem; border-radius: var(--border-radius-small); border: 1px solid rgba(58, 90, 109, 0.4); background: rgba(255, 255, 255, 0.05); text-shadow: 0 0.1rem 0.2rem rgba(0, 0, 0, 0.4); transition: background var(--transition-medium); } .timer-label:hover { background: rgba(255, 255, 255, 0.15); } .countdown-display { font-family: var(--font-led); font-size: 2.2rem; font-weight: bold; color: var(--color-primary); padding: 0.5rem 0.9rem; border-radius: var(--border-radius-small); letter-spacing: 0.15rem; min-width: 7.5rem; text-align: center; background: linear-gradient(135deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.05)); box-shadow: 0 0.4rem 0.8rem rgba(0, 255, 55, 0.4), inset 0 0.3rem 0.6rem rgba(0, 255, 55, 0.3); transition: transform var(--transition-medium), box-shadow var(--transition-medium); position: relative; } .countdown-display:hover { transform: scale(1.05); box-shadow: 0 0.6rem 1.2rem rgba(0, 255, 55, 0.6), inset 0 0.4rem 0.8rem rgba(0, 255, 55, 0.4); } .countdown-display::before { content: ''; position: absolute; inset: 0; background: radial-gradient(circle, rgba(0, 255, 55, 0.2), transparent 70%); border-radius: inherit; z-index: -1; transition: opacity var(--transition-medium); } .countdown-display.low-time { color: var(--color-danger); text-shadow: 0 0.5rem 0.75rem rgba(255, 64, 64, 0.8), 0 0.3rem 0.5rem rgba(255, 64, 64, 0.9); animation: led-pulse 0.6s ease-in-out infinite, emergency-glow 1.2s ease-in-out infinite; } @keyframes led-pulse { 0%, 100% { text-shadow: 0 0.5rem 0.75rem rgba(255, 64, 64, 0.8); } 50% { text-shadow: 0 0.8rem 1rem rgba(255, 64, 64, 1); } } @keyframes emergency-glow { 0%, 100% { box-shadow: 0 0.4rem 0.8rem rgba(255, 64, 64, 0.3); } 50% { box-shadow: 0 0.8rem 1.6rem rgba(255, 64, 64, 0.6); } } /*-------------------------------------------------- Spinner Utility --------------------------------------------------*/ .spinner { margin: 1.5rem auto; border: 4px solid var(--color-border); border-top: 4px solid var(--color-primary); border-radius: 50%; width: 2.8rem; height: 2.8rem; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /*-------------------------------------------------- Responsive Adjustments --------------------------------------------------*/ @media (max-width: 768px) { .poke-card { padding: 16px; gap: 16px; } .section-title { font-size: 18px; } .stats-radar-chart canvas { max-width: 100%; } } @media (max-width: 600px) { .spawn-timer { gap: 0.75rem; padding: 0.75rem 1rem; } .timer-label { font-size: 0.65rem; padding: 0.3rem 0.6rem; } .countdown-display { font-size: 1.8rem; min-width: 4.5rem; } } `; document.head.appendChild(style); } async waitForChat() { return new Promise((resolve) => { if (document.querySelector('[data-test-selector="chat-input"]')) { return resolve(); } const observer = new MutationObserver(() => { if (document.querySelector('[data-test-selector="chat-input"]')) { observer.disconnect(); resolve(); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } createInterface() { this.container = document.createElement('div'); this.container.className = 'pball-container'; this.button = this.createMainButton(); this.panel = this.createPanel(); this.container.append(this.button, this.panel); document.body.appendChild(this.container); } createMainButton() { const button = document.createElement('img'); button.draggable = false; button.className = 'pball-button'; button.src = this.catchBalls.poke.image; return button; } createPanel() { const panel = document.createElement('div'); panel.className = 'pball-panel'; panel.draggable = false; const tabsContainer = document.createElement('div'); tabsContainer.className = 'pball-tabs'; const catchTab = document.createElement('div'); catchTab.className = 'pball-tab active'; catchTab.textContent = 'Catch'; catchTab.dataset.tab = 'catch'; tabsContainer.appendChild(catchTab); const shopTab = document.createElement('div'); shopTab.className = 'pball-tab'; shopTab.textContent = 'Shop'; shopTab.dataset.tab = 'shop'; tabsContainer.appendChild(shopTab); const browseTab = document.createElement('div'); browseTab.className = 'pball-tab'; browseTab.textContent = 'Browse'; browseTab.dataset.tab = 'browse'; tabsContainer.appendChild(browseTab); const advancedTab = document.createElement('div'); advancedTab.className = 'pball-tab'; advancedTab.textContent = 'Advanced'; advancedTab.dataset.tab = 'advanced'; tabsContainer.appendChild(advancedTab); const searchContainer = document.createElement('div'); searchContainer.className = 'pball-search-container'; this.searchInput = document.createElement('input'); this.searchInput.type = 'text'; this.searchInput.className = 'pball-search'; this.searchInput.placeholder = 'Search...'; this.searchInput.setAttribute('aria-label', 'Search Pokémon'); this.clearBtn = document.createElement('button'); this.clearBtn.className = 'pball-clear-btn'; this.clearBtn.textContent = '⛒'; this.clearBtn.setAttribute('aria-label', 'Clear Search'); searchContainer.append(this.searchInput); this.gridContainer = document.createElement('div'); this.gridContainer.className = 'pball-grid'; panel.append(tabsContainer, searchContainer, this.gridContainer); return panel; } createTimerElement() { this.timerContainer = document.createElement('div'); this.timerContainer.className = 'spawn-timer'; this.timerContainer.innerHTML = ` <div class="timer-header"> <div class="countdown-display">88:88</div> </div> `; this.container.appendChild(this.timerContainer); this.initTimer(); } initTimer() { this.backendUrl = 'https://poketwitch.bframework.de/info/events/last_spawn/'; this.countdownElement = this.timerContainer.querySelector('.countdown-display'); this.remainingSeconds = 0; this.tickInterval = null; this.isFetching = false; // Start countdown mechanism this.startTick(); // Fetch the initial timer value this.fetchTimer(); // Allow manual refresh on click if not already fetching this.timerContainer.addEventListener('click', () => { if (!this.isFetching) { this.fetchTimer(); } }); } startTick() { if (this.tickInterval) clearInterval(this.tickInterval); this.tickInterval = setInterval(() => { if (this.remainingSeconds > 0) { this.remainingSeconds--; this.updateDisplay(this.remainingSeconds); } else { this.updateDisplay(0); this.fetchTimer(); // Fetch API only when timer hits 00:00 } }, 1000); } updateDisplay(seconds) { if (isNaN(seconds) || seconds < 0) { this.countdownElement.textContent = '--:--'; this.countdownElement.classList.remove('low-time'); return; } const mins = Math.floor(seconds / 60); const secs = seconds % 60; this.countdownElement.textContent = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; this.countdownElement.classList.toggle('low-time', seconds <= 30); } fetchTimer() { if (this.isFetching) return; this.isFetching = true; fetch(this.backendUrl) .then(response => { if (!response.ok) throw new Error('Network error'); return response.json(); }) .then(data => { const newTime = parseInt(data.next_spawn, 10); if (isNaN(newTime)) throw new Error('Invalid timer data'); // Reset countdown this.remainingSeconds = newTime; this.updateDisplay(this.remainingSeconds); }) .catch(error => { console.error('Timer error:', error); this.remainingSeconds = -1; this.updateDisplay(NaN); }) .finally(() => { this.isFetching = false; }); } renderGrid() { if (this.currentTab === 'advanced') { this.gridContainer.classList.remove('ball-items'); this.gridContainer.classList.add('search-results'); this.renderAdvancedInstruction(); } else if (this.currentTab === 'browse') { this.gridContainer.classList.remove('ball-items'); this.gridContainer.classList.add('search-results'); this.renderBrowse(); } else { this.gridContainer.classList.remove('search-results'); this.gridContainer.classList.add('ball-items'); this.gridContainer.innerHTML = ''; const balls = this.currentTab === 'catch' ? this.catchBalls : this.shopBalls; Object.entries(balls).forEach(([key, ball]) => { const item = document.createElement('div'); item.className = 'pball-item'; item.dataset.label = ball.tooltip.toLowerCase(); const img = document.createElement('img'); img.src = ball.image; img.dataset.ballType = ball.command; img.draggable = true; const label = document.createElement('div'); label.className = 'pball-label'; label.textContent = ball.tooltip; item.append(img, label); this.gridContainer.appendChild(item); }); this.filterGrid(); } } renderAdvancedInstruction() { this.gridContainer.innerHTML = ''; const info = document.createElement('div'); info.style.padding = '12px'; info.style.textAlign = 'center'; info.style.color = 'var(--text-light)'; info.textContent = 'Enter a Pokémon name and press Enter for detailed info.'; this.gridContainer.appendChild(info); } renderBrowse() { this.gridContainer.innerHTML = ''; if (!this.pokemonList) { this.gridContainer.innerHTML = '<div class="spinner"></div>'; fetch('https://pokeapi.co/api/v2/pokemon?limit=20000') .then(response => response.json()) .then(data => { this.pokemonList = data.results; this.renderBrowseGrid(); }) .catch(err => { this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading Pokémon list</div>`; }); } else { this.renderBrowseGrid(); } } renderBrowseGrid() { this.gridContainer.innerHTML = ''; this.gridContainer.classList.add('browse-container'); const query = this.searchInput.value.trim().toLowerCase(); const filtered = this.pokemonList.filter(poke => poke.name.includes(query)); filtered.forEach(poke => { const tile = document.createElement('div'); tile.className = 'browse-tile'; tile.dataset.label = poke.name.toLowerCase(); const idMatch = poke.url.match(/\/pokemon\/(\d+)\//); const id = idMatch ? idMatch[1] : ''; const img = document.createElement('img'); img.src = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`; const label = document.createElement('div'); label.className = 'tile-label'; label.textContent = poke.name; tile.append(img, label); tile.addEventListener('click', (e) => { e.stopPropagation(); this.panel.classList.add('active'); this.changeTab('advanced'); this.searchInput.value = poke.name; this.searchAdvancedPokemon(poke.name); }); this.gridContainer.appendChild(tile); }); if (filtered.length === 0) { this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">No Pokémon match your search.</div>`; } } addEventListeners() { this.button.addEventListener('mousedown', this.dragStart); this.button.addEventListener('click', (e) => { if (this.wasDragging) { this.wasDragging = false; return; } e.stopPropagation(); this.panel.classList.toggle('active'); if (this.panel.classList.contains('active')) { this.searchInput.focus(); } }); document.addEventListener('click', (e) => { if (!this.container.contains(e.target)) { this.panel.classList.remove('active'); } }); this.panel.addEventListener('dragstart', (e) => { const ballImg = e.target.closest('.pball-item img'); if (ballImg) { e.dataTransfer.setData('text/plain', ballImg.dataset.ballType); const dragImg = new Image(); dragImg.src = ballImg.src; dragImg.style.width = '36px'; dragImg.style.height = '36px'; dragImg.style.position = 'absolute'; dragImg.style.left = '-9999px'; document.body.appendChild(dragImg); e.dataTransfer.setDragImage(dragImg, 18, 18); setTimeout(() => document.body.removeChild(dragImg), 0); ballImg.classList.add('dragging'); const onDragEnd = () => { ballImg.classList.remove('dragging'); document.removeEventListener('dragend', onDragEnd); }; document.addEventListener('dragend', onDragEnd); } }); const chatInput = document.querySelector('#chatInput'); if (chatInput) { chatInput.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }); chatInput.addEventListener('drop', (e) => { e.preventDefault(); const ballType = e.dataTransfer.getData('text/plain'); if (ballType) { chatInput.value += ` ${ballType}`; } }); } const tabs = this.panel.querySelectorAll('.pball-tab'); tabs.forEach(tab => { tab.addEventListener('click', (e) => { e.stopPropagation(); this.changeTab(tab.dataset.tab); }); }); this.searchInput.addEventListener('input', () => { if (this.currentTab !== 'advanced') { this.filterGrid(); if (this.currentTab === 'browse') { this.renderBrowseGrid(); } } this.clearBtn.style.display = this.searchInput.value.trim() ? 'block' : 'none'; }); this.clearBtn.addEventListener('click', () => { this.searchInput.value = ''; this.clearBtn.style.display = 'none'; if (this.currentTab !== 'advanced') { this.filterGrid(); if (this.currentTab === 'browse') { this.renderBrowseGrid(); } } }); this.searchInput.addEventListener('keydown', (e) => { if (this.currentTab === 'advanced' && e.key === 'Enter') { this.searchAdvancedPokemon(this.searchInput.value.trim()); } }); } changeTab(tabName) { this.currentTab = tabName; const tabs = this.panel.querySelectorAll('.pball-tab'); tabs.forEach(tab => { tab.classList.toggle('active', tab.dataset.tab === tabName); }); if (tabName === 'advanced') { this.searchInput.placeholder = 'Enter Pokémon name for detailed info...'; } else if (tabName === 'browse') { this.searchInput.placeholder = 'Filter Pokémon...'; } else { this.searchInput.placeholder = 'Search...'; } this.searchInput.value = ''; this.clearBtn.style.display = 'none'; this.renderGrid(); } filterGrid() { const query = this.searchInput.value.trim().toLowerCase(); const items = this.gridContainer.querySelectorAll('.pball-item, .browse-tile'); items.forEach(item => { if (!query || item.dataset.label.includes(query)) { item.style.display = 'flex'; } else { item.style.display = 'none'; } }); } getChatInput() { return document.querySelector('[data-a-target="chat-input"]'); } insertCommand(ballType) { const chatInput = this.getChatInput(); if (!chatInput) return; chatInput.focus(); this.clearChatInput(); this.insertText(ballType); this.triggerInputEvent(chatInput); } clearChatInput() { const chatInput = this.getChatInput(); if (chatInput) { chatInput.value = ''; this.triggerInputEvent(chatInput); } } insertText(text) { document.execCommand('insertText', false, text); } triggerInputEvent(element) { element.dispatchEvent(new Event('input', { bubbles: true, composed: true })); } searchAdvancedPokemon(name) { if (!name) return; this.gridContainer.innerHTML = '<div class="spinner"></div>'; fetch(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`) .then(response => { if (!response.ok) { throw new Error("Pokémon not found"); } return response.json(); }) .then(data => { return fetch(data.species.url) .then(res => { if (!res.ok) { throw new Error("Species data not found"); } return res.json().then(speciesData => ({ data, speciesData })); }); }) .then(({ data, speciesData }) => { return fetch(speciesData.evolution_chain.url) .then(res => { if (!res.ok) { throw new Error("Evolution chain not found"); } return res.json().then(evoData => ({ data, speciesData, evoData })); }); }) .then(({ data, speciesData, evoData }) => { this.displayAdvancedPokemonData(data, speciesData, evoData); }) .catch(err => { this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">${err.message}</div>`; }); } displayAdvancedPokemonData(data, speciesData, evoData) { this.gridContainer.innerHTML = ''; const card = document.createElement('div'); card.className = 'poke-card'; // Constrain the card width to fit within the pop-up card.style.maxWidth = '100%'; // Ensure it doesn't exceed the pop-up width card.style.overflowX = 'hidden'; // Prevent horizontal scrolling // Header Section const header = this.createCardHeader(data); card.appendChild(header); // Single Column Layout for Advanced Tab card.style.display = 'flex'; card.style.flexDirection = 'column'; card.style.gap = '16px'; // Reduced gap for compact layout // Add sections in a single column card.append( this.createBasicInfoSection(data), this.createStatsRadarChart(data), this.createAbilitiesSection(data), this.createTypeRelationsGrid(data), this.createPokedexEntrySection(speciesData), this.createEvolutionVisualization(evoData.chain), this.createMovesSection(data) ); // Add Held Items and Forms sections if they exist if (data.held_items && data.held_items.length > 0) { card.appendChild(this.createHeldItemsSection(data)); } if (data.forms && data.forms.length > 0) { card.appendChild(this.createFormsSection(data)); } this.gridContainer.appendChild(card); } createCardHeader(data) { const header = document.createElement('header'); header.className = 'poke-card-header'; header.style.display = 'grid'; header.style.gridTemplateColumns = 'auto 1fr'; header.style.gap = '24px'; header.style.alignItems = 'center'; header.style.marginBottom = '24px'; // Image with Badge const imgContainer = document.createElement('div'); imgContainer.style.position = 'relative'; const img = document.createElement('img'); img.className = 'poke-image'; img.style.width = '90px'; img.style.height = '90px'; img.style.borderRadius = '16px'; img.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)'; img.src = data.sprites.other?.['official-artwork']?.front_default || data.sprites.front_default; // Type Badges const typeBadges = document.createElement('div'); typeBadges.style.display = 'flex'; typeBadges.style.gap = '4px'; typeBadges.style.position = 'absolute'; typeBadges.style.bottom = '-23px'; typeBadges.style.left = '55%'; typeBadges.style.transform = 'translateX(-50%)'; data.types.forEach(type => { const badge = document.createElement('span'); badge.className = 'type-badge'; badge.textContent = type.type.name.toUpperCase(); badge.style.background = this.getTypeColor(type.type.name); badge.style.padding = '3px 3px'; badge.style.borderRadius = '5px'; badge.style.fontSize = '12px'; badge.style.fontWeight = '700'; badge.style.color = '#fff'; badge.style.textShadow = '0 1px 2px rgba(0,0,0,0.3)'; typeBadges.appendChild(badge); }); imgContainer.append(img, typeBadges); // Title Section const titleSection = document.createElement('div'); const title = document.createElement('h1'); title.className = 'poke-title'; title.style.fontSize = '32px'; title.style.margin = '0 0 8px'; title.textContent = data.name.charAt(0).toUpperCase() + data.name.slice(1); const details = document.createElement('div'); details.style.display = 'grid'; details.style.gridTemplateColumns = 'repeat(3, auto)'; details.style.gap = '16px'; details.innerHTML = ` <div class="detail-item"> <span class="detail-label">ID</span> <span class="detail-value">#${data.id.toString().padStart(3, '0')}</span> </div> <div class="detail-item"> <span class="detail-label">EXP</span> <span class="detail-value">${data.base_experience}</span> </div> <div class="detail-item"> <span class="detail-label">SPECIES</span> <span class="detail-value">${data.species.name}</span> </div> `; titleSection.append(title, details); header.append(imgContainer, titleSection); return header; } createPokedexEntrySection(speciesData) { const section = document.createElement('div'); section.className = 'pokedex-entry-section'; section.innerHTML = `<h3 class="section-title">POKÉDEX ENTRY</h3>`; // Find the English flavor text const entry = speciesData.flavor_text_entries.find(e => e.language.name === 'en'); const flavorText = entry ? entry.flavor_text.replace(/\f|\n/g, ' ') : 'No entry available.'; // Create the entry container const entryContainer = document.createElement('div'); entryContainer.className = 'pokedex-entry'; entryContainer.style.background = 'var(--background-darker)'; entryContainer.style.padding = '16px'; entryContainer.style.borderRadius = '8px'; entryContainer.style.fontSize = '14px'; entryContainer.style.lineHeight = '1.5'; entryContainer.style.color = 'var(--text-muted)'; entryContainer.textContent = flavorText; section.appendChild(entryContainer); return section; } createBasicInfoSection(data) { const section = document.createElement('div'); section.className = 'info-grid'; section.innerHTML = ` <h3 class="section-title">PHYSICAL TRAITS</h3> <div class="metric"> <i class="icon-height"></i> <span class="label">Height</span> <span class="value">${data.height / 10}m</span> </div> <div class="metric"> <i class="icon-weight"></i> <span class="label">Weight</span> <span class="value">${data.weight / 10}kg</span> </div> <div class="metric"> <i class="icon-stats"></i> <span class="label">Total Stats</span> <span class="value">${data.stats.reduce((sum, s) => sum + s.base_stat, 0)}</span> </div> `; return section; } createAbilitiesSection(data) { const section = document.createElement('div'); section.className = 'abilities-section'; section.innerHTML = `<h3 class="section-title">ABILITIES</h3>`; const abilitiesGrid = document.createElement('div'); abilitiesGrid.className = 'abilities-grid'; abilitiesGrid.style.display = 'grid'; abilitiesGrid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(140px, 1fr))'; abilitiesGrid.style.gap = '12px'; data.abilities.forEach(ability => { const abilityCard = document.createElement('div'); abilityCard.className = 'ability-card'; abilityCard.style.background = 'var(--background-darker)'; abilityCard.style.padding = '12px'; abilityCard.style.borderRadius = '8px'; abilityCard.style.textAlign = 'center'; abilityCard.style.position = 'relative'; const abilityName = document.createElement('div'); abilityName.textContent = ability.ability.name.replace(/-/g, ' '); abilityName.style.fontWeight = '500'; abilityName.style.textTransform = 'capitalize'; if (ability.is_hidden) { const hiddenBadge = document.createElement('div'); hiddenBadge.textContent = 'Hidden'; hiddenBadge.style.position = 'absolute'; hiddenBadge.style.top = '4px'; hiddenBadge.style.right = '4px'; hiddenBadge.style.background = '#FF6B6B'; hiddenBadge.style.color = '#FFF'; hiddenBadge.style.fontSize = '10px'; hiddenBadge.style.padding = '2px 6px'; hiddenBadge.style.borderRadius = '12px'; abilityCard.appendChild(hiddenBadge); } abilityCard.appendChild(abilityName); abilitiesGrid.appendChild(abilityCard); // Add tooltip for ability description abilityCard.addEventListener('mouseenter', () => { fetch(ability.ability.url) .then(res => res.json()) .then(abilityData => { const description = abilityData.effect_entries.find(e => e.language.name === 'en')?.effect || 'No description available.'; this.showTooltip(abilityCard, description); }); }); abilityCard.addEventListener('mouseleave', () => { this.hideTooltip(); }); }); section.appendChild(abilitiesGrid); return section; } showTooltip(element, text) { if (this.tooltip) this.tooltip.remove(); this.tooltip = document.createElement('div'); this.tooltip.className = 'tooltip'; this.tooltip.textContent = text; // Calculate position with viewport boundary checks const rect = element.getBoundingClientRect(); const viewportHeight = window.innerHeight; const tooltipHeight = 100; // Estimated height let topPosition = rect.bottom + 8; if (topPosition + tooltipHeight > viewportHeight) { topPosition = rect.top - tooltipHeight - 8; } Object.assign(this.tooltip.style, { background: 'var(--background-dark)', color: 'var(--text-light)', borderRadius: '6px', padding: '8px 12px', position: 'fixed', top: `${topPosition}px`, left: `${rect.left}px`, maxWidth: '240px', zIndex: '10000', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)', pointerEvents: 'none' }); document.body.appendChild(this.tooltip); } hideTooltip() { if (this.tooltip) { this.tooltip.remove(); this.tooltip = null; } } createMovesSection(data) { const section = document.createElement('div'); section.className = 'moves-section'; section.innerHTML = `<h3 class="section-title">MOVES</h3>`; // Search container with unified styling const searchContainer = document.createElement('div'); searchContainer.className = 'pball-search-container'; const searchInput = document.createElement('input'); searchInput.className = 'pball-search'; searchInput.placeholder = 'Search moves...'; const searchButtons = document.createElement('div'); searchButtons.className = 'search-buttons'; const enterButton = document.createElement('button'); enterButton.className = 'search-enter-button'; enterButton.textContent = '✔'; const clearButton = document.createElement('button'); clearButton.className = 'pball-clear-btn'; clearButton.textContent = 'X'; // Event handling with debounce let searchTimeout; const handleSearch = () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { const query = searchInput.value.trim().toLowerCase(); Array.from(movesList.children).forEach(move => { move.style.display = move.textContent.toLowerCase().includes(query) ? 'block' : 'none'; }); }, 300); }; searchInput.addEventListener('input', handleSearch); enterButton.addEventListener('click', handleSearch); clearButton.addEventListener('click', () => { searchInput.value = ''; handleSearch(); }); // Assembly searchButtons.append(enterButton, clearButton); searchContainer.append(searchInput, searchButtons); section.append(searchContainer); // Moves list with virtual scroll const movesList = document.createElement('div'); movesList.className = 'moves-list'; movesList.style.maxHeight = '200px'; movesList.style.overflowY = 'auto'; movesList.style.display = 'grid'; movesList.style.gap = '8px'; data.moves.forEach(move => { const moveItem = document.createElement('div'); moveItem.className = 'move-item'; moveItem.textContent = move.move.name.replace(/-/g, ' '); moveItem.style.padding = '8px 12px'; moveItem.style.background = 'var(--background-darker)'; moveItem.style.borderRadius = '6px'; moveItem.style.textTransform = 'capitalize'; movesList.appendChild(moveItem); }); searchInput.addEventListener('input', () => { const query = searchInput.value.trim().toLowerCase(); Array.from(movesList.children).forEach(move => { move.style.display = move.textContent.toLowerCase().includes(query) ? 'block' : 'none'; }); }); section.append(searchInput, movesList); return section; } createHeldItemsSection(data) { const section = document.createElement('div'); section.className = 'held-items-section'; section.innerHTML = `<h3 class="section-title">HELD ITEMS</h3>`; const itemsGrid = document.createElement('div'); itemsGrid.className = 'items-grid'; itemsGrid.style.display = 'grid'; itemsGrid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(120px, 1fr))'; itemsGrid.style.gap = '12px'; data.held_items.forEach(item => { const itemCard = document.createElement('div'); itemCard.className = 'item-card'; itemCard.textContent = item.item.name.replace(/-/g, ' '); itemCard.style.padding = '12px'; itemCard.style.background = 'var(--background-darker)'; itemCard.style.borderRadius = '8px'; itemCard.style.textAlign = 'center'; itemCard.style.textTransform = 'capitalize'; itemsGrid.appendChild(itemCard); }); section.appendChild(itemsGrid); return section; } createFormsSection(data) { const section = document.createElement('div'); section.className = 'forms-section'; section.innerHTML = `<h3 class="section-title">FORMS</h3>`; const formsGrid = document.createElement('div'); formsGrid.className = 'forms-grid'; formsGrid.style.display = 'grid'; formsGrid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(120px, 1fr))'; formsGrid.style.gap = '12px'; data.forms.forEach(form => { const formCard = document.createElement('div'); formCard.className = 'form-card'; formCard.textContent = form.name.replace(/-/g, ' '); formCard.style.padding = '12px'; formCard.style.background = 'var(--background-darker)'; formCard.style.borderRadius = '8px'; formCard.style.textAlign = 'center'; formCard.style.textTransform = 'capitalize'; formsGrid.appendChild(formCard); }); section.appendChild(formsGrid); return section; } createStatsRadarChart(data) { const section = document.createElement('div'); section.className = 'stats-radar-card'; section.innerHTML = ` <div class="stats-header"> <h3 class="section-title">Stat Distribution</h3> <div class="stats-summary"> <span class="total-stats">Total: ${data.stats.reduce((sum, s) => sum + s.base_stat, 0)}</span> <div class="type-badge" style="background: ${this.getTypeColor(data.types[0].type.name)}"> ${data.types[0].type.name.toUpperCase()} </div> </div> </div> `; // Chart container with aspect ratio constraints const chartContainer = document.createElement('div'); chartContainer.className = 'radar-container'; chartContainer.style.position = 'relative'; chartContainer.style.height = 'clamp(280px, 35vh, 400px)'; chartContainer.style.margin = '16px 0'; const canvas = document.createElement('canvas'); canvas.setAttribute('aria-label', 'Pokémon stat radar chart'); canvas.style.touchAction = 'none'; // Dynamic gradient based on Pokémon type const typeColor = this.getTypeColor(data.types[0].type.name); const gradient = { light: this.hexToRgba(typeColor, 0.3), dark: this.hexToRgba(typeColor, 0.1) }; // Chart.js loader with error handling if (!window.Chart) { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/chart.js'; script.onload = () => this.drawEnhancedRadar(canvas, data, gradient); script.onerror = () => this.showChartError(chartContainer); document.head.appendChild(script); } else { this.drawEnhancedRadar(canvas, data, gradient); } chartContainer.appendChild(canvas); section.appendChild(chartContainer); return section; } drawEnhancedRadar(canvas, data, gradient) { try { const ctx = canvas.getContext('2d'); const stats = data.stats.map(s => s.base_stat); const labels = data.stats.map(s => ({ full: s.stat.name.replace(/-/g, ' '), short: this.getStatAbbreviation(s.stat.name) })); // Create gradient fill const chartGradient = ctx.createLinearGradient(0, 0, 0, canvas.height); chartGradient.addColorStop(0, gradient.light); chartGradient.addColorStop(1, gradient.dark); new Chart(ctx, { type: 'radar', data: { labels: labels.map(l => l.short), datasets: [{ data: stats, backgroundColor: chartGradient, borderColor: this.hexToRgba(gradient.light, 0.8), borderWidth: 1.8, pointBackgroundColor: '#ffffff', pointBorderColor: gradient.light, pointHoverRadius: 8, pointRadius: 4, pointHitRadius: 12, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 800, easing: 'easeOutQuint' }, scales: { r: { beginAtZero: true, max: Math.ceil(Math.max(...stats) / 10) * 10 + 10, ticks: { display: false, count: 5, z: 1 }, grid: { color: 'rgba(255, 255, 255, 0.12)', circular: true, lineWidth: 0.8 }, pointLabels: { color: '#ffffff', font: { size: 13, weight: '500' }, callback: (value, index) => [`${value}`, stats[index]], padding: 18 }, angleLines: { color: 'rgba(255, 255, 255, 0.08)', lineWidth: 0.8 } } }, plugins: { legend: { display: false }, tooltip: { enabled: true, intersect: false, callbacks: { title: (items) => labels[items[0].dataIndex].full, label: (context) => `Base Stat: ${context.raw}` }, bodyFont: { size: 13 }, titleFont: { size: 12 }, padding: 14, backgroundColor: 'rgba(28, 28, 34, 0.96)', borderColor: 'rgba(255, 255, 255, 0.12)', borderWidth: 1, cornerRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.24)' }, annotation: { annotations: { avgLine: { type: 'line', borderColor: 'rgba(255, 255, 255, 0.2)', borderWidth: 1, borderDash: [4, 4], scaleID: 'r', value: stats.reduce((a, b) => a + b, 0) / stats.length } } } }, onHover: (event, elements) => { canvas.style.cursor = elements.length ? 'pointer' : 'default'; } } }); } catch (error) { this.showChartError(canvas.parentElement); } } // Helper methods hexToRgba(hex, alpha = 1) { const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16)); return `rgba(${r},${g},${b},${alpha})`; } getStatAbbreviation(statName) { const abbreviations = { 'hp': 'HP', 'attack': 'ATK', 'defense': 'DEF', 'special-attack': 'SP.ATK', 'special-defense': 'SP.DEF', 'speed': 'SPD' }; return abbreviations[statName] || statName.slice(0, 3).toUpperCase(); } showChartError(container) { container.innerHTML = ` <div class="chart-error"> <svg class="error-icon" viewBox="0 0 24 24" width="48" height="48"> <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/> </svg> <div class="error-message"> <h4>Chart Unavailable</h4> <p>Failed to load stat visualization</p> </div> </div> `; } createTypeRelationsGrid(data) { const section = document.createElement('div'); section.className = 'type-relations-grid'; section.innerHTML = `<h3 class="section-title">TYPE INTERACTIONS</h3>`; const grid = document.createElement('div'); grid.style.display = 'grid'; grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(160px, 1fr))'; grid.style.gap = '12px'; data.types.forEach(type => { const typeCard = document.createElement('div'); typeCard.className = 'type-card'; typeCard.innerHTML = ` <div class="type-header">${type.type.name.toUpperCase()}</div> <div class="damage-relations"> <div class="strengths"> <h4>STRONG VS</h4> <div class="types-list"></div> </div> <div class="weaknesses"> <h4>WEAK TO</h4> <div class="types-list"></div> </div> </div> `; // Add this block to target the header specifically const typeHeader = typeCard.querySelector('.type-header'); typeHeader.style.background = this.getTypeColor(type.type.name); // Async load damage relations fetch(type.type.url) .then(res => res.json()) .then(typeData => { const strengths = typeData.damage_relations.double_damage_to; const weaknesses = typeData.damage_relations.double_damage_from; strengths.forEach(t => { const badge = this.createTypeBadge(t.name); typeCard.querySelector('.strengths .types-list').appendChild(badge); }); weaknesses.forEach(t => { const badge = this.createTypeBadge(t.name); typeCard.querySelector('.weaknesses .types-list').appendChild(badge); }); }); grid.appendChild(typeCard); }); section.appendChild(grid); return section; } createTypeBadge(typeName) { const badge = document.createElement('span'); badge.className = 'type-badge small'; badge.textContent = typeName.toUpperCase(); badge.style.background = this.getTypeColor(typeName); badge.style.padding = '2px 8px'; badge.style.borderRadius = '12px'; badge.style.fontSize = '10px'; return badge; } getTypeColor(typeName) { const typeColors = { normal: '#A8A878', fire: '#F08030', water: '#6890F0', electric: '#F8D030', grass: '#78C850', ice: '#98D8D8', fighting: '#C03028', poison: '#A040A0', ground: '#E0C068', flying: '#A890F0', psychic: '#F85888', bug: '#A8B820', rock: '#B8A038', ghost: '#705898', dragon: '#7038F8', dark: '#705848', steel: '#B8B8D0', fairy: '#EE99AC' }; return typeColors[typeName] || '#68A090'; } createEvolutionVisualization(chain) { const section = document.createElement('div'); section.className = 'evolution-chain'; section.innerHTML = `<h3 class="section-title">EVOLUTION LINE</h3>`; const stages = this.parseEvolutionChain(chain); const container = document.createElement('div'); container.style.display = 'flex'; container.style.justifyContent = 'center'; container.style.gap = '0px'; container.style.padding = '16px 0'; stages.forEach((stage, index) => { const stageDiv = document.createElement('div'); stageDiv.style.display = 'flex'; stageDiv.style.flexDirection = 'column'; stageDiv.style.alignItems = 'center'; stageDiv.style.gap = '8px'; if (index > 0) { const arrow = document.createElement('div'); arrow.textContent = '→'; arrow.style.fontSize = '24px'; arrow.style.opacity = '0.6'; container.appendChild(arrow); } const sprite = document.createElement('img'); sprite.src = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${stage.id}.png`; sprite.style.width = '64px'; sprite.style.height = '64px'; const name = document.createElement('div'); name.textContent = stage.name; name.style.fontWeight = '500'; stageDiv.append(sprite, name); container.appendChild(stageDiv); }); section.appendChild(container); return section; } parseEvolutionChain(chain, result = []) { const id = chain.species.url.split('/').slice(-2, -1)[0]; result.push({ name: chain.species.name, id }); if (chain.evolves_to.length > 0) { chain.evolves_to.forEach(e => this.parseEvolutionChain(e, result)); } return result.filter((v, i, a) => a.findIndex(t => t.id === v.id) === i); } } new PokeballHelper(); })();