#!/usr/bin/env python3 # -*- coding: utf-8 -*- import json import os import pygame from libstormgames import * from src.pack_sound_system import PackSoundSystem from src.level import Level from src.object import Object from src.player import Player from src.game_selection import select_game, get_level_path, get_game_dir_path from src.save_manager import SaveManager from src.survival_generator import SurvivalGenerator class WickedQuest: def __init__(self): """Initialize game and load sounds.""" self.sounds = initialize_gui("Wicked Quest") self.soundSystem = None # Will be created when game is selected self.currentLevel = None self.gameStartTime = None self.lastThrowTime = 0 self.throwDelay = 250 self.lastWeaponSwitchTime = 0 self.weaponSwitchDelay = 200 self.lastStatusTime = 0 self.statusDelay = 300 # 300ms between status checks self.player = None self.currentGame = None self.runLock = False # Toggle behavior of the run keys self.saveManager = SaveManager() self.survivalGenerator = None self.lastBroomLandingTime = 0 # Timestamp to prevent ducking after broom landing self.survivalWave = 1 self.survivalScore = 0 # Console system - no longer needed since we use get_input directly # Level tracking for proper progression self.currentLevelNum = 1 # Cheat save flag - allows one save per level/session self.canSave = True def initialize_pack_sounds(self): """Initialize pack-specific sound system after game selection.""" if self.currentGame: self.soundSystem = PackSoundSystem(self.sounds, "sounds/", levelPackName=self.currentGame) else: self.soundSystem = PackSoundSystem(self.sounds, "sounds/") def get_sounds(self): """Get the current sound system (pack-specific if available, otherwise original).""" if self.soundSystem is None and self.currentGame is not None: # Initialize pack sounds if not done yet self.initialize_pack_sounds() return self.soundSystem if self.soundSystem else self.sounds def get_closest_enemy_info(self): """Get information about the closest enemy, returns None if no enemies.""" if not self.currentLevel or not self.currentLevel.enemies: return None # Find active enemies active_enemies = [] for enemy in self.currentLevel.enemies: if enemy.isActive: distance = abs(enemy.xPos - self.player.xPos) direction = "right" if enemy.xPos > self.player.xPos else "left" active_enemies.append((enemy, distance, direction)) if not active_enemies: return None # Sort by distance and get closest active_enemies.sort(key=lambda x: x[1]) enemy, distance, direction = active_enemies[0] # Convert distance to natural language if distance == 0: return f"{enemy.enemyType} right on top of you" elif distance <= 10: distance_desc = "very close" elif distance <= 30: distance_desc = "close" elif distance <= 60: distance_desc = "far" elif distance <= 100: distance_desc = "very far" else: distance_desc = "extremely far" return f"{enemy.enemyType} {distance_desc} to the {direction}" def process_console_command(self, command): """Process console commands and execute their effects.""" command = command.lower().strip() if command == "samhain": # Level selection menu self.show_level_select_menu() elif command == "misfits": # Give 20 HP (but don't exceed max HP) if self.player: currentHealth = self.player.get_health() maxHealth = self.player.get_max_health() newHealth = min(currentHealth + 20, maxHealth) self.player._health = newHealth speak(f"Health restored to {newHealth}") if 'heal' in self.get_sounds(): play_sound(self.get_sounds()['heal']) elif command == "coffinshakers": # Give extra life if self.player: self.player._lives += 1 speak(f"Extra life granted. You now have {self.player.get_lives()} lives") if 'get_extra_life' in self.get_sounds(): play_sound(self.get_sounds()['get_extra_life']) elif command == "nekromantix": # Give 100 bone dust (both types) if self.player: self.player._coins += 100 self.player.add_save_bone_dust(100) speak(f"100 bone dust granted. You now have {self.player.get_coins()} bone dust and {self.player.get_save_bone_dust()} save bone dust") if 'coin' in self.get_sounds(): play_sound(self.get_sounds()['coin']) elif command == "calabrese": # Reveal closest enemy on current level if self.currentLevel and self.currentLevel.enemies: enemyCount = len([enemy for enemy in self.currentLevel.enemies if enemy.isActive]) if enemyCount > 0: speak(f"{enemyCount} enemies remaining on this level") closest_enemy_info = self.get_closest_enemy_info() if closest_enemy_info: speak(f"Closest enemy: {closest_enemy_info}") else: speak("No active enemies on this level") else: speak("No enemies found") elif command == "balzac": # Set bone dust to 200 (both types) if self.player: self.player._coins = 200 self.player._saveBoneDust = 200 speak(f"Bone dust set to 200. You now have {self.player.get_coins()} bone dust and {self.player.get_save_bone_dust()} save bone dust") if 'coin' in self.get_sounds(): play_sound(self.get_sounds()['coin']) elif command == "blitzkid": # Grant broom and nunchucks weapons with level pack override support if self.player: from src.weapon import Weapon # Check if player already has broom hasBroom = any(weapon.originalName == "witch_broom" for weapon in self.player.weapons) if not hasBroom: broomWeapon = Weapon.create_witch_broom() self.player.add_weapon(broomWeapon) # Check if player already has nunchucks hasNunchucks = any(weapon.originalName == "nunchucks" for weapon in self.player.weapons) if not hasNunchucks: nunchucksWeapon = Weapon.create_nunchucks() self.player.add_weapon(nunchucksWeapon) # Report what was granted (using display names with overrides) grantedWeapons = [] if not hasBroom: broomName = next((w.name for w in self.player.weapons if w.originalName == "witch_broom"), "witch broom") grantedWeapons.append(broomName.replace('_', ' ')) if not hasNunchucks: nunchucksName = next((w.name for w in self.player.weapons if w.originalName == "nunchucks"), "nunchucks") grantedWeapons.append(nunchucksName.replace('_', ' ')) if grantedWeapons: speak(f"Granted {', '.join(grantedWeapons)}") else: speak("You already have all weapons") elif command == "creepshow": # Toggle god mode (invincibility) if self.player: if hasattr(self.player, '_godMode'): self.player._godMode = not self.player._godMode else: self.player._godMode = True status = "enabled" if getattr(self.player, '_godMode', False) else "disabled" speak(f"God mode {status}") elif command == "diemonsterdie": # Give 100 jack o'lanterns if self.player: self.player._jack_o_lantern_count += 100 speak(f"100 jack o'lanterns granted. You now have {self.player.get_jack_o_lanterns()} jack o'lanterns") if 'get_jack_o_lantern' in self.get_sounds(): play_sound(self.get_sounds()['get_jack_o_lantern']) elif command == "murderland": # Set jack o'lanterns to exactly 13 (the special number) if self.player: self.player._jack_o_lantern_count = 13 speak("13 jack o'lanterns, this power courses through my soul.") if 'get_jack_o_lantern' in self.get_sounds(): play_sound(self.get_sounds()['get_jack_o_lantern']) elif command == "koffinkats": # Spawn a coffin with random item at player's current position if self.player and self.currentLevel: from src.coffin import CoffinObject # Create coffin at player's current position coffin = CoffinObject( int(self.player.xPos), # Use player's current x position self.player.yPos, # Use player's current y position self.get_sounds(), self.currentLevel, item="random" # Random item ) # Add coffin to the level's object list self.currentLevel.objects.append(coffin) speak("Coffin spawned at your location") if 'coffin' in self.get_sounds(): play_sound(self.get_sounds()['coffin']) elif command == "theother": # Save game on current level if not self.canSave: speak("Save already used for this level") return # Don't save in survival mode if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId == 999: speak("Cannot save in survival mode") return # Create save without bone dust requirement try: success, message = self.saveManager.create_save( self.player, self.currentLevel.levelId, self.gameStartTime, self.currentGame, bypass_cost=True # Skip bone dust requirement ) if success: self.canSave = False # Disable further saves for this level speak("Game saved") try: if 'save' in self.get_sounds(): play_sound(self.get_sounds()['save']) except Exception as e: print(f"Error playing save sound: {e}") pass else: speak(f"Save failed: {message}") except Exception as e: speak("Save failed due to error") print(f"Error during cheat save: {e}") elif command == "wednesday13": # Max out all stats - the ultimate power-up if self.player: from src.weapon import Weapon # Max health and restore to full self.player._maxHealth = 20 self.player._health = 20 # Max lives (set to 13 for the theme) self.player._lives = 13 # Max bone dust (both types) self.player._coins = 999 self.player._saveBoneDust = 999 # Max jack o'lanterns (set to 99 for practicality) self.player._jack_o_lantern_count = 99 # Grant all weapons with level pack override support weaponOverrides = getattr(self.currentLevel, 'weaponSoundOverrides', {}) if self.currentLevel else {} # Check if player already has broom hasBroom = any(w.weaponType == "broom" for w in self.player.weapons) if not hasBroom: broom = Weapon("broom", "witch broom", "broom_attack", "broom_hit", 7, 1.5, 2.0, 1.25) if weaponOverrides and "witch_broom" in weaponOverrides: override = weaponOverrides["witch_broom"] broom.name = override.get("name", broom.name) broom.attackSound = override.get("attack_sound", broom.attackSound) broom.hitSound = override.get("hit_sound", broom.hitSound) self.player.add_weapon(broom) # Check if player already has nunchucks hasNunchucks = any(w.weaponType == "nunchucks" for w in self.player.weapons) if not hasNunchucks: nunchucks = Weapon("nunchucks", "nunchucks", "nunchucks_attack", "nunchucks_hit", 10, 1.0, 1.0, 1.0) if weaponOverrides and "nunchucks" in weaponOverrides: override = weaponOverrides["nunchucks"] nunchucks.name = override.get("name", nunchucks.name) nunchucks.attackSound = override.get("attack_sound", nunchucks.attackSound) nunchucks.hitSound = override.get("hit_sound", nunchucks.hitSound) self.player.add_weapon(nunchucks) # Enable god mode for good measure self.player._godMode = True speak("MAXIMUM POWER! All stats maxed, all weapons granted, god mode enabled") if 'get_extra_life' in self.get_sounds(): play_sound(self.get_sounds()['get_extra_life']) else: speak("Unknown command") def show_level_select_menu(self): """Show menu to select and warp to different levels.""" if not self.currentGame: speak("No game loaded") return # Find available levels availableLevels = [] levelNumber = 1 while True: levelPath = get_level_path(self.currentGame, levelNumber) if not os.path.exists(levelPath): break availableLevels.append(levelNumber) levelNumber += 1 if not availableLevels: speak("No levels found") return # Create menu options options = [] for levelNum in availableLevels: options.append(f"Level {levelNum}") options.append("Cancel") choice = instruction_menu(self.get_sounds(), "Warp to which level?", *options) if choice and choice != "Cancel": # Extract level number from choice levelNum = int(choice.split()[-1]) # Stop all sounds and music before warping pygame.mixer.stop() try: pygame.mixer.music.stop() except: pass # Clear events for clean transition pygame.event.clear() # Load the selected level if self.load_level(levelNum): # Update the current level counter for proper progression self.currentLevelNum = levelNum # Reset cheat save flag for warped level self.canSave = True speak(f"Warped to level {levelNum}") else: speak(f"Failed to load level {levelNum}") def open_console(self): """Open the console for input and process the command.""" from libstormgames import get_input # Clear events before opening console for PyInstaller compatibility pygame.event.clear() # Get console input using libstormgames text input command = get_input("Enter console command:") # Process the command if user didn't cancel if command is not None: self.process_console_command(command) def load_level(self, levelNumber): """Load a level from its JSON file.""" levelFile = get_level_path(self.currentGame, levelNumber) try: with open(levelFile, 'r') as f: levelData = json.load(f) # Create player if this is the first level if self.player is None: self.player = Player(levelData["player_start"]["x"], levelData["player_start"]["y"], self.get_sounds()) else: # Reset player for new level. self.player.isDucking = False self.player.xPos = levelData["player_start"]["x"] self.player.yPos = levelData["player_start"]["y"] self.player.isInvincible = False if hasattr(self.player, '_last_countdown'): del self.player._last_countdown self.player.invincibilityStartTime = 0 # Clean up spider web effects if hasattr(self.player, 'webPenaltyEndTime'): del self.player.webPenaltyEndTime # Remove the penalty timer self.player.moveSpeed *= 2 # Restore normal speed if self.player.currentWeapon: self.player.currentWeapon.attackDuration *= 0.5 # Restore normal attack speed # Pass existing player to new level pygame.event.clear() self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame) return True except FileNotFoundError: return False def validate_levels(self): """Check if level files have valid JSON.""" errors = [] # Check levels from 1 until no more files are found levelNumber = 1 while True: levelPath = get_level_path(self.currentGame, levelNumber) if not os.path.exists(levelPath): break try: with open(levelPath, 'r') as f: # This will raise an exception if JSON is invalid json.load(f) except json.JSONDecodeError as e: errors.append(f"Level {levelNumber}: Invalid JSON format - {str(e)}") except Exception as e: errors.append(f"Level {levelNumber}: Error reading file - {str(e)}") levelNumber += 1 return errors def load_game_menu(self): """Display load game menu with available saves using instruction_menu""" save_files = self.saveManager.get_save_files() if not save_files: messagebox("No save files found.") return None # Create menu options options = [] for save_file in save_files: options.append(save_file['display_name']) options.append("Cancel") # Use instruction_menu for consistent behavior choice = instruction_menu(self.get_sounds(), "Select a save file to load:", *options) if choice == "Cancel" or choice is None: return None else: # Find the corresponding save file for save_file in save_files: if save_file['display_name'] == choice: return save_file return None def auto_save(self): """Automatically save the game if player has enough bone dust""" # Don't save in survival mode if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId == 999: return False if not self.player.can_save(): return False # Automatically create save try: success, message = self.saveManager.create_save( self.player, self.currentLevel.levelId, self.gameStartTime, self.currentGame ) if success: try: if 'save' in self.get_sounds(): play_sound(self.get_sounds()['save']) else: print("Save sound not found in sounds dictionary") except Exception as e: print(f"Error playing save sound: {e}") pass # Continue if save sound fails to play else: print(f"Save failed: {message}") return success except Exception as e: print(f"Error during save: {e}") return False def handle_input(self): """Process keyboard input for player actions.""" keys = pygame.key.get_pressed() player = self.currentLevel.player currentTime = pygame.time.get_ticks() # Update running and ducking states # Don't handle ducking if jumping (down key is used for instant landing with broom) # Also prevent ducking for 250ms after broom landing to avoid conflict broomLandingGracePeriod = 250 # milliseconds recentBroomLanding = (hasattr(self, 'lastBroomLandingTime') and currentTime - self.lastBroomLandingTime < broomLandingGracePeriod) if not player.isJumping and not recentBroomLanding: if (keys[pygame.K_s] or keys[pygame.K_DOWN]) and not player.isDucking: player.duck() elif (not keys[pygame.K_s] and not keys[pygame.K_DOWN]) and player.isDucking: player.stand() if self.runLock: player.isRunning = not (keys[pygame.K_SPACE] or keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]) else: player.isRunning = keys[pygame.K_SPACE] or keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT] # Get current speed (handles both running and jumping) currentSpeed = player.get_current_speed() # Track movement distance for this frame movementDistance = 0 # Horizontal movement if keys[pygame.K_a] or keys[pygame.K_LEFT]: # Left movementDistance = currentSpeed player.xPos -= currentSpeed player.facingRight = False elif keys[pygame.K_d] or keys[pygame.K_RIGHT]: # Right movementDistance = currentSpeed player.xPos += currentSpeed player.facingRight = True # Handle footsteps if movementDistance > 0 and not player.isJumping: player.distanceSinceLastStep += movementDistance if player.should_play_footstep(currentTime): play_sound(self.get_sounds()[player.footstepSound]) player.distanceSinceLastStep = 0 player.lastStepTime = currentTime # Status queries with debouncing if (keys[pygame.K_c] or keys[pygame.K_e] or keys[pygame.K_h] or keys[pygame.K_i]) and currentTime - self.lastStatusTime >= self.statusDelay: self.lastStatusTime = currentTime if keys[pygame.K_c]: # Simplified bone dust status only if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId == 999: speak(f"{player.get_coins()} bone dust collected") else: speak(f"{player.get_coins()} bone dust for extra lives, {player.get_save_bone_dust()} bone dust for saves") elif keys[pygame.K_e]: # Weapon and ammo status if player.currentWeapon: weapon_name = getattr(player.currentWeapon, 'displayName', player.currentWeapon.name.replace("_", " ")) status_message = f"Wielding {weapon_name}" # Check if it's a projectile weapon - always show ammo for projectile weapons weapon_type = getattr(player.currentWeapon, 'weaponType', 'melee') if weapon_type == "projectile": ammo_type = getattr(player.currentWeapon, 'ammoType', None) if ammo_type: ammo_count = 0 ammo_display_name = ammo_type.replace("_", " ") # Default fallback # Get current ammo count based on ammo type if ammo_type == "bone_dust": ammo_count = player.get_coins() ammo_display_name = "bone dust" elif ammo_type == "shin_bone": ammo_count = player.shinBoneCount ammo_display_name = "shin bones" elif ammo_type == "jack_o_lantern": ammo_count = player._jack_o_lantern_count ammo_display_name = "jack o'lanterns" elif ammo_type == "guts": ammo_count = player.collectedItems.count("guts") ammo_display_name = "guts" elif ammo_type == "hand_of_glory": ammo_count = player.collectedItems.count("hand_of_glory") ammo_display_name = "hands of glory" else: # Check for any other item type in collectedItems ammo_count = player.collectedItems.count(ammo_type) status_message += f". {ammo_count} {ammo_display_name}" speak(status_message) else: speak("No weapon equipped") elif keys[pygame.K_h]: speak(f"{player.get_health()} health of {player.get_max_health()}") elif keys[pygame.K_i]: if self.currentLevel.levelId == 999: base_info = f"Wave {self.survivalWave}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this wave so far. {player.get_lives()} lives remaining." else: base_info = f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this level so far. {player.get_lives()} lives remaining." # Add closest enemy info closest_enemy_info = self.get_closest_enemy_info() if closest_enemy_info: speak(f"{base_info} {closest_enemy_info}") else: speak(base_info) if keys[pygame.K_l]: speak(f"{player.get_lives()} lives") if keys[pygame.K_j]: # Check jack o'lanterns jackOLanternCount = player.get_jack_o_lanterns() if jackOLanternCount == 13: # Murderland rocks! speak(f"{jackOLanternCount} jack o'lanterns, this power courses through my soul.") else: speak(f"{jackOLanternCount} jack o'lanterns") if keys[pygame.K_f] or keys[pygame.K_z] or keys[pygame.K_SLASH]: currentTime = pygame.time.get_ticks() if currentTime - self.lastThrowTime >= self.throwDelay: self.currentLevel.throw_projectile() self.lastThrowTime = currentTime # Weapon switching (1=shovel, 2=broom, 3=nunchucks, 4-0=custom weapons) currentTime = pygame.time.get_ticks() if currentTime - self.lastWeaponSwitchTime >= self.weaponSwitchDelay: # Check standard weapon keys (1-3) if keys[pygame.K_1]: if player.switch_to_weapon(1): self.lastWeaponSwitchTime = currentTime elif keys[pygame.K_2]: if player.switch_to_weapon(2): self.lastWeaponSwitchTime = currentTime elif keys[pygame.K_3]: if player.switch_to_weapon(3): self.lastWeaponSwitchTime = currentTime # Check custom weapon keys (4-0) elif keys[pygame.K_4]: if player.switch_to_weapon(4): self.lastWeaponSwitchTime = currentTime elif keys[pygame.K_5]: if player.switch_to_weapon(5): self.lastWeaponSwitchTime = currentTime elif keys[pygame.K_6]: if player.switch_to_weapon(6): self.lastWeaponSwitchTime = currentTime elif keys[pygame.K_7]: if player.switch_to_weapon(7): self.lastWeaponSwitchTime = currentTime elif keys[pygame.K_8]: if player.switch_to_weapon(8): self.lastWeaponSwitchTime = currentTime elif keys[pygame.K_9]: if player.switch_to_weapon(9): self.lastWeaponSwitchTime = currentTime elif keys[pygame.K_0]: if player.switch_to_weapon(10): # 0 key maps to index 10 self.lastWeaponSwitchTime = currentTime # Handle attack with either CTRL key if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime): play_sound(self.get_sounds()[player.currentWeapon.attackSound]) # If this was a projectile weapon attack, create a projectile if hasattr(player, 'isProjectileAttack') and player.isProjectileAttack: self.currentLevel.create_weapon_projectile(player) # Handle jumping if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping and not player.isDucking: player.isJumping = True player.jumpStartTime = currentTime play_sound(self.get_sounds()['jump']) # Handle instant landing with broom (press down while jumping) if (player.isJumping and (keys[pygame.K_s] or keys[pygame.K_DOWN]) and player.currentWeapon and getattr(player.currentWeapon, 'originalName', player.currentWeapon.name) == "witch_broom"): player.isJumping = False play_sound(self.get_sounds()[player.footstepSound]) # Landing sound # Reset step distance tracking after landing player.distanceSinceLastStep = 0 player.lastStepTime = currentTime # Set timestamp to prevent immediate ducking after broom landing self.lastBroomLandingTime = currentTime # Check if jump should end naturally if player.isJumping and currentTime - player.jumpStartTime >= player.get_current_jump_duration(): player.isJumping = False play_sound(self.get_sounds()[player.footstepSound]) # Landing sound # Reset step distance tracking after landing player.distanceSinceLastStep = 0 player.lastStepTime = currentTime def display_level_stats(self, timeTaken): """Display level completion statistics.""" # Convert time from milliseconds to minutes:seconds minutes = timeTaken // 60000 seconds = (timeTaken % 60000) // 1000 # Update time in stats self.currentLevel.player.stats.update_stat('Total time', timeTaken, levelOnly=True) report = [f"Time taken: {minutes} minutes and {seconds} seconds"] # Add all level stats for key in self.currentLevel.player.stats.level: if key != 'Total time': # Skip time since we already displayed it report.append(f"{key}: {self.currentLevel.player.stats.get_level_stat(key)}") report.append(f"Score: {int(self.currentLevel.levelScore)}") # Stop all sounds and music then play fanfare try: speak(f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}, complete!") pygame.mixer.music.stop() cut_scene(self.get_sounds(), '_finish_level') except: pass display_text(report) self.currentLevel.player.stats.reset_level() def display_game_over(self, timeTaken): """Display game over screen with statistics.""" minutes = timeTaken // 60000 seconds = (timeTaken % 60000) // 1000 report = ["Game Over!"] report.append(f"Time taken: {minutes} minutes and {seconds} seconds") # Add all total stats for key in self.currentLevel.player.stats.total: if key not in ['Total time', 'levelsCompleted']: # Skip these report.append(f"Total {key}: {self.currentLevel.player.stats.get_total_stat(key)}") report.append(f"Final Score: {self.player.scoreboard.get_score()}") if self.player.scoreboard.check_high_score(): pygame.event.clear() self.player.scoreboard.add_high_score() cut_scene(self.get_sounds(), "game_over") display_text(report) def display_survival_stats(self, timeTaken): """Display survival mode completion statistics.""" # Convert time from milliseconds to minutes:seconds minutes = timeTaken // 60000 seconds = (timeTaken % 60000) // 1000 report = [f"Survival Mode Complete!"] report.append(f"Final Wave Reached: {self.survivalWave}") report.append(f"Final Score: {self.survivalScore}") report.append(f"Time Survived: {minutes} minutes and {seconds} seconds") report.append("") # Blank line # Add all total stats for key in self.currentLevel.player.stats.total: if key not in ['Total time', 'levelsCompleted']: # Skip these report.append(f"Total {key}: {self.currentLevel.player.stats.get_total_stat(key)}") if self.currentLevel.player.scoreboard.check_high_score(): pygame.event.clear() self.currentLevel.player.scoreboard.add_high_score() cut_scene(self.get_sounds(), "game_over") display_text(report) def game_loop(self, startingLevelNum=1): """Main game loop handling updates and state changes.""" clock = pygame.time.Clock() levelStartTime = pygame.time.get_ticks() self.currentLevelNum = startingLevelNum while True: currentTime = pygame.time.get_ticks() pygame.event.pump() # Game volume controls for event in pygame.event.get(): if event.type == pygame.KEYDOWN: # Check for Alt modifier mods = pygame.key.get_mods() altPressed = mods & pygame.KMOD_ALT # Console activation with grave accent key - only in story mode if event.key in (pygame.K_BACKQUOTE, pygame.K_QUOTE): # Grave accent key (`) # Open console (only in story mode, not survival) if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId != 999: self.open_console() continue if event.key == pygame.K_ESCAPE: # Stop all sounds before exiting (consistent with survival mode) pygame.mixer.stop() try: pygame.mixer.music.stop() except: pass # Calculate total time and show game over sequence totalTime = pygame.time.get_ticks() - self.gameStartTime self.display_game_over(totalTime) return elif event.key in [pygame.K_CAPSLOCK, pygame.K_TAB]: self.runLock = not self.runLock speak("Run lock " + ("enabled." if self.runLock else "disabled.")) elif event.key == pygame.K_BACKSPACE: pause_game() # Speech history controls elif event.key == pygame.K_F1: speak_previous() elif event.key == pygame.K_F2: speak_current() elif event.key == pygame.K_F3: speak_next() # Volume controls (require Alt) elif altPressed: if event.key == pygame.K_PAGEUP: adjust_master_volume(0.1) elif event.key == pygame.K_PAGEDOWN: adjust_master_volume(-0.1) elif event.key == pygame.K_HOME: adjust_bgm_volume(0.1) elif event.key == pygame.K_END: adjust_bgm_volume(-0.1) elif event.key == pygame.K_INSERT: adjust_sfx_volume(0.1) elif event.key == pygame.K_DELETE: adjust_sfx_volume(-0.1) # Update game state self.currentLevel.player.update(currentTime) self.handle_input() self.currentLevel.update_audio() # Handle combat and projectiles self.currentLevel.handle_combat(currentTime) self.currentLevel.handle_projectiles(currentTime) # Check for death first if self.currentLevel.player.get_health() <= 0: if self.currentLevel.player.get_lives() <= 0: # Game over - use gameStartTime for total time pygame.mixer.stop() totalTime = pygame.time.get_ticks() - self.gameStartTime self.display_game_over(totalTime) return else: pygame.mixer.stop() self.currentLevel.player._health = self.currentLevel.player._maxHealth self.load_level(self.currentLevelNum) levelStartTime = pygame.time.get_ticks() # Reset level timer continue # Handle collisions and check level completion if self.currentLevel.handle_collisions(): # Level time is from levelStartTime levelTime = pygame.time.get_ticks() - levelStartTime self.display_level_stats(levelTime) self.currentLevelNum += 1 if self.load_level(self.currentLevelNum): # Reset cheat save flag for new level self.canSave = True # Auto save at the beginning of new level if conditions are met self.auto_save() levelStartTime = pygame.time.get_ticks() # Reset level timer for new level continue else: # Game complete - use gameStartTime for total totalTime = pygame.time.get_ticks() - self.gameStartTime if self.player.xPos >= self.currentLevel.rightBoundary: # Check for end of game scene using unified path resolution for ext in ['.wav', '.ogg', '.mp3']: endFile = os.path.join(get_game_dir_path(self.currentGame), f'end{ext}') if os.path.exists(endFile): self.get_sounds()['end_scene'] = pygame.mixer.Sound(endFile) cut_scene(self.get_sounds(), 'end_scene') break else: messagebox("Congratulations! You've completed all available levels!") self.display_game_over(totalTime) return clock.tick(60) # 60 FPS def run(self): """Main game loop with menu system.""" # make sure no music is playing when the menu loads. try: pygame.mixer.music.stop() except: pass while True: # Add load game option if saves exist custom_options = [] if self.saveManager.has_saves(): custom_options.append("load_game") choice = game_menu(self.get_sounds(), None, *custom_options) if choice == "exit": exit_game() elif choice == "load_game": selected_save = self.load_game_menu() if selected_save: success, save_data = self.saveManager.load_save(selected_save['filepath']) if success: # Load the saved game self.currentGame = save_data['game_state']['currentGame'] self.gameStartTime = save_data['game_state']['gameStartTime'] current_level = save_data['game_state']['currentLevel'] # Initialize pack-specific sound system self.initialize_pack_sounds() # Load the level if self.load_level(current_level): # Restore player state self.saveManager.restore_player_state(self.player, save_data) # Re-apply weapon overrides after restoring player state to ensure # sound/name overrides work with restored weapon properties if hasattr(self.currentLevel, 'weaponOverrides') and self.currentLevel.weaponOverrides: self.currentLevel._apply_weapon_overrides(self.currentLevel.weaponOverrides) self.game_loop(current_level) else: messagebox("Failed to load saved level.") else: messagebox(f"Failed to load save: {save_data}") elif choice == "play": self.currentGame = select_game(self.get_sounds()) if self.currentGame is None: continue # User cancelled game selection, return to main menu # Initialize pack-specific sound system self.initialize_pack_sounds() # Validate level files before starting errors = self.validate_levels() if errors: errorLines = ["Level files contain errors:"] errorLines.extend(errors) errorLines.append("\nPlease fix these errors before playing.") display_text(errorLines) continue if self.currentGame: # Ask player to choose game mode mode_choice = game_mode_menu(self.get_sounds(), self.currentGame) if mode_choice == "story": self.player = None # Reset player for new game self.gameStartTime = pygame.time.get_ticks() if self.load_level(1): self.game_loop() elif mode_choice == "survival": self.start_survival_mode() elif choice == "high_scores": board = Scoreboard() scores = board.get_high_scores() lines = ["High Scores:"] for i, entry in enumerate(scores, 1): scoreStr = f"{i}. {entry['name']}: {entry['score']}" lines.append(scoreStr) pygame.event.clear() display_text(lines) elif choice == "learn_sounds": choice = learn_sounds(self.get_sounds()) def start_survival_mode(self): """Initialize and start survival mode.""" self.survivalGenerator = SurvivalGenerator(self.currentGame) self.survivalWave = 1 self.survivalScore = 0 self.player = Player(0, 0, self.get_sounds()) self.gameStartTime = pygame.time.get_ticks() # Show intro message before level starts messagebox(f"Survival Mode - Wave {self.survivalWave}! Survive as long as you can!") # Generate first survival segment levelData = self.survivalGenerator.generate_survival_level(self.survivalWave, 300) self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame) self.survival_loop() def survival_loop(self): """Main survival mode game loop with endless level generation.""" clock = pygame.time.Clock() while True: currentTime = pygame.time.get_ticks() pygame.event.pump() # Handle events for event in pygame.event.get(): if event.type == pygame.KEYDOWN: # Check for Alt modifier mods = pygame.key.get_mods() altPressed = mods & pygame.KMOD_ALT if event.key == pygame.K_ESCAPE: # Stop all sounds before exiting pygame.mixer.stop() pygame.mixer.music.stop() # Calculate survival time survivalTime = pygame.time.get_ticks() - self.gameStartTime self.display_survival_stats(survivalTime) return elif event.key in [pygame.K_CAPSLOCK, pygame.K_TAB]: self.runLock = not self.runLock speak("Run lock " + ("enabled." if self.runLock else "disabled.")) elif event.key == pygame.K_BACKSPACE: pause_game() # Speech history controls elif event.key == pygame.K_F1: speak_previous() elif event.key == pygame.K_F2: speak_current() elif event.key == pygame.K_F3: speak_next() # Volume controls (require Alt) elif altPressed: if event.key == pygame.K_PAGEUP: adjust_master_volume(0.1) elif event.key == pygame.K_PAGEDOWN: adjust_master_volume(-0.1) elif event.key == pygame.K_HOME: adjust_bgm_volume(0.1) elif event.key == pygame.K_END: adjust_bgm_volume(-0.1) elif event.key == pygame.K_INSERT: adjust_sfx_volume(0.1) elif event.key == pygame.K_DELETE: adjust_sfx_volume(-0.1) elif event.type == pygame.QUIT: exit_game() # Update game state (following main game_loop pattern) self.currentLevel.player.update(currentTime) self.handle_input() self.currentLevel.update_audio() # Handle combat and projectiles self.currentLevel.handle_combat(currentTime) self.currentLevel.handle_projectiles(currentTime) # Handle collisions (including collecting items) self.currentLevel.handle_collisions() # Check if player reached end of segment - generate new one if self.player.xPos >= self.currentLevel.rightBoundary - 20: # Check lock system - only advance if no active enemies remain if self.currentLevel.isLocked and any(enemy.isActive for enemy in self.currentLevel.enemies): speak("You must defeat all enemies before proceeding to the next wave!") play_sound(self.get_sounds()['locked']) # Push player back a bit self.player.xPos -= 5 else: self.advance_survival_wave() # Check for death first (following main game loop pattern) if self.currentLevel.player.get_health() <= 0: if self.currentLevel.player.get_lives() <= 0: # Game over - stop all sounds pygame.mixer.stop() # Calculate survival time survivalTime = pygame.time.get_ticks() - self.gameStartTime self.display_survival_stats(survivalTime) return else: # Player died but has lives left - respawn pygame.mixer.stop() self.currentLevel.player._health = self.currentLevel.player._maxHealth # Reset player position to beginning of current segment self.player.xPos = 10 # Update score based on survival time self.survivalScore += 1 clock.tick(60) # 60 FPS def advance_survival_wave(self): """Generate next wave/segment for survival mode.""" self.survivalWave += 1 # Clear any lingering projectiles/sounds from previous wave if hasattr(self, 'currentLevel') and self.currentLevel: self.currentLevel.projectiles.clear() pygame.mixer.stop() # Stop any ongoing catapult/enemy sounds # Check for all monsters unlocked bonus and prepare wave message waveMessage = f"Wave {self.survivalWave}!" if self.survivalGenerator: maxLevel = max(self.survivalGenerator.levelData.keys()) if self.survivalGenerator.levelData else 1 if self.survivalWave == maxLevel + 1: # First wave after all levels unlocked # Double the current score self.survivalScore *= 2 play_sound(self.get_sounds().get("survivor_bonus", self.get_sounds()["get_extra_life"])) waveMessage += " All monsters unlocked bonus! Score doubled!" # Announce new wave (with bonus message if applicable) speak(waveMessage) # Generate new segment segmentLength = min(500, 300 + (self.survivalWave * 20)) # Longer segments over time levelData = self.survivalGenerator.generate_survival_level(self.survivalWave, segmentLength) # Preserve player position but shift to start of new segment playerX = 10 self.player.xPos = playerX # Create new level self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame) def game_mode_menu(sounds, game_dir=None): """Display game mode selection menu using instruction_menu. Args: sounds (dict): Dictionary of loaded sound effects game_dir (str): Current game directory to check for instructions/credits Returns: str: Selected game mode or None if cancelled """ from src.game_selection import get_game_dir_path import os # Build base menu options menu_options = ["Story", "Survival Mode"] # Check for level pack specific files if game directory is provided if game_dir: try: game_path = get_game_dir_path(game_dir) # Check for instructions.txt instructions_path = os.path.join(game_path, "instructions.txt") if os.path.exists(instructions_path): menu_options.append("Instructions") # Check for credits.txt credits_path = os.path.join(game_path, "credits.txt") if os.path.exists(credits_path): menu_options.append("Credits") except Exception: # If there's any error checking files, just continue with basic menu pass while True: choice = instruction_menu(sounds, "Select game mode:", *menu_options) if choice == "Story": return "story" elif choice == "Survival Mode": return "survival" elif choice == "Instructions" and game_dir: # Display instructions file try: game_path = get_game_dir_path(game_dir) instructions_path = os.path.join(game_path, "instructions.txt") print(f"DEBUG: Looking for instructions at: {instructions_path}") if os.path.exists(instructions_path): print("DEBUG: Instructions file found, loading content...") with open(instructions_path, 'r', encoding='utf-8') as f: instructions_content = f.read() print(f"DEBUG: Content length: {len(instructions_content)} characters") print("DEBUG: Calling display_text...") # Convert string to list of lines for display_text content_lines = instructions_content.split('\n') print(f"DEBUG: Split into {len(content_lines)} lines") display_text(content_lines) print("DEBUG: display_text returned") else: print("DEBUG: Instructions file not found at expected path") speak("Instructions file not found") except Exception as e: print(f"DEBUG: Error loading instructions: {str(e)}") speak(f"Error loading instructions: {str(e)}") elif choice == "Credits" and game_dir: # Display credits file try: game_path = get_game_dir_path(game_dir) credits_path = os.path.join(game_path, "credits.txt") print(f"DEBUG: Looking for credits at: {credits_path}") if os.path.exists(credits_path): print("DEBUG: Credits file found, loading content...") with open(credits_path, 'r', encoding='utf-8') as f: credits_content = f.read() print(f"DEBUG: Content length: {len(credits_content)} characters") print("DEBUG: Calling display_text...") # Convert string to list of lines for display_text content_lines = credits_content.split('\n') print(f"DEBUG: Split into {len(content_lines)} lines") display_text(content_lines) print("DEBUG: display_text returned") else: print("DEBUG: Credits file not found at expected path") speak("Credits file not found") except Exception as e: print(f"DEBUG: Error loading credits: {str(e)}") speak(f"Error loading credits: {str(e)}") else: return None if __name__ == "__main__": game = WickedQuest() game.run()