Compare commits

...

18 Commits

Author SHA1 Message Date
Storm Dragon 60ab8cd868 Changes merged from testing. 2025-09-11 11:47:43 -04:00
Storm Dragon d83e7db248 A few adjustments to documentation and reworked how monsters are unlocked in survival mode. Added bonus for unlocking everything. 2025-09-11 11:44:46 -04:00
Storm Dragon cc0f0437f5 game cover picture added. 2025-09-10 01:03:26 -04:00
Storm Dragon 2bfb146ef5 game cover picture added. 2025-09-10 01:02:41 -04:00
Storm Dragon 21d3cd7788 lint and spacing fixes. Also fixed problem with levels not showing up in compiled version (hopefully). 2025-09-09 22:02:33 -04:00
Storm Dragon 949c12f193 A few minor cleanups. Added game over sound to survival mode. Not sure how I missed that for this long lol. 2025-09-09 18:12:51 -04:00
Storm Dragon dc1557e71d Oops, forgot to add some files in the last push. Rest of the sound changes. 2025-09-09 03:03:22 -04:00
Storm Dragon 494de84ba3 Improve sound playing code to check for specific level pack sounds. 2025-09-09 03:02:40 -04:00
Storm Dragon 8766290ccd Hopefully fixed a race condition where you could be killed while getting a cauldron and come back to life. Added sound for fill in grave and for survivor bonus. 2025-09-09 00:01:53 -04:00
Storm Dragon 1d37a16f9e Minor cosmetic updates. 2025-09-08 18:03:05 -04:00
Storm Dragon 2437e13604 Updated libstormgames. Fixed death sounds following player. Fixed volume keys in survival mode. 2025-09-08 16:00:13 -04:00
Storm Dragon 04067a4bb3 Updated credits and instructions. 2025-09-07 20:27:16 -04:00
Storm Dragon 8a60c8e454 Fixed traceback in menu. 2025-09-07 18:21:13 -04:00
Storm Dragon 3034712c95 If wielding witch broom instantly land when jumping by pressing down. 2025-09-07 14:50:09 -04:00
Storm Dragon 56a78aa4ff Weapons now do different things. Shovels fill in graves, broom gives a speed and jump bonus and nunchucks just thwack things real hard and have best reach. 2025-09-07 13:20:49 -04:00
Storm Dragon cf3f27d9b8 Add ability to switch weapons with number keys. 2025-09-07 12:11:52 -04:00
Storm Dragon 2bc27c0e28 More work on survival mode. 2025-09-07 11:13:01 -04:00
Storm Dragon ce353d0ed9 Updates to libstormgames submodule. Updated the game to use the menu improvements. Started work on survival mode. 2025-09-07 03:13:25 -04:00
28 changed files with 1269 additions and 543 deletions
+1
View File
@@ -1 +1,2 @@
*.ogg filter=lfs diff=lfs merge=lfs -text *.ogg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
-3
View File
@@ -25,9 +25,6 @@ https://hubert-humphrey.com
Steven Robinson: Steven Robinson:
Ko-fi Supporters:
https://ko-fi.com/stormux/leaderboard
Music: Music:
Choir of Doom: Choir of Doom:
+23 -3
View File
@@ -19,6 +19,9 @@ tab or capslock: Toggle run lock.
Control: Attack. Control: Attack.
Space or shift: Hold to run if runlock is disabled. If run lock is enabled, hold to walk. Space or shift: Hold to run if runlock is disabled. If run lock is enabled, hold to walk.
f, z, or /: Throw jack O'lantern. f, z, or /: Throw jack O'lantern.
1: Switch to rusty shovel (if available).
2: Switch to witch's broom (if available).
3: Switch to nunchucks (if available).
c: Check bone dust. c: Check bone dust.
e: Check currently wielded weapon. e: Check currently wielded weapon.
h: Check health. h: Check health.
@@ -28,7 +31,7 @@ l: Check lives remaining.
Alt+PageDown: Master volume decrease. Alt+PageDown: Master volume decrease.
Alt+PageUp: Master volume increase. Alt+PageUp: Master volume increase.
Alt+End: Ambience volume decrease. Alt+End: Ambience volume decrease.
Alt+Home: Game volume increase. Alt+Home: Ambience volume increase.
Alt+Delete: Game sounds volume decrease. Alt+Delete: Game sounds volume decrease.
Alt+Insert: Game sounds volume increase. Alt+Insert: Game sounds volume increase.
Backspace: Pause or resume the game. Backspace: Pause or resume the game.
@@ -41,7 +44,24 @@ Spider webs can be passed by ducking as you move by them.
If you hit a spiderweb, a spider spawns, and you are slowed for 15 seconds. If you are invincible, you are not slowed, but the spider still appears. If you hit a spiderweb, a spider spawns, and you are slowed for 15 seconds. If you are invincible, you are not slowed, but the spider still appears.
Running and jumping both move you at 1.5 your normal speed. Running and jumping both move you at 1.5 your normal speed.
Items bounce away from you when they are freed from the coffin. You must jump to catch them when they are in range. Items bounce away from you when they are freed from the coffin. You must jump to catch them when they are in range.
The game automatically saves your progress when you have collected 200 bone dust. You will hear a message saying the game has been saved, and a wolf will howl. To load a game, use the load option in the main menu. Load only appears if you have saved games. The first save should happen around level 6. You can switch between weapons you've collected using the number keys 1, 2, and 3. This allows strategic combat use different weapons for different situations based on their damage, range, and speed.
The shovel is a weak weapon, but it can fill in graves. While wielding the shovel hold down and walk (do not run) over the grave. If you run you will trip and fall in.
The witch's broom isn't in good enough condition from bashing enemies to be used for true flight, but it does grant a speed bonus and also allows you to land at any point while jumping, just press down.
The game automatically saves your progress when you have collected 200 bone dust. You will hear a message saying the game has been saved, and a wolf will howl. To load a game, use the load option in the main menu. Load only appears if you have saved games.
Game Modes
Story Mode: Traditional level-by-level progression through predefined stages. Your progress is saved, and you can collect extra lives by gathering bone dust.
Survival Mode: An endless challenge where you face wave after wave of increasingly difficult enemies. Each wave gets progressively harder with stronger enemies, faster spawn rates, and longer levels. Level length starts at 300 units and increases by 20 units per wave (capped at 500 units). In survival mode:
No saving or loading - each run is a fresh start
No extra lives are awarded
Each wave must be completed by defeating all enemies before you can advance
Bone dust still provides health bonuses but collecting 100 bone dust gives 2000 bonus score instead of extra lives
Monsters unlock per wave that matches the level in which they are found
Once all monsters are unlocked your score is doubled
All coffin contents are randomized
A detailed statistics report shows your performance at the end of each run
Enemies Enemies
@@ -61,7 +81,7 @@ Bonuses and items
Bone dust: Currency of the game. Collect it to gain health and extra lives. Bone dust: Currency of the game. Collect it to gain health and extra lives.
Cauldron: Restores you to full health. Cauldron: Restores you to full health.
Witch's broom: A weapon gained from witches. Stronger than the grave digger's rusty shovel. Witch's broom: A weapon gained from witches. Stronger than the grave digger's rusty shovel and grants extra speed and jump time.
Coffin: Contains items. Be quick, they try to escape. Coffin: Contains items. Be quick, they try to escape.
Guts: Adds 2 health to your maximum unlife if your unlife is below 20 points otherwise it restores your health. Guts: Adds 2 health to your maximum unlife if your unlife is below 20 points otherwise it restores your health.
Hand of Glory: Grants invincibility for a short time. Hand of Glory: Grants invincibility for a short time.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11 -13
View File
@@ -14,7 +14,7 @@ class Pumpkin:
self.isActive = True self.isActive = True
self.damage = playerMaxHealth // 2 # Half of player's max health self.damage = playerMaxHealth // 2 # Half of player's max health
self.soundChannel = None self.soundChannel = None
self.soundName = 'pumpkin_low' if isHigh else 'pumpkin_high' # Inverted mapping self.soundName = "pumpkin_low" if isHigh else "pumpkin_high" # Inverted mapping
def update(self, sounds, playerX): def update(self, sounds, playerX):
"""Update pumpkin position and sound""" """Update pumpkin position and sound"""
@@ -39,7 +39,7 @@ class Pumpkin:
# Calculate volume and pan for splat sound based on final position # Calculate volume and pan for splat sound based on final position
volume, left, right = calculate_volume_and_pan(playerX, self.x) volume, left, right = calculate_volume_and_pan(playerX, self.x)
if volume > 0: # Only play if within audible range if volume > 0: # Only play if within audible range
obj_play(sounds, 'pumpkin_splat', playerX, self.x, loop=False) obj_play(sounds, "pumpkin_splat", playerX, self.x, loop=False)
def check_collision(self, player): def check_collision(self, player):
"""Check if pumpkin hits player""" """Check if pumpkin hits player"""
@@ -58,7 +58,9 @@ class Pumpkin:
class Catapult(Object): class Catapult(Object):
def __init__(self, x, y, sounds, direction=1, fireInterval=5000, firingRange=20): def __init__(self, x, y, sounds, direction=1, fireInterval=5000, firingRange=20):
super().__init__( super().__init__(
x, y, "catapult", x,
y,
"catapult",
isStatic=True, isStatic=True,
isCollectible=False, isCollectible=False,
) )
@@ -77,18 +79,14 @@ class Catapult(Object):
self.lastFireTime = currentTime self.lastFireTime = currentTime
# Play launch sound using directional audio # Play launch sound using directional audio
play_directional_sound(self.sounds, 'catapult_launch', player.xPos, self.xPos) play_directional_sound(self.sounds, "catapult_launch", player.xPos, self.xPos)
# Set up pending pumpkin # Set up pending pumpkin
isHigh = random.choice([True, False]) isHigh = random.choice([True, False])
fireDirection = 1 if player.xPos > self.xPos else -1 fireDirection = 1 if player.xPos > self.xPos else -1
# Store pumpkin data for later creation # Store pumpkin data for later creation
self.pendingPumpkin = { self.pendingPumpkin = {"isHigh": isHigh, "direction": fireDirection, "playerMaxHealth": player.get_max_health()}
'isHigh': isHigh,
'direction': fireDirection,
'playerMaxHealth': player.get_max_health()
}
# Set when to actually launch the pumpkin # Set when to actually launch the pumpkin
self.pumpkinLaunchTime = currentTime + self.launchDelay self.pumpkinLaunchTime = currentTime + self.launchDelay
@@ -116,9 +114,9 @@ class Catapult(Object):
# Create and fire the pending pumpkin # Create and fire the pending pumpkin
pumpkin = Pumpkin( pumpkin = Pumpkin(
self.xPos, self.xPos,
self.pendingPumpkin['isHigh'], self.pendingPumpkin["isHigh"],
self.pendingPumpkin['direction'], self.pendingPumpkin["direction"],
self.pendingPumpkin['playerMaxHealth'] self.pendingPumpkin["playerMaxHealth"],
) )
self.activePumpkins.append(pumpkin) self.activePumpkins.append(pumpkin)
self.pendingPumpkin = None self.pendingPumpkin = None
@@ -140,4 +138,4 @@ class Catapult(Object):
pumpkin.isActive = False pumpkin.isActive = False
self.activePumpkins.remove(pumpkin) self.activePumpkins.remove(pumpkin)
if not player.isInvincible: if not player.isInvincible:
self.sounds['player_takes_damage'].play() self.sounds["player_takes_damage"].play()
+4 -15
View File
@@ -9,12 +9,7 @@ from src.powerup import PowerUp
class CoffinObject(Object): class CoffinObject(Object):
def __init__(self, x, y, sounds, level, item="random"): def __init__(self, x, y, sounds, level, item="random"):
super().__init__( super().__init__(x, y, "coffin", isStatic=True, isCollectible=False, isHazard=False)
x, y, "coffin",
isStatic=True,
isCollectible=False,
isHazard=False
)
self.sounds = sounds self.sounds = sounds
self.level = level self.level = level
self.isBroken = False self.isBroken = False
@@ -25,9 +20,9 @@ class CoffinObject(Object):
"""Handle being hit by the player's weapon""" """Handle being hit by the player's weapon"""
if not self.isBroken: if not self.isBroken:
self.isBroken = True self.isBroken = True
play_sound(self.sounds['coffin_shatter']) play_sound(self.sounds["coffin_shatter"])
self.level.levelScore += 500 self.level.levelScore += 500
self.level.player.stats.update_stat('Coffins broken', 1) self.level.player.stats.update_stat("Coffins broken", 1)
# Stop the ongoing coffin sound # Stop the ongoing coffin sound
if self.channel: if self.channel:
@@ -54,13 +49,7 @@ class CoffinObject(Object):
drop_x = self.xPos + (direction * drop_distance) drop_x = self.xPos + (direction * drop_distance)
self.dropped_item = PowerUp( self.dropped_item = PowerUp(
drop_x, drop_x, self.yPos, item_type, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
self.yPos,
item_type,
self.sounds,
direction,
self.level.leftBoundary,
self.level.rightBoundary
) )
return True return True
+40
View File
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Woohoo! Diemonsterdie!
# The moon is full
# The time is right to shed my evil demon's light on you
# Navigate by pumpkin's light
# This cornfield hides a shallow grave for you
import pygame
from libstormgames import *
from src.object import Object
class DeathSound(Object):
"""Special object that plays enemy death sounds at fixed positions and self-removes."""
def __init__(self, x, y, enemyType, sounds):
# Initialize as a static object with the death sound name
deathSoundName = f"{enemyType}_dies"
super().__init__(x, y, deathSoundName, isStatic=True, isCollectible=False, isHazard=False)
self.sounds = sounds
self.enemyType = enemyType
self.startTime = pygame.time.get_ticks()
# Get the duration of the death sound if it exists
if deathSoundName in sounds:
self.soundDuration = sounds[deathSoundName].get_length() * 1000 # Convert to milliseconds
else:
self.soundDuration = 1000 # Default 1 second if sound doesn't exist
def update(self, currentTime):
"""Check if sound has finished playing and mark for removal."""
if currentTime - self.startTime >= self.soundDuration:
# Sound has finished, deactivate and stop audio
self.isActive = False
if self.channel:
obj_stop(self.channel)
self.channel = None
+38 -42
View File
@@ -6,25 +6,20 @@ from libstormgames import *
from src.object import Object from src.object import Object
from src.powerup import PowerUp from src.powerup import PowerUp
class Enemy(Object): class Enemy(Object):
def __init__(self, xRange, y, enemyType, sounds, level, **kwargs): def __init__(self, xRange, y, enemyType, sounds, level, **kwargs):
# Track when critters should start hunting # Track when critters should start hunting
self.hunting = False self.hunting = False
# Initialize base object properties # Initialize base object properties
super().__init__( super().__init__(xRange, y, f"{enemyType}", isStatic=False, isHazard=True) # Base sound
xRange,
y,
f"{enemyType}", # Base sound
isStatic=False,
isHazard=True
)
# Enemy specific properties # Enemy specific properties
self.enemyType = enemyType self.enemyType = enemyType
self.level = level self.level = level
self.health = kwargs.get('health', 5) # Default 5 HP self.health = kwargs.get("health", 5) # Default 5 HP
self.damage = kwargs.get('damage', 1) # Default 1 damage self.damage = kwargs.get("damage", 1) # Default 1 damage
self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range self.attackRange = kwargs.get("attack_range", 1) # Default 1 tile range
self.sounds = sounds # Store reference to game sounds self.sounds = sounds # Store reference to game sounds
# Movement and behavior properties # Movement and behavior properties
@@ -37,25 +32,25 @@ class Enemy(Object):
self._currentX = self.xRange[0] # Initialize current position self._currentX = self.xRange[0] # Initialize current position
# Add spawn configuration # Add spawn configuration
self.canSpawn = kwargs.get('can_spawn', False) self.canSpawn = kwargs.get("can_spawn", False)
if self.canSpawn: if self.canSpawn:
self.spawnCooldown = kwargs.get('spawn_cooldown', 2000) self.spawnCooldown = kwargs.get("spawn_cooldown", 2000)
self.spawnChance = kwargs.get('spawn_chance', 25) self.spawnChance = kwargs.get("spawn_chance", 25)
self.spawnType = kwargs.get('spawn_type', 'zombie') # Default to zombie for backward compatibility self.spawnType = kwargs.get("spawn_type", "zombie") # Default to zombie for backward compatibility
self.spawnDistance = kwargs.get('spawn_distance', 5) self.spawnDistance = kwargs.get("spawn_distance", 5)
self.lastSpawnTime = 0 self.lastSpawnTime = 0
# Attack pattern configuration # Attack pattern configuration
self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'}) self.attackPattern = kwargs.get("attack_pattern", {"type": "patrol"})
self.turnThreshold = self.attackPattern.get('turn_threshold', 5) self.turnThreshold = self.attackPattern.get("turn_threshold", 5)
# Initialize vulnerability system # Initialize vulnerability system
self.hasVulnerabilitySystem = kwargs.get('has_vulnerability', False) self.hasVulnerabilitySystem = kwargs.get("has_vulnerability", False)
if self.hasVulnerabilitySystem: if self.hasVulnerabilitySystem:
self.isVulnerable = False # Start invulnerable self.isVulnerable = False # Start invulnerable
self.vulnerabilityTimer = pygame.time.get_ticks() self.vulnerabilityTimer = pygame.time.get_ticks()
self.vulnerabilityDuration = kwargs.get('vulnerability_duration', 2000) self.vulnerabilityDuration = kwargs.get("vulnerability_duration", 2000)
self.invulnerabilityDuration = kwargs.get('invulnerability_duration', 5000) self.invulnerabilityDuration = kwargs.get("invulnerability_duration", 5000)
soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType
self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos) self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos)
else: else:
@@ -68,19 +63,17 @@ class Enemy(Object):
self.health = 1 # Easy to kill self.health = 1 # Easy to kill
self.attackCooldown = 1500 # Slower attack rate self.attackCooldown = 1500 # Slower attack rate
elif enemyType == "spider": elif enemyType == "spider":
speedMultiplier = kwargs.get('speed_multiplier', 2.0) speedMultiplier = kwargs.get("speed_multiplier", 2.0)
self.movementSpeed *= speedMultiplier # Spiders are faster self.movementSpeed *= speedMultiplier # Spiders are faster
self.attackPattern = {'type': 'hunter'} # Spiders actively hunt the player self.attackPattern = {"type": "hunter"} # Spiders actively hunt the player
self.turnThreshold = 3 # Spiders turn around quickly to chase player self.turnThreshold = 3 # Spiders turn around quickly to chase player
@property @property
def xPos(self): def xPos(self):
"""Current x position""" """Current x position"""
return self._currentX return self._currentX
@xPos.setter @xPos.setter
def xPos(self, value): def xPos(self, value):
"""Set current x position""" """Set current x position"""
self._currentX = value self._currentX = value
@@ -111,7 +104,9 @@ class Enemy(Object):
self.channel = obj_update(self.channel, player.xPos, self.xPos) self.channel = obj_update(self.channel, player.xPos, self.xPos)
# Check for vulnerability state change # Check for vulnerability state change
if currentTime - self.vulnerabilityTimer >= (self.vulnerabilityDuration if self.isVulnerable else self.invulnerabilityDuration): if currentTime - self.vulnerabilityTimer >= (
self.vulnerabilityDuration if self.isVulnerable else self.invulnerabilityDuration
):
self.isVulnerable = not self.isVulnerable self.isVulnerable = not self.isVulnerable
self.vulnerabilityTimer = currentTime self.vulnerabilityTimer = currentTime
@@ -126,8 +121,7 @@ class Enemy(Object):
self.hunting = True self.hunting = True
# Handle movement based on enemy type and pattern # Handle movement based on enemy type and pattern
if (self.enemyType == "zombie" or if self.enemyType == "zombie" or (self.attackPattern["type"] == "hunter" and self.hunting):
(self.attackPattern['type'] == 'hunter' and self.hunting)):
distanceToPlayer = player.xPos - self.xPos distanceToPlayer = player.xPos - self.xPos
@@ -166,6 +160,10 @@ class Enemy(Object):
# Ensure spawn point is within level boundaries # Ensure spawn point is within level boundaries
spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary)) spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary))
# Set behavior based on game mode
behavior = "hunter" if self.level.levelId == 999 else "patrol"
turn_rate = 2 if self.level.levelId == 999 else 8 # Faster turn rate for survival
# Create new enemy of specified type # Create new enemy of specified type
spawned = Enemy( spawned = Enemy(
[spawnX, spawnX], # Single point range for spawn [spawnX, spawnX], # Single point range for spawn
@@ -175,7 +173,9 @@ class Enemy(Object):
self.level, self.level,
health=4, # Default health for spawned enemies health=4, # Default health for spawned enemies
damage=2, # Default damage for spawned enemies damage=2, # Default damage for spawned enemies
attack_range=1 # Default range for spawned enemies attack_range=1, # Default range for spawned enemies
attack_pattern={"type": behavior},
turn_rate=turn_rate,
) )
# Add to level's enemies # Add to level's enemies
@@ -215,7 +215,8 @@ class Enemy(Object):
def attack(self, currentTime, player): def attack(self, currentTime, player):
"""Perform attack on player""" """Perform attack on player"""
if player.isInvincible: return if player.isInvincible:
return
self.lastAttackTime = currentTime self.lastAttackTime = currentTime
# Play attack sound # Play attack sound
attackSound = f"{self.enemyType}_attack" attackSound = f"{self.enemyType}_attack"
@@ -223,7 +224,7 @@ class Enemy(Object):
self.sounds[attackSound].play() self.sounds[attackSound].play()
# Deal damage to player # Deal damage to player
player.set_health(player.get_health() - self.damage) player.set_health(player.get_health() - self.damage)
self.sounds['player_takes_damage'].play() self.sounds["player_takes_damage"].play()
def take_damage(self, amount): def take_damage(self, amount):
"""Handle enemy taking damage""" """Handle enemy taking damage"""
@@ -251,10 +252,11 @@ class Enemy(Object):
# Award points # Award points
self.level.levelScore += totalPoints self.level.levelScore += totalPoints
# Play death sound if available using positional audio # Create a DeathSound object to play death sound at fixed position
deathSound = f"{self.enemyType}_dies" from src.die_monster_die import DeathSound
if deathSound in self.sounds:
self.channel = obj_play(self.sounds, deathSound, self.level.player.xPos, self.xPos, loop=False) deathSoundObj = DeathSound(self.xPos, self.yPos, self.enemyType, self.sounds)
self.level.objects.append(deathSoundObj)
# Handle witch-specific drops # Handle witch-specific drops
if self.enemyType == "witch": if self.enemyType == "witch":
@@ -270,15 +272,9 @@ class Enemy(Object):
dropX = self.xPos + (direction * dropDistance) dropX = self.xPos + (direction * dropDistance)
droppedItem = PowerUp( droppedItem = PowerUp(
dropX, dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
self.yPos,
itemType,
self.sounds,
direction,
self.level.leftBoundary,
self.level.rightBoundary
) )
self.level.bouncing_items.append(droppedItem) self.level.bouncing_items.append(droppedItem)
# Update stats # Update stats
self.level.player.stats.update_stat('Enemies killed', 1) self.level.player.stats.update_stat("Enemies killed", 1)
+42 -82
View File
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import time import sys
import pygame
from os.path import isdir, join from os.path import isdir, join
from libstormgames import speak
from libstormgames import speak, instruction_menu
def get_available_games(): def get_available_games():
"""Get list of available game directories in levels folder. """Get list of available game directories in levels folder.
@@ -13,12 +14,22 @@ def get_available_games():
list: List of game directory names list: List of game directory names
""" """
try: try:
return [d for d in os.listdir("levels") if isdir(join("levels", d))] # Handle PyInstaller path issues
if hasattr(sys, "_MEIPASS"):
# Running as PyInstaller executable
base_path = sys._MEIPASS
else:
# Running as script
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
levels_path = os.path.join(base_path, "levels")
return [d for d in os.listdir(levels_path) if isdir(join(levels_path, d)) and not d.endswith(".md")]
except FileNotFoundError: except FileNotFoundError:
return [] return []
def selection_menu(sounds, *options): def selection_menu(sounds, *options):
"""Display level selection menu. """Display level selection menu using instruction_menu.
Args: Args:
sounds (dict): Dictionary of loaded sound effects sounds (dict): Dictionary of loaded sound effects
@@ -27,70 +38,8 @@ def selection_menu(sounds, *options):
Returns: Returns:
str: Selected option or None if cancelled str: Selected option or None if cancelled
""" """
loop = True return instruction_menu(sounds, "Select an adventure", *options)
pygame.mixer.stop()
i = 0
j = -1
# Clear any pending events
pygame.event.clear()
speak("Select an adventure")
time.sleep(1.0)
while loop:
if i != j:
speak(options[i])
j = i
pygame.event.pump()
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
return None
if event.key == pygame.K_DOWN and i < len(options) - 1:
i = i + 1
try:
sounds['menu-move'].play()
except:
pass
if event.key == pygame.K_UP and i > 0:
i = i - 1
try:
sounds['menu-move'].play()
except:
pass
if event.key == pygame.K_HOME and i != 0:
i = 0
try:
sounds['menu-move'].play()
except:
pass
if event.key == pygame.K_END and i != len(options) - 1:
i = len(options) - 1
try:
sounds['menu-move'].play()
except:
pass
if event.key == pygame.K_RETURN:
try:
sounds['menu-select'].play()
time.sleep(sounds['menu-select'].get_length())
except:
pass
return options[i]
elif event.type == pygame.QUIT:
return None
pygame.event.pump()
event = pygame.event.clear()
time.sleep(0.001)
def select_game(sounds): def select_game(sounds):
"""Display game selection menu and return chosen game. """Display game selection menu and return chosen game.
@@ -101,37 +50,48 @@ def select_game(sounds):
Returns: Returns:
str: Selected game directory name or None if cancelled str: Selected game directory name or None if cancelled
""" """
availableGames = get_available_games() available_games = get_available_games()
if not availableGames: if not available_games:
speak("No games found in levels directory!") speak("No games found in levels directory!")
return None return None
# Convert directory names to display names (replace underscores with spaces) # Convert directory names to display names (replace underscores with spaces)
menuOptions = [game.replace("_", " ") for game in availableGames] menu_options = [game.replace("_", " ") for game in available_games]
choice = selection_menu(sounds, *menuOptions) choice = selection_menu(sounds, *menu_options)
if choice is None: if choice is None:
return None return None
# Convert display name back to directory name if needed # Convert display name back to directory name if needed
gameDir = choice.replace(" ", "_") game_dir = choice.replace(" ", "_")
if gameDir not in availableGames: if game_dir not in available_games:
gameDir = choice # Use original if conversion doesn't match game_dir = choice # Use original if conversion doesn't match
return gameDir return game_dir
def get_level_path(gameDir, levelNum):
def get_level_path(game_dir, level_num):
"""Get full path to level JSON file. """Get full path to level JSON file.
Args: Args:
gameDir (str): Game directory name game_dir (str): Game directory name
levelNum (int): Level number level_num (int): Level number
Returns: Returns:
str: Full path to level JSON file str: Full path to level JSON file
""" """
if gameDir is None: if game_dir is None:
raise ValueError("gameDir cannot be None") raise ValueError("game_dir cannot be None")
return os.path.join("levels", gameDir, f"{levelNum}.json")
# Handle PyInstaller path issues
if hasattr(sys, "_MEIPASS"):
# Running as PyInstaller executable
base_path = sys._MEIPASS
else:
# Running as script
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
level_path = os.path.join(base_path, "levels", game_dir, f"{level_num}.json")
return level_path
+9 -7
View File
@@ -4,6 +4,7 @@ import pygame
from libstormgames import * from libstormgames import *
from src.object import Object from src.object import Object
class GraspingHands(Object): class GraspingHands(Object):
"""A hazard where the ground crumbles beneath the player as undead hands reach up.""" """A hazard where the ground crumbles beneath the player as undead hands reach up."""
@@ -14,7 +15,7 @@ class GraspingHands(Object):
"", # Empty string so no regular object sound plays "", # Empty string so no regular object sound plays
isStatic=True, isStatic=True,
isCollectible=False, isCollectible=False,
isHazard=True isHazard=True,
) )
self.sounds = sounds self.sounds = sounds
self.delay = delay # Delay in milliseconds before ground starts crumbling self.delay = delay # Delay in milliseconds before ground starts crumbling
@@ -51,7 +52,7 @@ class GraspingHands(Object):
self.isReset = False self.isReset = False
# Play initial warning sound # Play initial warning sound
play_sound(self.sounds['grasping_hands_start']) play_sound(self.sounds["grasping_hands_start"])
speak("The ground crumbles as the dead reach for you.") speak("The ground crumbles as the dead reach for you.")
def reset(self): def reset(self):
@@ -67,7 +68,7 @@ class GraspingHands(Object):
self.crumbleChannel = None self.crumbleChannel = None
# Play the end sound # Play the end sound
play_sound(self.sounds['grasping_hands_end']) play_sound(self.sounds["grasping_hands_end"])
def update(self, currentTime, player): def update(self, currentTime, player):
"""Update the grasping hands trap state""" """Update the grasping hands trap state"""
@@ -92,7 +93,7 @@ class GraspingHands(Object):
# Manage the looping positional audio for the crumbling ground # Manage the looping positional audio for the crumbling ground
if self.crumbleChannel is None or not self.crumbleChannel.get_busy(): if self.crumbleChannel is None or not self.crumbleChannel.get_busy():
# Start the sound if it's not playing # Start the sound if it's not playing
self.crumbleChannel = obj_play(self.sounds, 'grasping_hands', player.xPos, self.crumblePosition) self.crumbleChannel = obj_play(self.sounds, "grasping_hands", player.xPos, self.crumblePosition)
else: else:
# Update the sound position # Update the sound position
self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition) self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition)
@@ -100,8 +101,9 @@ class GraspingHands(Object):
# Check if player is caught by crumbling # Check if player is caught by crumbling
playerCaught = False playerCaught = False
if not player.isJumping: if not player.isJumping:
if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or \ if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or (
(self.crumbleDirection < 0 and player.xPos >= self.crumblePosition): self.crumbleDirection < 0 and player.xPos >= self.crumblePosition
):
playerCaught = True playerCaught = True
if playerCaught: if playerCaught:
@@ -122,6 +124,6 @@ class GraspingHands(Object):
def __del__(self): def __del__(self):
"""Cleanup when object is destroyed""" """Cleanup when object is destroyed"""
# Ensure sound is stopped when object is destroyed # Ensure sound is stopped when object is destroyed
if hasattr(self, 'crumbleChannel') and self.crumbleChannel: if hasattr(self, "crumbleChannel") and self.crumbleChannel:
obj_stop(self.crumbleChannel) obj_stop(self.crumbleChannel)
self.crumbleChannel = None self.crumbleChannel = None
+39 -7
View File
@@ -8,14 +8,11 @@ from src.powerup import PowerUp
class GraveObject(Object): class GraveObject(Object):
def __init__(self, x, y, sounds, item=None, zombieSpawnChance=0): def __init__(self, x, y, sounds, item=None, zombieSpawnChance=0):
super().__init__( super().__init__(
x, y, "grave", x, y, "grave", isStatic=True, isCollectible=False, isHazard=True, zombieSpawnChance=zombieSpawnChance
isStatic=True,
isCollectible=False,
isHazard=True,
zombieSpawnChance=zombieSpawnChance
) )
self.graveItem = item self.graveItem = item
self.isCollected = False # Renamed to match style of isHazard, isStatic etc self.isCollected = False # Renamed to match style of isHazard, isStatic etc
self.isFilled = False # Track if grave has been filled with shovel
self.sounds = sounds self.sounds = sounds
def collect_grave_item(self, player): def collect_grave_item(self, player):
@@ -28,9 +25,44 @@ class GraveObject(Object):
if not self.graveItem or self.isCollected: if not self.graveItem or self.isCollected:
return False return False
# Collect the item if player is ducking # Collect the item if player is ducking, walking (not running), and wielding shovel
if player.isDucking: if (
player.isDucking
and not player.isRunning
and player.currentWeapon
and player.currentWeapon.name == "rusty_shovel"
):
self.isCollected = True # Mark as collected when collection succeeds self.isCollected = True # Mark as collected when collection succeeds
return True return True
return False return False
def can_fill_grave(self, player):
"""Check if grave can be filled with shovel.
Returns:
bool: True if grave can be filled, False otherwise
"""
# Can only fill empty graves (no item) that haven't been filled yet
if self.graveItem or self.isFilled:
return False
# Must be ducking, walking (not running), and wielding shovel
return (
player.isDucking
and not player.isRunning
and player.currentWeapon
and player.currentWeapon.name == "rusty_shovel"
)
def fill_grave(self, player):
"""Fill the grave with dirt using shovel.
Returns:
bool: True if grave was filled successfully
"""
if self.can_fill_grave(player):
self.isFilled = True
self.isHazard = False # No longer a hazard once filled
return True
return False
+4 -5
View File
@@ -6,6 +6,7 @@ from enum import Enum, auto
class ItemType(Enum): class ItemType(Enum):
"""Defines available item types and their properties""" """Defines available item types and their properties"""
GUTS = auto() GUTS = auto()
HAND_OF_GLORY = auto() HAND_OF_GLORY = auto()
JACK_O_LANTERN = auto() JACK_O_LANTERN = auto()
@@ -13,14 +14,12 @@ class ItemType(Enum):
CAULDRON = auto() CAULDRON = auto()
WITCH_BROOM = auto() WITCH_BROOM = auto()
class ItemProperties: class ItemProperties:
"""Manages item properties and availability""" """Manages item properties and availability"""
# Items that can appear in random drops # Items that can appear in random drops
RANDOM_ELIGIBLE = { RANDOM_ELIGIBLE = {ItemType.HAND_OF_GLORY: "hand_of_glory", ItemType.JACK_O_LANTERN: "jack_o_lantern"}
ItemType.HAND_OF_GLORY: "hand_of_glory",
ItemType.JACK_O_LANTERN: "jack_o_lantern"
}
# All possible items (including special ones) # All possible items (including special ones)
ALL_ITEMS = { ALL_ITEMS = {
@@ -29,7 +28,7 @@ class ItemProperties:
ItemType.JACK_O_LANTERN: "jack_o_lantern", ItemType.JACK_O_LANTERN: "jack_o_lantern",
ItemType.EXTRA_LIFE: "extra_life", ItemType.EXTRA_LIFE: "extra_life",
ItemType.CAULDRON: "cauldron", ItemType.CAULDRON: "cauldron",
ItemType.WITCH_BROOM: "witch_broom" ItemType.WITCH_BROOM: "witch_broom",
} }
@staticmethod @staticmethod
+120 -72
View File
@@ -16,15 +16,16 @@ from src.skull_storm import SkullStorm
class Level: class Level:
def __init__(self, levelData, sounds, player): def __init__(self, levelData, sounds, player, levelPackName=None):
self.sounds = sounds self.sounds = sounds
self.levelPackName = levelPackName
self.objects = [] self.objects = []
self.enemies = [] self.enemies = []
self.bouncing_items = [] self.bouncing_items = []
self.projectiles = [] # Track active projectiles self.projectiles = [] # Track active projectiles
self.player = player self.player = player
self.lastWarningTime = 0 self.lastWarningTime = 0
self.warningInterval = int(self.sounds['edge'].get_length() * 1000) # Convert seconds to milliseconds self.warningInterval = int(self.sounds["edge"].get_length() * 1000) # Convert seconds to milliseconds
self.weapon_hit_channel = None self.weapon_hit_channel = None
self.leftBoundary = levelData["boundaries"]["left"] self.leftBoundary = levelData["boundaries"]["left"]
@@ -40,23 +41,35 @@ class Level:
# Pass footstep sound to player # Pass footstep sound to player
self.player.set_footstep_sound(self.footstepSound) self.player.set_footstep_sound(self.footstepSound)
# Level intro message # Level intro message (skip for survival mode)
if levelData["level_id"] != 999: # 999 is survival mode
levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. " levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. "
if self.isLocked: if self.isLocked:
levelIntro += "This is a boss level. You must defeat all enemies before you can advance. " levelIntro += "This is a boss level. You must defeat all enemies before you can advance. "
levelIntro += levelData['description'] levelIntro += levelData["description"]
messagebox(levelIntro) messagebox(levelIntro)
# Handle level music # Handle level music
try: try:
pygame.mixer.music.stop() pygame.mixer.music.stop()
if "ambience" in levelData: if "ambience" in levelData:
ambientFile = levelData["ambience"]
# Build list of paths to try (pack-specific first, then generic)
ambiencePaths = []
if self.levelPackName:
ambiencePaths.append(f"sounds/{self.levelPackName}/ambience/{ambientFile}")
ambiencePaths.append(f"sounds/ambience/{ambientFile}")
# Try each path until one works
for ambiencePath in ambiencePaths:
try: try:
pygame.mixer.music.load(f"sounds/ambience/{levelData['ambience']}") pygame.mixer.music.load(ambiencePath)
pygame.mixer.music.play(-1) # Loop indefinitely pygame.mixer.music.play(-1) # Loop indefinitely
except: break
pass except Exception:
except: continue
except Exception:
pass pass
# Create end of level object at right boundary # Create end of level object at right boundary
@@ -66,7 +79,7 @@ class Level:
"end_of_level", "end_of_level",
isStatic=True, isStatic=True,
isCollectible=False, isCollectible=False,
isHazard=False isHazard=False,
) )
self.objects.append(endLevel) self.objects.append(endLevel)
@@ -85,7 +98,7 @@ class Level:
obj["y"], obj["y"],
self.sounds, self.sounds,
fireInterval=obj.get("fireInterval", 5000), fireInterval=obj.get("fireInterval", 5000),
firingRange=obj.get("range", 20) firingRange=obj.get("range", 20),
) )
self.objects.append(catapult) self.objects.append(catapult)
# Check if this is grasping hands # Check if this is grasping hands
@@ -95,7 +108,7 @@ class Level:
obj["y"], obj["y"],
self.sounds, self.sounds,
delay=obj.get("delay", 1000), delay=obj.get("delay", 1000),
crumble_speed=obj.get("crumble_speed", 0.03) crumble_speed=obj.get("crumble_speed", 0.03),
) )
self.objects.append(graspingHands) self.objects.append(graspingHands)
# Check if this is a grave # Check if this is a grave
@@ -105,7 +118,7 @@ class Level:
obj["y"], obj["y"],
self.sounds, self.sounds,
item=obj.get("item", None), item=obj.get("item", None),
zombieSpawnChance=obj.get("zombie_spawn_chance", 0) zombieSpawnChance=obj.get("zombie_spawn_chance", 0),
) )
self.objects.append(grave) self.objects.append(grave)
# Check if this is a skull storm # Check if this is a skull storm
@@ -117,7 +130,7 @@ class Level:
obj.get("damage", 5), obj.get("damage", 5),
obj.get("maximum_skulls", 3), obj.get("maximum_skulls", 3),
obj.get("frequency", {}).get("min", 2), obj.get("frequency", {}).get("min", 2),
obj.get("frequency", {}).get("max", 5) obj.get("frequency", {}).get("max", 5),
) )
self.objects.append(skullStorm) self.objects.append(skullStorm)
# Check if this is a coffin # Check if this is a coffin
@@ -127,7 +140,7 @@ class Level:
obj["y"], obj["y"],
self.sounds, self.sounds,
self, # Pass level reference self, # Pass level reference
item=obj.get("item", "random") # Get item type or default to random item=obj.get("item", "random"), # Get item type or default to random
) )
self.objects.append(coffin) self.objects.append(coffin)
# Check if this is a spider web # Check if this is a spider web
@@ -135,8 +148,7 @@ class Level:
# Check distance from graves # Check distance from graves
isValidPosition = True isValidPosition = True
for existingObj in self.objects: for existingObj in self.objects:
if (existingObj.soundName == "grave" and if existingObj.soundName == "grave" and not hasattr(existingObj, "graveItem"):
not hasattr(existingObj, 'graveItem')):
distance = abs(obj["x"] - existingObj.xPos) distance = abs(obj["x"] - existingObj.xPos)
if distance < 3: if distance < 3:
isValidPosition = False isValidPosition = False
@@ -163,7 +175,7 @@ class Level:
damage=obj.get("damage", 1), damage=obj.get("damage", 1),
attack_range=obj.get("attack_range", 1), attack_range=obj.get("attack_range", 1),
movement_range=obj.get("movement_range", 5), movement_range=obj.get("movement_range", 5),
attack_pattern=obj.get("attack_pattern", {'type': 'patrol'}), attack_pattern=obj.get("attack_pattern", {"type": "patrol"}),
can_spawn=obj.get("can_spawn", False), can_spawn=obj.get("can_spawn", False),
spawn_type=obj.get("spawn_type", "zombie"), spawn_type=obj.get("spawn_type", "zombie"),
spawn_cooldown=obj.get("spawn_cooldown", 2000), spawn_cooldown=obj.get("spawn_cooldown", 2000),
@@ -172,7 +184,7 @@ class Level:
has_vulnerability=obj.get("has_vulnerability", False), has_vulnerability=obj.get("has_vulnerability", False),
is_vulnerable=obj.get("is_vulnerable", False), is_vulnerable=obj.get("is_vulnerable", False),
vulnerability_duration=obj.get("vulnerability_duration", 1000), vulnerability_duration=obj.get("vulnerability_duration", 1000),
invulnerability_duration=obj.get("invulnerability_duration", 5000) invulnerability_duration=obj.get("invulnerability_duration", 5000),
) )
self.enemies.append(enemy) self.enemies.append(enemy)
else: else:
@@ -183,13 +195,13 @@ class Level:
isStatic=obj.get("static", True), isStatic=obj.get("static", True),
isCollectible=obj.get("collectible", False), isCollectible=obj.get("collectible", False),
isHazard=obj.get("hazard", False), isHazard=obj.get("hazard", False),
zombieSpawnChance=obj.get("zombieSpawnChance", 0) zombieSpawnChance=obj.get("zombieSpawnChance", 0),
) )
self.objects.append(gameObject) self.objects.append(gameObject)
enemyCount = len(self.enemies) enemyCount = len(self.enemies)
coffinCount = sum(1 for obj in self.objects if hasattr(obj, 'isBroken')) coffinCount = sum(1 for obj in self.objects if hasattr(obj, "isBroken"))
player.stats.update_stat('Enemies remaining', enemyCount) player.stats.update_stat("Enemies remaining", enemyCount)
player.stats.update_stat('Coffins remaining', coffinCount) player.stats.update_stat("Coffins remaining", coffinCount)
def update_audio(self): def update_audio(self):
"""Update all audio and entity state.""" """Update all audio and entity state."""
@@ -201,9 +213,7 @@ class Level:
continue continue
# Check for potential zombie spawn from graves # Check for potential zombie spawn from graves
if (obj.soundName == "grave" and if obj.soundName == "grave" and obj.zombieSpawnChance > 0 and not obj.hasSpawned:
obj.zombieSpawnChance > 0 and
not obj.hasSpawned):
distance = abs(self.player.xPos - obj.xPos) distance = abs(self.player.xPos - obj.xPos)
if distance < 6: # Within 6 tiles if distance < 6: # Within 6 tiles
@@ -212,6 +222,10 @@ class Level:
roll = random.randint(1, 100) roll = random.randint(1, 100)
if roll <= obj.zombieSpawnChance: if roll <= obj.zombieSpawnChance:
# Set behavior based on game mode
behavior = "hunter" if self.levelId == 999 else "patrol"
turn_rate = 2 if self.levelId == 999 else 8 # Faster turn rate for survival
zombie = Enemy( zombie = Enemy(
[obj.xPos, obj.xPos], [obj.xPos, obj.xPos],
obj.yPos, obj.yPos,
@@ -220,7 +234,9 @@ class Level:
self, # Pass the level reference self, # Pass the level reference
health=3, health=3,
damage=10, damage=10,
attack_range=1 attack_range=1,
attack_pattern={"type": behavior},
turn_rate=turn_rate,
) )
self.enemies.append(zombie) self.enemies.append(zombie)
speak("A zombie emerges from the grave!") speak("A zombie emerges from the grave!")
@@ -265,6 +281,16 @@ class Level:
if caught: if caught:
return # Stop if player is dead return # Stop if player is dead
# Update death sound objects
from src.die_monster_die import DeathSound
for obj in self.objects:
if isinstance(obj, DeathSound):
obj.update(currentTime)
# Clean up inactive objects (including finished death sounds)
self.objects = [obj for obj in self.objects if obj.isActive]
# Update bouncing items # Update bouncing items
for item in self.bouncing_items[:]: # Copy list to allow removal for item in self.bouncing_items[:]: # Copy list to allow removal
if not item.update(currentTime, self.player.xPos): if not item.update(currentTime, self.player.xPos):
@@ -275,8 +301,8 @@ class Level:
# Check for item collection # Check for item collection
if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping: if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping:
play_sound(self.sounds[f'get_{item.soundName}']) play_sound(self.sounds[f"get_{item.soundName}"])
item.apply_effect(self.player) item.apply_effect(self.player, self)
self.levelScore += 1000 # All items collected points awarded self.levelScore += 1000 # All items collected points awarded
item.isActive = False item.isActive = False
self.bouncing_items.remove(item) self.bouncing_items.remove(item)
@@ -297,11 +323,13 @@ class Level:
# Check for coffin hits # Check for coffin hits
for obj in self.objects: for obj in self.objects:
if hasattr(obj, 'isBroken'): # Check if it's a coffin without using isinstance if hasattr(obj, "isBroken"): # Check if it's a coffin without using isinstance
if (not obj.isBroken and if (
obj.xPos >= attackRange[0] and not obj.isBroken
obj.xPos <= attackRange[1] and and obj.xPos >= attackRange[0]
self.player.isJumping): # Must be jumping to hit floating coffins and obj.xPos <= attackRange[1]
and self.player.isJumping
): # Must be jumping to hit floating coffins
if obj.hit(self.player.xPos): if obj.hit(self.player.xPos):
self.bouncing_items.append(obj.dropped_item) self.bouncing_items.append(obj.dropped_item)
@@ -317,7 +345,7 @@ class Level:
health=8, health=8,
damage=8, damage=8,
attack_range=1, attack_range=1,
speed_multiplier=2.0 speed_multiplier=2.0,
) )
self.enemies.append(spider) self.enemies.append(spider)
@@ -336,62 +364,71 @@ class Level:
continue continue
# Handle grave edge warnings # Handle grave edge warnings
if obj.isHazard and obj.soundName != "spiderweb" and not isinstance(obj, GraspingHands): # Exclude spiderwebs and grasping hands if (
obj.isHazard and obj.soundName != "spiderweb" and not isinstance(obj, GraspingHands)
): # Exclude spiderwebs and grasping hands
distance = abs(self.player.xPos - obj.xPos) distance = abs(self.player.xPos - obj.xPos)
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
if (distance <= 2 and not self.player.isJumping and not self.player.isInvincible if (
and currentTime - self.lastWarningTime >= self.warningInterval): distance <= 2
and not self.player.isJumping
and not self.player.isInvincible
and currentTime - self.lastWarningTime >= self.warningInterval
):
if isinstance(obj, GraveObject) and obj.graveItem and not obj.isCollected: if isinstance(obj, GraveObject) and obj.graveItem and not obj.isCollected:
play_sound(self.sounds['_edge']) play_sound(self.sounds["_edge"])
else: else:
play_sound(self.sounds['edge']) play_sound(self.sounds["edge"])
self.lastWarningTime = currentTime self.lastWarningTime = currentTime
if not obj.is_in_range(self.player.xPos): if not obj.is_in_range(self.player.xPos):
continue continue
# Handle collectibles # Handle collectibles (but not if player died this frame)
if obj.isCollectible and self.player.isJumping: if obj.isCollectible and self.player.isJumping and not self.player.diedThisFrame:
currentPos = round(self.player.xPos) currentPos = round(self.player.xPos)
if currentPos not in obj.collectedPositions: if currentPos not in obj.collectedPositions:
play_sound(self.sounds[f'get_{obj.soundName}']) play_sound(self.sounds[f"get_{obj.soundName}"])
obj.collect_at_position(currentPos) obj.collect_at_position(currentPos)
self.player.collectedItems.append(obj.soundName) self.player.collectedItems.append(obj.soundName)
self.player.stats.update_stat('Items collected', 1) self.player.stats.update_stat("Items collected", 1)
if obj.soundName == "bone_dust": if obj.soundName == "bone_dust":
self.player._coins += 1 self.player._coins += 1
self.player.add_save_bone_dust(1) # Add to save bone dust counter too self.player.add_save_bone_dust(1) # Add to save bone dust counter too
self.levelScore += 100 self.levelScore += 100
self.player.stats.update_stat('Bone dust', 1) self.player.stats.update_stat("Bone dust", 1)
if self.player._coins % 5 == 0: if self.player._coins % 5 == 0:
# Only heal if below max health # Only heal if below max health
if self.player.get_health() < self.player.get_max_health(): if self.player.get_health() < self.player.get_max_health():
self.player.set_health(min( self.player.set_health(min(self.player.get_health() + 1, self.player.get_max_health()))
self.player.get_health() + 1,
self.player.get_max_health()
))
if self.player._coins % 100 == 0: if self.player._coins % 100 == 0:
# Only give extra lives in story mode, not survival mode (level_id 999)
if self.levelId != 999:
# Extra life # Extra life
self.player._coins = 0 self.player._coins = 0
self.player._lives += 1 self.player._lives += 1
self.levelScore += 1000 self.levelScore += 1000
play_sound(self.sounds['get_extra_life']) play_sound(self.sounds["get_extra_life"])
else:
# In survival mode, reset coin counter but give bonus score instead
self.player._coins = 0
self.levelScore += 2000 # Double score bonus instead of extra life
speak("100 bone dust collected! Bonus score!")
play_sound(
self.sounds.get("survivor_bonus", "bone_dust")
) # Use survivor_bonus sound if available, fallback to bone_dust
continue continue
# Handle spiderweb - this should trigger for both walking and jumping if not ducking # Handle spiderweb - this should trigger for both walking and jumping if not ducking
if obj.soundName == "spiderweb" and not self.player.isDucking: if obj.soundName == "spiderweb" and not self.player.isDucking:
# Create and apply web effect # Create and apply web effect
webEffect = PowerUp( webEffect = PowerUp(
obj.xPos, obj.xPos, obj.yPos, "spiderweb", self.sounds, 0 # No direction needed since it's just for effect
obj.yPos,
'spiderweb',
self.sounds,
0 # No direction needed since it's just for effect
) )
webEffect.level = self # Pass level reference for spider spawning webEffect.level = self # Pass level reference for spider spawning
play_sound(self.sounds['hit_spiderweb']) play_sound(self.sounds["hit_spiderweb"])
webEffect.apply_effect(self.player) webEffect.apply_effect(self.player, self)
# Deactivate web # Deactivate web
obj.isActive = False obj.isActive = False
@@ -401,16 +438,19 @@ class Level:
# Handle graves and other hazards # Handle graves and other hazards
if obj.isHazard and not self.player.isJumping: if obj.isHazard and not self.player.isJumping:
if isinstance(obj, GraveObject): if isinstance(obj, GraveObject):
can_collect = obj.collect_grave_item(self.player) can_collect = obj.collect_grave_item(self.player) and not self.player.diedThisFrame
can_fill = obj.can_fill_grave(self.player)
if can_collect: if can_collect:
# Successfully collected item while ducking # Successfully collected item while ducking with shovel
play_sound(self.sounds[f'get_{obj.graveItem}']) play_sound(self.sounds[f"get_{obj.graveItem}"])
self.player.stats.update_stat('Items collected', 1) play_sound(self.sounds.get("fill_in_grave", "shovel_dig")) # Also play fill sound
self.player.stats.update_stat("Items collected", 1)
# Create PowerUp to handle the item effect # Create PowerUp to handle the item effect
item = PowerUp(obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1, item = PowerUp(
self.leftBoundary, self.rightBoundary) obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1, self.leftBoundary, self.rightBoundary
item.apply_effect(self.player) )
item.apply_effect(self.player, self)
# Stop grave's current audio channel # Stop grave's current audio channel
if obj.channel: if obj.channel:
obj_stop(obj.channel) obj_stop(obj.channel)
@@ -419,6 +459,18 @@ class Level:
obj.channel = None obj.channel = None
obj.isActive = False # Mark the grave as inactive after collection obj.isActive = False # Mark the grave as inactive after collection
continue continue
elif can_fill and obj.fill_grave(self.player):
# Successfully filled empty grave with shovel
play_sound(
self.sounds.get("fill_in_grave", "shovel_dig")
) # Use fill_in_grave sound if available, fallback to shovel_dig
self.player.stats.update_stat("Graves filled", 1)
# Stop grave's current audio channel
if obj.channel:
obj_stop(obj.channel)
obj.channel = None
obj.isActive = False # Mark grave as inactive after filling
continue
elif not self.player.isInvincible: elif not self.player.isInvincible:
# Kill player for normal graves or non-ducking collision # Kill player for normal graves or non-ducking collision
play_sound(self.sounds[obj.soundName]) play_sound(self.sounds[obj.soundName])
@@ -440,14 +492,14 @@ class Level:
# If level is locked, check for remaining enemies # If level is locked, check for remaining enemies
if self.isLocked and any(enemy.isActive for enemy in self.enemies): if self.isLocked and any(enemy.isActive for enemy in self.enemies):
speak("You must defeat all enemies before proceeding!") speak("You must defeat all enemies before proceeding!")
play_sound(self.sounds['locked']) play_sound(self.sounds["locked"])
# Push player back a bit # Push player back a bit
self.player.xPos -= 5 self.player.xPos -= 5
return False return False
# Level complete # Level complete
pygame.mixer.stop() pygame.mixer.stop()
play_sound(self.sounds['end_of_level']) play_sound(self.sounds["end_of_level"])
self.levelScore += 10000 self.levelScore += 10000
# Actually update the scoreboard with level completion # Actually update the scoreboard with level completion
self.player.scoreboard.increase_score(self.levelScore) self.player.scoreboard.increase_score(self.levelScore)
@@ -470,7 +522,7 @@ class Level:
# Calculate volume and pan for splat sound based on final position # Calculate volume and pan for splat sound based on final position
volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x) volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x)
if volume > 0: # Only play if within audible range if volume > 0: # Only play if within audible range
obj_play(self.sounds, 'pumpkin_splat', self.player.xPos, proj.x, loop=False) obj_play(self.sounds, "pumpkin_splat", self.player.xPos, proj.x, loop=False)
break break
def throw_projectile(self): def throw_projectile(self):
@@ -480,10 +532,6 @@ class Level:
speak("No jack o'lanterns to throw!") speak("No jack o'lanterns to throw!")
return return
self.projectiles.append(Projectile( self.projectiles.append(Projectile(proj_info["type"], proj_info["start_x"], proj_info["direction"]))
proj_info['type'],
proj_info['start_x'],
proj_info['direction']
))
# Play throw sound # Play throw sound
play_sound(self.sounds['throw_jack_o_lantern']) play_sound(self.sounds["throw_jack_o_lantern"])
+1
View File
@@ -2,6 +2,7 @@
from libstormgames import * from libstormgames import *
class Object: class Object:
def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False, zombieSpawnChance=0): def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False, zombieSpawnChance=0):
# x can be either a single position or a range [start, end] # x can be either a single position or a range [start, end]
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Pack-specific sound system for Wicked Quest.
Provides hierarchical sound loading that checks pack-specific sounds first,
then falls back to generic sounds, without modifying libstormgames.
"""
import os
import pygame
from libstormgames.sound import Sound
class PackSoundSystem(dict):
"""Sound system with hierarchical pack-specific loading."""
def __init__(self, originalSounds, soundDir="sounds/", levelPackName=None):
"""Initialize pack-specific sound system.
Args:
originalSounds (dict): Original sound dictionary from initialize_gui
soundDir (str): Base sound directory
levelPackName (str): Name of level pack for pack-specific sounds
"""
# Initialize dict with original sounds
super().__init__(originalSounds)
self.soundDir = soundDir
self.levelPackName = levelPackName
# Load pack-specific sounds if pack name provided
if levelPackName:
self._load_pack_sounds()
def _load_pack_sounds(self):
"""Load pack-specific sounds from sounds/[pack_name]/ directory."""
packSoundDir = os.path.join(self.soundDir, self.levelPackName)
if not os.path.exists(packSoundDir):
return
try:
for dirPath, _, fileNames in os.walk(packSoundDir):
relPath = os.path.relpath(dirPath, packSoundDir)
for fileName in fileNames:
if fileName.lower().endswith((".ogg", ".wav")):
fullPath = os.path.join(dirPath, fileName)
baseName = os.path.splitext(fileName)[0]
# Create sound key same as base system
soundKey = baseName if relPath == "." else os.path.join(relPath, baseName).replace("\\", "/")
# Add/override sound in the main dictionary
self[soundKey] = pygame.mixer.Sound(fullPath)
except Exception as e:
print(f"Error loading pack sounds: {e}")
+70 -26
View File
@@ -59,28 +59,35 @@ class Player:
self.invincibilityStartTime = 0 self.invincibilityStartTime = 0
self.invincibilityDuration = 10000 # 10 seconds of invincibility self.invincibilityDuration = 10000 # 10 seconds of invincibility
# Death state tracking (to prevent revival after death in same frame)
self.diedThisFrame = False
# Initialize starting weapon (rusty shovel) # Initialize starting weapon (rusty shovel)
self.add_weapon(Weapon( self.add_weapon(
Weapon(
name="rusty_shovel", name="rusty_shovel",
damage=2, damage=2,
range=2, range=2,
attackSound="player_shovel_attack", attackSound="player_shovel_attack",
hitSound="player_shovel_hit", hitSound="player_shovel_hit",
attackDuration=200 # 200ms attack duration attackDuration=200, # 200ms attack duration
)) )
)
self.scoreboard = Scoreboard() self.scoreboard = Scoreboard()
def should_play_footstep(self, currentTime): def should_play_footstep(self, currentTime):
"""Check if it's time to play a footstep sound""" """Check if it's time to play a footstep sound"""
return (self.distanceSinceLastStep >= self.get_step_distance() and return (
currentTime - self.lastStepTime >= self.get_step_interval()) self.distanceSinceLastStep >= self.get_step_distance()
and currentTime - self.lastStepTime >= self.get_step_interval()
)
def duck(self): def duck(self):
"""Start ducking""" """Start ducking"""
if not self.isDucking and not self.isJumping: # Can't duck while jumping if not self.isDucking and not self.isJumping: # Can't duck while jumping
self.isDucking = True self.isDucking = True
play_sound(self.sounds['duck']) play_sound(self.sounds["duck"])
return True return True
return False return False
@@ -88,11 +95,14 @@ class Player:
"""Stop ducking state and play sound""" """Stop ducking state and play sound"""
if self.isDucking: if self.isDucking:
self.isDucking = False self.isDucking = False
play_sound(self.sounds['stand']) play_sound(self.sounds["stand"])
def update(self, currentTime): def update(self, currentTime):
"""Update player state""" """Update player state"""
if hasattr(self, 'webPenaltyEndTime'): # Reset death flag at start of each frame
self.diedThisFrame = False
if hasattr(self, "webPenaltyEndTime"):
if currentTime >= self.webPenaltyEndTime: if currentTime >= self.webPenaltyEndTime:
self.moveSpeed *= 2 # Restore speed self.moveSpeed *= 2 # Restore speed
if self.currentWeapon: if self.currentWeapon:
@@ -101,15 +111,17 @@ class Player:
# Check invincibility status # Check invincibility status
if self.isInvincible: if self.isInvincible:
remaining_time = (self.invincibilityStartTime + self.invincibilityDuration - currentTime) / 1000 # Convert to seconds remaining_time = (
self.invincibilityStartTime + self.invincibilityDuration - currentTime
) / 1000 # Convert to seconds
# Handle countdown sounds # Handle countdown sounds
if not hasattr(self, '_last_countdown'): if not hasattr(self, "_last_countdown"):
self._last_countdown = 4 # Start counting from 4 to catch 3,2,1 self._last_countdown = 4 # Start counting from 4 to catch 3,2,1
current_second = int(remaining_time) current_second = int(remaining_time)
if current_second < self._last_countdown and current_second <= 3 and current_second > 0: if current_second < self._last_countdown and current_second <= 3 and current_second > 0:
play_sound(self.sounds['end_of_invincibility_warning']) play_sound(self.sounds["end_of_invincibility_warning"])
self._last_countdown = current_second self._last_countdown = current_second
# Check if invincibility has expired # Check if invincibility has expired
@@ -122,7 +134,7 @@ class Player:
"""Activate invincibility from Hand of Glory""" """Activate invincibility from Hand of Glory"""
self.isInvincible = True self.isInvincible = True
self.invincibilityStartTime = pygame.time.get_ticks() self.invincibilityStartTime = pygame.time.get_ticks()
if hasattr(self, '_last_countdown'): if hasattr(self, "_last_countdown"):
del self._last_countdown # Reset countdown if it exists del self._last_countdown # Reset countdown if it exists
def extra_life(self): def extra_life(self):
@@ -150,23 +162,27 @@ class Player:
return None return None
self._jack_o_lantern_count -= 1 self._jack_o_lantern_count -= 1
return { return {"type": "jack_o_lantern", "start_x": self.xPos, "direction": 1 if self.facingRight else -1}
'type': 'jack_o_lantern',
'start_x': self.xPos,
'direction': 1 if self.facingRight else -1
}
def get_step_distance(self): def get_step_distance(self):
"""Get step distance based on current speed""" """Get step distance based on current speed"""
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
totalMultiplier = weaponBonus
if self.isRunning or self.isJumping: if self.isRunning or self.isJumping:
return self.baseStepDistance / self.runMultiplier totalMultiplier *= self.runMultiplier
return self.baseStepDistance
return self.baseStepDistance / totalMultiplier
def get_step_interval(self): def get_step_interval(self):
"""Get minimum time between steps based on current speed""" """Get minimum time between steps based on current speed"""
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
totalMultiplier = weaponBonus
if self.isRunning or self.isJumping: if self.isRunning or self.isJumping:
return self.baseStepInterval / self.runMultiplier totalMultiplier *= self.runMultiplier
return self.baseStepInterval
return self.baseStepInterval / totalMultiplier
def get_health(self): def get_health(self):
"""Get current health""" """Get current health"""
@@ -181,10 +197,18 @@ class Player:
self._health = self._maxHealth self._health = self._maxHealth
def get_current_speed(self): def get_current_speed(self):
"""Calculate current speed based on state""" """Calculate current speed based on state and weapon"""
baseSpeed = self.moveSpeed baseSpeed = self.moveSpeed
if self.isJumping or self.isRunning: return baseSpeed * self.runMultiplier weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
return baseSpeed
if self.isJumping or self.isRunning:
return baseSpeed * self.runMultiplier * weaponBonus
return baseSpeed * weaponBonus
def get_current_jump_duration(self):
"""Calculate current jump duration based on weapon bonus"""
weaponBonus = self.currentWeapon.jumpDurationBonus if self.currentWeapon else 1.0
return int(self.jumpDuration * weaponBonus)
def set_footstep_sound(self, soundName): def set_footstep_sound(self, soundName):
"""Set the current footstep sound""" """Set the current footstep sound"""
@@ -202,14 +226,16 @@ class Player:
if self._health == 0 and old_health > 0: if self._health == 0 and old_health > 0:
self._lives -= 1 self._lives -= 1
# Mark that player died this frame to prevent revival
self.diedThisFrame = True
# Stop all current sounds before playing death sound # Stop all current sounds before playing death sound
pygame.mixer.stop() pygame.mixer.stop()
try: try:
pygame.mixer.music.stop() pygame.mixer.music.stop()
except: except Exception:
pass pass
cut_scene(self.sounds, 'lose_a_life') cut_scene(self.sounds, "lose_a_life")
def set_max_health(self, value): def set_max_health(self, value):
"""Set max health""" """Set max health"""
@@ -253,6 +279,24 @@ class Player:
if weapon in self.weapons: if weapon in self.weapons:
self.currentWeapon = weapon self.currentWeapon = weapon
def switch_to_weapon(self, weaponIndex):
"""Switch to weapon by index (1=shovel, 2=broom, 3=nunchucks)"""
weaponMap = {1: "rusty_shovel", 2: "witch_broom", 3: "nunchucks"}
targetWeaponName = weaponMap.get(weaponIndex)
if not targetWeaponName:
return False
# Find the weapon in player's inventory
for weapon in self.weapons:
if weapon.name == targetWeaponName:
self.equip_weapon(weapon)
speak(weapon.name.replace("_", " "))
return True
# Weapon not found in inventory
return False
def add_item(self, item): def add_item(self, item):
"""Add an item to inventory""" """Add an item to inventory"""
self.inventory.append(item) self.inventory.append(item)
+48 -27
View File
@@ -5,14 +5,10 @@ from libstormgames import *
from src.object import Object from src.object import Object
from src.weapon import Weapon from src.weapon import Weapon
class PowerUp(Object): class PowerUp(Object):
def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100): def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100):
super().__init__( super().__init__(x, y, item_type, isStatic=False, isCollectible=True, isHazard=False)
x, y, item_type,
isStatic=False,
isCollectible=True,
isHazard=False
)
self.sounds = sounds self.sounds = sounds
self.direction = direction self.direction = direction
self.speed = 0.049 # Base movement speed self.speed = 0.049 # Base movement speed
@@ -56,35 +52,58 @@ class PowerUp(Object):
return True return True
def apply_effect(self, player): def apply_effect(self, player, level=None):
"""Apply the item's effect when collected""" """Apply the item's effect when collected"""
if self.item_type == 'hand_of_glory': if self.item_type == "hand_of_glory":
player.start_invincibility() player.start_invincibility()
elif self.item_type == 'cauldron': elif self.item_type == "cauldron":
player.restore_health() player.restore_health()
elif self.item_type == 'guts': elif self.item_type == "guts":
player.add_guts() player.add_guts()
player.collectedItems.append('guts') player.collectedItems.append("guts")
self.check_for_nunchucks(player) self.check_for_nunchucks(player)
elif self.item_type == 'jack_o_lantern': elif self.item_type == "jack_o_lantern":
player.add_jack_o_lantern() player.add_jack_o_lantern()
elif self.item_type == 'extra_life': elif self.item_type == "extra_life":
# Don't give extra lives in survival mode
if level and level.levelId == 999:
# In survival mode, give bonus score instead
level.levelScore += 2000
speak("Extra life found! Bonus score in survival mode!")
play_sound(self.sounds.get("survivor_bonus", "get_extra_life")) # Use survivor_bonus sound if available
else:
player.extra_life() player.extra_life()
elif self.item_type == 'shin_bone': # Add shin bone handling elif self.item_type == "shin_bone": # Add shin bone handling
player.shinBoneCount += 1 player.shinBoneCount += 1
player._coins += 5 player._coins += 5
player.add_save_bone_dust(5) # Add to save bone dust counter too player.add_save_bone_dust(5) # Add to save bone dust counter too
if player.get_health() < player.get_max_health(): if player.get_health() < player.get_max_health():
player.set_health(min( player.set_health(min(player.get_health() + 1, player.get_max_health()))
player.get_health() + 1,
player.get_max_health() # Check for 100 coin bonus after adding shin bone coins
)) if player._coins >= 100:
# Only give extra lives in story mode, not survival mode (level_id 999)
if level.levelId != 999:
# Extra life
player._coins = 0
player._lives += 1
level.levelScore += 1000
play_sound(self.sounds["get_extra_life"])
else:
# In survival mode, reset coin counter but give bonus score instead
player._coins = 0
level.levelScore += 2000 # Double score bonus instead of extra life
speak("100 bone dust collected! Bonus score!")
play_sound(
self.sounds.get("survivor_bonus", "bone_dust")
) # Use survivor_bonus sound if available, fallback to bone_dust
self.check_for_nunchucks(player) self.check_for_nunchucks(player)
elif self.item_type == 'witch_broom': elif self.item_type == "witch_broom":
broomWeapon = Weapon.create_witch_broom() broomWeapon = Weapon.create_witch_broom()
player.add_weapon(broomWeapon) player.add_weapon(broomWeapon)
player.equip_weapon(broomWeapon) player.equip_weapon(broomWeapon)
elif self.item_type == 'spiderweb': elif self.item_type == "spiderweb":
# Bounce player back (happens even if invincible) # Bounce player back (happens even if invincible)
player.xPos -= 3 if player.xPos > self.xPos else -3 player.xPos -= 3 if player.xPos > self.xPos else -3
@@ -98,7 +117,7 @@ class PowerUp(Object):
player.webPenaltyEndTime = pygame.time.get_ticks() + 15000 player.webPenaltyEndTime = pygame.time.get_ticks() + 15000
# Tell level to spawn a spider # Tell level to spawn a spider
if hasattr(self, 'level'): if hasattr(self, "level"):
self.level.spawn_spider(self.xPos, self.yPos) self.level.spawn_spider(self.xPos, self.yPos)
# Stop movement sound when collected # Stop movement sound when collected
@@ -107,18 +126,20 @@ class PowerUp(Object):
self.channel = None self.channel = None
# Item tracking # Item tracking
player.stats.update_stat('Items collected', 1) player.stats.update_stat("Items collected", 1)
def check_for_nunchucks(self, player): def check_for_nunchucks(self, player):
"""Check if player has materials for nunchucks and create if conditions are met""" """Check if player has materials for nunchucks and create if conditions are met"""
if (player.shinBoneCount >= 2 and if (
'guts' in player.collectedItems and player.shinBoneCount >= 2
not any(weapon.name == "nunchucks" for weapon in player.weapons)): and "guts" in player.collectedItems
and not any(weapon.name == "nunchucks" for weapon in player.weapons)
):
nunchucksWeapon = Weapon.create_nunchucks() nunchucksWeapon = Weapon.create_nunchucks()
player.add_weapon(nunchucksWeapon) player.add_weapon(nunchucksWeapon)
player.equip_weapon(nunchucksWeapon) player.equip_weapon(nunchucksWeapon)
basePoints = nunchucksWeapon.damage * 1000 basePoints = nunchucksWeapon.damage * 1000
rangeModifier = nunchucksWeapon.range * 500 rangeModifier = nunchucksWeapon.range * 500
player.scoreboard.increase_score(basePoints + rangeModifier) player.scoreboard.increase_score(basePoints + rangeModifier)
play_sound(self.sounds['get_nunchucks']) play_sound(self.sounds["get_nunchucks"])
player.stats.update_stat('Items collected', 1) player.stats.update_stat("Items collected", 1)
+1
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
class Projectile: class Projectile:
def __init__(self, projectile_type, start_x, direction): def __init__(self, projectile_type, start_x, direction):
self.type = projectile_type self.type = projectile_type
+114 -95
View File
@@ -11,8 +11,8 @@ class SaveManager:
def __init__(self): def __init__(self):
"""Initialize save manager with XDG-compliant save directory""" """Initialize save manager with XDG-compliant save directory"""
# Use XDG_CONFIG_HOME or default to ~/.config # Use XDG_CONFIG_HOME or default to ~/.config
config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
self.save_dir = Path(config_home) / 'storm-games' / 'wicked-quest' self.save_dir = Path(config_home) / "storm-games" / "wicked-quest"
self.save_dir.mkdir(parents=True, exist_ok=True) self.save_dir.mkdir(parents=True, exist_ok=True)
self.max_saves = 10 self.max_saves = 10
@@ -34,30 +34,30 @@ class SaveManager:
# Create save data # Create save data
save_data = { save_data = {
'player_state': { "player_state": {
'xPos': player.xPos, "xPos": player.xPos,
'yPos': player.yPos, "yPos": player.yPos,
'health': player._health, "health": player._health,
'maxHealth': player._maxHealth, "maxHealth": player._maxHealth,
'lives': player._lives, "lives": player._lives,
'coins': player._coins, "coins": player._coins,
'saveBoneDust': player._saveBoneDust, "saveBoneDust": player._saveBoneDust,
'jackOLanternCount': player._jack_o_lantern_count, "jackOLanternCount": player._jack_o_lantern_count,
'shinBoneCount': player.shinBoneCount, "shinBoneCount": player.shinBoneCount,
'inventory': player.inventory, "inventory": player.inventory,
'collectedItems': player.collectedItems, "collectedItems": player.collectedItems,
'weapons': self._serialize_weapons(player.weapons), "weapons": self._serialize_weapons(player.weapons),
'currentWeaponName': player.currentWeapon.name if player.currentWeapon else None, "currentWeaponName": player.currentWeapon.name if player.currentWeapon else None,
'stats': self._serialize_stats(player.stats), "stats": self._serialize_stats(player.stats),
'scoreboard': self._serialize_scoreboard(player.scoreboard) "scoreboard": self._serialize_scoreboard(player.scoreboard),
}, },
'game_state': { "game_state": {
'currentLevel': current_level, "currentLevel": current_level,
'currentGame': current_game, "currentGame": current_game,
'gameStartTime': game_start_time, "gameStartTime": game_start_time,
'saveTime': datetime.now() "saveTime": datetime.now(),
}, },
'version': '1.0' "version": "1.0",
} }
# Generate filename with timestamp # Generate filename with timestamp
@@ -67,9 +67,9 @@ class SaveManager:
try: try:
# Write to temporary file first, then rename for atomic operation # Write to temporary file first, then rename for atomic operation
temp_filepath = filepath.with_suffix('.tmp') temp_filepath = filepath.with_suffix(".tmp")
with open(temp_filepath, 'wb') as f: with open(temp_filepath, "wb") as f:
pickle.dump(save_data, f) pickle.dump(save_data, f)
f.flush() # Ensure data is written to disk f.flush() # Ensure data is written to disk
os.fsync(f.fileno()) # Force write to disk os.fsync(f.fileno()) # Force write to disk
@@ -86,7 +86,7 @@ class SaveManager:
if temp_filepath.exists(): if temp_filepath.exists():
try: try:
temp_filepath.unlink() temp_filepath.unlink()
except: except Exception:
pass pass
return False, f"Failed to save game: {str(e)}" return False, f"Failed to save game: {str(e)}"
@@ -94,64 +94,79 @@ class SaveManager:
"""Serialize weapons for saving""" """Serialize weapons for saving"""
serialized = [] serialized = []
for weapon in weapons: for weapon in weapons:
serialized.append({ serialized.append(
'name': weapon.name, {
'damage': weapon.damage, "name": weapon.name,
'range': weapon.range, "damage": weapon.damage,
'attackSound': weapon.attackSound, "range": weapon.range,
'hitSound': weapon.hitSound, "attackSound": weapon.attackSound,
'attackDuration': weapon.attackDuration "hitSound": weapon.hitSound,
}) "attackDuration": weapon.attackDuration,
"speedBonus": getattr(weapon, "speedBonus", 1.0),
"jumpDurationBonus": getattr(weapon, "jumpDurationBonus", 1.0),
}
)
return serialized return serialized
def _deserialize_weapons(self, weapon_data): def _deserialize_weapons(self, weapon_data):
"""Deserialize weapons from save data""" """Deserialize weapons from save data"""
from src.weapon import Weapon from src.weapon import Weapon
weapons = [] weapons = []
for data in weapon_data: for data in weapon_data:
# Handle backward compatibility for old saves
speedBonus = data.get("speedBonus", 1.0)
jumpDurationBonus = data.get("jumpDurationBonus", 1.0)
# For old saves, restore proper bonuses for specific weapons
if data["name"] == "witch_broom" and speedBonus == 1.0:
speedBonus = 1.17
jumpDurationBonus = 1.25
weapon = Weapon( weapon = Weapon(
name=data['name'], name=data["name"],
damage=data['damage'], damage=data["damage"],
range=data['range'], range=data["range"],
attackSound=data['attackSound'], attackSound=data["attackSound"],
hitSound=data['hitSound'], hitSound=data["hitSound"],
attackDuration=data['attackDuration'] attackDuration=data["attackDuration"],
speedBonus=speedBonus,
jumpDurationBonus=jumpDurationBonus,
) )
weapons.append(weapon) weapons.append(weapon)
return weapons return weapons
def _serialize_stats(self, stats): def _serialize_stats(self, stats):
"""Serialize stats for saving""" """Serialize stats for saving"""
return { return {"total": stats.total.copy(), "level": stats.level.copy()}
'total': stats.total.copy(),
'level': stats.level.copy()
}
def _deserialize_stats(self, stats_data): def _deserialize_stats(self, stats_data):
"""Deserialize stats from save data""" """Deserialize stats from save data"""
from src.stat_tracker import StatTracker from src.stat_tracker import StatTracker
stats = StatTracker() stats = StatTracker()
if 'total' in stats_data: if "total" in stats_data:
stats.total.update(stats_data['total']) stats.total.update(stats_data["total"])
if 'level' in stats_data: if "level" in stats_data:
stats.level.update(stats_data['level']) stats.level.update(stats_data["level"])
return stats return stats
def _serialize_scoreboard(self, scoreboard): def _serialize_scoreboard(self, scoreboard):
"""Serialize scoreboard for saving""" """Serialize scoreboard for saving"""
return { return {
'currentScore': getattr(scoreboard, 'currentScore', 0), "currentScore": getattr(scoreboard, "currentScore", 0),
'highScores': getattr(scoreboard, 'highScores', []) "highScores": getattr(scoreboard, "highScores", []),
} }
def _deserialize_scoreboard(self, scoreboard_data): def _deserialize_scoreboard(self, scoreboard_data):
"""Deserialize scoreboard from save data""" """Deserialize scoreboard from save data"""
from libstormgames import Scoreboard from libstormgames import Scoreboard
scoreboard = Scoreboard() scoreboard = Scoreboard()
if 'currentScore' in scoreboard_data: if "currentScore" in scoreboard_data:
scoreboard.currentScore = scoreboard_data['currentScore'] scoreboard.currentScore = scoreboard_data["currentScore"]
if 'highScores' in scoreboard_data: if "highScores" in scoreboard_data:
scoreboard.highScores = scoreboard_data['highScores'] scoreboard.highScores = scoreboard_data["highScores"]
return scoreboard return scoreboard
def get_save_files(self): def get_save_files(self):
@@ -161,7 +176,7 @@ class SaveManager:
for filepath in glob.glob(pattern): for filepath in glob.glob(pattern):
try: try:
with open(filepath, 'rb') as f: with open(filepath, "rb") as f:
save_data = pickle.load(f) save_data = pickle.load(f)
# Validate save data structure # Validate save data structure
@@ -170,29 +185,31 @@ class SaveManager:
continue continue
# Extract save info # Extract save info
save_time = save_data['game_state']['saveTime'] save_time = save_data["game_state"]["saveTime"]
level = save_data['game_state']['currentLevel'] level = save_data["game_state"]["currentLevel"]
game_name = save_data['game_state']['currentGame'] game_name = save_data["game_state"]["currentGame"]
# Format display name # Format display name
formatted_time = save_time.strftime("%B %d %I:%M%p") formatted_time = save_time.strftime("%B %d %I:%M%p")
display_name = f"{formatted_time} {game_name} Level {level}" display_name = f"{formatted_time} {game_name} Level {level}"
save_files.append({ save_files.append(
'filepath': filepath, {
'display_name': display_name, "filepath": filepath,
'save_time': save_time, "display_name": display_name,
'level': level, "save_time": save_time,
'game_name': game_name, "level": level,
'save_data': save_data "game_name": game_name,
}) "save_data": save_data,
}
)
except (pickle.PickleError, EOFError, OSError) as e: except (pickle.PickleError, EOFError, OSError) as e:
print(f"Corrupted save file {filepath}: {e}") print(f"Corrupted save file {filepath}: {e}")
# Try to remove corrupted save file # Try to remove corrupted save file
try: try:
os.remove(filepath) os.remove(filepath)
print(f"Removed corrupted save file: {filepath}") print(f"Removed corrupted save file: {filepath}")
except: except Exception:
pass pass
continue continue
except Exception as e: except Exception as e:
@@ -200,13 +217,13 @@ class SaveManager:
continue continue
# Sort by save time (newest first) # Sort by save time (newest first)
save_files.sort(key=lambda x: x['save_time'], reverse=True) save_files.sort(key=lambda x: x["save_time"], reverse=True)
return save_files return save_files
def load_save(self, filepath): def load_save(self, filepath):
"""Load game state from save file""" """Load game state from save file"""
try: try:
with open(filepath, 'rb') as f: with open(filepath, "rb") as f:
save_data = pickle.load(f) save_data = pickle.load(f)
return True, save_data return True, save_data
except Exception as e: except Exception as e:
@@ -214,26 +231,26 @@ class SaveManager:
def restore_player_state(self, player, save_data): def restore_player_state(self, player, save_data):
"""Restore player state from save data""" """Restore player state from save data"""
player_state = save_data['player_state'] player_state = save_data["player_state"]
# Restore basic attributes # Restore basic attributes
player.xPos = player_state['xPos'] player.xPos = player_state["xPos"]
player.yPos = player_state['yPos'] player.yPos = player_state["yPos"]
player._health = player_state['health'] player._health = player_state["health"]
player._maxHealth = player_state['maxHealth'] player._maxHealth = player_state["maxHealth"]
player._lives = player_state['lives'] player._lives = player_state["lives"]
player._coins = player_state['coins'] player._coins = player_state["coins"]
player._saveBoneDust = player_state['saveBoneDust'] player._saveBoneDust = player_state["saveBoneDust"]
player._jack_o_lantern_count = player_state['jackOLanternCount'] player._jack_o_lantern_count = player_state["jackOLanternCount"]
player.shinBoneCount = player_state['shinBoneCount'] player.shinBoneCount = player_state["shinBoneCount"]
player.inventory = player_state['inventory'] player.inventory = player_state["inventory"]
player.collectedItems = player_state['collectedItems'] player.collectedItems = player_state["collectedItems"]
# Restore weapons # Restore weapons
player.weapons = self._deserialize_weapons(player_state['weapons']) player.weapons = self._deserialize_weapons(player_state["weapons"])
# Restore current weapon # Restore current weapon
current_weapon_name = player_state.get('currentWeaponName') current_weapon_name = player_state.get("currentWeaponName")
if current_weapon_name: if current_weapon_name:
for weapon in player.weapons: for weapon in player.weapons:
if weapon.name == current_weapon_name: if weapon.name == current_weapon_name:
@@ -241,17 +258,19 @@ class SaveManager:
break break
# Restore stats # Restore stats
if 'stats' in player_state: if "stats" in player_state:
player.stats = self._deserialize_stats(player_state['stats']) player.stats = self._deserialize_stats(player_state["stats"])
else: else:
from src.stat_tracker import StatTracker from src.stat_tracker import StatTracker
player.stats = StatTracker() player.stats = StatTracker()
# Restore scoreboard # Restore scoreboard
if 'scoreboard' in player_state: if "scoreboard" in player_state:
player.scoreboard = self._deserialize_scoreboard(player_state['scoreboard']) player.scoreboard = self._deserialize_scoreboard(player_state["scoreboard"])
else: else:
from libstormgames import Scoreboard from libstormgames import Scoreboard
player.scoreboard = Scoreboard() player.scoreboard = Scoreboard()
def _cleanup_old_saves(self): def _cleanup_old_saves(self):
@@ -262,7 +281,7 @@ class SaveManager:
# Remove oldest saves # Remove oldest saves
for save_file in save_files[self.max_saves:]: for save_file in save_files[self.max_saves:]:
try: try:
os.remove(save_file['filepath']) os.remove(save_file["filepath"])
except Exception as e: except Exception as e:
print(f"Error removing old save {save_file['filepath']}: {e}") print(f"Error removing old save {save_file['filepath']}: {e}")
@@ -270,22 +289,22 @@ class SaveManager:
"""Validate that save data has required structure""" """Validate that save data has required structure"""
try: try:
# Check for required top-level keys # Check for required top-level keys
required_keys = ['player_state', 'game_state', 'version'] required_keys = ["player_state", "game_state", "version"]
if not all(key in save_data for key in required_keys): if not all(key in save_data for key in required_keys):
return False return False
# Check player_state structure # Check player_state structure
player_required = ['xPos', 'yPos', 'health', 'maxHealth', 'lives', 'coins', 'saveBoneDust'] player_required = ["xPos", "yPos", "health", "maxHealth", "lives", "coins", "saveBoneDust"]
if not all(key in save_data['player_state'] for key in player_required): if not all(key in save_data["player_state"] for key in player_required):
return False return False
# Check game_state structure # Check game_state structure
game_required = ['currentLevel', 'currentGame', 'gameStartTime', 'saveTime'] game_required = ["currentLevel", "currentGame", "gameStartTime", "saveTime"]
if not all(key in save_data['game_state'] for key in game_required): if not all(key in save_data["game_state"] for key in game_required):
return False return False
return True return True
except: except Exception:
return False return False
def has_saves(self): def has_saves(self):
+24 -29
View File
@@ -5,17 +5,13 @@ import random
from libstormgames import * from libstormgames import *
from src.object import Object from src.object import Object
class SkullStorm(Object): class SkullStorm(Object):
"""Handles falling skulls within a specified range.""" """Handles falling skulls within a specified range."""
def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5): def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5):
super().__init__( super().__init__(
xRange, xRange, y, "", isStatic=True, isCollectible=False, isHazard=False # No ambient sound for the skull storm
y,
"", # No ambient sound for the skull storm
isStatic=True,
isCollectible=False,
isHazard=False
) )
self.sounds = sounds self.sounds = sounds
self.damage = damage self.damage = damage
@@ -37,7 +33,7 @@ class SkullStorm(Object):
inRange = self.xRange[0] <= player.xPos <= self.xRange[1] inRange = self.xRange[0] <= player.xPos <= self.xRange[1]
if inRange and not self.playerInRange: if inRange and not self.playerInRange:
# Player just entered range - play the warning sound # Player just entered range - play the warning sound
play_sound(self.sounds['skull_storm']) play_sound(self.sounds["skull_storm"])
self.playerInRange = True self.playerInRange = True
elif not inRange and self.playerInRange: # Only speak when actually leaving range elif not inRange and self.playerInRange: # Only speak when actually leaving range
# Player just left range # Player just left range
@@ -46,8 +42,8 @@ class SkullStorm(Object):
# Clear any active skulls when player leaves the range # Clear any active skulls when player leaves the range
for skull in self.activeSkulls[:]: for skull in self.activeSkulls[:]:
if skull['channel']: if skull["channel"]:
obj_stop(skull['channel']) obj_stop(skull["channel"])
self.activeSkulls = [] # Reset the list of active skulls self.activeSkulls = [] # Reset the list of active skulls
if not inRange: if not inRange:
@@ -55,29 +51,28 @@ class SkullStorm(Object):
# Update existing skulls # Update existing skulls
for skull in self.activeSkulls[:]: # Copy list to allow removal for skull in self.activeSkulls[:]: # Copy list to allow removal
if currentTime >= skull['land_time']: if currentTime >= skull["land_time"]:
# Skull has landed # Skull has landed
self.handle_landing(skull, player) self.handle_landing(skull, player)
self.activeSkulls.remove(skull) self.activeSkulls.remove(skull)
else: else:
# Update falling sound # Update falling sound
timeElapsed = currentTime - skull['start_time'] timeElapsed = currentTime - skull["start_time"]
fallProgress = timeElapsed / skull['fall_duration'] fallProgress = timeElapsed / skull["fall_duration"]
currentY = self.yPos * (1 - fallProgress) currentY = self.yPos * (1 - fallProgress)
skull['channel'] = play_random_falling( skull["channel"] = play_random_falling(
self.sounds, self.sounds,
'falling_skull', "falling_skull",
player.xPos, player.xPos,
skull['x'], skull["x"],
self.yPos, self.yPos,
currentY, currentY,
existingChannel=skull['channel'] existingChannel=skull["channel"],
) )
# Check if we should spawn a new skull # Check if we should spawn a new skull
if (len(self.activeSkulls) < self.maxSkulls and if len(self.activeSkulls) < self.maxSkulls and currentTime - self.lastSkullTime >= self.nextSkullDelay:
currentTime - self.lastSkullTime >= self.nextSkullDelay):
self.spawn_skull(currentTime) self.spawn_skull(currentTime)
def spawn_skull(self, currentTime): def spawn_skull(self, currentTime):
@@ -91,11 +86,11 @@ class SkullStorm(Object):
# Create new skull # Create new skull
skull = { skull = {
'x': random.uniform(self.xRange[0], self.xRange[1]), "x": random.uniform(self.xRange[0], self.xRange[1]),
'start_time': currentTime, "start_time": currentTime,
'fall_duration': fallDuration, "fall_duration": fallDuration,
'land_time': currentTime + fallDuration, "land_time": currentTime + fallDuration,
'channel': None "channel": None,
} }
self.activeSkulls.append(skull) self.activeSkulls.append(skull)
@@ -103,18 +98,18 @@ class SkullStorm(Object):
def handle_landing(self, skull, player): def handle_landing(self, skull, player):
"""Handle a skull landing.""" """Handle a skull landing."""
# Stop falling sound # Stop falling sound
if skull['channel']: if skull["channel"]:
obj_stop(skull['channel']) obj_stop(skull["channel"])
# Play landing sound with positional audio once # Play landing sound with positional audio once
channel = pygame.mixer.find_channel(True) # Find an available channel channel = pygame.mixer.find_channel(True) # Find an available channel
if channel: if channel:
soundObj = self.sounds['skull_lands'] soundObj = self.sounds["skull_lands"]
obj_play(self.sounds, 'skull_lands', player.xPos, skull['x'], loop=False) obj_play(self.sounds, "skull_lands", player.xPos, skull["x"], loop=False)
# Check if player was hit # Check if player was hit
if abs(player.xPos - skull['x']) < 1: # Within 1 tile if abs(player.xPos - skull["x"]) < 1: # Within 1 tile
if not player.isInvincible: if not player.isInvincible:
player.set_health(player.get_health() - self.damage) player.set_health(player.get_health() - self.damage)
self.sounds['player_takes_damage'].play() self.sounds["player_takes_damage"].play()
speak("Hit by falling skull!") speak("Hit by falling skull!")
+3 -8
View File
@@ -1,20 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
class StatTracker: class StatTracker:
def __init__(self): def __init__(self):
# Base dictionary for tracking stats # Base dictionary for tracking stats
self.total = { self.total = {"Bone dust": 0, "Enemies killed": 0, "Coffins broken": 0, "Items collected": 0, "Total time": 0}
'Bone dust': 0,
'Enemies killed': 0,
'Coffins broken': 0,
'Items collected': 0,
'Total time': 0
}
# Create level stats from total (shallow copy is fine here) # Create level stats from total (shallow copy is fine here)
self.level = self.total.copy() self.level = self.total.copy()
self.total['levelsCompleted'] = 0 self.total["levelsCompleted"] = 0
def reset_level(self): def reset_level(self):
"""Reset level stats based on variable type""" """Reset level stats based on variable type"""
+245
View File
@@ -0,0 +1,245 @@
# -*- coding: utf-8 -*-
import json
import os
import random
import copy
from src.game_selection import get_level_path
class SurvivalGenerator:
def __init__(self, gamePack):
"""Initialize the survival generator for a specific game pack.
Args:
gamePack (str): Name of the game pack directory
"""
self.gamePack = gamePack
self.levelData = {}
self.objectTemplates = []
self.enemyTemplates = []
self.collectibleTemplates = []
self.hazardTemplates = []
self.ambientSounds = []
self.footstepSounds = []
self.loadLevelData()
self.parseTemplates()
def loadLevelData(self):
"""Load all level JSON files from the game pack."""
levelFiles = []
packPath = os.path.join("levels", self.gamePack)
if not os.path.exists(packPath):
raise FileNotFoundError(f"Game pack '{self.gamePack}' not found")
# Get all JSON files in the pack directory
for file in os.listdir(packPath):
if file.endswith(".json") and file[0].isdigit():
levelFiles.append(file)
# Load each level file
for levelFile in levelFiles:
levelPath = os.path.join(packPath, levelFile)
with open(levelPath, "r") as f:
levelNum = int(levelFile.split(".")[0])
self.levelData[levelNum] = json.load(f)
def parseTemplates(self):
"""Parse all level data to extract object templates by type."""
for levelNum, data in self.levelData.items():
# Store ambience and footstep sounds (remove duplicates)
if "ambience" in data and data["ambience"] not in self.ambientSounds:
self.ambientSounds.append(data["ambience"])
if "footstep_sound" in data and data["footstep_sound"] not in self.footstepSounds:
self.footstepSounds.append(data["footstep_sound"])
# Parse objects
for obj in data.get("objects", []):
objCopy = copy.deepcopy(obj)
# Add source level information to track difficulty progression
objCopy["source_level"] = levelNum
# Categorize objects
if "enemy_type" in obj:
self.enemyTemplates.append(objCopy)
elif obj.get("collectible", False) or obj.get("sound") == "bone_dust":
self.collectibleTemplates.append(objCopy)
elif obj.get("type") in ["skull_storm", "catapult", "grasping_hands"]:
self.hazardTemplates.append(objCopy)
else:
self.objectTemplates.append(objCopy)
def generate_survival_level(self, difficultyLevel=1, segmentLength=100):
"""Generate an endless survival level segment.
Args:
difficultyLevel (int): Current difficulty level (increases over time)
segmentLength (int): Length of this level segment
Returns:
dict: Generated level data
"""
# Base level structure
levelData = {
"level_id": 999, # Special ID for survival mode
"name": f"Wave {difficultyLevel}",
"description": "", # Empty description to avoid automatic messagebox
"player_start": {"x": 0, "y": 0},
"objects": [],
"boundaries": {"left": 0, "right": segmentLength},
"locked": True, # Enable lock system for survival mode
"ambience": "Escaping the Grave.ogg", # Will be overridden below
"footstep_sound": "footstep_stone", # Will be overridden below
}
# Choose random music and footstep from collected unique tracks
if self.ambientSounds:
levelData["ambience"] = random.choice(self.ambientSounds)
if self.footstepSounds:
levelData["footstep_sound"] = random.choice(self.footstepSounds)
# Calculate spawn rates based on difficulty
collectibleDensity = max(0.1, 0.3 - (difficultyLevel * 0.02)) # Fewer collectibles over time
enemyDensity = min(0.8, 0.2 + (difficultyLevel * 0.05)) # More enemies over time
hazardDensity = min(0.4, 0.1 + (difficultyLevel * 0.03)) # More hazards over time
objectDensity = max(0.1, 0.2 - (difficultyLevel * 0.01)) # Fewer misc objects over time
# Generate objects across the segment with buffer zones
startBufferZone = 25 # Safe zone at start of each wave
endBufferZone = 30 # Safe zone at end of each wave
currentX = startBufferZone # Start placing objects after start buffer zone
while currentX < segmentLength - endBufferZone:
# Determine what to place based on probability
rand = random.random()
if rand < collectibleDensity and self.collectibleTemplates:
obj = self.place_collectible(currentX, difficultyLevel)
currentX += random.randint(8, 15)
elif rand < collectibleDensity + enemyDensity and self.enemyTemplates:
obj = self.place_enemy(currentX, difficultyLevel)
currentX += random.randint(15, 25)
elif rand < collectibleDensity + enemyDensity + hazardDensity and self.hazardTemplates:
obj = self.place_hazard(currentX, difficultyLevel)
currentX += random.randint(20, 35)
elif rand < collectibleDensity + enemyDensity + hazardDensity + objectDensity and self.objectTemplates:
obj = self.place_object(currentX, difficultyLevel)
currentX += random.randint(12, 20)
else:
currentX += random.randint(5, 15)
continue
if obj:
levelData["objects"].append(obj)
# Add end-of-level marker at the end, within the end buffer zone
endMarker = {
"x": segmentLength - (endBufferZone // 2), # Place marker in middle of end buffer
"y": 0,
"sound": "end_of_level",
}
levelData["objects"].append(endMarker)
return levelData
def place_collectible(self, xPos, difficultyLevel):
"""Place a collectible at the given position."""
template = random.choice(self.collectibleTemplates)
obj = copy.deepcopy(template)
# Handle x_range vs single x
if "x_range" in obj:
rangeSize = obj["x_range"][1] - obj["x_range"][0]
obj["x_range"] = [xPos, xPos + rangeSize]
else:
obj["x"] = xPos
return obj
def place_enemy(self, xPos, difficultyLevel):
"""Place an enemy at the given position with scaled difficulty."""
# Level-based filtering: allow enemies from levels 1 through current wave
maxLevel = max(self.levelData.keys()) if self.levelData else 1
allowedLevels = list(range(1, min(difficultyLevel, maxLevel) + 1))
# If we're past the max level, allow all enemies
if difficultyLevel > maxLevel:
availableEnemies = self.enemyTemplates
else:
availableEnemies = [e for e in self.enemyTemplates if e.get("source_level", 1) in allowedLevels]
# Fallback to all enemies if filtering removed everything
if not availableEnemies:
availableEnemies = self.enemyTemplates
template = random.choice(availableEnemies)
obj = copy.deepcopy(template)
# Dynamic health scaling: random between wave/2 and wave
minHealth = max(1, difficultyLevel // 2)
maxHealth = max(1, difficultyLevel)
obj["health"] = random.randint(minHealth, maxHealth)
# Damage scaling (keep existing logic)
damageMultiplier = 1 + (difficultyLevel * 0.1)
obj["damage"] = max(1, int(obj.get("damage", 1) * damageMultiplier))
# Set all enemies to hunter mode for survival
obj["behavior"] = "hunter"
# Progressive turn rate reduction: start at 6, decrease to 1
obj["turn_rate"] = max(1, 7 - difficultyLevel)
# Handle x_range vs single x
if "x_range" in obj:
rangeSize = obj["x_range"][1] - obj["x_range"][0]
obj["x_range"] = [xPos, xPos + rangeSize]
else:
obj["x"] = xPos
return obj
def place_hazard(self, xPos, difficultyLevel):
"""Place a hazard at the given position with scaled difficulty."""
template = random.choice(self.hazardTemplates)
obj = copy.deepcopy(template)
# Scale hazard difficulty
if obj.get("type") == "skull_storm":
obj["damage"] = max(1, int(obj.get("damage", 1) * (1 + difficultyLevel * 0.1)))
obj["maximum_skulls"] = min(6, obj.get("maximum_skulls", 2) + (difficultyLevel // 3))
elif obj.get("type") == "catapult":
obj["fire_interval"] = max(1000, obj.get("fire_interval", 4000) - (difficultyLevel * 100))
# Handle x_range vs single x
if "x_range" in obj:
rangeSize = obj["x_range"][1] - obj["x_range"][0]
obj["x_range"] = [xPos, xPos + rangeSize]
else:
obj["x"] = xPos
return obj
def place_object(self, xPos, difficultyLevel):
"""Place a misc object at the given position."""
template = random.choice(self.objectTemplates)
obj = copy.deepcopy(template)
# Handle graves - increase zombie spawn chance with difficulty
if obj.get("type") == "grave":
baseChance = obj.get("zombie_spawn_chance", 0)
obj["zombie_spawn_chance"] = min(50, baseChance + (difficultyLevel * 2))
# Handle coffins - make all items random in survival mode
if obj.get("type") == "coffin":
obj["item"] = "random" # Override any specified item
# Handle x_range vs single x
if "x_range" in obj:
rangeSize = obj["x_range"][1] - obj["x_range"][0]
obj["x_range"] = [xPos, xPos + rangeSize]
else:
obj["x"] = xPos
return obj
+19 -3
View File
@@ -1,7 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
class Weapon: class Weapon:
def __init__(self, name, damage, range, attackSound, hitSound, cooldown=500, attackDuration=200): def __init__(
self,
name,
damage,
range,
attackSound,
hitSound,
cooldown=500,
attackDuration=200,
speedBonus=1.0,
jumpDurationBonus=1.0,
):
self.name = name self.name = name
self.damage = damage self.damage = damage
self.range = range # Range in tiles self.range = range # Range in tiles
@@ -9,6 +21,8 @@ class Weapon:
self.hitSound = hitSound self.hitSound = hitSound
self.cooldown = cooldown # Milliseconds between attacks self.cooldown = cooldown # Milliseconds between attacks
self.attackDuration = attackDuration # Milliseconds the attack is active self.attackDuration = attackDuration # Milliseconds the attack is active
self.speedBonus = speedBonus # Speed multiplier when wielding this weapon
self.jumpDurationBonus = jumpDurationBonus # Jump duration multiplier when wielding this weapon
self.lastAttackTime = 0 self.lastAttackTime = 0
self.hitEnemies = set() self.hitEnemies = set()
@@ -22,7 +36,7 @@ class Weapon:
attackSound="player_nunchuck_attack", attackSound="player_nunchuck_attack",
hitSound="player_nunchuck_hit", hitSound="player_nunchuck_hit",
cooldown=250, cooldown=250,
attackDuration=100 attackDuration=100,
) )
@classmethod @classmethod
@@ -35,7 +49,9 @@ class Weapon:
attackSound="player_broom_attack", attackSound="player_broom_attack",
hitSound="player_broom_hit", hitSound="player_broom_hit",
cooldown=500, cooldown=500,
attackDuration=200 attackDuration=200,
speedBonus=1.17, # 17% speed bonus when wielding the broom
jumpDurationBonus=1.25, # 25% longer jump duration for better traversal
) )
def can_attack(self, currentTime): def can_attack(self, currentTime):
+283 -54
View File
@@ -5,25 +5,48 @@ import json
import os import os
import pygame import pygame
from libstormgames import * from libstormgames import *
from src.pack_sound_system import PackSoundSystem
from src.level import Level from src.level import Level
from src.object import Object from src.object import Object
from src.player import Player from src.player import Player
from src.game_selection import select_game, get_level_path from src.game_selection import select_game, get_level_path
from src.save_manager import SaveManager from src.save_manager import SaveManager
from src.survival_generator import SurvivalGenerator
class WickedQuest: class WickedQuest:
def __init__(self): def __init__(self):
"""Initialize game and load sounds.""" """Initialize game and load sounds."""
self.sounds = initialize_gui("Wicked Quest") self.sounds = initialize_gui("Wicked Quest")
self.soundSystem = None # Will be created when game is selected
self.currentLevel = None self.currentLevel = None
self.gameStartTime = None self.gameStartTime = None
self.lastThrowTime = 0 self.lastThrowTime = 0
self.throwDelay = 250 self.throwDelay = 250
self.lastWeaponSwitchTime = 0
self.weaponSwitchDelay = 200
self.player = None self.player = None
self.currentGame = None self.currentGame = None
self.runLock = False # Toggle behavior of the run keys self.runLock = False # Toggle behavior of the run keys
self.saveManager = SaveManager() self.saveManager = SaveManager()
self.survivalGenerator = None
self.lastBroomLandingTime = 0 # Timestamp to prevent ducking after broom landing
self.survivalWave = 1
self.survivalScore = 0
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 load_level(self, levelNumber): def load_level(self, levelNumber):
"""Load a level from its JSON file.""" """Load a level from its JSON file."""
@@ -36,7 +59,7 @@ class WickedQuest:
if self.player is None: if self.player is None:
self.player = Player(levelData["player_start"]["x"], self.player = Player(levelData["player_start"]["x"],
levelData["player_start"]["y"], levelData["player_start"]["y"],
self.sounds) self.get_sounds())
else: else:
# Reset player for new level. # Reset player for new level.
self.player.isDucking = False self.player.isDucking = False
@@ -56,7 +79,7 @@ class WickedQuest:
# Pass existing player to new level # Pass existing player to new level
pygame.event.clear() pygame.event.clear()
self.currentLevel = Level(levelData, self.sounds, self.player) self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame)
return True return True
except FileNotFoundError: except FileNotFoundError:
@@ -87,7 +110,7 @@ class WickedQuest:
return errors return errors
def load_game_menu(self): def load_game_menu(self):
"""Display load game menu with available saves""" """Display load game menu with available saves using instruction_menu"""
save_files = self.saveManager.get_save_files() save_files = self.saveManager.get_save_files()
if not save_files: if not save_files:
@@ -101,48 +124,24 @@ class WickedQuest:
options.append("Cancel") options.append("Cancel")
# Show menu # Use instruction_menu for consistent behavior
currentIndex = 0 choice = instruction_menu(self.get_sounds(), "Select a save file to load:", *options)
lastSpoken = -1
messagebox("Select a save file to load:") if choice == "Cancel" or choice is None:
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 return None
else: else:
return save_files[currentIndex] # Find the corresponding save file
for save_file in save_files:
pygame.event.clear() if save_file['display_name'] == choice:
return save_file
return None
def auto_save(self): def auto_save(self):
"""Automatically save the game if player has enough bone dust""" """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(): if not self.player.can_save():
return False return False
@@ -157,8 +156,8 @@ class WickedQuest:
if success: if success:
try: try:
if 'save' in self.sounds: if 'save' in self.get_sounds():
play_sound(self.sounds['save']) play_sound(self.get_sounds()['save'])
else: else:
print("Save sound not found in sounds dictionary") print("Save sound not found in sounds dictionary")
except Exception as e: except Exception as e:
@@ -179,6 +178,13 @@ class WickedQuest:
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
# Update running and ducking states # 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: if (keys[pygame.K_s] or keys[pygame.K_DOWN]) and not player.isDucking:
player.duck() player.duck()
elif (not keys[pygame.K_s] and not keys[pygame.K_DOWN]) and player.isDucking: elif (not keys[pygame.K_s] and not keys[pygame.K_DOWN]) and player.isDucking:
@@ -209,12 +215,16 @@ class WickedQuest:
if movementDistance > 0 and not player.isJumping: if movementDistance > 0 and not player.isJumping:
player.distanceSinceLastStep += movementDistance player.distanceSinceLastStep += movementDistance
if player.should_play_footstep(currentTime): if player.should_play_footstep(currentTime):
play_sound(self.sounds[player.footstepSound]) play_sound(self.get_sounds()[player.footstepSound])
player.distanceSinceLastStep = 0 player.distanceSinceLastStep = 0
player.lastStepTime = currentTime player.lastStepTime = currentTime
# Status queries # Status queries
if keys[pygame.K_c]: if keys[pygame.K_c]:
# Different status message for survival vs story mode
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") speak(f"{player.get_coins()} bone dust for extra lives, {player.get_save_bone_dust()} bone dust for saves")
if keys[pygame.K_h]: if keys[pygame.K_h]:
speak(f"{player.get_health()} health of {player.get_max_health()}") speak(f"{player.get_health()} health of {player.get_max_health()}")
@@ -232,20 +242,44 @@ class WickedQuest:
if keys[pygame.K_e]: if keys[pygame.K_e]:
speak(f"Wielding {self.currentLevel.player.currentWeapon.name.replace('_', ' ')}") speak(f"Wielding {self.currentLevel.player.currentWeapon.name.replace('_', ' ')}")
# Weapon switching (1=shovel, 2=broom, 3=nunchucks)
currentTime = pygame.time.get_ticks()
if currentTime - self.lastWeaponSwitchTime >= self.weaponSwitchDelay:
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
# Handle attack with either CTRL key # Handle attack with either CTRL key
if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime): if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime):
play_sound(self.sounds[player.currentWeapon.attackSound]) play_sound(self.get_sounds()[player.currentWeapon.attackSound])
# Handle jumping # Handle jumping
if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping: if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping:
player.isJumping = True player.isJumping = True
player.jumpStartTime = currentTime player.jumpStartTime = currentTime
play_sound(self.sounds['jump']) play_sound(self.get_sounds()['jump'])
# Check if jump should end # Handle instant landing with broom (press down while jumping)
if player.isJumping and currentTime - player.jumpStartTime >= player.jumpDuration: if (player.isJumping and (keys[pygame.K_s] or keys[pygame.K_DOWN]) and
player.currentWeapon and player.currentWeapon.name == "witch_broom"):
player.isJumping = False player.isJumping = False
play_sound(self.sounds[player.footstepSound]) # Landing sound 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 # Reset step distance tracking after landing
player.distanceSinceLastStep = 0 player.distanceSinceLastStep = 0
player.lastStepTime = currentTime player.lastStepTime = currentTime
@@ -272,7 +306,7 @@ class WickedQuest:
try: try:
speak(f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}, complete!") speak(f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}, complete!")
pygame.mixer.music.stop() pygame.mixer.music.stop()
cut_scene(self.sounds, '_finish_level') cut_scene(self.get_sounds(), '_finish_level')
except: except:
pass pass
@@ -298,7 +332,31 @@ class WickedQuest:
pygame.event.clear() pygame.event.clear()
self.player.scoreboard.add_high_score() self.player.scoreboard.add_high_score()
cut_scene(self.sounds, "game_over") 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) display_text(report)
def game_loop(self, startingLevelNum=1): def game_loop(self, startingLevelNum=1):
@@ -389,8 +447,8 @@ class WickedQuest:
for ext in ['.wav', '.ogg', '.mp3']: for ext in ['.wav', '.ogg', '.mp3']:
endFile = os.path.join(gamePath, f'end{ext}') endFile = os.path.join(gamePath, f'end{ext}')
if os.path.exists(endFile): if os.path.exists(endFile):
self.sounds['end_scene'] = pygame.mixer.Sound(endFile) self.get_sounds()['end_scene'] = pygame.mixer.Sound(endFile)
cut_scene(self.sounds, 'end_scene') cut_scene(self.get_sounds(), 'end_scene')
break break
else: else:
messagebox("Congratulations! You've completed all available levels!") messagebox("Congratulations! You've completed all available levels!")
@@ -414,7 +472,7 @@ class WickedQuest:
if self.saveManager.has_saves(): if self.saveManager.has_saves():
custom_options.append("load_game") custom_options.append("load_game")
choice = game_menu(self.sounds, None, *custom_options) choice = game_menu(self.get_sounds(), None, *custom_options)
if choice == "exit": if choice == "exit":
exit_game() exit_game()
@@ -427,6 +485,8 @@ class WickedQuest:
self.currentGame = save_data['game_state']['currentGame'] self.currentGame = save_data['game_state']['currentGame']
self.gameStartTime = save_data['game_state']['gameStartTime'] self.gameStartTime = save_data['game_state']['gameStartTime']
current_level = save_data['game_state']['currentLevel'] current_level = save_data['game_state']['currentLevel']
# Initialize pack-specific sound system
self.initialize_pack_sounds()
# Load the level # Load the level
if self.load_level(current_level): if self.load_level(current_level):
@@ -438,7 +498,11 @@ class WickedQuest:
else: else:
messagebox(f"Failed to load save: {save_data}") messagebox(f"Failed to load save: {save_data}")
elif choice == "play": elif choice == "play":
self.currentGame = select_game(self.sounds) 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 # Validate level files before starting
errors = self.validate_levels() errors = self.validate_levels()
if errors: if errors:
@@ -448,10 +512,15 @@ class WickedQuest:
display_text(errorLines) display_text(errorLines)
continue continue
if self.currentGame: if self.currentGame:
# Ask player to choose game mode
mode_choice = game_mode_menu(self.get_sounds())
if mode_choice == "story":
self.player = None # Reset player for new game self.player = None # Reset player for new game
self.gameStartTime = pygame.time.get_ticks() self.gameStartTime = pygame.time.get_ticks()
if self.load_level(1): if self.load_level(1):
self.game_loop() self.game_loop()
elif mode_choice == "survival":
self.start_survival_mode()
elif choice == "high_scores": elif choice == "high_scores":
board = Scoreboard() board = Scoreboard()
scores = board.get_high_scores() scores = board.get_high_scores()
@@ -463,9 +532,169 @@ class WickedQuest:
pygame.event.clear() pygame.event.clear()
display_text(lines) display_text(lines)
elif choice == "learn_sounds": elif choice == "learn_sounds":
choice = learn_sounds(self.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()
# 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):
"""Display game mode selection menu using instruction_menu.
Args:
sounds (dict): Dictionary of loaded sound effects
Returns:
str: Selected game mode or None if cancelled
"""
choice = instruction_menu(sounds, "Select game mode:", "Story", "Survival Mode")
if choice == "Story":
return "story"
elif choice == "Survival Mode":
return "survival"
else:
return None
if __name__ == "__main__": if __name__ == "__main__":
game = WickedQuest() game = WickedQuest()
game.run() game.run()
+14 -1
View File
@@ -1,11 +1,24 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
import os
# Collect level directories dynamically
level_dirs = []
levels_path = 'levels'
if os.path.exists(levels_path):
for item in os.listdir(levels_path):
item_path = os.path.join(levels_path, item)
if os.path.isdir(item_path):
level_dirs.append((item_path, item_path))
a = Analysis( a = Analysis(
['wicked_quest.py'], ['wicked_quest.py'],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[], datas=level_dirs + [
('sounds', 'sounds'),
('libstormgames', 'libstormgames'),
],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},