Twitch Pokemon Community Game Helper - NO TIMER

Twitch PokéBall Drag and Drop to chat! !pokecatch <balltype> Pokemoncommunitygame Timer and Spawn Helper. Pokemon stats and more!

  1. // ==UserScript==
  2. // @name Twitch Pokemon Community Game Helper - NO TIMER
  3. // @namespace http://tampermonkey.net/
  4. // @version 19
  5. // @description Twitch PokéBall Drag and Drop to chat! !pokecatch <balltype> Pokemoncommunitygame Timer and Spawn Helper. Pokemon stats and more!
  6. // @match https://www.twitch.tv/*
  7. // @icon https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. class PokeballHelper {
  15. constructor() {
  16. this.catchBalls = {
  17. dollars: { command: '$', tooltip: 'Poke Dollars', image: 'https://i.postimg.cc/T20dR1qH/f547e065261b657c49d5702826b0deca.png', quantity: 0 },
  18. check: { command: '!pokecheck', tooltip: 'Poke Check', image: 'https://i.postimg.cc/0N7vhyyn/ea9752334aa08543e2f148c0a903719e.png', quantity: 0 },
  19. poke: { command: '!pokecatch', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png', quantity: 0 },
  20. great: { command: '!pokecatch greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png', quantity: 0 },
  21. ultra: { command: '!pokecatch ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png', quantity: 0 },
  22. premier: { command: '!pokecatch premierball', tooltip: 'Premier Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/premier_ball.png', quantity: 0 },
  23. basic: { command: '!pokecatch basicball', tooltip: 'Basic Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/basic_ball.png', quantity: 0 },
  24. heavy: { command: '!pokecatch heavyball', tooltip: 'Heavy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heavy_ball.png', quantity: 0 },
  25. feather: { command: '!pokecatch featherball', tooltip: 'Feather Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/feather_ball.png', quantity: 0 },
  26. timer: { command: '!pokecatch timerball', tooltip: 'Timer Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/timer_ball.png', quantity: 0 },
  27. quick: { command: '!pokecatch quickball', tooltip: 'Quick Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/quick_ball.png', quantity: 0 },
  28. nest: { command: '!pokecatch nestball', tooltip: 'Nest Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/nest_ball.png', quantity: 0 },
  29. fast: { command: '!pokecatch fastball', tooltip: 'Fast Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fast_ball.png', quantity: 0 },
  30. heal: { command: '!pokecatch healball', tooltip: 'Heal Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/heal_ball.png', quantity: 0 },
  31. repeat: { command: '!pokecatch repeatball', tooltip: 'Repeat Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/repeat_ball.png', quantity: 0 },
  32. friend: { command: '!pokecatch friendball', tooltip: 'Friend Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/friend_ball.png', quantity: 0 },
  33. frozen: { command: '!pokecatch frozenball', tooltip: 'Frozen Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/frozen_ball.png', quantity: 0 },
  34. night: { command: '!pokecatch nightball', tooltip: 'Night Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/night_ball.png', quantity: 0 },
  35. phantom: { command: '!pokecatch phantomball', tooltip: 'Phantom Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/phantom_ball.png', quantity: 0 },
  36. cipher: { command: '!pokecatch cipherball', tooltip: 'Cipher Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cipher_ball.png', quantity: 0 },
  37. magnet: { command: '!pokecatch magnetball', tooltip: 'Magnet Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/magnet_ball.png', quantity: 0 },
  38. net: { command: '!pokecatch netball', tooltip: 'Net Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/net_ball.png', quantity: 0 },
  39. luxury: { command: '!pokecatch luxuryball', tooltip: 'Luxury Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/luxury_ball.png', quantity: 0 },
  40. stone: { command: '!pokecatch stoneball', tooltip: 'Stone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/stone_ball.png', quantity: 0 },
  41. level: { command: '!pokecatch levelball', tooltip: 'Level Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/level_ball.png', quantity: 0 },
  42. clone: { command: '!pokecatch cloneball', tooltip: 'Clone Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/clone_ball.png', quantity: 0 },
  43. sun: { command: '!pokecatch sunball', tooltip: 'Sun Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/sun_ball.png', quantity: 0 },
  44. fantasy: { command: '!pokecatch fantasyball', tooltip: 'Fantasy Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/fantasy_ball.png', quantity: 0 },
  45. mach: { command: '!pokecatch machball', tooltip: 'Mach Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/mach_ball.png', quantity: 0 },
  46. geo: { command: '!pokecatch geoball', tooltip: 'Geo Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/geo_ball.png', quantity: 0 },
  47. dive: { command: '!pokecatch diveball', tooltip: 'Dive Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/dive_ball.png', quantity: 0 },
  48. master: { command: '!pokecatch masterball', tooltip: 'Master Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/master_ball.png', quantity: 0 },
  49. cherish: { command: '!pokecatch cherishball', tooltip: 'Cherish Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/cherish_ball.png', quantity: 0 },
  50. greatCherish: { command: '!pokecatch greatcherishball', tooltip: 'Great Cherish', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_cherish_ball.png', quantity: 0 },
  51. ultraCherish: { command: '!pokecatch ultracherishball', tooltip: 'Ultra Cherish', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_cherish_ball.png', quantity: 0 }
  52. };
  53.  
  54. this.shopBalls = {
  55. pokeball: { command: '!pokeshop pokeball', tooltip: 'Poke Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/poke_ball.png' },
  56. great: { command: '!pokeshop greatball', tooltip: 'Great Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/great_ball.png' },
  57. ultra: { command: '!pokeshop ultraball', tooltip: 'Ultra Ball', image: 'https://poketwitch.bframework.de/static/twitchextension/items/ball/ultra_ball.png' },
  58. gift: { command: '!pokegift', tooltip: 'Poke Gift', image: 'https://i.postimg.cc/CxNNP2Zz/Pngtree-gift-box-3d-illustration-6508903.png' },
  59. v5: { command: ' 5', tooltip: '5', image: 'https://i.postimg.cc/wM8LT5tC/pngaaa-com-3588470.png' },
  60. v10: { command: ' 10', tooltip: '10', image: 'https://i.postimg.cc/NjJXrv97/pngaaa-com-2133853.png' },
  61. v25: { command: ' 25', tooltip: '25', image: 'https://i.postimg.cc/wTFySYV8/pngaaa-com-1433934.png' },
  62. v50: { command: ' 50', tooltip: '50', image: 'https://i.postimg.cc/bNqSCw19/pngaaa-com-973335.png' }
  63. };
  64. this.currentTab = 'catch';
  65. this.allPokemonList = null;
  66. this.isDragging = false;
  67. this.startX = 0;
  68. this.startY = 0;
  69. this.containerStartLeft = 0;
  70. this.containerStartTop = 0;
  71. this.wasDragging = false;
  72.  
  73. // Bind drag methods.
  74. this.dragStart = this.dragStart.bind(this);
  75. this.drag = this.drag.bind(this);
  76. this.dragEnd = this.dragEnd.bind(this);
  77.  
  78. // Initialize UI and observers.
  79. this.init();
  80. this.gridContainer = document.getElementById('grid-container');
  81. this.tooltip = null;
  82. this.initIntersectionObserver();
  83. this.initInventoryObserver();
  84. }
  85.  
  86. initIntersectionObserver() {
  87. const lazyImages = document.querySelectorAll('img.lazy');
  88. const observer = new IntersectionObserver((entries, obs) => {
  89. entries.forEach(entry => {
  90. if (entry.isIntersecting) {
  91. const img = entry.target;
  92. img.src = img.dataset.src;
  93. img.addEventListener('load', () => {
  94. img.classList.add('fade-in', 'visible');
  95. });
  96. obs.unobserve(img);
  97. }
  98. });
  99. });
  100. lazyImages.forEach(img => observer.observe(img));
  101. }
  102.  
  103. init() {
  104. this.setupStyles();
  105. this.waitForChat().then(() => {
  106. this.createInterface();
  107. // Removed timer element creation.
  108. this.addEventListeners();
  109. this.renderGrid();
  110. this.initSearchButtons();
  111. this.imageUpdateStarted = false;
  112. // Removed spawn timer update.
  113. this.updateInventoryFromDOM();
  114. });
  115. }
  116.  
  117. handleSearch(query) {
  118. switch (this.currentTab) {
  119. case 'advanced':
  120. this.searchAdvancedPokemon(query);
  121. break;
  122. case 'browse':
  123. this.renderBrowseGrid();
  124. break;
  125. default:
  126. this.filterGrid();
  127. }
  128. }
  129.  
  130. initSearchButtons() {
  131. document.querySelectorAll('.pball-search-container').forEach(container => {
  132. const input = container.querySelector('.pball-search');
  133. if (!input || container.dataset.initialized) return;
  134.  
  135. const btnContainer = document.createElement('div');
  136. btnContainer.className = 'search-buttons';
  137.  
  138. const enterButton = Object.assign(document.createElement('button'), {
  139. className: 'search-enter-button',
  140. innerHTML: '✔',
  141. title: 'Search (Enter)',
  142. onclick: () => this.handleSearch(input.value.trim())
  143. });
  144.  
  145. const clearButton = Object.assign(document.createElement('button'), {
  146. className: 'pball-clear-btn',
  147. innerHTML: '×',
  148. title: 'Clear search',
  149. style: 'display: none;',
  150. onclick: () => {
  151. input.value = '';
  152. input.focus();
  153. this.handleSearch('');
  154. clearButton.style.display = 'none';
  155. }
  156. });
  157.  
  158. input.addEventListener('input', () => {
  159. clearButton.style.display = input.value ? 'flex' : 'none';
  160. });
  161.  
  162. input.addEventListener('keydown', (e) => {
  163. if (e.key === 'Enter') enterButton.click();
  164. });
  165.  
  166. btnContainer.append(enterButton, clearButton);
  167. container.append(btnContainer);
  168. container.dataset.initialized = true;
  169. });
  170. }
  171.  
  172. createInterface() {
  173. let inventoryDisplay = document.getElementById('inventory-display');
  174. if (!inventoryDisplay) {
  175. inventoryDisplay = document.createElement('div');
  176. inventoryDisplay.id = 'inventory-display';
  177. Object.assign(inventoryDisplay.style, {
  178. position: 'fixed',
  179. top: '10px',
  180. right: '10px',
  181. backgroundColor: '#fff',
  182. border: '1px solid #ccc',
  183. padding: '10px',
  184. zIndex: '9999'
  185. });
  186. document.body.appendChild(inventoryDisplay);
  187. }
  188. inventoryDisplay.innerHTML = `
  189. <h3>Inventory</h3>
  190. <div id="dollars-display">Poke Dollars: 0</div>
  191. <ul id="balls-list"></ul>
  192. `;
  193. }
  194.  
  195. loadPosition() {
  196. const savedPos = localStorage.getItem('pballPosition');
  197. if (savedPos) {
  198. const { x, y } = JSON.parse(savedPos);
  199. this.container.style.left = `${x}px`;
  200. this.container.style.top = `${y}px`;
  201. }
  202. }
  203.  
  204. dragStart(e) {
  205. e.preventDefault();
  206. this.wasDragging = false;
  207. const startX = e.clientX;
  208. const startY = e.clientY;
  209. const rect = this.container.getBoundingClientRect();
  210. const origLeft = rect.left;
  211. const origTop = rect.top;
  212.  
  213. const onMouseMove = (moveEvent) => {
  214. const deltaX = moveEvent.clientX - startX;
  215. const deltaY = moveEvent.clientY - startY;
  216. if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
  217. this.wasDragging = true;
  218. }
  219. this.container.style.left = `${origLeft + deltaX}px`;
  220. this.container.style.top = `${origTop + deltaY}px`;
  221. };
  222.  
  223. const onMouseUp = () => {
  224. window.removeEventListener('mousemove', onMouseMove);
  225. window.removeEventListener('mouseup', onMouseUp);
  226. const ballImg = e.target.closest('.pball-item img');
  227. if (ballImg) {
  228. ballImg.style.cursor = 'grab';
  229. }
  230. };
  231.  
  232. const ballImg = e.target.closest('.pball-item img');
  233. if (ballImg) {
  234. ballImg.style.cursor = 'grabbing';
  235. }
  236.  
  237. window.addEventListener('mousemove', onMouseMove);
  238. window.addEventListener('mouseup', onMouseUp);
  239. }
  240.  
  241. drag(e) {
  242. this.container.classList.remove('dragging');
  243. e.preventDefault();
  244. const dx = e.clientX - this.startX;
  245. const dy = e.clientY - this.startY;
  246. if (!this.isDragging && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
  247. this.isDragging = true;
  248. }
  249. if (this.isDragging) {
  250. let newX = this.containerStartLeft + dx;
  251. let newY = this.containerStartTop + dy;
  252. const chatWindow = document.querySelector('.chat-window');
  253. if (chatWindow) {
  254. const chatRect = chatWindow.getBoundingClientRect();
  255. const ballRect = this.container.getBoundingClientRect();
  256. newX = Math.max(chatRect.left, Math.min(newX, chatRect.right - ballRect.width));
  257. newY = Math.max(chatRect.top, Math.min(newY, chatRect.bottom - ballRect.height));
  258. }
  259. requestAnimationFrame(() => {
  260. this.container.style.left = `${newX}px`;
  261. this.container.style.top = `${newY}px`;
  262. });
  263. }
  264. }
  265.  
  266. dragEnd(e) {
  267. document.removeEventListener('mousemove', this.drag);
  268. document.removeEventListener('mouseup', this.dragEnd);
  269. if (this.isDragging) {
  270. this.wasDragging = true;
  271. const left = this.container.offsetLeft;
  272. const top = this.container.offsetTop;
  273. localStorage.setItem('pballPosition', JSON.stringify({ x: left, y: top }));
  274. }
  275. this.container.style.transition = '';
  276. }
  277.  
  278. setupStyles() {
  279. const style = document.createElement('style');
  280. style.textContent = `
  281. /* ============================================
  282. Ultra Stunning UI & Theme – Dark Transparent with Soft White Illuminations
  283. ============================================ */
  284.  
  285. /*--------------------------------------------------
  286. Import Fonts
  287. --------------------------------------------------*/
  288. @import url('https://fonts.googleapis.com/css2?family=Segment7Standard&display=swap');
  289. @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
  290. @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
  291.  
  292. /*--------------------------------------------------
  293. Global Variables & Base Styles
  294. --------------------------------------------------*/
  295. :root {
  296. --color-primary: rgba(255, 255, 255, 0.8);
  297. --color-secondary: rgba(255, 255, 255, 0.8);
  298. --color-accent: rgba(255, 255, 255, 0.6);
  299. --color-dark: #1c1c1e;
  300. --color-darker: #141414;
  301. --color-card: rgba(20, 20, 20, 0.8);
  302. --color-border: rgba(255, 255, 255, 0.1);
  303. --color-glass: rgba(255, 255, 255, 0.05);
  304. --gradient-accent: linear-gradient(90deg, rgba(255,255,255,0.5), rgba(255,255,255,0.2));
  305. --gradient-bg: radial-gradient(circle at top left, rgba(10,10,10,1), rgba(0,0,0,1));
  306. --color-text: #fefefe;
  307. --font-base: 'Roboto', sans-serif;
  308. --font-led: 'Segment7Standard', monospace;
  309. --font-label: 'Press Start 2P', cursive;
  310. --font-size-base: clamp(0.9rem, 1vw + 0.8rem, 1.1rem);
  311. --border-radius-small: 4px;
  312. --border-radius-medium: 12px;
  313. --border-radius-large: 16px;
  314. --spacing-small: 8px;
  315. --spacing-medium: 16px;
  316. --spacing-large: 24px;
  317. --transition-fast: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  318. --transition-medium: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  319. --box-shadow-light: 0 2px 12px rgba(0, 0, 0, 0.4);
  320. --box-shadow-heavy: 0 4px 20px rgba(0, 0, 0, 0.6);
  321. --backdrop-blur: blur(10px);
  322. --neon-glow: drop-shadow(0 0 8px rgba(255,255,255,0.7)) drop-shadow(0 0 8px rgba(255,255,255,0.7));
  323. --soft-glow: drop-shadow(0 0 10px rgba(255,255,255,0.8));
  324. --breakpoint-md: 768px;
  325. --breakpoint-sm: 600px;
  326. }
  327.  
  328. *, *::before, *::after {
  329. margin: 0;
  330. padding: 0;
  331. box-sizing: border-box;
  332. font-family: var(--font-base);
  333. transition: background var(--transition-fast), color var(--transition-fast);
  334. }
  335.  
  336. html {
  337. scroll-behavior: smooth;
  338. }
  339.  
  340. body {
  341. background: var(--gradient-bg);
  342. color: var(--color-text);
  343. font-size: var(--font-size-base);
  344. line-height: 1.5;
  345. }
  346.  
  347. /*--------------------------------------------------
  348. Scrollbar Styles
  349. --------------------------------------------------*/
  350. ::-webkit-scrollbar {
  351. width: 8px;
  352. height: 8px;
  353. }
  354. ::-webkit-scrollbar-track {
  355. background: var(--color-darker);
  356. border-radius: var(--border-radius-medium);
  357. }
  358. ::-webkit-scrollbar-thumb {
  359. background: var(--color-border);
  360. border-radius: var(--border-radius-medium);
  361. border: 1px solid var(--color-dark);
  362. }
  363. ::-webkit-scrollbar-thumb:hover {
  364. background: rgba(255,255,255,0.3);
  365. }
  366. * {
  367. scrollbar-width: thin;
  368. scrollbar-color: var(--color-border) var(--color-darker);
  369. }
  370.  
  371. /*--------------------------------------------------
  372. Main UI Components
  373. --------------------------------------------------*/
  374. .pball-container {
  375. position: fixed;
  376. bottom: calc(var(--spacing-large) + 50px);
  377. right: var(--spacing-medium);
  378. z-index: 10000;
  379. pointer-events: none;
  380. transform: scale(1);
  381. transform-origin: top right;
  382. width: fit-content;
  383. height: fit-content;
  384. }
  385. .pball-container > * {
  386. pointer-events: auto;
  387. }
  388. .pball-button {
  389. position: relative;
  390. width: 60px;
  391. height: 60px;
  392. border-radius: 50%;
  393. cursor: pointer;
  394. transition: transform var(--transition-fast), filter var(--transition-fast);
  395. display: flex;
  396. align-items: center;
  397. justify-content: center;
  398. }
  399. .pball-button img {
  400. width: 48px;
  401. height: 48px;
  402. object-fit: contain;
  403. transition: transform var(--transition-fast), filter var(--transition-fast);
  404. }
  405. .pball-button:hover,
  406. .pball-button:focus-visible {
  407. transform: scale(1.25) rotate(3deg);
  408. filter: var(--soft-glow);
  409. outline: none;
  410. }
  411. .pball-button:hover img,
  412. .pball-button:focus-visible img {
  413. transform: scale(1.1);
  414. }
  415.  
  416. /*--------------------------------------------------
  417. Panel & Tab System
  418. --------------------------------------------------*/
  419. .pball-panel {
  420. position: absolute;
  421. bottom: calc(100% + var(--spacing-small));
  422. right: 0;
  423. width: 340px;
  424. height: 500px;
  425. min-width: 300px;
  426. min-height: 300px;
  427. overflow: auto;
  428. background: var(--color-card);
  429. backdrop-filter: var(--backdrop-blur);
  430. border-radius: var(--border-radius-large);
  431. border: 1px solid var(--color-border);
  432. box-shadow: var(--box-shadow-heavy);
  433. opacity: 0;
  434. visibility: hidden;
  435. transform: translateY(var(--spacing-small));
  436. transition: opacity var(--transition-medium), transform var(--transition-medium), visibility var(--transition-medium);
  437. }
  438. .pball-panel.active {
  439. opacity: 1;
  440. visibility: visible;
  441. transform: translateY(0);
  442. }
  443. .pball-tabs {
  444. display: flex;
  445. background: var(--color-darker);
  446. border-bottom: 1px solid var(--color-border);
  447. border-top-left-radius: var(--border-radius-large);
  448. border-top-right-radius: var(--border-radius-large);
  449. overflow: hidden;
  450. }
  451. .pball-tab {
  452. position: relative;
  453. flex: 1;
  454. padding: var(--spacing-small);
  455. text-align: center;
  456. font-size: 15px;
  457. cursor: pointer;
  458. color: var(--color-text);
  459. transition: background var(--transition-fast), color var(--transition-fast);
  460. }
  461. .pball-tab.active::after {
  462. content: "";
  463. position: absolute;
  464. bottom: 0;
  465. left: 50%;
  466. width: 60%;
  467. height: 3px;
  468. background: var(--color-primary);
  469. box-shadow: 0 0 12px var(--color-primary);
  470. border-radius: 2px;
  471. transform: translateX(-50%);
  472. }
  473.  
  474. /*--------------------------------------------------
  475. Search & Input Components
  476. --------------------------------------------------*/
  477. .pball-search-container {
  478. position: relative;
  479. margin: var(--spacing-medium);
  480. background: var(--color-card);
  481. backdrop-filter: var(--backdrop-blur);
  482. border-radius: var(--border-radius-medium);
  483. border: 1px solid var(--color-border);
  484. overflow: hidden;
  485. }
  486. .pball-search {
  487. width: 100%;
  488. padding: calc(var(--spacing-small) + 2px) var(--spacing-medium);
  489. padding-right: 70px;
  490. border: none;
  491. background: transparent;
  492. color: var(--color-text);
  493. font-size: 15px;
  494. outline: none;
  495. }
  496. .search-buttons {
  497. position: absolute;
  498. right: var(--spacing-small);
  499. top: 50%;
  500. transform: translateY(-50%);
  501. display: flex;
  502. gap: var(--spacing-small);
  503. }
  504.  
  505. /*--------------------------------------------------
  506. Grid Layouts & Item Cards
  507. --------------------------------------------------*/
  508. .pball-grid {
  509. padding: var(--spacing-medium);
  510. display: grid;
  511. gap: var(--spacing-medium);
  512. max-height: 320px;
  513. overflow-y: auto;
  514. }
  515. .pball-panel.shop .pball-grid,
  516. .pball-panel.catch-shop .pball-grid {
  517. grid-template-columns: repeat(3, minmax(80px, 1fr));
  518. justify-content: center;
  519. }
  520. .pball-grid.ball-items {
  521. grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));
  522. justify-items: center;
  523. }
  524.  
  525. /*--------------------------------------------------
  526. Item Cards & Catch Tab Overrides
  527. --------------------------------------------------*/
  528. .pball-item {
  529. display: flex;
  530. flex-direction: column;
  531. align-items: center;
  532. justify-content: center;
  533. background: transparent;
  534. border: none;
  535. border-radius: 50%;
  536. padding: var(--spacing-small);
  537. transition: transform var(--transition-fast);
  538. }
  539. .pball-item img {
  540. width: 40px;
  541. height: 40px;
  542. transition: transform var(--transition-fast);
  543. cursor: grab;
  544. }
  545. .pball-item img:hover,
  546. .pball-item img:focus-visible {
  547. transform: scale(1.2);
  548. outline: none;
  549. }
  550. .pball-item img:active,
  551. .pball-item img.grabbing {
  552. filter: var(--neon-glow);
  553. animation: neonPulse 0.26s infinite alternate;
  554. }
  555. @keyframes neonPulse {
  556. 0% {
  557. filter: drop-shadow(0 0 8px rgba(255,255,255,0.7)) drop-shadow(0 0 8px rgba(255,255,255,0.7));
  558. }
  559. 100% {
  560. filter: drop-shadow(0 0 12px rgba(255,255,255,0.7)) drop-shadow(0 0 12px rgba(255,255,255,0.7));
  561. }
  562. }
  563. .pball-item .pball-label {
  564. margin-top: var(--spacing-small);
  565. font-size: 14px;
  566. color: var(--color-text);
  567. text-align: center;
  568. }
  569. .catch-shop .pball-item {
  570. flex-direction: row;
  571. justify-content: flex-start;
  572. align-items: center;
  573. }
  574. .catch-shop .pball-item img {
  575. margin-right: var(--spacing-small);
  576. }
  577. .catch-shop .pball-item .pball-label {
  578. margin-top: 0;
  579. text-align: left;
  580. }
  581.  
  582. /*--------------------------------------------------
  583. Pokémon Card Styles - Transparent Version
  584. --------------------------------------------------*/
  585. .pokemon-card {
  586. background: transparent !important;
  587. border-radius: 12px;
  588. padding: 10px;
  589. text-align: center;
  590. transition: transform 0.2s ease, box-shadow 0.2s ease;
  591. position: relative;
  592. overflow: hidden;
  593. cursor: pointer;
  594. }
  595. .pokemon-card:hover {
  596. transform: translateY(-4px);
  597. box-shadow: 0 6px 16px rgba(255,255,255,0.5);
  598. }
  599. .dex-number {
  600. position: absolute;
  601. top: 6px;
  602. left: 8px;
  603. font-weight: bold;
  604. background: rgba(0, 0, 0, 0.4);
  605. padding: 4px 8px;
  606. border-radius: 8px;
  607. color: #fff;
  608. }
  609. .pokemon-image {
  610. max-width: 100px;
  611. max-height: 100px;
  612. object-fit: contain;
  613. transition: transform var(--transition-fast);
  614. background: transparent !important;
  615. }
  616. .pokemon-card:hover .pokemon-image,
  617. .pokemon-card:hover .pball-label,
  618. .pball-item:hover img {
  619. filter: drop-shadow(0px 0px 10px rgba(255,255,255,0.8));
  620. transition: filter var(--transition-fast);
  621. }
  622.  
  623. /*--------------------------------------------------
  624. Utility Classes
  625. --------------------------------------------------*/
  626. .spinner {
  627. margin: 1.5rem auto;
  628. border: 4px solid var(--color-border);
  629. border-top: 4px solid var(--color-primary);
  630. border-radius: 50%;
  631. width: 2.8rem;
  632. height: 2.8rem;
  633. animation: spin 1s linear infinite;
  634. }
  635. .animate-fadeIn {
  636. opacity: 0;
  637. transform: translateY(20px);
  638. animation: fadeInUp 0.5s forwards;
  639. }
  640.  
  641. @keyframes spin {
  642. 0% { transform: rotate(0deg); }
  643. 100% { transform: rotate(360deg); }
  644. }
  645. @keyframes fadeInUp {
  646. to {
  647. opacity: 1;
  648. transform: translateY(0);
  649. }
  650. }
  651.  
  652. /*--------------------------------------------------
  653. Responsive Adjustments for Auto-Alignment
  654. --------------------------------------------------*/
  655. @media (max-width: var(--breakpoint-md)) {
  656. .pball-panel {
  657. width: 90%;
  658. height: auto;
  659. bottom: var(--spacing-medium);
  660. right: var(--spacing-medium);
  661. }
  662. }
  663. `;
  664. document.head.appendChild(style);
  665. }
  666.  
  667. // -----------------------------
  668. // Helper: Debounce Function
  669. // -----------------------------
  670. debounce(func, wait) {
  671. let timeout;
  672. return function(...args) {
  673. const context = this;
  674. clearTimeout(timeout);
  675. timeout = setTimeout(() => func.apply(context, args), wait);
  676. };
  677. }
  678.  
  679. // -----------------------------
  680. // Wait for Chat Input to be Available
  681. // -----------------------------
  682. async waitForChat() {
  683. return new Promise((resolve) => {
  684. const chatSelector = '[data-test-selector="chat-input"]';
  685. if (document.querySelector(chatSelector)) {
  686. return resolve();
  687. }
  688. const observer = new MutationObserver((mutations, obs) => {
  689. if (document.querySelector(chatSelector)) {
  690. obs.disconnect();
  691. resolve();
  692. }
  693. });
  694. observer.observe(document.body, { childList: true, subtree: true });
  695. });
  696. }
  697.  
  698. // -----------------------------
  699. // Create Interface Elements
  700. // -----------------------------
  701. createInterface() {
  702. this.container = document.createElement('div');
  703. this.container.className = 'pball-container';
  704. this.button = this.createMainButton();
  705. this.panel = this.createPanel();
  706. this.container.append(this.button, this.panel);
  707. document.body.appendChild(this.container);
  708. }
  709.  
  710. // -----------------------------
  711. // Create Main Button (Static)
  712. // -----------------------------
  713. createMainButton() {
  714. const button = document.createElement('div');
  715. button.className = 'pball-button';
  716.  
  717. const icon = document.createElement('img');
  718. // Use the static pokeball image
  719. icon.src = this.catchBalls.poke.image;
  720. icon.style.width = '46px';
  721. icon.style.height = '46px';
  722. icon.style.filter = 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))';
  723.  
  724. button.appendChild(icon);
  725. this.buttonIcon = icon;
  726. return button;
  727. }
  728.  
  729. // -----------------------------
  730. // Create Panel with Tabs, Search, and Grid
  731. // -----------------------------
  732. createPanel() {
  733. const panel = document.createElement('div');
  734. panel.className = 'pball-panel';
  735. panel.draggable = false;
  736.  
  737. const tabsContainer = document.createElement('div');
  738. tabsContainer.className = 'pball-tabs';
  739.  
  740. const createTab = (name, isActive = false) => {
  741. const tab = document.createElement('div');
  742. tab.className = `pball-tab${isActive ? ' active' : ''}`;
  743. tab.textContent = name;
  744. tab.dataset.tab = name.toLowerCase();
  745. tab.addEventListener('click', () => {
  746. this.switchTab(name.toLowerCase());
  747. });
  748. return tab;
  749. };
  750.  
  751. tabsContainer.appendChild(createTab('Catch', true));
  752. tabsContainer.appendChild(createTab('Shop'));
  753. tabsContainer.appendChild(createTab('Pokemon'));
  754. tabsContainer.appendChild(createTab('Moves'));
  755. tabsContainer.appendChild(createTab('Advanced'));
  756.  
  757. const searchContainer = document.createElement('div');
  758. searchContainer.className = 'pball-search-container';
  759.  
  760. this.searchInput = document.createElement('input');
  761. this.searchInput.type = 'text';
  762. this.searchInput.className = 'pball-search';
  763. this.searchInput.placeholder = 'Search...';
  764. this.searchInput.setAttribute('aria-label', 'Search Pokémon');
  765.  
  766. this.clearBtn = document.createElement('button');
  767. this.clearBtn.setAttribute('aria-label', 'Clear Search');
  768.  
  769. searchContainer.append(this.searchInput, this.clearBtn);
  770.  
  771. this.gridContainer = document.createElement('div');
  772. this.gridContainer.className = 'pball-grid';
  773.  
  774. this.tabContainers = {};
  775. ['catch', 'shop', 'pokemon', 'moves', 'advanced'].forEach(tabName => {
  776. const container = document.createElement('div');
  777. container.className = 'tab-content';
  778. container.dataset.tab = tabName;
  779. container.style.display = tabName === 'catch' ? 'block' : 'none';
  780. this.tabContainers[tabName] = container;
  781. this.gridContainer.appendChild(container);
  782. });
  783.  
  784. const footer = document.createElement('div');
  785. Object.assign(footer.style, {
  786. position: 'absolute',
  787. bottom: '0',
  788. left: '0',
  789. width: '100%',
  790. padding: '4px 12px',
  791. borderTop: '1px solid rgba(255,255,255,0.1)',
  792. fontSize: '15px',
  793. color: '#666',
  794. textAlign: 'center'
  795. });
  796.  
  797. const message = document.createTextNode('Like the extension? ');
  798. const cashTag = document.createElement('span');
  799. cashTag.textContent = '$yeetsquadcuz';
  800. Object.assign(cashTag.style, {
  801. color: '#888',
  802. fontWeight: '500',
  803. cursor: 'pointer',
  804. transition: 'color 0.2s ease',
  805. display: 'inline-flex',
  806. alignItems: 'center'
  807. });
  808. cashTag.onmouseenter = () => cashTag.style.color = '#aaa';
  809. cashTag.onmouseleave = () => cashTag.style.color = '#888';
  810.  
  811. const cashLogo = document.createElement('img');
  812. cashLogo.src = 'https://i.postimg.cc/qq9LWcjm/pngegg.png';
  813. Object.assign(cashLogo.style, {
  814. width: '14px',
  815. height: '14px',
  816. marginLeft: '4px'
  817. });
  818. cashTag.appendChild(cashLogo);
  819.  
  820. cashTag.addEventListener('click', () => {
  821. const audio = new Audio('https://www.myinstants.com/media/sounds/yeet.mp3');
  822. audio.volume = 0.1;
  823. audio.play();
  824. });
  825.  
  826. footer.appendChild(message);
  827. footer.appendChild(cashTag);
  828.  
  829. panel.append(tabsContainer, searchContainer, this.gridContainer, footer);
  830. return panel;
  831. }
  832.  
  833. renderGrid() {
  834. // Cache control elements if they exist
  835. const movesControls = document.getElementById('moves-controls');
  836. const pokemonControls = document.getElementById('pokemon-controls');
  837.  
  838. // Only remove controls if needed
  839. if (movesControls && this.currentTab !== 'moves') {
  840. movesControls.remove();
  841. }
  842. if (pokemonControls && this.currentTab !== 'pokemon') {
  843. pokemonControls.remove();
  844. }
  845.  
  846. // Clear existing classes to avoid duplication
  847. this.gridContainer.classList.remove('ball-items', 'search-results');
  848.  
  849. if (this.currentTab === 'advanced') {
  850. this.gridContainer.classList.add('search-results');
  851. this.renderAdvancedInstruction();
  852. } else if (this.currentTab === 'pokemon') {
  853. this.gridContainer.classList.add('search-results');
  854. this.renderPokemon();
  855. } else if (this.currentTab === 'moves') {
  856. this.gridContainer.classList.add('search-results');
  857. this.renderMoves();
  858. } else {
  859. // For "catch" and "shop" tabs
  860. this.gridContainer.classList.add('ball-items');
  861. // Clear out the container
  862. this.gridContainer.innerHTML = '';
  863.  
  864. const balls = this.currentTab === 'catch' ? this.catchBalls : this.shopBalls;
  865. const fragment = document.createDocumentFragment();
  866.  
  867. Object.entries(balls).forEach(([key, ball]) => {
  868. const item = document.createElement('div');
  869. item.className = 'pball-item';
  870. item.dataset.label = ball.tooltip.toLowerCase();
  871.  
  872. const img = document.createElement('img');
  873. img.src = ball.image;
  874. img.dataset.ballType = ball.command;
  875. img.draggable = true;
  876.  
  877. const label = document.createElement('div');
  878. label.className = 'pball-label';
  879. label.textContent = ball.tooltip;
  880.  
  881. item.append(img, label);
  882. fragment.appendChild(item);
  883. });
  884.  
  885. this.gridContainer.appendChild(fragment);
  886. this.filterGrid();
  887. }
  888. }
  889.  
  890. renderMoves() {
  891. // Clear the grid container.
  892. this.gridContainer.innerHTML = '';
  893. // Render the custom controls panel above the grid.
  894. this.renderMovesControls();
  895.  
  896. // Try to load from localStorage first.
  897. const cachedMoves = localStorage.getItem('movesList');
  898. if (cachedMoves) {
  899. this.movesList = JSON.parse(cachedMoves);
  900. this.renderMovesGrid();
  901. } else if (!this.movesList) {
  902. this.gridContainer.innerHTML += '<div class="spinner"></div>';
  903. fetch('https://pokeapi.co/api/v2/move?limit=1000')
  904. .then(response => response.json())
  905. .then(data => {
  906. this.movesList = data.results;
  907. // Cache the moves list in localStorage.
  908. localStorage.setItem('movesList', JSON.stringify(data.results));
  909. this.renderMovesGrid();
  910. })
  911. .catch(err => {
  912. this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading moves list</div>`;
  913. });
  914. } else {
  915. // Use the already fetched moves list.
  916. this.renderMovesGrid();
  917. }
  918. }
  919.  
  920.  
  921. renderMovesControls() {
  922. let controlsContainer = document.getElementById('moves-controls');
  923. if (!controlsContainer) {
  924. controlsContainer = document.createElement('div');
  925. controlsContainer.id = 'moves-controls';
  926. Object.assign(controlsContainer.style, {
  927. display: 'flex',
  928. flexWrap: 'nowrap',
  929. justifyContent: 'center', // Changed to center
  930. alignItems: 'center',
  931. marginBottom: '10px',
  932. backgroundColor: '#202020',
  933. color: '#fff',
  934. padding: '3px',
  935. borderRadius: '5px',
  936. gap: '4px',
  937. overflowX: 'auto',
  938. width: '100%' // Ensure full width for proper centering
  939. });
  940. this.gridContainer.parentNode.insertBefore(controlsContainer, this.gridContainer);
  941. }
  942. controlsContainer.innerHTML = '';
  943.  
  944. // --- Filter Buttons ---
  945. const filterOptions = ['All', 'Physical', 'Special', 'Status'];
  946. const filterGroup = document.createElement('div');
  947. filterGroup.style.display = 'flex';
  948. filterGroup.style.gap = '4px';
  949. filterGroup.style.margin = '0';
  950. filterOptions.forEach(option => {
  951. const btn = document.createElement('button');
  952. btn.textContent = option;
  953. btn.dataset.filter = option.toLowerCase();
  954. Object.assign(btn.style, {
  955. backgroundColor: '#202020',
  956. color: '#fff',
  957. border: '1px solid #666',
  958. borderRadius: '3px',
  959. padding: '2px 4px',
  960. cursor: 'pointer',
  961. fontSize: '10px',
  962. height: '24px',
  963. transition: 'background-color 0.2s ease',
  964. whiteSpace: 'nowrap'
  965. });
  966. btn.addEventListener('click', () => {
  967. Array.from(filterGroup.children).forEach(child => {
  968. child.style.backgroundColor = '#202020';
  969. });
  970. btn.style.backgroundColor = '#444';
  971. this.selectedDamageFilter = btn.dataset.filter;
  972. this.renderMovesGrid();
  973. });
  974. filterGroup.appendChild(btn);
  975. });
  976. controlsContainer.appendChild(filterGroup);
  977.  
  978. // --- Sort Dropdown ---
  979. const sortContainer = document.createElement('div');
  980. sortContainer.style.display = 'flex';
  981. sortContainer.style.alignItems= 'center';
  982. sortContainer.style.gap = '3px';
  983. const sortLabel = document.createElement('label');
  984. sortLabel.textContent = 'Sort:';
  985. Object.assign(sortLabel.style, {
  986. fontSize: '10px',
  987. color: '#fff',
  988. whiteSpace: 'nowrap',
  989. marginLeft: '8px' // Added spacing between filter buttons and sort
  990. });
  991. const sortSelect = document.createElement('select');
  992. sortSelect.id = 'moves-sort';
  993. Object.assign(sortSelect.style, {
  994. backgroundColor: '#202020',
  995. color: '#fff',
  996. border: '1px solid #666',
  997. borderRadius: '3px',
  998. padding: '2px',
  999. fontSize: '10px',
  1000. height: '24px',
  1001. cursor: 'pointer',
  1002. minWidth: '72px'
  1003. });
  1004. sortSelect.innerHTML = `
  1005. <option value="name-asc">AZ</option>
  1006. <option value="name-desc">ZA</option>
  1007. `;
  1008. sortSelect.addEventListener('change', () => {
  1009. this.renderMovesGrid();
  1010. });
  1011. sortContainer.appendChild(sortLabel);
  1012. sortContainer.appendChild(sortSelect);
  1013. controlsContainer.appendChild(sortContainer);
  1014. }
  1015.  
  1016. // Render the moves grid using the custom controls (all moves displayed, with search highlighting)
  1017. renderMovesGrid() {
  1018. this.gridContainer.innerHTML = '';
  1019. this.gridContainer.classList.add('browse-container');
  1020. const query = this.searchInput.value.trim().toLowerCase();
  1021.  
  1022. // Get the selected damage filter from the buttons (default to 'all' if not set)
  1023. const damageFilter = this.selectedDamageFilter || 'all';
  1024. const sortOption = document.getElementById('moves-sort')?.value || 'name-asc';
  1025.  
  1026. // Filter moves by search query.
  1027. let filtered = this.movesList.filter(move => move.name.includes(query));
  1028.  
  1029. // Further filter by damage class if not set to 'all'
  1030. if (damageFilter !== 'all') {
  1031. filtered = filtered.filter(move => {
  1032. if (this.moveDetailCache && this.moveDetailCache.has(move.url)) {
  1033. const moveData = this.moveDetailCache.get(move.url);
  1034. return moveData.damage_class.name === damageFilter;
  1035. }
  1036. // Include moves without loaded details by default.
  1037. return true;
  1038. });
  1039. }
  1040.  
  1041. // Sort moves alphabetically.
  1042. if (sortOption === 'name-asc') {
  1043. filtered.sort((a, b) => a.name.localeCompare(b.name));
  1044. } else if (sortOption === 'name-desc') {
  1045. filtered.sort((a, b) => b.name.localeCompare(a.name));
  1046. }
  1047.  
  1048. // Build the grid using a document fragment.
  1049. const fragment = document.createDocumentFragment();
  1050. filtered.forEach(move => {
  1051. const tile = document.createElement('div');
  1052. tile.className = 'browse-tile';
  1053. tile.dataset.label = move.name.toLowerCase();
  1054.  
  1055. // Card container for move info.
  1056. const content = document.createElement('div');
  1057. content.className = 'move-card';
  1058. Object.assign(content.style, {
  1059. padding: '12px',
  1060. borderRadius: '12px',
  1061. background: 'var(--background-darker)',
  1062. display: 'flex',
  1063. flexDirection: 'column',
  1064. alignItems: 'center',
  1065. cursor: 'pointer',
  1066. transition: 'box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out'
  1067. });
  1068.  
  1069. // Hover glow effect.
  1070. content.addEventListener('mouseenter', () => {
  1071. content.style.boxShadow = '0 0 20px rgba(255, 255, 255, 0.6)';
  1072. content.style.transform = 'scale(1.05)';
  1073. });
  1074. content.addEventListener('mouseleave', () => {
  1075. content.style.boxShadow = '0 0 0px rgba(255, 255, 255)';
  1076. content.style.transform = 'scale(1)';
  1077. });
  1078.  
  1079. // Highlight matching search text in move title.
  1080. let titleText = move.name;
  1081. if (query) {
  1082. const regex = new RegExp(`(${query})`, 'gi');
  1083. titleText = move.name.replace(regex, '<mark>$1</mark>');
  1084. }
  1085. const title = document.createElement('div');
  1086. title.className = 'move-title';
  1087. Object.assign(title.style, {
  1088. fontSize: '28px',
  1089. fontWeight: 'bold',
  1090. marginBottom: '8px',
  1091. color: 'var(--text-glow)',
  1092. textShadow: '0 0 5px rgba(255, 255, 255, 0.8)',
  1093. textAlign: 'center'
  1094. });
  1095. title.innerHTML = titleText;
  1096.  
  1097. // Container for badges.
  1098. const badgeContainer = document.createElement('div');
  1099. badgeContainer.className = 'badge-container';
  1100. Object.assign(badgeContainer.style, {
  1101. display: 'flex',
  1102. gap: '8px'
  1103. });
  1104.  
  1105. content.append(title, badgeContainer);
  1106. tile.appendChild(content);
  1107.  
  1108. // Click event to switch to advanced view.
  1109. tile.addEventListener('click', (e) => {
  1110. e.stopPropagation();
  1111. this.panel.classList.add('active');
  1112. this.changeTab('advanced');
  1113. this.searchInput.value = move.name;
  1114. this.searchAdvancedMove(move.name);
  1115. });
  1116.  
  1117. // Fetch move details for badges using caching.
  1118. if (this.moveDetailCache && this.moveDetailCache.has(move.url)) {
  1119. const moveData = this.moveDetailCache.get(move.url);
  1120. const typeBadge = this.createBadge(moveData.type.name, this.getTypeColor(moveData.type.name));
  1121. const damageColor = moveData.damage_class.name === 'physical' ? '#F08030' :
  1122. moveData.damage_class.name === 'special' ? '#6890F0' : '#A8A878';
  1123. const damageBadge = this.createBadge(moveData.damage_class.name, damageColor);
  1124. badgeContainer.append(typeBadge, damageBadge);
  1125. } else {
  1126. fetch(move.url)
  1127. .then(response => response.json())
  1128. .then(moveData => {
  1129. if (!this.moveDetailCache) this.moveDetailCache = new Map();
  1130. this.moveDetailCache.set(move.url, moveData);
  1131. const typeBadge = this.createBadge(moveData.type.name, this.getTypeColor(moveData.type.name));
  1132. const damageColor = moveData.damage_class.name === 'physical' ? '#F08030' :
  1133. moveData.damage_class.name === 'special' ? '#6890F0' : '#A8A878';
  1134. const damageBadge = this.createBadge(moveData.damage_class.name, damageColor);
  1135. badgeContainer.append(typeBadge, damageBadge);
  1136. })
  1137. .catch(err => {
  1138. console.error('Error fetching move details for grid tile:', err);
  1139. });
  1140. }
  1141. fragment.appendChild(tile);
  1142. });
  1143. this.gridContainer.appendChild(fragment);
  1144.  
  1145. if (filtered.length === 0) {
  1146. this.gridContainer.innerHTML = '<div style="padding:12px; color: var(--text-light); text-align: center;">No moves match your search.</div>';
  1147. }
  1148. }
  1149.  
  1150. // -----------------------------
  1151. // Advanced Move Details Section
  1152. // -----------------------------
  1153. async searchAdvancedMove(moveName) {
  1154. try {
  1155. const normalizedMove = moveName.trim().toLowerCase();
  1156. const response = await fetch(`https://pokeapi.co/api/v2/move/${normalizedMove}`);
  1157. if (!response.ok) throw new Error('Move not found');
  1158. const moveData = await response.json();
  1159. this.displayAdvancedMoveData(moveData);
  1160. } catch (error) {
  1161. console.error("Error fetching move details:", error);
  1162. this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading move details</div>`;
  1163. }
  1164. }
  1165.  
  1166. displayAdvancedPokemonData(data, speciesData, evoData) {
  1167. this.gridContainer.innerHTML = '';
  1168. const card = document.createElement('div');
  1169. card.className = 'poke-card animate-fadeIn';
  1170. card.style.maxWidth = '100%';
  1171. card.style.overflowX = 'hidden';
  1172. card.style.display = 'flex';
  1173. card.style.flexDirection = 'column';
  1174. card.style.gap = '16px';
  1175.  
  1176. // Assemble the card sections with animations
  1177. card.append(
  1178. this.createCardHeader(data),
  1179. this.createBasicInfoSection(data),
  1180. this.createStatsRadarChart(data),
  1181. this.createAbilitiesSection(data),
  1182. // Remove old type relations grid
  1183. // Instead, add our new advanced analysis section:
  1184. this.createAdvancedBattleAnalysisTab(data),
  1185. this.createPokedexEntrySection(speciesData),
  1186. this.createEvolutionVisualization(evoData.chain),
  1187. this.createMovesSection(data)
  1188. );
  1189.  
  1190. // Append optional sections
  1191. if (data.held_items && data.held_items.length > 0) {
  1192. card.appendChild(this.createHeldItemsSection(data));
  1193. }
  1194. if (data.forms && data.forms.length > 0) {
  1195. card.appendChild(this.createFormsSection(data));
  1196. }
  1197.  
  1198. this.gridContainer.appendChild(card);
  1199. this.initIntersectionObserver(); // Reinitialize for new lazy images
  1200.  
  1201. // Trigger an animation frame for smoother entrance
  1202. requestAnimationFrame(() => {
  1203. card.classList.add('visible');
  1204. });
  1205. }
  1206.  
  1207. displayAdvancedMoveData(moveData) {
  1208. this.gridContainer.innerHTML = '';
  1209. const card = document.createElement('div');
  1210. card.className = 'poke-card animate-fadeIn';
  1211. card.style.maxWidth = '100%';
  1212. card.style.overflowX = 'hidden';
  1213. card.style.display = 'flex';
  1214. card.style.flexDirection = 'column';
  1215. card.style.gap = '16px';
  1216.  
  1217. card.append(
  1218. this.createMoveCardHeader(moveData),
  1219. this.createMoveBasicInfoSection(moveData),
  1220. this.createMoveStatsChart(moveData),
  1221. this.createMoveDescriptionSection(moveData)
  1222. );
  1223.  
  1224. this.gridContainer.appendChild(card);
  1225. requestAnimationFrame(() => card.classList.add('visible'));
  1226. }
  1227.  
  1228. // -----------------------------
  1229. // Helper: Create a Badge Element
  1230. // -----------------------------
  1231. createBadge(text, backgroundColor) {
  1232. const badge = document.createElement('span');
  1233. badge.className = 'type-badge';
  1234. badge.textContent = text.toUpperCase();
  1235. badge.style.background = backgroundColor;
  1236. badge.style.padding = '3px 3px';
  1237. badge.style.borderRadius = '5px';
  1238. badge.style.fontSize = '10px';
  1239. badge.style.fontWeight = '700';
  1240. badge.style.color = '#fff';
  1241. badge.style.textShadow = '0 1px 2px rgba(0,0,0,0.3)';
  1242. return badge;
  1243. }
  1244.  
  1245. // -----------------------------
  1246. // Header: Move Title, Badges, and Numeric Details
  1247. // -----------------------------
  1248. createMoveCardHeader(moveData) {
  1249. const header = document.createElement('header');
  1250. header.className = 'poke-card-header animate-slideDown';
  1251. header.setAttribute('role', 'banner');
  1252.  
  1253. const title = document.createElement('h1');
  1254. title.className = 'poke-title';
  1255. title.style.fontSize = '32px';
  1256. title.style.margin = '0 0 8px';
  1257. title.textContent = moveData.name.charAt(0).toUpperCase() + moveData.name.slice(1);
  1258.  
  1259. const badgeContainer = document.createElement('div');
  1260. badgeContainer.style.display = 'flex';
  1261. badgeContainer.style.gap = '8px';
  1262. badgeContainer.style.marginBottom = '16px';
  1263.  
  1264. const typeBadge = this.createBadge(moveData.type.name, this.getTypeColor(moveData.type.name));
  1265. const damageColor = moveData.damage_class.name === 'physical'
  1266. ? '#F08030'
  1267. : moveData.damage_class.name === 'special'
  1268. ? '#6890F0'
  1269. : '#A8A878';
  1270. const damageBadge = this.createBadge(moveData.damage_class.name, damageColor);
  1271.  
  1272. badgeContainer.append(typeBadge, damageBadge);
  1273.  
  1274. const details = document.createElement('div');
  1275. details.style.display = 'grid';
  1276. details.style.gridTemplateColumns = 'repeat(4, auto)';
  1277. details.style.gap = '16px';
  1278. details.innerHTML = `
  1279. <div class="detail-item">
  1280. <span class="detail-label">POWER</span>
  1281. <span class="detail-value">${moveData.power !== null ? moveData.power : '—'}</span>
  1282. </div>
  1283. <div class="detail-item">
  1284. <span class="detail-label">ACC</span>
  1285. <span class="detail-value">${moveData.accuracy !== null ? moveData.accuracy : '—'}</span>
  1286. </div>
  1287. <div class="detail-item">
  1288. <span class="detail-label">PP</span>
  1289. <span class="detail-value">${moveData.pp}</span>
  1290. </div>
  1291. <div class="detail-item">
  1292. <span class="detail-label">PRI</span>
  1293. <span class="detail-value">${moveData.priority}</span>
  1294. </div>
  1295. `;
  1296.  
  1297. header.append(title, badgeContainer, details);
  1298. return header;
  1299. }
  1300.  
  1301. // -----------------------------
  1302. // Basic Info Section: Additional Move Details
  1303. // -----------------------------
  1304. createMoveBasicInfoSection(moveData) {
  1305. const section = document.createElement('section');
  1306. section.className = 'info-grid animate-fadeInUp';
  1307. section.innerHTML = `
  1308. <h3 class="section-title">BASIC INFO</h3>
  1309. <div class="metric">
  1310. <span class="label">CATEGORY</span>
  1311. <span class="value">${moveData.damage_class.name.toUpperCase()}</span>
  1312. </div>
  1313. <div class="metric">
  1314. <span class="label">TARGET</span>
  1315. <span class="value">${moveData.target.name.replace(/-/g, ' ').toUpperCase()}</span>
  1316. </div>
  1317. <div class="metric">
  1318. <span class="label">EFFECT CHANCE</span>
  1319. <span class="value">${moveData.effect_chance !== null ? moveData.effect_chance + '%' : '—'}</span>
  1320. </div>
  1321. `;
  1322. return section;
  1323. }
  1324.  
  1325. // -----------------------------
  1326. // Statistics Chart Section: Visualize Move Data with Chart.js
  1327. // -----------------------------
  1328. createMoveStatsChart(moveData) {
  1329. const section = document.createElement('section');
  1330. section.className = 'stats-chart-card animate-fadeInUp';
  1331. section.innerHTML = `<h3 class="section-title">STATISTICS</h3>`;
  1332.  
  1333. const chartContainer = document.createElement('div');
  1334. chartContainer.className = 'chart-container';
  1335. chartContainer.style.position = 'relative';
  1336. chartContainer.style.height = '220px';
  1337. chartContainer.style.width = '100%';
  1338. chartContainer.style.margin = '16px 0';
  1339.  
  1340. const canvas = document.createElement('canvas');
  1341. canvas.setAttribute('aria-label', 'Move statistics chart');
  1342. canvas.style.touchAction = 'none';
  1343. canvas.style.width = '100%';
  1344. canvas.style.height = '100%';
  1345.  
  1346. const renderChart = () => {
  1347. const ctx = canvas.getContext('2d');
  1348. const labels = [];
  1349. const dataValues = [];
  1350.  
  1351. if (moveData.power !== null) {
  1352. labels.push('Power');
  1353. dataValues.push(moveData.power);
  1354. }
  1355. if (moveData.accuracy !== null) {
  1356. labels.push('Accuracy');
  1357. dataValues.push(moveData.accuracy);
  1358. }
  1359. labels.push('PP');
  1360. dataValues.push(moveData.pp);
  1361. labels.push('Priority');
  1362. dataValues.push(moveData.priority);
  1363.  
  1364. new Chart(ctx, {
  1365. type: 'bar',
  1366. data: {
  1367. labels: labels,
  1368. datasets: [{
  1369. label: moveData.name.toUpperCase(),
  1370. data: dataValues,
  1371. backgroundColor: [
  1372. 'rgba(145,70,255,0.6)',
  1373. 'rgba(245,25,255,0.6)',
  1374. 'rgba(255,159,64,0.6)',
  1375. 'rgba(255,64,64,0.6)'
  1376. ],
  1377. borderColor: [
  1378. 'rgba(145,70,255,1)',
  1379. 'rgba(245,25,255,1)',
  1380. 'rgba(255,159,64,1)',
  1381. 'rgba(255,64,64,1)'
  1382. ],
  1383. borderWidth: 1
  1384. }]
  1385. },
  1386. options: {
  1387. responsive: true,
  1388. maintainAspectRatio: false,
  1389. scales: {
  1390. y: { beginAtZero: true }
  1391. },
  1392. plugins: {
  1393. legend: { display: false }
  1394. }
  1395. }
  1396. });
  1397. };
  1398.  
  1399. if (typeof Chart === 'undefined') {
  1400. const script = document.createElement('script');
  1401. script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
  1402. script.onload = renderChart;
  1403. script.onerror = () => {
  1404. console.error("Failed to load Chart.js");
  1405. section.innerHTML += `<div style="padding:12px; color: var(--text-light);">Failed to load chart library.</div>`;
  1406. };
  1407. document.head.appendChild(script);
  1408. } else {
  1409. renderChart();
  1410. }
  1411.  
  1412. chartContainer.appendChild(canvas);
  1413. section.appendChild(chartContainer);
  1414. return section;
  1415. }
  1416.  
  1417. // -----------------------------
  1418. // Move Description Section: Display Move Effect Text
  1419. // -----------------------------
  1420. createMoveDescriptionSection(moveData) {
  1421. const section = document.createElement('section');
  1422. section.className = 'move-description-section animate-fadeInUp';
  1423. section.innerHTML = `<h3 class="section-title">MOVE DESCRIPTION</h3>`;
  1424.  
  1425. const effectEntry = moveData.effect_entries.find(e => e.language.name === 'en');
  1426. const effectText = effectEntry
  1427. ? effectEntry.effect.replace(/\n|\f/g, ' ')
  1428. : 'No description available.';
  1429.  
  1430. const descContainer = document.createElement('div');
  1431. descContainer.className = 'move-description';
  1432. descContainer.style.background = 'var(--background-darker)';
  1433. descContainer.style.padding = '16px';
  1434. descContainer.style.borderRadius = '8px';
  1435. descContainer.style.fontSize = '14px';
  1436. descContainer.style.lineHeight = '1.5';
  1437. descContainer.style.color = 'var(--text-muted)';
  1438. descContainer.textContent = effectText;
  1439.  
  1440. section.appendChild(descContainer);
  1441. return section;
  1442. }
  1443.  
  1444. // -----------------------------
  1445. // Instruction & Browse Sections
  1446. // -----------------------------
  1447. renderAdvancedInstruction() {
  1448. this.gridContainer.innerHTML = '';
  1449. const info = document.createElement('div');
  1450. info.style.padding = '12px';
  1451. info.style.textAlign = 'center';
  1452. info.style.color = 'var(--text-light)';
  1453. info.textContent = 'Enter a Pokémon name and press Enter for detailed info.';
  1454. this.gridContainer.appendChild(info);
  1455. }
  1456.  
  1457. renderBrowse() {
  1458. this.gridContainer.innerHTML = '';
  1459. if (!this.pokemonList) {
  1460. this.gridContainer.innerHTML = '<div class="spinner"></div>';
  1461. fetch('https://pokeapi.co/api/v2/pokemon?limit=20000')
  1462. .then(response => response.json())
  1463. .then(data => {
  1464. this.pokemonList = data.results;
  1465. this.renderBrowseGrid();
  1466. })
  1467. .catch(err => {
  1468. this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading Pokémon list</div>`;
  1469. });
  1470. } else {
  1471. this.renderBrowseGrid();
  1472. }
  1473. }
  1474.  
  1475. renderBrowseGrid() {
  1476. this.gridContainer.innerHTML = '';
  1477. this.gridContainer.classList.add('browse-container');
  1478. const query = this.searchInput.value.trim().toLowerCase();
  1479. const filtered = this.pokemonList.filter(poke => poke.name.includes(query));
  1480. const fragment = document.createDocumentFragment();
  1481.  
  1482. filtered.forEach(poke => {
  1483. const tile = document.createElement('div');
  1484. tile.className = 'browse-tile';
  1485. tile.dataset.label = poke.name.toLowerCase();
  1486.  
  1487. const idMatch = poke.url.match(/\/pokemon\/(\d+)\//);
  1488. const id = idMatch ? idMatch[1] : '';
  1489.  
  1490. const img = document.createElement('img');
  1491. img.src = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`;
  1492.  
  1493. const label = document.createElement('div');
  1494. label.className = 'tile-label';
  1495. label.textContent = poke.name;
  1496.  
  1497. tile.append(img, label);
  1498. tile.addEventListener('click', (e) => {
  1499. e.stopPropagation();
  1500. this.panel.classList.add('active');
  1501. this.changeTab('advanced');
  1502. this.searchInput.value = poke.name;
  1503. this.searchAdvancedPokemon(poke.name);
  1504. });
  1505.  
  1506. fragment.appendChild(tile);
  1507. });
  1508.  
  1509. this.gridContainer.appendChild(fragment);
  1510. if (filtered.length === 0) {
  1511. this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">No Pokémon match your search.</div>`;
  1512. }
  1513. }
  1514.  
  1515. // -----------------------------
  1516. // Render Pokémon with Sorting & Filtering via Dropdowns (with BST)
  1517. // -----------------------------
  1518. renderPokemon() {
  1519. this.gridContainer.innerHTML = '';
  1520. // Render the custom controls panel for Pokémon filters and sort.
  1521. this.renderPokemonControls();
  1522. if (!this.pokemonList) {
  1523. this.gridContainer.innerHTML = '<div class="spinner"></div>';
  1524. fetch('https://pokeapi.co/api/v2/pokemon?limit=20000')
  1525. .then(response => response.json())
  1526. .then(data => {
  1527. this.pokemonList = data.results;
  1528. this.renderPokemonGrid();
  1529. })
  1530. .catch(err => {
  1531. this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">Error loading Pokémon list</div>`;
  1532. });
  1533. } else {
  1534. this.renderPokemonGrid();
  1535. }
  1536. }
  1537.  
  1538. renderPokemonControls() {
  1539. let controlsContainer = document.getElementById('pokemon-controls');
  1540. if (!controlsContainer) {
  1541. controlsContainer = document.createElement('div');
  1542. controlsContainer.id = 'pokemon-controls';
  1543. Object.assign(controlsContainer.style, {
  1544. display: 'flex',
  1545. flexDirection: 'row',
  1546. flexWrap: 'nowrap',
  1547. justifyContent: 'flex-start',
  1548. alignItems: 'center',
  1549. gap: '2px', // Slightly increased from 3px
  1550. marginBottom: '5px',
  1551. backgroundColor: '#202020',
  1552. color: '#fff',
  1553. padding: '1.8px', // Slightly increased from 2px
  1554. borderRadius: '5px',
  1555. overflowX: 'auto'
  1556. });
  1557. this.gridContainer.parentNode.insertBefore(controlsContainer, this.gridContainer);
  1558. }
  1559.  
  1560. controlsContainer.innerHTML = '';
  1561.  
  1562. // Updated helper function with slight size increases
  1563. const createControl = (labelText, id, optionsHTML, onChange) => {
  1564. const container = document.createElement('div');
  1565. Object.assign(container.style, {
  1566. display: 'flex',
  1567. flexDirection: 'column',
  1568. alignItems: 'center',
  1569. gap: '1.2px', // Slightly increased from 1px
  1570. flexShrink: '0',
  1571. minWidth: '50px', // Increased from 48px
  1572. maxWidth: '76px' // Increased from 72px
  1573. });
  1574.  
  1575. const label = document.createElement('div');
  1576. label.textContent = labelText;
  1577. Object.assign(label.style, {
  1578. fontSize: '11px', // Increased from 10px
  1579. textAlign: 'center',
  1580. whiteSpace: 'nowrap',
  1581. padding: '0 2px'
  1582. });
  1583.  
  1584. const select = document.createElement('select');
  1585. select.id = id;
  1586. Object.assign(select.style, {
  1587. backgroundColor: '#202020',
  1588. color: '#fff',
  1589. border: '1px solid #666',
  1590. borderRadius: '3px',
  1591. padding: '2px', // Increased from 1px
  1592. fontSize: '10px', // Increased from 9px
  1593. width: '92%',
  1594. height: '22px', // Increased from 22px
  1595. cursor: 'pointer'
  1596. });
  1597. select.innerHTML = optionsHTML;
  1598. select.addEventListener('change', onChange);
  1599. container.appendChild(label);
  1600. container.appendChild(select);
  1601. return container;
  1602. };
  1603.  
  1604. // Create all controls
  1605. const typeControl = createControl('Type', 'pokemon-type-filter', `
  1606. <option value="all">All</option>
  1607. <option value="normal">Normal</option>
  1608. <option value="fire">Fire</option>
  1609. <option value="water">Water</option>
  1610. <option value="grass">Grass</option>
  1611. <option value="electric">Electric</option>
  1612. <option value="ice">Ice</option>
  1613. <option value="fighting">Fighting</option>
  1614. <option value="poison">Poison</option>
  1615. <option value="ground">Ground</option>
  1616. <option value="flying">Flying</option>
  1617. <option value="psychic">Psychic</option>
  1618. <option value="bug">Bug</option>
  1619. <option value="rock">Rock</option>
  1620. <option value="ghost">Ghost</option>
  1621. <option value="dragon">Dragon</option>
  1622. <option value="dark">Dark</option>
  1623. <option value="steel">Steel</option>
  1624. <option value="fairy">Fairy</option>
  1625. `, () => {
  1626. this.selectedTypeFilter = document.getElementById('pokemon-type-filter').value;
  1627. this.renderPokemonGrid();
  1628. });
  1629.  
  1630. const weightControl = createControl('Weight', 'pokemon-weight-filter', `
  1631. <option value="all">All</option>
  1632. <option value="light">&lt;30kg</option>
  1633. <option value="medium">30-70kg</option>
  1634. <option value="heavy">&gt;70kg</option>
  1635. `, () => {
  1636. this.selectedWeightFilter = document.getElementById('pokemon-weight-filter').value;
  1637. this.renderPokemonGrid();
  1638. });
  1639.  
  1640. const heightControl = createControl('Height', 'pokemon-height-filter', `
  1641. <option value="all">All</option>
  1642. <option value="short">&lt;1m</option>
  1643. <option value="medium">1-2m</option>
  1644. <option value="tall">&gt;2m</option>
  1645. `, () => {
  1646. this.selectedHeightFilter = document.getElementById('pokemon-height-filter').value;
  1647. this.renderPokemonGrid();
  1648. });
  1649.  
  1650. const bstControl = createControl('BST', 'pokemon-bst-filter', `
  1651. <option value="all">All</option>
  1652. <option value="low">&lt;400</option>
  1653. <option value="medium">400-600</option>
  1654. <option value="high">&gt;600</option>
  1655. `, () => {
  1656. this.selectedBSTFilter = document.getElementById('pokemon-bst-filter').value;
  1657. this.renderPokemonGrid();
  1658. });
  1659.  
  1660. const sortControl = createControl('Sort', 'pokemon-sort', `
  1661. <option value="name-asc">Name A-Z</option>
  1662. <option value="name-desc">Name Z-A</option>
  1663. <option value="dex-asc">Dex Ascending</option>
  1664. <option value="dex-desc">Dex Descending</option>
  1665. <option value="bst-asc">BST Ascending</option>
  1666. <option value="bst-desc">BST Descending</option>
  1667. `, () => {
  1668. this.renderPokemonGrid();
  1669. });
  1670.  
  1671. // Append controls together
  1672. controlsContainer.appendChild(typeControl);
  1673. controlsContainer.appendChild(weightControl);
  1674. controlsContainer.appendChild(heightControl);
  1675. controlsContainer.appendChild(bstControl);
  1676. controlsContainer.appendChild(sortControl);
  1677. }
  1678.  
  1679. /**
  1680. * This function should be called when switching tabs to ensure
  1681. * the sorting controls are removed and re-rendered correctly.
  1682. */
  1683. switchTab(tabName) {
  1684. console.log(`Switching to tab: ${tabName}`);
  1685.  
  1686. // Clear and rebuild controls
  1687. this.renderPokemonControls();
  1688. this.renderPokemonGrid(); // Re-render grid to reflect any new filters
  1689. }
  1690.  
  1691.  
  1692. // Render the Pokémon grid with search, filtering (Type, Weight, Height, BST), and sorting.
  1693. renderPokemonGrid() {
  1694. this.gridContainer.innerHTML = '';
  1695. this.gridContainer.classList.add('browse-container');
  1696. const query = this.searchInput.value.trim().toLowerCase();
  1697. let filtered = this.pokemonList.filter(poke => poke.name.includes(query));
  1698.  
  1699. // Filter by Type if selected.
  1700. if (this.selectedTypeFilter && this.selectedTypeFilter !== 'all') {
  1701. filtered = filtered.filter(poke => {
  1702. if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
  1703. const pokeData = this.pokemonDetailCache.get(poke.url);
  1704. return pokeData.types.some(typeInfo => typeInfo.type.name === this.selectedTypeFilter);
  1705. }
  1706. return true;
  1707. });
  1708. }
  1709.  
  1710. // Filter by Weight if selected.
  1711. if (this.selectedWeightFilter && this.selectedWeightFilter !== 'all') {
  1712. filtered = filtered.filter(poke => {
  1713. if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
  1714. const pokeData = this.pokemonDetailCache.get(poke.url);
  1715. const weightKg = pokeData.weight / 10;
  1716. if (this.selectedWeightFilter === 'light') {
  1717. return weightKg < 30;
  1718. } else if (this.selectedWeightFilter === 'medium') {
  1719. return weightKg >= 30 && weightKg <= 70;
  1720. } else if (this.selectedWeightFilter === 'heavy') {
  1721. return weightKg > 70;
  1722. }
  1723. }
  1724. return true;
  1725. });
  1726. }
  1727.  
  1728. // Filter by Height if selected.
  1729. if (this.selectedHeightFilter && this.selectedHeightFilter !== 'all') {
  1730. filtered = filtered.filter(poke => {
  1731. if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
  1732. const pokeData = this.pokemonDetailCache.get(poke.url);
  1733. const heightM = pokeData.height / 10;
  1734. if (this.selectedHeightFilter === 'short') {
  1735. return heightM < 1;
  1736. } else if (this.selectedHeightFilter === 'medium') {
  1737. return heightM >= 1 && heightM <= 2;
  1738. } else if (this.selectedHeightFilter === 'tall') {
  1739. return heightM > 2;
  1740. }
  1741. }
  1742. return true;
  1743. });
  1744. }
  1745.  
  1746. // Filter by BST if selected.
  1747. if (this.selectedBSTFilter && this.selectedBSTFilter !== 'all') {
  1748. filtered = filtered.filter(poke => {
  1749. if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
  1750. const pokeData = this.pokemonDetailCache.get(poke.url);
  1751. const totalStats = pokeData.stats.reduce((sum, stat) => sum + stat.base_stat, 0);
  1752. if (this.selectedBSTFilter === 'low') {
  1753. return totalStats < 400;
  1754. } else if (this.selectedBSTFilter === 'medium') {
  1755. return totalStats >= 400 && totalStats <= 600;
  1756. } else if (this.selectedBSTFilter === 'high') {
  1757. return totalStats > 600;
  1758. }
  1759. }
  1760. return true;
  1761. });
  1762. }
  1763.  
  1764. // Sort the filtered Pokémon.
  1765. const sortOption = document.getElementById('pokemon-sort')?.value || 'name-asc';
  1766. if (sortOption === 'name-asc') {
  1767. filtered.sort((a, b) => a.name.localeCompare(b.name));
  1768. } else if (sortOption === 'name-desc') {
  1769. filtered.sort((a, b) => b.name.localeCompare(a.name));
  1770. } else if (sortOption === 'dex-asc') {
  1771. filtered.sort((a, b) => {
  1772. const idA = parseInt(a.url.match(/\/pokemon\/(\d+)\//)[1]);
  1773. const idB = parseInt(b.url.match(/\/pokemon\/(\d+)\//)[1]);
  1774. return idA - idB;
  1775. });
  1776. } else if (sortOption === 'dex-desc') {
  1777. filtered.sort((a, b) => {
  1778. const idA = parseInt(a.url.match(/\/pokemon\/(\d+)\//)[1]);
  1779. const idB = parseInt(b.url.match(/\/pokemon\/(\d+)\//)[1]);
  1780. return idB - idA;
  1781. });
  1782. } else if (sortOption === 'bst-asc') {
  1783. filtered.sort((a, b) => {
  1784. const bstA = (this.pokemonDetailCache && this.pokemonDetailCache.has(a.url))
  1785. ? this.pokemonDetailCache.get(a.url).stats.reduce((sum, stat) => sum + stat.base_stat, 0)
  1786. : 0;
  1787. const bstB = (this.pokemonDetailCache && this.pokemonDetailCache.has(b.url))
  1788. ? this.pokemonDetailCache.get(b.url).stats.reduce((sum, stat) => sum + stat.base_stat, 0)
  1789. : 0;
  1790. return bstA - bstB;
  1791. });
  1792. } else if (sortOption === 'bst-desc') {
  1793. filtered.sort((a, b) => {
  1794. const bstA = (this.pokemonDetailCache && this.pokemonDetailCache.has(a.url))
  1795. ? this.pokemonDetailCache.get(a.url).stats.reduce((sum, stat) => sum + stat.base_stat, 0)
  1796. : 0;
  1797. const bstB = (this.pokemonDetailCache && this.pokemonDetailCache.has(b.url))
  1798. ? this.pokemonDetailCache.get(b.url).stats.reduce((sum, stat) => sum + stat.base_stat, 0)
  1799. : 0;
  1800. return bstB - bstA;
  1801. });
  1802. }
  1803.  
  1804. // Build the grid as before.
  1805. const fragment = document.createDocumentFragment();
  1806. filtered.forEach(poke => {
  1807. const tile = document.createElement('div');
  1808. tile.className = 'pokemon-card';
  1809.  
  1810. // Hover effects.
  1811. tile.addEventListener('mouseenter', () => {
  1812. tile.style.boxShadow = '0 0 15px rgba(255, 255, 255, 0.6)';
  1813. tile.style.transform = 'scale(1.05)';
  1814. });
  1815. tile.addEventListener('mouseleave', () => {
  1816. tile.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.1)';
  1817. tile.style.transform = 'scale(1)';
  1818. });
  1819.  
  1820. // Dex Number (Top Left)
  1821. const dexNumber = document.createElement('div');
  1822. dexNumber.className = 'dex-number';
  1823. tile.appendChild(dexNumber);
  1824.  
  1825. // Image Container
  1826. const imageContainer = document.createElement('div');
  1827. imageContainer.className = 'pokemon-image-container';
  1828. tile.appendChild(imageContainer);
  1829.  
  1830. // Pokémon Image with lazy loading.
  1831. const img = document.createElement('img');
  1832. img.className = 'pokemon-image';
  1833. img.loading = 'lazy';
  1834. imageContainer.appendChild(img);
  1835.  
  1836. // Extract Pokémon ID from URL.
  1837. const idMatch = poke.url.match(/\/pokemon\/(\d+)\//);
  1838. const id = idMatch ? idMatch[1] : '';
  1839.  
  1840. // Try several image sources with fallbacks.
  1841. const imageSources = [
  1842. `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/showdown/${id}.gif`,
  1843. `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`,
  1844. `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/${id}.svg`,
  1845. `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`,
  1846. `https://via.placeholder.com/150x150?text=No+Image`
  1847. ];
  1848. let imgIndex = 0;
  1849. function loadNextImage() {
  1850. if (imgIndex >= imageSources.length) return;
  1851. img.src = imageSources[imgIndex];
  1852. img.onerror = () => {
  1853. imgIndex++;
  1854. loadNextImage();
  1855. };
  1856. }
  1857. loadNextImage();
  1858.  
  1859. // Pokémon Name
  1860. const nameLabel = document.createElement('div');
  1861. nameLabel.className = 'pokemon-name';
  1862. nameLabel.textContent = poke.name;
  1863. tile.appendChild(nameLabel);
  1864.  
  1865. // Type Badges (Centered)
  1866. const typesContainer = document.createElement('div');
  1867. typesContainer.className = 'pokemon-types';
  1868. tile.appendChild(typesContainer);
  1869.  
  1870. // Basic Info container (for height, weight, BST, etc.)
  1871. const basicInfoContainer = document.createElement('div');
  1872. basicInfoContainer.className = 'pokemon-info';
  1873. basicInfoContainer.textContent = 'Loading info...';
  1874. tile.appendChild(basicInfoContainer);
  1875.  
  1876. // Function to update the tile with fetched Pokémon details.
  1877. const updatePokemonDetails = (detail) => {
  1878. dexNumber.textContent = `#${detail.id}`;
  1879. typesContainer.innerHTML = '';
  1880. detail.types.forEach(typeInfo => {
  1881. const badge = this.createBadge(
  1882. typeInfo.type.name,
  1883. this.getTypeColor(typeInfo.type.name)
  1884. );
  1885. typesContainer.appendChild(badge);
  1886. });
  1887. const height = (detail.height / 10).toFixed(1);
  1888. const weight = (detail.weight / 10).toFixed(1);
  1889. const baseExp = detail.base_experience;
  1890. const totalStats = detail.stats.reduce((sum, stat) => sum + stat.base_stat, 0);
  1891. basicInfoContainer.innerHTML = `
  1892. <span> ${height}m | ${weight}kg </span>
  1893. <span> Exp: ${baseExp} | BST: ${totalStats} </span>
  1894. `;
  1895. };
  1896.  
  1897. // Fetch details if not already cached.
  1898. if (this.pokemonDetailCache && this.pokemonDetailCache.has(poke.url)) {
  1899. const cachedDetail = this.pokemonDetailCache.get(poke.url);
  1900. updatePokemonDetails(cachedDetail);
  1901. } else {
  1902. fetch(poke.url)
  1903. .then(response => response.json())
  1904. .then(detail => {
  1905. if (!this.pokemonDetailCache) this.pokemonDetailCache = new Map();
  1906. this.pokemonDetailCache.set(poke.url, detail);
  1907. updatePokemonDetails(detail);
  1908. })
  1909. .catch(err => {
  1910. console.error("Error fetching Pokémon detail:", err);
  1911. basicInfoContainer.textContent = 'Info not available';
  1912. });
  1913. }
  1914.  
  1915. // Click event for showing detailed view.
  1916. tile.addEventListener('click', (e) => {
  1917. e.stopPropagation();
  1918. this.panel.classList.add('active');
  1919. this.changeTab('advanced');
  1920. this.searchInput.value = poke.name;
  1921. this.searchAdvancedPokemon(poke.name);
  1922. });
  1923.  
  1924. fragment.appendChild(tile);
  1925. });
  1926.  
  1927. this.gridContainer.appendChild(fragment);
  1928. if (filtered.length === 0) {
  1929. this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">No Pokémon match your search.</div>`;
  1930. }
  1931. }
  1932.  
  1933.  
  1934. // -----------------------------
  1935. // Add Global Event Listeners
  1936. // -----------------------------
  1937. addEventListeners() {
  1938. this.button.addEventListener('mousedown', this.dragStart);
  1939.  
  1940. this.button.addEventListener('click', (e) => {
  1941. if (this.wasDragging) {
  1942. this.wasDragging = false;
  1943. return;
  1944. }
  1945. e.stopPropagation();
  1946. this.panel.classList.toggle('active');
  1947. if (this.panel.classList.contains('active')) {
  1948. this.searchInput.focus();
  1949. }
  1950. });
  1951.  
  1952. document.addEventListener('click', (e) => {
  1953. if (!this.container.contains(e.target)) {
  1954. this.panel.classList.remove('active');
  1955. }
  1956. });
  1957.  
  1958. this.panel.addEventListener('dragstart', (e) => {
  1959. const ballImg = e.target.closest('.pball-item img');
  1960. if (ballImg) {
  1961. e.dataTransfer.setData('text/plain', ballImg.dataset.ballType);
  1962.  
  1963. const dragImg = new Image();
  1964. dragImg.src = ballImg.src;
  1965. dragImg.style.width = '36px';
  1966. dragImg.style.height = '36px';
  1967. dragImg.style.position = 'absolute';
  1968. dragImg.style.left = '-9999px';
  1969. document.body.appendChild(dragImg);
  1970.  
  1971. e.dataTransfer.setDragImage(dragImg, 18, 18);
  1972. setTimeout(() => document.body.removeChild(dragImg), 0);
  1973.  
  1974. ballImg.classList.add('dragging');
  1975. const onDragEnd = () => {
  1976. ballImg.classList.remove('dragging');
  1977. document.removeEventListener('dragend', onDragEnd);
  1978. };
  1979. document.addEventListener('dragend', onDragEnd);
  1980. }
  1981. });
  1982.  
  1983. const chatInput = document.querySelector('#chatInput');
  1984. if (chatInput) {
  1985. chatInput.addEventListener('dragover', (e) => {
  1986. e.preventDefault();
  1987. e.dataTransfer.dropEffect = 'copy';
  1988. });
  1989.  
  1990. chatInput.addEventListener('drop', (e) => {
  1991. e.preventDefault();
  1992. const ballType = e.dataTransfer.getData('text/plain');
  1993. if (ballType) {
  1994. chatInput.value += ` ${ballType}`;
  1995. }
  1996. });
  1997. }
  1998.  
  1999. const tabs = this.panel.querySelectorAll('.pball-tab');
  2000. tabs.forEach(tab => {
  2001. tab.addEventListener('click', (e) => {
  2002. e.stopPropagation();
  2003. this.changeTab(tab.dataset.tab);
  2004. });
  2005. });
  2006.  
  2007. // Use debounced handler for the search input
  2008. const debouncedSearch = this.debounce(() => {
  2009. if (this.currentTab !== 'advanced') {
  2010. this.filterGrid();
  2011. if (this.currentTab === 'pokemon') {
  2012. this.renderPokemonGrid();
  2013. }
  2014. }
  2015. this.clearBtn.style.display = this.searchInput.value.trim() ? 'block' : 'none';
  2016. }, 300);
  2017. this.searchInput.addEventListener('input', debouncedSearch);
  2018.  
  2019. this.clearBtn.addEventListener('click', () => {
  2020. this.searchInput.value = '';
  2021. this.clearBtn.style.display = 'none';
  2022. if (this.currentTab !== 'advanced') {
  2023. this.filterGrid();
  2024. if (this.currentTab === 'browse') {
  2025. this.renderBrowseGrid();
  2026. }
  2027. }
  2028. });
  2029.  
  2030. this.searchInput.addEventListener('keydown', (e) => {
  2031. if (this.currentTab === 'advanced' && e.key === 'Enter') {
  2032. this.searchAdvancedPokemon(this.searchInput.value.trim());
  2033. }
  2034. });
  2035. }
  2036.  
  2037. // 2. Update changeTab() to handle the new "moves" tab
  2038. changeTab(tabName) {
  2039. this.currentTab = tabName;
  2040. const tabs = this.panel.querySelectorAll('.pball-tab');
  2041. tabs.forEach(tab => {
  2042. tab.classList.toggle('active', tab.dataset.tab === tabName);
  2043. });
  2044. if (tabName === 'advanced') {
  2045. this.searchInput.placeholder = 'Enter Pokémon name for detailed info...';
  2046. } else if (tabName === 'pokemon') {
  2047. this.searchInput.placeholder = 'Filter Pokémon...';
  2048. } else if (tabName === 'moves') {
  2049. this.searchInput.placeholder = 'Filter moves...';
  2050. } else {
  2051. this.searchInput.placeholder = 'Search...';
  2052. }
  2053. this.searchInput.value = '';
  2054. this.clearBtn.style.display = 'none';
  2055. this.renderGrid();
  2056. }
  2057.  
  2058. filterGrid() {
  2059. const query = this.searchInput.value.trim().toLowerCase();
  2060. const items = this.gridContainer.querySelectorAll('.pball-item, .browse-tile');
  2061. items.forEach(item => {
  2062. if (!query || item.dataset.label.includes(query)) {
  2063. item.style.display = 'flex';
  2064. } else {
  2065. item.style.display = 'none';
  2066. }
  2067. });
  2068. }
  2069.  
  2070. getChatInput() {
  2071. return document.querySelector('[data-a-target="chat-input"]');
  2072. }
  2073.  
  2074. insertCommand(ballType) {
  2075. const chatInput = this.getChatInput();
  2076. if (!chatInput) return;
  2077. chatInput.focus();
  2078. this.clearChatInput();
  2079. this.insertText(ballType);
  2080. this.triggerInputEvent(chatInput);
  2081. }
  2082.  
  2083. clearChatInput() {
  2084. const chatInput = this.getChatInput();
  2085. if (chatInput) {
  2086. chatInput.value = '';
  2087. this.triggerInputEvent(chatInput);
  2088. }
  2089. }
  2090.  
  2091. insertText(text) {
  2092. document.execCommand('insertText', false, text);
  2093. }
  2094.  
  2095. triggerInputEvent(element) {
  2096. element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
  2097. }
  2098.  
  2099. searchAdvancedPokemon(name) {
  2100. if (!name) return;
  2101. this.gridContainer.innerHTML = '<div class="spinner"></div>';
  2102. fetch(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`)
  2103. .then(response => {
  2104. if (!response.ok) { throw new Error("Pokémon not found"); }
  2105. return response.json();
  2106. })
  2107. .then(data => {
  2108. return fetch(data.species.url)
  2109. .then(res => {
  2110. if (!res.ok) { throw new Error("Species data not found"); }
  2111. return res.json().then(speciesData => ({ data, speciesData }));
  2112. });
  2113. })
  2114. .then(({ data, speciesData }) => {
  2115. return fetch(speciesData.evolution_chain.url)
  2116. .then(res => {
  2117. if (!res.ok) { throw new Error("Evolution chain not found"); }
  2118. return res.json().then(evoData => ({ data, speciesData, evoData }));
  2119. });
  2120. })
  2121. .then(({ data, speciesData, evoData }) => {
  2122. this.displayAdvancedPokemonData(data, speciesData, evoData);
  2123. })
  2124. .catch(err => {
  2125. this.gridContainer.innerHTML = `<div style="padding:12px; color: var(--text-light);">${err.message}</div>`;
  2126. });
  2127. }
  2128.  
  2129. createAdvancedBattleAnalysisTab(data) {
  2130. const container = document.createElement('section');
  2131. container.className = 'advanced-battle-analysis animate-fadeInUp';
  2132. container.style.padding = '1rem';
  2133. container.style.borderTop = '1px solid var(--color-border)';
  2134.  
  2135. // Section title
  2136. const title = document.createElement('h3');
  2137. title.className = 'section-title';
  2138. title.textContent = 'Comprehensive Type Interactions';
  2139. container.appendChild(title);
  2140.  
  2141. // Grid container for type cards
  2142. const grid = document.createElement('div');
  2143. grid.style.display = 'grid';
  2144. grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(250px, 1fr))';
  2145. grid.style.gap = '16px';
  2146.  
  2147. // Create an interactive card for each type
  2148. data.types.forEach(typeObj => {
  2149. const typeCard = document.createElement('div');
  2150. typeCard.className = 'advanced-type-card';
  2151. typeCard.style.border = '1px solid var(--color-border)';
  2152. typeCard.style.borderRadius = '8px';
  2153. typeCard.style.background = 'var(--color-card)';
  2154. typeCard.style.boxShadow = 'var(--box-shadow-light)';
  2155. typeCard.style.overflow = 'hidden';
  2156. typeCard.style.display = 'flex';
  2157. typeCard.style.flexDirection = 'column';
  2158.  
  2159. // Card header with type name and color
  2160. const header = document.createElement('div');
  2161. header.textContent = typeObj.type.name.toUpperCase();
  2162. header.style.background = this.getTypeColor(typeObj.type.name);
  2163. header.style.color = '#fff';
  2164. header.style.padding = '8px';
  2165. header.style.fontWeight = 'bold';
  2166. header.style.textAlign = 'center';
  2167. typeCard.appendChild(header);
  2168.  
  2169. // Tab header container for "Offense" and "Defense"
  2170. const tabHeaderContainer = document.createElement('div');
  2171. tabHeaderContainer.style.display = 'flex';
  2172.  
  2173. // Both buttons now use the card's background
  2174. const offenseTab = document.createElement('button');
  2175. offenseTab.textContent = 'Offense';
  2176. offenseTab.style.flex = '1';
  2177. offenseTab.style.padding = '8px';
  2178. offenseTab.style.border = 'none';
  2179. offenseTab.style.cursor = 'pointer';
  2180. offenseTab.style.background = 'var(--color-card)';
  2181. offenseTab.style.fontWeight = 'bold';
  2182. offenseTab.style.transition = 'border-bottom 0.2s ease';
  2183.  
  2184. const defenseTab = document.createElement('button');
  2185. defenseTab.textContent = 'Defense';
  2186. defenseTab.style.flex = '1';
  2187. defenseTab.style.padding = '8px';
  2188. defenseTab.style.border = 'none';
  2189. defenseTab.style.cursor = 'pointer';
  2190. defenseTab.style.background = 'var(--color-card)';
  2191. defenseTab.style.fontWeight = 'bold';
  2192. defenseTab.style.transition = 'border-bottom 0.2s ease';
  2193.  
  2194. tabHeaderContainer.append(offenseTab, defenseTab);
  2195. typeCard.appendChild(tabHeaderContainer);
  2196.  
  2197. // Create content containers for each tab
  2198. const offenseContent = document.createElement('div');
  2199. offenseContent.style.padding = '8px';
  2200. offenseContent.style.display = 'block';
  2201.  
  2202. const defenseContent = document.createElement('div');
  2203. defenseContent.style.padding = '8px';
  2204. defenseContent.style.display = 'none';
  2205.  
  2206. typeCard.appendChild(offenseContent);
  2207. typeCard.appendChild(defenseContent);
  2208.  
  2209. // Fetch the type data and build the interactive details
  2210. fetch(typeObj.type.url)
  2211. .then(res => res.json())
  2212. .then(typeData => {
  2213. // --- Offense details ---
  2214. const offenseSections = [
  2215. { label: 'Double Damage To', data: typeData.damage_relations.double_damage_to },
  2216. { label: 'Half Damage To', data: typeData.damage_relations.half_damage_to },
  2217. { label: 'No Damage To', data: typeData.damage_relations.no_damage_to }
  2218. ];
  2219. offenseSections.forEach(section => {
  2220. const sectionTitle = document.createElement('h4');
  2221. sectionTitle.textContent = section.label;
  2222. sectionTitle.style.marginBottom = '4px';
  2223. sectionTitle.style.fontSize = '0.9rem';
  2224. offenseContent.appendChild(sectionTitle);
  2225.  
  2226. const sectionContent = document.createElement('div');
  2227. sectionContent.style.display = 'flex';
  2228. sectionContent.style.flexWrap = 'wrap';
  2229. sectionContent.style.gap = '4px';
  2230. if (section.data.length > 0) {
  2231. section.data.forEach(item => {
  2232. const badge = document.createElement('span');
  2233. badge.textContent = item.name.toUpperCase();
  2234. badge.style.background = this.getTypeColor(item.name);
  2235. badge.style.color = '#fff';
  2236. badge.style.padding = '2px 6px';
  2237. badge.style.borderRadius = '4px';
  2238. badge.style.fontSize = '0.8rem';
  2239. sectionContent.appendChild(badge);
  2240. });
  2241. } else {
  2242. const noData = document.createElement('span');
  2243. noData.textContent = 'None';
  2244. noData.style.fontSize = '0.8rem';
  2245. sectionContent.appendChild(noData);
  2246. }
  2247. offenseContent.appendChild(sectionContent);
  2248. });
  2249.  
  2250. // --- Defense details ---
  2251. const defenseSections = [
  2252. { label: 'Double Damage From', data: typeData.damage_relations.double_damage_from },
  2253. { label: 'Half Damage From', data: typeData.damage_relations.half_damage_from },
  2254. { label: 'No Damage From', data: typeData.damage_relations.no_damage_from }
  2255. ];
  2256. defenseSections.forEach(section => {
  2257. const sectionTitle = document.createElement('h4');
  2258. sectionTitle.textContent = section.label;
  2259. sectionTitle.style.marginBottom = '4px';
  2260. sectionTitle.style.fontSize = '0.9rem';
  2261. defenseContent.appendChild(sectionTitle);
  2262.  
  2263. const sectionContent = document.createElement('div');
  2264. sectionContent.style.display = 'flex';
  2265. sectionContent.style.flexWrap = 'wrap';
  2266. sectionContent.style.gap = '4px';
  2267. if (section.data.length > 0) {
  2268. section.data.forEach(item => {
  2269. const badge = document.createElement('span');
  2270. badge.textContent = item.name.toUpperCase();
  2271. badge.style.background = this.getTypeColor(item.name);
  2272. badge.style.color = '#fff';
  2273. badge.style.padding = '2px 6px';
  2274. badge.style.borderRadius = '4px';
  2275. badge.style.fontSize = '0.8rem';
  2276. sectionContent.appendChild(badge);
  2277. });
  2278. } else {
  2279. const noData = document.createElement('span');
  2280. noData.textContent = 'None';
  2281. noData.style.fontSize = '0.8rem';
  2282. sectionContent.appendChild(noData);
  2283. }
  2284. defenseContent.appendChild(sectionContent);
  2285. });
  2286. })
  2287. .catch(err => {
  2288. console.error('Error fetching type interactions:', err);
  2289. offenseContent.textContent = 'Unable to load data.';
  2290. defenseContent.textContent = 'Unable to load data.';
  2291. });
  2292.  
  2293. // --- Tab switching functionality ---
  2294. // Instead of changing the background, we use a bottom border to indicate the active tab.
  2295. offenseTab.addEventListener('click', () => {
  2296. offenseTab.style.borderBottom = '2px solid var(--color-primary)';
  2297. defenseTab.style.borderBottom = 'none';
  2298. offenseContent.style.display = 'block';
  2299. defenseContent.style.display = 'none';
  2300. });
  2301. defenseTab.addEventListener('click', () => {
  2302. defenseTab.style.borderBottom = '2px solid var(--color-primary)';
  2303. offenseTab.style.borderBottom = 'none';
  2304. offenseContent.style.display = 'none';
  2305. defenseContent.style.display = 'block';
  2306. });
  2307.  
  2308. // Set initial active state for offense tab
  2309. offenseTab.style.borderBottom = '2px solid var(--color-primary)';
  2310.  
  2311. grid.appendChild(typeCard);
  2312. });
  2313.  
  2314. container.appendChild(grid);
  2315. return container;
  2316. }
  2317.  
  2318.  
  2319. /**
  2320. * Helper function to get a color based on the Pokémon type.
  2321. * @param {string} typeName
  2322. * @returns {string} The color string.
  2323. */
  2324. getTypeColor(typeName) {
  2325. const typeColors = {
  2326. normal: '#A8A878',
  2327. fire: '#F08030',
  2328. water: '#6890F0',
  2329. electric: '#F8D030',
  2330. grass: '#78C850',
  2331. ice: '#98D8D8',
  2332. fighting: '#C03028',
  2333. poison: '#A040A0',
  2334. ground: '#E0C068',
  2335. flying: '#A890F0',
  2336. psychic: '#F85888',
  2337. bug: '#A8B820',
  2338. rock: '#B8A038',
  2339. ghost: '#705898',
  2340. dragon: '#7038F8',
  2341. dark: '#705848',
  2342. steel: '#B8B8D0',
  2343. fairy: '#EE99AC'
  2344. };
  2345. return typeColors[typeName] || '#68A090';
  2346. }
  2347.  
  2348. // CARD HEADER WITH IMAGE, NAME, AND DETAILS
  2349. createCardHeader(data) {
  2350. const header = document.createElement('header');
  2351. header.className = 'poke-card-header animate-slideDown';
  2352. header.setAttribute('role', 'banner');
  2353.  
  2354. // Image container with lazy loading and type badges
  2355. const imgContainer = document.createElement('div');
  2356. imgContainer.style.position = 'relative';
  2357.  
  2358. const img = document.createElement('img');
  2359. img.className = 'poke-image lazy';
  2360. img.style.width = '90px';
  2361. img.style.height = '90px';
  2362. img.style.borderRadius = '16px';
  2363. img.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
  2364. img.dataset.src =
  2365. data.sprites.other?.['official-artwork']?.front_default ||
  2366. data.sprites.front_default;
  2367. img.src = ''; // initially empty—loaded via IntersectionObserver
  2368.  
  2369. // Type badges positioned over the image
  2370. const typeBadges = document.createElement('div');
  2371. typeBadges.style.display = 'flex';
  2372. typeBadges.style.gap = '4px';
  2373. typeBadges.style.position = 'absolute';
  2374. typeBadges.style.bottom = '60px';
  2375. typeBadges.style.left = '80%';
  2376. typeBadges.style.transform = 'translateX(-50%)';
  2377. data.types.forEach(type => {
  2378. const badge = document.createElement('span');
  2379. badge.className = 'type-badge';
  2380. badge.textContent = type.type.name.toUpperCase();
  2381. badge.style.background = this.getTypeColor(type.type.name);
  2382. badge.style.padding = '3px 3px';
  2383. badge.style.borderRadius = '5px';
  2384. badge.style.fontSize = '12px';
  2385. badge.style.fontWeight = '700';
  2386. badge.style.color = '#fff';
  2387. badge.style.textShadow = '0 1px 2px rgba(0,0,0,0.3)';
  2388. typeBadges.appendChild(badge);
  2389. });
  2390. imgContainer.append(img, typeBadges);
  2391.  
  2392. // Title and details section
  2393. const titleSection = document.createElement('div');
  2394. const title = document.createElement('h1');
  2395. title.className = 'poke-title';
  2396. title.style.fontSize = '32px';
  2397. title.style.margin = '0 0 8px';
  2398. title.textContent =
  2399. data.name.charAt(0).toUpperCase() + data.name.slice(1);
  2400.  
  2401. const details = document.createElement('div');
  2402. details.style.display = 'grid';
  2403. details.style.gridTemplateColumns = 'repeat(3, auto)';
  2404. details.style.gap = '16px';
  2405. details.innerHTML = `
  2406. <div class="detail-item">
  2407. <span class="detail-label">ID</span>
  2408. <span class="detail-value">#${data.id
  2409. .toString()
  2410. .padStart(3, '0')}</span>
  2411. </div>
  2412. <div class="detail-item">
  2413. <span class="detail-label">EXP</span>
  2414. <span class="detail-value">${data.base_experience}</span>
  2415. </div>
  2416. <div class="detail-item">
  2417. <span class="detail-label">SPECIES</span>
  2418. <span class="detail-value">${data.species.name}</span>
  2419. </div>
  2420. `;
  2421. titleSection.append(title, details);
  2422. header.append(imgContainer, titleSection);
  2423. return header;
  2424. }
  2425.  
  2426. // POKÉDEX ENTRY SECTION
  2427. createPokedexEntrySection(speciesData) {
  2428. const section = document.createElement('section');
  2429. section.className = 'pokedex-entry-section animate-fadeInUp';
  2430. section.innerHTML = `<h3 class="section-title">POKÉDEX ENTRY</h3>`;
  2431. const entry = speciesData.flavor_text_entries.find(
  2432. e => e.language.name === 'en'
  2433. );
  2434. const flavorText = entry
  2435. ? entry.flavor_text.replace(/\f|\n/g, ' ')
  2436. : 'No entry available.';
  2437. const entryContainer = document.createElement('div');
  2438. entryContainer.className = 'pokedex-entry';
  2439. entryContainer.style.background = 'var(--background-darker)';
  2440. entryContainer.style.padding = '16px';
  2441. entryContainer.style.borderRadius = '8px';
  2442. entryContainer.style.fontSize = '14px';
  2443. entryContainer.style.lineHeight = '1.5';
  2444. entryContainer.style.color = 'var(--text-muted)';
  2445. entryContainer.textContent = flavorText;
  2446. section.appendChild(entryContainer);
  2447. return section;
  2448. }
  2449.  
  2450. // BASIC PHYSICAL INFORMATION SECTION
  2451. createBasicInfoSection(data) {
  2452. const section = document.createElement('section');
  2453. section.className = 'info-grid animate-fadeInUp';
  2454. section.innerHTML = `
  2455. <h3 class="section-title">PHYSICAL TRAITS</h3>
  2456. <div class="metric">
  2457. <i class="icon-height"></i>
  2458. <span class="label">Height</span>
  2459. <span class="value">${data.height / 10}m</span>
  2460. </div>
  2461. <div class="metric">
  2462. <i class="icon-weight"></i>
  2463. <span class="label">Weight</span>
  2464. <span class="value">${data.weight / 10}kg</span>
  2465. </div>
  2466. <div class="metric">
  2467. <i class="icon-stats"></i>
  2468. <span class="label">Total Stats</span>
  2469. <span class="value">${data.stats.reduce(
  2470. (sum, s) => sum + s.base_stat,
  2471. 0
  2472. )}</span>
  2473. </div>
  2474. `;
  2475. return section;
  2476. }
  2477. // ABILITIES SECTION WITH TOOLTIP (using async/await)
  2478. createAbilitiesSection(data) {
  2479. const section = document.createElement('section');
  2480. section.className = 'abilities-section animate-fadeInUp';
  2481. section.innerHTML = `<h3 class="section-title">ABILITIES</h3>`;
  2482. const abilitiesGrid = document.createElement('div');
  2483. abilitiesGrid.className = 'abilities-grid';
  2484. abilitiesGrid.style.display = 'grid';
  2485. abilitiesGrid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(140px, 1fr))';
  2486. abilitiesGrid.style.gap = '12px';
  2487.  
  2488. data.abilities.forEach(ability => {
  2489. const abilityCard = document.createElement('div');
  2490. abilityCard.className = 'ability-card';
  2491. abilityCard.style.background = 'var(--background-darker)';
  2492. abilityCard.style.padding = '12px';
  2493. abilityCard.style.borderRadius = '8px';
  2494. abilityCard.style.textAlign = 'center';
  2495. abilityCard.style.position = 'relative';
  2496. abilityCard.style.cursor = 'pointer';
  2497.  
  2498. const abilityName = document.createElement('div');
  2499. abilityName.textContent = ability.ability.name.replace(/-/g, ' ');
  2500. abilityName.style.fontWeight = '500';
  2501. abilityName.style.textTransform = 'capitalize';
  2502.  
  2503. if (ability.is_hidden) {
  2504. const hiddenBadge = document.createElement('div');
  2505. hiddenBadge.textContent = 'Hidden';
  2506. hiddenBadge.style.position = 'absolute';
  2507. hiddenBadge.style.top = '4px';
  2508. hiddenBadge.style.right = '4px';
  2509. hiddenBadge.style.background = '#FF6B6B';
  2510. hiddenBadge.style.color = '#FFF';
  2511. hiddenBadge.style.fontSize = '10px';
  2512. hiddenBadge.style.padding = '2px 6px';
  2513. hiddenBadge.style.borderRadius = '12px';
  2514. abilityCard.appendChild(hiddenBadge);
  2515. }
  2516. abilityCard.appendChild(abilityName);
  2517. abilitiesGrid.appendChild(abilityCard);
  2518.  
  2519. // Use async/await for fetching ability descriptions
  2520. abilityCard.addEventListener('mouseenter', async () => {
  2521. try {
  2522. const res = await fetch(ability.ability.url);
  2523. const abilityData = await res.json();
  2524. const description =
  2525. abilityData.effect_entries.find(e => e.language.name === 'en')?.effect ||
  2526. 'No description available.';
  2527. this.showTooltip(abilityCard, description);
  2528. } catch (err) {
  2529. console.error('Error fetching ability data:', err);
  2530. }
  2531. });
  2532. abilityCard.addEventListener('mouseleave', () => {
  2533. this.hideTooltip();
  2534. });
  2535. });
  2536. section.appendChild(abilitiesGrid);
  2537. return section;
  2538. }
  2539.  
  2540. // Show tooltip with smooth fade-in/out transitions using a solid background color
  2541. showTooltip(element, text) {
  2542. if (this.tooltip) this.tooltip.remove();
  2543. this.tooltip = document.createElement('div');
  2544. this.tooltip.className = 'tooltip animate-fadeIn';
  2545. this.tooltip.textContent = text;
  2546. const rect = element.getBoundingClientRect();
  2547. const viewportHeight = window.innerHeight;
  2548. const tooltipHeight = 100; // estimated height
  2549. let topPosition = rect.bottom + 8;
  2550. if (topPosition + tooltipHeight > viewportHeight) {
  2551. topPosition = rect.top - tooltipHeight - 8;
  2552. }
  2553. Object.assign(this.tooltip.style, {
  2554. background: 'var(--color-card)', // Updated to a solid color
  2555. color: 'var(--text-light)',
  2556. borderRadius: '6px',
  2557. padding: '8px 12px',
  2558. position: 'fixed',
  2559. top: `${topPosition}px`,
  2560. left: `${rect.left}px`,
  2561. maxWidth: '240px',
  2562. zIndex: '10000',
  2563. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
  2564. pointerEvents: 'none',
  2565. opacity: '0',
  2566. transition: 'opacity 0.3s ease'
  2567. });
  2568. document.body.appendChild(this.tooltip);
  2569. requestAnimationFrame(() => {
  2570. this.tooltip.style.opacity = '1';
  2571. });
  2572. }
  2573.  
  2574. hideTooltip() {
  2575. if (this.tooltip) {
  2576. this.tooltip.style.opacity = '0';
  2577. setTimeout(() => {
  2578. if (this.tooltip) {
  2579. this.tooltip.remove();
  2580. this.tooltip = null;
  2581. }
  2582. }, 300);
  2583. }
  2584. }
  2585.  
  2586. // MOVES SECTION WITH SEARCH AND DETAILED TOOLTIP
  2587. createMovesSection(data) {
  2588. const section = document.createElement('section');
  2589. section.className = 'moves-section animate-fadeInUp';
  2590. section.innerHTML = `<h3 class="section-title">MOVES</h3>`;
  2591.  
  2592. // Search container (same as before)
  2593. const searchContainer = document.createElement('div');
  2594. searchContainer.className = 'pball-search-container';
  2595. const searchInput = document.createElement('input');
  2596. searchInput.className = 'pball-search';
  2597. searchInput.placeholder = 'Search moves...';
  2598. searchInput.setAttribute('aria-label', 'Search moves');
  2599. const searchButtons = document.createElement('div');
  2600. searchButtons.className = 'search-buttons';
  2601. const enterButton = document.createElement('button');
  2602. enterButton.className = 'search-enter-button';
  2603. enterButton.textContent = '✔';
  2604. const clearButton = document.createElement('button');
  2605. clearButton.className = 'pball-clear-btn';
  2606. clearButton.textContent = 'X';
  2607.  
  2608. let searchTimeout;
  2609. const handleSearch = () => {
  2610. clearTimeout(searchTimeout);
  2611. searchTimeout = setTimeout(() => {
  2612. const query = searchInput.value.trim().toLowerCase();
  2613. Array.from(movesList.children).forEach(move => {
  2614. move.style.display = move.textContent.toLowerCase().includes(query)
  2615. ? 'block'
  2616. : 'none';
  2617. });
  2618. }, 300);
  2619. };
  2620.  
  2621. searchInput.addEventListener('input', handleSearch);
  2622. enterButton.addEventListener('click', handleSearch);
  2623. clearButton.addEventListener('click', () => {
  2624. searchInput.value = '';
  2625. handleSearch();
  2626. });
  2627. searchButtons.append(enterButton, clearButton);
  2628. searchContainer.append(searchInput, searchButtons);
  2629. section.append(searchContainer);
  2630.  
  2631. // Moves list container with virtual scroll behavior
  2632. const movesList = document.createElement('div');
  2633. movesList.className = 'moves-list';
  2634. movesList.style.maxHeight = '200px';
  2635. movesList.style.overflowY = 'auto';
  2636. movesList.style.display = 'grid';
  2637. movesList.style.gap = '8px';
  2638.  
  2639. data.moves.forEach(move => {
  2640. const moveItem = document.createElement('div');
  2641. moveItem.className = 'move-item';
  2642. moveItem.textContent = move.move.name.replace(/-/g, ' ');
  2643. moveItem.style.padding = '8px 12px';
  2644. moveItem.style.background = 'var(--background-darker)';
  2645. moveItem.style.borderRadius = '6px';
  2646. moveItem.style.textTransform = 'capitalize';
  2647. moveItem.style.cursor = 'pointer';
  2648.  
  2649. // Fetch and show move details on hover
  2650. moveItem.addEventListener('mouseenter', async () => {
  2651. try {
  2652. const res = await fetch(move.move.url);
  2653. const moveData = await res.json();
  2654. const effectEntry = moveData.effect_entries.find(
  2655. e => e.language.name === 'en'
  2656. );
  2657. const effectText = effectEntry
  2658. ? effectEntry.short_effect.replace(/\n|\f/g, ' ')
  2659. : 'No description available.';
  2660. // Format move details
  2661. const details = `
  2662. Name: ${moveData.name.replace(/-/g, ' ')}
  2663. Type: ${moveData.type.name.toUpperCase()}
  2664. Category: ${moveData.damage_class.name.toUpperCase()}
  2665. Power: ${moveData.power || '—'}
  2666. Accuracy: ${moveData.accuracy || '—'}
  2667. PP: ${moveData.pp}
  2668. Priority: ${moveData.priority}
  2669. Effect: ${effectText}
  2670. `;
  2671. this.showTooltip(moveItem, details);
  2672. } catch (error) {
  2673. console.error('Error fetching move data:', error);
  2674. }
  2675. });
  2676.  
  2677. moveItem.addEventListener('mouseleave', () => {
  2678. this.hideTooltip();
  2679. });
  2680.  
  2681. movesList.appendChild(moveItem);
  2682. });
  2683. section.appendChild(movesList);
  2684. return section;
  2685. }
  2686.  
  2687.  
  2688. // HELD ITEMS SECTION
  2689. createHeldItemsSection(data) {
  2690. const section = document.createElement('section');
  2691. section.className = 'held-items-section animate-fadeInUp';
  2692. section.innerHTML = `<h3 class="section-title">HELD ITEMS</h3>`;
  2693. const itemsGrid = document.createElement('div');
  2694. itemsGrid.className = 'items-grid';
  2695. itemsGrid.style.display = 'grid';
  2696. itemsGrid.style.gridTemplateColumns =
  2697. 'repeat(auto-fit, minmax(120px, 1fr))';
  2698. itemsGrid.style.gap = '12px';
  2699.  
  2700. data.held_items.forEach(item => {
  2701. const itemCard = document.createElement('div');
  2702. itemCard.className = 'item-card';
  2703. itemCard.textContent = item.item.name.replace(/-/g, ' ');
  2704. itemCard.style.padding = '12px';
  2705. itemCard.style.background = 'var(--background-darker)';
  2706. itemCard.style.borderRadius = '8px';
  2707. itemCard.style.textAlign = 'center';
  2708. itemCard.style.textTransform = 'capitalize';
  2709. itemsGrid.appendChild(itemCard);
  2710. });
  2711. section.appendChild(itemsGrid);
  2712. return section;
  2713. }
  2714.  
  2715. // FORMS SECTION
  2716. createFormsSection(data) {
  2717. const section = document.createElement('section');
  2718. section.className = 'forms-section animate-fadeInUp';
  2719. section.innerHTML = `<h3 class="section-title">FORMS</h3>`;
  2720. const formsGrid = document.createElement('div');
  2721. formsGrid.className = 'forms-grid';
  2722. formsGrid.style.display = 'grid';
  2723. formsGrid.style.gridTemplateColumns =
  2724. 'repeat(auto-fit, minmax(120px, 1fr))';
  2725. formsGrid.style.gap = '12px';
  2726.  
  2727. data.forms.forEach(form => {
  2728. const formCard = document.createElement('div');
  2729. formCard.className = 'form-card';
  2730. formCard.textContent = form.name.replace(/-/g, ' ');
  2731. formCard.style.padding = '12px';
  2732. formCard.style.background = 'var(--background-darker)';
  2733. formCard.style.borderRadius = '8px';
  2734. formCard.style.textAlign = 'center';
  2735. formCard.style.textTransform = 'capitalize';
  2736. formsGrid.appendChild(formCard);
  2737. });
  2738. section.appendChild(formsGrid);
  2739. return section;
  2740. }
  2741.  
  2742. // STATS RADAR CHART SECTION (with Chart.js and animated rendering)
  2743. createStatsRadarChart(data) {
  2744. const section = document.createElement('section');
  2745. section.className = 'stats-radar-card animate-fadeInUp';
  2746. section.innerHTML = `
  2747. <div class="stats-header">
  2748. <h3 class="section-title">Stat Distribution</h3>
  2749. </span>
  2750. </div>
  2751. </div>
  2752. </div>
  2753. `;
  2754. const chartContainer = document.createElement('div');
  2755. chartContainer.className = 'radar-container';
  2756. chartContainer.style.position = 'relative';
  2757. chartContainer.style.height = 'clamp(280px, 35vh, 400px)';
  2758. chartContainer.style.margin = '16px 0';
  2759.  
  2760. const canvas = document.createElement('canvas');
  2761. canvas.setAttribute('aria-label', 'Pokémon stat radar chart');
  2762. canvas.style.touchAction = 'none';
  2763.  
  2764. const typeColor = this.getTypeColor(data.types[0].type.name);
  2765. const gradient = {
  2766. light: this.hexToRgba(typeColor, 0.3),
  2767. dark: this.hexToRgba(typeColor, 0.1)
  2768. };
  2769.  
  2770. if (!window.Chart) {
  2771. const script = document.createElement('script');
  2772. script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
  2773. script.onload = () => this.drawEnhancedRadar(canvas, data, gradient);
  2774. script.onerror = () => this.showChartError(chartContainer);
  2775. document.head.appendChild(script);
  2776. } else {
  2777. this.drawEnhancedRadar(canvas, data, gradient);
  2778. }
  2779. chartContainer.appendChild(canvas);
  2780. section.appendChild(chartContainer);
  2781. return section;
  2782. }
  2783.  
  2784. drawEnhancedRadar(canvas, data, gradient) {
  2785. try {
  2786. const ctx = canvas.getContext('2d');
  2787. const stats = data.stats.map(s => s.base_stat);
  2788. const labels = data.stats.map(s => ({
  2789. full: s.stat.name.replace(/-/g, ' '),
  2790. short: this.getStatAbbreviation(s.stat.name)
  2791. }));
  2792. const chartGradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
  2793. chartGradient.addColorStop(0, gradient.light);
  2794. chartGradient.addColorStop(1, gradient.dark);
  2795.  
  2796. new Chart(ctx, {
  2797. type: 'radar',
  2798. data: {
  2799. labels: labels.map(l => l.short),
  2800. datasets: [
  2801. {
  2802. data: stats,
  2803. backgroundColor: chartGradient,
  2804. borderColor: this.hexToRgba(gradient.light, 0.8),
  2805. borderWidth: 1.8,
  2806. pointBackgroundColor: '#ffffff',
  2807. pointBorderColor: gradient.light,
  2808. pointHoverRadius: 8,
  2809. pointRadius: 4,
  2810. pointHitRadius: 12,
  2811. fill: true
  2812. }
  2813. ]
  2814. },
  2815. options: {
  2816. responsive: true,
  2817. maintainAspectRatio: false,
  2818. animation: {
  2819. duration: 800,
  2820. easing: 'easeOutQuint'
  2821. },
  2822. scales: {
  2823. r: {
  2824. beginAtZero: true,
  2825. max: Math.ceil(Math.max(...stats) / 10) * 10 + 10,
  2826. ticks: {
  2827. display: false,
  2828. count: 5,
  2829. z: 1
  2830. },
  2831. grid: {
  2832. color: 'rgba(255, 255, 255, 0.12)',
  2833. circular: true,
  2834. lineWidth: 0.8
  2835. },
  2836. pointLabels: {
  2837. color: '#ffffff',
  2838. font: {
  2839. size: 13,
  2840. weight: '500'
  2841. },
  2842. callback: (value, index) => [`${value}`, stats[index]],
  2843. padding: 18
  2844. },
  2845. angleLines: {
  2846. color: 'rgba(255, 255, 255, 0.08)',
  2847. lineWidth: 0.8
  2848. }
  2849. }
  2850. },
  2851. plugins: {
  2852. legend: { display: false },
  2853. tooltip: {
  2854. enabled: true,
  2855. intersect: false,
  2856. callbacks: {
  2857. title: items => labels[items[0].dataIndex].full,
  2858. label: context => `Base Stat: ${context.raw}`
  2859. },
  2860. bodyFont: { size: 13 },
  2861. titleFont: { size: 12 },
  2862. padding: 14,
  2863. backgroundColor: 'rgba(28, 28, 34, 0.96)',
  2864. borderColor: 'rgba(255, 255, 255, 0.12)',
  2865. borderWidth: 1,
  2866. cornerRadius: 8,
  2867. boxShadow: '0 4px 12px rgba(0,0,0,0.24)'
  2868. },
  2869. annotation: {
  2870. annotations: {
  2871. avgLine: {
  2872. type: 'line',
  2873. borderColor: 'rgba(255, 255, 255, 0.2)',
  2874. borderWidth: 1,
  2875. borderDash: [4, 4],
  2876. scaleID: 'r',
  2877. value: stats.reduce((a, b) => a + b, 0) / stats.length
  2878. }
  2879. }
  2880. }
  2881. },
  2882. onHover: (event, elements) => {
  2883. canvas.style.cursor = elements.length ? 'pointer' : 'default';
  2884. }
  2885. }
  2886. });
  2887. } catch (error) {
  2888. this.showChartError(canvas.parentElement);
  2889. }
  2890. }
  2891.  
  2892. // Utility to convert HEX to RGBA
  2893. hexToRgba(hex, alpha = 1) {
  2894. const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
  2895. return `rgba(${r},${g},${b},${alpha})`;
  2896. }
  2897.  
  2898. // Abbreviate stat names
  2899. getStatAbbreviation(statName) {
  2900. const abbreviations = {
  2901. hp: 'HP',
  2902. attack: 'ATK',
  2903. defense: 'DEF',
  2904. 'special-attack': 'SP.ATK',
  2905. 'special-defense': 'SP.DEF',
  2906. speed: 'SPD'
  2907. };
  2908. return abbreviations[statName] || statName.slice(0, 3).toUpperCase();
  2909. }
  2910.  
  2911. // Display error if Chart.js fails
  2912. showChartError(container) {
  2913. container.innerHTML = `
  2914. <div class="chart-error">
  2915. <svg class="error-icon" viewBox="0 0 24 24" width="48" height="48">
  2916. <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"/>
  2917. </svg>
  2918. <div class="error-message">
  2919. <h4>Chart Unavailable</h4>
  2920. <p>Failed to load stat visualization</p>
  2921. </div>
  2922. </div>
  2923. `;
  2924. }
  2925.  
  2926. // TYPE INTERACTIONS SECTION
  2927. createTypeRelationsGrid(data) {
  2928. const section = document.createElement('section');
  2929. section.className = 'type-relations-grid animate-fadeInUp';
  2930. section.innerHTML = `<h3 class="section-title">TYPE INTERACTIONS</h3>`;
  2931. const grid = document.createElement('div');
  2932. grid.style.display = 'grid';
  2933. grid.style.gridTemplateColumns =
  2934. 'repeat(auto-fit, minmax(160px, 1fr))';
  2935. grid.style.gap = '12px';
  2936.  
  2937. data.types.forEach(type => {
  2938. const typeCard = document.createElement('div');
  2939. typeCard.className = 'type-card';
  2940. typeCard.innerHTML = `
  2941. <div class="type-header">${type.type.name.toUpperCase()}</div>
  2942. <div class="damage-relations">
  2943. <div class="strengths">
  2944. <h4>STRONG VS</h4>
  2945. <div class="types-list"></div>
  2946. </div>
  2947. <div class="weaknesses">
  2948. <h4>WEAK TO</h4>
  2949. <div class="types-list"></div>
  2950. </div>
  2951. </div>
  2952. `;
  2953. const typeHeader = typeCard.querySelector('.type-header');
  2954. typeHeader.style.background = this.getTypeColor(type.type.name);
  2955. fetch(type.type.url)
  2956. .then(res => res.json())
  2957. .then(typeData => {
  2958. const strengths = typeData.damage_relations.double_damage_to;
  2959. const weaknesses = typeData.damage_relations.double_damage_from;
  2960. strengths.forEach(t => {
  2961. const badge = this.createTypeBadge(t.name);
  2962. typeCard.querySelector('.strengths .types-list').appendChild(badge);
  2963. });
  2964. weaknesses.forEach(t => {
  2965. const badge = this.createTypeBadge(t.name);
  2966. typeCard.querySelector('.weaknesses .types-list').appendChild(badge);
  2967. });
  2968. });
  2969. grid.appendChild(typeCard);
  2970. });
  2971. section.appendChild(grid);
  2972. return section;
  2973. }
  2974.  
  2975. // Create a small type badge element
  2976. createTypeBadge(typeName) {
  2977. const badge = document.createElement('span');
  2978. badge.className = 'type-badge small';
  2979. badge.textContent = typeName.toUpperCase();
  2980. badge.style.background = this.getTypeColor(typeName);
  2981. badge.style.padding = '2px 8px';
  2982. badge.style.borderRadius = '12px';
  2983. badge.style.fontSize = '10px';
  2984. return badge;
  2985. }
  2986.  
  2987. // Get a color based on the Pokémon type
  2988. getTypeColor(typeName) {
  2989. const typeColors = {
  2990. normal: '#A8A878',
  2991. fire: '#F08030',
  2992. water: '#6890F0',
  2993. electric: '#F8D030',
  2994. grass: '#78C850',
  2995. ice: '#98D8D8',
  2996. fighting: '#C03028',
  2997. poison: '#A040A0',
  2998. ground: '#E0C068',
  2999. flying: '#A890F0',
  3000. psychic: '#F85888',
  3001. bug: '#A8B820',
  3002. rock: '#B8A038',
  3003. ghost: '#705898',
  3004. dragon: '#7038F8',
  3005. dark: '#705848',
  3006. steel: '#B8B8D0',
  3007. fairy: '#EE99AC'
  3008. };
  3009. return typeColors[typeName] || '#68A090';
  3010. }
  3011. // EVOLUTION CHAIN VISUALIZATION WITH MULTIPLE IMAGE SOURCES & FALLBACK HANDLING
  3012. createEvolutionVisualization(chain) {
  3013. const section = document.createElement('section');
  3014. section.className = 'evolution-chain animate-fadeInUp';
  3015. section.innerHTML = `<h3 class="section-title">EVOLUTION LINE</h3>`;
  3016. const stages = this.parseEvolutionChain(chain);
  3017. const container = document.createElement('div');
  3018. container.style.display = 'flex';
  3019. container.style.justifyContent = 'center';
  3020. container.style.gap = '0px';
  3021. container.style.padding = '16px 0';
  3022.  
  3023. stages.forEach((stage, index) => {
  3024. const stageDiv = document.createElement('div');
  3025. stageDiv.style.display = 'flex';
  3026. stageDiv.style.flexDirection = 'column';
  3027. stageDiv.style.alignItems = 'center';
  3028. stageDiv.style.gap = '8px';
  3029.  
  3030. if (index > 0) {
  3031. const arrow = document.createElement('div');
  3032. arrow.textContent = '→';
  3033. arrow.style.fontSize = '24px';
  3034. arrow.style.opacity = '0.6';
  3035. container.appendChild(arrow);
  3036. }
  3037. const sprite = document.createElement('img');
  3038. sprite.className = 'lazy';
  3039. sprite.alt = stage.name;
  3040. sprite.style.width = '64px';
  3041. sprite.style.height = '64px';
  3042. // Initially hidden until the image loads
  3043. sprite.style.opacity = '0';
  3044.  
  3045. // Attach onload event to reveal the image once it loads
  3046. sprite.onload = () => {
  3047. sprite.style.opacity = '1';
  3048. };
  3049.  
  3050. // Set multiple image sources and fallback handling
  3051. const imageSources = [
  3052. `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/showdown/${stage.id}.gif`, // Animated
  3053. `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${stage.id}.png`, // Official
  3054. `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/${stage.id}.svg`, // SVG
  3055. `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${stage.id}.png`, // Default
  3056. `https://via.placeholder.com/150x150?text=No+Image` // Placeholder
  3057. ];
  3058.  
  3059. let imgIndex = 0;
  3060. function loadNextImage() {
  3061. if (imgIndex >= imageSources.length) return;
  3062. sprite.src = imageSources[imgIndex++];
  3063. // If the current image fails to load, try the next one
  3064. sprite.onerror = loadNextImage;
  3065. }
  3066. loadNextImage();
  3067.  
  3068. const name = document.createElement('div');
  3069. name.textContent = stage.name;
  3070. name.style.fontWeight = '500';
  3071. stageDiv.append(sprite, name);
  3072. container.appendChild(stageDiv);
  3073. });
  3074. section.appendChild(container);
  3075. // If you have a lazy loading observer, you may reinitialize it here:
  3076. // this.initIntersectionObserver();
  3077. return section;
  3078. }
  3079.  
  3080. // Recursively parse the evolution chain into an array of stages
  3081. parseEvolutionChain(chain, result = []) {
  3082. const id = chain.species.url.split('/').slice(-2, -1)[0];
  3083. result.push({ name: chain.species.name, id });
  3084. if (chain.evolves_to.length > 0) {
  3085. chain.evolves_to.forEach(e => this.parseEvolutionChain(e, result));
  3086. }
  3087. return result.filter((v, i, a) => a.findIndex(t => t.id === v.id) === i);
  3088. }
  3089.  
  3090.  
  3091. }
  3092.  
  3093. new PokeballHelper();
  3094. })();

QingJ © 2025

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