diff --git a/DBEditor/DBEditor.py b/DBEditor/DBEditor.py index de7beac..9773e2a 100644 --- a/DBEditor/DBEditor.py +++ b/DBEditor/DBEditor.py @@ -1,7 +1,7 @@ import sys from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QLineEdit, QLabel, QCheckBox, QPushButton, QFormLayout, QListWidgetItem, QSplitter, QTreeWidget, - QTreeWidgetItem, QDialog, QDialogButtonBox, QComboBox, QMessageBox) + QTreeWidgetItem, QDialog, QDialogButtonBox, QComboBox, QMessageBox, QSpinBox) from PyQt6.QtCore import Qt, QSize from PyQt6.QtGui import QPixmap, QFontMetrics, QColor import sqlite3 @@ -54,6 +54,10 @@ class EvolutionEditDialog(QDialog): def delete_evolution(self): self.done(2) # Use a custom return code for delete action +def parse_pfic(pfic): + parts = pfic.split('-') + return tuple(int(part) if part.isdigit() else part for part in parts) + class DBEditor(QMainWindow): def __init__(self): super().__init__() @@ -65,7 +69,9 @@ class DBEditor(QMainWindow): self.init_database() self.patches = self.load_and_apply_patches() + self.encounter_cache = {} # Add this line self.init_ui() + #self.load_pokemon_list() # Make sure this is called after init_ui def init_database(self): # Copy the original database to the in-memory database @@ -121,15 +127,28 @@ class DBEditor(QMainWindow): # Left side: Search and List left_layout = QVBoxLayout() + search_layout = QHBoxLayout() self.search_bar = QLineEdit() self.search_bar.setPlaceholderText("Search Pokémon...") self.search_bar.textChanged.connect(self.filter_pokemon_list) - left_layout.addWidget(self.search_bar) + search_layout.addWidget(self.search_bar) + + left_layout.addLayout(search_layout) self.pokemon_list = QListWidget() self.pokemon_list.currentItemChanged.connect(self.load_pokemon_details) left_layout.addWidget(self.pokemon_list) + # Move the checkbox here, after the pokemon_list + self.highlight_no_encounters = QCheckBox("Highlight Pokémon without encounters") + self.highlight_no_encounters.stateChanged.connect(self.toggle_highlight_mode) + left_layout.addWidget(self.highlight_no_encounters) + + # Add the new checkbox for filtering Home-storable Pokémon + self.filter_home_storable = QCheckBox("Show only Home-storable Pokémon") + self.filter_home_storable.stateChanged.connect(self.filter_pokemon_list) + left_layout.addWidget(self.filter_home_storable) + # Right side: Edit panel right_layout = QHBoxLayout() @@ -156,6 +175,19 @@ class DBEditor(QMainWindow): self.evolution_tree.setColumnWidth(0, 200) text_layout.addWidget(self.evolution_tree) + # Add Locations tree + self.locations_tree = QTreeWidget() + self.locations_tree.setHeaderLabels(["Game/Location", "Details"]) + self.locations_tree.setColumnWidth(0, 200) + self.locations_tree.itemDoubleClicked.connect(self.edit_encounter) + text_layout.addWidget(QLabel("Locations:")) + text_layout.addWidget(self.locations_tree) + + # Add New Encounter button + self.add_encounter_button = QPushButton("Add New Encounter") + self.add_encounter_button.clicked.connect(self.add_new_encounter) + text_layout.addWidget(self.add_encounter_button) + # Move buttons to the bottom text_layout.addStretch(1) @@ -187,7 +219,6 @@ class DBEditor(QMainWindow): main_layout.addLayout(right_layout, 1) self.load_pokemon_list() - self.adjust_list_width() def adjust_list_width(self): max_width = 0 @@ -202,13 +233,16 @@ class DBEditor(QMainWindow): self.search_bar.setFixedWidth(list_width) def load_pokemon_list(self): + self.pokemon_list.clear() self.cursor.execute(''' SELECT pf.PFIC, pf.name, pf.form_name, pf.national_dex FROM pokemon_forms pf - ORDER BY pf.national_dex, pf.form_name ''') pokemon_data = self.cursor.fetchall() + # Sort the pokemon_data based on PFIC + pokemon_data.sort(key=lambda x: parse_pfic(x[0])) + for pfic, name, form_name, national_dex in pokemon_data: display_name = f"{national_dex:04d} - {name}" if form_name: @@ -217,11 +251,33 @@ class DBEditor(QMainWindow): item.setData(Qt.ItemDataRole.UserRole, pfic) self.pokemon_list.addItem(item) + self.update_encounter_cache() + self.update_pokemon_list_highlights() + self.adjust_list_width() + self.filter_pokemon_list() + def filter_pokemon_list(self): search_text = self.search_bar.text().lower() + show_only_home_storable = self.filter_home_storable.isChecked() + for i in range(self.pokemon_list.count()): item = self.pokemon_list.item(i) - item.setHidden(search_text not in item.text().lower()) + pfic = item.data(Qt.ItemDataRole.UserRole) + + # Check if the item matches the search text + text_match = search_text in item.text().lower() + + # Check if the item is storable in Home (if the filter is active) + home_storable = True + if show_only_home_storable: + self.cursor.execute('SELECT storable_in_home FROM pokemon_storage WHERE PFIC = ?', (pfic,)) + result = self.cursor.fetchone() + home_storable = result[0] if result else False + + # Show the item only if it matches both filters + item.setHidden(not (text_match and home_storable)) + + self.update_pokemon_list_highlights() def load_pokemon_details(self, current, previous): if not current: @@ -243,6 +299,7 @@ class DBEditor(QMainWindow): self.national_dex_label.setText(str(national_dex)) self.generation_label.setText(str(generation)) self.home_checkbox.setChecked(bool(storable_in_home)) + self.home_checkbox.stateChanged.connect(self.update_home_storable) # Load and display the image image_path = f"images-new/{pfic}.png" @@ -255,9 +312,19 @@ class DBEditor(QMainWindow): # Load and display evolution chain self.load_evolution_chain(pfic) + # Load and display encounter locations + self.load_encounter_locations(pfic) + self.current_pfic = pfic self.add_evolution_button.setEnabled(True) # Enable the button when a Pokémon is selected + def update_home_storable(self): + if hasattr(self, 'current_pfic'): + storable_in_home = self.home_checkbox.isChecked() + self.cursor.execute('UPDATE pokemon_storage SET storable_in_home = ? WHERE PFIC = ?', (storable_in_home, self.current_pfic)) + self.conn.commit() + self.filter_pokemon_list() # Reapply the filter + def edit_evolution(self, item, column): parent = item.parent() if not parent: @@ -443,6 +510,265 @@ class DBEditor(QMainWindow): # Refresh the evolution chain display self.load_evolution_chain(self.current_pfic) + def load_encounter_locations(self, pfic): + self.locations_tree.clear() + self.cursor.execute(''' + SELECT game, location, day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars, rods, fishing + FROM encounters + WHERE pfic = ? + ORDER BY game, location + ''', (pfic,)) + encounters = self.cursor.fetchall() + + game_items = {} + for encounter in encounters: + game, location, day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars, rods, fishing = encounter + + if game not in game_items: + game_item = QTreeWidgetItem([game]) + game_items[game] = game_item + # Use generation for sorting, default to 0 if not found + game_item.setData(0, Qt.ItemDataRole.UserRole, self.game_generations.get(game, 0)) + + location_item = QTreeWidgetItem([location]) + details = [] + if day: + details.append(f"Day: {day}") + if time: + details.append(f"Time: {time}") + if dual_slot: + details.append(f"Dual Slot: {dual_slot}") + if static_encounter: + details.append(f"Static Encounter (Count: {static_encounter_count})") + if extra_text: + details.append(f"Extra: {extra_text}") + if stars: + details.append(f"Stars: {stars}") + if fishing: + details.append(f"Fishing") + if rods: + details.append(f"Rods: {rods}") + + location_item.setText(1, ", ".join(details)) + game_items[game].addChild(location_item) + + # Sort game items by generation and add them to the tree + sorted_game_items = sorted(game_items.values(), key=lambda x: x.data(0, Qt.ItemDataRole.UserRole)) + self.locations_tree.addTopLevelItems(sorted_game_items) + self.locations_tree.expandAll() + + # Update the cache for this Pokémon + self.encounter_cache[pfic] = len(encounters) > 0 + + # After updating the locations tree + self.update_pokemon_list_highlights() + + def edit_encounter(self, item, column): + if item.parent() is None: # This is a game item, not a location item + return + + game = item.parent().text(0) + location = item.text(0) + + dialog = QDialog(self) + dialog.setWindowTitle("Edit Encounter") + layout = QFormLayout(dialog) + + game_edit = QLineEdit(game) + location_edit = QLineEdit(location) + day_edit = QLineEdit() + time_edit = QLineEdit() + dual_slot_edit = QLineEdit() + static_encounter_check = QCheckBox("Static Encounter") + static_encounter_count_edit = QSpinBox() + extra_text_edit = QLineEdit() + stars_edit = QLineEdit() + fishing_check = QCheckBox("Fishing") + rods_edit = QLineEdit() + + layout.addRow("Game:", game_edit) + layout.addRow("Location:", location_edit) + layout.addRow("Day:", day_edit) + layout.addRow("Time:", time_edit) + layout.addRow("Dual Slot:", dual_slot_edit) + layout.addRow("Static Encounter:", static_encounter_check) + layout.addRow("Static Encounter Count:", static_encounter_count_edit) + layout.addRow("Extra Text:", extra_text_edit) + layout.addRow("Stars:", stars_edit) + layout.addRow("Fishing:", fishing_check) + layout.addRow("Rods:", rods_edit) + + # Fetch current values + self.cursor.execute(''' + SELECT day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars, fishing, rods + FROM encounters + WHERE pfic = ? AND game = ? AND location = ? + ''', (self.current_pfic, game, location)) + current_values = self.cursor.fetchone() + + if current_values: + day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars, fishing, rods = current_values + day_edit.setText(day or "") + time_edit.setText(time or "") + dual_slot_edit.setText(dual_slot or "") + static_encounter_check.setChecked(bool(static_encounter)) + static_encounter_count_edit.setValue(static_encounter_count or 0) + extra_text_edit.setText(extra_text or "") + stars_edit.setText(stars or "") + fishing_check.setChecked(bool(fishing)) + rods_edit.setText(rods or "") + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, + Qt.Orientation.Horizontal, dialog) + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + layout.addRow(buttons) + + if dialog.exec() == QDialog.DialogCode.Accepted: + new_game = game_edit.text() + new_location = location_edit.text() + new_day = day_edit.text() or None + new_time = time_edit.text() or None + new_dual_slot = dual_slot_edit.text() or None + new_static_encounter = static_encounter_check.isChecked() + new_static_encounter_count = static_encounter_count_edit.value() + new_extra_text = extra_text_edit.text() or None + new_stars = stars_edit.text() or None + new_fishing = fishing_check.isChecked() + new_rods = rods_edit.text() or None + + # Update the database + self.cursor.execute(''' + UPDATE encounters + SET game = ?, location = ?, day = ?, time = ?, dual_slot = ?, + static_encounter = ?, static_encounter_count = ?, extra_text = ?, + stars = ?, fishing = ?, rods = ? + WHERE pfic = ? AND game = ? AND location = ? + ''', (new_game, new_location, new_day, new_time, new_dual_slot, + new_static_encounter, new_static_encounter_count, new_extra_text, + new_stars, new_fishing, new_rods, + self.current_pfic, game, location)) + self.conn.commit() + + # Update the cache if all encounters for this Pokémon were deleted + if not self.check_pokemon_has_encounters(self.current_pfic): + self.encounter_cache[self.current_pfic] = False + + # Refresh the locations tree + self.load_encounter_locations(self.current_pfic) + + def add_new_encounter(self): + dialog = QDialog(self) + dialog.setWindowTitle("Add New Encounter") + layout = QFormLayout(dialog) + + game_edit = QLineEdit() + location_edit = QLineEdit() + day_edit = QLineEdit() + time_edit = QLineEdit() + dual_slot_edit = QLineEdit() + static_encounter_check = QCheckBox("Static Encounter") + static_encounter_count_edit = QSpinBox() + extra_text_edit = QLineEdit() + stars_edit = QLineEdit() + fishing_check = QCheckBox("Fishing") + rods_edit = QLineEdit() + + layout.addRow("Game:", game_edit) + layout.addRow("Location:", location_edit) + layout.addRow("Day:", day_edit) + layout.addRow("Time:", time_edit) + layout.addRow("Dual Slot:", dual_slot_edit) + layout.addRow("Static Encounter:", static_encounter_check) + layout.addRow("Static Encounter Count:", static_encounter_count_edit) + layout.addRow("Extra Text:", extra_text_edit) + layout.addRow("Stars:", stars_edit) + layout.addRow("Fishing:", fishing_check) + layout.addRow("Rods:", rods_edit) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, + Qt.Orientation.Horizontal, dialog) + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + layout.addRow(buttons) + + if dialog.exec() == QDialog.DialogCode.Accepted: + game = game_edit.text() + location = location_edit.text() + day = day_edit.text() or None + time = time_edit.text() or None + dual_slot = dual_slot_edit.text() or None + static_encounter = static_encounter_check.isChecked() + static_encounter_count = static_encounter_count_edit.value() + extra_text = extra_text_edit.text() or None + stars = stars_edit.text() or None + fishing = fishing_check.isChecked() + rods = rods_edit.text() or None + + # Insert new encounter into the database + self.cursor.execute(''' + INSERT INTO encounters + (pfic, game, location, day, time, dual_slot, static_encounter, static_encounter_count, extra_text, stars, fishing, rods) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (self.current_pfic, game, location, day, time, dual_slot, static_encounter, static_encounter_count, extra_text, stars, fishing, rods)) + self.conn.commit() + + # Update the cache + self.encounter_cache[self.current_pfic] = True + + # Refresh the locations tree + self.load_encounter_locations(self.current_pfic) + + # Add this as a class attribute in the DBEditor class + game_generations = { + "Red": 1, "Blue": 1, "Yellow": 1, + "Gold": 2, "Silver": 2, "Crystal": 2, + "Ruby": 3, "Sapphire": 3, "Emerald": 3, "FireRed": 3, "LeafGreen": 3, + "Diamond": 4, "Pearl": 4, "Platinum": 4, "HeartGold": 4, "SoulSilver": 4, + "Black": 5, "White": 5, "Black 2": 5, "White 2": 5, + "X": 6, "Y": 6, "Omega Ruby": 6, "Alpha Sapphire": 6, + "Sun": 7, "Moon": 7, "Ultra Sun": 7, "Ultra Moon": 7, + "Sword": 8, "Shield": 8, "Brilliant Diamond": 8, "Shining Pearl": 8, "Expansion Pass": 8, + "Legends: Arceus": 8, + "Scarlet": 9, "Violet": 9, "The Teal Mask": 9, "The Hidden Treasure of Area Zero": 9, "The Hidden Treasure of Area Zero (Scarlet)": 9, "The Hidden Treasure of Area Zero (Violet)": 9, "The Teal Mask (Scarlet)": 9, "The Teal Mask (Violet)": 9, + "Pokémon Go": 0, "Pokémon Home": 0 + } + + def toggle_highlight_mode(self): + self.update_pokemon_list_highlights() + + def update_pokemon_list_highlights(self): + highlight_mode = self.highlight_no_encounters.isChecked() + for i in range(self.pokemon_list.count()): + item = self.pokemon_list.item(i) + pfic = item.data(Qt.ItemDataRole.UserRole) + + if highlight_mode: + has_encounters = self.encounter_cache.get(pfic, False) + if not has_encounters: + item.setData(Qt.ItemDataRole.BackgroundRole, QColor(255, 200, 200)) # Light red background + else: + item.setData(Qt.ItemDataRole.BackgroundRole, None) # White background + else: + item.setData(Qt.ItemDataRole.BackgroundRole, None) # White background + + def update_encounter_cache(self): + self.cursor.execute(''' + SELECT DISTINCT pfic + FROM encounters + ''') + pokemon_with_encounters = set(row[0] for row in self.cursor.fetchall()) + + for i in range(self.pokemon_list.count()): + item = self.pokemon_list.item(i) + pfic = item.data(Qt.ItemDataRole.UserRole) + self.encounter_cache[pfic] = pfic in pokemon_with_encounters + + def check_pokemon_has_encounters(self, pfic): + return self.encounter_cache.get(pfic, False) + if __name__ == '__main__': app = QApplication(sys.argv) editor = DBEditor() diff --git a/DataGatherers/DefaultForms.json b/DataGatherers/DefaultForms.json new file mode 100644 index 0000000..e09ba7d --- /dev/null +++ b/DataGatherers/DefaultForms.json @@ -0,0 +1,49 @@ +[ + "Male", + "Normal Forme", + "Hero of Many Battles", + "Altered Forme", + "Land Forme", + "Standard Mode", + "Galarian Standard Mode", + "Ordinary Forme", + "Aria Forme", + "Natural Form", + "Shield Forme", + "Neutral Mode", + "Hoopa Confined", + "Solo Form", + "Type: Normal", + "Red Core", + "Disguised Form", + "Ice Face", + "Full Belly Mode", + "Zero Form", + "Curly Form", + "Chest Form", + "Apex Build", + "Ultimate Mode", + "Teal Mask", + "Normal Form", + "Plant Cloak", + "Overcast Form", + "West Sea", + "Normal", + "Red-Striped Form", + "Spring Form", + "Incarnate Forme", + "Meadow Pattern", + "Red Flower", + "Average Size", + "50% Forme", + "Confined", + "Baile Style", + "Midday Form", + "Amped Form", + "Vanilla Cream Strawberry Sweet", + "Single Strike Style", + "Family of Three", + "Green Plumage", + "Two-Segment Form", + "Standard Form" +] \ No newline at end of file diff --git a/DataGatherers/DetermineOriginGame.py b/DataGatherers/DetermineOriginGame.py index 6312cf7..ea306b4 100644 --- a/DataGatherers/DetermineOriginGame.py +++ b/DataGatherers/DetermineOriginGame.py @@ -529,6 +529,24 @@ def get_intro_generation(pokemon_name, form, cache: CacheManager): return None +def compare_forms(a, b): + if a == None or b == None: + return False + + if a == b: + return True + + temp_a = a.lower().replace("forme", "").replace("form", "").replace("é", "e").strip() + temp_b = b.lower().replace("forme", "").replace("form", "").replace("é", "e").strip() + + temp_a = temp_a.replace("deputante", "debutante").replace("p'au", "pa'u").replace("blood moon", "bloodmoon") + temp_b = temp_b.replace("deputante", "debutante").replace("p'au", "pa'u").replace("blood moon", "bloodmoon") + + if temp_a == temp_b: + return True + + return False + def get_locations_from_bulbapedia(pokemon_name, form, cache: CacheManager): page_data = get_pokemon_data_bulbapedia(pokemon_name, cache) if not page_data: @@ -631,11 +649,19 @@ def get_locations_from_bulbapedia(pokemon_name, form, cache: CacheManager): if not main_form: continue + if main_form == "Kantonian Form": + continue + if main_form == "All Forms": main_form = form - main_form_match = fuzz.partial_ratio(form.lower(), main_form.lower()) >= 80 - sub_form_match = False if not sub_form else fuzz.partial_ratio(form.lower(), sub_form.lower()) >= 80 + main_form_match = compare_forms(form, main_form) + if not main_form_match: + main_form_match = fuzz.partial_ratio(form.lower(), main_form.lower()) >= 80 + + sub_form_match = compare_forms(form, sub_form) + if not sub_form_match: + sub_form_match = False if not sub_form else fuzz.partial_ratio(form.lower(), sub_form.lower()) >= 80 if main_form_match or sub_form_match: raw_text = raw_location.get_text() diff --git a/DataGatherers/update_location_information.py b/DataGatherers/update_location_information.py index debf21a..599c184 100644 --- a/DataGatherers/update_location_information.py +++ b/DataGatherers/update_location_information.py @@ -1,3 +1,4 @@ +import json import sqlite3 from cache_manager import CacheManager from DetermineOriginGame import get_locations_from_bulbapedia @@ -11,6 +12,7 @@ def create_encounters_table(): cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS encounters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, pfic TEXT, game TEXT, location TEXT, @@ -19,12 +21,11 @@ def create_encounters_table(): dual_slot TEXT, static_encounter_count INTEGER, static_encounter BOOLEAN, - only_two BOOLEAN, + starter BOOLEAN, extra_text TEXT, stars TEXT, fishing BOOLEAN, - fishing_rod_needed TEXT, - PRIMARY KEY (pfic, game, location) + rods TEXT ) ''') conn.commit() @@ -112,6 +113,7 @@ def extract_additional_information(s): details["dual_slot"] = None details["static_encounter_count"] = 0 details["static_encounter"] = False + details["starter"] = False details["extra_text"] = [] details["stars"] = [] details["Fishing"] = False @@ -123,16 +125,21 @@ def extract_additional_information(s): soup = BeautifulSoup(s, 'html.parser') full_text = soup.get_text() sup_tags = soup.find_all('sup') - sup_text = None + sup_text = [] + + if "first partner" in full_text.lower(): + details["starter"] = True for sup_tag in sup_tags: - sup_text = sup_tag.get_text(strip=True) + text = sup_tag.get_text(strip=True) - if find_match(sup_text, days): - details["days"].append(sup_text) + if find_match(text, days): + details["days"].append(text) + sup_text.append(text) - if find_match(sup_text, times): - details["times"].append(sup_text) + if find_match(text, times): + details["times"].append(text) + sup_text.append(text) bracket_text = extract_bracketed_text(full_text, 2) @@ -141,59 +148,99 @@ def extract_additional_information(s): text_lower = text.lower() if text_lower in all_games: - details["dual_slot"] = text + match = find_match(text_lower, all_games) + if match: + details["dual_slot"] = match + text = re.sub(match, '', text_lower, flags=re.IGNORECASE) + + match = find_match(text_lower, days) + if match: + details["days"].append(match) + text = re.sub(match, '', text_lower, flags=re.IGNORECASE) + + match = find_match(text_lower, times) + if match: + details["times"].append(match) + text = re.sub(match, '', text_lower, flags=re.IGNORECASE) if "only one" in text_lower: details["static_encounter_count"] = 1 details["static_encounter"] = True - text = re.sub(r'only one', '', text_lower, flags=re.IGNORECASE).strip() + text = re.sub(r'only one', '', text_lower, flags=re.IGNORECASE) elif "only two" in text_lower: details["static_encounter_count"] = 2 details["static_encounter"] = True - text = re.sub(r'only two', '', text_lower, flags=re.IGNORECASE).strip() - #elif "rod" in text_lower: - # details["static_encounter_count"] = 2 - # details["static_encounter"] = True - # text = re.sub(r'only two', '', text_lower, flags=re.IGNORECASE).strip() + text = re.sub(r'only two', '', text_lower, flags=re.IGNORECASE) + + if "rod" in text_lower: + match = find_match(text_lower, rods) + if match: + details["Fishing"] = True + details["Rods"].append(match) + text = re.sub(match, '', text_lower, flags=re.IGNORECASE) if "★" in text: star_parts = re.findall(r'\d★,*', text) for part in star_parts: details["stars"].append(part.replace(',', '').strip()) - text = re.sub(r'\d★,*', '', text).strip() + text = re.sub(r'\d★,*', '', text) + + if text.strip() != "": + details["extra_text"].append(text.strip()) + sup_text.append(text.strip()) - if text: - details["extra_text"].append(text) + if len(sup_text) > 0: + for text in sup_text: + full_text = full_text.replace(text, "") - if sup_text: - return full_text.replace(sup_text, ""), details + if len(bracket_text) > 0: + for text in bracket_text: + full_text = full_text.replace(text, "") + full_text = full_text.replace('(', "").replace(')', "") + + return full_text.strip(), details else: return full_text, details -def save_encounter(conn, pfic, game, location, days, times, dual_slot,static_encounter, static_encounter_count, extra_text, stars): +def save_encounter(conn, pfic, game, location, days, times, dual_slot, static_encounter, static_encounter_count, extra_text, stars, rods, fishing, starter): cursor = conn.cursor() if len(days) > 0: for day in days: cursor.execute(''' INSERT OR REPLACE INTO encounters - (pfic, game, location, day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (pfic, game, location, day, None, dual_slot, static_encounter_count, static_encounter, ' '.join(extra_text), ','.join(stars))) + (pfic, game, location, day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars, rods, fishing, starter) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (pfic, game, location, day, None, dual_slot, static_encounter_count, static_encounter, ' '.join(extra_text), ','.join(stars), ','.join(rods), fishing, starter)) elif len(times) > 0: for time in times: cursor.execute(''' INSERT OR REPLACE INTO encounters - (pfic, game, location, day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (pfic, game, location, None, time, dual_slot, static_encounter_count, static_encounter, ' '.join(extra_text), ','.join(stars))) + (pfic, game, location, day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars, rods, fishing, starter) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (pfic, game, location, None, time, dual_slot, static_encounter_count, static_encounter, ' '.join(extra_text), ','.join(stars), ','.join(rods), fishing, starter)) else: cursor.execute(''' INSERT OR REPLACE INTO encounters - (pfic, game, location, day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (pfic, game, location, None, None, dual_slot, static_encounter_count, static_encounter, ' '.join(extra_text), ','.join(stars))) + (pfic, game, location, day, time, dual_slot, static_encounter_count, static_encounter, extra_text, stars, rods, fishing, starter) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (pfic, game, location, None, None, dual_slot, static_encounter_count, static_encounter, ' '.join(extra_text), ','.join(stars), ','.join(rods), fishing, starter)) conn.commit() +def compare_forms(a, b): + if a == b: + return True + + temp_a = a.lower().replace("forme", "").replace("form", "").replace("é", "e").strip() + temp_b = b.lower().replace("forme", "").replace("form", "").replace("é", "e").strip() + + temp_a = temp_a.replace("deputante", "debutante").replace("p'au", "pa'u").replace("blood moon", "bloodmoon") + temp_b = temp_b.replace("deputante", "debutante").replace("p'au", "pa'u").replace("blood moon", "bloodmoon") + + if temp_a == temp_b: + return True + + return False + if __name__ == "__main__": cache = CacheManager() @@ -206,20 +253,41 @@ if __name__ == "__main__": ''') pokemon_forms = cursor.fetchall() + try: + with open('./DataGatherers/DefaultForms.json', 'r') as f: + default_forms = json.load(f) + except FileNotFoundError: + default_forms = [] + for pfic, name, form, national_dex in pokemon_forms: print(f"Processing {name} {form if form else ''}") if form and name in form: form = form.replace(name, "").strip() - gender = None - if form and "male" in form.lower(): - gender = form + if form and form in default_forms: + form = None + + if name == "Unown" and (form != "!" and form != "?"): + form = None + + if name == "Tauros" and form == "Combat Breed": + form = "Paldean Form" + + if name == "Alcremie": form = None - encounters_to_ignore = ["trade", "time capsule", "unobtainable", "evolve", "tradeversion", "poké transfer", "friend safari"] + if form and form.lower() == "female": + form = None + + search_form = form + # unrecognized_forms = ["Unown", "Zacian", "Zamazenta"] + # if name in unrecognized_forms: + # search_form = None + + encounters_to_ignore = ["trade", "time capsule", "unobtainable", "evolve", "tradeversion", "poké transfer", "friend safari", "unavailable", "pokémon home"] - encounter_data = get_locations_from_bulbapedia(name, form, cache) + encounter_data = get_locations_from_bulbapedia(name, search_form, cache) if encounter_data == None: continue @@ -253,15 +321,15 @@ if __name__ == "__main__": print(f"Remaining: {remaining.strip()}") print(f"Details: {details}") - if len(details["days"]) > 0 and len(details["times"]) > 0: + if len(details["times"]) > 0: print("Stupid Data") for route in routes: route_name = f"Route {route}" - save_encounter(conn, pfic, encounter, route_name, details["days"], details["times"], details["dual_slot"], details["static_encounter"], details["static_encounter_count"], details["extra_text"], details["stars"]) + save_encounter(conn, pfic, encounter, route_name, details["days"], details["times"], details["dual_slot"], details["static_encounter"], details["static_encounter_count"], details["extra_text"], details["stars"], details["Rods"], details["Fishing"], details["starter"] ) if remaining != "": remaining_locations = remaining.replace(" and ", ",").split(",") for remaining_location in remaining_locations: - save_encounter(conn, pfic, encounter, remaining_location.strip(), details["days"], details["times"], details["dual_slot"], details["static_encounter"], details["static_encounter_count"], details["extra_text"], details["stars"]) + save_encounter(conn, pfic, encounter, remaining_location.strip(), details["days"], details["times"], details["dual_slot"], details["static_encounter"], details["static_encounter_count"], details["extra_text"], details["stars"], details["Rods"], details["Fishing"], details["starter"] ) conn.close() diff --git a/DataGatherers/update_storable_in_home.py b/DataGatherers/update_storable_in_home.py new file mode 100644 index 0000000..7322a17 --- /dev/null +++ b/DataGatherers/update_storable_in_home.py @@ -0,0 +1,186 @@ +import json +import sqlite3 +from cache_manager import CacheManager +from bs4 import BeautifulSoup, Tag + +def create_pokemon_storage_db(): + conn = sqlite3.connect('pokemon_forms.db') + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pokemon_storage ( + PFIC TEXT PRIMARY KEY, + storable_in_home BOOLEAN NOT NULL, + FOREIGN KEY (PFIC) REFERENCES pokemon_forms (PFIC) + ) + ''') + conn.commit() + return conn + +def insert_pokemon_storage(conn, pfic: str, storable_in_home: bool): + cursor = conn.cursor() + cursor.execute(''' + INSERT OR REPLACE INTO pokemon_storage + (PFIC, storable_in_home) + VALUES (?, ?) + ''', (pfic, storable_in_home)) + conn.commit() + +def scrape_serebii_region_pokemon(url, cache): + response = cache.fetch_url(url) + + if not response: + return [] + + soup = BeautifulSoup(response, 'html.parser') + + pokemon_list = [] + + # Find the main table containing Pokémon data + table = soup.find('table', class_='dextable') + + if table: + rows = table.find_all('tr')[2:] # Skip the header row and the game intro row + for row in rows: + cells = row.find_all('td') + if len(cells) <= 5: # Ensure we have enough cells to check depositability. if only 5 then its not depositable in any game. + continue + + number = cells[0].text.strip().lstrip('#') + name = cells[2].text.strip() + + # Get the image URL + img_url = cells[1].find('img')['src'] + full_img_url = f"https://www.serebii.net{img_url}" + + pokemon_list.append({ + 'number': number, + 'name': name, + 'image_url': full_img_url + }) + + return pokemon_list + +def scrape_all_regions(cache): + base_url = "https://www.serebii.net/pokemonhome/" + regions = ["kanto", "johto", "hoenn", "sinnoh", "unova", "kalos", "alola", "galar", "paldea", "hisui", "unknown"] + all_pokemon = [] + + for region in regions: + url = f"{base_url}{region}pokemon.shtml" + region_pokemon = scrape_serebii_region_pokemon(url, cache) + all_pokemon.extend(region_pokemon) + print(f"Scraped {len(region_pokemon)} Pokémon from {region.capitalize()} region") + + return all_pokemon + +def get_objects_by_number(array, target_number): + return [obj for obj in array if obj['number'] == target_number] + +def extract_bracketed_text(string): + results = [] + stack = [] + start_index = -1 + + for i, char in enumerate(string): + if char == '(': + if not stack: + start_index = i + stack.append(i) + elif char == ')': + if stack: + stack.pop() + if not stack: + results.append(string[start_index + 1:i]) + start_index = -1 + else: + print(f"Warning: Unmatched closing parenthesis at position {i}") + + # Handle any remaining unclosed brackets + if stack: + print(f"Warning: {len(stack)} unmatched opening parentheses") + for unmatched_start in stack: + results.append(string[unmatched_start + 1:]) + + return results + +def compare_forms(a, b): + if a == b: + return True + + temp_a = a.lower().replace("forme", "").replace("form", "").replace("é", "e").strip() + temp_b = b.lower().replace("forme", "").replace("form", "").replace("é", "e").strip() + + temp_a = temp_a.replace("deputante", "debutante").replace("p'au", "pa'u").replace("blood moon", "bloodmoon") + temp_b = temp_b.replace("deputante", "debutante").replace("p'au", "pa'u").replace("blood moon", "bloodmoon") + + if temp_a == temp_b: + return True + + return False + +if __name__ == "__main__": + cache = CacheManager() + + conn = create_pokemon_storage_db() + cursor = conn.cursor() + cursor.execute(''' + SELECT pf.PFIC, pf.name, pf.form_name, pf.national_dex + FROM pokemon_forms pf + ORDER BY pf.national_dex, pf.form_name + ''') + pokemon_forms = cursor.fetchall() + + all_depositable_pokemon = scrape_all_regions(cache) + + try: + with open('./DataGatherers/DefaultForms.json', 'r') as f: + default_forms = json.load(f) + except FileNotFoundError: + default_forms = [] + + for pfic, name, form, national_dex in pokemon_forms: + print(f"Processing {name} {form if form else ''}") + + storable_in_home = False + + if form and name in form: + form = form.replace(name, "").strip() + + # serebii doesn't list gender in the table so we have to assume based on form name. + if form and ("male" in form.lower() or "female" in form.lower()): + form = None + + if form and form in default_forms: + form = None + + if name == "Unown" and (form != "!" and form != "?"): + form = None + + if name == "Tauros" and form == "Combat Breed": + form = "Paldean Form" + + if name == "Alcremie": + form = None + + pokemon = get_objects_by_number(all_depositable_pokemon, f"{national_dex:04d}") + for p in pokemon: + if form == None and name.lower() in p['name'].lower(): + storable_in_home = True + break + + parts = p['name'].split(" ") + if len(parts) > 1 and parts[0] == form: + storable_in_home = True + + brackets = extract_bracketed_text(p['name']) + if brackets: + for bracket in brackets: + if name in bracket: + bracket = bracket.replace(name, "").strip() + if compare_forms(form, bracket): + storable_in_home = True + break + + print(f"{name} {form if form else ''} is storable in home: {storable_in_home}") + insert_pokemon_storage(conn, pfic, storable_in_home) + \ No newline at end of file diff --git a/patches.json b/patches.json index 548e30a..d3ce75e 100644 --- a/patches.json +++ b/patches.json @@ -1,131 +1 @@ -{ - "evolution_0019-01-000-2_0020-01-001-0": { - "action": "delete" - }, - "evolution_0868-08-000-0_0869-08-001-0": { - "action": "update", - "new_from_pfic": "0868-08-000-0", - "new_to_pfic": "0869-08-001-0", - "new_method": "Spin clockwise for more than 5 seconds during the day while holding a Berry Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-002-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-002-0", - "method": "Spin clockwise for more than 5 seconds during the day while holding a Clover Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-003-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-003-0", - "method": "Spin clockwise for more than 5 seconds during the day while holding a Flower Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-004-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-004-0", - "method": "Spin clockwise for more than 5 seconds during the day while holding a Love Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-005-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-005-0", - "method": "Spin clockwise for more than 5 seconds during the day while holding a Ribbon Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-006-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-006-0", - "method": "Spin clockwise for more than 5 seconds during the day while holding a Star Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-007-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-007-0", - "method": "Spin clockwise for more than 5 seconds during the day while holding a Strawberry Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-008-0": { - "action": "update", - "new_from_pfic": "0868-08-000-0", - "new_to_pfic": "0869-08-008-0", - "new_method": "Spin clockwise for more than 5 seconds at night while holding a Berry Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-009-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-009-0", - "method": "Spin clockwise for more than 5 seconds at night while holding a Clover Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-010-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-010-0", - "method": "Spin clockwise for more than 5 seconds at night while holding a Flower Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-011-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-011-0", - "method": "Spin clockwise for more than 5 seconds at night while holding a Love Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-012-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-012-0", - "method": "Spin clockwise for more than 5 seconds at night while holding a Ribbon Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-013-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-013-0", - "method": "Spin clockwise for more than 5 seconds at night while holding a Star Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-014-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-014-0", - "method": "Spin clockwise for more than 5 seconds at night while holding a Strawberry Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-016-0": { - "action": "update", - "new_from_pfic": "0868-08-000-0", - "new_to_pfic": "0869-08-016-0", - "new_method": "Spin clockwise for less than 5 seconds at night while holding a Berry Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-017-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-017-0", - "method": "Spin clockwise for less than 5 seconds at night while holding a Clover Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-018-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-018-0", - "method": "Spin clockwise for less than 5 seconds at night while holding a Flower Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-019-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-019-0", - "method": "Spin clockwise for less than 5 seconds at night while holding a Love Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-020-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-020-0", - "method": "Spin clockwise for less than 5 seconds at night while holding a Ribbon Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-021-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-021-0", - "method": "Spin clockwise for less than 5 seconds at night while holding a Star Sweet \u2192" - }, - "evolution_0868-08-000-0_0869-08-022-0": { - "action": "add", - "from_pfic": "0868-08-000-0", - "to_pfic": "0869-08-022-0", - "method": "Spin clockwise for less than 5 seconds at night while holding a Strawberry Sweet \u2192" - } -} \ No newline at end of file +{"evolution_0019-01-000-2_0020-01-001-0": {"action": "delete"}, "evolution_0868-08-000-0_0869-08-001-0": {"action": "update", "new_from_pfic": "0868-08-000-0", "new_to_pfic": "0869-08-001-0", "new_method": "Spin clockwise for more than 5 seconds during the day while holding a Berry Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-002-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-002-0", "method": "Spin clockwise for more than 5 seconds during the day while holding a Clover Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-003-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-003-0", "method": "Spin clockwise for more than 5 seconds during the day while holding a Flower Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-004-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-004-0", "method": "Spin clockwise for more than 5 seconds during the day while holding a Love Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-005-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-005-0", "method": "Spin clockwise for more than 5 seconds during the day while holding a Ribbon Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-006-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-006-0", "method": "Spin clockwise for more than 5 seconds during the day while holding a Star Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-007-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-007-0", "method": "Spin clockwise for more than 5 seconds during the day while holding a Strawberry Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-008-0": {"action": "update", "new_from_pfic": "0868-08-000-0", "new_to_pfic": "0869-08-008-0", "new_method": "Spin clockwise for more than 5 seconds at night while holding a Berry Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-009-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-009-0", "method": "Spin clockwise for more than 5 seconds at night while holding a Clover Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-010-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-010-0", "method": "Spin clockwise for more than 5 seconds at night while holding a Flower Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-011-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-011-0", "method": "Spin clockwise for more than 5 seconds at night while holding a Love Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-012-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-012-0", "method": "Spin clockwise for more than 5 seconds at night while holding a Ribbon Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-013-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-013-0", "method": "Spin clockwise for more than 5 seconds at night while holding a Star Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-014-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-014-0", "method": "Spin clockwise for more than 5 seconds at night while holding a Strawberry Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-016-0": {"action": "update", "new_from_pfic": "0868-08-000-0", "new_to_pfic": "0869-08-016-0", "new_method": "Spin clockwise for less than 5 seconds at night while holding a Berry Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-017-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-017-0", "method": "Spin clockwise for less than 5 seconds at night while holding a Clover Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-018-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-018-0", "method": "Spin clockwise for less than 5 seconds at night while holding a Flower Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-019-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-019-0", "method": "Spin clockwise for less than 5 seconds at night while holding a Love Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-020-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-020-0", "method": "Spin clockwise for less than 5 seconds at night while holding a Ribbon Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-021-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-021-0", "method": "Spin clockwise for less than 5 seconds at night while holding a Star Sweet \u2192"}, "evolution_0868-08-000-0_0869-08-022-0": {"action": "add", "from_pfic": "0868-08-000-0", "to_pfic": "0869-08-022-0", "method": "Spin clockwise for less than 5 seconds at night while holding a Strawberry Sweet \u2192"}, "0869-08-064-0": {"storable_in_home": false}} \ No newline at end of file diff --git a/pokemon_forms.db b/pokemon_forms.db index fb03d0c..981df6a 100644 Binary files a/pokemon_forms.db and b/pokemon_forms.db differ