localdex 0.1.20__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
localdex/__init__.py CHANGED
@@ -3,7 +3,6 @@ from .core import LocalDex
3
3
  from .models import Pokemon, Move, Ability, Item, BaseStats
4
4
  from .exceptions import PokemonNotFoundError, MoveNotFoundError, AbilityNotFoundError, ItemNotFoundError
5
5
  from .random_battles import RandomBattleSets
6
- from .sprite_downloader import SpriteDownloader
7
6
 
8
7
  __all__ = [
9
8
  "LocalDex",
localdex/core.py CHANGED
@@ -19,7 +19,6 @@ from .exceptions import (
19
19
  )
20
20
  from .data_loader import DataLoader
21
21
  from .random_battles import RandomBattleSets
22
- from .sprite_downloader import SpriteDownloader
23
22
 
24
23
 
25
24
  class LocalDex:
@@ -31,7 +30,7 @@ class LocalDex:
31
30
  performance and comprehensive search capabilities.
32
31
  """
33
32
 
34
- def __init__(self, data_path: Optional[str] = None, data_dir: Optional[str] = None, enable_caching: bool = True, enable_random_battles: bool = True, enable_sprite_downloader: bool = True, sprite_max_workers: int = 8):
33
+ def __init__(self, data_path: Optional[str] = None, data_dir: Optional[str] = None, enable_caching: bool = True, enable_random_battles: bool = True):
35
34
  """
36
35
  Initialize the LocalDex.
37
36
 
@@ -40,17 +39,14 @@ class LocalDex:
40
39
  data_dir: Alias for data_path for backward compatibility.
41
40
  enable_caching: Whether to enable caching for better performance.
42
41
  enable_random_battles: Whether to enable random battle sets functionality.
43
- enable_sprite_downloader: Whether to enable sprite downloader functionality.
44
- sprite_max_workers: Maximum number of worker threads for parallel sprite processing.
42
+
45
43
  """
46
44
  # Use data_dir if provided, otherwise use data_path
47
45
  final_data_path = data_dir if data_dir is not None else data_path
48
46
  self.data_loader = DataLoader(final_data_path)
49
47
  self.data_dir = final_data_path # Store for backward compatibility
50
48
  self.enable_caching = enable_caching
51
-
52
- # Sprite downloader integration
53
- self.sprite_downloader = SpriteDownloader(final_data_path, max_workers=sprite_max_workers) if enable_sprite_downloader else None
49
+
54
50
 
55
51
  # Initialize random battle sets
56
52
  self.random_battles = RandomBattleSets() if enable_random_battles else None
@@ -703,42 +699,4 @@ class LocalDex:
703
699
  if self.random_battles is not None:
704
700
  self.random_battles.cleanup_downloads()
705
701
 
706
- def download_all_sprites(self):
707
- """
708
- Download and extract all sprites (Pokemon and items).
709
- """
710
- if not self.sprite_downloader:
711
- raise RuntimeError("SpriteDownloader is not enabled.")
712
- return self.sprite_downloader.download_all_sprites()
713
-
714
- def get_pokemon_sprite_path(self, pokemon_name: str):
715
- """
716
- Get the path to a Pokemon sprite image.
717
- """
718
- if not self.sprite_downloader:
719
- raise RuntimeError("SpriteDownloader is not enabled.")
720
- return self.sprite_downloader.get_pokemon_sprite_path(pokemon_name)
721
-
722
- def get_item_sprite_path(self, item_name: str):
723
- """
724
- Get the path to an item sprite image.
725
- """
726
- if not self.sprite_downloader:
727
- raise RuntimeError("SpriteDownloader is not enabled.")
728
- return self.sprite_downloader.get_item_sprite_path(item_name)
729
-
730
- def get_sprite_metadata(self, sprite_type: str = "pokemon"):
731
- """
732
- Get sprite metadata for either 'pokemon' or 'items'.
733
- """
734
- if not self.sprite_downloader:
735
- raise RuntimeError("SpriteDownloader is not enabled.")
736
- return self.sprite_downloader.get_sprite_metadata(sprite_type)
737
-
738
- def list_available_sprites(self, sprite_type: str = "pokemon"):
739
- """
740
- List all available sprites of a given type ('pokemon' or 'items').
741
- """
742
- if not self.sprite_downloader:
743
- raise RuntimeError("SpriteDownloader is not enabled.")
744
- return self.sprite_downloader.list_available_sprites(sprite_type)
702
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localdex
3
- Version: 0.1.20
3
+ Version: 0.2.2
4
4
  Summary: A local Pokemon data repository/Pokedex with fast offline access
5
5
  Author-email: LocalDex Team <localdex@example.com>
6
6
  License: MIT
@@ -22,7 +22,6 @@ Requires-Python: >=3.8
22
22
  Description-Content-Type: text/markdown
23
23
  Requires-Dist: requests
24
24
  Requires-Dist: typing-extensions
25
- Requires-Dist: pillow
26
25
  Provides-Extra: core
27
26
  Provides-Extra: gen1
28
27
  Provides-Extra: gen2
@@ -47,8 +46,6 @@ Requires-Dist: black>=22.0.0; extra == "dev"
47
46
  Requires-Dist: isort>=5.0.0; extra == "dev"
48
47
  Requires-Dist: mypy>=1.0.0; extra == "dev"
49
48
  Requires-Dist: flake8>=5.0.0; extra == "dev"
50
- Requires-Dist: pillow>=9.0.0; extra == "dev"
51
- Requires-Dist: selenium>=4.0.0; extra == "dev"
52
49
 
53
50
  # LocalDex
54
51
 
@@ -1,11 +1,10 @@
1
- localdex/__init__.py,sha256=hrXzDvHAGSDUNhnajIGpxEbeMrO1sy9AiAuqs15muTs,544
1
+ localdex/__init__.py,sha256=cJMmDgmw3d16WB3d1lR4-j-h7Bp8syqrWWTfYXsBB28,496
2
2
  localdex/cli.py,sha256=WqBCyA0akAFJNrYa8jCA3zoZgYEptDYDMGfAGkvKyZc,19317
3
- localdex/core.py,sha256=I8LwvKxtmEgdYtdEBVQLLp3JwafM8FhmvMrMpgxXkvA,28792
3
+ localdex/core.py,sha256=hhQETVgy0lIRfeou3lolgZ7ay1RZaEbrM9h8xkwLVVc,26733
4
4
  localdex/data_loader.py,sha256=hi9aSTto5Ti-OBGOgrQ-XwD5hmivsUwS1uC4rulhwQI,11366
5
5
  localdex/download_data.py,sha256=ibAHDxL60sV4LVN9isCmf8vvd_aI9IQbyjJpU0FHGUo,18869
6
6
  localdex/exceptions.py,sha256=Z02-8Kci6jFDk2nnGdVSHZJMDDWE9vuwuASs4VM3To8,2777
7
7
  localdex/random_battles.py,sha256=xRGQ6v-g9NG1-4jcQJWHOfskNUv-tFXspCXPRdctdmQ,9213
8
- localdex/sprite_downloader.py,sha256=q4ropKb6vCZmAHbetIXLOzcdbk50oa53Vv_WiiXbOk8,41379
9
8
  localdex/data/abilities/adaptability.json,sha256=FGEEZmL80YVcIXHV-h287Wu-UJycM1QEJxiOHK2I4mY,289
10
9
  localdex/data/abilities/aerilate.json,sha256=KRZOVH2KGdEQ_3UktFoXoagUOaC8jIPZ6Ti-YGzW44s,301
11
10
  localdex/data/abilities/aftermath.json,sha256=Awv_8jquu3pM4CemD5ylS1v83ABrqGtrgMEkL-1ldBk,285
@@ -4792,8 +4791,8 @@ localdex/models/ability.py,sha256=AQzv3XUHHl4sustMJjPDDjJOjXu2GMLTfcM3-tqQ_1w,30
4792
4791
  localdex/models/item.py,sha256=zXao8F-jBPUGq_YLeGeYeK_dZVI7aZMXtWOPwR3qusY,4677
4793
4792
  localdex/models/move.py,sha256=hfgcWI4ziz5MMvc9ddmkotxzYYdrSUqZZQ72IU5tucs,7629
4794
4793
  localdex/models/pokemon.py,sha256=v5zkxY1NGGR-MczFCZe9fHt6u_BAqAjMiQZlpcLXVGc,5408
4795
- localdex-0.1.20.dist-info/METADATA,sha256=ac1o8E2NVgUmFBnb7om4hclyKVN4FbQ4UcRqFGRszVE,14109
4796
- localdex-0.1.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4797
- localdex-0.1.20.dist-info/entry_points.txt,sha256=n5GxSeQo-MRuvrT2wVk7hOzEFFsWf6tkBjkzmGIYJe4,47
4798
- localdex-0.1.20.dist-info/top_level.txt,sha256=vtupDMH-IaxVCoEZrmE0QzdTwhaKzngVJbTA1NkR_MY,9
4799
- localdex-0.1.20.dist-info/RECORD,,
4794
+ localdex-0.2.2.dist-info/METADATA,sha256=MTmMmKoEZuK8-P6-_JTbwkGw7s4kRDcVySb7nzjQ8iQ,13994
4795
+ localdex-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4796
+ localdex-0.2.2.dist-info/entry_points.txt,sha256=n5GxSeQo-MRuvrT2wVk7hOzEFFsWf6tkBjkzmGIYJe4,47
4797
+ localdex-0.2.2.dist-info/top_level.txt,sha256=vtupDMH-IaxVCoEZrmE0QzdTwhaKzngVJbTA1NkR_MY,9
4798
+ localdex-0.2.2.dist-info/RECORD,,
@@ -1,932 +0,0 @@
1
- """
2
- Sprite downloader for LocalDex.
3
-
4
- This module handles downloading and managing Pokemon and item sprites
5
- from Pokemon Showdown and other sources.
6
- """
7
-
8
- import os
9
- import sys
10
- import re
11
- import json
12
- import glob
13
- import shutil
14
- import subprocess
15
- import tempfile
16
- import urllib.request
17
- import zipfile
18
- import concurrent.futures
19
- import threading
20
- import requests
21
- import time
22
- from pathlib import Path
23
- from typing import Dict, List, Any, Tuple, Optional, Set, Union
24
- from PIL import Image
25
- from selenium import webdriver
26
- from selenium.webdriver.chrome.options import Options
27
- from selenium.webdriver.chrome.service import Service
28
- from selenium.webdriver.common.by import By
29
- from selenium.webdriver.support.ui import WebDriverWait
30
- from selenium.webdriver.support import expected_conditions as EC
31
-
32
- from .exceptions import DataLoadError
33
-
34
-
35
- class SpriteDownloader:
36
- """
37
- Downloads and manages Pokemon and item sprites.
38
-
39
- This class handles downloading spritesheets from Pokemon Showdown,
40
- extracting individual sprites, and managing sprite metadata.
41
- """
42
-
43
- def __init__(self, data_dir: Optional[str] = None, enable_progress_output: bool = False, max_workers: int = 8):
44
- """
45
- Initialize the sprite downloader.
46
-
47
- Args:
48
- data_dir: Directory to save sprites and metadata. If None, uses package data.
49
- enable_progress_output: Whether to output progress messages.
50
- max_workers: Maximum number of worker threads for parallel processing.
51
- """
52
- if data_dir is None:
53
- # Use package data directory
54
- package_dir = Path(__file__).parent
55
- self.data_dir = package_dir / "data"
56
- else:
57
- self.data_dir = Path(data_dir)
58
-
59
- # Create sprite directories
60
- self.sprites_dir = self.data_dir / "sprites"
61
- self.spritesheet_dir = self.data_dir / "spritesheets"
62
- self.pokemon_sprites_dir = self.sprites_dir / "pokemon"
63
- self.item_sprites_dir = self.sprites_dir / "items"
64
-
65
- for directory in [self.sprites_dir, self.spritesheet_dir,
66
- self.pokemon_sprites_dir, self.item_sprites_dir]:
67
- directory.mkdir(parents=True, exist_ok=True)
68
-
69
- self.enable_progress_output = enable_progress_output
70
- self.max_workers = max_workers
71
-
72
- # Metadata files
73
- self.pokemon_coords_file = self.spritesheet_dir / "pokemon_sprite_coords.json"
74
- self.item_coords_file = self.spritesheet_dir / "item_sprite_coords.json"
75
- self.pokemon_metadata_file = self.spritesheet_dir / "pokemon_sprite_metadata.json"
76
- self.item_metadata_file = self.spritesheet_dir / "item_sprite_metadata.json"
77
-
78
- def _update_progress(self, progress: float, message: str = ""):
79
- """Update progress output."""
80
- if self.enable_progress_output:
81
- print(f"PROGRESS:{progress}:{message}", flush=True)
82
- else:
83
- print(f"Progress: {progress*100:.1f}% - {message}")
84
-
85
- def _update_status(self, message: str):
86
- """Update status output."""
87
- if self.enable_progress_output:
88
- print(f"STATUS:{message}", flush=True)
89
- else:
90
- print(f"Status: {message}")
91
-
92
- def _run_command(self, command: str) -> Tuple[int, str, str]:
93
- """
94
- Run a shell command and return the exit code, stdout, and stderr.
95
-
96
- Args:
97
- command (str): Command to run.
98
-
99
- Returns:
100
- Tuple[int, str, str]: Exit code, stdout, and stderr.
101
- """
102
- # Use a different approach on Windows to avoid shell issues
103
- if sys.platform == 'win32':
104
- try:
105
- # Split the command into parts for better Windows compatibility
106
- if isinstance(command, str):
107
- command_parts = command.split()
108
- else:
109
- command_parts = command
110
-
111
- # Run the command with shell=False for better Windows compatibility
112
- process = subprocess.Popen(
113
- command_parts,
114
- shell=False,
115
- stdout=subprocess.PIPE,
116
- stderr=subprocess.PIPE,
117
- text=True
118
- )
119
- stdout, stderr = process.communicate()
120
- return process.returncode, stdout, stderr
121
- except Exception as e:
122
- # If there's an error with the non-shell approach, fall back to shell=True
123
- self._update_status(f"Warning: Non-shell command execution failed, falling back to shell: {e}")
124
- process = subprocess.Popen(
125
- command,
126
- shell=True,
127
- stdout=subprocess.PIPE,
128
- stderr=subprocess.PIPE,
129
- text=True
130
- )
131
- stdout, stderr = process.communicate()
132
- return process.returncode, stdout, stderr
133
- else:
134
- # On Unix-like systems, use the original approach
135
- process = subprocess.Popen(
136
- command,
137
- shell=True,
138
- stdout=subprocess.PIPE,
139
- stderr=subprocess.PIPE,
140
- text=True
141
- )
142
- stdout, stderr = process.communicate()
143
- return process.returncode, stdout, stderr
144
-
145
- def download_pokemon_spritesheet(self) -> Path:
146
- """
147
- Download the Pokemon spritesheet from Pokemon Showdown.
148
-
149
- Returns:
150
- Path to the downloaded spritesheet
151
- """
152
- self._update_status("Checking for existing Pokemon spritesheet...")
153
- self._update_progress(0.0, "Starting Pokemon spritesheet check...")
154
-
155
- # URL of the Pokemon spritesheet
156
- spritesheet_url = "https://play.pokemonshowdown.com/sprites/pokemonicons-sheet.png"
157
- spritesheet_path = self.spritesheet_dir / "pokemonicons-sheet.png"
158
-
159
- # Check if the spritesheet already exists
160
- if spritesheet_path.exists():
161
- self._update_status(f"Using existing spritesheet from {spritesheet_path}")
162
- self._update_progress(1.0, "Using existing Pokemon spritesheet")
163
-
164
- # Verify the existing image
165
- try:
166
- img = Image.open(spritesheet_path)
167
- self._update_status(f"Spritesheet dimensions: {img.width}x{img.height}")
168
- except Exception as e:
169
- self._update_status(f"Warning: Could not verify existing spritesheet image: {e}")
170
- self._update_status("Will download a fresh copy...")
171
- # If verification fails, we'll download a fresh copy
172
- else:
173
- return spritesheet_path
174
-
175
- # Download the spritesheet if it doesn't exist or verification failed
176
- self._update_status("Downloading Pokemon spritesheet...")
177
- try:
178
- response = requests.get(spritesheet_url, stream=True)
179
- if response.status_code != 200:
180
- raise Exception(f"Failed to download spritesheet, status code: {response.status_code}")
181
-
182
- with open(spritesheet_path, 'wb') as f:
183
- for chunk in response.iter_content(chunk_size=8192):
184
- f.write(chunk)
185
-
186
- self._update_status(f"Spritesheet downloaded successfully to {spritesheet_path}")
187
- self._update_progress(1.0, "Pokemon spritesheet download complete")
188
-
189
- # Verify the downloaded image
190
- try:
191
- img = Image.open(spritesheet_path)
192
- self._update_status(f"Spritesheet dimensions: {img.width}x{img.height}")
193
- except Exception as e:
194
- self._update_status(f"Warning: Could not verify spritesheet image: {e}")
195
-
196
- return spritesheet_path
197
- except Exception as e:
198
- self._update_status(f"Error downloading Pokemon spritesheet: {str(e)}")
199
- raise
200
-
201
- def download_item_spritesheet(self) -> Path:
202
- """
203
- Download the item spritesheet from Pokemon Showdown.
204
-
205
- Returns:
206
- Path to the downloaded spritesheet
207
- """
208
- self._update_status("Checking for existing item spritesheet...")
209
- self._update_progress(0.0, "Starting item spritesheet check...")
210
-
211
- # URL of the item spritesheet
212
- spritesheet_url = "https://play.pokemonshowdown.com/sprites/itemicons-sheet.png"
213
- spritesheet_path = self.spritesheet_dir / "itemicons-sheet.png"
214
-
215
- # Check if the spritesheet already exists
216
- if spritesheet_path.exists():
217
- self._update_status(f"Using existing spritesheet from {spritesheet_path}")
218
- self._update_progress(1.0, "Using existing item spritesheet")
219
-
220
- # Verify the existing image
221
- try:
222
- img = Image.open(spritesheet_path)
223
- self._update_status(f"Spritesheet dimensions: {img.width}x{img.height}")
224
- except Exception as e:
225
- self._update_status(f"Warning: Could not verify existing spritesheet image: {e}")
226
- self._update_status("Will download a fresh copy...")
227
- # If verification fails, we'll download a fresh copy
228
- else:
229
- return spritesheet_path
230
-
231
- # Download the spritesheet if it doesn't exist or verification failed
232
- self._update_status("Downloading item spritesheet...")
233
- try:
234
- response = requests.get(spritesheet_url, stream=True)
235
- if response.status_code != 200:
236
- raise Exception(f"Failed to download spritesheet, status code: {response.status_code}")
237
-
238
- with open(spritesheet_path, 'wb') as f:
239
- for chunk in response.iter_content(chunk_size=8192):
240
- f.write(chunk)
241
-
242
- self._update_status(f"Spritesheet downloaded successfully to {spritesheet_path}")
243
- self._update_progress(1.0, "Item spritesheet download complete")
244
-
245
- # Verify the downloaded image
246
- try:
247
- img = Image.open(spritesheet_path)
248
- self._update_status(f"Spritesheet dimensions: {img.width}x{img.height}")
249
- except Exception as e:
250
- self._update_status(f"Warning: Could not verify spritesheet image: {e}")
251
-
252
- return spritesheet_path
253
- except Exception as e:
254
- self._update_status(f"Error downloading item spritesheet: {str(e)}")
255
- raise
256
-
257
- def parse_pokemon_sprite_coordinates(self) -> Dict[str, Dict[str, int]]:
258
- """
259
- Parse Pokemon sprite coordinates from the gallery test page.
260
-
261
- Returns:
262
- Dictionary mapping Pokemon names to their sprite coordinates
263
- """
264
- self._update_status("Checking for existing Pokemon sprite coordinates...")
265
- self._update_progress(0.0, "Starting coordinate check...")
266
-
267
- # Check if the coordinates JSON already exists
268
- if self.pokemon_coords_file.exists():
269
- self._update_status(f"Using existing coordinates from {self.pokemon_coords_file}")
270
- try:
271
- with open(self.pokemon_coords_file, 'r') as f:
272
- pokemon_coords = json.load(f)
273
-
274
- self._update_status(f"Loaded coordinates for {len(pokemon_coords)} Pokemon")
275
- self._update_progress(1.0, "Using existing coordinates")
276
- return pokemon_coords
277
- except Exception as e:
278
- self._update_status(f"Error loading existing coordinates: {str(e)}")
279
- self._update_status("Will parse fresh coordinates...")
280
- # If loading fails, we'll parse fresh coordinates
281
-
282
- # URL of the gallery test page
283
- gallery_url = "https://play.pokemonshowdown.com/sprites/gallery-test.html"
284
-
285
- # Create a dictionary to store the Pokemon sprite coordinates
286
- pokemon_coords = {}
287
-
288
- # Add MissingNo. to the coordinates dictionary
289
- pokemon_coords["MissingNo."] = {
290
- "x": 0,
291
- "y": 0
292
- }
293
-
294
- # Set up Chrome options for headless browsing
295
- self._update_status("Setting up headless browser...")
296
- chrome_options = Options()
297
- chrome_options.add_argument("--headless")
298
- chrome_options.add_argument("--no-sandbox")
299
-
300
- try:
301
- # Initialize the Chrome driver
302
- driver = webdriver.Chrome(options=chrome_options)
303
-
304
- # Navigate to the gallery page
305
- self._update_status(f"Navigating to {gallery_url}...")
306
- driver.get(gallery_url)
307
-
308
- # Wait for the page to load (wait for the table to be rendered)
309
- self._update_status("Waiting for page to load...")
310
- WebDriverWait(driver, 10).until(
311
- EC.presence_of_element_located((By.TAG_NAME, "table"))
312
- )
313
-
314
- # Give the JavaScript some time to fully render the page
315
- time.sleep(2)
316
-
317
- # Find the navigation buttons to determine how many pages there are
318
- nav_buttons = driver.find_elements(By.CSS_SELECTOR, "ul li button")
319
- total_pages = len(nav_buttons)
320
-
321
- self._update_status(f"Found {total_pages} pages of Pokemon data")
322
-
323
- # Process each page
324
- total_pokemon = 0
325
- for page in range(total_pages):
326
- self._update_status(f"Processing page {page+1}/{total_pages}...")
327
-
328
- # If not on the first page, click the navigation button to go to the page
329
- if page > 0:
330
- # Find the navigation buttons again (they might have been refreshed)
331
- nav_buttons = driver.find_elements(By.CSS_SELECTOR, "ul li button")
332
- nav_buttons[page].click()
333
-
334
- # Wait for the page to load
335
- time.sleep(1)
336
-
337
- # Find all table rows (skip the header row)
338
- rows = driver.find_elements(By.CSS_SELECTOR, "table tr:not(:first-child)")
339
-
340
- page_rows = len(rows)
341
- self._update_status(f"Found {page_rows} Pokemon entries on page {page+1}")
342
- total_pokemon += page_rows
343
-
344
- # Process each row to extract the Pokemon name and sprite coordinates
345
- for i, row in enumerate(rows):
346
- try:
347
- # Get the cells in the row
348
- cells = row.find_elements(By.TAG_NAME, "td")
349
-
350
- if len(cells) >= 2:
351
- # First cell contains the icon with style attribute containing coordinates
352
- icon_cell = cells[0]
353
- icon_spans = icon_cell.find_elements(By.CLASS_NAME, "picon")
354
-
355
- if icon_spans:
356
- # Get the style attribute which contains the coordinates
357
- style = icon_spans[0].get_attribute("style")
358
-
359
- # Extract the coordinates using regex
360
- # The format is: background: transparent url("...") Xpx Ypx no-repeat;
361
- coords_match = re.search(r'(-?\d+)px (-?\d+)px', style)
362
-
363
- if coords_match:
364
- x_coord = int(coords_match.group(1))
365
- y_coord = int(coords_match.group(2))
366
-
367
- # Second cell contains the Pokemon name
368
- pokemon_name = cells[1].text.strip()
369
-
370
- # Store the coordinates
371
- pokemon_coords[pokemon_name] = {
372
- "x": x_coord,
373
- "y": y_coord
374
- }
375
-
376
- except Exception as e:
377
- self._update_status(f"Error processing row {i} on page {page+1}: {str(e)}")
378
-
379
- # Update progress after each page
380
- progress = (page + 1) / total_pages
381
- self._update_progress(progress, f"Processed page {page+1}/{total_pages}")
382
-
383
- # Close the browser
384
- driver.quit()
385
-
386
- # Save the coordinates to a JSON file
387
- self._update_status(f"Saving coordinates for {len(pokemon_coords)} Pokemon to {self.pokemon_coords_file}...")
388
- with open(self.pokemon_coords_file, 'w') as f:
389
- json.dump(pokemon_coords, f, indent=2)
390
-
391
- self._update_status(f"Coordinates saved successfully to {self.pokemon_coords_file}")
392
- self._update_progress(1.0, "Coordinate parsing complete")
393
-
394
- return pokemon_coords
395
-
396
- except Exception as e:
397
- self._update_status(f"Error parsing Pokemon sprite coordinates: {str(e)}")
398
- if 'driver' in locals():
399
- driver.quit()
400
- raise
401
-
402
- def parse_item_sprite_coordinates(self) -> Dict[str, Dict[str, Any]]:
403
- """
404
- Parse item sprite coordinates from the items.js data file.
405
-
406
- Returns:
407
- Dictionary mapping item names to their sprite coordinates
408
- """
409
- self._update_status("Checking for existing item sprite coordinates...")
410
- self._update_progress(0.0, "Starting coordinate check...")
411
-
412
- # Check if the coordinates JSON already exists
413
- if self.item_coords_file.exists():
414
- self._update_status(f"Using existing coordinates from {self.item_coords_file}")
415
- try:
416
- with open(self.item_coords_file, 'r') as f:
417
- item_coords = json.load(f)
418
-
419
- self._update_status(f"Loaded coordinates for {len(item_coords)} items")
420
- self._update_progress(1.0, "Using existing coordinates")
421
- return item_coords
422
- except Exception as e:
423
- self._update_status(f"Error loading existing coordinates: {str(e)}")
424
- self._update_status("Will parse fresh coordinates...")
425
- # If loading fails, we'll parse fresh coordinates
426
-
427
- # Create a dictionary to store the item sprite coordinates
428
- item_coords = {}
429
-
430
- try:
431
- # Download the items.js file to get item data
432
- self._update_status("Downloading item data from Pokemon Showdown...")
433
- items_url = "https://play.pokemonshowdown.com/data/items.js"
434
- response = requests.get(items_url)
435
-
436
- if response.status_code != 200:
437
- raise Exception(f"Failed to download items data, status code: {response.status_code}")
438
-
439
- self._update_progress(0.3, "Item data downloaded, parsing...")
440
-
441
- # Parse the JavaScript content to extract item information
442
- content = response.text
443
-
444
- # Extract item entries with names and sprite numbers
445
- # Pattern matches: itemid:{...name:"Item Name"...spritenum:123...}
446
- item_pattern = r'(\w+):\s*{[^}]*name:\s*"([^"]+)"[^}]*spritenum:\s*(\d+)[^}]*}'
447
- item_matches = re.findall(item_pattern, content)
448
-
449
- self._update_status(f"Found {len(item_matches)} items in data file")
450
- self._update_progress(0.6, "Calculating sprite coordinates...")
451
-
452
- # Calculate spritesheet grid layout
453
- # The spritesheet is 384x1152 with 24x24 sprites
454
- sprite_width = 24
455
- sprite_height = 24
456
- cols = 384 // sprite_width # 16 columns
457
-
458
- # Process each item to calculate coordinates
459
- for i, (item_id, item_name, spritenum_str) in enumerate(item_matches):
460
- try:
461
- spritenum = int(spritenum_str)
462
-
463
- # Calculate grid position from sprite number
464
- row = spritenum // cols
465
- col = spritenum % cols
466
-
467
- # Calculate pixel coordinates (top-left corner)
468
- x = col * sprite_width
469
- y = row * sprite_height
470
-
471
- # Store the coordinates
472
- item_coords[item_name] = {
473
- "x": x,
474
- "y": y,
475
- "spritenum": spritenum,
476
- "item_id": item_id
477
- }
478
-
479
- # Update progress periodically
480
- if i % 50 == 0 or i == len(item_matches) - 1:
481
- progress = 0.6 + (0.3 * (i + 1) / len(item_matches))
482
- self._update_progress(progress, f"Processed {i+1}/{len(item_matches)} items")
483
-
484
- except Exception as e:
485
- self._update_status(f"Error processing item {item_name}: {str(e)}")
486
-
487
- # Save the coordinates to a JSON file
488
- self._update_status(f"Saving coordinates for {len(item_coords)} items to {self.item_coords_file}...")
489
- with open(self.item_coords_file, 'w') as f:
490
- json.dump(item_coords, f, indent=2)
491
-
492
- self._update_status(f"Coordinates saved successfully to {self.item_coords_file}")
493
- self._update_progress(1.0, "Coordinate parsing complete")
494
-
495
- return item_coords
496
-
497
- except Exception as e:
498
- self._update_status(f"Error parsing item sprite coordinates: {str(e)}")
499
- raise
500
-
501
- def _extract_single_pokemon_sprite(self, args: Tuple[str, Dict[str, int], Image.Image, Path, int, int]) -> Tuple[str, Dict[str, Any]]:
502
- """
503
- Extract a single Pokemon sprite from the spritesheet.
504
-
505
- Args:
506
- args: Tuple containing (pokemon_name, coords, spritesheet, output_dir, sprite_width, sprite_height)
507
-
508
- Returns:
509
- Tuple of (pokemon_name, metadata_dict)
510
- """
511
- pokemon_name, coords, spritesheet, output_dir, sprite_width, sprite_height = args
512
-
513
- try:
514
- # Get the coordinates
515
- x = abs(coords["x"])
516
- y = abs(coords["y"])
517
-
518
- # Extract the sprite from the spritesheet
519
- sprite = spritesheet.crop((x, y, x + sprite_width, y + sprite_height))
520
-
521
- # Create a safe filename (replace spaces and special characters)
522
- safe_name = re.sub(r'[^\w\-_]', '_', pokemon_name)
523
- safe_name = safe_name.replace(' ', '').replace('.','').replace('-','').replace('_','').lower()
524
- sprite_path = output_dir / f"{safe_name}.png"
525
-
526
- # Save the sprite with transparency
527
- sprite.save(sprite_path, 'PNG')
528
-
529
- # Create metadata
530
- metadata = {
531
- "filename": f"{safe_name}.png",
532
- "path": str(sprite_path),
533
- "coordinates": {
534
- "x": x,
535
- "y": y,
536
- "width": sprite_width,
537
- "height": sprite_height
538
- },
539
- "extracted_at": time.strftime("%Y-%m-%d %H:%M:%S")
540
- }
541
-
542
- return pokemon_name, metadata
543
-
544
- except Exception as e:
545
- self._update_status(f"Error extracting sprite for {pokemon_name}: {str(e)}")
546
- return pokemon_name, None
547
-
548
- def extract_pokemon_sprites(self, spritesheet_path: Path, pokemon_coords: Dict[str, Dict[str, int]]) -> Path:
549
- """
550
- Extract individual Pokemon sprites from the spritesheet using parallel processing.
551
-
552
- Args:
553
- spritesheet_path: Path to the spritesheet image
554
- pokemon_coords: Dictionary mapping Pokemon names to their sprite coordinates
555
-
556
- Returns:
557
- Path to the directory containing the extracted sprites
558
- """
559
- self._update_status("Checking for existing Pokemon sprite metadata...")
560
- self._update_progress(0.0, "Starting sprite extraction check...")
561
-
562
- # Check if the metadata JSON already exists
563
- if self.pokemon_metadata_file.exists():
564
- self._update_status(f"Using existing sprite metadata from {self.pokemon_metadata_file}")
565
- try:
566
- with open(self.pokemon_metadata_file, 'r') as f:
567
- metadata = json.load(f)
568
-
569
- # Verify that the sprites directory exists and has files
570
- if self.pokemon_sprites_dir.exists() and len(list(self.pokemon_sprites_dir.glob("*.png"))) > 0:
571
- self._update_status(f"Found existing sprites in {self.pokemon_sprites_dir}")
572
- self._update_progress(1.0, "Using existing sprites")
573
- return self.pokemon_sprites_dir
574
- else:
575
- self._update_status("Sprite directory empty or missing, will extract sprites...")
576
- except Exception as e:
577
- self._update_status(f"Error loading existing metadata: {str(e)}")
578
- self._update_status("Will extract sprites...")
579
- # If loading fails, we'll extract sprites
580
-
581
- self._update_status("Extracting Pokemon sprites from spritesheet using parallel processing...")
582
- self._update_progress(0.1, "Starting parallel sprite extraction...")
583
-
584
- # Create a metadata file to store information about the sprites
585
- metadata = {}
586
-
587
- try:
588
- # Load the spritesheet image
589
- spritesheet = Image.open(spritesheet_path)
590
- self._update_status(f"Loaded spritesheet with dimensions: {spritesheet.width}x{spritesheet.height}")
591
-
592
- # Convert to RGBA to preserve transparency
593
- if spritesheet.mode != 'RGBA':
594
- spritesheet = spritesheet.convert('RGBA')
595
-
596
- # Define the size of each sprite (40x30 pixels)
597
- sprite_width = 40
598
- sprite_height = 30
599
-
600
- # Prepare arguments for parallel processing
601
- total_pokemon = len(pokemon_coords)
602
- self._update_status(f"Starting parallel extraction of {total_pokemon} Pokemon sprites with {self.max_workers} workers...")
603
-
604
- # Create arguments for each sprite extraction
605
- extraction_args = []
606
- for pokemon_name, coords in pokemon_coords.items():
607
- args = (pokemon_name, coords, spritesheet, self.pokemon_sprites_dir, sprite_width, sprite_height)
608
- extraction_args.append(args)
609
-
610
- # Thread-safe counter for progress tracking
611
- completed_count = 0
612
- lock = threading.Lock()
613
-
614
- def update_progress():
615
- nonlocal completed_count
616
- with lock:
617
- completed_count += 1
618
- progress = 0.1 + (0.8 * completed_count / total_pokemon)
619
- if completed_count % 10 == 0 or completed_count == total_pokemon:
620
- self._update_progress(progress, f"Extracted {completed_count}/{total_pokemon} sprites")
621
-
622
- # Process sprites in parallel
623
- with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
624
- # Submit all extraction tasks
625
- future_to_pokemon = {
626
- executor.submit(self._extract_single_pokemon_sprite, args): args[0]
627
- for args in extraction_args
628
- }
629
-
630
- # Collect results as they complete
631
- for future in concurrent.futures.as_completed(future_to_pokemon):
632
- pokemon_name, sprite_metadata = future.result()
633
- if sprite_metadata:
634
- metadata[pokemon_name] = sprite_metadata
635
- update_progress()
636
-
637
- # Save the metadata
638
- with open(self.pokemon_metadata_file, 'w') as f:
639
- json.dump(metadata, f, indent=2)
640
-
641
- self._update_status(f"Extracted {len(metadata)} Pokemon sprites to {self.pokemon_sprites_dir}")
642
- self._update_status(f"Metadata saved to {self.pokemon_metadata_file}")
643
- self._update_progress(1.0, "Parallel sprite extraction complete")
644
-
645
- return self.pokemon_sprites_dir
646
-
647
- except Exception as e:
648
- self._update_status(f"Error extracting Pokemon sprites: {str(e)}")
649
- raise
650
-
651
- def _extract_single_item_sprite(self, args: Tuple[str, Dict[str, Any], Image.Image, Path, int, int]) -> Tuple[str, Dict[str, Any]]:
652
- """
653
- Extract a single item sprite from the spritesheet.
654
-
655
- Args:
656
- args: Tuple containing (item_name, coords, spritesheet, output_dir, sprite_width, sprite_height)
657
-
658
- Returns:
659
- Tuple of (item_name, metadata_dict)
660
- """
661
- item_name, coords, spritesheet, output_dir, sprite_width, sprite_height = args
662
-
663
- try:
664
- # Get the coordinates
665
- x = abs(coords["x"])
666
- y = abs(coords["y"])
667
-
668
- # Extract the sprite from the spritesheet
669
- sprite = spritesheet.crop((x, y, x + sprite_width, y + sprite_height))
670
-
671
- # Create a safe filename (replace spaces and special characters)
672
- safe_name = re.sub(r'[^\w\-_]', '_', item_name)
673
- safe_name = safe_name.replace(' ', '').replace('.','').replace('-','').replace('_','').lower()
674
- sprite_path = output_dir / f"{safe_name}.png"
675
-
676
- # Save the sprite with transparency
677
- sprite.save(sprite_path, 'PNG')
678
-
679
- # Create metadata
680
- metadata = {
681
- "filename": f"{safe_name}.png",
682
- "path": str(sprite_path),
683
- "coordinates": {
684
- "x": x,
685
- "y": y,
686
- "width": sprite_width,
687
- "height": sprite_height
688
- },
689
- "extracted_at": time.strftime("%Y-%m-%d %H:%M:%S")
690
- }
691
-
692
- return item_name, metadata
693
-
694
- except Exception as e:
695
- self._update_status(f"Error extracting sprite for {item_name}: {str(e)}")
696
- return item_name, None
697
-
698
- def extract_item_sprites(self, spritesheet_path: Path, item_coords: Dict[str, Dict[str, Any]]) -> Path:
699
- """
700
- Extract individual item sprites from the spritesheet using parallel processing.
701
-
702
- Args:
703
- spritesheet_path: Path to the spritesheet image
704
- item_coords: Dictionary mapping item names to their sprite coordinates
705
-
706
- Returns:
707
- Path to the directory containing the extracted sprites
708
- """
709
- self._update_status("Checking for existing item sprite metadata...")
710
- self._update_progress(0.0, "Starting sprite extraction check...")
711
-
712
- # Check if the metadata JSON already exists
713
- if self.item_metadata_file.exists():
714
- self._update_status(f"Using existing sprite metadata from {self.item_metadata_file}")
715
- try:
716
- with open(self.item_metadata_file, 'r') as f:
717
- metadata = json.load(f)
718
-
719
- # Verify that the sprites directory exists and has files
720
- if self.item_sprites_dir.exists() and len(list(self.item_sprites_dir.glob("*.png"))) > 0:
721
- self._update_status(f"Found existing sprites in {self.item_sprites_dir}")
722
- self._update_progress(1.0, "Using existing sprites")
723
- return self.item_sprites_dir
724
- else:
725
- self._update_status("Sprite directory empty or missing, will extract sprites...")
726
- except Exception as e:
727
- self._update_status(f"Error loading existing metadata: {str(e)}")
728
- self._update_status("Will extract sprites...")
729
- # If loading fails, we'll extract sprites
730
-
731
- self._update_status("Extracting item sprites from spritesheet using parallel processing...")
732
- self._update_progress(0.1, "Starting parallel sprite extraction...")
733
-
734
- # Create a metadata file to store information about the sprites
735
- metadata = {}
736
-
737
- try:
738
- # Load the spritesheet image
739
- spritesheet = Image.open(spritesheet_path)
740
- self._update_status(f"Loaded spritesheet with dimensions: {spritesheet.width}x{spritesheet.height}")
741
-
742
- # Convert to RGBA to preserve transparency
743
- if spritesheet.mode != 'RGBA':
744
- spritesheet = spritesheet.convert('RGBA')
745
-
746
- # Define the size of each sprite (24x24 pixels for items)
747
- sprite_width = 24
748
- sprite_height = 24
749
-
750
- # Prepare arguments for parallel processing
751
- total_items = len(item_coords)
752
- self._update_status(f"Starting parallel extraction of {total_items} item sprites with {self.max_workers} workers...")
753
-
754
- # Create arguments for each sprite extraction
755
- extraction_args = []
756
- for item_name, coords in item_coords.items():
757
- args = (item_name, coords, spritesheet, self.item_sprites_dir, sprite_width, sprite_height)
758
- extraction_args.append(args)
759
-
760
- # Thread-safe counter for progress tracking
761
- completed_count = 0
762
- lock = threading.Lock()
763
-
764
- def update_progress():
765
- nonlocal completed_count
766
- with lock:
767
- completed_count += 1
768
- progress = 0.1 + (0.8 * completed_count / total_items)
769
- if completed_count % 10 == 0 or completed_count == total_items:
770
- self._update_progress(progress, f"Extracted {completed_count}/{total_items} sprites")
771
-
772
- # Process sprites in parallel
773
- with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
774
- # Submit all extraction tasks
775
- future_to_item = {
776
- executor.submit(self._extract_single_item_sprite, args): args[0]
777
- for args in extraction_args
778
- }
779
-
780
- # Collect results as they complete
781
- for future in concurrent.futures.as_completed(future_to_item):
782
- item_name, sprite_metadata = future.result()
783
- if sprite_metadata:
784
- metadata[item_name] = sprite_metadata
785
- update_progress()
786
-
787
- # Save the metadata
788
- with open(self.item_metadata_file, 'w') as f:
789
- json.dump(metadata, f, indent=2)
790
-
791
- self._update_status(f"Extracted {len(metadata)} item sprites to {self.item_sprites_dir}")
792
- self._update_status(f"Metadata saved to {self.item_metadata_file}")
793
- self._update_progress(1.0, "Parallel sprite extraction complete")
794
-
795
- return self.item_sprites_dir
796
-
797
- except Exception as e:
798
- self._update_status(f"Error extracting item sprites: {str(e)}")
799
- raise
800
-
801
- def download_all_sprites(self) -> Dict[str, Path]:
802
- """
803
- Download and extract all sprites (Pokemon and items).
804
-
805
- Returns:
806
- Dictionary with paths to the extracted sprite directories
807
- """
808
- self._update_status("Starting sprite download process...")
809
- self._update_progress(0.0, "Starting sprite download...")
810
-
811
- try:
812
- # Download Pokemon spritesheet
813
- pokemon_spritesheet = self.download_pokemon_spritesheet()
814
- self._update_status(f"Pokemon spritesheet ready at: {pokemon_spritesheet}")
815
- self._update_progress(0.2, "Pokemon spritesheet downloaded")
816
-
817
- # Download item spritesheet
818
- item_spritesheet = self.download_item_spritesheet()
819
- self._update_status(f"Item spritesheet ready at: {item_spritesheet}")
820
- self._update_progress(0.4, "Item spritesheet downloaded")
821
-
822
- # Parse Pokemon sprite coordinates
823
- pokemon_coords = self.parse_pokemon_sprite_coordinates()
824
- self._update_status(f"Pokemon sprite coordinates parsed")
825
- self._update_status(f"Found coordinates for {len(pokemon_coords)} Pokemon")
826
- self._update_progress(0.6, "Pokemon coordinates parsed")
827
-
828
- # Parse item sprite coordinates
829
- item_coords = self.parse_item_sprite_coordinates()
830
- self._update_status(f"Item sprite coordinates parsed")
831
- self._update_status(f"Found coordinates for {len(item_coords)} items")
832
- self._update_progress(0.7, "Item coordinates parsed")
833
-
834
- # Extract Pokemon sprites
835
- pokemon_sprites_dir = self.extract_pokemon_sprites(pokemon_spritesheet, pokemon_coords)
836
- self._update_status(f"Pokemon sprites extracted to: {pokemon_sprites_dir}")
837
- self._update_progress(0.85, "Pokemon sprites extracted")
838
-
839
- # Extract item sprites
840
- item_sprites_dir = self.extract_item_sprites(item_spritesheet, item_coords)
841
- self._update_status(f"Item sprites extracted to: {item_sprites_dir}")
842
- self._update_progress(1.0, "All sprites downloaded and extracted")
843
-
844
- return {
845
- "pokemon": pokemon_sprites_dir,
846
- "items": item_sprites_dir
847
- }
848
-
849
- except Exception as e:
850
- self._update_status(f"Error in sprite download process: {str(e)}")
851
- raise
852
-
853
- def get_pokemon_sprite_path(self, pokemon_name: str) -> Optional[Path]:
854
- """
855
- Get the path to a Pokemon sprite.
856
-
857
- Args:
858
- pokemon_name: Name of the Pokemon
859
-
860
- Returns:
861
- Path to the sprite file, or None if not found
862
- """
863
- # Create safe filename
864
- safe_name = re.sub(r'[^\w\-_]', '_', pokemon_name)
865
- safe_name = safe_name.replace(' ', '').replace('.','').replace('-','').replace('_','').lower()
866
- sprite_path = self.pokemon_sprites_dir / f"{safe_name}.png"
867
-
868
- if sprite_path.exists():
869
- return sprite_path
870
- return None
871
-
872
- def get_item_sprite_path(self, item_name: str) -> Optional[Path]:
873
- """
874
- Get the path to an item sprite.
875
-
876
- Args:
877
- item_name: Name of the item
878
-
879
- Returns:
880
- Path to the sprite file, or None if not found
881
- """
882
- # Create safe filename
883
- safe_name = re.sub(r'[^\w\-_]', '_', item_name)
884
- safe_name = safe_name.replace(' ', '').replace('.','').replace('-','').replace('_','').lower()
885
- sprite_path = self.item_sprites_dir / f"{safe_name}.png"
886
-
887
- if sprite_path.exists():
888
- return sprite_path
889
- return None
890
-
891
- def get_sprite_metadata(self, sprite_type: str = "pokemon") -> Dict[str, Any]:
892
- """
893
- Get sprite metadata.
894
-
895
- Args:
896
- sprite_type: Type of sprites ("pokemon" or "items")
897
-
898
- Returns:
899
- Dictionary containing sprite metadata
900
- """
901
- if sprite_type == "pokemon":
902
- metadata_file = self.pokemon_metadata_file
903
- elif sprite_type == "items":
904
- metadata_file = self.item_metadata_file
905
- else:
906
- raise ValueError("sprite_type must be 'pokemon' or 'items'")
907
-
908
- if metadata_file.exists():
909
- with open(metadata_file, 'r') as f:
910
- return json.load(f)
911
- return {}
912
-
913
- def list_available_sprites(self, sprite_type: str = "pokemon") -> List[str]:
914
- """
915
- List all available sprites of a given type.
916
-
917
- Args:
918
- sprite_type: Type of sprites ("pokemon" or "items")
919
-
920
- Returns:
921
- List of sprite names
922
- """
923
- if sprite_type == "pokemon":
924
- sprite_dir = self.pokemon_sprites_dir
925
- elif sprite_type == "items":
926
- sprite_dir = self.item_sprites_dir
927
- else:
928
- raise ValueError("sprite_type must be 'pokemon' or 'items'")
929
-
930
- if sprite_dir.exists():
931
- return [f.stem for f in sprite_dir.glob("*.png")]
932
- return []