#!/usr/bin/env python3 import json import os from libstormgames import * from src.level import Level from src.object import Object from src.player import Player class WickedQuest: def __init__(self): """Initialize game and load sounds.""" self.sounds = initialize_gui("Wicked Quest") self.currentLevel = None self.gameStartTime = None self.lastThrowTime = 0 self.throwDelay = 250 self.player = None # Will be initialized when first level loads def load_level(self, levelNumber): """Load a level from its JSON file.""" levelFile = f"levels/{levelNumber}.json" 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.sounds) else: # Just update player position for new level self.player.xPos = levelData["player_start"]["x"] self.player.yPos = levelData["player_start"]["y"] # Pass existing player to new level self.currentLevel = Level(levelData, self.sounds, self.player) # Announce level details levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. {levelData['description']}" messagebox(levelIntro) return True except FileNotFoundError: speak("Level not found") 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 state player.isRunning = keys[pygame.K_SPACE] # 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]: # Left movementDistance = currentSpeed player.xPos -= currentSpeed player.facingRight = False elif keys[pygame.K_d]: # 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.sounds[player.footstepSound]) player.distanceSinceLastStep = 0 player.lastStepTime = currentTime # Status queries if keys[pygame.K_c]: speak(f"{player.get_coins()} gbone dust") if keys[pygame.K_h]: speak(f"{player.get_health()} HP") if keys[pygame.K_l]: speak(f"{player.get_lives()} lives") if keys[pygame.K_j]: # Check jack o'lanterns speak(f"{player.get_jack_o_lanterns()} jack o'lanterns") if keys[pygame.K_f]: currentTime = pygame.time.get_ticks() if currentTime - self.lastThrowTime >= self.throwDelay: self.currentLevel.throw_projectile() self.lastThrowTime = currentTime if keys[pygame.K_e]: speak(f"Wielding {self.currentLevel.player.currentWeapon.name.replace('_', ' ')}") # Handle attack with either CTRL key if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime): play_sound(self.sounds[player.currentWeapon.attackSound]) # Handle jumping if keys[pygame.K_w] and not player.isJumping: player.isJumping = True player.jumpStartTime = currentTime play_sound(self.sounds['jump']) # Check if jump should end if player.isJumping and currentTime - player.jumpStartTime >= player.jumpDuration: player.isJumping = False play_sound(self.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"Level {self.currentLevel.levelId} Complete!"] report.append(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)}") pygame.mixer.stop() 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)}") display_text(report) def game_loop(self): """Main game loop handling updates and state changes.""" clock = pygame.time.Clock() levelStartTime = pygame.time.get_ticks() currentLevelNum = 1 while True: currentTime = pygame.time.get_ticks() # Game volume controls for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: return 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(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) currentLevelNum += 1 if self.load_level(currentLevelNum): 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 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.""" while True: choice = game_menu(self.sounds, "play", "instructions", "learn_sounds", "credits", "donate", "exit") if choice == "exit": exit_game() elif choice == "play": self.player = None # Reset player for new game self.gameStartTime = pygame.time.get_ticks() # Set game start time here if self.load_level(1): self.game_loop() elif choice == "learn_sounds": choice = learn_sounds(self.sounds) if __name__ == "__main__": game = WickedQuest() game.run()