diff --git a/fix_duplicate_dragons.py b/fix_duplicate_dragons.py new file mode 100644 index 0000000..ac4a3a8 --- /dev/null +++ b/fix_duplicate_dragons.py @@ -0,0 +1,199 @@ +"""Utility script to remove duplicate dragons from save database.""" +import sys +import os + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from src.save_system_sqlite import SQLiteSaveSystem +from src.utils.constants import SAVE_FILE + + +def remove_duplicate_dragons(db_file: str, dry_run: bool = True): + """Remove duplicate dragons from database. + + Dragons are considered duplicates if they have the same: + - dragon_type + - name + - speed + - strength + - rarity + - birth_time (within 1 second) + + Args: + db_file: Path to SQLite database + dry_run: If True, only show what would be removed without actually removing + """ + print(f"Opening database: {db_file}") + save_system = SQLiteSaveSystem(db_file) + + # Load all dragons + all_dragons = save_system.load_all_dragons() + + if not all_dragons: + print("No dragons found in database.") + return + + print(f"\nFound {len(all_dragons)} total dragons") + + # Track seen dragons and duplicates + seen = {} + duplicates = [] + + for i, dragon_data in enumerate(all_dragons): + # Create signature for this dragon + signature = ( + dragon_data.get('dragon_type'), + dragon_data.get('name'), + dragon_data.get('speed'), + dragon_data.get('strength'), + dragon_data.get('rarity'), + round(dragon_data.get('birth_time', 0)) # Round to nearest second + ) + + if signature in seen: + # This is a duplicate + original_idx = seen[signature] + original_db_id = all_dragons[original_idx].get('_db_id') + duplicate_db_id = dragon_data.get('_db_id') + + duplicates.append({ + 'index': i, + 'db_id': duplicate_db_id, + 'data': dragon_data, + 'original_index': original_idx, + 'original_db_id': original_db_id + }) + + print(f"\n DUPLICATE FOUND:") + print(f" Dragon #{i+1}: {dragon_data.get('name')} (DB ID: {duplicate_db_id})") + print(f" - Type: {dragon_data.get('dragon_type')}, Stage: {dragon_data.get('stage')}") + print(f" - Speed: {dragon_data.get('speed')}, Strength: {dragon_data.get('strength')}, Rarity: {dragon_data.get('rarity')}") + print(f" - Duplicate of Dragon #{original_idx+1} (DB ID: {original_db_id})") + else: + # First time seeing this dragon + seen[signature] = i + + if not duplicates: + print("\nāœ“ No duplicate dragons found!") + return + + print(f"\n{'='*60}") + print(f"Found {len(duplicates)} duplicate dragon(s)") + print(f"Unique dragons: {len(seen)}") + print(f"{'='*60}") + + if dry_run: + print("\n⚠ DRY RUN MODE - No changes will be made") + print("Run with --apply to actually remove duplicates") + return + + # Remove duplicates + print("\nšŸ—‘ļø Removing duplicate dragons...") + removed_count = 0 + + for dup in duplicates: + db_id = dup['db_id'] + name = dup['data'].get('name') + + if save_system.delete_dragon(db_id): + print(f" āœ“ Removed: {name} (DB ID: {db_id})") + removed_count += 1 + else: + print(f" āœ— Failed to remove: {name} (DB ID: {db_id})") + + print(f"\n{'='*60}") + print(f"āœ“ Removed {removed_count} duplicate dragon(s)") + print(f"{'='*60}") + + # Vacuum database to reclaim space + print("\nOptimizing database...") + save_system.vacuum() + print("āœ“ Database optimized") + + +def backup_database(db_file: str) -> str: + """Create a backup of the database before modifications. + + Args: + db_file: Path to database file + + Returns: + Path to backup file + """ + import shutil + from datetime import datetime + + if not os.path.exists(db_file): + print(f"Database file not found: {db_file}") + return None + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_file = f"{db_file}.backup_{timestamp}" + + try: + shutil.copy2(db_file, backup_file) + print(f"āœ“ Backup created: {backup_file}") + return backup_file + except Exception as e: + print(f"āœ— Failed to create backup: {e}") + return None + + +def main(): + """Main function.""" + import argparse + + parser = argparse.ArgumentParser( + description="Remove duplicate dragons from Dragon Riders save database" + ) + parser.add_argument( + '--db', + default=SAVE_FILE.replace('.json', '.db'), + help='Path to SQLite database file (default: dragon_save.db)' + ) + parser.add_argument( + '--apply', + action='store_true', + help='Actually remove duplicates (default is dry run)' + ) + parser.add_argument( + '--no-backup', + action='store_true', + help='Skip creating backup before applying changes' + ) + + args = parser.parse_args() + + if not os.path.exists(args.db): + print(f"Error: Database file not found: {args.db}") + print("\nMake sure you're running this from the DragonRider directory") + return 1 + + print("="*60) + print("Dragon Riders - Duplicate Dragon Removal Tool") + print("="*60) + + # Create backup if applying changes + if args.apply and not args.no_backup: + print("\nCreating backup before making changes...") + backup_file = backup_database(args.db) + if not backup_file: + print("\n⚠ Failed to create backup. Aborting.") + return 1 + print() + + # Remove duplicates + try: + remove_duplicate_dragons(args.db, dry_run=not args.apply) + print("\nāœ“ Done!") + return 0 + except Exception as e: + print(f"\nāœ— Error: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/dragon.py b/src/dragon.py index e0e130c..ea4c9de 100644 --- a/src/dragon.py +++ b/src/dragon.py @@ -366,7 +366,7 @@ def get_equipped_enchantments(self) -> list: def to_dict(self) -> Dict[str, Any]: """Convert dragon to dictionary for saving.""" - return { + data = { "dragon_type": self.dragon_type, "name": self.name, "stage": self.stage, @@ -385,6 +385,10 @@ def to_dict(self) -> Dict[str, Any]: "enchantments": self.enchantments, "max_enchantments": self.max_enchantments, } + # Include database ID if it exists + if hasattr(self, '_db_id'): + data['_db_id'] = self._db_id + return data @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Dragon": @@ -415,6 +419,9 @@ def from_dict(cls, data: Dict[str, Any]) -> "Dragon": dragon.is_legendary = data.get("is_legendary", data["dragon_type"] in LEGENDARY_DRAGON_TYPES) dragon.enchantments = data.get("enchantments", []) dragon.max_enchantments = data.get("max_enchantments", 5) + # Restore database ID if present + if '_db_id' in data: + dragon._db_id = data['_db_id'] return dragon def __str__(self) -> str: diff --git a/src/dragon_pen.py b/src/dragon_pen.py index 3f87faa..f11d84f 100644 --- a/src/dragon_pen.py +++ b/src/dragon_pen.py @@ -1,6 +1,6 @@ -"""Dragon Pen screen for viewing and managing all dragons.""" +"""Dragon Pen screen for viewing and managing all dragons with grid layout, search, and sorting.""" import pygame -from typing import Optional +from typing import Optional, List, Callable from src.game_state import GameState from src.sprite_manager import SpriteManager from src.background_manager import BackgroundManager @@ -11,11 +11,11 @@ ) -class DragonCard: - """Card displaying dragon information.""" +class DragonGridCard: + """Compact grid card displaying dragon sprite and name.""" def __init__(self, x: int, y: int, width: int, height: int, dragon, dragon_index: int): - """Initialize dragon card. + """Initialize dragon grid card. Args: x, y: Position @@ -27,126 +27,394 @@ def __init__(self, x: int, y: int, width: int, height: int, dragon, dragon_index self.dragon = dragon self.dragon_index = dragon_index self.is_hovered = False - self.sell_button = pygame.Rect(x + width - 70, y + height - 35, 60, 30) - self.sell_button_hovered = False + self.is_selected = False def draw(self, surface: pygame.Surface, sprite_manager: SpriteManager, small_font: pygame.font.Font, tiny_font: pygame.font.Font): - """Draw dragon card.""" + """Draw dragon grid card.""" # Card background - color = WOOD_MED if self.is_hovered else PARCHMENT - pygame.draw.rect(surface, color, self.rect) - pygame.draw.rect(surface, WOOD_DARK, self.rect, 3) - - # 3D effect - pygame.draw.line(surface, STONE_LIGHT, - (self.rect.left, self.rect.top), - (self.rect.right, self.rect.top), 2) - pygame.draw.line(surface, STONE_LIGHT, - (self.rect.left, self.rect.top), - (self.rect.left, self.rect.bottom), 2) + if self.is_selected: + color = GOLD + border_color = ORANGE + border_width = 4 + elif self.is_hovered: + color = WOOD_MED + border_color = GOLD + border_width = 3 + else: + color = PARCHMENT + border_color = WOOD_DARK + border_width = 2 - # Dragon sprite with enchantments (left side) + pygame.draw.rect(surface, color, self.rect) + pygame.draw.rect(surface, border_color, self.rect, border_width) + + # 3D effect for non-selected cards + if not self.is_selected: + pygame.draw.line(surface, STONE_LIGHT, + (self.rect.left, self.rect.top), + (self.rect.right, self.rect.top), 1) + pygame.draw.line(surface, STONE_LIGHT, + (self.rect.left, self.rect.top), + (self.rect.left, self.rect.bottom), 1) + + # Dragon sprite with enchantments enchantments = self.dragon.get_equipped_enchantments() sprite = sprite_manager.get_dragon_with_enchantments( self.dragon.dragon_type, self.dragon.stage, enchantments ) - sprite_size = 80 + + # Scale sprite to fit card + sprite_size = min(self.rect.width - 20, self.rect.height - 50) sprite_scaled = pygame.transform.scale(sprite, (sprite_size, sprite_size)) - sprite_x = self.rect.x + 10 - sprite_y = self.rect.y + (self.rect.height - sprite_size) // 2 + sprite_x = self.rect.x + (self.rect.width - sprite_size) // 2 + sprite_y = self.rect.y + 10 surface.blit(sprite_scaled, (sprite_x, sprite_y)) - # Dragon info (right side) - info_x = self.rect.x + 100 - info_y = self.rect.y + 10 + # Dragon name with index number to distinguish duplicates + # Show dragon index from original list to help identify individual dragons + name = self.dragon.name + if len(name) > 8: + name = name[:6] + ".." + + # Add index number to distinguish dragons with same name + display_name = f"{name} #{self.dragon_index + 1}" + + name_text = tiny_font.render(display_name, True, WOOD_DARK) + name_x = self.rect.x + (self.rect.width - name_text.get_width()) // 2 + name_y = self.rect.y + self.rect.height - 35 + surface.blit(name_text, (name_x, name_y)) + + # Dragon stage indicator (full stage name) + stage_color = self._get_stage_color(self.dragon.stage) + stage_text = tiny_font.render(self.dragon.stage.capitalize(), True, stage_color) + stage_x = self.rect.x + (self.rect.width - stage_text.get_width()) // 2 + stage_y = self.rect.y + self.rect.height - 18 + surface.blit(stage_text, (stage_x, stage_y)) + + # Legendary indicator + if self.dragon.is_legendary: + star_x = self.rect.x + 5 + star_y = self.rect.y + 5 + pygame.draw.circle(surface, PURPLE, (star_x + 6, star_y + 6), 8) + star_text = tiny_font.render("ā˜…", True, GOLD) + surface.blit(star_text, (star_x, star_y)) + + def _get_stage_color(self, stage: str) -> tuple: + """Get color based on dragon stage.""" + stage_colors = { + "egg": STONE_DARK, + "hatchling": GREEN, + "juvenile": BLUE, + "adult": ORANGE, + "elder": PURPLE + } + return stage_colors.get(stage, STONE_DARK) + + +class SearchBar: + """Search bar for filtering dragons by name.""" + + def __init__(self, x: int, y: int, width: int, height: int): + """Initialize search bar.""" + self.rect = pygame.Rect(x, y, width, height) + self.text = "" + self.active = False + self.cursor_visible = True + self.cursor_timer = 0 - # Name - name_text = small_font.render(self.dragon.name, True, WOOD_DARK) - surface.blit(name_text, (info_x, info_y)) + def handle_event(self, event: pygame.event.Event) -> bool: + """Handle input events. - # Type and Stage - type_text = tiny_font.render(f"{self.dragon.dragon_type.capitalize()} - {self.dragon.stage.capitalize()}", - True, STONE_DARK) - surface.blit(type_text, (info_x, info_y + 25)) + Returns: + True if search text changed + """ + changed = False - # Stats with enchantment bonuses - stats_y = info_y + 50 + if event.type == pygame.MOUSEBUTTONDOWN: + self.active = self.rect.collidepoint(event.pos) - # Speed stat with bonuses - base_speed = self.dragon.speed - total_speed = self.dragon.get_speed_with_bonuses() - speed_color = self._get_stat_color(base_speed) - if total_speed > base_speed: - speed_text = tiny_font.render(f"Speed: {base_speed} (+{total_speed - base_speed})", True, GREEN) - else: - speed_text = tiny_font.render(f"Speed: {base_speed}", True, speed_color) - surface.blit(speed_text, (info_x, stats_y)) + elif event.type == pygame.KEYDOWN and self.active: + if event.key == pygame.K_BACKSPACE: + self.text = self.text[:-1] + changed = True + elif event.key == pygame.K_RETURN: + self.active = False + elif event.unicode.isprintable(): + self.text += event.unicode + changed = True - # Strength stat with bonuses - base_strength = self.dragon.strength - total_strength = self.dragon.get_strength_with_bonuses() - strength_color = self._get_stat_color(base_strength) - if total_strength > base_strength: - strength_text = tiny_font.render(f"Strength: {base_strength} (+{total_strength - base_strength})", True, GREEN) + return changed + + def update(self, dt: float): + """Update search bar animation.""" + self.cursor_timer += dt + if self.cursor_timer >= 0.5: + self.cursor_visible = not self.cursor_visible + self.cursor_timer = 0 + + def draw(self, surface: pygame.Surface, font: pygame.font.Font): + """Draw search bar.""" + # Background + color = WHITE if self.active else PARCHMENT + pygame.draw.rect(surface, color, self.rect) + pygame.draw.rect(surface, WOOD_DARK if self.active else STONE_DARK, self.rect, 3) + + # Placeholder or text + if self.text: + text_surface = font.render(self.text, True, BLACK) else: - strength_text = tiny_font.render(f"Strength: {base_strength}", True, strength_color) - surface.blit(strength_text, (info_x + 120, stats_y)) + text_surface = font.render("Search by name...", True, STONE_DARK) - # Rarity stat with color - rarity_color = self._get_stat_color(self.dragon.rarity) - rarity_text = tiny_font.render(f"Rarity: {self.dragon.rarity}", True, rarity_color) - surface.blit(rarity_text, (info_x + 270, stats_y)) + surface.blit(text_surface, (self.rect.x + 10, self.rect.y + 8)) - # Sell value - sell_value = self.dragon.get_sell_value() - value_text = tiny_font.render(f"Value: {sell_value} coins", True, GOLD) - surface.blit(value_text, (info_x, stats_y + 25)) + # Cursor + if self.active and self.cursor_visible and self.text: + cursor_x = self.rect.x + 10 + text_surface.get_width() + 2 + cursor_y = self.rect.y + 8 + pygame.draw.line(surface, BLACK, + (cursor_x, cursor_y), + (cursor_x, cursor_y + 20), 2) - # Sell button - sell_color = RED if self.sell_button_hovered else STONE_MED - pygame.draw.rect(surface, sell_color, self.sell_button) - pygame.draw.rect(surface, WOOD_DARK, self.sell_button, 2) - sell_text = tiny_font.render("Sell", True, WHITE) - sell_text_rect = sell_text.get_rect(center=self.sell_button.center) - surface.blit(sell_text, sell_text_rect) +class SortDropdown: + """Dropdown menu for selecting sort method.""" - def _get_stat_color(self, stat: int) -> tuple: - """Get color based on stat value. + def __init__(self, x: int, y: int, width: int, height: int, options: List[tuple]): + """Initialize sort dropdown. Args: - stat: Stat value (0-100) + x, y: Position + width, height: Button dimensions + options: List of (label, sort_key) tuples + """ + self.button_rect = pygame.Rect(x, y, width, height) + self.options = options + self.is_open = False + self.is_hovered = False + self.hovered_option_index = -1 + self.selected_index = 0 + + # Dropdown menu dimensions + self.dropdown_height = len(options) * 35 + self.dropdown_rect = pygame.Rect(x, y + height, width, self.dropdown_height) + + def get_selected_sort_key(self) -> str: + """Get currently selected sort key.""" + return self.options[self.selected_index][1] + + def get_selected_label(self) -> str: + """Get currently selected label.""" + return self.options[self.selected_index][0] + + def handle_event(self, event: pygame.event.Event) -> bool: + """Handle input events. Returns: - Color tuple + True if selection changed """ - if stat >= 80: - return PURPLE # Legendary - elif stat >= 60: - return BLUE # Rare - elif stat >= 40: - return GREEN # Uncommon - else: - return STONE_DARK # Common + if event.type == pygame.MOUSEMOTION: + self.is_hovered = self.button_rect.collidepoint(event.pos) + + if self.is_open: + # Check which option is hovered + self.hovered_option_index = -1 + for i in range(len(self.options)): + option_rect = pygame.Rect( + self.dropdown_rect.x, + self.dropdown_rect.y + i * 35, + self.dropdown_rect.width, + 35 + ) + if option_rect.collidepoint(event.pos): + self.hovered_option_index = i + break + + elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + if self.is_open: + # Check if clicked on an option + if self.dropdown_rect.collidepoint(event.pos): + for i in range(len(self.options)): + option_rect = pygame.Rect( + self.dropdown_rect.x, + self.dropdown_rect.y + i * 35, + self.dropdown_rect.width, + 35 + ) + if option_rect.collidepoint(event.pos): + old_index = self.selected_index + self.selected_index = i + self.is_open = False + return old_index != i # Return True if changed + + # Clicked outside dropdown - close it + self.is_open = False + + elif self.button_rect.collidepoint(event.pos): + # Toggle dropdown + self.is_open = not self.is_open + + return False + + def draw(self, surface: pygame.Surface, font: pygame.font.Font): + """Draw sort dropdown button (dropdown menu is drawn separately on top).""" + # This method is no longer used - drawing is done inline in DragonPen.draw() + # Keeping it for compatibility but it does nothing + pass + + +class DragonDetailsPanel: + """Panel showing detailed info about selected dragon.""" + + def __init__(self, x: int, y: int, width: int, height: int): + """Initialize details panel.""" + self.rect = pygame.Rect(x, y, width, height) + self.dragon = None + self.dragon_index = -1 + self.sell_button = pygame.Rect(x + width - 110, y + height - 50, 100, 40) + self.select_button = pygame.Rect(x + 10, y + height - 50, 100, 40) + self.close_button = pygame.Rect(x + width - 35, y + 5, 30, 30) + self.sell_button_hovered = False + self.select_button_hovered = False + self.close_button_hovered = False + + def set_dragon(self, dragon, dragon_index: int): + """Set dragon to display.""" + self.dragon = dragon + self.dragon_index = dragon_index def handle_event(self, event: pygame.event.Event) -> Optional[str]: - """Handle mouse events. + """Handle input events. Returns: - 'sell' if sell button clicked, None otherwise + 'sell', 'select', or 'close' if button clicked """ if event.type == pygame.MOUSEMOTION: - self.is_hovered = self.rect.collidepoint(event.pos) - self.sell_button_hovered = self.sell_button.collidepoint(event.pos) + self.close_button_hovered = self.close_button.collidepoint(event.pos) + if self.dragon: + self.sell_button_hovered = self.sell_button.collidepoint(event.pos) + self.select_button_hovered = self.select_button.collidepoint(event.pos) elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: - if self.sell_button.collidepoint(event.pos): - return 'sell' + if self.close_button.collidepoint(event.pos): + return 'close' + if self.dragon: + if self.sell_button.collidepoint(event.pos): + return 'sell' + if self.select_button.collidepoint(event.pos): + return 'select' return None + def draw(self, surface: pygame.Surface, small_font: pygame.font.Font, tiny_font: pygame.font.Font): + """Draw details panel.""" + # Semi-transparent backdrop + backdrop = pygame.Surface((surface.get_width(), surface.get_height())) + backdrop.set_alpha(180) + backdrop.fill(BLACK) + surface.blit(backdrop, (0, 0)) + + # Panel background + pygame.draw.rect(surface, PARCHMENT, self.rect) + pygame.draw.rect(surface, WOOD_DARK, self.rect, 4) + + # Close button (always visible) + close_color = RED if self.close_button_hovered else STONE_MED + pygame.draw.rect(surface, close_color, self.close_button) + pygame.draw.rect(surface, WOOD_DARK, self.close_button, 2) + close_text = small_font.render("X", True, WHITE) + close_rect = close_text.get_rect(center=self.close_button.center) + surface.blit(close_text, close_rect) + + if not self.dragon: + return + + # Title + title = small_font.render("Dragon Details", True, WOOD_DARK) + surface.blit(title, (self.rect.x + 10, self.rect.y + 10)) + + y_offset = self.rect.y + 45 + + # Name + name_text = tiny_font.render(f"Name: {self.dragon.name}", True, BLACK) + surface.blit(name_text, (self.rect.x + 10, y_offset)) + y_offset += 25 + + # Type + type_text = tiny_font.render(f"Type: {self.dragon.dragon_type.capitalize()}", True, BLACK) + surface.blit(type_text, (self.rect.x + 10, y_offset)) + y_offset += 25 + + # Stage + stage_text = tiny_font.render(f"Stage: {self.dragon.stage.capitalize()}", True, BLACK) + surface.blit(stage_text, (self.rect.x + 10, y_offset)) + y_offset += 25 + + # Legendary status + if self.dragon.is_legendary: + legendary_text = tiny_font.render("ā˜… LEGENDARY ā˜…", True, PURPLE) + surface.blit(legendary_text, (self.rect.x + 10, y_offset)) + y_offset += 25 + + # Stats + y_offset += 10 + stats_title = tiny_font.render("Stats:", True, WOOD_DARK) + surface.blit(stats_title, (self.rect.x + 10, y_offset)) + y_offset += 22 + + # Speed + base_speed = self.dragon.speed + total_speed = self.dragon.get_speed_with_bonuses() + if total_speed > base_speed: + speed_text = tiny_font.render(f" Speed: {base_speed} (+{total_speed - base_speed}) = {total_speed}", True, GREEN) + else: + speed_text = tiny_font.render(f" Speed: {base_speed}", True, BLACK) + surface.blit(speed_text, (self.rect.x + 10, y_offset)) + y_offset += 22 + + # Strength + base_strength = self.dragon.strength + total_strength = self.dragon.get_strength_with_bonuses() + if total_strength > base_strength: + strength_text = tiny_font.render(f" Strength: {base_strength} (+{total_strength - base_strength}) = {total_strength}", True, GREEN) + else: + strength_text = tiny_font.render(f" Strength: {base_strength}", True, BLACK) + surface.blit(strength_text, (self.rect.x + 10, y_offset)) + y_offset += 22 + + # Rarity + rarity_text = tiny_font.render(f" Rarity: {self.dragon.rarity}", True, BLACK) + surface.blit(rarity_text, (self.rect.x + 10, y_offset)) + y_offset += 30 + + # Enchantments + enchantments = self.dragon.get_equipped_enchantments() + enchant_text = tiny_font.render(f"Enchantments: {len(enchantments)}/{self.dragon.max_enchantments}", True, WOOD_DARK) + surface.blit(enchant_text, (self.rect.x + 10, y_offset)) + y_offset += 30 + + # Sell value + sell_value = self.dragon.get_sell_value() + value_text = tiny_font.render(f"Sell Value: {sell_value} coins", True, GOLD) + surface.blit(value_text, (self.rect.x + 10, y_offset)) + + # Select button + select_color = GREEN if self.select_button_hovered else STONE_MED + pygame.draw.rect(surface, select_color, self.select_button) + pygame.draw.rect(surface, WOOD_DARK, self.select_button, 2) + select_text = tiny_font.render("Select", True, WHITE) + select_rect = select_text.get_rect(center=self.select_button.center) + surface.blit(select_text, select_rect) + + # Sell button + sell_color = RED if self.sell_button_hovered else STONE_MED + pygame.draw.rect(surface, sell_color, self.sell_button) + pygame.draw.rect(surface, WOOD_DARK, self.sell_button, 2) + sell_text = tiny_font.render("Sell", True, WHITE) + sell_rect = sell_text.get_rect(center=self.sell_button.center) + surface.blit(sell_text, sell_rect) + class DragonPen: - """Dragon Pen screen for viewing and managing all dragons.""" + """Dragon Pen screen for viewing and managing all dragons with grid layout.""" def __init__(self, game_state: GameState, sprite_manager: SpriteManager): """Initialize dragon pen. @@ -165,10 +433,53 @@ def __init__(self, game_state: GameState, sprite_manager: SpriteManager): self.bg_manager = BackgroundManager("assets/ui/dragon-pen.jpg") self.bg_manager.set_size(SCREEN_WIDTH, SCREEN_HEIGHT) + # Grid settings + self.grid_cols = 4 + self.card_width = 140 + self.card_height = 160 + self.card_spacing = 15 + self.grid_start_x = 20 + self.grid_start_y = 160 + self.grid_width = self.grid_cols * (self.card_width + self.card_spacing) - self.card_spacing + + # Scrolling + self.scroll_offset = 0 + self.max_scroll = 0 + + # Search and filter + self.search_bar = SearchBar(20, 90, 400, 35) + self.search_query = "" + self.filtered_dragons = [] + self.filtered_indices = [] # Original indices in game_state.dragons + + # Sorting + self.sort_methods = [ + ("Name", "name"), + ("Type", "type"), + ("Stage", "stage"), + ("Speed", "speed"), + ("Strength", "strength"), + ("Rarity", "rarity"), + ] + self.sort_dropdown = SortDropdown(440, 90, 150, 35, self.sort_methods) + self.active_sort = "name" + self.sort_reverse = False + + # Sort order toggle button + self.sort_order_button = pygame.Rect(600, 90, 120, 35) + self.sort_order_button_hovered = False + # Dragon cards self.cards = [] - self.scroll_offset = 0 - self.max_visible_cards = 4 + self.selected_card_index = -1 + + # Details panel (floating modal) + panel_width = 400 + panel_height = 450 + panel_x = (SCREEN_WIDTH - panel_width) // 2 + panel_y = (SCREEN_HEIGHT - panel_height) // 2 + self.details_panel = DragonDetailsPanel(panel_x, panel_y, panel_width, panel_height) + self.show_details = False # Back button self.back_button = pygame.Rect(20, SCREEN_HEIGHT - 70, 100, 50) @@ -180,38 +491,101 @@ def __init__(self, game_state: GameState, sprite_manager: SpriteManager): self.message_timer = 0 self.message_color = WHITE - # Scroll arrows - self.scroll_up_arrow = pygame.Rect(SCREEN_WIDTH - 60, 120, 40, 40) - self.scroll_down_arrow = pygame.Rect(SCREEN_WIDTH - 60, SCREEN_HEIGHT - 180, 40, 40) - self.scroll_up_hovered = False - self.scroll_down_hovered = False - + # Initialize + self._update_filtered_dragons() self._create_cards() + def _update_filtered_dragons(self): + """Update filtered and sorted dragon list.""" + # Filter by search query + query = self.search_query.lower() + + if query: + self.filtered_dragons = [] + self.filtered_indices = [] + for i, dragon in enumerate(self.game_state.dragons): + if query in dragon.name.lower(): + self.filtered_dragons.append(dragon) + self.filtered_indices.append(i) + else: + self.filtered_dragons = self.game_state.dragons.copy() + self.filtered_indices = list(range(len(self.game_state.dragons))) + + # Sort dragons + if self.active_sort == "name": + sort_key = lambda x: x[0].name.lower() + elif self.active_sort == "type": + sort_key = lambda x: x[0].dragon_type + elif self.active_sort == "stage": + stage_order = {"egg": 0, "hatchling": 1, "juvenile": 2, "adult": 3, "elder": 4} + sort_key = lambda x: stage_order.get(x[0].stage, 0) + elif self.active_sort == "speed": + sort_key = lambda x: x[0].get_speed_with_bonuses() + elif self.active_sort == "strength": + sort_key = lambda x: x[0].get_strength_with_bonuses() + elif self.active_sort == "rarity": + sort_key = lambda x: x[0].rarity + else: + sort_key = lambda x: x[0].name.lower() + + # Create paired list and sort + paired = list(zip(self.filtered_dragons, self.filtered_indices)) + paired.sort(key=sort_key, reverse=self.sort_reverse) + + # Unzip back + if paired: + dragons_tuple, indices_tuple = zip(*paired) + self.filtered_dragons = list(dragons_tuple) + self.filtered_indices = list(indices_tuple) + else: + self.filtered_dragons = [] + self.filtered_indices = [] + + # Update max scroll + total_rows = (len(self.filtered_dragons) + self.grid_cols - 1) // self.grid_cols + visible_rows = (SCREEN_HEIGHT - self.grid_start_y - 80) // (self.card_height + self.card_spacing) + self.max_scroll = max(0, total_rows - visible_rows) + def _create_cards(self): - """Create cards for all dragons.""" + """Create grid cards for visible dragons.""" self.cards = [] - if not self.game_state.dragons: + if not self.filtered_dragons: return - # Card dimensions - card_width = SCREEN_WIDTH - 140 - card_height = 100 - start_x = 60 - start_y = 120 - spacing = 10 + # Calculate visible range + cards_per_row = self.grid_cols + visible_height = SCREEN_HEIGHT - self.grid_start_y - 80 + visible_rows = visible_height // (self.card_height + self.card_spacing) + 1 + + start_row = self.scroll_offset + end_row = min(start_row + visible_rows, + (len(self.filtered_dragons) + cards_per_row - 1) // cards_per_row) # Create cards for visible dragons - visible_start = self.scroll_offset - visible_end = min(visible_start + self.max_visible_cards, len(self.game_state.dragons)) + for row in range(start_row, end_row): + for col in range(cards_per_row): + dragon_idx = row * cards_per_row + col + + # Stop if we've run out of dragons + if dragon_idx >= len(self.filtered_dragons): + break + + dragon = self.filtered_dragons[dragon_idx] + original_idx = self.filtered_indices[dragon_idx] - for i in range(visible_start, visible_end): - dragon = self.game_state.dragons[i] - card_index = i - visible_start - y = start_y + card_index * (card_height + spacing) - card = DragonCard(start_x, y, card_width, card_height, dragon, i) - self.cards.append(card) + x = self.grid_start_x + col * (self.card_width + self.card_spacing) + y = self.grid_start_y + (row - start_row) * (self.card_height + self.card_spacing) + + card = DragonGridCard(x, y, self.card_width, self.card_height, dragon, original_idx) + + # Mark as selected if this is the selected dragon + if original_idx == self.game_state.selected_dragon_index: + card.is_selected = True + if self.selected_card_index == -1: + self.selected_card_index = len(self.cards) + + self.cards.append(card) def handle_event(self, event: pygame.event.Event) -> Optional[str]: """Handle input events. @@ -219,41 +593,89 @@ def handle_event(self, event: pygame.event.Event) -> Optional[str]: Returns: 'idle' to return to main screen, None otherwise """ + # Handle search bar + if self.search_bar.handle_event(event): + self.search_query = self.search_bar.text + self.scroll_offset = 0 + self._update_filtered_dragons() + self._create_cards() + return None + + # Handle sort dropdown + if self.sort_dropdown.handle_event(event): + # Sort selection changed + new_sort = self.sort_dropdown.get_selected_sort_key() + if new_sort != self.active_sort: + # Different sort method - reset reverse + self.active_sort = new_sort + self.sort_reverse = False + self._update_filtered_dragons() + self._create_cards() + return None + + # Handle sort order toggle button + if event.type == pygame.MOUSEMOTION: + self.sort_order_button_hovered = self.sort_order_button.collidepoint(event.pos) + elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + if self.sort_order_button.collidepoint(event.pos): + self.sort_reverse = not self.sort_reverse + self._update_filtered_dragons() + self._create_cards() + return None + # Handle back button if event.type == pygame.MOUSEMOTION: self.back_button_hovered = self.back_button.collidepoint(event.pos) - self.scroll_up_hovered = self.scroll_up_arrow.collidepoint(event.pos) - self.scroll_down_hovered = self.scroll_down_arrow.collidepoint(event.pos) elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: if self.back_button.collidepoint(event.pos): return "idle" - # Handle scroll arrows - if self.scroll_up_arrow.collidepoint(event.pos) and self.scroll_offset > 0: - self.scroll_offset -= 1 + # Handle scrolling with mouse wheel + if event.type == pygame.MOUSEWHEEL: + if event.y > 0: # Scroll up + self.scroll_offset = max(0, self.scroll_offset - 1) + self._create_cards() + elif event.y < 0: # Scroll down + self.scroll_offset = min(self.max_scroll, self.scroll_offset + 1) self._create_cards() - return None - if self.scroll_down_arrow.collidepoint(event.pos): - if self.scroll_offset + self.max_visible_cards < len(self.game_state.dragons): - self.scroll_offset += 1 - self._create_cards() - return None + # Handle card clicks + if event.type == pygame.MOUSEMOTION: + for i, card in enumerate(self.cards): + card.is_hovered = card.rect.collidepoint(event.pos) - # Handle card events - for card in self.cards: - result = card.handle_event(event) - if result == 'sell': - self._sell_dragon(card.dragon_index) + elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + # Handle details panel first if visible + if self.show_details: + result = self.details_panel.handle_event(event) + if result == 'close': + self.show_details = False + return None + elif result == 'sell': + self._sell_dragon(self.details_panel.dragon_index) + self.show_details = False + elif result == 'select': + self.game_state.selected_dragon_index = self.details_panel.dragon_index + self._create_cards() + self.show_details = False + return "idle" return None + # Handle card clicks + for i, card in enumerate(self.cards): + if card.rect.collidepoint(event.pos): + self.selected_card_index = i + self.details_panel.set_dragon(card.dragon, card.dragon_index) + self.show_details = True + return None + return None def _sell_dragon(self, dragon_index: int): """Sell a dragon for coins. Args: - dragon_index: Index of dragon to sell + dragon_index: Index of dragon to sell in game_state.dragons """ if dragon_index >= len(self.game_state.dragons): return @@ -277,14 +699,21 @@ def _sell_dragon(self, dragon_index: int): # Adjust selected dragon index if needed if self.game_state.selected_dragon_index >= len(self.game_state.dragons): self.game_state.selected_dragon_index = len(self.game_state.dragons) - 1 + elif self.game_state.selected_dragon_index > dragon_index: + self.game_state.selected_dragon_index -= 1 - # Adjust scroll offset if needed - if self.scroll_offset >= len(self.game_state.dragons): - self.scroll_offset = max(0, len(self.game_state.dragons) - self.max_visible_cards) + # Clear details panel + self.details_panel.set_dragon(None, -1) + self.selected_card_index = -1 + self.show_details = False - # Recreate cards + # Update filtered list and recreate cards + self._update_filtered_dragons() self._create_cards() + # Save game state to persist the changes + self.game_state.save() + # Show message self._show_message(f"Sold {dragon_name} for {sell_value} coins!", GREEN) @@ -301,16 +730,20 @@ def update(self, dt: float): Args: dt: Delta time in seconds """ + # Update search bar + self.search_bar.update(dt) + # Update message timer if self.show_message: self.message_timer -= dt if self.message_timer <= 0: self.show_message = False - # Recreate cards if dragon count changed - expected_cards = min(self.max_visible_cards, - len(self.game_state.dragons) - self.scroll_offset) - if len(self.cards) != expected_cards: + # Recreate cards if dragon count changed significantly + if len(self.filtered_dragons) != len([d for d in self.game_state.dragons + if not self.search_query or + self.search_query.lower() in d.name.lower()]): + self._update_filtered_dragons() self._create_cards() def draw(self, surface: pygame.Surface): @@ -327,38 +760,81 @@ def draw(self, surface: pygame.Surface): surface.blit(title, (20, 10)) # Draw dragon count - count_text = self.small_font.render(f"Dragons: {len(self.game_state.dragons)}", - True, PARCHMENT) + total_count = len(self.game_state.dragons) + filtered_count = len(self.filtered_dragons) + if filtered_count != total_count: + count_text = self.small_font.render(f"Dragons: {filtered_count}/{total_count}", + True, PARCHMENT) + else: + count_text = self.small_font.render(f"Dragons: {total_count}", True, PARCHMENT) surface.blit(count_text, (SCREEN_WIDTH - 200, 15)) + # Draw search bar + self.search_bar.draw(surface, self.small_font) + + # Draw sort label + sort_label = self.tiny_font.render("Sort by:", True, PARCHMENT) + surface.blit(sort_label, (440, 65)) + + # Draw sort button (dropdown menu will be drawn later on top) + button_color = WOOD_MED if self.sort_dropdown.is_hovered else STONE_MED + pygame.draw.rect(surface, button_color, self.sort_dropdown.button_rect) + pygame.draw.rect(surface, WOOD_DARK, self.sort_dropdown.button_rect, 2) + + # Button text + label = self.sort_dropdown.get_selected_label() + button_text = self.tiny_font.render(label, True, PARCHMENT) + text_rect = button_text.get_rect(center=self.sort_dropdown.button_rect.center) + surface.blit(button_text, text_rect) + + # Draw sort order toggle button + order_button_color = WOOD_MED if self.sort_order_button_hovered else STONE_MED + pygame.draw.rect(surface, order_button_color, self.sort_order_button) + pygame.draw.rect(surface, WOOD_DARK, self.sort_order_button, 2) + + order_text = "Descending" if self.sort_reverse else "Ascending" + order_color = ORANGE if self.sort_reverse else BLUE + order_button_text = self.tiny_font.render(order_text, True, order_color) + order_text_rect = order_button_text.get_rect(center=self.sort_order_button.center) + surface.blit(order_button_text, order_text_rect) + + # Draw separator line + pygame.draw.line(surface, WOOD_DARK, (20, 140), (SCREEN_WIDTH - 20, 140), 2) + # Draw cards or no dragons message if self.cards: for card in self.cards: card.draw(surface, self.sprite_manager, self.small_font, self.tiny_font) else: - no_dragons = self.font.render("No dragons in the pen!", True, STONE_DARK) + if self.search_query: + no_dragons = self.small_font.render("No dragons match your search!", True, STONE_DARK) + else: + no_dragons = self.font.render("No dragons in the pen!", True, STONE_DARK) surface.blit(no_dragons, (SCREEN_WIDTH // 2 - no_dragons.get_width() // 2, SCREEN_HEIGHT // 2)) - # Draw scroll arrows if needed - total_dragons = len(self.game_state.dragons) - - if self.scroll_offset > 0: - arrow_color = GOLD if self.scroll_up_hovered else STONE_DARK - pygame.draw.rect(surface, arrow_color, self.scroll_up_arrow) - pygame.draw.rect(surface, WOOD_DARK, self.scroll_up_arrow, 3) - up_text = self.font.render("^", True, WHITE) - up_rect = up_text.get_rect(center=self.scroll_up_arrow.center) - surface.blit(up_text, up_rect) - - if self.scroll_offset + self.max_visible_cards < total_dragons: - arrow_color = GOLD if self.scroll_down_hovered else STONE_DARK - pygame.draw.rect(surface, arrow_color, self.scroll_down_arrow) - pygame.draw.rect(surface, WOOD_DARK, self.scroll_down_arrow, 3) - down_text = self.font.render("v", True, WHITE) - down_rect = down_text.get_rect(center=self.scroll_down_arrow.center) - surface.blit(down_text, down_rect) + # Draw scroll indicator + if self.max_scroll > 0: + scroll_text = self.tiny_font.render(f"Row {self.scroll_offset + 1}/{self.max_scroll + 1}", + True, STONE_DARK) + surface.blit(scroll_text, (self.grid_start_x, SCREEN_HEIGHT - 40)) + + # Scroll bar + scrollbar_x = self.grid_start_x + self.grid_width + 10 + scrollbar_y = self.grid_start_y + scrollbar_height = SCREEN_HEIGHT - self.grid_start_y - 80 + scrollbar_width = 10 + + # Background + pygame.draw.rect(surface, STONE_DARK, + (scrollbar_x, scrollbar_y, scrollbar_width, scrollbar_height)) + + # Thumb + thumb_height = max(20, scrollbar_height // (self.max_scroll + 1)) + thumb_y = scrollbar_y + (scrollbar_height - thumb_height) * (self.scroll_offset / self.max_scroll) if self.max_scroll > 0 else scrollbar_y + pygame.draw.rect(surface, GOLD, + (scrollbar_x, thumb_y, scrollbar_width, thumb_height)) # Draw back button back_color = WOOD_MED if self.back_button_hovered else STONE_DARK @@ -369,6 +845,43 @@ def draw(self, surface: pygame.Surface): back_rect = back_text.get_rect(center=self.back_button.center) surface.blit(back_text, back_rect) + # Draw details panel (floating modal) if visible + if self.show_details: + self.details_panel.draw(surface, self.small_font, self.tiny_font) + + # Draw dropdown menu on top of everything if open + if self.sort_dropdown.is_open: + # Dropdown background + pygame.draw.rect(surface, PARCHMENT, self.sort_dropdown.dropdown_rect) + pygame.draw.rect(surface, WOOD_DARK, self.sort_dropdown.dropdown_rect, 3) + + # Draw each option + for i, (label, _) in enumerate(self.sort_dropdown.options): + option_rect = pygame.Rect( + self.sort_dropdown.dropdown_rect.x, + self.sort_dropdown.dropdown_rect.y + i * 35, + self.sort_dropdown.dropdown_rect.width, + 35 + ) + + # Highlight selected or hovered option + if i == self.sort_dropdown.selected_index: + pygame.draw.rect(surface, GOLD, option_rect) + elif i == self.sort_dropdown.hovered_option_index: + pygame.draw.rect(surface, WOOD_MED, option_rect) + + # Draw option text + text_color = BLACK if i == self.sort_dropdown.selected_index else WOOD_DARK + option_text = self.tiny_font.render(label, True, text_color) + text_rect = option_text.get_rect(center=option_rect.center) + surface.blit(option_text, text_rect) + + # Draw separator line + if i < len(self.sort_dropdown.options) - 1: + pygame.draw.line(surface, STONE_DARK, + (option_rect.left, option_rect.bottom), + (option_rect.right, option_rect.bottom), 1) + # Draw message if self.show_message: self._draw_message(surface) diff --git a/src/equipment_screen.py b/src/equipment_screen.py index edb45d9..c49271a 100644 --- a/src/equipment_screen.py +++ b/src/equipment_screen.py @@ -283,6 +283,7 @@ def handle_event(self, event: pygame.event.Event) -> Optional[str]: if button.item_id: # Unequip enchantment if self.game_state.unequip_enchantment_from_dragon(self.selected_dragon_index, button.slot_index): + self.game_state.save() # Save after unequipping self._show_message(f"Unequipped enchantment from slot {button.slot_index + 1}", GREEN) self._create_slot_buttons() self._update_inventory_buttons() @@ -297,6 +298,7 @@ def handle_event(self, event: pygame.event.Event) -> Optional[str]: if dragon.can_equip_enchantment(): # Equip the enchantment if self.game_state.equip_enchantment_on_dragon(self.selected_dragon_index, button.item.item_id): + self.game_state.save() # Save after equipping self._show_message(f"Equipped {button.item.name}!", GREEN) self._create_slot_buttons() self._update_inventory_buttons() diff --git a/src/game_state.py b/src/game_state.py index 7bd0627..fbde7bf 100644 --- a/src/game_state.py +++ b/src/game_state.py @@ -137,6 +137,7 @@ def hatch_egg(self, egg_index: int, name: str = "Dragon") -> Optional[Dragon]: self.eggs.pop(egg_index) dragon = Dragon(dragon_type, name) self.dragons.append(dragon) + self.save() # Save after hatching return dragon return None @@ -282,6 +283,7 @@ def feed_dragon(self, dragon: Dragon) -> bool: if dragon.feed(): self.meat -= FEED_COST + self.save() # Save after feeding return True return False @@ -362,6 +364,9 @@ def from_dict(self, data: Dict[str, Any]): dragon._db_id = dragon_data['_db_id'] self.dragons.append(dragon) + # Remove duplicate dragons (fix for migration/load bugs) + self._remove_duplicate_dragons() + # Handle egg format (convert old string format to new dict format) eggs_data = data.get("eggs", []) self.eggs = [] @@ -532,10 +537,59 @@ def save(self) -> bool: True if save succeeded, False otherwise """ if self.use_sqlite: - return self.save_system_sqlite.save_all(self.to_dict()) + result = self.save_system_sqlite.save_all(self.to_dict()) + if result['success']: + # Update Dragon objects with their database IDs + dragon_ids = result.get('dragon_ids', []) + for i, dragon_id in enumerate(dragon_ids): + if i < len(self.dragons) and dragon_id is not None: + self.dragons[i]._db_id = dragon_id + return result['success'] else: return self.save_system_json.save(self.to_dict()) + def _remove_duplicate_dragons(self): + """Remove duplicate dragons from the dragons list. + + Dragons are considered duplicates if they have the same: + - dragon_type, name, speed, strength, rarity, and birth_time (within 1 second) + + This fixes issues from save/load bugs or corrupted saves. + """ + if not self.dragons: + return + + seen = {} + unique_dragons = [] + duplicates_found = 0 + + for dragon in self.dragons: + # Create signature for this dragon + signature = ( + dragon.dragon_type, + dragon.name, + dragon.speed, + dragon.strength, + dragon.rarity, + round(dragon.birth_time) # Round to nearest second + ) + + if signature not in seen: + # First time seeing this dragon + seen[signature] = True + unique_dragons.append(dragon) + else: + # This is a duplicate + duplicates_found += 1 + + if duplicates_found > 0: + print(f"WARNING: Removed {duplicates_found} duplicate dragon(s) from save data") + self.dragons = unique_dragons + + # Adjust selected index if needed + if self.selected_dragon_index >= len(self.dragons): + self.selected_dragon_index = max(0, len(self.dragons) - 1) + def load(self) -> bool: """Load game state from file. @@ -549,6 +603,13 @@ def load(self) -> bool: print("Loading from SQLite database...") data = self.save_system_sqlite.load_all() self.use_sqlite = True + if data: + self.from_dict(data) + # Add starter items if inventory is empty (for existing saves) + if not self.inventory: + self._add_starter_items() + return True + return False # Fall back to JSON for migration elif self.save_system_json.exists(): print("Migrating from JSON save to SQLite database...") @@ -557,19 +618,20 @@ def load(self) -> bool: # Migrate to SQLite self.use_sqlite = True self.from_dict(data) - success = self.save_system_sqlite.save_all(self.to_dict()) + result = self.save_system_sqlite.save_all(self.to_dict()) + success = result['success'] if success: + # Update Dragon objects with their database IDs + dragon_ids = result.get('dragon_ids', []) + for i, dragon_id in enumerate(dragon_ids): + if i < len(self.dragons) and dragon_id is not None: + self.dragons[i]._db_id = dragon_id print("Migration successful! Future saves will use SQLite.") # Optionally keep JSON as backup or delete it # self.save_system_json.delete() return success + return False - if data: - self.from_dict(data) - # Add starter items if inventory is empty (for existing saves) - if not self.inventory: - self._add_starter_items() - return True return False def has_save(self) -> bool: diff --git a/src/idle_game.py b/src/idle_game.py index cec7d85..29c9d9d 100644 --- a/src/idle_game.py +++ b/src/idle_game.py @@ -182,23 +182,12 @@ def __init__(self, game_state: GameState, sprite_manager: SpriteManager, music_m self.bg_manager = BackgroundManager("assets/ui/bg.png") self.bg_manager.set_size(SCREEN_WIDTH, SCREEN_HEIGHT) - # Dragon tabs with pagination - self.tabs = [] - self.tab_scroll_offset = 0 # Which dragon index to start showing from - self.max_visible_tabs = 3 # Maximum tabs to show at once - - # Tab navigation arrows - self.tab_left_arrow = Button(20, 60, 40, 40, "<", STONE_DARK) - self.tab_right_arrow = Button(20, 60, 40, 40, ">", STONE_DARK) # X position will be updated - # Rename dragon system self.rename_button = None # Will be created when needed self.is_renaming = False self.rename_text = "" self.rename_max_length = 20 - self._create_tabs() - # Interaction buttons (bottom center) button_y = SCREEN_HEIGHT - 90 button_x_start = SCREEN_WIDTH // 2 - 130 @@ -240,54 +229,7 @@ def __init__(self, game_state: GameState, sprite_manager: SpriteManager, music_m self.message_timer = 0 self.message_color = WHITE - def _create_tabs(self): - """Create tabs for visible dragons with pagination.""" - self.tabs = [] - if not self.game_state.dragons: - return - - total_dragons = len(self.game_state.dragons) - # Ensure scroll offset is valid - if self.tab_scroll_offset >= total_dragons: - self.tab_scroll_offset = max(0, total_dragons - self.max_visible_tabs) - - # Calculate which dragons to show - visible_start = self.tab_scroll_offset - visible_end = min(visible_start + self.max_visible_tabs, total_dragons) - visible_count = visible_end - visible_start - - # Tab dimensions - tab_width = 150 - tab_height = 40 - y = 60 - - # Calculate positions with arrows - arrow_width = 40 - arrow_spacing = 5 - - # Left arrow position (only show if we can scroll left) - if self.tab_scroll_offset > 0: - self.tab_left_arrow.rect.x = 20 - self.tab_left_arrow.rect.y = y - start_x = 20 + arrow_width + arrow_spacing - else: - start_x = 20 - - # Create visible tabs - for i in range(visible_start, visible_end): - dragon = self.game_state.dragons[i] - tab_index_in_row = i - visible_start - x = start_x + tab_index_in_row * (tab_width + 5) - tab = Tab(x, y, tab_width, tab_height, dragon.name[:12], i) - tab.is_selected = (i == self.game_state.selected_dragon_index) - self.tabs.append(tab) - - # Right arrow position (only show if we can scroll right) - if visible_end < total_dragons: - last_tab_x = start_x + (visible_count - 1) * (tab_width + 5) - self.tab_right_arrow.rect.x = last_tab_x + tab_width + arrow_spacing - self.tab_right_arrow.rect.y = y def handle_event(self, event: pygame.event.Event) -> Optional[str]: """Handle input events.""" @@ -299,8 +241,8 @@ def handle_event(self, event: pygame.event.Event) -> Optional[str]: dragon = self.game_state.get_selected_dragon() if dragon and self.rename_text.strip(): dragon.name = self.rename_text.strip() + self.game_state.save() # Save after renaming self._show_message(f"Renamed to {dragon.name}!", GREEN) - self._create_tabs() # Refresh tabs with new name self.is_renaming = False self.rename_text = "" return None @@ -329,43 +271,9 @@ def handle_event(self, event: pygame.event.Event) -> Optional[str]: self.mute_button.text = "Unmute" if self.music_manager.is_muted else "Mute" # Save mute state to game state self.game_state.music_muted = self.music_manager.is_muted + self.game_state.sfx_muted = not self.game_state.sfx_muted return None - # Handle rename button click - if self.rename_button and self.rename_button.handle_event(event): - dragon = self.game_state.get_selected_dragon() - if dragon: - self.is_renaming = True - self.rename_text = dragon.name - return None - - # Handle tab arrow clicks - total_dragons = len(self.game_state.dragons) - if self.tab_scroll_offset > 0: - if self.tab_left_arrow.handle_event(event): - self.tab_scroll_offset = max(0, self.tab_scroll_offset - 1) - self._create_tabs() - return None - - if self.tab_scroll_offset + self.max_visible_tabs < total_dragons: - if self.tab_right_arrow.handle_event(event): - self.tab_scroll_offset = min(total_dragons - self.max_visible_tabs, - self.tab_scroll_offset + 1) - self._create_tabs() - return None - - # Handle tab clicks - for tab in self.tabs: - if tab.handle_event(event): - self.game_state.select_dragon(tab.index) - # Ensure selected dragon is visible - if tab.index < self.tab_scroll_offset: - self.tab_scroll_offset = tab.index - elif tab.index >= self.tab_scroll_offset + self.max_visible_tabs: - self.tab_scroll_offset = tab.index - self.max_visible_tabs + 1 - self._create_tabs() # Refresh tabs - return None - # Handle interaction button clicks for button in self.interaction_buttons: if button.handle_event(event): @@ -374,7 +282,7 @@ def handle_event(self, event: pygame.event.Event) -> Optional[str]: elif button == self.pet_button: self._handle_pet() - # Handle navigation button clicks + # Handle navigation buttons for button in self.navigation_buttons: if button.handle_event(event): if button == self.eggs_button: @@ -438,6 +346,7 @@ def _handle_pet(self): return if dragon.pet(): + self.game_state.save() # Save after petting self._show_message(f"Pet {dragon.name}!", BLUE) else: self._show_message("Could not pet dragon!", RED) @@ -455,17 +364,6 @@ def _show_message(self, text: str, color: tuple = WHITE): def update(self, dt: float): """Update game state.""" - # Recreate tabs if dragon count changed - total_dragons = len(self.game_state.dragons) - - # Check if we need to recreate tabs (dragon count changed) - visible_start = self.tab_scroll_offset - visible_end = min(visible_start + self.max_visible_tabs, total_dragons) - expected_tab_count = visible_end - visible_start - - if len(self.tabs) != expected_tab_count: - self._create_tabs() - # Update dragons self.game_state.update_dragons() @@ -525,22 +423,11 @@ def draw(self, surface: pygame.Surface): # Draw resources self._draw_resources(surface) - # Draw tab navigation arrows - total_dragons = len(self.game_state.dragons) - if self.tab_scroll_offset > 0: - self.tab_left_arrow.draw(surface, self.small_font) - - # Draw tabs - for tab in self.tabs: - tab.draw(surface, self.small_font) - - if self.tab_scroll_offset + self.max_visible_tabs < total_dragons: - self.tab_right_arrow.draw(surface, self.small_font) - # Draw navigation panel (right side) self._draw_navigation_panel(surface) - # Draw main content area (parchment) + # Draw main content area (parchment) - larger now without tabs + # Leave space at bottom for interaction buttons (90px buttons + 20px margin = 110px) content_rect = pygame.Rect(20, 110, SCREEN_WIDTH - 200, SCREEN_HEIGHT - 220) pygame.draw.rect(surface, PARCHMENT, content_rect) # Medieval border @@ -571,10 +458,11 @@ def draw(self, surface: pygame.Surface): for button in self.interaction_buttons: button.draw_cooldown_bar(surface) - # Draw message + # Draw messages if self.show_message: self._draw_message(surface) + def _draw_navigation_panel(self, surface: pygame.Surface): """Draw navigation menu panel on the right side.""" panel_rect = pygame.Rect(SCREEN_WIDTH - 180, 110, 160, 400) diff --git a/src/save_system_sqlite.py b/src/save_system_sqlite.py index 72ef5e3..6f5a401 100644 --- a/src/save_system_sqlite.py +++ b/src/save_system_sqlite.py @@ -496,14 +496,14 @@ def load_inventory(self) -> Dict[str, int]: return inventory - def save_all(self, game_data: Dict[str, Any]) -> bool: + def save_all(self, game_data: Dict[str, Any]) -> Dict[str, Any]: """Save complete game state to database. Args: game_data: Complete game data dictionary (from GameState.to_dict()) Returns: - True if save succeeded, False otherwise + Dictionary with 'success' (bool) and 'dragon_ids' (list of db IDs in order) """ try: conn = self._get_connection() @@ -514,10 +514,28 @@ def save_all(self, game_data: Dict[str, Any]) -> bool: try: # Save dragons dragons = game_data.get("dragons", []) - for dragon_data in dragons: + + # Collect IDs of dragons that should exist + dragon_ids_to_keep = set() + dragon_ids_in_order = [] + for i, dragon_data in enumerate(dragons): # Check if dragon already has a database ID dragon_id = dragon_data.get("_db_id") - self.save_dragon(dragon_data, dragon_id) + saved_id = self.save_dragon(dragon_data, dragon_id) + if saved_id: + dragon_ids_to_keep.add(saved_id) + dragon_ids_in_order.append(saved_id) + else: + dragon_ids_in_order.append(None) + + # Delete dragons from database that are no longer in the game state + cursor = conn.cursor() + cursor.execute("SELECT id FROM dragons") + all_db_dragon_ids = [row[0] for row in cursor.fetchall()] + + for db_dragon_id in all_db_dragon_ids: + if db_dragon_id not in dragon_ids_to_keep: + cursor.execute("DELETE FROM dragons WHERE id = ?", (db_dragon_id,)) # Save eggs eggs = game_data.get("eggs", []) @@ -544,17 +562,17 @@ def save_all(self, game_data: Dict[str, Any]) -> bool: # Commit transaction conn.commit() - return True + return {'success': True, 'dragon_ids': dragon_ids_in_order} except Exception as e: # Rollback on error conn.rollback() print(f"Error in save transaction: {e}") - return False + return {'success': False, 'dragon_ids': []} except Exception as e: print(f"Error saving game: {e}") - return False + return {'success': False, 'dragon_ids': []} def load_all(self) -> Optional[Dict[str, Any]]: """Load complete game state from database.