您需要先安装一个扩展,例如 篡改猴、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 10 // @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://cdn.discordapp.com/attachments/1095453488684744786/1344388523750457354/pngwing.com_13.png?ex=67c0bae1&is=67bf6961&hm=ef378cc914ec3d785094e9a21690c377fbc3d5187ee243ae0d9d21b522ece867&' }, check: { command: '!pokecheck', tooltip: 'Poke Check', image: 'https://cdn.discordapp.com/attachments/1095453488684744786/1344383577323995168/pngwing.com_2.png?ex=67c0b646&is=67bf64c6&hm=71bdf7b7a547df375849fa3874370067476fad556422dd16df77d6beba710a90&' }, poke: { command: '!pokecatch', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png' }, great: { command: '!pokecatch greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png' }, ultra: { command: '!pokecatch ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png' }, master: { command: '!pokecatch masterball', tooltip: 'Master Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/master_ball.png' }, premier: { command: '!pokecatch premierball', tooltip: 'Premier Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/premier_ball.png' }, cherish: { command: '!pokecatch cherishball', tooltip: 'Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cherish_ball.png' }, greatCherish: { command: '!pokecatch greatcherishball', tooltip: 'Great Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_cherish_ball.png' }, ultraCherish: { command: '!pokecatch ultracherishball', tooltip: 'Ultra Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_cherish_ball.png' }, heavy: { command: '!pokecatch heavyball', tooltip: 'Heavy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heavy_ball.png' }, feather: { command: '!pokecatch featherball', tooltip: 'Feather Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/feather_ball.png' }, timer: { command: '!pokecatch timerball', tooltip: 'Timer Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/timer_ball.png' }, quick: { command: '!pokecatch quickball', tooltip: 'Quick Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/quick_ball.png' }, nest: { command: '!pokecatch nestball', tooltip: 'Nest Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/nest_ball.png' }, fast: { command: '!pokecatch fastball', tooltip: 'Fast Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fast_ball.png' }, heal: { command: '!pokecatch healball', tooltip: 'Heal Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heal_ball.png' }, repeat: { command: '!pokecatch repeatball', tooltip: 'Repeat Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/repeat_ball.png' }, friend: { command: '!pokecatch friendball', tooltip: 'Friend Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/friend_ball.png' }, frozen: { command: '!pokecatch frozenball', tooltip: 'Frozen Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/frozen_ball.png' }, night: { command: '!pokecatch nightball', tooltip: 'Night Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/night_ball.png' }, phantom: { command: '!pokecatch phantomball', tooltip: 'Phantom Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/phantom_ball.png' }, cipher: { command: '!pokecatch cipherball', tooltip: 'Cipher Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cipher_ball.png' }, magnet: { command: '!pokecatch magnetball', tooltip: 'Magnet Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/magnet_ball.png' }, net: { command: '!pokecatch netball', tooltip: 'Net Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/net_ball.png' }, luxury: { command: '!pokecatch luxuryball', tooltip: 'Luxury Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/luxury_ball.png' }, stone: { command: '!pokecatch stoneball', tooltip: 'Stone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/stone_ball.png' }, level: { command: '!pokecatch levelball', tooltip: 'Level Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/level_ball.png' }, clone: { command: '!pokecatch cloneball', tooltip: 'Clone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/clone_ball.png' }, sun: { command: '!pokecatch sunball', tooltip: 'Sun Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/sun_ball.png' }, fantasy: { command: '!pokecatch fantasyball', tooltip: 'Fantasy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fantasy_ball.png' }, mach: { command: '!pokecatch machball', tooltip: 'Mach Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/mach_ball.png' }, dive: { command: '!pokecatch diveball', tooltip: 'Dive Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/dive_ball.png' } }; 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(); } init() { this.setupStyles(); this.waitForChat().then(() => { this.createInterface(); this.createTimerElement(); this.addEventListeners(); this.renderGrid(); this.updateSpawnTimer(); }); } loadPosition() { const savedPos = localStorage.getItem('pballPosition'); if (savedPos) { const { x, y } = JSON.parse(savedPos); this.container.style.left = `${x}px`; this.container.style.top = `${y}px`; } } dragStart(e) { e.preventDefault(); this.wasDragging = false; 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 */ /*-------------------------------------------------- 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 { /* Pball UI Variables */ --background-dark: #18181b; --background-darker: #2e2e35; --card-background: #1f1f26; --pball-border-color: #3e3e45; --pball-highlight-color: #76c7c0; --pball-highlight-gradient: linear-gradient(90deg, var(--pball-highlight-color), #4db6ac); --text-light: #ffffff; --text-muted: #ccc; --font-family: 'Roboto', sans-serif; --glass-effect: rgba(255, 255, 255, 0.1); --scrollbar-track: var(--background-darker); --scrollbar-thumb: var(--pball-border-color); --scrollbar-thumb-hover: #555; /* Timer UI Variables */ --timer-color-led: #00ff37; --timer-color-low: #ff4040; --timer-font-led: 'Segment7Standard', monospace; --timer-font-label: 'Press Start 2P', cursive; --timer-label-text-color: #FFE135; --timer-transition-speed: 0.3s; --timer-font-size-label: 0.75rem; /* 12px */ --timer-font-size-countdown: 2rem; /* 32px */ } /* Global & Scrollbar Styles */ .pball-container, .pball-container * { font-family: var(--font-family); box-sizing: border-box; } ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 8px; } ::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 8px; border: 1px solid var(--background-dark); } ::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); } /*-------------------------------------------------- Pball Container & Components --------------------------------------------------*/ /* Main Container (Fixed Position) */ .pball-container { position: fixed; right: 12px; bottom: 95px; z-index: 10000; user-select: none; transform: scale(1); transform-origin: top left; width: fit-content; height: fit-content; pointer-events: none; } /* Allow interactions for designated children */ .pball-container > .pball-button, .pball-panel.active { pointer-events: auto; } /* Interactive Button */ .pball-button { cursor: pointer; width: 50px; height: 50px; border-radius: 50%; border: 2px solid var(--pball-border-color); background: var(--background-dark); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); transition: transform 0.2s ease, box-shadow 0.2s ease; } .pball-button:hover { transform: scale(1.3); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6); } /* Floating Panel */ .pball-panel { position: absolute; bottom: calc(100% + 10px); right: 0; width: 320px; background: var(--background-dark); border-radius: 16px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); border: 1px solid var(--glass-effect); opacity: 0; visibility: hidden; transform: translateY(20px); transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s; } .pball-panel.active { opacity: 1; visibility: visible; transform: translateY(0); } /* Tab System */ .pball-tabs { display: flex; background: var(--background-darker); border-bottom: 1px solid var(--pball-border-color); } .pball-tabs .pball-tab { flex: 1; padding: 10px; text-align: center; font-size: 16px; cursor: pointer; color: var(--text-muted); transition: background 0.2s ease, color 0.2s ease; } .pball-tabs .pball-tab.active, .pball-tabs .pball-tab:hover { background: var(--pball-border-color); color: var(--text-light); } /* Search Components */ .pball-search-container { position: relative; margin: 12px; } .pball-search-container .pball-search { width: 100%; padding: 8px 36px 8px 12px; border: 1px solid var(--pball-border-color); border-radius: 8px; background: var(--background-dark); color: var(--text-light); font-size: 15px; outline: none; transition: border-color 0.2s ease; } .pball-search-container .pball-search:focus { border-color: var(--pball-highlight-color); } .pball-search-container .pball-search::placeholder { color: var(--text-muted); } .pball-search-container .pball-clear-btn { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: transparent; border: none; color: var(--text-muted); font-size: 18px; cursor: pointer; display: none; } /* Grid Layouts for Items */ .pball-grid { padding: 12px; display: grid; gap: 12px; max-height: 300px; overflow-y: auto; } .pball-grid.ball-items { grid-template-columns: repeat(3, 1fr); } .pball-grid.search-results { grid-template-columns: 1fr; } /* Item Card */ .pball-item { display: flex; flex-direction: column; align-items: center; justify-content: center; background: transparent; border: none; border-radius: 50%; padding: 8px; cursor: default; } .pball-item img { pointer-events: auto; user-select: none; cursor: grab; width: 36px; height: 36px; transition: transform 0.2s ease; } .pball-item img:hover { transform: scale(1.3); } .pball-item img.dragging { opacity: 0.6; transform: scale(0.8); filter: drop-shadow(0 0 4px rgba(118, 199, 192, 0.5)); } .pball-item .pball-label { margin-top: 6px; font-size: 13px; color: var(--text-light); text-align: center; } /* Browse Tab Components */ .browse-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; padding: 16px; } .browse-tile { display: flex; flex-direction: column; align-items: center; background: var(--background-darker); border: 1px solid var(--pball-border-color); border-radius: 12px; padding: 12px; transition: transform 0.2s ease, box-shadow 0.2s ease; cursor: pointer; text-align: center; } .browse-tile:hover { transform: translateY(-3px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } .browse-tile img { width: 64px; height: 64px; margin-bottom: 6px; } .browse-tile .tile-label { font-size: 14px; font-weight: 500; color: var(--text-light); text-transform: capitalize; } /* Advanced Card Components */ .poke-card { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; } .poke-card .section-title { font-size: 20px; font-weight: 700; margin-bottom: 12px; color: var(--text-light); } .poke-card .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; } /* Evolution Chain */ .evolution-chain { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; padding: 8px; } .evolution-chain .evolution-item { text-align: center; padding: 6px; background: var(--background-darker); border-radius: 8px; transition: transform 0.2s ease; margin-bottom: 4px; } .evolution-chain .evolution-item img { width: 64px; height: 64px; object-fit: contain; margin-bottom: 4px; } .evolution-chain .evolution-item p { font-size: 12px; color: var(--text-light); margin: 0; } /* Moves Section */ .moves-section { max-height: 300px; overflow-y: auto; } /*-------------------------------------------------- Timer Container & Components --------------------------------------------------*/ /* Timer Container with Glassmorphism & Grid Layout */ .spawn-timer { position: absolute; bottom: calc(100% - 4.7rem); right: 4.8rem; padding: 0.75rem 1rem; transform: perspective(500px) rotateX(5deg); z-index: 10001; display: flex; /* Changed from grid to flex */ align-items: center; /* Vertical alignment */ gap: 1rem; /* Space between elements */ transition: transform var(--timer-transition-speed) ease; } .spawn-timer:hover { transform: perspective(500px) rotateX(0deg); } .spawn-timer::before { content: ''; position: absolute; top: -0.5rem; left: -0.5rem; right: -0.5rem; bottom: -0.5rem; pointer-events: none; } /* Timer Header: Centered Layout */ .timer-header { display: flex; grid-template-columns: 1fr; justify-items: center; align-items: center; gap: 0.25rem; width: 100%; } /* Timer Label with Elegant Typography */ .timer-label { font-family: var(--timer-font-label); font-size: var(--timer-font-size-label); color: var(--timer-label-text-color); text-transform: uppercase; letter-spacing: 0.09375rem; padding: 0.375rem 0.625rem; border-radius: 0.375rem; border: 0.0625rem solid rgba(58, 90, 109, 0.4); background: rgba(255, 255, 255, 0.05); text-shadow: 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.4), 0 0.5rem 0.5rem rgba(148, 180, 196, 0.3); transition: background var(--timer-transition-speed) ease; } .timer-label:hover { background: rgba(255, 255, 255, 0.15); } /* Countdown Display (Neon & Interactive) */ .countdown-display { font-family: var(--timer-font-led); font-size: var(--timer-font-size-countdown); font-weight: bold; color: var(--timer-color-led); padding: 0.375rem 0.75rem; border-radius: 0.375rem; letter-spacing: 0.125rem; 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.375rem 0.75rem rgba(0, 255, 55, 0.4), inset 0 0.25rem 0.5rem rgba(0, 255, 55, 0.3); position: relative; line-height: 1; transition: transform var(--timer-transition-speed) ease, box-shadow var(--timer-transition-speed) ease; } .countdown-display:hover { transform: scale(1.05); box-shadow: 0 0.5rem 1rem rgba(0, 255, 55, 0.6), inset 0 0.375rem 0.75rem rgba(0, 255, 55, 0.4); } .countdown-display::before { content: ''; position: absolute; inset: 0; background: radial-gradient(circle, rgba(0, 255, 55, 0.2) 0%, transparent 70%); border-radius: inherit; z-index: -1; transition: opacity var(--timer-transition-speed) ease; } .countdown-display::after { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; z-index: -2; } /* Low Time Warning (Enhanced Neon Pulse) */ .countdown-display.low-time { color: var(--timer-color-low); text-shadow: 0 0.5rem 0.75rem rgba(255, 64, 64, 0.8), 0 0.25rem 0.375rem 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.75rem 1rem rgba(255, 64, 64, 1); } } @keyframes emergency-glow { 0%, 100% { box-shadow: 0 0.375rem 0.75rem rgba(255, 64, 64, 0.3); } 50% { box-shadow: 0 0.75rem 1rem rgba(255, 64, 64, 0.6); } } /* Utility: Spinner Animation */ .spinner { margin: 1.5rem auto; border: 0.25rem solid var(--pball-border-color); border-top: 0.25rem solid var(--pball-highlight-color); border-radius: 50%; width: 2.5rem; height: 2.5rem; 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%; } } /* Responsive adjustments */ @media (max-width: 600px) { .spawn-timer { gap: 0.75rem; /* Reduced gap for mobile */ padding: 0.5rem 0.75rem; } .timer-label { font-size: 0.6rem; /* Slightly smaller text */ padding: 0.2rem 0.4rem; } .countdown-display { font-size: 1.5rem; /* Slightly smaller countdown */ 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.clearBtn); this.gridContainer = document.createElement('div'); this.gridContainer.className = 'pball-grid'; panel.append(tabsContainer, searchContainer, this.gridContainer); return panel; } createTimerElement() { // Create and insert timer element 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() { // Save references and default values this.backendUrl = 'https://poketwitch.bframework.de/info/events/last_spawn/'; this.countdownElement = this.timerContainer.querySelector('.countdown-display'); this.remainingSeconds = 0; this.tickInterval = null; this.apiInterval = null; this.isFetching = false; // Start the per-second countdown this.startTick(); // Get the initial time from the API this.fetchTimer(); // Update the timer from the API every minute this.apiInterval = setInterval(() => { this.fetchTimer(); }, 60000); // Emergency refresh on click (ignoring if a fetch is already in progress) this.timerContainer.addEventListener('click', () => { if (!this.isFetching) { this.fetchTimer(); } }); } startTick() { // Clear any existing tick interval if (this.tickInterval) clearInterval(this.tickInterval); // Update the display every second using the local remainingSeconds counter this.tickInterval = setInterval(() => { if (this.remainingSeconds > 0) { this.remainingSeconds--; this.updateDisplay(this.remainingSeconds); } else { // If timer has run out, you might decide to call fetchTimer() here to refresh this.updateDisplay(0); } }, 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')}`; // Toggle warning state when time is low (<= 30 seconds) this.countdownElement.classList.toggle('low-time', seconds <= 30); } fetchTimer() { // Prevent overlapping API calls 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 the remaining seconds with the new value from the backend. 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; this.tooltip.style.position = 'absolute'; this.tooltip.style.background = 'var(--background-dark)'; this.tooltip.style.color = 'var(--text-light)'; this.tooltip.style.padding = '8px 12px'; this.tooltip.style.borderRadius = '6px'; this.tooltip.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)'; this.tooltip.style.zIndex = '10000'; this.tooltip.style.maxWidth = '240px'; this.tooltip.style.fontSize = '14px'; const rect = element.getBoundingClientRect(); this.tooltip.style.top = `${rect.bottom + window.scrollY + 8}px`; this.tooltip.style.left = `${rect.left + window.scrollX}px`; 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>`; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Search moves...'; searchInput.style.width = '100%'; searchInput.style.padding = '8px 12px'; searchInput.style.marginBottom = '12px'; searchInput.style.borderRadius = '6px'; searchInput.style.background = 'var(--background-darker)'; searchInput.style.color = 'var(--text-light)'; searchInput.style.border = '1px solid var(--border-color)'; 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(); })();