您需要先安装一个扩展,例如 篡改猴、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 9 // @description Twitch Poké Ball Helper with a three-column grid for Catch/Shop plus two distinct lookup tabs: a visually rich Browse tab and a detailed Advanced tab featuring a full-width Pokémon info card with Pokédex entry and evolution chain. All styled with advanced UI techniques and a unified Roboto font. Shoutout doubleupmafia @doubleupmolly @doubleuplowlow219 @doubleupap @doubleupeazy @musiclov3r1435 // @author // @match https://www.twitch.tv/* // @icon https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png // @grant none // ==/UserScript== (function () { 'use strict'; class PokeballHelper { constructor() { // Define balls for the Catch tab this.catchBalls = { dollars: { command: '$', tooltip: 'Poke Dollars', image: 'https://cdn.discordapp.com/attachments/1095453488684744786/1344388523750457354/pngwing.com_13.png?ex=67c0bae1&is=67bf6961&hm=ef378cc914ec3d785094e9a21690c377fbc3d5187ee243ae0d9d21b522ece867&' }, check: { command: '!pokecheck', tooltip: 'Poke Check', image: 'https://cdn.discordapp.com/attachments/1095453488684744786/1344383577323995168/pngwing.com_2.png?ex=67c0b646&is=67bf64c6&hm=71bdf7b7a547df375849fa3874370067476fad556422dd16df77d6beba710a90&' }, poke: { command: '!pokecatch', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png' }, great: { command: '!pokecatch greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png' }, ultra: { command: '!pokecatch ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png' }, master: { command: '!pokecatch masterball', tooltip: 'Master Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/master_ball.png' }, premier: { command: '!pokecatch premierball', tooltip: 'Premier Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/premier_ball.png' }, cherish: { command: '!pokecatch cherishball', tooltip: 'Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cherish_ball.png' }, greatCherish: { command: '!pokecatch greatcherishball', tooltip: 'Great Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_cherish_ball.png' }, ultraCherish: { command: '!pokecatch ultracherishball', tooltip: 'Ultra Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_cherish_ball.png' }, heavy: { command: '!pokecatch heavyball', tooltip: 'Heavy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heavy_ball.png' }, feather: { command: '!pokecatch featherball', tooltip: 'Feather Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/feather_ball.png' }, timer: { command: '!pokecatch timerball', tooltip: 'Timer Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/timer_ball.png' }, quick: { command: '!pokecatch quickball', tooltip: 'Quick Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/quick_ball.png' }, nest: { command: '!pokecatch nestball', tooltip: 'Nest Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/nest_ball.png' }, fast: { command: '!pokecatch fastball', tooltip: 'Fast Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fast_ball.png' }, heal: { command: '!pokecatch healball', tooltip: 'Heal Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heal_ball.png' }, repeat: { command: '!pokecatch repeatball', tooltip: 'Repeat Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/repeat_ball.png' }, friend: { command: '!pokecatch friendball', tooltip: 'Friend Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/friend_ball.png' }, frozen: { command: '!pokecatch frozenball', tooltip: 'Frozen Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/frozen_ball.png' }, night: { command: '!pokecatch nightball', tooltip: 'Night Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/night_ball.png' }, phantom: { command: '!pokecatch phantomball', tooltip: 'Phantom Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/phantom_ball.png' }, cipher: { command: '!pokecatch cipherball', tooltip: 'Cipher Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cipher_ball.png' }, magnet: { command: '!pokecatch magnetball', tooltip: 'Magnet Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/magnet_ball.png' }, net: { command: '!pokecatch netball', tooltip: 'Net Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/net_ball.png' }, luxury: { command: '!pokecatch luxuryball', tooltip: 'Luxury Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/luxury_ball.png' }, stone: { command: '!pokecatch stoneball', tooltip: 'Stone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/stone_ball.png' }, level: { command: '!pokecatch levelball', tooltip: 'Level Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/level_ball.png' }, clone: { command: '!pokecatch cloneball', tooltip: 'Clone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/clone_ball.png' }, sun: { command: '!pokecatch sunball', tooltip: 'Sun Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/sun_ball.png' }, fantasy: { command: '!pokecatch fantasyball', tooltip: 'Fantasy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fantasy_ball.png' }, mach: { command: '!pokecatch machball', tooltip: 'Mach Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/mach_ball.png' }, dive: { command: '!pokecatch diveball', tooltip: 'Dive Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/dive_ball.png' } }; // Define balls for the Shop tab this.shopBalls = { pokeball: { command: '!pokeshop pokeball', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png' }, great: { command: '!pokeshop greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png' }, ultra: { command: '!pokeshop ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png' } }; this.currentTab = 'catch'; this.allPokemonList = null; // Drag functionality properties this.isDragging = false; this.startX = 0; this.startY = 0; this.containerStartLeft = 0; this.containerStartTop = 0; this.wasDragging = false; // Flag to differentiate click vs drag // Bind drag methods this.dragStart = this.dragStart.bind(this); this.drag = this.drag.bind(this); this.dragEnd = this.dragEnd.bind(this); this.init(); } init() { this.setupStyles(); this.waitForChat().then(() => { this.createInterface();// Creates the container with both button and timer this.createTimerElement();// Creates the timer element inside the container this.addEventListeners(); this.renderGrid(); this.updateSpawnTimer(); }); } loadPosition() { const savedPos = localStorage.getItem('pballPosition'); if (savedPos) { const { x, y } = JSON.parse(savedPos); this.container.style.left = `${x}px`; this.container.style.top = `${y}px`; } } dragStart = (e) => { e.preventDefault(); this.wasDragging = false; // Remove fixed positioning so left/top can be used this.container.style.right = ''; this.container.style.bottom = ''; const startX = e.clientX; const startY = e.clientY; const rect = this.container.getBoundingClientRect(); const origLeft = rect.left; const origTop = rect.top; const onMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - startX; const deltaY = moveEvent.clientY - startY; if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { this.wasDragging = true; } this.container.style.left = `${origLeft + deltaX}px`; this.container.style.top = `${origTop + deltaY}px`; }; const onMouseUp = () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); // Reset the cursor style after drag completes const ballImg = e.target.closest('.pball-item img'); if (ballImg) { ballImg.style.cursor = 'grab'; } }; // Update the dragged element's cursor style during dragging const ballImg = e.target.closest('.pball-item img'); if (ballImg) { ballImg.style.cursor = 'grabbing'; } window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }; drag(e) { this.container.classList.remove('dragging'); // Remove dragging class e.preventDefault(); const dx = e.clientX - this.startX; const dy = e.clientY - this.startY; if (!this.isDragging && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) { this.isDragging = true; } if (this.isDragging) { let newX = this.containerStartLeft + dx; let newY = this.containerStartTop + dy; // Optionally, clamp newX and newY within a chat window if it exists const chatWindow = document.querySelector('.chat-window'); if (chatWindow) { const chatRect = chatWindow.getBoundingClientRect(); const ballRect = this.container.getBoundingClientRect(); newX = Math.max(chatRect.left, Math.min(newX, chatRect.right - ballRect.width)); newY = Math.max(chatRect.top, Math.min(newY, chatRect.bottom - ballRect.height)); } requestAnimationFrame(() => { this.container.style.left = `${newX}px`; this.container.style.top = `${newY}px`; }); } } dragEnd(e) { document.removeEventListener('mousemove', this.drag); document.removeEventListener('mouseup', this.dragEnd); if (this.isDragging) { this.wasDragging = true; // Save new position const left = this.container.offsetLeft; const top = this.container.offsetTop; localStorage.setItem('pballPosition', JSON.stringify({ x: left, y: top })); } // Optionally restore transition styles if needed this.container.style.transition = ''; } setupStyles() { const style = document.createElement('style'); style.textContent = ` /* Import Roboto font for a modern look */ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); :root { --background-dark: #18181b; --background-darker: #2e2e35; --card-background: #1f1f26; --border-color: #3e3e45; --highlight-color: #76c7c0; --highlight-gradient: linear-gradient(90deg, var(--highlight-color), #4db6ac); --text-light: #ffffff; --text-muted: #ccc; --font-family: 'Roboto', sans-serif; --glass-effect: rgba(255, 255, 255, 0.1); /* Adjust as needed */ } /* Global resets & accessibility */ .pball-container, .pball-container * { font-family: var(--font-family); box-sizing: border-box; } /* Draggable container that holds the button and timer. - Restrict its dimensions to fit its children. - Disable pointer events on the container so that only designated children receive them. */ .pball-container { position: fixed; right: 12px; bottom: 95px; /* Positions above chat input */ z-index: 10000; /* Remove default cursor so that only the interactive elements show a pointer */ cursor: default; user-select: none; -webkit-user-select: none; will-change: left, top; transform: scale(1); transform-origin: top left; /* Limit the container's hit area */ width: fit-content; height: fit-content; pointer-events: none; } /* Ensure only the button (and active panel) accept mouse events */ .pball-container > .pball-button, .pball-panel.active { pointer-events: auto; } /* Button styling (draggable) */ .my-button { position: relative; cursor: pointer; } .spawn-timer { position: absolute; left: calc(100% - 65px); bottom: -33px; background: transparent; border: none; padding: 8px 12px; color: var(--text-light); white-space: nowrap; z-index: 10001; pointer-events: none; text-align: center; } .timer-header { display: block; margin-bottom: 2px; position: relative; z-index: 1; } .timer-label { color: #fff; font-size: 16px; font-weight: bold; text-shadow: 0 0 5px #000; display: block; } .countdown-display { font-size: 22px !important; color: #fdc331 !important; margin: 0; line-height: 1; text-shadow: 0 0 5px #000, 0 0 5px #000, 0 0 5px #000, 0 0 5px #000, 0 0 5px #000, 0 0 5px #000, 0 0 5px #000, 0 0 5px #000, 0 0 5px #000, 0 0 5px #000 !important; font-family: 'Arial Black', sans-serif; letter-spacing: -2px; } /* Main button styling */ .pball-button { cursor: pointer; width: 50px; height: 50px; border-radius: 50%; border: 2px solid var(--border-color); background: var(--background-dark); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); transition: transform 0.2s ease, box-shadow 0.2s ease; } .pball-button:hover { transform: scale(1.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6); } /* Panel with glassmorphic effect and smooth slide-in */ .pball-panel { cursor: default; position: absolute; bottom: calc(100% + 10px); right: 0; width: 320px; background: var(--background-dark); border-radius: 16px; overflow: hidden; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); border: 1px solid var(--glass-effect); opacity: 0; visibility: hidden; transform: translateY(20px); transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s; pointer-events: none; } .pball-panel.active { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; } /* Tabs styling */ .pball-tabs { display: flex; background: var(--background-darker); border-bottom: 1px solid var(--border-color); } .pball-tab { flex: 1; padding: 10px; text-align: center; font-size: 16px; cursor: pointer; color: var(--text-muted); transition: background 0.2s ease, color 0.2s ease; } .pball-tab.active, .pball-tab:hover { background: var(--border-color); color: var(--text-light); } /* Search input area */ .pball-search-container { position: relative; margin: 12px; } .pball-search { width: 100%; padding: 8px 36px 8px 12px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--background-dark); color: var(--text-light); font-size: 15px; outline: none; transition: border-color 0.2s ease; } .pball-search:focus { border-color: var(--highlight-color); } .pball-search::placeholder { color: var(--text-muted); } .pball-clear-btn { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: transparent; border: none; color: var(--text-muted); font-size: 18px; cursor: pointer; display: none; } /* Grid layout for content */ .pball-grid { padding: 12px; display: grid; gap: 12px; max-height: 300px; overflow-y: auto; } .pball-grid.ball-items { grid-template-columns: repeat(3, 1fr); } .pball-grid.search-results { grid-template-columns: 1fr; } .pball-grid::-webkit-scrollbar { width: 8px; } .pball-grid::-webkit-scrollbar-track { background: var(--background-dark); border-radius: 8px; } .pball-grid::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 8px; } .pball-grid::-webkit-scrollbar-thumb:hover { background: #555; } /* Catch & Shop items: clean circular icons with transparent background */ .pball-item { display: flex; flex-direction: column; align-items: center; justify-content: center; background: transparent; border: none; border-radius: 50%; padding: 8px; transition: none; cursor: arrow; } .pball-item:hover { transform: none; box-shadow: none; } .pball-item img { pointer-events: auto; user-select: none; cursor: grab; width: 36px; height: 36px; transition: transform 0.2s ease; } .pball-item:hover img { transform: scale(1.3); } .pball-item img.dragging { opacity: 0.6; cursor: grabbing; } .pball-label { margin-top: 6px; font-size: 13px; color: var(--text-light); text-align: center; } .spawn-timer, .timer-header, .countdown-display { pointer-events: none; } /* Browse Tab: Pokémon Grid */ .browse-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; padding: 16px; } /* Browse Tab: Pokémon Tiles */ .browse-tile { display: flex; flex-direction: column; align-items: center; background: var(--background-darker); border: 1px solid var(--border-color); border-radius: 12px; padding: 12px; transition: transform 0.2s ease, box-shadow 0.2s ease; cursor: pointer; text-align: center; } .browse-tile:hover { transform: translateY(-3px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } .browse-tile img { width: 64px; height: 64px; margin-bottom: 6px; } .tile-label { font-size: 14px; font-weight: 500; color: var(--text-light); text-transform: capitalize; } /* Advanced Tab: Pokémon Info Card */ .poke-card { width: 100%; border: 1px solid var(--border-color); border-radius: 16px; padding: 20px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); color: var(--text-light); display: flex; flex-direction: column; gap: 20px; animation: fadeIn 0.5s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .poke-card-header { display: flex; align-items: center; gap: 16px; border-bottom: 1px solid var(--border-color); padding-bottom: 12px; } .poke-image { width: 100px; height: 100px; border-radius: 12px; background: var(--background-dark); object-fit: contain; } .poke-title { font-size: 26px; font-weight: 700; margin: 0; } .section { border-top: 1px solid var(--border-color); padding-top: 12px; } .section h3 { margin: 0 0 8px; font-size: 20px; font-weight: 700; color: var(--text-light); border-bottom: 1px solid var(--border-color); padding-bottom: 4px; } /* Stats Grid in Advanced Card */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; } .stat { display: flex; flex-direction: column; } .stat-label { font-size: 14px; margin-bottom: 4px; color: #ddd; } .stat-bar { width: 100%; background: var(--background-dark); border: 1px solid var(--border-color); border-radius: 6px; height: 18px; overflow: hidden; position: relative; } .stat-fill { background: var(--highlight-gradient); height: 100%; width: 0; transition: width 0.5s ease; border-radius: 6px; position: relative; } .stat-value { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); font-size: 12px; font-weight: bold; color: var(--text-light); } /* Custom Scrollbar for Moves Section */ .moves-section { max-height: 160px; overflow-y: auto; font-size: 15px; color: var(--text-muted); padding-right: 6px; /* Ensures smooth scrolling */ scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb, #888) var(--scrollbar-track, #222); } /* Webkit Browsers (Chrome, Edge, Safari) */ .moves-section::-webkit-scrollbar { width: 6px; /* Same width as other sections */ } .moves-section::-webkit-scrollbar-track { background: var(--scrollbar-track, #222); /* Darker background */ border-radius: 3px; } .moves-section::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb, #888); /* Scrollbar thumb color */ border-radius: 3px; } .moves-section::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover, #666); /* Slightly darker on hover */ } /* Type Damage Relations Section */ .type-relations { display: flex; flex-direction: column; gap: 8px; } .type-box { background: var(--background-dark); border: 1px solid var(--border-color); border-radius: 6px; padding: 6px; font-size: 13px; color: var(--text-light); transition: transform 0.2s ease; } .type-box:hover { transform: scale(1.02); } .type-box strong { display: block; margin-bottom: 4px; font-size: 14px; } .pball-item img.dragging { opacity: 0.5; transform: scale(0.8); transition: all 0.2s ease; filter: drop-shadow(0 0 4px rgba(118, 199, 192, 0.5)); } /* Spinner */ .spinner { margin: 24px auto; border: 4px solid var(--border-color); border-top: 4px solid var(--highlight-color); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Modified browse tile cursor */ .browse-tile { cursor: arrow; user-select: none; } /* Media Query fix for small screens */ @media (max-width: 400px) { .pball-container { transform-origin: bottom right; } } `; document.head.appendChild(style); } async waitForChat() { return new Promise((resolve) => { if (document.querySelector('[data-test-selector="chat-input"]')) { return resolve(); } const observer = new MutationObserver(() => { if (document.querySelector('[data-test-selector="chat-input"]')) { observer.disconnect(); resolve(); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } createInterface() { this.container = document.createElement('div'); this.container.className = 'pball-container'; this.button = this.createMainButton(); this.panel = this.createPanel(); this.container.append(this.button, this.panel); document.body.appendChild(this.container); } createMainButton() { const button = document.createElement('img'); button.draggable = false; // Add this line button.className = 'pball-button'; button.src = this.catchBalls.poke.image; return button; } createPanel() { const panel = document.createElement('div'); panel.className = 'pball-panel'; panel.draggable = false; // Add this line const tabsContainer = document.createElement('div'); tabsContainer.className = 'pball-tabs'; // Tab: Catch const catchTab = document.createElement('div'); catchTab.className = 'pball-tab active'; catchTab.textContent = 'Catch'; catchTab.dataset.tab = 'catch'; tabsContainer.appendChild(catchTab); // Tab: Shop const shopTab = document.createElement('div'); shopTab.className = 'pball-tab'; shopTab.textContent = 'Shop'; shopTab.dataset.tab = 'shop'; tabsContainer.appendChild(shopTab); // Tab: Browse const browseTab = document.createElement('div'); browseTab.className = 'pball-tab'; browseTab.textContent = 'Browse'; browseTab.dataset.tab = 'browse'; tabsContainer.appendChild(browseTab); // Tab: Advanced const advancedTab = document.createElement('div'); advancedTab.className = 'pball-tab'; advancedTab.textContent = 'Advanced'; advancedTab.dataset.tab = 'advanced'; tabsContainer.appendChild(advancedTab); const searchContainer = document.createElement('div'); searchContainer.className = 'pball-search-container'; this.searchInput = document.createElement('input'); this.searchInput.type = 'text'; this.searchInput.className = 'pball-search'; this.searchInput.placeholder = 'Search...'; this.searchInput.setAttribute('aria-label', 'Search Pokémon'); this.clearBtn = document.createElement('button'); this.clearBtn.className = 'pball-clear-btn'; this.clearBtn.textContent = '×'; this.clearBtn.setAttribute('aria-label', 'Clear Search'); searchContainer.append(this.searchInput, this.clearBtn); this.gridContainer = document.createElement('div'); this.gridContainer.className = 'pball-grid'; panel.append(tabsContainer, searchContainer, this.gridContainer); return panel; } createTimerElement() { this.timerContainer = document.createElement('div'); this.timerContainer.className = 'spawn-timer'; this.timerContainer.innerHTML = ` <div class="timer-header"> <span class="timer-label"></span> <div class="countdown-display" id="countdown">--:--</div> </div> `; this.container.appendChild(this.timerContainer); this.startTimerLoop(); } startTimerLoop() { const backendUrl = 'https://poketwitch.bframework.de/info/events/last_spawn/'; const countdownElement = document.querySelector('.countdown-display'); const updateDisplay = (seconds) => { if (isNaN(seconds) || seconds < 0) { countdownElement.textContent = '--:--'; return; } const mins = Math.floor(seconds / 60); const secs = seconds % 60; countdownElement.textContent = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; }; let tickTimeout; // To store the current tick timeout ID // Start the tick loop given an initial remaining time const startTick = (remaining) => { const tick = () => { if (remaining >= 0) { updateDisplay(remaining); remaining--; tickTimeout = setTimeout(tick, 1000); } }; tick(); }; // Fetch new timer data and restart the tick loop const fetchAndRestartTick = () => { // Clear any existing tick loop if (tickTimeout) clearTimeout(tickTimeout); fetch(backendUrl) .then(response => { if (!response.ok) throw new Error('Network error'); return response.json(); }) .then(data => { let remaining = parseInt(data.next_spawn, 10); if (isNaN(remaining)) { throw new Error('Invalid timer data'); } startTick(remaining); }) .catch(error => { console.error('Timer error:', error); updateDisplay(NaN); }); }; // Start immediately fetchAndRestartTick(); // Refresh the timer every 10 seconds (10000 ms) setInterval(fetchAndRestartTick, 60000); } renderGrid() { if (this.currentTab === 'advanced') { this.gridContainer.classList.remove('ball-items'); this.gridContainer.classList.add('search-results'); this.renderAdvancedInstruction(); } else if (this.currentTab === 'browse') { this.gridContainer.classList.remove('ball-items'); this.gridContainer.classList.add('search-results'); this.renderBrowse(); } else { this.gridContainer.classList.remove('search-results'); this.gridContainer.classList.add('ball-items'); this.gridContainer.innerHTML = ''; const balls = this.currentTab === 'catch' ? this.catchBalls : this.shopBalls; Object.entries(balls).forEach(([key, ball]) => { const item = document.createElement('div'); item.className = 'pball-item'; item.dataset.label = ball.tooltip.toLowerCase(); const img = document.createElement('img'); img.src = ball.image; img.dataset.ballType = ball.command; img.draggable = true; const label = document.createElement('div'); label.className = 'pball-label'; label.textContent = ball.tooltip; item.append(img, label); this.gridContainer.appendChild(item); }); this.filterGrid(); } } renderAdvancedInstruction() { this.gridContainer.innerHTML = ''; const info = document.createElement('div'); info.style.padding = '12px'; info.style.textAlign = 'center'; info.style.color = 'var(--text-light)'; info.textContent = 'Enter a Pokémon name and press Enter for detailed info.'; this.gridContainer.appendChild(info); } renderBrowse() { this.gridContainer.innerHTML = ''; if (!this.pokemonList) { this.gridContainer.innerHTML = '<div class="spinner"></div>'; fetch('https://pokeapi.co/api/v2/pokemon?limit=20000') .then(response => response.json()) .then(data => { this.pokemonList = data.results; this.renderBrowseGrid(); }) .catch(err => { this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading Pokémon list</div>`; }); } else { this.renderBrowseGrid(); } } renderBrowseGrid() { this.gridContainer.innerHTML = ''; this.gridContainer.classList.add('browse-container'); // Ensure grid layout const query = this.searchInput.value.trim().toLowerCase(); const filtered = this.pokemonList.filter(poke => poke.name.includes(query)); filtered.forEach(poke => { const tile = document.createElement('div'); tile.className = 'browse-tile'; tile.dataset.label = poke.name.toLowerCase(); const idMatch = poke.url.match(/\/pokemon\/(\d+)\//); const id = idMatch ? idMatch[1] : ''; const img = document.createElement('img'); img.src = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`; const label = document.createElement('div'); label.className = 'tile-label'; label.textContent = poke.name; tile.append(img, label); // Clicking a tile switches to Advanced tab and loads details. tile.addEventListener('click', (e) => { e.stopPropagation(); this.panel.classList.add('active'); this.changeTab('advanced'); this.searchInput.value = poke.name; this.searchAdvancedPokemon(poke.name); }); this.gridContainer.appendChild(tile); }); if (filtered.length === 0) { this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">No Pokémon match your search.</div>`; } } addEventListeners() { // Start drag on mousedown for the main button this.button.addEventListener('mousedown', this.dragStart); // Modified click event: only toggle panel if not dragging this.button.addEventListener('click', (e) => { if (this.wasDragging) { this.wasDragging = false; return; } e.stopPropagation(); this.panel.classList.toggle('active'); if (this.panel.classList.contains('active')) { this.searchInput.focus(); } }); document.addEventListener('click', (e) => { if (!this.container.contains(e.target)) { this.panel.classList.remove('active'); } }); // In the addEventListeners method, update the dragstart handler: this.panel.addEventListener('dragstart', (e) => { const ballImg = e.target.closest('.pball-item img'); if (ballImg) { e.dataTransfer.setData('text/plain', ballImg.dataset.ballType); // Create a temporary drag image const dragImg = new Image(); dragImg.src = ballImg.src; dragImg.style.width = '36px'; dragImg.style.height = '36px'; // Position off-screen to render dragImg.style.position = 'absolute'; dragImg.style.left = '-9999px'; document.body.appendChild(dragImg); // Set custom drag image centered under cursor e.dataTransfer.setDragImage(dragImg, 18, 18); // Cleanup after drag starts setTimeout(() => document.body.removeChild(dragImg), 0); // Add visual feedback to original image ballImg.classList.add('dragging'); // Remove class on drag end const onDragEnd = () => { ballImg.classList.remove('dragging'); document.removeEventListener('dragend', onDragEnd); }; document.addEventListener('dragend', onDragEnd); } }); const chatInput = document.querySelector('#chatInput'); // Change to your input selector if (chatInput) { chatInput.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }); chatInput.addEventListener('drop', (e) => { e.preventDefault(); const ballType = e.dataTransfer.getData('text/plain'); if (ballType) { chatInput.value += ` ${ballType}`; } }); } const tabs = this.panel.querySelectorAll('.pball-tab'); tabs.forEach(tab => { tab.addEventListener('click', (e) => { e.stopPropagation(); this.changeTab(tab.dataset.tab); }); }); this.searchInput.addEventListener('input', () => { if (this.currentTab !== 'advanced') { this.filterGrid(); if (this.currentTab === 'browse') { this.renderBrowseGrid(); } } this.clearBtn.style.display = this.searchInput.value.trim() ? 'block' : 'none'; }); this.clearBtn.addEventListener('click', () => { this.searchInput.value = ''; this.clearBtn.style.display = 'none'; if (this.currentTab !== 'advanced') { this.filterGrid(); if (this.currentTab === 'browse') { this.renderBrowseGrid(); } } }); this.searchInput.addEventListener('keydown', (e) => { if (this.currentTab === 'advanced' && e.key === 'Enter') { this.searchAdvancedPokemon(this.searchInput.value.trim()); } }); } changeTab(tabName) { this.currentTab = tabName; const tabs = this.panel.querySelectorAll('.pball-tab'); tabs.forEach(tab => { tab.classList.toggle('active', tab.dataset.tab === tabName); }); if (tabName === 'advanced') { this.searchInput.placeholder = 'Enter Pokémon name for detailed info...'; } else if (tabName === 'browse') { this.searchInput.placeholder = 'Filter Pokémon...'; } else { this.searchInput.placeholder = 'Search...'; } this.searchInput.value = ''; this.clearBtn.style.display = 'none'; this.renderGrid(); } filterGrid() { const query = this.searchInput.value.trim().toLowerCase(); const items = this.gridContainer.querySelectorAll('.pball-item, .browse-tile'); items.forEach(item => { if (!query || item.dataset.label.includes(query)) { item.style.display = 'flex'; } else { item.style.display = 'none'; } }); } getChatInput() { return document.querySelector('[data-a-target="chat-input"]'); } insertCommand(ballType) { const chatInput = this.getChatInput(); if (!chatInput) return; chatInput.focus(); this.clearChatInput(); this.insertText(ballType); this.triggerInputEvent(chatInput); } clearChatInput() { const chatInput = this.getChatInput(); if (chatInput) { chatInput.value = ''; // Reset the input field to empty this.triggerInputEvent(chatInput); // Ensure Twitch detects the reset } } insertText(text) { document.execCommand('insertText', false, text); } triggerInputEvent(element) { element.dispatchEvent(new Event('input', { bubbles: true, composed: true })); } // Advanced Lookup Methods searchAdvancedPokemon(name) { if (!name) return; this.gridContainer.innerHTML = '<div class="spinner"></div>'; fetch(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`) .then(response => { if (!response.ok) { throw new Error("Pokémon not found"); } return response.json(); }) .then(data => { return fetch(data.species.url) .then(res => { if (!res.ok) { throw new Error("Species data not found"); } return res.json().then(speciesData => ({ data, speciesData })); }); }) .then(({ data, speciesData }) => { return fetch(speciesData.evolution_chain.url) .then(res => { if (!res.ok) { throw new Error("Evolution chain not found"); } return res.json().then(evoData => ({ data, speciesData, evoData })); }); }) .then(({ data, speciesData, evoData }) => { this.displayAdvancedPokemonData(data, speciesData, evoData); }) .catch(err => { this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">${err.message}</div>`; }); } displayAdvancedPokemonData(data, speciesData, evoData) { this.gridContainer.innerHTML = ''; const card = document.createElement('div'); card.className = 'poke-card'; // Build card sections (back button removed) card.appendChild(this.createCardHeader(data)); card.appendChild(this.createBasicInfoSection(data)); card.appendChild(this.createAbilitiesSection(data)); card.appendChild(this.createStatsSection(data)); card.appendChild(this.createTypesSection(data)); card.appendChild(this.createDamageRelationsSection(data)); card.appendChild(this.createMovesSection(data)); if (data.held_items && data.held_items.length > 0) { card.appendChild(this.createHeldItemsSection(data)); } if (data.forms && data.forms.length > 0) { card.appendChild(this.createFormsSection(data)); } // Advanced sections: Pokédex Entry and Evolution Chain card.appendChild(this.createPokedexEntrySection(speciesData)); card.appendChild(this.createEvolutionChainSection(evoData)); this.gridContainer.appendChild(card); } // Modular Pokémon Card Sections createCardHeader(data) { const header = document.createElement('header'); header.className = 'poke-card-header'; const img = document.createElement('img'); img.className = 'poke-image'; img.src = (data.sprites.other && data.sprites.other['official-artwork'] && data.sprites.other['official-artwork'].front_default) || data.sprites.front_default || ''; header.appendChild(img); const title = document.createElement('h2'); title.className = 'poke-title'; title.textContent = `${data.name.charAt(0).toUpperCase() + data.name.slice(1)} (ID: ${data.id})`; header.appendChild(title); return header; } createBasicInfoSection(data) { const section = document.createElement('div'); section.className = 'section'; const totalStats = data.stats.reduce((sum, stat) => sum + stat.base_stat, 0); section.innerHTML = ` <h3>Basic Info</h3> <p><strong>Total Stats:</strong> ${totalStats}</p> <p><strong>Height:</strong> ${data.height}</p> <p><strong>Weight:</strong> ${data.weight}</p> <p><strong>Base Exp:</strong> ${data.base_experience}</p> <p><strong>Species:</strong> ${data.species.name}</p> `; return section; } createAbilitiesSection(data) { const section = document.createElement('div'); section.className = 'section'; section.innerHTML = `<h3>Abilities</h3>`; const ul = document.createElement('ul'); data.abilities.forEach(a => { const li = document.createElement('li'); li.textContent = `${a.ability.name}${a.is_hidden ? ' (Hidden)' : ''}`; ul.appendChild(li); }); section.appendChild(ul); return section; } createStatsSection(data) { const section = document.createElement('div'); section.className = 'section'; section.innerHTML = `<h3>Stats</h3>`; const statsGrid = document.createElement('div'); statsGrid.className = 'stats-grid'; data.stats.forEach(stat => { const statDiv = document.createElement('div'); statDiv.className = 'stat'; const label = document.createElement('div'); label.className = 'stat-label'; label.textContent = `${stat.stat.name.toUpperCase()}: ${stat.base_stat}`; statDiv.appendChild(label); const bar = document.createElement('div'); bar.className = 'stat-bar'; const fill = document.createElement('div'); fill.className = 'stat-fill'; const percentage = Math.min(100, (stat.base_stat / 255) * 100); fill.style.width = `${percentage}%`; const statValue = document.createElement('span'); statValue.className = 'stat-value'; statValue.textContent = stat.base_stat; fill.appendChild(statValue); bar.appendChild(fill); statDiv.appendChild(bar); statsGrid.appendChild(statDiv); }); section.appendChild(statsGrid); return section; } createTypesSection(data) { const section = document.createElement('div'); section.className = 'section'; section.innerHTML = `<h3>Types</h3>`; const ul = document.createElement('ul'); data.types.forEach(typeInfo => { const li = document.createElement('li'); li.textContent = typeInfo.type.name; ul.appendChild(li); }); section.appendChild(ul); return section; } createDamageRelationsSection(data) { const section = document.createElement('div'); section.className = 'section'; section.innerHTML = `<h3>Type Damage Relations</h3>`; const container = document.createElement('div'); container.className = 'type-relations'; data.types.forEach(typeInfo => { const typeBox = document.createElement('div'); typeBox.className = 'type-box'; typeBox.innerHTML = `<strong>${typeInfo.type.name.toUpperCase()}</strong>`; fetch(typeInfo.type.url) .then(res => res.json()) .then(typeData => { const strengths = typeData.damage_relations.double_damage_to.map(d => d.name).join(', ') || "None"; const weaknesses = typeData.damage_relations.double_damage_from.map(d => d.name).join(', ') || "None"; const details = document.createElement('div'); details.innerHTML = `<p><strong>Strengths:</strong> ${strengths}</p><p><strong>Weaknesses:</strong> ${weaknesses}</p>`; typeBox.appendChild(details); }) .catch(() => { const errMsg = document.createElement('div'); errMsg.textContent = "Error loading type data"; typeBox.appendChild(errMsg); }); container.appendChild(typeBox); }); section.appendChild(container); return section; } createMovesSection(data) { const section = document.createElement('div'); section.className = 'section moves-section'; section.innerHTML = `<h3>Moves</h3>`; const ul = document.createElement('ul'); data.moves.forEach(moveInfo => { const li = document.createElement('li'); li.textContent = moveInfo.move.name; ul.appendChild(li); }); section.appendChild(ul); return section; } createHeldItemsSection(data) { const section = document.createElement('div'); section.className = 'section'; section.innerHTML = `<h3>Held Items</h3>`; const ul = document.createElement('ul'); data.held_items.forEach(itemInfo => { const li = document.createElement('li'); li.textContent = itemInfo.item.name; ul.appendChild(li); }); section.appendChild(ul); return section; } createFormsSection(data) { const section = document.createElement('div'); section.className = 'section'; section.innerHTML = `<h3>Forms</h3>`; const ul = document.createElement('ul'); data.forms.forEach(form => { const li = document.createElement('li'); li.textContent = form.name; ul.appendChild(li); }); section.appendChild(ul); return section; } // Advanced Sections createPokedexEntrySection(speciesData) { const section = document.createElement('div'); section.className = 'section'; section.innerHTML = `<h3>Pokédex Entry</h3>`; const entry = speciesData.flavor_text_entries.find(e => e.language.name === 'en'); const flavorText = entry ? entry.flavor_text.replace(/\f|\n/g, ' ') : 'No entry available.'; const para = document.createElement('p'); para.textContent = flavorText; section.appendChild(para); return section; } createEvolutionChainSection(evoData) { const section = document.createElement('div'); section.className = 'section'; section.innerHTML = `<h3>Evolution Chain</h3>`; const chainText = this.getEvolutionChain(evoData.chain); const para = document.createElement('p'); para.textContent = chainText; section.appendChild(para); return section; } getEvolutionChain(chain) { let result = chain.species.name; if (chain.evolves_to && chain.evolves_to.length > 0) { result += " → " + chain.evolves_to.map(subChain => this.getEvolutionChain(subChain)).join(" / "); } return result; } } new PokeballHelper(); })();