Files
wicked-quest/wicked_quest.py

362 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import os
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
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
def load_level(self, levelNumber):
"""Load a level from its JSON file."""
levelFile = get_level_path(self.currentGame, levelNumber)
pygame.event.pump()
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.pump()
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 handle_input(self):
"""Process keyboard input for player actions."""
keys = pygame.key.get_pressed()
player = self.currentLevel.player
currentTime = pygame.time.get_ticks()
pygame.event.pump()
# 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")
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()
pygame.event.clear()
pygame.event.pump()
cut_scene(self.sounds, "game_over")
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()
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):
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:
choice = game_menu(self.sounds, "play", "high_scores", "instructions",
"learn_sounds", "credits", "donate", "exit")
if choice == "exit":
exit_game()
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()