Compare commits
18 Commits
76a49baa15
...
60ab8cd868
| Author | SHA1 | Date | |
|---|---|---|---|
| 60ab8cd868 | |||
| d83e7db248 | |||
| cc0f0437f5 | |||
| 2bfb146ef5 | |||
| 21d3cd7788 | |||
| 949c12f193 | |||
| dc1557e71d | |||
| 494de84ba3 | |||
| 8766290ccd | |||
| 1d37a16f9e | |||
| 2437e13604 | |||
| 04067a4bb3 | |||
| 8a60c8e454 | |||
| 3034712c95 | |||
| 56a78aa4ff | |||
| cf3f27d9b8 | |||
| 2bc27c0e28 | |||
| ce353d0ed9 |
@@ -1 +1,2 @@
|
||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -25,9 +25,6 @@ https://hubert-humphrey.com
|
||||
|
||||
Steven Robinson:
|
||||
|
||||
Ko-fi Supporters:
|
||||
https://ko-fi.com/stormux/leaderboard
|
||||
|
||||
Music:
|
||||
|
||||
Choir of Doom:
|
||||
|
||||
+23
-3
@@ -19,6 +19,9 @@ tab or capslock: Toggle run lock.
|
||||
Control: Attack.
|
||||
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.
|
||||
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.
|
||||
e: Check currently wielded weapon.
|
||||
h: Check health.
|
||||
@@ -28,7 +31,7 @@ l: Check lives remaining.
|
||||
Alt+PageDown: Master volume decrease.
|
||||
Alt+PageUp: Master volume increase.
|
||||
Alt+End: Ambience volume decrease.
|
||||
Alt+Home: Game volume increase.
|
||||
Alt+Home: Ambience volume increase.
|
||||
Alt+Delete: Game sounds volume decrease.
|
||||
Alt+Insert: Game sounds volume increase.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -61,7 +81,7 @@ Bonuses and items
|
||||
|
||||
Bone dust: Currency of the game. Collect it to gain health and extra lives.
|
||||
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.
|
||||
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.
|
||||
|
||||
+1
-1
Submodule libstormgames updated: ca2d0d34bd...f2079261d1
BIN
Binary file not shown.
BIN
Binary file not shown.
+11
-13
@@ -14,7 +14,7 @@ class Pumpkin:
|
||||
self.isActive = True
|
||||
self.damage = playerMaxHealth // 2 # Half of player's max health
|
||||
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):
|
||||
"""Update pumpkin position and sound"""
|
||||
@@ -39,7 +39,7 @@ class Pumpkin:
|
||||
# Calculate volume and pan for splat sound based on final position
|
||||
volume, left, right = calculate_volume_and_pan(playerX, self.x)
|
||||
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):
|
||||
"""Check if pumpkin hits player"""
|
||||
@@ -58,7 +58,9 @@ class Pumpkin:
|
||||
class Catapult(Object):
|
||||
def __init__(self, x, y, sounds, direction=1, fireInterval=5000, firingRange=20):
|
||||
super().__init__(
|
||||
x, y, "catapult",
|
||||
x,
|
||||
y,
|
||||
"catapult",
|
||||
isStatic=True,
|
||||
isCollectible=False,
|
||||
)
|
||||
@@ -77,18 +79,14 @@ class Catapult(Object):
|
||||
self.lastFireTime = currentTime
|
||||
|
||||
# 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
|
||||
isHigh = random.choice([True, False])
|
||||
fireDirection = 1 if player.xPos > self.xPos else -1
|
||||
|
||||
# Store pumpkin data for later creation
|
||||
self.pendingPumpkin = {
|
||||
'isHigh': isHigh,
|
||||
'direction': fireDirection,
|
||||
'playerMaxHealth': player.get_max_health()
|
||||
}
|
||||
self.pendingPumpkin = {"isHigh": isHigh, "direction": fireDirection, "playerMaxHealth": player.get_max_health()}
|
||||
|
||||
# Set when to actually launch the pumpkin
|
||||
self.pumpkinLaunchTime = currentTime + self.launchDelay
|
||||
@@ -116,9 +114,9 @@ class Catapult(Object):
|
||||
# Create and fire the pending pumpkin
|
||||
pumpkin = Pumpkin(
|
||||
self.xPos,
|
||||
self.pendingPumpkin['isHigh'],
|
||||
self.pendingPumpkin['direction'],
|
||||
self.pendingPumpkin['playerMaxHealth']
|
||||
self.pendingPumpkin["isHigh"],
|
||||
self.pendingPumpkin["direction"],
|
||||
self.pendingPumpkin["playerMaxHealth"],
|
||||
)
|
||||
self.activePumpkins.append(pumpkin)
|
||||
self.pendingPumpkin = None
|
||||
@@ -140,4 +138,4 @@ class Catapult(Object):
|
||||
pumpkin.isActive = False
|
||||
self.activePumpkins.remove(pumpkin)
|
||||
if not player.isInvincible:
|
||||
self.sounds['player_takes_damage'].play()
|
||||
self.sounds["player_takes_damage"].play()
|
||||
|
||||
+4
-15
@@ -9,12 +9,7 @@ from src.powerup import PowerUp
|
||||
|
||||
class CoffinObject(Object):
|
||||
def __init__(self, x, y, sounds, level, item="random"):
|
||||
super().__init__(
|
||||
x, y, "coffin",
|
||||
isStatic=True,
|
||||
isCollectible=False,
|
||||
isHazard=False
|
||||
)
|
||||
super().__init__(x, y, "coffin", isStatic=True, isCollectible=False, isHazard=False)
|
||||
self.sounds = sounds
|
||||
self.level = level
|
||||
self.isBroken = False
|
||||
@@ -25,9 +20,9 @@ class CoffinObject(Object):
|
||||
"""Handle being hit by the player's weapon"""
|
||||
if not self.isBroken:
|
||||
self.isBroken = True
|
||||
play_sound(self.sounds['coffin_shatter'])
|
||||
play_sound(self.sounds["coffin_shatter"])
|
||||
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
|
||||
if self.channel:
|
||||
@@ -54,13 +49,7 @@ class CoffinObject(Object):
|
||||
drop_x = self.xPos + (direction * drop_distance)
|
||||
|
||||
self.dropped_item = PowerUp(
|
||||
drop_x,
|
||||
self.yPos,
|
||||
item_type,
|
||||
self.sounds,
|
||||
direction,
|
||||
self.level.leftBoundary,
|
||||
self.level.rightBoundary
|
||||
drop_x, self.yPos, item_type, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -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
@@ -6,25 +6,20 @@ from libstormgames import *
|
||||
from src.object import Object
|
||||
from src.powerup import PowerUp
|
||||
|
||||
|
||||
class Enemy(Object):
|
||||
def __init__(self, xRange, y, enemyType, sounds, level, **kwargs):
|
||||
# Track when critters should start hunting
|
||||
self.hunting = False
|
||||
# Initialize base object properties
|
||||
super().__init__(
|
||||
xRange,
|
||||
y,
|
||||
f"{enemyType}", # Base sound
|
||||
isStatic=False,
|
||||
isHazard=True
|
||||
)
|
||||
super().__init__(xRange, y, f"{enemyType}", isStatic=False, isHazard=True) # Base sound
|
||||
|
||||
# Enemy specific properties
|
||||
self.enemyType = enemyType
|
||||
self.level = level
|
||||
self.health = kwargs.get('health', 5) # Default 5 HP
|
||||
self.damage = kwargs.get('damage', 1) # Default 1 damage
|
||||
self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range
|
||||
self.health = kwargs.get("health", 5) # Default 5 HP
|
||||
self.damage = kwargs.get("damage", 1) # Default 1 damage
|
||||
self.attackRange = kwargs.get("attack_range", 1) # Default 1 tile range
|
||||
self.sounds = sounds # Store reference to game sounds
|
||||
|
||||
# Movement and behavior properties
|
||||
@@ -37,25 +32,25 @@ class Enemy(Object):
|
||||
self._currentX = self.xRange[0] # Initialize current position
|
||||
|
||||
# Add spawn configuration
|
||||
self.canSpawn = kwargs.get('can_spawn', False)
|
||||
self.canSpawn = kwargs.get("can_spawn", False)
|
||||
if self.canSpawn:
|
||||
self.spawnCooldown = kwargs.get('spawn_cooldown', 2000)
|
||||
self.spawnChance = kwargs.get('spawn_chance', 25)
|
||||
self.spawnType = kwargs.get('spawn_type', 'zombie') # Default to zombie for backward compatibility
|
||||
self.spawnDistance = kwargs.get('spawn_distance', 5)
|
||||
self.spawnCooldown = kwargs.get("spawn_cooldown", 2000)
|
||||
self.spawnChance = kwargs.get("spawn_chance", 25)
|
||||
self.spawnType = kwargs.get("spawn_type", "zombie") # Default to zombie for backward compatibility
|
||||
self.spawnDistance = kwargs.get("spawn_distance", 5)
|
||||
self.lastSpawnTime = 0
|
||||
|
||||
# Attack pattern configuration
|
||||
self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'})
|
||||
self.turnThreshold = self.attackPattern.get('turn_threshold', 5)
|
||||
self.attackPattern = kwargs.get("attack_pattern", {"type": "patrol"})
|
||||
self.turnThreshold = self.attackPattern.get("turn_threshold", 5)
|
||||
|
||||
# Initialize vulnerability system
|
||||
self.hasVulnerabilitySystem = kwargs.get('has_vulnerability', False)
|
||||
self.hasVulnerabilitySystem = kwargs.get("has_vulnerability", False)
|
||||
if self.hasVulnerabilitySystem:
|
||||
self.isVulnerable = False # Start invulnerable
|
||||
self.vulnerabilityTimer = pygame.time.get_ticks()
|
||||
self.vulnerabilityDuration = kwargs.get('vulnerability_duration', 2000)
|
||||
self.invulnerabilityDuration = kwargs.get('invulnerability_duration', 5000)
|
||||
self.vulnerabilityDuration = kwargs.get("vulnerability_duration", 2000)
|
||||
self.invulnerabilityDuration = kwargs.get("invulnerability_duration", 5000)
|
||||
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)
|
||||
else:
|
||||
@@ -68,19 +63,17 @@ class Enemy(Object):
|
||||
self.health = 1 # Easy to kill
|
||||
self.attackCooldown = 1500 # Slower attack rate
|
||||
elif enemyType == "spider":
|
||||
speedMultiplier = kwargs.get('speed_multiplier', 2.0)
|
||||
speedMultiplier = kwargs.get("speed_multiplier", 2.0)
|
||||
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
|
||||
|
||||
|
||||
@property
|
||||
def xPos(self):
|
||||
"""Current x position"""
|
||||
return self._currentX
|
||||
|
||||
@xPos.setter
|
||||
|
||||
def xPos(self, value):
|
||||
"""Set current x position"""
|
||||
self._currentX = value
|
||||
@@ -111,7 +104,9 @@ class Enemy(Object):
|
||||
self.channel = obj_update(self.channel, player.xPos, self.xPos)
|
||||
|
||||
# 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.vulnerabilityTimer = currentTime
|
||||
|
||||
@@ -126,8 +121,7 @@ class Enemy(Object):
|
||||
self.hunting = True
|
||||
|
||||
# Handle movement based on enemy type and pattern
|
||||
if (self.enemyType == "zombie" or
|
||||
(self.attackPattern['type'] == 'hunter' and self.hunting)):
|
||||
if self.enemyType == "zombie" or (self.attackPattern["type"] == "hunter" and self.hunting):
|
||||
|
||||
distanceToPlayer = player.xPos - self.xPos
|
||||
|
||||
@@ -166,6 +160,10 @@ class Enemy(Object):
|
||||
# Ensure spawn point is within level boundaries
|
||||
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
|
||||
spawned = Enemy(
|
||||
[spawnX, spawnX], # Single point range for spawn
|
||||
@@ -175,7 +173,9 @@ class Enemy(Object):
|
||||
self.level,
|
||||
health=4, # Default health 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
|
||||
@@ -215,7 +215,8 @@ class Enemy(Object):
|
||||
|
||||
def attack(self, currentTime, player):
|
||||
"""Perform attack on player"""
|
||||
if player.isInvincible: return
|
||||
if player.isInvincible:
|
||||
return
|
||||
self.lastAttackTime = currentTime
|
||||
# Play attack sound
|
||||
attackSound = f"{self.enemyType}_attack"
|
||||
@@ -223,7 +224,7 @@ class Enemy(Object):
|
||||
self.sounds[attackSound].play()
|
||||
# Deal damage to player
|
||||
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):
|
||||
"""Handle enemy taking damage"""
|
||||
@@ -251,10 +252,11 @@ class Enemy(Object):
|
||||
# Award points
|
||||
self.level.levelScore += totalPoints
|
||||
|
||||
# Play death sound if available using positional audio
|
||||
deathSound = f"{self.enemyType}_dies"
|
||||
if deathSound in self.sounds:
|
||||
self.channel = obj_play(self.sounds, deathSound, self.level.player.xPos, self.xPos, loop=False)
|
||||
# Create a DeathSound object to play death sound at fixed position
|
||||
from src.die_monster_die import DeathSound
|
||||
|
||||
deathSoundObj = DeathSound(self.xPos, self.yPos, self.enemyType, self.sounds)
|
||||
self.level.objects.append(deathSoundObj)
|
||||
|
||||
# Handle witch-specific drops
|
||||
if self.enemyType == "witch":
|
||||
@@ -270,15 +272,9 @@ class Enemy(Object):
|
||||
dropX = self.xPos + (direction * dropDistance)
|
||||
|
||||
droppedItem = PowerUp(
|
||||
dropX,
|
||||
self.yPos,
|
||||
itemType,
|
||||
self.sounds,
|
||||
direction,
|
||||
self.level.leftBoundary,
|
||||
self.level.rightBoundary
|
||||
dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
|
||||
)
|
||||
self.level.bouncing_items.append(droppedItem)
|
||||
|
||||
# Update stats
|
||||
self.level.player.stats.update_stat('Enemies killed', 1)
|
||||
self.level.player.stats.update_stat("Enemies killed", 1)
|
||||
|
||||
+42
-82
@@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import time
|
||||
import pygame
|
||||
import sys
|
||||
from os.path import isdir, join
|
||||
from libstormgames import speak
|
||||
|
||||
from libstormgames import speak, instruction_menu
|
||||
|
||||
|
||||
def get_available_games():
|
||||
"""Get list of available game directories in levels folder.
|
||||
@@ -13,12 +14,22 @@ def get_available_games():
|
||||
list: List of game directory names
|
||||
"""
|
||||
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:
|
||||
return []
|
||||
|
||||
|
||||
def selection_menu(sounds, *options):
|
||||
"""Display level selection menu.
|
||||
"""Display level selection menu using instruction_menu.
|
||||
|
||||
Args:
|
||||
sounds (dict): Dictionary of loaded sound effects
|
||||
@@ -27,70 +38,8 @@ def selection_menu(sounds, *options):
|
||||
Returns:
|
||||
str: Selected option or None if cancelled
|
||||
"""
|
||||
loop = True
|
||||
pygame.mixer.stop()
|
||||
i = 0
|
||||
j = -1
|
||||
return instruction_menu(sounds, "Select an adventure", *options)
|
||||
|
||||
# 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):
|
||||
"""Display game selection menu and return chosen game.
|
||||
@@ -101,37 +50,48 @@ def select_game(sounds):
|
||||
Returns:
|
||||
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!")
|
||||
return None
|
||||
|
||||
# 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:
|
||||
return None
|
||||
|
||||
# Convert display name back to directory name if needed
|
||||
gameDir = choice.replace(" ", "_")
|
||||
if gameDir not in availableGames:
|
||||
gameDir = choice # Use original if conversion doesn't match
|
||||
game_dir = choice.replace(" ", "_")
|
||||
if game_dir not in available_games:
|
||||
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.
|
||||
|
||||
Args:
|
||||
gameDir (str): Game directory name
|
||||
levelNum (int): Level number
|
||||
game_dir (str): Game directory name
|
||||
level_num (int): Level number
|
||||
|
||||
Returns:
|
||||
str: Full path to level JSON file
|
||||
"""
|
||||
if gameDir is None:
|
||||
raise ValueError("gameDir cannot be None")
|
||||
return os.path.join("levels", gameDir, f"{levelNum}.json")
|
||||
if game_dir is None:
|
||||
raise ValueError("game_dir cannot be None")
|
||||
|
||||
# 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
|
||||
|
||||
@@ -4,6 +4,7 @@ import pygame
|
||||
from libstormgames import *
|
||||
from src.object import Object
|
||||
|
||||
|
||||
class GraspingHands(Object):
|
||||
"""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
|
||||
isStatic=True,
|
||||
isCollectible=False,
|
||||
isHazard=True
|
||||
isHazard=True,
|
||||
)
|
||||
self.sounds = sounds
|
||||
self.delay = delay # Delay in milliseconds before ground starts crumbling
|
||||
@@ -51,7 +52,7 @@ class GraspingHands(Object):
|
||||
self.isReset = False
|
||||
|
||||
# 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.")
|
||||
|
||||
def reset(self):
|
||||
@@ -67,7 +68,7 @@ class GraspingHands(Object):
|
||||
self.crumbleChannel = None
|
||||
|
||||
# Play the end sound
|
||||
play_sound(self.sounds['grasping_hands_end'])
|
||||
play_sound(self.sounds["grasping_hands_end"])
|
||||
|
||||
def update(self, currentTime, player):
|
||||
"""Update the grasping hands trap state"""
|
||||
@@ -92,7 +93,7 @@ class GraspingHands(Object):
|
||||
# Manage the looping positional audio for the crumbling ground
|
||||
if self.crumbleChannel is None or not self.crumbleChannel.get_busy():
|
||||
# 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:
|
||||
# Update the sound position
|
||||
self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition)
|
||||
@@ -100,8 +101,9 @@ class GraspingHands(Object):
|
||||
# Check if player is caught by crumbling
|
||||
playerCaught = False
|
||||
if not player.isJumping:
|
||||
if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or \
|
||||
(self.crumbleDirection < 0 and player.xPos >= self.crumblePosition):
|
||||
if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or (
|
||||
self.crumbleDirection < 0 and player.xPos >= self.crumblePosition
|
||||
):
|
||||
playerCaught = True
|
||||
|
||||
if playerCaught:
|
||||
@@ -122,6 +124,6 @@ class GraspingHands(Object):
|
||||
def __del__(self):
|
||||
"""Cleanup 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)
|
||||
self.crumbleChannel = None
|
||||
|
||||
+40
-8
@@ -8,14 +8,11 @@ from src.powerup import PowerUp
|
||||
class GraveObject(Object):
|
||||
def __init__(self, x, y, sounds, item=None, zombieSpawnChance=0):
|
||||
super().__init__(
|
||||
x, y, "grave",
|
||||
isStatic=True,
|
||||
isCollectible=False,
|
||||
isHazard=True,
|
||||
zombieSpawnChance=zombieSpawnChance
|
||||
x, y, "grave", 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.isFilled = False # Track if grave has been filled with shovel
|
||||
self.sounds = sounds
|
||||
|
||||
def collect_grave_item(self, player):
|
||||
@@ -28,9 +25,44 @@ class GraveObject(Object):
|
||||
if not self.graveItem or self.isCollected:
|
||||
return False
|
||||
|
||||
# Collect the item if player is ducking
|
||||
if player.isDucking:
|
||||
# Collect the item if player is ducking, walking (not running), and wielding shovel
|
||||
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
|
||||
return True
|
||||
|
||||
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
@@ -6,6 +6,7 @@ from enum import Enum, auto
|
||||
|
||||
class ItemType(Enum):
|
||||
"""Defines available item types and their properties"""
|
||||
|
||||
GUTS = auto()
|
||||
HAND_OF_GLORY = auto()
|
||||
JACK_O_LANTERN = auto()
|
||||
@@ -13,14 +14,12 @@ class ItemType(Enum):
|
||||
CAULDRON = auto()
|
||||
WITCH_BROOM = auto()
|
||||
|
||||
|
||||
class ItemProperties:
|
||||
"""Manages item properties and availability"""
|
||||
|
||||
# Items that can appear in random drops
|
||||
RANDOM_ELIGIBLE = {
|
||||
ItemType.HAND_OF_GLORY: "hand_of_glory",
|
||||
ItemType.JACK_O_LANTERN: "jack_o_lantern"
|
||||
}
|
||||
RANDOM_ELIGIBLE = {ItemType.HAND_OF_GLORY: "hand_of_glory", ItemType.JACK_O_LANTERN: "jack_o_lantern"}
|
||||
|
||||
# All possible items (including special ones)
|
||||
ALL_ITEMS = {
|
||||
@@ -29,7 +28,7 @@ class ItemProperties:
|
||||
ItemType.JACK_O_LANTERN: "jack_o_lantern",
|
||||
ItemType.EXTRA_LIFE: "extra_life",
|
||||
ItemType.CAULDRON: "cauldron",
|
||||
ItemType.WITCH_BROOM: "witch_broom"
|
||||
ItemType.WITCH_BROOM: "witch_broom",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
+130
-82
@@ -16,15 +16,16 @@ from src.skull_storm import SkullStorm
|
||||
|
||||
|
||||
class Level:
|
||||
def __init__(self, levelData, sounds, player):
|
||||
def __init__(self, levelData, sounds, player, levelPackName=None):
|
||||
self.sounds = sounds
|
||||
self.levelPackName = levelPackName
|
||||
self.objects = []
|
||||
self.enemies = []
|
||||
self.bouncing_items = []
|
||||
self.projectiles = [] # Track active projectiles
|
||||
self.player = player
|
||||
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.leftBoundary = levelData["boundaries"]["left"]
|
||||
@@ -40,23 +41,35 @@ class Level:
|
||||
# Pass footstep sound to player
|
||||
self.player.set_footstep_sound(self.footstepSound)
|
||||
|
||||
# Level intro message
|
||||
levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. "
|
||||
if self.isLocked:
|
||||
levelIntro += "This is a boss level. You must defeat all enemies before you can advance. "
|
||||
levelIntro += levelData['description']
|
||||
messagebox(levelIntro)
|
||||
# Level intro message (skip for survival mode)
|
||||
if levelData["level_id"] != 999: # 999 is survival mode
|
||||
levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. "
|
||||
if self.isLocked:
|
||||
levelIntro += "This is a boss level. You must defeat all enemies before you can advance. "
|
||||
levelIntro += levelData["description"]
|
||||
messagebox(levelIntro)
|
||||
|
||||
# Handle level music
|
||||
try:
|
||||
pygame.mixer.music.stop()
|
||||
if "ambience" in levelData:
|
||||
try:
|
||||
pygame.mixer.music.load(f"sounds/ambience/{levelData['ambience']}")
|
||||
pygame.mixer.music.play(-1) # Loop indefinitely
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
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:
|
||||
pygame.mixer.music.load(ambiencePath)
|
||||
pygame.mixer.music.play(-1) # Loop indefinitely
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Create end of level object at right boundary
|
||||
@@ -66,7 +79,7 @@ class Level:
|
||||
"end_of_level",
|
||||
isStatic=True,
|
||||
isCollectible=False,
|
||||
isHazard=False
|
||||
isHazard=False,
|
||||
)
|
||||
self.objects.append(endLevel)
|
||||
|
||||
@@ -85,7 +98,7 @@ class Level:
|
||||
obj["y"],
|
||||
self.sounds,
|
||||
fireInterval=obj.get("fireInterval", 5000),
|
||||
firingRange=obj.get("range", 20)
|
||||
firingRange=obj.get("range", 20),
|
||||
)
|
||||
self.objects.append(catapult)
|
||||
# Check if this is grasping hands
|
||||
@@ -95,7 +108,7 @@ class Level:
|
||||
obj["y"],
|
||||
self.sounds,
|
||||
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)
|
||||
# Check if this is a grave
|
||||
@@ -105,7 +118,7 @@ class Level:
|
||||
obj["y"],
|
||||
self.sounds,
|
||||
item=obj.get("item", None),
|
||||
zombieSpawnChance=obj.get("zombie_spawn_chance", 0)
|
||||
zombieSpawnChance=obj.get("zombie_spawn_chance", 0),
|
||||
)
|
||||
self.objects.append(grave)
|
||||
# Check if this is a skull storm
|
||||
@@ -117,7 +130,7 @@ class Level:
|
||||
obj.get("damage", 5),
|
||||
obj.get("maximum_skulls", 3),
|
||||
obj.get("frequency", {}).get("min", 2),
|
||||
obj.get("frequency", {}).get("max", 5)
|
||||
obj.get("frequency", {}).get("max", 5),
|
||||
)
|
||||
self.objects.append(skullStorm)
|
||||
# Check if this is a coffin
|
||||
@@ -127,7 +140,7 @@ class Level:
|
||||
obj["y"],
|
||||
self.sounds,
|
||||
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)
|
||||
# Check if this is a spider web
|
||||
@@ -135,8 +148,7 @@ class Level:
|
||||
# Check distance from graves
|
||||
isValidPosition = True
|
||||
for existingObj in self.objects:
|
||||
if (existingObj.soundName == "grave" and
|
||||
not hasattr(existingObj, 'graveItem')):
|
||||
if existingObj.soundName == "grave" and not hasattr(existingObj, "graveItem"):
|
||||
distance = abs(obj["x"] - existingObj.xPos)
|
||||
if distance < 3:
|
||||
isValidPosition = False
|
||||
@@ -163,7 +175,7 @@ class Level:
|
||||
damage=obj.get("damage", 1),
|
||||
attack_range=obj.get("attack_range", 1),
|
||||
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),
|
||||
spawn_type=obj.get("spawn_type", "zombie"),
|
||||
spawn_cooldown=obj.get("spawn_cooldown", 2000),
|
||||
@@ -172,7 +184,7 @@ class Level:
|
||||
has_vulnerability=obj.get("has_vulnerability", False),
|
||||
is_vulnerable=obj.get("is_vulnerable", False),
|
||||
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)
|
||||
else:
|
||||
@@ -183,13 +195,13 @@ class Level:
|
||||
isStatic=obj.get("static", True),
|
||||
isCollectible=obj.get("collectible", False),
|
||||
isHazard=obj.get("hazard", False),
|
||||
zombieSpawnChance=obj.get("zombieSpawnChance", 0)
|
||||
zombieSpawnChance=obj.get("zombieSpawnChance", 0),
|
||||
)
|
||||
self.objects.append(gameObject)
|
||||
enemyCount = len(self.enemies)
|
||||
coffinCount = sum(1 for obj in self.objects if hasattr(obj, 'isBroken'))
|
||||
player.stats.update_stat('Enemies remaining', enemyCount)
|
||||
player.stats.update_stat('Coffins remaining', coffinCount)
|
||||
coffinCount = sum(1 for obj in self.objects if hasattr(obj, "isBroken"))
|
||||
player.stats.update_stat("Enemies remaining", enemyCount)
|
||||
player.stats.update_stat("Coffins remaining", coffinCount)
|
||||
|
||||
def update_audio(self):
|
||||
"""Update all audio and entity state."""
|
||||
@@ -201,9 +213,7 @@ class Level:
|
||||
continue
|
||||
|
||||
# Check for potential zombie spawn from graves
|
||||
if (obj.soundName == "grave" and
|
||||
obj.zombieSpawnChance > 0 and
|
||||
not obj.hasSpawned):
|
||||
if obj.soundName == "grave" and obj.zombieSpawnChance > 0 and not obj.hasSpawned:
|
||||
|
||||
distance = abs(self.player.xPos - obj.xPos)
|
||||
if distance < 6: # Within 6 tiles
|
||||
@@ -212,6 +222,10 @@ class Level:
|
||||
|
||||
roll = random.randint(1, 100)
|
||||
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(
|
||||
[obj.xPos, obj.xPos],
|
||||
obj.yPos,
|
||||
@@ -220,7 +234,9 @@ class Level:
|
||||
self, # Pass the level reference
|
||||
health=3,
|
||||
damage=10,
|
||||
attack_range=1
|
||||
attack_range=1,
|
||||
attack_pattern={"type": behavior},
|
||||
turn_rate=turn_rate,
|
||||
)
|
||||
self.enemies.append(zombie)
|
||||
speak("A zombie emerges from the grave!")
|
||||
@@ -265,6 +281,16 @@ class Level:
|
||||
if caught:
|
||||
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
|
||||
for item in self.bouncing_items[:]: # Copy list to allow removal
|
||||
if not item.update(currentTime, self.player.xPos):
|
||||
@@ -275,8 +301,8 @@ class Level:
|
||||
|
||||
# Check for item collection
|
||||
if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping:
|
||||
play_sound(self.sounds[f'get_{item.soundName}'])
|
||||
item.apply_effect(self.player)
|
||||
play_sound(self.sounds[f"get_{item.soundName}"])
|
||||
item.apply_effect(self.player, self)
|
||||
self.levelScore += 1000 # All items collected points awarded
|
||||
item.isActive = False
|
||||
self.bouncing_items.remove(item)
|
||||
@@ -297,11 +323,13 @@ class Level:
|
||||
|
||||
# Check for coffin hits
|
||||
for obj in self.objects:
|
||||
if hasattr(obj, 'isBroken'): # Check if it's a coffin without using isinstance
|
||||
if (not obj.isBroken and
|
||||
obj.xPos >= attackRange[0] and
|
||||
obj.xPos <= attackRange[1] and
|
||||
self.player.isJumping): # Must be jumping to hit floating coffins
|
||||
if hasattr(obj, "isBroken"): # Check if it's a coffin without using isinstance
|
||||
if (
|
||||
not obj.isBroken
|
||||
and obj.xPos >= attackRange[0]
|
||||
and obj.xPos <= attackRange[1]
|
||||
and self.player.isJumping
|
||||
): # Must be jumping to hit floating coffins
|
||||
|
||||
if obj.hit(self.player.xPos):
|
||||
self.bouncing_items.append(obj.dropped_item)
|
||||
@@ -317,7 +345,7 @@ class Level:
|
||||
health=8,
|
||||
damage=8,
|
||||
attack_range=1,
|
||||
speed_multiplier=2.0
|
||||
speed_multiplier=2.0,
|
||||
)
|
||||
self.enemies.append(spider)
|
||||
|
||||
@@ -336,62 +364,71 @@ class Level:
|
||||
continue
|
||||
|
||||
# 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)
|
||||
currentTime = pygame.time.get_ticks()
|
||||
if (distance <= 2 and not self.player.isJumping and not self.player.isInvincible
|
||||
and currentTime - self.lastWarningTime >= self.warningInterval):
|
||||
if (
|
||||
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:
|
||||
play_sound(self.sounds['_edge'])
|
||||
play_sound(self.sounds["_edge"])
|
||||
else:
|
||||
play_sound(self.sounds['edge'])
|
||||
play_sound(self.sounds["edge"])
|
||||
self.lastWarningTime = currentTime
|
||||
|
||||
if not obj.is_in_range(self.player.xPos):
|
||||
continue
|
||||
|
||||
# Handle collectibles
|
||||
if obj.isCollectible and self.player.isJumping:
|
||||
# Handle collectibles (but not if player died this frame)
|
||||
if obj.isCollectible and self.player.isJumping and not self.player.diedThisFrame:
|
||||
currentPos = round(self.player.xPos)
|
||||
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)
|
||||
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":
|
||||
self.player._coins += 1
|
||||
self.player.add_save_bone_dust(1) # Add to save bone dust counter too
|
||||
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:
|
||||
# Only heal if below max health
|
||||
if self.player.get_health() < self.player.get_max_health():
|
||||
self.player.set_health(min(
|
||||
self.player.get_health() + 1,
|
||||
self.player.get_max_health()
|
||||
))
|
||||
self.player.set_health(min(self.player.get_health() + 1, self.player.get_max_health()))
|
||||
|
||||
if self.player._coins % 100 == 0:
|
||||
# Extra life
|
||||
self.player._coins = 0
|
||||
self.player._lives += 1
|
||||
self.levelScore += 1000
|
||||
play_sound(self.sounds['get_extra_life'])
|
||||
# Only give extra lives in story mode, not survival mode (level_id 999)
|
||||
if self.levelId != 999:
|
||||
# Extra life
|
||||
self.player._coins = 0
|
||||
self.player._lives += 1
|
||||
self.levelScore += 1000
|
||||
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
|
||||
|
||||
# Handle spiderweb - this should trigger for both walking and jumping if not ducking
|
||||
if obj.soundName == "spiderweb" and not self.player.isDucking:
|
||||
# Create and apply web effect
|
||||
webEffect = PowerUp(
|
||||
obj.xPos,
|
||||
obj.yPos,
|
||||
'spiderweb',
|
||||
self.sounds,
|
||||
0 # No direction needed since it's just for effect
|
||||
obj.xPos, obj.yPos, "spiderweb", self.sounds, 0 # No direction needed since it's just for effect
|
||||
)
|
||||
webEffect.level = self # Pass level reference for spider spawning
|
||||
play_sound(self.sounds['hit_spiderweb'])
|
||||
webEffect.apply_effect(self.player)
|
||||
play_sound(self.sounds["hit_spiderweb"])
|
||||
webEffect.apply_effect(self.player, self)
|
||||
|
||||
# Deactivate web
|
||||
obj.isActive = False
|
||||
@@ -401,16 +438,19 @@ class Level:
|
||||
# Handle graves and other hazards
|
||||
if obj.isHazard and not self.player.isJumping:
|
||||
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:
|
||||
# Successfully collected item while ducking
|
||||
play_sound(self.sounds[f'get_{obj.graveItem}'])
|
||||
self.player.stats.update_stat('Items collected', 1)
|
||||
# Successfully collected item while ducking with shovel
|
||||
play_sound(self.sounds[f"get_{obj.graveItem}"])
|
||||
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
|
||||
item = PowerUp(obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1,
|
||||
self.leftBoundary, self.rightBoundary)
|
||||
item.apply_effect(self.player)
|
||||
item = PowerUp(
|
||||
obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1, self.leftBoundary, self.rightBoundary
|
||||
)
|
||||
item.apply_effect(self.player, self)
|
||||
# Stop grave's current audio channel
|
||||
if obj.channel:
|
||||
obj_stop(obj.channel)
|
||||
@@ -419,6 +459,18 @@ class Level:
|
||||
obj.channel = None
|
||||
obj.isActive = False # Mark the grave as inactive after collection
|
||||
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:
|
||||
# Kill player for normal graves or non-ducking collision
|
||||
play_sound(self.sounds[obj.soundName])
|
||||
@@ -440,14 +492,14 @@ class Level:
|
||||
# If level is locked, check for remaining enemies
|
||||
if self.isLocked and any(enemy.isActive for enemy in self.enemies):
|
||||
speak("You must defeat all enemies before proceeding!")
|
||||
play_sound(self.sounds['locked'])
|
||||
play_sound(self.sounds["locked"])
|
||||
# Push player back a bit
|
||||
self.player.xPos -= 5
|
||||
return False
|
||||
|
||||
# Level complete
|
||||
pygame.mixer.stop()
|
||||
play_sound(self.sounds['end_of_level'])
|
||||
play_sound(self.sounds["end_of_level"])
|
||||
self.levelScore += 10000
|
||||
# Actually update the scoreboard with level completion
|
||||
self.player.scoreboard.increase_score(self.levelScore)
|
||||
@@ -470,7 +522,7 @@ class Level:
|
||||
# Calculate volume and pan for splat sound based on final position
|
||||
volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x)
|
||||
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
|
||||
|
||||
def throw_projectile(self):
|
||||
@@ -480,10 +532,6 @@ class Level:
|
||||
speak("No jack o'lanterns to throw!")
|
||||
return
|
||||
|
||||
self.projectiles.append(Projectile(
|
||||
proj_info['type'],
|
||||
proj_info['start_x'],
|
||||
proj_info['direction']
|
||||
))
|
||||
self.projectiles.append(Projectile(proj_info["type"], proj_info["start_x"], proj_info["direction"]))
|
||||
# Play throw sound
|
||||
play_sound(self.sounds['throw_jack_o_lantern'])
|
||||
play_sound(self.sounds["throw_jack_o_lantern"])
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from libstormgames import *
|
||||
|
||||
|
||||
class Object:
|
||||
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]
|
||||
|
||||
@@ -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}")
|
||||
+76
-32
@@ -59,28 +59,35 @@ class Player:
|
||||
self.invincibilityStartTime = 0
|
||||
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)
|
||||
self.add_weapon(Weapon(
|
||||
name="rusty_shovel",
|
||||
damage=2,
|
||||
range=2,
|
||||
attackSound="player_shovel_attack",
|
||||
hitSound="player_shovel_hit",
|
||||
attackDuration=200 # 200ms attack duration
|
||||
))
|
||||
self.add_weapon(
|
||||
Weapon(
|
||||
name="rusty_shovel",
|
||||
damage=2,
|
||||
range=2,
|
||||
attackSound="player_shovel_attack",
|
||||
hitSound="player_shovel_hit",
|
||||
attackDuration=200, # 200ms attack duration
|
||||
)
|
||||
)
|
||||
|
||||
self.scoreboard = Scoreboard()
|
||||
|
||||
def should_play_footstep(self, currentTime):
|
||||
"""Check if it's time to play a footstep sound"""
|
||||
return (self.distanceSinceLastStep >= self.get_step_distance() and
|
||||
currentTime - self.lastStepTime >= self.get_step_interval())
|
||||
return (
|
||||
self.distanceSinceLastStep >= self.get_step_distance()
|
||||
and currentTime - self.lastStepTime >= self.get_step_interval()
|
||||
)
|
||||
|
||||
def duck(self):
|
||||
"""Start ducking"""
|
||||
if not self.isDucking and not self.isJumping: # Can't duck while jumping
|
||||
self.isDucking = True
|
||||
play_sound(self.sounds['duck'])
|
||||
play_sound(self.sounds["duck"])
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -88,11 +95,14 @@ class Player:
|
||||
"""Stop ducking state and play sound"""
|
||||
if self.isDucking:
|
||||
self.isDucking = False
|
||||
play_sound(self.sounds['stand'])
|
||||
play_sound(self.sounds["stand"])
|
||||
|
||||
def update(self, currentTime):
|
||||
"""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:
|
||||
self.moveSpeed *= 2 # Restore speed
|
||||
if self.currentWeapon:
|
||||
@@ -101,15 +111,17 @@ class Player:
|
||||
|
||||
# Check invincibility status
|
||||
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
|
||||
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
|
||||
|
||||
current_second = int(remaining_time)
|
||||
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
|
||||
|
||||
# Check if invincibility has expired
|
||||
@@ -122,7 +134,7 @@ class Player:
|
||||
"""Activate invincibility from Hand of Glory"""
|
||||
self.isInvincible = True
|
||||
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
|
||||
|
||||
def extra_life(self):
|
||||
@@ -150,23 +162,27 @@ class Player:
|
||||
return None
|
||||
|
||||
self._jack_o_lantern_count -= 1
|
||||
return {
|
||||
'type': 'jack_o_lantern',
|
||||
'start_x': self.xPos,
|
||||
'direction': 1 if self.facingRight else -1
|
||||
}
|
||||
return {"type": "jack_o_lantern", "start_x": self.xPos, "direction": 1 if self.facingRight else -1}
|
||||
|
||||
def get_step_distance(self):
|
||||
"""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:
|
||||
return self.baseStepDistance / self.runMultiplier
|
||||
return self.baseStepDistance
|
||||
totalMultiplier *= self.runMultiplier
|
||||
|
||||
return self.baseStepDistance / totalMultiplier
|
||||
|
||||
def get_step_interval(self):
|
||||
"""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:
|
||||
return self.baseStepInterval / self.runMultiplier
|
||||
return self.baseStepInterval
|
||||
totalMultiplier *= self.runMultiplier
|
||||
|
||||
return self.baseStepInterval / totalMultiplier
|
||||
|
||||
def get_health(self):
|
||||
"""Get current health"""
|
||||
@@ -181,10 +197,18 @@ class Player:
|
||||
self._health = self._maxHealth
|
||||
|
||||
def get_current_speed(self):
|
||||
"""Calculate current speed based on state"""
|
||||
"""Calculate current speed based on state and weapon"""
|
||||
baseSpeed = self.moveSpeed
|
||||
if self.isJumping or self.isRunning: return baseSpeed * self.runMultiplier
|
||||
return baseSpeed
|
||||
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
|
||||
|
||||
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):
|
||||
"""Set the current footstep sound"""
|
||||
@@ -196,20 +220,22 @@ class Player:
|
||||
|
||||
# Oops, allow healing while invincible.
|
||||
if self.isInvincible and value < old_health:
|
||||
return
|
||||
return
|
||||
|
||||
self._health = max(0, value) # Health can't go below 0
|
||||
|
||||
if self._health == 0 and old_health > 0:
|
||||
self._lives -= 1
|
||||
# Mark that player died this frame to prevent revival
|
||||
self.diedThisFrame = True
|
||||
# Stop all current sounds before playing death sound
|
||||
pygame.mixer.stop()
|
||||
try:
|
||||
pygame.mixer.music.stop()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cut_scene(self.sounds, 'lose_a_life')
|
||||
cut_scene(self.sounds, "lose_a_life")
|
||||
|
||||
def set_max_health(self, value):
|
||||
"""Set max health"""
|
||||
@@ -253,6 +279,24 @@ class Player:
|
||||
if weapon in self.weapons:
|
||||
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):
|
||||
"""Add an item to inventory"""
|
||||
self.inventory.append(item)
|
||||
|
||||
+49
-28
@@ -5,14 +5,10 @@ from libstormgames import *
|
||||
from src.object import Object
|
||||
from src.weapon import Weapon
|
||||
|
||||
|
||||
class PowerUp(Object):
|
||||
def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100):
|
||||
super().__init__(
|
||||
x, y, item_type,
|
||||
isStatic=False,
|
||||
isCollectible=True,
|
||||
isHazard=False
|
||||
)
|
||||
super().__init__(x, y, item_type, isStatic=False, isCollectible=True, isHazard=False)
|
||||
self.sounds = sounds
|
||||
self.direction = direction
|
||||
self.speed = 0.049 # Base movement speed
|
||||
@@ -56,35 +52,58 @@ class PowerUp(Object):
|
||||
|
||||
return True
|
||||
|
||||
def apply_effect(self, player):
|
||||
def apply_effect(self, player, level=None):
|
||||
"""Apply the item's effect when collected"""
|
||||
if self.item_type == 'hand_of_glory':
|
||||
if self.item_type == "hand_of_glory":
|
||||
player.start_invincibility()
|
||||
elif self.item_type == 'cauldron':
|
||||
elif self.item_type == "cauldron":
|
||||
player.restore_health()
|
||||
elif self.item_type == 'guts':
|
||||
elif self.item_type == "guts":
|
||||
player.add_guts()
|
||||
player.collectedItems.append('guts')
|
||||
player.collectedItems.append("guts")
|
||||
self.check_for_nunchucks(player)
|
||||
elif self.item_type == 'jack_o_lantern':
|
||||
elif self.item_type == "jack_o_lantern":
|
||||
player.add_jack_o_lantern()
|
||||
elif self.item_type == 'extra_life':
|
||||
player.extra_life()
|
||||
elif self.item_type == 'shin_bone': # Add shin bone handling
|
||||
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()
|
||||
elif self.item_type == "shin_bone": # Add shin bone handling
|
||||
player.shinBoneCount += 1
|
||||
player._coins += 5
|
||||
player.add_save_bone_dust(5) # Add to save bone dust counter too
|
||||
if player.get_health() < player.get_max_health():
|
||||
player.set_health(min(
|
||||
player.get_health() + 1,
|
||||
player.get_max_health()
|
||||
))
|
||||
player.set_health(min(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)
|
||||
elif self.item_type == 'witch_broom':
|
||||
elif self.item_type == "witch_broom":
|
||||
broomWeapon = Weapon.create_witch_broom()
|
||||
player.add_weapon(broomWeapon)
|
||||
player.equip_weapon(broomWeapon)
|
||||
elif self.item_type == 'spiderweb':
|
||||
elif self.item_type == "spiderweb":
|
||||
# Bounce player back (happens even if invincible)
|
||||
player.xPos -= 3 if player.xPos > self.xPos else -3
|
||||
|
||||
@@ -98,7 +117,7 @@ class PowerUp(Object):
|
||||
player.webPenaltyEndTime = pygame.time.get_ticks() + 15000
|
||||
|
||||
# Tell level to spawn a spider
|
||||
if hasattr(self, 'level'):
|
||||
if hasattr(self, "level"):
|
||||
self.level.spawn_spider(self.xPos, self.yPos)
|
||||
|
||||
# Stop movement sound when collected
|
||||
@@ -107,18 +126,20 @@ class PowerUp(Object):
|
||||
self.channel = None
|
||||
|
||||
# Item tracking
|
||||
player.stats.update_stat('Items collected', 1)
|
||||
player.stats.update_stat("Items collected", 1)
|
||||
|
||||
def check_for_nunchucks(self, player):
|
||||
"""Check if player has materials for nunchucks and create if conditions are met"""
|
||||
if (player.shinBoneCount >= 2 and
|
||||
'guts' in player.collectedItems and
|
||||
not any(weapon.name == "nunchucks" for weapon in player.weapons)):
|
||||
if (
|
||||
player.shinBoneCount >= 2
|
||||
and "guts" in player.collectedItems
|
||||
and not any(weapon.name == "nunchucks" for weapon in player.weapons)
|
||||
):
|
||||
nunchucksWeapon = Weapon.create_nunchucks()
|
||||
player.add_weapon(nunchucksWeapon)
|
||||
player.equip_weapon(nunchucksWeapon)
|
||||
basePoints = nunchucksWeapon.damage * 1000
|
||||
rangeModifier = nunchucksWeapon.range * 500
|
||||
player.scoreboard.increase_score(basePoints + rangeModifier)
|
||||
play_sound(self.sounds['get_nunchucks'])
|
||||
player.stats.update_stat('Items collected', 1)
|
||||
play_sound(self.sounds["get_nunchucks"])
|
||||
player.stats.update_stat("Items collected", 1)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class Projectile:
|
||||
def __init__(self, projectile_type, start_x, direction):
|
||||
self.type = projectile_type
|
||||
|
||||
+135
-116
@@ -11,8 +11,8 @@ class SaveManager:
|
||||
def __init__(self):
|
||||
"""Initialize save manager with XDG-compliant save directory"""
|
||||
# Use XDG_CONFIG_HOME or default to ~/.config
|
||||
config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
|
||||
self.save_dir = Path(config_home) / 'storm-games' / 'wicked-quest'
|
||||
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.mkdir(parents=True, exist_ok=True)
|
||||
self.max_saves = 10
|
||||
|
||||
@@ -24,7 +24,7 @@ class SaveManager:
|
||||
# Validate required parameters
|
||||
if current_game is None:
|
||||
return False, "No game selected to save"
|
||||
|
||||
|
||||
if current_level is None:
|
||||
return False, "No current level to save"
|
||||
|
||||
@@ -34,30 +34,30 @@ class SaveManager:
|
||||
|
||||
# Create save data
|
||||
save_data = {
|
||||
'player_state': {
|
||||
'xPos': player.xPos,
|
||||
'yPos': player.yPos,
|
||||
'health': player._health,
|
||||
'maxHealth': player._maxHealth,
|
||||
'lives': player._lives,
|
||||
'coins': player._coins,
|
||||
'saveBoneDust': player._saveBoneDust,
|
||||
'jackOLanternCount': player._jack_o_lantern_count,
|
||||
'shinBoneCount': player.shinBoneCount,
|
||||
'inventory': player.inventory,
|
||||
'collectedItems': player.collectedItems,
|
||||
'weapons': self._serialize_weapons(player.weapons),
|
||||
'currentWeaponName': player.currentWeapon.name if player.currentWeapon else None,
|
||||
'stats': self._serialize_stats(player.stats),
|
||||
'scoreboard': self._serialize_scoreboard(player.scoreboard)
|
||||
"player_state": {
|
||||
"xPos": player.xPos,
|
||||
"yPos": player.yPos,
|
||||
"health": player._health,
|
||||
"maxHealth": player._maxHealth,
|
||||
"lives": player._lives,
|
||||
"coins": player._coins,
|
||||
"saveBoneDust": player._saveBoneDust,
|
||||
"jackOLanternCount": player._jack_o_lantern_count,
|
||||
"shinBoneCount": player.shinBoneCount,
|
||||
"inventory": player.inventory,
|
||||
"collectedItems": player.collectedItems,
|
||||
"weapons": self._serialize_weapons(player.weapons),
|
||||
"currentWeaponName": player.currentWeapon.name if player.currentWeapon else None,
|
||||
"stats": self._serialize_stats(player.stats),
|
||||
"scoreboard": self._serialize_scoreboard(player.scoreboard),
|
||||
},
|
||||
'game_state': {
|
||||
'currentLevel': current_level,
|
||||
'currentGame': current_game,
|
||||
'gameStartTime': game_start_time,
|
||||
'saveTime': datetime.now()
|
||||
"game_state": {
|
||||
"currentLevel": current_level,
|
||||
"currentGame": current_game,
|
||||
"gameStartTime": game_start_time,
|
||||
"saveTime": datetime.now(),
|
||||
},
|
||||
'version': '1.0'
|
||||
"version": "1.0",
|
||||
}
|
||||
|
||||
# Generate filename with timestamp
|
||||
@@ -67,26 +67,26 @@ class SaveManager:
|
||||
|
||||
try:
|
||||
# Write to temporary file first, then rename for atomic operation
|
||||
temp_filepath = filepath.with_suffix('.tmp')
|
||||
|
||||
with open(temp_filepath, 'wb') as f:
|
||||
temp_filepath = filepath.with_suffix(".tmp")
|
||||
|
||||
with open(temp_filepath, "wb") as f:
|
||||
pickle.dump(save_data, f)
|
||||
f.flush() # Ensure data is written to disk
|
||||
os.fsync(f.fileno()) # Force write to disk
|
||||
|
||||
|
||||
# Atomic rename (replaces old file if it exists)
|
||||
temp_filepath.rename(filepath)
|
||||
|
||||
|
||||
# Clean up old saves if we exceed max_saves
|
||||
self._cleanup_old_saves()
|
||||
|
||||
|
||||
return True, f"Game saved to {filename}"
|
||||
except Exception as e:
|
||||
# Clean up temp file if it exists
|
||||
if temp_filepath.exists():
|
||||
try:
|
||||
temp_filepath.unlink()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return False, f"Failed to save game: {str(e)}"
|
||||
|
||||
@@ -94,119 +94,136 @@ class SaveManager:
|
||||
"""Serialize weapons for saving"""
|
||||
serialized = []
|
||||
for weapon in weapons:
|
||||
serialized.append({
|
||||
'name': weapon.name,
|
||||
'damage': weapon.damage,
|
||||
'range': weapon.range,
|
||||
'attackSound': weapon.attackSound,
|
||||
'hitSound': weapon.hitSound,
|
||||
'attackDuration': weapon.attackDuration
|
||||
})
|
||||
serialized.append(
|
||||
{
|
||||
"name": weapon.name,
|
||||
"damage": weapon.damage,
|
||||
"range": weapon.range,
|
||||
"attackSound": weapon.attackSound,
|
||||
"hitSound": weapon.hitSound,
|
||||
"attackDuration": weapon.attackDuration,
|
||||
"speedBonus": getattr(weapon, "speedBonus", 1.0),
|
||||
"jumpDurationBonus": getattr(weapon, "jumpDurationBonus", 1.0),
|
||||
}
|
||||
)
|
||||
return serialized
|
||||
|
||||
def _deserialize_weapons(self, weapon_data):
|
||||
"""Deserialize weapons from save data"""
|
||||
from src.weapon import Weapon
|
||||
|
||||
weapons = []
|
||||
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(
|
||||
name=data['name'],
|
||||
damage=data['damage'],
|
||||
range=data['range'],
|
||||
attackSound=data['attackSound'],
|
||||
hitSound=data['hitSound'],
|
||||
attackDuration=data['attackDuration']
|
||||
name=data["name"],
|
||||
damage=data["damage"],
|
||||
range=data["range"],
|
||||
attackSound=data["attackSound"],
|
||||
hitSound=data["hitSound"],
|
||||
attackDuration=data["attackDuration"],
|
||||
speedBonus=speedBonus,
|
||||
jumpDurationBonus=jumpDurationBonus,
|
||||
)
|
||||
weapons.append(weapon)
|
||||
return weapons
|
||||
|
||||
def _serialize_stats(self, stats):
|
||||
"""Serialize stats for saving"""
|
||||
return {
|
||||
'total': stats.total.copy(),
|
||||
'level': stats.level.copy()
|
||||
}
|
||||
return {"total": stats.total.copy(), "level": stats.level.copy()}
|
||||
|
||||
def _deserialize_stats(self, stats_data):
|
||||
"""Deserialize stats from save data"""
|
||||
from src.stat_tracker import StatTracker
|
||||
|
||||
stats = StatTracker()
|
||||
if 'total' in stats_data:
|
||||
stats.total.update(stats_data['total'])
|
||||
if 'level' in stats_data:
|
||||
stats.level.update(stats_data['level'])
|
||||
if "total" in stats_data:
|
||||
stats.total.update(stats_data["total"])
|
||||
if "level" in stats_data:
|
||||
stats.level.update(stats_data["level"])
|
||||
return stats
|
||||
|
||||
def _serialize_scoreboard(self, scoreboard):
|
||||
"""Serialize scoreboard for saving"""
|
||||
return {
|
||||
'currentScore': getattr(scoreboard, 'currentScore', 0),
|
||||
'highScores': getattr(scoreboard, 'highScores', [])
|
||||
"currentScore": getattr(scoreboard, "currentScore", 0),
|
||||
"highScores": getattr(scoreboard, "highScores", []),
|
||||
}
|
||||
|
||||
def _deserialize_scoreboard(self, scoreboard_data):
|
||||
"""Deserialize scoreboard from save data"""
|
||||
from libstormgames import Scoreboard
|
||||
|
||||
scoreboard = Scoreboard()
|
||||
if 'currentScore' in scoreboard_data:
|
||||
scoreboard.currentScore = scoreboard_data['currentScore']
|
||||
if 'highScores' in scoreboard_data:
|
||||
scoreboard.highScores = scoreboard_data['highScores']
|
||||
if "currentScore" in scoreboard_data:
|
||||
scoreboard.currentScore = scoreboard_data["currentScore"]
|
||||
if "highScores" in scoreboard_data:
|
||||
scoreboard.highScores = scoreboard_data["highScores"]
|
||||
return scoreboard
|
||||
|
||||
def get_save_files(self):
|
||||
"""Get list of save files with metadata"""
|
||||
save_files = []
|
||||
pattern = str(self.save_dir / "save_*.pickle")
|
||||
|
||||
|
||||
for filepath in glob.glob(pattern):
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
with open(filepath, "rb") as f:
|
||||
save_data = pickle.load(f)
|
||||
|
||||
|
||||
# Validate save data structure
|
||||
if not self._validate_save_data(save_data):
|
||||
print(f"Invalid save file structure: {filepath}")
|
||||
continue
|
||||
|
||||
|
||||
# Extract save info
|
||||
save_time = save_data['game_state']['saveTime']
|
||||
level = save_data['game_state']['currentLevel']
|
||||
game_name = save_data['game_state']['currentGame']
|
||||
|
||||
save_time = save_data["game_state"]["saveTime"]
|
||||
level = save_data["game_state"]["currentLevel"]
|
||||
game_name = save_data["game_state"]["currentGame"]
|
||||
|
||||
# Format display name
|
||||
formatted_time = save_time.strftime("%B %d %I:%M%p")
|
||||
display_name = f"{formatted_time} {game_name} Level {level}"
|
||||
|
||||
save_files.append({
|
||||
'filepath': filepath,
|
||||
'display_name': display_name,
|
||||
'save_time': save_time,
|
||||
'level': level,
|
||||
'game_name': game_name,
|
||||
'save_data': save_data
|
||||
})
|
||||
|
||||
save_files.append(
|
||||
{
|
||||
"filepath": filepath,
|
||||
"display_name": display_name,
|
||||
"save_time": save_time,
|
||||
"level": level,
|
||||
"game_name": game_name,
|
||||
"save_data": save_data,
|
||||
}
|
||||
)
|
||||
except (pickle.PickleError, EOFError, OSError) as e:
|
||||
print(f"Corrupted save file {filepath}: {e}")
|
||||
# Try to remove corrupted save file
|
||||
try:
|
||||
os.remove(filepath)
|
||||
print(f"Removed corrupted save file: {filepath}")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Error reading save file {filepath}: {e}")
|
||||
continue
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
def load_save(self, filepath):
|
||||
"""Load game state from save file"""
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
with open(filepath, "rb") as f:
|
||||
save_data = pickle.load(f)
|
||||
return True, save_data
|
||||
except Exception as e:
|
||||
@@ -214,55 +231,57 @@ class SaveManager:
|
||||
|
||||
def restore_player_state(self, player, save_data):
|
||||
"""Restore player state from save data"""
|
||||
player_state = save_data['player_state']
|
||||
|
||||
player_state = save_data["player_state"]
|
||||
|
||||
# Restore basic attributes
|
||||
player.xPos = player_state['xPos']
|
||||
player.yPos = player_state['yPos']
|
||||
player._health = player_state['health']
|
||||
player._maxHealth = player_state['maxHealth']
|
||||
player._lives = player_state['lives']
|
||||
player._coins = player_state['coins']
|
||||
player._saveBoneDust = player_state['saveBoneDust']
|
||||
player._jack_o_lantern_count = player_state['jackOLanternCount']
|
||||
player.shinBoneCount = player_state['shinBoneCount']
|
||||
player.inventory = player_state['inventory']
|
||||
player.collectedItems = player_state['collectedItems']
|
||||
|
||||
player.xPos = player_state["xPos"]
|
||||
player.yPos = player_state["yPos"]
|
||||
player._health = player_state["health"]
|
||||
player._maxHealth = player_state["maxHealth"]
|
||||
player._lives = player_state["lives"]
|
||||
player._coins = player_state["coins"]
|
||||
player._saveBoneDust = player_state["saveBoneDust"]
|
||||
player._jack_o_lantern_count = player_state["jackOLanternCount"]
|
||||
player.shinBoneCount = player_state["shinBoneCount"]
|
||||
player.inventory = player_state["inventory"]
|
||||
player.collectedItems = player_state["collectedItems"]
|
||||
|
||||
# Restore weapons
|
||||
player.weapons = self._deserialize_weapons(player_state['weapons'])
|
||||
|
||||
player.weapons = self._deserialize_weapons(player_state["weapons"])
|
||||
|
||||
# Restore current weapon
|
||||
current_weapon_name = player_state.get('currentWeaponName')
|
||||
current_weapon_name = player_state.get("currentWeaponName")
|
||||
if current_weapon_name:
|
||||
for weapon in player.weapons:
|
||||
if weapon.name == current_weapon_name:
|
||||
player.currentWeapon = weapon
|
||||
break
|
||||
|
||||
|
||||
# Restore stats
|
||||
if 'stats' in player_state:
|
||||
player.stats = self._deserialize_stats(player_state['stats'])
|
||||
if "stats" in player_state:
|
||||
player.stats = self._deserialize_stats(player_state["stats"])
|
||||
else:
|
||||
from src.stat_tracker import StatTracker
|
||||
|
||||
player.stats = StatTracker()
|
||||
|
||||
|
||||
# Restore scoreboard
|
||||
if 'scoreboard' in player_state:
|
||||
player.scoreboard = self._deserialize_scoreboard(player_state['scoreboard'])
|
||||
if "scoreboard" in player_state:
|
||||
player.scoreboard = self._deserialize_scoreboard(player_state["scoreboard"])
|
||||
else:
|
||||
from libstormgames import Scoreboard
|
||||
|
||||
player.scoreboard = Scoreboard()
|
||||
|
||||
def _cleanup_old_saves(self):
|
||||
"""Remove old save files if we exceed max_saves"""
|
||||
save_files = self.get_save_files()
|
||||
|
||||
|
||||
if len(save_files) > self.max_saves:
|
||||
# Remove oldest saves
|
||||
for save_file in save_files[self.max_saves:]:
|
||||
try:
|
||||
os.remove(save_file['filepath'])
|
||||
os.remove(save_file["filepath"])
|
||||
except Exception as e:
|
||||
print(f"Error removing old save {save_file['filepath']}: {e}")
|
||||
|
||||
@@ -270,24 +289,24 @@ class SaveManager:
|
||||
"""Validate that save data has required structure"""
|
||||
try:
|
||||
# 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):
|
||||
return False
|
||||
|
||||
|
||||
# Check player_state structure
|
||||
player_required = ['xPos', 'yPos', 'health', 'maxHealth', 'lives', 'coins', 'saveBoneDust']
|
||||
if not all(key in save_data['player_state'] for key in player_required):
|
||||
player_required = ["xPos", "yPos", "health", "maxHealth", "lives", "coins", "saveBoneDust"]
|
||||
if not all(key in save_data["player_state"] for key in player_required):
|
||||
return False
|
||||
|
||||
|
||||
# Check game_state structure
|
||||
game_required = ['currentLevel', 'currentGame', 'gameStartTime', 'saveTime']
|
||||
if not all(key in save_data['game_state'] for key in game_required):
|
||||
game_required = ["currentLevel", "currentGame", "gameStartTime", "saveTime"]
|
||||
if not all(key in save_data["game_state"] for key in game_required):
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def has_saves(self):
|
||||
"""Check if any save files exist"""
|
||||
return len(self.get_save_files()) > 0
|
||||
return len(self.get_save_files()) > 0
|
||||
|
||||
+24
-29
@@ -5,17 +5,13 @@ import random
|
||||
from libstormgames import *
|
||||
from src.object import Object
|
||||
|
||||
|
||||
class SkullStorm(Object):
|
||||
"""Handles falling skulls within a specified range."""
|
||||
|
||||
def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5):
|
||||
super().__init__(
|
||||
xRange,
|
||||
y,
|
||||
"", # No ambient sound for the skull storm
|
||||
isStatic=True,
|
||||
isCollectible=False,
|
||||
isHazard=False
|
||||
xRange, y, "", isStatic=True, isCollectible=False, isHazard=False # No ambient sound for the skull storm
|
||||
)
|
||||
self.sounds = sounds
|
||||
self.damage = damage
|
||||
@@ -37,7 +33,7 @@ class SkullStorm(Object):
|
||||
inRange = self.xRange[0] <= player.xPos <= self.xRange[1]
|
||||
if inRange and not self.playerInRange:
|
||||
# Player just entered range - play the warning sound
|
||||
play_sound(self.sounds['skull_storm'])
|
||||
play_sound(self.sounds["skull_storm"])
|
||||
self.playerInRange = True
|
||||
elif not inRange and self.playerInRange: # Only speak when actually leaving range
|
||||
# Player just left range
|
||||
@@ -46,8 +42,8 @@ class SkullStorm(Object):
|
||||
|
||||
# Clear any active skulls when player leaves the range
|
||||
for skull in self.activeSkulls[:]:
|
||||
if skull['channel']:
|
||||
obj_stop(skull['channel'])
|
||||
if skull["channel"]:
|
||||
obj_stop(skull["channel"])
|
||||
self.activeSkulls = [] # Reset the list of active skulls
|
||||
|
||||
if not inRange:
|
||||
@@ -55,29 +51,28 @@ class SkullStorm(Object):
|
||||
|
||||
# Update existing skulls
|
||||
for skull in self.activeSkulls[:]: # Copy list to allow removal
|
||||
if currentTime >= skull['land_time']:
|
||||
if currentTime >= skull["land_time"]:
|
||||
# Skull has landed
|
||||
self.handle_landing(skull, player)
|
||||
self.activeSkulls.remove(skull)
|
||||
else:
|
||||
# Update falling sound
|
||||
timeElapsed = currentTime - skull['start_time']
|
||||
fallProgress = timeElapsed / skull['fall_duration']
|
||||
timeElapsed = currentTime - skull["start_time"]
|
||||
fallProgress = timeElapsed / skull["fall_duration"]
|
||||
currentY = self.yPos * (1 - fallProgress)
|
||||
|
||||
skull['channel'] = play_random_falling(
|
||||
skull["channel"] = play_random_falling(
|
||||
self.sounds,
|
||||
'falling_skull',
|
||||
"falling_skull",
|
||||
player.xPos,
|
||||
skull['x'],
|
||||
skull["x"],
|
||||
self.yPos,
|
||||
currentY,
|
||||
existingChannel=skull['channel']
|
||||
existingChannel=skull["channel"],
|
||||
)
|
||||
|
||||
# Check if we should spawn a new skull
|
||||
if (len(self.activeSkulls) < self.maxSkulls and
|
||||
currentTime - self.lastSkullTime >= self.nextSkullDelay):
|
||||
if len(self.activeSkulls) < self.maxSkulls and currentTime - self.lastSkullTime >= self.nextSkullDelay:
|
||||
self.spawn_skull(currentTime)
|
||||
|
||||
def spawn_skull(self, currentTime):
|
||||
@@ -91,11 +86,11 @@ class SkullStorm(Object):
|
||||
|
||||
# Create new skull
|
||||
skull = {
|
||||
'x': random.uniform(self.xRange[0], self.xRange[1]),
|
||||
'start_time': currentTime,
|
||||
'fall_duration': fallDuration,
|
||||
'land_time': currentTime + fallDuration,
|
||||
'channel': None
|
||||
"x": random.uniform(self.xRange[0], self.xRange[1]),
|
||||
"start_time": currentTime,
|
||||
"fall_duration": fallDuration,
|
||||
"land_time": currentTime + fallDuration,
|
||||
"channel": None,
|
||||
}
|
||||
|
||||
self.activeSkulls.append(skull)
|
||||
@@ -103,18 +98,18 @@ class SkullStorm(Object):
|
||||
def handle_landing(self, skull, player):
|
||||
"""Handle a skull landing."""
|
||||
# Stop falling sound
|
||||
if skull['channel']:
|
||||
obj_stop(skull['channel'])
|
||||
if skull["channel"]:
|
||||
obj_stop(skull["channel"])
|
||||
|
||||
# Play landing sound with positional audio once
|
||||
channel = pygame.mixer.find_channel(True) # Find an available channel
|
||||
if channel:
|
||||
soundObj = self.sounds['skull_lands']
|
||||
obj_play(self.sounds, 'skull_lands', player.xPos, skull['x'], loop=False)
|
||||
soundObj = self.sounds["skull_lands"]
|
||||
obj_play(self.sounds, "skull_lands", player.xPos, skull["x"], loop=False)
|
||||
|
||||
# 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:
|
||||
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!")
|
||||
|
||||
+3
-8
@@ -1,20 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class StatTracker:
|
||||
def __init__(self):
|
||||
# Base dictionary for tracking stats
|
||||
self.total = {
|
||||
'Bone dust': 0,
|
||||
'Enemies killed': 0,
|
||||
'Coffins broken': 0,
|
||||
'Items collected': 0,
|
||||
'Total time': 0
|
||||
}
|
||||
self.total = {"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)
|
||||
self.level = self.total.copy()
|
||||
|
||||
self.total['levelsCompleted'] = 0
|
||||
self.total["levelsCompleted"] = 0
|
||||
|
||||
def reset_level(self):
|
||||
"""Reset level stats based on variable type"""
|
||||
|
||||
@@ -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
@@ -1,7 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
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.damage = damage
|
||||
self.range = range # Range in tiles
|
||||
@@ -9,6 +21,8 @@ class Weapon:
|
||||
self.hitSound = hitSound
|
||||
self.cooldown = cooldown # Milliseconds between attacks
|
||||
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.hitEnemies = set()
|
||||
|
||||
@@ -22,7 +36,7 @@ class Weapon:
|
||||
attackSound="player_nunchuck_attack",
|
||||
hitSound="player_nunchuck_hit",
|
||||
cooldown=250,
|
||||
attackDuration=100
|
||||
attackDuration=100,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -35,7 +49,9 @@ class Weapon:
|
||||
attackSound="player_broom_attack",
|
||||
hitSound="player_broom_hit",
|
||||
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):
|
||||
|
||||
+294
-65
@@ -5,25 +5,48 @@ import json
|
||||
import os
|
||||
import pygame
|
||||
from libstormgames import *
|
||||
from src.pack_sound_system import PackSoundSystem
|
||||
from src.level import Level
|
||||
from src.object import Object
|
||||
from src.player import Player
|
||||
from src.game_selection import select_game, get_level_path
|
||||
from src.save_manager import SaveManager
|
||||
from src.survival_generator import SurvivalGenerator
|
||||
|
||||
|
||||
class WickedQuest:
|
||||
def __init__(self):
|
||||
"""Initialize game and load sounds."""
|
||||
self.sounds = initialize_gui("Wicked Quest")
|
||||
self.soundSystem = None # Will be created when game is selected
|
||||
self.currentLevel = None
|
||||
self.gameStartTime = None
|
||||
self.lastThrowTime = 0
|
||||
self.throwDelay = 250
|
||||
self.lastWeaponSwitchTime = 0
|
||||
self.weaponSwitchDelay = 200
|
||||
self.player = None
|
||||
self.currentGame = None
|
||||
self.runLock = False # Toggle behavior of the run keys
|
||||
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):
|
||||
"""Load a level from its JSON file."""
|
||||
@@ -36,7 +59,7 @@ class WickedQuest:
|
||||
if self.player is None:
|
||||
self.player = Player(levelData["player_start"]["x"],
|
||||
levelData["player_start"]["y"],
|
||||
self.sounds)
|
||||
self.get_sounds())
|
||||
else:
|
||||
# Reset player for new level.
|
||||
self.player.isDucking = False
|
||||
@@ -56,7 +79,7 @@ class WickedQuest:
|
||||
|
||||
# Pass existing player to new level
|
||||
pygame.event.clear()
|
||||
self.currentLevel = Level(levelData, self.sounds, self.player)
|
||||
self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame)
|
||||
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
@@ -87,7 +110,7 @@ class WickedQuest:
|
||||
return errors
|
||||
|
||||
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()
|
||||
|
||||
if not save_files:
|
||||
@@ -101,48 +124,24 @@ class WickedQuest:
|
||||
|
||||
options.append("Cancel")
|
||||
|
||||
# Show menu
|
||||
currentIndex = 0
|
||||
lastSpoken = -1
|
||||
# Use instruction_menu for consistent behavior
|
||||
choice = instruction_menu(self.get_sounds(), "Select a save file to load:", *options)
|
||||
|
||||
messagebox("Select a save file to load:")
|
||||
|
||||
while True:
|
||||
if currentIndex != lastSpoken:
|
||||
speak(options[currentIndex])
|
||||
lastSpoken = currentIndex
|
||||
|
||||
event = pygame.event.wait()
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
return None
|
||||
elif event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(options) - 1:
|
||||
currentIndex += 1
|
||||
try:
|
||||
self.sounds['menu-move'].play()
|
||||
except:
|
||||
pass
|
||||
elif event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
||||
currentIndex -= 1
|
||||
try:
|
||||
self.sounds['menu-move'].play()
|
||||
except:
|
||||
pass
|
||||
elif event.key == pygame.K_RETURN:
|
||||
try:
|
||||
self.sounds['menu-select'].play()
|
||||
except:
|
||||
pass
|
||||
|
||||
if currentIndex == len(options) - 1: # Cancel
|
||||
return None
|
||||
else:
|
||||
return save_files[currentIndex]
|
||||
|
||||
pygame.event.clear()
|
||||
if choice == "Cancel" or choice is None:
|
||||
return None
|
||||
else:
|
||||
# Find the corresponding save file
|
||||
for save_file in save_files:
|
||||
if save_file['display_name'] == choice:
|
||||
return save_file
|
||||
return None
|
||||
|
||||
def auto_save(self):
|
||||
"""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():
|
||||
return False
|
||||
|
||||
@@ -157,8 +156,8 @@ class WickedQuest:
|
||||
|
||||
if success:
|
||||
try:
|
||||
if 'save' in self.sounds:
|
||||
play_sound(self.sounds['save'])
|
||||
if 'save' in self.get_sounds():
|
||||
play_sound(self.get_sounds()['save'])
|
||||
else:
|
||||
print("Save sound not found in sounds dictionary")
|
||||
except Exception as e:
|
||||
@@ -179,10 +178,17 @@ class WickedQuest:
|
||||
currentTime = pygame.time.get_ticks()
|
||||
|
||||
# Update running and ducking states
|
||||
if (keys[pygame.K_s] or keys[pygame.K_DOWN]) and not player.isDucking:
|
||||
player.duck()
|
||||
elif (not keys[pygame.K_s] and not keys[pygame.K_DOWN]) and player.isDucking:
|
||||
player.stand()
|
||||
# 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:
|
||||
player.duck()
|
||||
elif (not keys[pygame.K_s] and not keys[pygame.K_DOWN]) and player.isDucking:
|
||||
player.stand()
|
||||
|
||||
if self.runLock:
|
||||
player.isRunning = not (keys[pygame.K_SPACE] or keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT])
|
||||
@@ -209,13 +215,17 @@ class WickedQuest:
|
||||
if movementDistance > 0 and not player.isJumping:
|
||||
player.distanceSinceLastStep += movementDistance
|
||||
if player.should_play_footstep(currentTime):
|
||||
play_sound(self.sounds[player.footstepSound])
|
||||
play_sound(self.get_sounds()[player.footstepSound])
|
||||
player.distanceSinceLastStep = 0
|
||||
player.lastStepTime = currentTime
|
||||
|
||||
# Status queries
|
||||
if keys[pygame.K_c]:
|
||||
speak(f"{player.get_coins()} bone dust for extra lives, {player.get_save_bone_dust()} bone dust for saves")
|
||||
# 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")
|
||||
if keys[pygame.K_h]:
|
||||
speak(f"{player.get_health()} health of {player.get_max_health()}")
|
||||
if keys[pygame.K_i]:
|
||||
@@ -232,20 +242,44 @@ class WickedQuest:
|
||||
if keys[pygame.K_e]:
|
||||
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
|
||||
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
|
||||
if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping:
|
||||
player.isJumping = True
|
||||
player.jumpStartTime = currentTime
|
||||
play_sound(self.sounds['jump'])
|
||||
play_sound(self.get_sounds()['jump'])
|
||||
|
||||
# Check if jump should end
|
||||
if player.isJumping and currentTime - player.jumpStartTime >= player.jumpDuration:
|
||||
# Handle instant landing with broom (press down while jumping)
|
||||
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
|
||||
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
|
||||
player.distanceSinceLastStep = 0
|
||||
player.lastStepTime = currentTime
|
||||
@@ -272,7 +306,7 @@ class WickedQuest:
|
||||
try:
|
||||
speak(f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}, complete!")
|
||||
pygame.mixer.music.stop()
|
||||
cut_scene(self.sounds, '_finish_level')
|
||||
cut_scene(self.get_sounds(), '_finish_level')
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -298,7 +332,31 @@ class WickedQuest:
|
||||
pygame.event.clear()
|
||||
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)
|
||||
|
||||
def game_loop(self, startingLevelNum=1):
|
||||
@@ -389,8 +447,8 @@ class WickedQuest:
|
||||
for ext in ['.wav', '.ogg', '.mp3']:
|
||||
endFile = os.path.join(gamePath, f'end{ext}')
|
||||
if os.path.exists(endFile):
|
||||
self.sounds['end_scene'] = pygame.mixer.Sound(endFile)
|
||||
cut_scene(self.sounds, 'end_scene')
|
||||
self.get_sounds()['end_scene'] = pygame.mixer.Sound(endFile)
|
||||
cut_scene(self.get_sounds(), 'end_scene')
|
||||
break
|
||||
else:
|
||||
messagebox("Congratulations! You've completed all available levels!")
|
||||
@@ -414,7 +472,7 @@ class WickedQuest:
|
||||
if self.saveManager.has_saves():
|
||||
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":
|
||||
exit_game()
|
||||
@@ -427,6 +485,8 @@ class WickedQuest:
|
||||
self.currentGame = save_data['game_state']['currentGame']
|
||||
self.gameStartTime = save_data['game_state']['gameStartTime']
|
||||
current_level = save_data['game_state']['currentLevel']
|
||||
# Initialize pack-specific sound system
|
||||
self.initialize_pack_sounds()
|
||||
|
||||
# Load the level
|
||||
if self.load_level(current_level):
|
||||
@@ -438,7 +498,11 @@ class WickedQuest:
|
||||
else:
|
||||
messagebox(f"Failed to load save: {save_data}")
|
||||
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
|
||||
errors = self.validate_levels()
|
||||
if errors:
|
||||
@@ -448,10 +512,15 @@ class WickedQuest:
|
||||
display_text(errorLines)
|
||||
continue
|
||||
if self.currentGame:
|
||||
self.player = None # Reset player for new game
|
||||
self.gameStartTime = pygame.time.get_ticks()
|
||||
if self.load_level(1):
|
||||
self.game_loop()
|
||||
# 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.gameStartTime = pygame.time.get_ticks()
|
||||
if self.load_level(1):
|
||||
self.game_loop()
|
||||
elif mode_choice == "survival":
|
||||
self.start_survival_mode()
|
||||
elif choice == "high_scores":
|
||||
board = Scoreboard()
|
||||
scores = board.get_high_scores()
|
||||
@@ -463,9 +532,169 @@ class WickedQuest:
|
||||
pygame.event.clear()
|
||||
display_text(lines)
|
||||
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__":
|
||||
game = WickedQuest()
|
||||
game.run()
|
||||
|
||||
|
||||
|
||||
+14
-1
@@ -1,11 +1,24 @@
|
||||
# -*- 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(
|
||||
['wicked_quest.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
datas=level_dirs + [
|
||||
('sounds', 'sounds'),
|
||||
('libstormgames', 'libstormgames'),
|
||||
],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
|
||||
Reference in New Issue
Block a user