Files
wicked-quest/wicked_quest.py

472 lines
19 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import os
import pygame
from libstormgames import *
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
from src.save_manager import SaveManager
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
self.currentGame = None
self.runLock = False # Toggle behavior of the run keys
self.saveManager = SaveManager()
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.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.sounds, self.player)
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"""
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")
# Show menu
currentIndex = 0
lastSpoken = -1
messagebox("Select a save file to load:")
while True:
if currentIndex != lastSpoken:
speak(options[currentIndex])
lastSpoken = currentIndex
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
return None
elif event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(options) - 1:
currentIndex += 1
try:
self.sounds['menu-move'].play()
except:
pass
elif event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
currentIndex -= 1
try:
self.sounds['menu-move'].play()
except:
pass
elif event.key == pygame.K_RETURN:
try:
self.sounds['menu-select'].play()
except:
pass
if currentIndex == len(options) - 1: # Cancel
return None
else:
return save_files[currentIndex]
pygame.event.clear()
def auto_save(self):
"""Automatically save the game if player has enough bone dust"""
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.sounds:
play_sound(self.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
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.sounds[player.footstepSound])
player.distanceSinceLastStep = 0
player.lastStepTime = currentTime
# Status queries
if keys[pygame.K_c]:
speak(f"{player.get_coins()} bone dust for extra lives, {player.get_save_bone_dust()} bone dust for saves")
if keys[pygame.K_h]:
speak(f"{player.get_health()} health of {player.get_max_health()}")
if keys[pygame.K_i]:
speak(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.")
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] 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
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] or keys[pygame.K_UP]) 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"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.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.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()
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
if event.key == pygame.K_ESCAPE:
try:
pygame.mixer.music.stop()
except:
pass
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()
# 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(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):
# 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
gamePath = os.path.dirname(get_level_path(self.currentGame, 1))
for ext in ['.wav', '.ogg', '.mp3']:
endFile = os.path.join(gamePath, f'end{ext}')
if os.path.exists(endFile):
self.sounds['end_scene'] = pygame.mixer.Sound(endFile)
cut_scene(self.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.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']
# Load the level
if self.load_level(current_level):
# Restore player state
self.saveManager.restore_player_state(self.player, save_data)
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.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:
self.player = None # Reset player for new game
self.gameStartTime = pygame.time.get_ticks()
if self.load_level(1):
self.game_loop()
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.sounds)
if __name__ == "__main__":
game = WickedQuest()
game.run()