278 lines
11 KiB
Python
278 lines
11 KiB
Python
#!/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
|
|
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
|
|
|
|
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"]
|
|
|
|
# Pass existing player to new level
|
|
pygame.event.pump()
|
|
self.currentLevel = Level(levelData, self.sounds, self.player)
|
|
|
|
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()
|
|
pygame.event.pump()
|
|
|
|
# Update running and ducking states
|
|
if keys[pygame.K_s] and not player.isDucking:
|
|
player.duck()
|
|
elif not keys[pygame.K_s] and player.isDucking:
|
|
player.stand()
|
|
|
|
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()} health of {player.get_max_health()}")
|
|
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)}")
|
|
|
|
# Stop all sounds and music
|
|
pygame.mixer.stop()
|
|
try:
|
|
pygame.mixer.music.stop()
|
|
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)}")
|
|
|
|
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
|
|
# 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:
|
|
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", "instructions", "learn_sounds",
|
|
"credits", "donate", "exit")
|
|
|
|
if choice == "exit":
|
|
exit_game()
|
|
elif choice == "play":
|
|
self.currentGame = select_game(self.sounds)
|
|
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 == "learn_sounds":
|
|
choice = learn_sounds(self.sounds)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
game = WickedQuest()
|
|
game.run()
|