diff --git a/docs/GENE_DRAGON_TYPES.md b/docs/GENE_DRAGON_TYPES.md new file mode 100644 index 0000000..5a57af7 --- /dev/null +++ b/docs/GENE_DRAGON_TYPES.md @@ -0,0 +1,101 @@ +# Gene Dragon Types + +## Overview + +Gene-based dragons are procedurally generated with unique sprites, but they are also assigned traditional dragon types (fire, ice, forest, storm, shadow) based on their color genetics. This ensures they have appropriate abilities in arena combat and proper classification. + +## How Types Are Determined + +When a gene-based dragon is created, its type is automatically determined by analyzing its RGB color values: + +### Type Assignment Rules + +1. **Fire Dragons** (Red Dominant) + - Red channel is highest + - Red > Green AND Red > Blue + - Example colors: (255, 100, 50), (200, 80, 80) + +2. **Ice Dragons** (Blue Dominant) + - Blue channel is highest + - Blue > Red AND Blue > Green + - Example colors: (50, 100, 255), (80, 120, 220) + +3. **Forest Dragons** (Green Dominant) + - Green channel is highest + - Green > Red AND Green > Blue + - Example colors: (50, 255, 100), (80, 200, 90) + +4. **Storm Dragons** (Purple/Balanced) + - Red and Blue channels are similar + - Difference between Red and Blue < 40 + - Both Red and Blue > 100 + - Example colors: (150, 80, 140), (180, 90, 170) + +5. **Shadow Dragons** (Low Brightness) + - Maximum RGB value < 100 + - Overall dark coloration + - Example colors: (60, 50, 55), (80, 70, 75) + +6. **Default: Fire** + - If no clear dominant color matches + - Ensures all dragons have a valid type + +## Implementation + +The type determination happens in `Dragon._determine_type_from_color()` when creating gene-based dragons: + +```python +# In dragon.py +dragon_type = cls._determine_type_from_color(genotype.color) +``` + +## Benefits + +### Combat Balance +- Gene dragons use moves and abilities appropriate to their type +- Fire dragons have fire attacks +- Ice dragons have ice attacks +- etc. + +### Clear Classification +- Players can identify dragon types at a glance +- "Fire Elder" instead of "Gene-based elder" +- Consistent with legacy dragon system + +### Visual Consistency +- Dragon's visual appearance (color) matches its type +- Red dragons breathe fire +- Blue dragons use ice abilities +- Green dragons have nature powers + +## Display + +Gene dragons are displayed with their assigned type: +- **Without title**: "Fire Elder", "Ice Adult", etc. +- **With title**: Title is shown instead (e.g., "the Eternal") +- **Legendary genes**: Can still show ★ legendary status + +## Arena Behavior + +In arena minigames, gene dragons now: +- Use abilities matching their assigned type +- Have type-appropriate stats and moves +- Balance properly against opponents + +## Examples + +| Color RGB | Assigned Type | Reason | +|-----------------|---------------|---------------------------| +| (255, 50, 50) | Fire | Red dominant | +| (50, 50, 255) | Ice | Blue dominant | +| (50, 255, 50) | Forest | Green dominant | +| (180, 80, 170) | Storm | Purple balanced | +| (60, 60, 60) | Shadow | Low brightness | +| (150, 150, 150) | Fire | No clear dominant (default)| + +## Technical Notes + +- Type is assigned once when dragon is created +- Type is saved/loaded with dragon data +- Cannot be changed after creation (tied to genetics) +- Deterministic: Same genotype seed = same color = same type \ No newline at end of file diff --git a/docs/OFFLINE_PROGRESS.md b/docs/OFFLINE_PROGRESS.md new file mode 100644 index 0000000..c03682c --- /dev/null +++ b/docs/OFFLINE_PROGRESS.md @@ -0,0 +1,124 @@ +# Offline Progress System + +## Overview + +The Dragon Riders game implements an offline progress system that rewards players for time spent away from the game, with specific rules for different game mechanics. + +## How It Works + +### Passive Coin Income +- **Continues while offline**: Yes, with a 4-hour cap +- **Rate**: 1 coin every 10 seconds (same as active play) +- **Maximum offline time**: 4 hours (14,400 seconds) +- **Maximum offline coins**: 1,440 coins (4 hours worth) + +If you're away for longer than 4 hours, you'll still only receive 4 hours worth of coins. + +### Dragon Growth +- **Continues while offline**: No +- Dragons will NOT grow or progress to the next stage while the game is closed +- Growth only happens during active play sessions +- This prevents dragons from growing too quickly without player interaction + +### Other Systems +- **Feed/Pet cooldowns**: Continue counting down while offline +- **Inventory items**: Preserved as-is +- **Equipment**: No changes while offline +- **Eggs**: Do not progress while offline (require clicks) + +## Offline Progress Summary + +When you return to the game after being away for more than 1 minute, you'll see a popup displaying: +- Total time you were away +- Coins earned during offline time +- Notification if the 4-hour cap was applied +- Reminder that dragons don't grow offline + +Press ENTER, SPACE, or click "Continue" to dismiss the popup. + +## Technical Details + +### How Time Is Tracked + +1. **On Game Close/Save**: + - `last_close_time` is recorded + - All dragon `last_update` times are saved + +2. **On Game Load**: + - System calculates `time_away = current_time - last_close_time` + - Caps offline time to 4 hours maximum + - Awards coins based on capped time + - Resets all dragon `last_update` times to current time (prevents offline growth) + - Creates offline progress popup if away > 1 minute + +### Implementation Files + +- **src/game_state.py**: Core offline progress logic + - `calculate_offline_progress()`: Calculates what was earned + - `load()`: Applies offline progress on game load + - `save()`: Records close time + +- **src/offline_progress_popup.py**: Popup UI component + - Displays offline progress summary + - Blocks other input until dismissed + - Medieval-styled popup design + +- **src/idle_game.py**: Integration + - Shows popup on game start if applicable + - Blocks input while popup is visible + +- **src/save_system_sqlite.py**: Persistence + - Saves/loads `last_close_time` field + +## Configuration + +To modify offline progress behavior, edit these constants in `src/utils/constants.py`: + +```python +PASSIVE_COIN_RATE = 10.0 # Seconds per coin +``` + +And this constant in `src/game_state.py`: + +```python +MAX_OFFLINE_TIME = 4 * 60 * 60 # 4 hours in seconds (in calculate_offline_progress()) +``` + +## Design Philosophy + +The offline progress system is designed to: + +1. **Reward returning players** without making active play feel wasteful +2. **Cap passive benefits** to encourage regular play sessions +3. **Prevent dragon auto-growth** to maintain player engagement with growth mechanics +4. **Maintain balance** by limiting maximum offline gains + +This creates a balanced idle game experience where: +- You're rewarded for returning after time away +- Active play is still more beneficial than passive waiting +- Dragon care remains an active, engaging mechanic +- The game respects your time while encouraging regular check-ins + +## Examples + +### Short Break (30 minutes) +``` +Time away: 30m +Coins earned: 180 coins +Dragons: No growth +``` + +### Long Session (8 hours) +``` +Time away: 8h +Coins earned: 1,440 coins (capped at 4 hours) +Dragons: No growth +Warning: "Capped at 4 hours" +``` + +### Quick Check (45 seconds) +``` +No popup shown (< 1 minute away) +Minimal coins earned: ~4 coins +Dragons: No growth +``` diff --git a/docs/SAVE_SYSTEM.md b/docs/SAVE_SYSTEM.md index 167493e..5954d9e 100644 --- a/docs/SAVE_SYSTEM.md +++ b/docs/SAVE_SYSTEM.md @@ -8,6 +8,41 @@ Dragon Riders uses a secure SQLite-based save system to store game data. This sy - **Prevent cheating** - Encrypt sensitive data and verify integrity with HMAC hashes - **Provide safety** - Automatic migration from legacy JSON saves with backup support - **Optimize storage** - Compress data and use efficient indexing +- **Save intelligently** - Only save when changes are made, with auto-save every 5 minutes + +## Save Behavior + +### When Does the Game Save? + +The game uses a "dirty flag" system to track unsaved changes and only saves when necessary: + +1. **Auto-save** - Every 5 minutes if there are unsaved changes +2. **On game close** - Always saves when you quit the game +3. **Manual saves removed** - No longer saves on every view change or action + +### What Triggers Unsaved Changes? + +The following actions mark the game state as "dirty" (needing save): + +- **Shop**: Purchasing items +- **Arena**: Completing minigames +- **Enchanting**: Equipping or unequipping enchantments +- **Dragon Pen**: Selling dragons +- **Hatchery**: Hatching eggs +- **Main Screen**: Feeding, petting, or renaming dragons + +### Visual Indicators + +- **"● Unsaved"** indicator appears in the title bar (orange) when there are unsaved changes +- **"Auto-saved!"** message appears (blue) when the game auto-saves +- Both indicators disappear once changes are saved + +### Benefits of This System + +- **Better performance** - Reduces unnecessary disk writes +- **Smoother gameplay** - No stuttering when switching views +- **Data safety** - Still saves regularly (every 5 minutes) and on close +- **Clear feedback** - Visual indicator shows when changes need saving ## Architecture diff --git a/run_game.py b/run_game.py index 88d00fc..c317e1f 100644 --- a/run_game.py +++ b/run_game.py @@ -112,9 +112,6 @@ def __init__(self): # Initialize scenes self.idle_game = IdleGame(self.game_state, self.sprite_manager, self.music_manager) - # Update mute button text if music is muted - if self.music_manager and self.music_manager.is_muted: - self.idle_game.mute_button.text = "Unmute" self.egg_screen = EggScreen(self.game_state, self.sprite_manager, self.sound_manager) self.dragon_pen = DragonPen(self.game_state, self.sprite_manager) self.equipment_screen = EquipmentScreen(self.game_state, self.sprite_manager) @@ -154,6 +151,10 @@ def run(self): # Update current scene self._update(dt) + # Auto-save check (works for all scenes) + if self.game_state.auto_save_if_needed(): + print("✓ Auto-saved game") + # Draw current scene self._draw() @@ -289,12 +290,7 @@ def _switch_scene(self, scene: str): self.arena_screen = ArenaScreen(self.game_state) # Refresh arena self.current_scene = "arena" elif scene == "idle": - # Save game when returning to idle - print("Saving game on scene return...") - if self.game_state.save(): - print("✓ Game saved") - else: - print("✗ Save failed") + # Return to idle without saving (auto-save handles it) self.current_scene = "idle" diff --git a/src/dragon.py b/src/dragon.py index f156290..fa22b5b 100644 --- a/src/dragon.py +++ b/src/dragon.py @@ -33,6 +33,13 @@ def __init__(self, dragon_type: str, name: str = "Dragon", speed: int = None, genotype: DragonGenotype for gene-based dragons (NEW SYSTEM) genotype_seed: Seed string to generate genotype (NEW SYSTEM) """ + # Migrate old 'gene' type to proper type based on color + if dragon_type == 'gene' and genotype is not None: + dragon_type = self._determine_type_from_color(genotype.color) + elif dragon_type == 'gene': + # Fallback for gene dragons without genotype + dragon_type = 'fire' + if dragon_type not in ALL_DRAGON_TYPES: raise ValueError(f"Invalid dragon type: {dragon_type}") @@ -47,6 +54,7 @@ def __init__(self, dragon_type: str, name: str = "Dragon", speed: int = None, self.total_pets = 0 self.last_fed_time = 0.0 # Last time dragon was fed self.last_pet_time = 0.0 # Last time dragon was pet + self.title: Optional[str] = None # Optional title for the dragon # Check if this is a legendary dragon self.is_legendary = dragon_type in LEGENDARY_DRAGON_TYPES @@ -410,6 +418,7 @@ def to_dict(self) -> Dict[str, Any]: data = { "dragon_type": self.dragon_type, "name": self.name, + "title": self.title, "stage": self.stage, "stage_index": self.stage_index, "birth_time": self.birth_time, @@ -425,7 +434,6 @@ def to_dict(self) -> Dict[str, Any]: "is_legendary": self.is_legendary, "enchantments": self.enchantments, "max_enchantments": self.max_enchantments, - "genotype_seed": self.genotype_seed, } # Include genotype data if present if self.genotype: @@ -459,6 +467,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "Dragon": genotype=genotype, genotype_seed=data.get("genotype_seed") ) + dragon.title = data.get("title", None) dragon.stage = data["stage"] dragon.stage_index = data["stage_index"] dragon.birth_time = data["birth_time"] @@ -502,22 +511,75 @@ def create_gene_based(cls, name: str = None, seed: str = None) -> "Dragon": else: genotype = gene_system.generate_from_seed(seed) - # Generate fantasy name if none provided + # Determine dragon type based on genotype color + dragon_type = cls._determine_type_from_color(genotype.color) + + # Generate fantasy name and title if none provided if name is None: - name = generate_dragon_name(genotype.seed) + # Generate name with title (30% chance) + full_name = generate_dragon_name(genotype.seed, include_title=True) + # Check if name has a title + if ' ' in full_name and (' the ' in full_name or full_name.count(' ') >= 2): + # Split name and title + parts = full_name.split(' ', 1) + name = parts[0] + title = parts[1] + else: + name = full_name + title = None + else: + title = None - # Create dragon with genotype (use 'gene' as dragon_type for compatibility) + # Create dragon with genotype and determined type dragon = cls( - dragon_type='gene', + dragon_type=dragon_type, name=name, genotype=genotype, genotype_seed=genotype.seed ) + dragon.title = title return dragon + @staticmethod + def _determine_type_from_color(color: tuple) -> str: + """Determine dragon type based on RGB color values. + + Args: + color: RGB tuple (r, g, b) + + Returns: + Dragon type string (fire, ice, forest, storm, or shadow) + """ + r, g, b = color + + # Calculate which color channel is dominant + max_val = max(r, g, b) + + # Fire: Red dominant + if r == max_val and r > g and r > b: + return "fire" + + # Ice: Blue dominant + elif b == max_val and b > r and b > g: + return "ice" + + # Forest: Green dominant + elif g == max_val and g > r and g > b: + return "forest" + + # Storm: Blue and red similar (purple-ish) + elif abs(r - b) < 40 and r > 100 and b > 100: + return "storm" + + # Shadow: Low overall brightness + elif max_val < 100: + return "shadow" + + # Default to fire if unclear + else: + return "fire" + def __str__(self) -> str: """String representation of dragon.""" - if self.is_gene_based(): - return f"{self.name} (Gene-based {self.stage})" return f"{self.name} ({self.dragon_type} {self.stage})" diff --git a/src/dragon_gene_system.py b/src/dragon_gene_system.py index 79b2109..19e405a 100644 --- a/src/dragon_gene_system.py +++ b/src/dragon_gene_system.py @@ -396,8 +396,8 @@ def _apply_color_tint(self, surface: pygame.Surface, color: Tuple[int, int, int] overlay = pygame.Surface(tinted.get_size(), pygame.SRCALPHA) overlay.fill((*color, 128)) # 128 alpha for 50% blend - # Blend the overlay with multiply mode - tinted.blit(overlay, (0, 0), special_flags=pygame.BLEND_RGBA_MULT) + # Blend the overlay with multiply mode (RGB only, preserve alpha) + tinted.blit(overlay, (0, 0), special_flags=pygame.BLEND_RGB_MULT) return tinted diff --git a/src/dragon_pen.py b/src/dragon_pen.py index 1bd73ea..beb96fd 100644 --- a/src/dragon_pen.py +++ b/src/dragon_pen.py @@ -431,13 +431,13 @@ def __init__(self, game_state: GameState, sprite_manager: SpriteManager): self.bg_manager.set_size(SCREEN_WIDTH, SCREEN_HEIGHT) # Grid settings - self.grid_cols = 4 - self.card_width = 140 - self.card_height = 160 + self.grid_cols = 6 + self.card_width = 120 + self.card_height = 150 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 + self.grid_start_x = (SCREEN_WIDTH - self.grid_width) // 2 # Center horizontally + self.grid_start_y = 160 # Scrolling self.scroll_offset = 0 @@ -708,8 +708,8 @@ def _sell_dragon(self, dragon_index: int): self._update_filtered_dragons() self._create_cards() - # Save game state to persist the changes - self.game_state.save() + # Mark as dirty instead of immediate save + self.game_state.mark_dirty() # Show message self._show_message(f"Sold {dragon_name} for {sell_value} coins!", GREEN) diff --git a/src/equipment_screen.py b/src/equipment_screen.py index c49271a..003769a 100644 --- a/src/equipment_screen.py +++ b/src/equipment_screen.py @@ -283,7 +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.game_state.mark_dirty() # Mark as dirty instead of immediate save self._show_message(f"Unequipped enchantment from slot {button.slot_index + 1}", GREEN) self._create_slot_buttons() self._update_inventory_buttons() @@ -298,7 +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.game_state.mark_dirty() # Mark as dirty instead of immediate save self._show_message(f"Equipped {button.item.name}!", GREEN) self._create_slot_buttons() self._update_inventory_buttons() @@ -362,10 +362,7 @@ def draw(self, surface: pygame.Surface): surface.blit(next_text, (self.next_dragon_button.x + 10, self.next_dragon_button.y + 5)) # Dragon sprite with enchantment color tinting (center) - enchantments = dragon.get_equipped_enchantments() - sprite = self.sprite_manager.get_dragon_with_enchantments( - dragon.dragon_type, dragon.stage, enchantments - ) + sprite = self.sprite_manager.get_dragon_with_enchantments(dragon=dragon) sprite_size = 200 sprite_scaled = pygame.transform.scale(sprite, (sprite_size, sprite_size)) sprite_x = SCREEN_WIDTH // 2 - sprite_size // 2 @@ -412,7 +409,7 @@ def draw(self, surface: pygame.Surface): button.draw(surface, self.tiny_font) # Slot counter - slot_info = self.small_font.render(f"Slots: {len(enchantments)}/{dragon.max_enchantments}", True, PARCHMENT) + slot_info = self.small_font.render(f"Slots: {len(dragon.get_equipped_enchantments())}/{dragon.max_enchantments}", True, PARCHMENT) surface.blit(slot_info, (SCREEN_WIDTH - 220, 155)) # Inventory section diff --git a/src/game_state.py b/src/game_state.py index 9dd0f79..fbe46d0 100644 --- a/src/game_state.py +++ b/src/game_state.py @@ -29,6 +29,12 @@ def __init__(self): self.coins: int = STARTING_COINS self.meat: int = STARTING_MEAT self.last_passive_coin_time: float = time.time() + self.last_close_time: float = time.time() # Track when game was last closed + self.offline_progress_data: Dict[str, Any] = None # Store offline progress for summary + + # Dirty flag to track if changes need saving + self.dirty: bool = False + self.last_auto_save_time: float = time.time() # Track last auto-save time # Use SQLite save system with fallback to JSON for migration db_file = SAVE_FILE.replace('.json', '.db') @@ -147,7 +153,7 @@ def hatch_egg(self, egg_index: int, name: str = "Dragon") -> Optional[Dragon]: dragon = Dragon(dragon_type, name if name else "Dragon") self.dragons.append(dragon) - self.save() # Save after hatching + self.dirty = True # Mark as dirty instead of immediate save return dragon return None @@ -181,7 +187,7 @@ def update_dragons(self): for dragon in self.dragons: dragon.update(current_time) - def update_passive_income(self): + def update_passive_income(self) -> int: """Update passive coin income.""" current_time = time.time() time_elapsed = current_time - self.last_passive_coin_time @@ -194,6 +200,29 @@ def update_passive_income(self): return coins_earned + def calculate_offline_progress(self) -> Dict[str, Any]: + """Calculate what was earned while game was closed. + + Returns: + Dictionary with offline progress info + """ + current_time = time.time() + time_away = current_time - self.last_close_time + + # Cap offline time to 4 hours (14400 seconds) + MAX_OFFLINE_TIME = 4 * 60 * 60 # 4 hours in seconds + capped_time = min(time_away, MAX_OFFLINE_TIME) + + # Calculate coins earned during offline time + coins_earned = int(capped_time / PASSIVE_COIN_RATE) + + return { + "time_away": time_away, + "capped_time": capped_time, + "coins_earned": coins_earned, + "was_capped": time_away > MAX_OFFLINE_TIME + } + def get_mature_dragons(self) -> List[Dragon]: """Get list of mature dragons that can play minigames. @@ -293,7 +322,7 @@ def feed_dragon(self, dragon: Dragon) -> bool: if dragon.feed(): self.meat -= FEED_COST - self.save() # Save after feeding + self.dirty = True # Mark as dirty instead of immediate save return True return False @@ -351,6 +380,7 @@ def to_dict(self) -> Dict[str, Any]: "coins": self.coins, "meat": self.meat, "last_passive_coin_time": self.last_passive_coin_time, + "last_close_time": self.last_close_time, "inventory": self.inventory, "volume": self.volume, "sfx_volume": self.sfx_volume, @@ -393,6 +423,7 @@ def from_dict(self, data: Dict[str, Any]): self.coins = data.get("coins", STARTING_COINS) self.meat = data.get("meat", STARTING_MEAT) self.last_passive_coin_time = data.get("last_passive_coin_time", time.time()) + self.last_close_time = data.get("last_close_time", time.time()) self.inventory = data.get("inventory", {}) self.volume = data.get("volume", 0.5) self.sfx_volume = data.get("sfx_volume", 0.7) @@ -546,6 +577,12 @@ def save(self) -> bool: Returns: True if save succeeded, False otherwise """ + # Record close time for offline progress tracking + self.last_close_time = time.time() + + # Clear dirty flag after successful save + self.dirty = False + if self.use_sqlite: result = self.save_system_sqlite.save_all(self.to_dict()) if result['success']: @@ -558,6 +595,41 @@ def save(self) -> bool: else: return self.save_system_json.save(self.to_dict()) + def mark_dirty(self): + """Mark game state as having unsaved changes.""" + self.dirty = True + + def needs_save(self) -> bool: + """Check if there are unsaved changes. + + Returns: + True if there are unsaved changes, False otherwise + """ + return self.dirty + + def auto_save_if_needed(self, current_time: float = None) -> bool: + """Auto-save if 5 minutes have passed since last save. + + Args: + current_time: Current timestamp (defaults to time.time()) + + Returns: + True if save was performed, False otherwise + """ + if current_time is None: + current_time = time.time() + + # Check if 5 minutes (300 seconds) have passed + time_since_last_save = current_time - self.last_auto_save_time + AUTO_SAVE_INTERVAL = 300.0 # 5 minutes + + if time_since_last_save >= AUTO_SAVE_INTERVAL and self.dirty: + if self.save(): + self.last_auto_save_time = current_time + return True + + return False + def _remove_duplicate_dragons(self): """Remove duplicate dragons from the dragons list. @@ -618,6 +690,17 @@ def load(self) -> bool: # Add starter items if inventory is empty (for existing saves) if not self.inventory: self._add_starter_items() + # Calculate offline progress + self.offline_progress_data = self.calculate_offline_progress() + # Apply capped offline coins + if self.offline_progress_data["coins_earned"] > 0: + self.coins += self.offline_progress_data["coins_earned"] + # Update last_passive_coin_time to reflect the capped time + self.last_passive_coin_time = time.time() - (self.offline_progress_data["capped_time"] % PASSIVE_COIN_RATE) + # Reset dragon last_update times to prevent offline growth + current_time = time.time() + for dragon in self.dragons: + dragon.last_update = current_time return True return False # Fall back to JSON for migration @@ -628,6 +711,17 @@ def load(self) -> bool: # Migrate to SQLite self.use_sqlite = True self.from_dict(data) + # Calculate offline progress for migration + self.offline_progress_data = self.calculate_offline_progress() + # Apply capped offline coins + if self.offline_progress_data["coins_earned"] > 0: + self.coins += self.offline_progress_data["coins_earned"] + # Update last_passive_coin_time to reflect the capped time + self.last_passive_coin_time = time.time() - (self.offline_progress_data["capped_time"] % PASSIVE_COIN_RATE) + # Reset dragon last_update times to prevent offline growth + current_time = time.time() + for dragon in self.dragons: + dragon.last_update = current_time result = self.save_system_sqlite.save_all(self.to_dict()) success = result['success'] if success: @@ -637,8 +731,6 @@ def load(self) -> bool: 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 diff --git a/src/idle_game.py b/src/idle_game.py index 29c9d9d..274c560 100644 --- a/src/idle_game.py +++ b/src/idle_game.py @@ -4,6 +4,7 @@ from src.game_state import GameState from src.sprite_manager import SpriteManager from src.background_manager import BackgroundManager +from src.offline_progress_popup import OfflineProgressPopup from src.utils.constants import ( SCREEN_WIDTH, SCREEN_HEIGHT, WHITE, BLACK, GRAY, STONE_DARK, STONE_MED, STONE_LIGHT, WOOD_DARK, WOOD_MED, @@ -182,12 +183,6 @@ 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) - # Rename dragon system - self.rename_button = None # Will be created when needed - self.is_renaming = False - self.rename_text = "" - self.rename_max_length = 20 - # Interaction buttons (bottom center) button_y = SCREEN_HEIGHT - 90 button_x_start = SCREEN_WIDTH // 2 - 130 @@ -199,20 +194,20 @@ def __init__(self, game_state: GameState, sprite_manager: SpriteManager, music_m self.pet_button, ] - # Navigation menu (right side) - nav_x = SCREEN_WIDTH - 160 - nav_y_start = 120 - self.eggs_button = Button(nav_x, nav_y_start, 120, 40, "Hatchery", ORANGE) - self.dragon_pen_button = Button(nav_x, nav_y_start + 50, 120, 40, "Dragon Pen", BRONZE) - self.equipment_button = Button(nav_x, nav_y_start + 100, 120, 40, "Enchanting", BLUE) - self.shop_button = Button(nav_x, nav_y_start + 150, 120, 40, "Shop", GOLD) - self.minigame_button = Button(nav_x, nav_y_start + 200, 120, 40, "Arena", PURPLE) - - # Mute button (in navigation panel below Arena) - self.mute_button = Button(nav_x, nav_y_start + 250, 120, 40, "Mute", STONE_DARK) - - # Settings button (below mute button) - self.settings_button = Button(nav_x, nav_y_start + 300, 120, 40, "Settings", GRAY) + # Navigation menu (horizontal navbar at top) + nav_y = 60 + button_width = 120 + button_height = 40 + spacing = 10 + total_width = (button_width * 6) + (spacing * 5) + nav_x_start = (SCREEN_WIDTH - total_width) // 2 + + self.eggs_button = Button(nav_x_start, nav_y, button_width, button_height, "Hatchery", ORANGE) + self.dragon_pen_button = Button(nav_x_start + (button_width + spacing), nav_y, button_width, button_height, "Dragon Pen", BRONZE) + self.equipment_button = Button(nav_x_start + (button_width + spacing) * 2, nav_y, button_width, button_height, "Enchanting", BLUE) + self.shop_button = Button(nav_x_start + (button_width + spacing) * 3, nav_y, button_width, button_height, "Shop", GOLD) + self.minigame_button = Button(nav_x_start + (button_width + spacing) * 4, nav_y, button_width, button_height, "Arena", PURPLE) + self.settings_button = Button(nav_x_start + (button_width + spacing) * 5, nav_y, button_width, button_height, "Settings", GRAY) self.navigation_buttons = [ self.eggs_button, @@ -229,49 +224,19 @@ def __init__(self, game_state: GameState, sprite_manager: SpriteManager, music_m self.message_timer = 0 self.message_color = WHITE + # Offline progress popup + self.offline_popup = None + if game_state.offline_progress_data and game_state.offline_progress_data["time_away"] > 60: + # Only show popup if away for more than 1 minute + self.offline_popup = OfflineProgressPopup(game_state.offline_progress_data) + def handle_event(self, event: pygame.event.Event) -> Optional[str]: """Handle input events.""" - # Handle rename mode keyboard input - if self.is_renaming: - if event.type == pygame.KEYDOWN: - if event.key == pygame.K_RETURN: - # Confirm rename - 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.is_renaming = False - self.rename_text = "" - return None - elif event.key == pygame.K_ESCAPE: - # Cancel rename - self.is_renaming = False - self.rename_text = "" - return None - elif event.key == pygame.K_BACKSPACE: - # Delete character - self.rename_text = self.rename_text[:-1] - return None - else: - # Add character if it's printable and under max length - if len(self.rename_text) < self.rename_max_length: - if event.unicode.isprintable(): - self.rename_text += event.unicode - return None - return None # Consume all events while renaming - - # Handle mute button click - if self.mute_button.handle_event(event): - if self.music_manager: - self.music_manager.toggle_mute() - # Update button text - 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 + # Check offline popup first (blocks other input) + if self.offline_popup and self.offline_popup.is_visible(): + self.offline_popup.handle_event(event) return None # Handle interaction button clicks @@ -346,7 +311,7 @@ def _handle_pet(self): return if dragon.pet(): - self.game_state.save() # Save after petting + self.game_state.mark_dirty() # Mark as dirty instead of immediate save self._show_message(f"Pet {dragon.name}!", BLUE) else: self._show_message("Could not pet dragon!", RED) @@ -423,26 +388,14 @@ def draw(self, surface: pygame.Surface): # Draw resources self._draw_resources(surface) - # Draw navigation panel (right side) - self._draw_navigation_panel(surface) - - # 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 - pygame.draw.rect(surface, WOOD_DARK, content_rect, 4) - pygame.draw.line(surface, STONE_LIGHT, - (content_rect.left, content_rect.top), - (content_rect.right, content_rect.top), 2) - pygame.draw.line(surface, STONE_LIGHT, - (content_rect.left, content_rect.top), - (content_rect.left, content_rect.bottom), 2) + # Draw navigation buttons (horizontal navbar) + for button in self.navigation_buttons: + button.draw(surface, self.small_font) - # Draw dragon or no dragons message + # Draw dragon or no dragons message (no content box) dragon = self.game_state.get_selected_dragon() if dragon: - self._draw_dragon_display(surface, dragon, content_rect) + self._draw_dragon_display(surface, dragon) else: no_dragons = self.font.render("No dragons yet!", True, STONE_MED) surface.blit( @@ -462,34 +415,9 @@ def draw(self, surface: pygame.Surface): 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) - - # Panel background (dark wood) - pygame.draw.rect(surface, WOOD_DARK, panel_rect) - pygame.draw.rect(surface, STONE_DARK, panel_rect, 4) - pygame.draw.line(surface, STONE_LIGHT, - (panel_rect.left, panel_rect.top), - (panel_rect.right, panel_rect.top), 2) - pygame.draw.line(surface, STONE_LIGHT, - (panel_rect.left, panel_rect.top), - (panel_rect.left, panel_rect.bottom), 2) - - # Title - title = self.tiny_font.render("Navigation", True, GOLD) - surface.blit(title, (panel_rect.centerx - title.get_width() // 2, panel_rect.top + 10)) - - # Draw navigation buttons - for button in self.navigation_buttons: - button.draw(surface, self.small_font) - - # Draw mute button - self.mute_button.draw(surface, self.small_font) - - # Draw settings button - self.settings_button.draw(surface, self.small_font) + # Draw offline progress popup (on top of everything) + if self.offline_popup and self.offline_popup.is_visible(): + self.offline_popup.draw(surface) def _draw_resources(self, surface: pygame.Surface): """Draw resource display with medieval styling.""" @@ -512,77 +440,71 @@ def _draw_resources(self, surface: pygame.Surface): surface.blit(egg_label, (SCREEN_WIDTH - 470, 15)) surface.blit(egg_value, (SCREEN_WIDTH - 410, 15)) - def _draw_dragon_display(self, surface: pygame.Surface, dragon, content_rect: pygame.Rect): + def _draw_dragon_display(self, surface: pygame.Surface, dragon): """Draw the selected dragon and its info.""" - # Dragon sprite with enchantments (centered) - enchantments = dragon.get_equipped_enchantments() - sprite = self.sprite_manager.get_dragon_with_enchantments( - dragon.dragon_type, dragon.stage, enchantments + # Dragon name above sprite (centered) with background box + name_text = self.font.render(dragon.name, True, GOLD) + name_x = SCREEN_WIDTH // 2 - name_text.get_width() // 2 + name_y = 130 + + # Draw background box for name + name_box_padding = 15 + name_box = pygame.Rect( + name_x - name_box_padding, + name_y - 5, + name_text.get_width() + name_box_padding * 2, + name_text.get_height() + 10 ) - sprite_size = 150 - sprite_scaled = pygame.transform.scale(sprite, (sprite_size, sprite_size)) - sprite_x = SCREEN_WIDTH // 2 - sprite_size // 2 - sprite_y = content_rect.y + 30 - surface.blit(sprite_scaled, (sprite_x, sprite_y)) + pygame.draw.rect(surface, WOOD_DARK, name_box) + pygame.draw.rect(surface, GOLD, name_box, 2) - # Dragon info (left side) - x = content_rect.x + 20 - y = content_rect.y + 20 - - # Name with edit button or rename input - if self.is_renaming: - # Show text input box - name_text = self.small_font.render(f"Name: {self.rename_text}|", True, WOOD_DARK) - surface.blit(name_text, (x, y)) - # Show instructions - instruction_text = self.tiny_font.render("Press ENTER to save, ESC to cancel", True, ORANGE) - surface.blit(instruction_text, (x, y + 25)) - y += 60 + surface.blit(name_text, (name_x, name_y)) + + # Dragon title below name (centered) + if dragon.title: + # Use custom title if available + title = dragon.title + title_color = GOLD else: - # Show name with edit button - name_text = self.small_font.render(f"Name: {dragon.name}", True, WOOD_DARK) - surface.blit(name_text, (x, y)) - - # Create/update rename button position - button_x = x + name_text.get_width() + 10 - button_y = y - 5 - if not self.rename_button: - self.rename_button = Button(button_x, button_y, 60, 30, "Edit", BLUE) - else: - self.rename_button.rect.x = button_x - self.rename_button.rect.y = button_y + # Show dragon type and stage + title = f"{dragon.dragon_type.capitalize()} {dragon.stage.capitalize()}" + title_color = WOOD_DARK - self.rename_button.draw(surface, self.tiny_font) - y += 30 + if dragon.is_legendary and not dragon.title: + title = "★ " + title + " ★" + title_color = PURPLE - # Other info lines - info_lines = [ - (f"Type: {dragon.dragon_type.capitalize()}", WOOD_DARK), - (f"Stage: {dragon.stage.capitalize()}", WOOD_DARK), - ] + title_text = self.small_font.render(title, True, title_color) + title_x = SCREEN_WIDTH // 2 - title_text.get_width() // 2 + title_y = name_y + name_text.get_height() + 5 + surface.blit(title_text, (title_x, title_y)) - for line, color in info_lines: - text = self.small_font.render(line, True, color) - surface.blit(text, (x, y)) - y += 30 + # Dragon sprite with enchantments (centered) + sprite = self.sprite_manager.get_dragon_with_enchantments(dragon=dragon) + sprite_size = 300 + sprite_scaled = pygame.transform.scale(sprite, (sprite_size, sprite_size)) + sprite_x = SCREEN_WIDTH // 2 - sprite_size // 2 + sprite_y = title_y + title_text.get_height() + 20 + surface.blit(sprite_scaled, (sprite_x, sprite_y)) - # Growth progress (bottom) + # Growth progress (bottom center) if not dragon.is_mature(): - self._draw_growth_progress(surface, dragon, content_rect) + self._draw_growth_progress(surface, dragon) - # Cooldowns (right side) - self._draw_cooldowns(surface, dragon, content_rect) + # Cooldowns (right side) - only for immature dragons + if not dragon.is_mature(): + self._draw_cooldowns(surface, dragon) # Stats (bottom left) - self._draw_stats(surface, dragon, content_rect) + self._draw_stats(surface, dragon) - def _draw_growth_progress(self, surface: pygame.Surface, dragon, content_rect: pygame.Rect): + def _draw_growth_progress(self, surface: pygame.Surface, dragon): """Draw growth progress bar with medieval styling.""" progress = dragon.get_growth_progress() - bar_width = content_rect.width - 40 + bar_width = 600 bar_height = 30 - bar_x = content_rect.x + 20 - bar_y = content_rect.bottom - 60 + bar_x = (SCREEN_WIDTH - bar_width) // 2 + bar_y = SCREEN_HEIGHT - 160 # Label #label = self.small_font.render("Growth Progress:", True, WOOD_DARK) @@ -604,13 +526,13 @@ def _draw_growth_progress(self, surface: pygame.Surface, dragon, content_rect: p percent_rect = percent_text.get_rect(center=(bar_x + bar_width // 2, bar_y + bar_height // 2)) surface.blit(percent_text, percent_rect) - def _draw_cooldowns(self, surface: pygame.Surface, dragon, content_rect: pygame.Rect): + def _draw_cooldowns(self, surface: pygame.Surface, dragon): """Draw cooldown timers.""" if dragon.is_mature(): return - x = content_rect.right - 180 - y = content_rect.y + 20 + x = SCREEN_WIDTH - 200 + y = 250 # Feed cooldown feed_ready = dragon.can_feed() @@ -643,20 +565,30 @@ def _draw_cooldowns(self, surface: pygame.Surface, dragon, content_rect: pygame. text = self.tiny_font.render(pet_text, True, pet_color) surface.blit(text, (x, y)) - def _draw_stats(self, surface: pygame.Surface, dragon, content_rect: pygame.Rect): + def _draw_stats(self, surface: pygame.Surface, dragon): """Draw dragon stats with medieval styling.""" - x = content_rect.x + 20 - y = content_rect.bottom - 120 + x = 10 + y = SCREEN_HEIGHT - 60 age_hours = dragon.get_age() / 3600 - stats = [ - f"Age: {age_hours:.1f}h", - f"Fed: {dragon.total_feeds} times", - f"Pet: {dragon.total_pets} times", - ] + + if dragon.is_mature(): + # Show different stats for mature dragons + stats = [ + f"Age: {age_hours:.1f}h", + f"Speed: {dragon.speed}", + f"Strength: {dragon.strength}", + ] + else: + # Show growth stats for immature dragons + stats = [ + f"Age: {age_hours:.1f}h", + f"Fed: {dragon.total_feeds} times", + f"Pet: {dragon.total_pets} times", + ] for stat in stats: - text = self.tiny_font.render(stat, True, WOOD_DARK) + text = self.tiny_font.render(stat, True, WHITE) surface.blit(text, (x, y)) y += 20 diff --git a/src/minigames/dragon_battle.py b/src/minigames/dragon_battle.py index 81dc0bb..0230212 100644 --- a/src/minigames/dragon_battle.py +++ b/src/minigames/dragon_battle.py @@ -550,7 +550,7 @@ def _end_game(self, player_won: bool): # Consolation prize for losing self.game_state.add_coins(BATTLE_LOSE_COINS) - self.game_state.save() + self.game_state.mark_dirty() # Mark as dirty instead of immediate save def update(self, dt: float): """Update battle state. @@ -638,9 +638,7 @@ def _draw_combatant(self, surface: pygame.Surface, combatant: BattleDragon, x: i """Draw a dragon combatant.""" # Dragon sprite if is_player and self.dragon: - sprite = self.sprite_manager.get_dragon_with_enchantments( - self.dragon.dragon_type, self.dragon.stage, self.dragon.get_equipped_enchantments() - ) + sprite = self.sprite_manager.get_dragon_with_enchantments(dragon=self.dragon) sprite_scaled = pygame.transform.scale(sprite, (100, 100)) surface.blit(sprite_scaled, (x, y)) else: diff --git a/src/minigames/dragon_race.py b/src/minigames/dragon_race.py index 1c660bd..e0effce 100644 --- a/src/minigames/dragon_race.py +++ b/src/minigames/dragon_race.py @@ -369,7 +369,7 @@ def _end_game(self): if self.distance >= 200: self.game_state.add_egg(allow_legendary=True) - self.game_state.save() + self.game_state.mark_dirty() # Mark as dirty instead of immediate save def draw(self, surface: pygame.Surface): """Draw race. @@ -423,9 +423,7 @@ def draw(self, surface: pygame.Surface): # Draw player dragon if self.dragon: - sprite = self.sprite_manager.get_dragon_with_enchantments( - self.dragon.dragon_type, self.dragon.stage, self.dragon.get_equipped_enchantments() - ) + sprite = self.sprite_manager.get_dragon_with_enchantments(dragon=self.dragon) sprite_scaled = pygame.transform.scale(sprite, (self.player_size, self.player_size)) surface.blit(sprite_scaled, (int(self.player_x), int(self.player_y))) else: diff --git a/src/minigames/flappy_dragon.py b/src/minigames/flappy_dragon.py index 58851c0..6e00bf3 100644 --- a/src/minigames/flappy_dragon.py +++ b/src/minigames/flappy_dragon.py @@ -228,7 +228,7 @@ def _end_game(self): self.game_over = True self.game_state.add_minigame_score(self.score) self.game_state.add_minigame_rewards(self.score) # Score = obstacles passed - self.game_state.save() + self.game_state.mark_dirty() # Mark as dirty instead of immediate save def draw(self, surface: pygame.Surface): """Draw minigame. @@ -245,7 +245,7 @@ def draw(self, surface: pygame.Surface): # Draw player (dragon) if self.dragon: - sprite = self.sprite_manager.get_sprite(self.dragon.dragon_type, self.dragon.stage) + sprite = self.sprite_manager.get_dragon_sprite(self.dragon) sprite_scaled = pygame.transform.scale(sprite, (self.player_size, self.player_size)) surface.blit(sprite_scaled, (self.player_x, self.player_y)) else: diff --git a/src/offline_progress_popup.py b/src/offline_progress_popup.py new file mode 100644 index 0000000..514b99b --- /dev/null +++ b/src/offline_progress_popup.py @@ -0,0 +1,171 @@ +"""Offline Progress Popup - Shows what was earned while away from the game.""" +import pygame +from src.utils.constants import ( + WHITE, BLACK, PARCHMENT, WOOD_DARK, GOLD, ORANGE, BLUE, GREEN, + SCREEN_WIDTH, SCREEN_HEIGHT +) + + +class OfflineProgressPopup: + """Popup that displays offline progress summary.""" + + def __init__(self, offline_data: dict): + """Initialize offline progress popup. + + Args: + offline_data: Dictionary containing offline progress info + """ + self.offline_data = offline_data + self.visible = True + + # Popup dimensions + self.width = 450 + self.height = 280 + self.x = (SCREEN_WIDTH - self.width) // 2 + self.y = (SCREEN_HEIGHT - self.height) // 2 + + # Create popup rect + self.rect = pygame.Rect(self.x, self.y, self.width, self.height) + + # Close button + button_width = 120 + button_height = 40 + self.close_button = pygame.Rect( + self.x + (self.width - button_width) // 2, + self.y + self.height - 70, + button_width, + button_height + ) + + # Fonts + self.title_font = pygame.font.Font(None, 48) + self.font = pygame.font.Font(None, 32) + self.small_font = pygame.font.Font(None, 24) + + def handle_event(self, event: pygame.event.Event) -> bool: + """Handle input events. + + Args: + event: Pygame event + + Returns: + True if popup should close, False otherwise + """ + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: # Left click + if self.close_button.collidepoint(event.pos): + self.visible = False + return True + + if event.type == pygame.KEYDOWN: + if event.key in (pygame.K_RETURN, pygame.K_ESCAPE, pygame.K_SPACE): + self.visible = False + return True + + return False + + def draw(self, surface: pygame.Surface): + """Draw the offline progress popup. + + Args: + surface: Surface to draw on + """ + if not self.visible: + return + + # Draw semi-transparent overlay + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) + overlay.fill((0, 0, 0, 180)) + surface.blit(overlay, (0, 0)) + + # Add 3D shadow effect (draw BEFORE main popup) + shadow_rect = self.rect.copy() + shadow_rect.x += 5 + shadow_rect.y += 5 + shadow_surface = pygame.Surface((shadow_rect.width, shadow_rect.height), pygame.SRCALPHA) + shadow_surface.fill((0, 0, 0, 100)) + surface.blit(shadow_surface, (shadow_rect.x, shadow_rect.y)) + + # Draw popup background + pygame.draw.rect(surface, PARCHMENT, self.rect) + pygame.draw.rect(surface, WOOD_DARK, self.rect, 4) + + # Title + title = self.title_font.render("Welcome Back!", True, GOLD) + title_x = self.x + (self.width - title.get_width()) // 2 + title_y = self.y + 20 + surface.blit(title, (title_x, title_y)) + + # Draw decorative line under title + line_y = title_y + title.get_height() + 10 + pygame.draw.line(surface, WOOD_DARK, + (self.x + 40, line_y), + (self.x + self.width - 40, line_y), 2) + + # Time away text + time_away = self.offline_data["time_away"] + hours = int(time_away // 3600) + minutes = int((time_away % 3600) // 60) + + time_text = "" + if hours > 0: + time_text = f"Time away: {hours}h {minutes}m" + else: + time_text = f"Time away: {minutes}m" + + time_surface = self.font.render(time_text, True, BLUE) + time_x = self.x + (self.width - time_surface.get_width()) // 2 + time_y = line_y + 30 + surface.blit(time_surface, (time_x, time_y)) + + # Coins earned + coins_earned = self.offline_data["coins_earned"] + if coins_earned > 0: + coins_text = f"+{coins_earned} coins earned!" + coins_surface = self.font.render(coins_text, True, GOLD) + coins_x = self.x + (self.width - coins_surface.get_width()) // 2 + coins_y = time_y + 50 + surface.blit(coins_surface, (coins_x, coins_y)) + else: + coins_text = "No coins earned" + coins_surface = self.font.render(coins_text, True, WOOD_DARK) + coins_x = self.x + (self.width - coins_surface.get_width()) // 2 + coins_y = time_y + 50 + surface.blit(coins_surface, (coins_x, coins_y)) + + # Cap warning if applicable + if self.offline_data["was_capped"]: + cap_text = "(Capped at 4 hours)" + cap_surface = self.small_font.render(cap_text, True, ORANGE) + cap_x = self.x + (self.width - cap_surface.get_width()) // 2 + cap_y = coins_y + 35 + surface.blit(cap_surface, (cap_x, cap_y)) + + # Note about dragons + note_y = coins_y + (65 if self.offline_data["was_capped"] else 45) + note_text = "Dragons do not grow while offline" + note_surface = self.small_font.render(note_text, True, WOOD_DARK) + note_x = self.x + (self.width - note_surface.get_width()) // 2 + surface.blit(note_surface, (note_x, note_y)) + + # Draw close button + button_hovered = self.close_button.collidepoint(pygame.mouse.get_pos()) + button_color = GOLD if button_hovered else GREEN + pygame.draw.rect(surface, button_color, self.close_button) + pygame.draw.rect(surface, WOOD_DARK, self.close_button, 3) + + # Button text + button_text = self.font.render("Continue", True, WHITE) + button_text_x = self.close_button.x + (self.close_button.width - button_text.get_width()) // 2 + button_text_y = self.close_button.y + (self.close_button.height - button_text.get_height()) // 2 + surface.blit(button_text, (button_text_x, button_text_y)) + + + + def is_visible(self) -> bool: + """Check if popup is visible. + + Returns: + True if visible, False otherwise + """ + return self.visible diff --git a/src/save_system_sqlite.py b/src/save_system_sqlite.py index 6f5a401..dbbf5a4 100644 --- a/src/save_system_sqlite.py +++ b/src/save_system_sqlite.py @@ -552,6 +552,7 @@ def save_all(self, game_data: Dict[str, Any]) -> Dict[str, Any]: "coins": game_data.get("coins", 0), "meat": game_data.get("meat", 0), "last_passive_coin_time": game_data.get("last_passive_coin_time", time.time()), + "last_close_time": game_data.get("last_close_time", time.time()), "volume": game_data.get("volume", 0.5), "sfx_volume": game_data.get("sfx_volume", 0.7), "music_muted": game_data.get("music_muted", False), diff --git a/src/settings_screen.py b/src/settings_screen.py index b2a51cd..c6ba7e8 100644 --- a/src/settings_screen.py +++ b/src/settings_screen.py @@ -27,14 +27,14 @@ def __init__(self, game_state: GameState, sound_manager=None): self.font_small = pygame.font.Font(None, 28) # UI layout - self.panel_rect = pygame.Rect(150, 100, SCREEN_WIDTH - 300, SCREEN_HEIGHT - 200) + self.panel_rect = pygame.Rect(100, 80, SCREEN_WIDTH - 200, SCREEN_HEIGHT - 160) # Music volume slider self.volume_label_pos = (self.panel_rect.x + 50, self.panel_rect.y + 80) self.volume_slider_rect = pygame.Rect( self.panel_rect.x + 50, self.panel_rect.y + 120, - self.panel_rect.width - 100, + self.panel_rect.width - 250, 20 ) self.volume_handle_radius = 15 @@ -45,22 +45,37 @@ def __init__(self, game_state: GameState, sound_manager=None): self.sfx_volume_slider_rect = pygame.Rect( self.panel_rect.x + 50, self.panel_rect.y + 220, - self.panel_rect.width - 100, + self.panel_rect.width - 250, 20 ) self.sfx_volume_handle_radius = 15 self.dragging_sfx_volume = False + # Mute toggle buttons (positioned inside panel) + mute_button_x = self.panel_rect.x + self.panel_rect.width - 170 + self.music_mute_button = pygame.Rect( + mute_button_x, + self.panel_rect.y + 110, + 120, + 40 + ) + self.sfx_mute_button = pygame.Rect( + mute_button_x, + self.panel_rect.y + 210, + 120, + 40 + ) + # FPS limit selector self.fps_label_pos = (self.panel_rect.x + 50, self.panel_rect.y + 300) self.fps_options = [30, 60, 100, 144, 240, 0] # 0 = unlimited self.fps_button_rects = [] self._create_fps_buttons() - # Back button + # Back button (positioned lower to avoid FPS buttons) self.back_button = pygame.Rect( self.panel_rect.x + self.panel_rect.width // 2 - 100, - self.panel_rect.y + self.panel_rect.height - 80, + self.panel_rect.y + self.panel_rect.height - 60, 200, 50 ) @@ -96,6 +111,14 @@ def handle_event(self, event: pygame.event.Event) -> Optional[str]: if self.back_button.collidepoint(mouse_pos): return "idle" + # Check music mute button + if self.music_mute_button.collidepoint(mouse_pos): + self.game_state.music_muted = not self.game_state.music_muted + + # Check sfx mute button + if self.sfx_mute_button.collidepoint(mouse_pos): + self.game_state.sfx_muted = not self.game_state.sfx_muted + # Check music volume slider if self._get_volume_handle_rect().collidepoint(mouse_pos): self.dragging_volume = True @@ -259,6 +282,18 @@ def draw(self, screen: pygame.Surface): 2 ) + # Music mute toggle button + music_mute_hovered = self.music_mute_button.collidepoint(pygame.mouse.get_pos()) + music_mute_color = GOLD if music_mute_hovered else (WOOD_DARK if self.game_state.music_muted else GREEN) + pygame.draw.rect(screen, music_mute_color, self.music_mute_button) + pygame.draw.rect(screen, STONE_LIGHT, self.music_mute_button, 2) + + mute_text = "Unmute" if self.game_state.music_muted else "Mute" + mute_surface = self.font_small.render(mute_text, True, WHITE) + mute_x = self.music_mute_button.centerx - mute_surface.get_width() // 2 + mute_y = self.music_mute_button.centery - mute_surface.get_height() // 2 + screen.blit(mute_surface, (mute_x, mute_y)) + # === SOUND EFFECTS VOLUME SECTION === # SFX volume label @@ -306,6 +341,18 @@ def draw(self, screen: pygame.Surface): 2 ) + # SFX mute toggle button + sfx_mute_hovered = self.sfx_mute_button.collidepoint(pygame.mouse.get_pos()) + sfx_mute_color = GOLD if sfx_mute_hovered else (WOOD_DARK if self.game_state.sfx_muted else GREEN) + pygame.draw.rect(screen, sfx_mute_color, self.sfx_mute_button) + pygame.draw.rect(screen, STONE_LIGHT, self.sfx_mute_button, 2) + + sfx_mute_text = "Unmute" if self.game_state.sfx_muted else "Mute" + sfx_mute_surface = self.font_small.render(sfx_mute_text, True, WHITE) + sfx_mute_x = self.sfx_mute_button.centerx - sfx_mute_surface.get_width() // 2 + sfx_mute_y = self.sfx_mute_button.centery - sfx_mute_surface.get_height() // 2 + screen.blit(sfx_mute_surface, (sfx_mute_x, sfx_mute_y)) + # === FPS LIMIT SECTION === # FPS label diff --git a/src/shop_screen.py b/src/shop_screen.py index 929f68e..f25b310 100644 --- a/src/shop_screen.py +++ b/src/shop_screen.py @@ -125,7 +125,7 @@ def _buy_selected_item(self): # Attempt to buy if item["action"](): self._show_message(f"Purchased {item['name']}!", GREEN) - self.game_state.save() + self.game_state.mark_dirty() # Mark as dirty instead of immediate save else: self._show_message("Purchase failed!", RED) else: diff --git a/src/sprite_manager.py b/src/sprite_manager.py index fa5080c..c4340f1 100644 --- a/src/sprite_manager.py +++ b/src/sprite_manager.py @@ -306,7 +306,7 @@ def render_gene_dragon(self, dragon, scale: float = 1.0) -> Optional[pygame.Surf # For egg stage, show legacy egg sprite instead of gene-based dragon if dragon.stage == "egg": - return self.get_sprite("gene", "egg") + return self.get_sprite(dragon.dragon_type, "egg") # All other stages (hatchling, juvenile, adult, elder) show full grown dragon gene_system = get_gene_system() diff --git a/src/utils/constants.py b/src/utils/constants.py index fbdbe33..bf7ec34 100644 --- a/src/utils/constants.py +++ b/src/utils/constants.py @@ -111,7 +111,6 @@ "forest", "storm", "shadow", - "gene", # New gene-based procedural dragons ] # Legendary dragon types (rare variants)