From 310e025dac4d4c6e651c57601cd3ad42305edbe3 Mon Sep 17 00:00:00 2001 From: Quildra Date: Sun, 13 Oct 2024 19:24:13 +0100 Subject: [PATCH] - Added most of the new stuff for home status, a better ui --- DBEditor/DBEditor.py | 336 ++++++++++++++++++- DataGatherers/DefaultForms.json | 49 +++ DataGatherers/DetermineOriginGame.py | 30 +- DataGatherers/update_location_information.py | 146 +++++--- DataGatherers/update_storable_in_home.py | 186 ++++++++++ patches.json | 132 +------- pokemon_forms.db | Bin 282624 -> 9416704 bytes 7 files changed, 702 insertions(+), 177 deletions(-) create mode 100644 DataGatherers/DefaultForms.json create mode 100644 DataGatherers/update_storable_in_home.py 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 fb03d0c625ca5906829e97cdbf73792a266a4676..981df6af1bb70688b432d9c0cfb29873d1eb2662 100644 GIT binary patch literal 9416704 zcmeFacVJu9`9G{H$kd!Vd zwnKq#I_ac)x3sjSo3?Z>+A_N5x8Ls4(%HLKc{DrG-V-<>;5dPc+uH-d)4mu8xQ_?Se_{Mef7O4E`sLsLOEQ-3 ze^D~^*MJ*e9ccP`>fYqNO)qXdFZrza?Es$n?->g`V}WNZ@QekXvA{DHc*X+HSm6Ig z3+!+svz9DzE}zKu7jom*7cidOJD$6~k}D3>UoKs}bM4AqYuk6NT(xCwdmVRiF*}+& z6L)qUo*#|Q>gaH8JyFPnpi-qckt>gz-_Gz!GI;yqcsu`>A8g;aZP(iMYj?Kq*tv1* z%AJ?DZ(e(O`^rmqZQr;Jw{Km$ZP%HaJ+?1D(7tQ!Wfn99ed?e>X&^h1FBR+H!R%xm zGLe7My@Qo(VefdMG*L$#pMc1Ly*dwjMQ)YuM#Fku`>O5Rx2#>c&BrLiOixb89h@j< z_fFuSCqKJ9UWe_=j}PaILr<1cE)7bqFk06w7dAv^b$2^Q&XT3Hzf`D@ZT1cfXYPlb-=CWcFcby={>d$E>xW;=`MGA3KMedpSZ>$i!T zVVGU*JJ+sTyL0Wf)oc6O$4Vo)QDlqeNaO8`cdXmE`ripq@}KT@X~2sjky(A+P9R?# z%pK&$kgZIV#P!~K`|b7Wk<87zLQ!gZx9k-qpcEaSD3!BAxw^|%pZ)6+E%Q_UggNb2 zV+g;q5Cw*2C#izL9BKVXR;bPVu!_(MNuKRXcwQ z`wiuaxw7nTR7a+sAuG^1!N{zwolj>dks)IyaT&sk#-*1{L+Ukw6H+gyU(>0#rT(7! zW9sFppQj#8{WkTj)I+H+q#jItEOmG4-KqOiA531J>`A5?f71B*#(Ns48V4IUGU!r1BV@7=Gs?{IH* zhujtJ?C39}Ux>afT8oZGw@1C`$&o)rz8iUGWG2!dSrwTVaT*?NxUb>W4M!RV8rC$l zh5r$LB>buH-QnklN5U6}=ZF3pdMNas(DOt6p|zno!G8uH4&EQUJNUfdp5Qsbg!5bH zE6zKeJDhQ6r?b>)r0hNYf9<76XX~j>ES)}kS$Ep&PN&n|nU#g?NcK>6bZfSdTRE=? z#%w>sQ;c=FQR1|XMmn3NlwK`mb9OM>U(S+{i#rM+NFkcwF+s|>`1FxT=cxhloE{}3 zy%GX0YP~Mf*(OsmI%P+xkUxN$GJIicUME9;nT51S9?q8w7c>v!(em!JKRG*e{+UC_ zTsJ****P*1_pdAuWQ+M?ZbN>&lFt^~)%AHT`@j~W6dMwC>e3waI7`S{p=GTvmkwZb zFsy}X;M~?hrS~%CLz+o$8PE?~ZuBnW9h)7?oinFD(m7kIa<)$0nk|mxCRa3PRgRbG zGEJ{96$UEnw)rq&Cy-4g5S>Q5&Id)(W0yC^ElnO8gUoC6CM$0k9 zr4koh4edIF94;|%w&qF)CWhDNhqA?q>}d92Z`V~|EwNc3Gb>xjmdW<=vbXa}P&;iZ z6q9MJ7Zc}Pq3vSo3a52lu5*XG@rVAdsSHh&^CgzTS3S*_>D)0DWup?i_Gmd=qexDE zsF0hOP;%r|rg^uP!&H*Pi7l6^67J>7hs52vVrjHAG3+((Qn&V+>OraKNz*NT`c|uQ zWXsv{+`z1XNk(uYbwVMN?qD<2`ZgvslB>xWOnBZxv@fJG)HZ0oY}Sw$x?<+_C&J0|I9gCRXO#V>a%@t5aX~VbGIN( z%GPC=LH+H|PLP6Uv~Nbb6t2@fnJ%TD(Yi@%r;9IhMTG@)wQbbe>10nN%lo_LZisYt zNCTQ;+mjy}DojpP_U&6dcfCsQF(tQ2q>mxJvwIy(D7uv%(-mw(mofqU$GMK^>qV_= zRZpPkc&bV6T!Z|I%HP9<4Xt-q3S;?$EJ|K1oU>XPRS~H|3F5+*RjS7Hm^!h#klRNy zm<25>b*iowX_1;QPtI?-Sf}dJgnomi5saDVwOpiAO;J*6IbSSw%)L;XN*9*6x123L z`qEq4TQAUg*JUBnF`3F0+gi@o!c9rA@a){V=PBVCQw}1Pgg>XX`&?BH8B-2@+2Y`M zAv-?I#?#j=t>-9R6dlhr$=xfoG*b>DdAOWAU`wOx=GNs}nrWk&-rRY%D%A`Z2=c!( zn;#giuqb&kXYMi(MRQO@yrN|qHHSIvy+{|$!K8aKT~_Bg3wzL&i0(MUr3-8A$d6?4 zEYly)UXTH;jXGIHgVC6_r&KNswy(~P&0gTuvyeMxU0N*d&kEMUbp0cdNoMusi^bBg zpe;HJwE6ZUp&--HVNGB#D0rPq!CPqaP!XB8y__G)7qbPaU3a~9sFO_GoGp~bCF{(3 z7V0LmHk5M3qGT%W%OkQvkrR6xm)IXtDS1Nw-O zg~u&bJ`(EyS@T4?I=QV~8B|e4eV?F2dfv$$ZOWjAK+hwBeB8;Mb3qaH#WM|mM}BZ9 zSDM(G8_Krx?<`HG*dVB;30)va`9>C z8bzD;xN?vJl?kks>?`MTrr9^nI~7b(z={cb8zz~&alt9zh$1!|&v2yG8=Fto#>B($ z2a^em(k8NFrNKnYNjjUlmZVps!j;GJxp?P^Ftt!O&GiFXuR<@4rGarv+t`v5Kok{8 z5h-7aC@rh5Vy!7vfQpPqYjUi$NhfnHLGm7SJEihOA?LOvmBwjP{C(F~@{}6wY}6Wa z>7xu*4`bMRC|e#h_KkEWKoo<4G!v0EO%xjItMbdwi=Z@|8x%SQt_<7px zOYrloJ5R#TlWrTt&r@E!7C%$RXx4k;u|xRTIPKwQV(JR~Y?|r7&)CrWxVaOSUDGjgL3}qVd~}pKg3l<7=>9 z@a)Es#>*SmH!f?O-*{r;Uy0u&zMuGf;sc2{ByLOGoG2x(PHaw`ljuyGnh3>zAOBJO zOYx7y-yFX){@nO@JR9F0zaYLOK06+b{VDcX?5nYl$L@)}JobXvfmklq7h4%SE7lrI zxPNnh=6=)tl>09CF1P01;O5;u?pn9U?Qm1ke@1^5{ciNL(f37P7oCaTgms52qZ^~k zqYI-aM+1@HM7|gKY~;O>*F=s+Zioy=F2yRuS&^1Vtl`fMk2O5laBss~8t%ku#Pto= zG;D5I(Xg=Lr0~DOzYc#l{F(53!mke3!iT~`;a%aC;ici`up9bQ=+V$uLLUviIdn(p zaA+)ab!by)d1yiC#NZRbUj@Gt{B-c$!MlRh;AC)LurGLVusb*>7lR}-rw3pFcTPE%n)0G^7tk{L^r;XA^Z*o z{SDmhhluqC0j19|W1GP1{SdKU$B<>5*D|DylL@@09`XXOX2`PFT@0CCJ@6`q*f_{) z;FW%ew10&kBE4SDkX4yp#*k&Nmofw)WC^Gs36_zFL@I!Rt?F_LcNUszOg94e0I`ytBq^ZXErzg0rZEfYT14-xAKLzW2-`ypc8!VsH~o_Mn#BGz+G40Sr` z^m4W;SU33*kZxqeG|*@J5pbTx2pbu4Ztx@E9Ad;&wn;{eIS(=d0r*2z?EoYAm681t zIh(D7gbE|ZA`^ZDoN-1>l`1o0YSi_Nn4FIJ5p=Dkdc-R-Vk+2ZJ>nG@F?kF*vtf`r`?v6fyhWrQsT z&Mrobefk(N_SwmZwom4gdc@noh_+Z}dp+W9W5m>rt&Hfpk=as@c$*nvYoKOsszVnpXX zeIX+{r|AnAG5I=QBGzKyd5oBRo$E(%^Er%|e68>!;4Ei^^93c&_9L+TvU9+~zd5OukHC~?`Vr(}i622O&ZtMcE=EjwEoQ`& zS0^K;ycRKH%4;DbTwX|6;79QE`Spl5Pa5jSyi*tf9e?OOcrqhwL;Q1+ zMART04>-|};La2L2<}QT!uA2u#E7v*k`cBBGSlcoFjqTWj3rjkOBj;Q1|_i<@Tn4} z`8~vu-tjL1K3TwLuYitJsR^D3_yhqjqtOSYCD?rca9qGWJ%Axz zz|DZ80?v>c5Krcc763;C>=DcsjC?#9iUiJdW(6A0z)t?p689%wf|>ZXM0X+?{{i;# z561`NYvK!He~vwX`Sw(7IJO%5_>a3kbU*Ii=I(cQV<*2U_-OFH;ERK$;FjRrV8D6I z`Kd0o7U4U(r@7_VZNNjN58ZrZ&t{wv<)KHYc->8iM(-R zgWI_<5Oh2^Kj0-O+tSVw7BJxmx3yF%hJF*n>f2+mqadLXGHLa2zWee@=+~5dKP)Md zQ3Ra43j(^gR6@U^^w-06L!_e^%s%?}NiDg|U|l(MuAo=Ksf5&ZGkgG2&by%z&quf)Jdo@D>O? zN{khvJi#z!AsCw~LzO~)JoE^WPFDFb1tLfrXj?S&Fet(QQQDXi5tLPxBDNMnKO)Wp ziem~zaCVh*S;+Yzao)uG&|&~*oAy5^+xvz~10&<19}wxVA{qPk36Gg_`yLcq=piCq zqe#ZW>)?4wj-POP$%Vd8q;nL>Sb0^sf=I7br6RW8L*FCPDXjMjW9hyD=seoKE1NHb zz6*}?KWw5E#?tFSfg@>Ud?fT8qI`)9V}&vGW@wuo?axnyzD@v&@qAh!!!#JTRi+|X#Q$hGZTbXTQwez$!WO2EGBK8ni*#49J^bHnA6Btp?B_$emi*-X!zwnBIisuz%&hs5xmDUo7NzrVb`Yzye=G^6 zzS&!dRcsy~*B)XO`^U$-iCCrW2!p6&uJgxWrHrr>?5N6^+vIHvNF;dp+SX!yV_2+yM4h5xfz0(BXt` z7yTv}{4$|_6E4nU*`d-n{elpOg6KEH0iy5gY{O8-(5c^4^uCE)0e+Dddxhv_4I3J2 z_BM=ne4-PglLr|pu&&~jy&4W9#n8X-^x)6MD-;ww)~2k&I11arp(lv+De(#g2~T6v z&MaJT5I#{VOoaYPoV%1D^Tt_U!n=aTVf5Dd2ayi3@n~+(f;LPJ!Zu?lzQ>7i8LLdr z8O*@QYB*b{3}SrxccLuoY+gbJqfVD7F!S~yI2DwNdtj~cF*%&g;>GMi<-{LpFDtlHREi_Qw=VP-Vk{T0Gh~Nr7tFn#(ueY)KU4ZF zCBc+A+^_QEcw1*fe*z=$CoU;+>|j0MJWc6CIq*V%B+`TGF{a4Z_{&3dm;^qkqV|Hrogvi9D^JcL;pjB zb&6nK|ChPH;52LBa& zB>0KoYl621`%^zmeJFKD>blg{)LE$$n|{&sxu!QZ-P*LTX>C(y^6}(D$+stuC9g|v zO?D)MjbDRD!QsYSLWxF<20*ppZuT#Oy`Ut|CLU08h>MWzlr{myw#bKoB^ z#Z&y(a_m&1rBeJ!1G#fhUYe>W0Y(<^WAXZDO38b3vPzb_N)$( z-qYPt!+_yr;T>Y__ie3=utyhuyL?s+BZZU2>p^kwKuYPXVR#_@T34X7NfrEEPaf`N z79cM!n^VIGL5^A!6EZllxkmnC@&Tw5oTD|l14E_a(4Nz47$3;6f)(cV)|bl_cF(P0 zKp-6}X)+*h%&aNFMXN+FkW1TZ7z0SpN<;7@LB2jNbrN3Sv?sskJF17ElbC;>t<6{; z?@*mon$fx$>n#Hwst46O`E0aGPO>5%st0sB676&u@=)EcOma31Wv5fjL$!i3KK`JG z>I78@;d_r}%fiqLix`RXmR851%YPI0P%SIvpE~ZLdcA(0DbF1`@S!@U>cTQppepjA zT2dK*s?djOQK#xsF|iNTQKTMc@I$qrC7H6*(GS%Ts3@AnGA=%lWDtTG+4Xi+uLJEk z;~%PdaE~hhqB^YYW9p8IfT#}XJaWasKmR5KqPkD#k-4Z#ItHSe(~`J~k))>%f~XE^ zNv1mLD2VESDsO8q4#PAKgQ)hao1ZEUqMFq=Ta`rzLR7C+ON393gsARCJrpBI6X#GL z3Q=AqcFh{ z)h*g)y5xM(5!KDw2D)7A@QCUrE#H)iiI1pmRPwDoQ9D4Qxl1O(oAytQF^?%4~f7lZfokrLI5 z>S@R$i?c!{su$Kj6)!fX*|8GU3&1;`V2SGaARbq=L=^!v{*0iO@}o_-1jdn|wbroY zA-g$R7)3c?=t$$GiWn~KYO7(%Lw0i}orX)5k{m7V@@iQ1=;)!L1g)Er1pLKX>OOLqW%ojH@AlM54pUc?m>;S2rV@%c*yYyEwxIu(p$4bSn@3uced8_`o;>R*~z0g zwc(P^8rCMSoPpxscD%i zwYx@39b(jExp`44MQi(n5wN|bM#~-I1)=!V?wG&5yGBbLQk)q|@63+n2NCC_#VKH0 zOAV_YEROjQr<1#CSoV-36Ix{@o8a$`QgL!3tA)wSt#fK{qL;5O#f2zNTv&q({VY)v zE#ESP>g7h&2%mo6GP?$6dbyRScn~6ZTb9(|KQH&=mU9OTW!9H5u`E~<1$nu-r3UwT z&Y9`16?=164es)Ckhh%cFbT_{k`AiK7rxrmTys?kDlRD{xtr$JwCDVC)3fiwYJPU0 zT*(zSw$|V}FBjI9n|dh;rBY$T+!|cwWyM|&9q@-{V!QJLSWD0K=L+lR*20>A6eeJ; zb}U~+dSTt%T1XKrFSu25)uU9ISi7(mL=HrMcDAV&TM_n^9UU|M*_!5>qjIWvcp&qT zO08eiy|YapO{u7vtLId2QssZPJswBu*s8ATjVKNAidNy}$c73Y7p+TOtZc778(h(W zD=xYhl}ugu%6Zjifg-0>x^SNqi(b6}>Pk7Lo3ew^WNsK10NUTrvCOQ&L@yFvokAE}%$@n|suZX`OJ|6s4 z{Mz`|__^`U_$hHG_S@L^W1ow?FZNn^Bs?pYkM#w=6T3Lp9h(!2x_@*Zalh<-#C?-{ zyL*dUa<6hXx;?>9yX|f=`uFJ1qTh&qBKr2|%cIYaR-*kldEoqL*E9bA|6lO`*M~Ns zRYh(Tt}f|iy3(g3MG2YtjTgB_Le}a|WSfN4kO!?ea-oEj(-j(GWT}K`>5jT0dg5G$ z@H;{1Ga@OB6;4k3{cQxxv$jDZA7jXL7!i(yPE%DQh z90{Qyx{)IxtTp}!M?#Pwl0`RiB!q(KMvjD_%qbQ}LMVt>G7Bg3cJX_nxj8?qJvzVy+r%dFAddQ0$V#sQ5lMGqy?I1%|&JQqT z<$S+{tWbbR#Sc+7Cm6CS%eWsRR+%BIyYh|lo&FTy2z-u&oV2U1txMf zkrEm4L)1*J^Fvg0c|Szj4l~5|A=Z!|BGx{J@H-fyndJNsu?8h%EkH&F7_vI+eugaj zWEryTb1g&KKAFf~hOBCFwYHD7CK$PjiI&N(WJuSHOyr7s$ctRg5F3MD*q7BqUStnL zYz)fzZV6dSc9BaNvMS3iKSVe7F~r6o^Y8RS#JYqb%RW08()LM5wlid9d#kojkIs2I zvW1B{o9W1AhO9hol909J71_v;m8T7Uh_qeLkd>!(eu!9W8R9%q&e!-MGXLs&$cwCE zh;xp?LS&^Mf_jmQ{Sb7CT;zvvf8+vfA2n`6<%*o|CsO-5&ks@iI@b?T2F~$Alz|oX zkQZ6bkgmt+$k_~8MYoJ0tK51SvdXQ8AucykBIAch39laVB54V!u_@hnmLE!Ydy#H! zA9b(;5zl0z)u@*+WHstD7_usD7eichBx|uBqMFgkkY%4m3|aPB$Pn9yWG(PRl=J!Z zkQbT95bnSq@hm5Q#t850S}Es)xMDi43u=Nr@BUO$!5WaqbH= zos|4n^4H1lCO?yWPx95tTJjJg`FAB(CYL6glWyam8Xs-^O5;Zx-`scycHYNQ-*dm@ ze%O7Z`(pQIx9DE!Zg7{m^AKbIkLWL=-->=J`p)Ppqc4Pqz+m)}=!MZG(bJ<1k^hbS zDDpt$Ly@~9w?&>48I4>KSs&?*bVQmO9&h-0!#5i~+3=2rS2Vn!VLwj%+tF}A!x;^; z8p7eU`a~FZJ2Rs~b1LgJ1#9 zB6uS4tHgH_pH93R=NMMuORz7|m$(=)5OWgI_#fks#J`M_2;PJ*7TkhU4X=uCjGrB! zA3q`X&)6?x-;UiMdspmL@RT@+a|m|EE{dHQn;na|e{dhBm6ZR+rj8s>X2>H6B}6sa zk;5L3rYsW5IA=7i2sEWNB#KmDp|MA0zvR&bhQ&l>zvR&bMnSS)@@N91AlWZ@G=Y&& zCMx?S3-Y3}U-D=Iqi&S_l1H-@1<8KNquGjrWWVImY(G+R+D*-v=} z@}jbz@@TfA#LIrlqp694WIyH6)I>qDpYmvGLJ{i$bD4yQy#5ss~crM<H9 zd(kXGL9*ZSXdzWWvfuKSTN#r5mPfM$#ghG&M^gd?$$rbDDS?7yzva=CKth?Q?6)k) zi^_h>qxpbh$$ra%aOkJhp8P_Tc-e1x8E|{-K}~_(m&_DTb^(Hc5!)1L{T@ zNac~Yg@PKH#d)MUk?=!QC*ln0Y^S3!hIAg&QI{c|p-ePd4|&lDLslLe7_#yhW(W%5 z5A_Gp5JUK#AdsU$KSbI)eu!=iNXQ=3|BE45g2hLkU=jPpB&EaUu^AvO+){|!TI34(sj5NDg9Uom9m@t1yxUg5u}hrGzo8RBe{ zte-K&dBi`FpZXzU{e&U>PT3~bkNprm@v(Zyi##eJGk?fL9;t`C$iob=y^ycSkNglc zj{MLMkrF@fL!|0M46&h!^?g4?tnV>o+5Wo>S+@UBIC(Tf86m0UQiEt(eoMN)I|88Y>957t9oZ>?fLd+H3w#1l9FQs6zb0Ww7!_$Z*a8i@4z2%wh$vW)X#K+gqaiZ<{e zKxIItXagSvwAY5z5oNaRqXkhy8lrc{h%7W~o)}#rA*+NU--h^;O*!I5=jsMvi4zO(RhF3OBxG}3ll#{dtWF zOne}|EbhiW5xXrm6kF{61H139#mNAh+_};JjeaKjY^(q*j64*1JLdemA`2V-*6@*r zsfJ4$7KZ|v;=UqSrxZe$Q@#?wnqkBZDTHi9uM5LM+U&X63J{xQga2(7|~RzJKur; zp!Nw04jw2Y3gc2lZiOfv} z&O+^31TT-bAHZh_+Oy+hxq&>SjtU1keE(-(K35oo$X&yT?pCJVy>a8lB_eNG&KndZ zU8T*;GudWoaF-sr23&B|E~VgGu5!|KYybyDh+|Lzq6&Dv4chQmE;I=|()4%x+LQ^%n>&T@nluKDEVD6GaGgOJs zJF*L0G#4dQDqyOT68dkNOP~Kq@@`$9Sbg!Nt|Ob6Yl;MqHLa9QNM>1bWCOUJNQOYR zFfQ;LUm#d(pVhL~OUc%rs_$0LFV*Vvj;sO~`L(uDaiQNn>8Hu>N`8_tkWUmpiQ4da zN6zOb*=^12z-FH2N$2sCjDheZBT7EWqt5coBN=ef5uI%egbQQl`i{tB(YA4|dpQ$S zJ(e6f6I>L8ihmLIw{~zTLQ)sRk|Rs}Y*8H)8y7zIx+9Chz8bPkUxn8JIz;^O^$Xd3 z)R9)uwG1J8Gl#Iu@rg{joj&3<4kf#ebjl1_Q8qrQR;hFyS-@OvN<7Poz>(aZ9>`sH zWF9NXrj(gC-J$NdsJ|M<5gz->I07epP9%cujO2 znZ;bYN%Wc`Wx4eFS#so5a8aAgJ~CcE#^npIA78URg(Vw9`EF&&)U3}xk^pxjlC_01 zX?ykbKNnTkkvKm}+k+G{viMOMk%^GZgtZ_AE|&oQq4Q{TlUQ>k2=?U&`9*!Bq67f} z^v0B{@czU*ZTdeec*fzIAZ8)%wBIQz)&xp#9I%?<;Tu^)Qwq4#^l`psxa9B+%r@ON z*rqi7`8j-u^MgV$`7yU1Uw)PxKEMyP%hvMr^F6fd@P4o{a