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
|
*.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:
|
Steven Robinson:
|
||||||
|
|
||||||
Ko-fi Supporters:
|
|
||||||
https://ko-fi.com/stormux/leaderboard
|
|
||||||
|
|
||||||
Music:
|
Music:
|
||||||
|
|
||||||
Choir of Doom:
|
Choir of Doom:
|
||||||
|
|||||||
+23
-3
@@ -19,6 +19,9 @@ tab or capslock: Toggle run lock.
|
|||||||
Control: Attack.
|
Control: Attack.
|
||||||
Space or shift: Hold to run if runlock is disabled. If run lock is enabled, hold to walk.
|
Space or shift: Hold to run if runlock is disabled. If run lock is enabled, hold to walk.
|
||||||
f, z, or /: Throw jack O'lantern.
|
f, z, or /: Throw jack O'lantern.
|
||||||
|
1: Switch to rusty shovel (if available).
|
||||||
|
2: Switch to witch's broom (if available).
|
||||||
|
3: Switch to nunchucks (if available).
|
||||||
c: Check bone dust.
|
c: Check bone dust.
|
||||||
e: Check currently wielded weapon.
|
e: Check currently wielded weapon.
|
||||||
h: Check health.
|
h: Check health.
|
||||||
@@ -28,7 +31,7 @@ l: Check lives remaining.
|
|||||||
Alt+PageDown: Master volume decrease.
|
Alt+PageDown: Master volume decrease.
|
||||||
Alt+PageUp: Master volume increase.
|
Alt+PageUp: Master volume increase.
|
||||||
Alt+End: Ambience volume decrease.
|
Alt+End: Ambience volume decrease.
|
||||||
Alt+Home: Game volume increase.
|
Alt+Home: Ambience volume increase.
|
||||||
Alt+Delete: Game sounds volume decrease.
|
Alt+Delete: Game sounds volume decrease.
|
||||||
Alt+Insert: Game sounds volume increase.
|
Alt+Insert: Game sounds volume increase.
|
||||||
Backspace: Pause or resume the game.
|
Backspace: Pause or resume the game.
|
||||||
@@ -41,7 +44,24 @@ Spider webs can be passed by ducking as you move by them.
|
|||||||
If you hit a spiderweb, a spider spawns, and you are slowed for 15 seconds. If you are invincible, you are not slowed, but the spider still appears.
|
If you hit a spiderweb, a spider spawns, and you are slowed for 15 seconds. If you are invincible, you are not slowed, but the spider still appears.
|
||||||
Running and jumping both move you at 1.5 your normal speed.
|
Running and jumping both move you at 1.5 your normal speed.
|
||||||
Items bounce away from you when they are freed from the coffin. You must jump to catch them when they are in range.
|
Items bounce away from you when they are freed from the coffin. You must jump to catch them when they are in range.
|
||||||
The game automatically saves your progress when you have collected 200 bone dust. You will hear a message saying the game has been saved, and a wolf will howl. To load a game, use the load option in the main menu. Load only appears if you have saved games. The first save should happen around level 6.
|
You can switch between weapons you've collected using the number keys 1, 2, and 3. This allows strategic combat use different weapons for different situations based on their damage, range, and speed.
|
||||||
|
The shovel is a weak weapon, but it can fill in graves. While wielding the shovel hold down and walk (do not run) over the grave. If you run you will trip and fall in.
|
||||||
|
The witch's broom isn't in good enough condition from bashing enemies to be used for true flight, but it does grant a speed bonus and also allows you to land at any point while jumping, just press down.
|
||||||
|
The game automatically saves your progress when you have collected 200 bone dust. You will hear a message saying the game has been saved, and a wolf will howl. To load a game, use the load option in the main menu. Load only appears if you have saved games.
|
||||||
|
|
||||||
|
Game Modes
|
||||||
|
|
||||||
|
Story Mode: Traditional level-by-level progression through predefined stages. Your progress is saved, and you can collect extra lives by gathering bone dust.
|
||||||
|
|
||||||
|
Survival Mode: An endless challenge where you face wave after wave of increasingly difficult enemies. Each wave gets progressively harder with stronger enemies, faster spawn rates, and longer levels. Level length starts at 300 units and increases by 20 units per wave (capped at 500 units). In survival mode:
|
||||||
|
No saving or loading - each run is a fresh start
|
||||||
|
No extra lives are awarded
|
||||||
|
Each wave must be completed by defeating all enemies before you can advance
|
||||||
|
Bone dust still provides health bonuses but collecting 100 bone dust gives 2000 bonus score instead of extra lives
|
||||||
|
Monsters unlock per wave that matches the level in which they are found
|
||||||
|
Once all monsters are unlocked your score is doubled
|
||||||
|
All coffin contents are randomized
|
||||||
|
A detailed statistics report shows your performance at the end of each run
|
||||||
|
|
||||||
Enemies
|
Enemies
|
||||||
|
|
||||||
@@ -61,7 +81,7 @@ Bonuses and items
|
|||||||
|
|
||||||
Bone dust: Currency of the game. Collect it to gain health and extra lives.
|
Bone dust: Currency of the game. Collect it to gain health and extra lives.
|
||||||
Cauldron: Restores you to full health.
|
Cauldron: Restores you to full health.
|
||||||
Witch's broom: A weapon gained from witches. Stronger than the grave digger's rusty shovel.
|
Witch's broom: A weapon gained from witches. Stronger than the grave digger's rusty shovel and grants extra speed and jump time.
|
||||||
Coffin: Contains items. Be quick, they try to escape.
|
Coffin: Contains items. Be quick, they try to escape.
|
||||||
Guts: Adds 2 health to your maximum unlife if your unlife is below 20 points otherwise it restores your health.
|
Guts: Adds 2 health to your maximum unlife if your unlife is below 20 points otherwise it restores your health.
|
||||||
Hand of Glory: Grants invincibility for a short time.
|
Hand of Glory: Grants invincibility for a short time.
|
||||||
|
|||||||
+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.isActive = True
|
||||||
self.damage = playerMaxHealth // 2 # Half of player's max health
|
self.damage = playerMaxHealth // 2 # Half of player's max health
|
||||||
self.soundChannel = None
|
self.soundChannel = None
|
||||||
self.soundName = 'pumpkin_low' if isHigh else 'pumpkin_high' # Inverted mapping
|
self.soundName = "pumpkin_low" if isHigh else "pumpkin_high" # Inverted mapping
|
||||||
|
|
||||||
def update(self, sounds, playerX):
|
def update(self, sounds, playerX):
|
||||||
"""Update pumpkin position and sound"""
|
"""Update pumpkin position and sound"""
|
||||||
@@ -39,7 +39,7 @@ class Pumpkin:
|
|||||||
# Calculate volume and pan for splat sound based on final position
|
# Calculate volume and pan for splat sound based on final position
|
||||||
volume, left, right = calculate_volume_and_pan(playerX, self.x)
|
volume, left, right = calculate_volume_and_pan(playerX, self.x)
|
||||||
if volume > 0: # Only play if within audible range
|
if volume > 0: # Only play if within audible range
|
||||||
obj_play(sounds, 'pumpkin_splat', playerX, self.x, loop=False)
|
obj_play(sounds, "pumpkin_splat", playerX, self.x, loop=False)
|
||||||
|
|
||||||
def check_collision(self, player):
|
def check_collision(self, player):
|
||||||
"""Check if pumpkin hits player"""
|
"""Check if pumpkin hits player"""
|
||||||
@@ -58,7 +58,9 @@ class Pumpkin:
|
|||||||
class Catapult(Object):
|
class Catapult(Object):
|
||||||
def __init__(self, x, y, sounds, direction=1, fireInterval=5000, firingRange=20):
|
def __init__(self, x, y, sounds, direction=1, fireInterval=5000, firingRange=20):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
x, y, "catapult",
|
x,
|
||||||
|
y,
|
||||||
|
"catapult",
|
||||||
isStatic=True,
|
isStatic=True,
|
||||||
isCollectible=False,
|
isCollectible=False,
|
||||||
)
|
)
|
||||||
@@ -77,18 +79,14 @@ class Catapult(Object):
|
|||||||
self.lastFireTime = currentTime
|
self.lastFireTime = currentTime
|
||||||
|
|
||||||
# Play launch sound using directional audio
|
# Play launch sound using directional audio
|
||||||
play_directional_sound(self.sounds, 'catapult_launch', player.xPos, self.xPos)
|
play_directional_sound(self.sounds, "catapult_launch", player.xPos, self.xPos)
|
||||||
|
|
||||||
# Set up pending pumpkin
|
# Set up pending pumpkin
|
||||||
isHigh = random.choice([True, False])
|
isHigh = random.choice([True, False])
|
||||||
fireDirection = 1 if player.xPos > self.xPos else -1
|
fireDirection = 1 if player.xPos > self.xPos else -1
|
||||||
|
|
||||||
# Store pumpkin data for later creation
|
# Store pumpkin data for later creation
|
||||||
self.pendingPumpkin = {
|
self.pendingPumpkin = {"isHigh": isHigh, "direction": fireDirection, "playerMaxHealth": player.get_max_health()}
|
||||||
'isHigh': isHigh,
|
|
||||||
'direction': fireDirection,
|
|
||||||
'playerMaxHealth': player.get_max_health()
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set when to actually launch the pumpkin
|
# Set when to actually launch the pumpkin
|
||||||
self.pumpkinLaunchTime = currentTime + self.launchDelay
|
self.pumpkinLaunchTime = currentTime + self.launchDelay
|
||||||
@@ -116,9 +114,9 @@ class Catapult(Object):
|
|||||||
# Create and fire the pending pumpkin
|
# Create and fire the pending pumpkin
|
||||||
pumpkin = Pumpkin(
|
pumpkin = Pumpkin(
|
||||||
self.xPos,
|
self.xPos,
|
||||||
self.pendingPumpkin['isHigh'],
|
self.pendingPumpkin["isHigh"],
|
||||||
self.pendingPumpkin['direction'],
|
self.pendingPumpkin["direction"],
|
||||||
self.pendingPumpkin['playerMaxHealth']
|
self.pendingPumpkin["playerMaxHealth"],
|
||||||
)
|
)
|
||||||
self.activePumpkins.append(pumpkin)
|
self.activePumpkins.append(pumpkin)
|
||||||
self.pendingPumpkin = None
|
self.pendingPumpkin = None
|
||||||
@@ -140,4 +138,4 @@ class Catapult(Object):
|
|||||||
pumpkin.isActive = False
|
pumpkin.isActive = False
|
||||||
self.activePumpkins.remove(pumpkin)
|
self.activePumpkins.remove(pumpkin)
|
||||||
if not player.isInvincible:
|
if not player.isInvincible:
|
||||||
self.sounds['player_takes_damage'].play()
|
self.sounds["player_takes_damage"].play()
|
||||||
|
|||||||
+4
-15
@@ -9,12 +9,7 @@ from src.powerup import PowerUp
|
|||||||
|
|
||||||
class CoffinObject(Object):
|
class CoffinObject(Object):
|
||||||
def __init__(self, x, y, sounds, level, item="random"):
|
def __init__(self, x, y, sounds, level, item="random"):
|
||||||
super().__init__(
|
super().__init__(x, y, "coffin", isStatic=True, isCollectible=False, isHazard=False)
|
||||||
x, y, "coffin",
|
|
||||||
isStatic=True,
|
|
||||||
isCollectible=False,
|
|
||||||
isHazard=False
|
|
||||||
)
|
|
||||||
self.sounds = sounds
|
self.sounds = sounds
|
||||||
self.level = level
|
self.level = level
|
||||||
self.isBroken = False
|
self.isBroken = False
|
||||||
@@ -25,9 +20,9 @@ class CoffinObject(Object):
|
|||||||
"""Handle being hit by the player's weapon"""
|
"""Handle being hit by the player's weapon"""
|
||||||
if not self.isBroken:
|
if not self.isBroken:
|
||||||
self.isBroken = True
|
self.isBroken = True
|
||||||
play_sound(self.sounds['coffin_shatter'])
|
play_sound(self.sounds["coffin_shatter"])
|
||||||
self.level.levelScore += 500
|
self.level.levelScore += 500
|
||||||
self.level.player.stats.update_stat('Coffins broken', 1)
|
self.level.player.stats.update_stat("Coffins broken", 1)
|
||||||
|
|
||||||
# Stop the ongoing coffin sound
|
# Stop the ongoing coffin sound
|
||||||
if self.channel:
|
if self.channel:
|
||||||
@@ -54,13 +49,7 @@ class CoffinObject(Object):
|
|||||||
drop_x = self.xPos + (direction * drop_distance)
|
drop_x = self.xPos + (direction * drop_distance)
|
||||||
|
|
||||||
self.dropped_item = PowerUp(
|
self.dropped_item = PowerUp(
|
||||||
drop_x,
|
drop_x, self.yPos, item_type, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
|
||||||
self.yPos,
|
|
||||||
item_type,
|
|
||||||
self.sounds,
|
|
||||||
direction,
|
|
||||||
self.level.leftBoundary,
|
|
||||||
self.level.rightBoundary
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -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.object import Object
|
||||||
from src.powerup import PowerUp
|
from src.powerup import PowerUp
|
||||||
|
|
||||||
|
|
||||||
class Enemy(Object):
|
class Enemy(Object):
|
||||||
def __init__(self, xRange, y, enemyType, sounds, level, **kwargs):
|
def __init__(self, xRange, y, enemyType, sounds, level, **kwargs):
|
||||||
# Track when critters should start hunting
|
# Track when critters should start hunting
|
||||||
self.hunting = False
|
self.hunting = False
|
||||||
# Initialize base object properties
|
# Initialize base object properties
|
||||||
super().__init__(
|
super().__init__(xRange, y, f"{enemyType}", isStatic=False, isHazard=True) # Base sound
|
||||||
xRange,
|
|
||||||
y,
|
|
||||||
f"{enemyType}", # Base sound
|
|
||||||
isStatic=False,
|
|
||||||
isHazard=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Enemy specific properties
|
# Enemy specific properties
|
||||||
self.enemyType = enemyType
|
self.enemyType = enemyType
|
||||||
self.level = level
|
self.level = level
|
||||||
self.health = kwargs.get('health', 5) # Default 5 HP
|
self.health = kwargs.get("health", 5) # Default 5 HP
|
||||||
self.damage = kwargs.get('damage', 1) # Default 1 damage
|
self.damage = kwargs.get("damage", 1) # Default 1 damage
|
||||||
self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range
|
self.attackRange = kwargs.get("attack_range", 1) # Default 1 tile range
|
||||||
self.sounds = sounds # Store reference to game sounds
|
self.sounds = sounds # Store reference to game sounds
|
||||||
|
|
||||||
# Movement and behavior properties
|
# Movement and behavior properties
|
||||||
@@ -37,25 +32,25 @@ class Enemy(Object):
|
|||||||
self._currentX = self.xRange[0] # Initialize current position
|
self._currentX = self.xRange[0] # Initialize current position
|
||||||
|
|
||||||
# Add spawn configuration
|
# Add spawn configuration
|
||||||
self.canSpawn = kwargs.get('can_spawn', False)
|
self.canSpawn = kwargs.get("can_spawn", False)
|
||||||
if self.canSpawn:
|
if self.canSpawn:
|
||||||
self.spawnCooldown = kwargs.get('spawn_cooldown', 2000)
|
self.spawnCooldown = kwargs.get("spawn_cooldown", 2000)
|
||||||
self.spawnChance = kwargs.get('spawn_chance', 25)
|
self.spawnChance = kwargs.get("spawn_chance", 25)
|
||||||
self.spawnType = kwargs.get('spawn_type', 'zombie') # Default to zombie for backward compatibility
|
self.spawnType = kwargs.get("spawn_type", "zombie") # Default to zombie for backward compatibility
|
||||||
self.spawnDistance = kwargs.get('spawn_distance', 5)
|
self.spawnDistance = kwargs.get("spawn_distance", 5)
|
||||||
self.lastSpawnTime = 0
|
self.lastSpawnTime = 0
|
||||||
|
|
||||||
# Attack pattern configuration
|
# Attack pattern configuration
|
||||||
self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'})
|
self.attackPattern = kwargs.get("attack_pattern", {"type": "patrol"})
|
||||||
self.turnThreshold = self.attackPattern.get('turn_threshold', 5)
|
self.turnThreshold = self.attackPattern.get("turn_threshold", 5)
|
||||||
|
|
||||||
# Initialize vulnerability system
|
# Initialize vulnerability system
|
||||||
self.hasVulnerabilitySystem = kwargs.get('has_vulnerability', False)
|
self.hasVulnerabilitySystem = kwargs.get("has_vulnerability", False)
|
||||||
if self.hasVulnerabilitySystem:
|
if self.hasVulnerabilitySystem:
|
||||||
self.isVulnerable = False # Start invulnerable
|
self.isVulnerable = False # Start invulnerable
|
||||||
self.vulnerabilityTimer = pygame.time.get_ticks()
|
self.vulnerabilityTimer = pygame.time.get_ticks()
|
||||||
self.vulnerabilityDuration = kwargs.get('vulnerability_duration', 2000)
|
self.vulnerabilityDuration = kwargs.get("vulnerability_duration", 2000)
|
||||||
self.invulnerabilityDuration = kwargs.get('invulnerability_duration', 5000)
|
self.invulnerabilityDuration = kwargs.get("invulnerability_duration", 5000)
|
||||||
soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType
|
soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType
|
||||||
self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos)
|
self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos)
|
||||||
else:
|
else:
|
||||||
@@ -68,19 +63,17 @@ class Enemy(Object):
|
|||||||
self.health = 1 # Easy to kill
|
self.health = 1 # Easy to kill
|
||||||
self.attackCooldown = 1500 # Slower attack rate
|
self.attackCooldown = 1500 # Slower attack rate
|
||||||
elif enemyType == "spider":
|
elif enemyType == "spider":
|
||||||
speedMultiplier = kwargs.get('speed_multiplier', 2.0)
|
speedMultiplier = kwargs.get("speed_multiplier", 2.0)
|
||||||
self.movementSpeed *= speedMultiplier # Spiders are faster
|
self.movementSpeed *= speedMultiplier # Spiders are faster
|
||||||
self.attackPattern = {'type': 'hunter'} # Spiders actively hunt the player
|
self.attackPattern = {"type": "hunter"} # Spiders actively hunt the player
|
||||||
self.turnThreshold = 3 # Spiders turn around quickly to chase player
|
self.turnThreshold = 3 # Spiders turn around quickly to chase player
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def xPos(self):
|
def xPos(self):
|
||||||
"""Current x position"""
|
"""Current x position"""
|
||||||
return self._currentX
|
return self._currentX
|
||||||
|
|
||||||
@xPos.setter
|
@xPos.setter
|
||||||
|
|
||||||
def xPos(self, value):
|
def xPos(self, value):
|
||||||
"""Set current x position"""
|
"""Set current x position"""
|
||||||
self._currentX = value
|
self._currentX = value
|
||||||
@@ -111,7 +104,9 @@ class Enemy(Object):
|
|||||||
self.channel = obj_update(self.channel, player.xPos, self.xPos)
|
self.channel = obj_update(self.channel, player.xPos, self.xPos)
|
||||||
|
|
||||||
# Check for vulnerability state change
|
# Check for vulnerability state change
|
||||||
if currentTime - self.vulnerabilityTimer >= (self.vulnerabilityDuration if self.isVulnerable else self.invulnerabilityDuration):
|
if currentTime - self.vulnerabilityTimer >= (
|
||||||
|
self.vulnerabilityDuration if self.isVulnerable else self.invulnerabilityDuration
|
||||||
|
):
|
||||||
self.isVulnerable = not self.isVulnerable
|
self.isVulnerable = not self.isVulnerable
|
||||||
self.vulnerabilityTimer = currentTime
|
self.vulnerabilityTimer = currentTime
|
||||||
|
|
||||||
@@ -126,8 +121,7 @@ class Enemy(Object):
|
|||||||
self.hunting = True
|
self.hunting = True
|
||||||
|
|
||||||
# Handle movement based on enemy type and pattern
|
# Handle movement based on enemy type and pattern
|
||||||
if (self.enemyType == "zombie" or
|
if self.enemyType == "zombie" or (self.attackPattern["type"] == "hunter" and self.hunting):
|
||||||
(self.attackPattern['type'] == 'hunter' and self.hunting)):
|
|
||||||
|
|
||||||
distanceToPlayer = player.xPos - self.xPos
|
distanceToPlayer = player.xPos - self.xPos
|
||||||
|
|
||||||
@@ -166,6 +160,10 @@ class Enemy(Object):
|
|||||||
# Ensure spawn point is within level boundaries
|
# Ensure spawn point is within level boundaries
|
||||||
spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary))
|
spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary))
|
||||||
|
|
||||||
|
# Set behavior based on game mode
|
||||||
|
behavior = "hunter" if self.level.levelId == 999 else "patrol"
|
||||||
|
turn_rate = 2 if self.level.levelId == 999 else 8 # Faster turn rate for survival
|
||||||
|
|
||||||
# Create new enemy of specified type
|
# Create new enemy of specified type
|
||||||
spawned = Enemy(
|
spawned = Enemy(
|
||||||
[spawnX, spawnX], # Single point range for spawn
|
[spawnX, spawnX], # Single point range for spawn
|
||||||
@@ -175,7 +173,9 @@ class Enemy(Object):
|
|||||||
self.level,
|
self.level,
|
||||||
health=4, # Default health for spawned enemies
|
health=4, # Default health for spawned enemies
|
||||||
damage=2, # Default damage for spawned enemies
|
damage=2, # Default damage for spawned enemies
|
||||||
attack_range=1 # Default range for spawned enemies
|
attack_range=1, # Default range for spawned enemies
|
||||||
|
attack_pattern={"type": behavior},
|
||||||
|
turn_rate=turn_rate,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add to level's enemies
|
# Add to level's enemies
|
||||||
@@ -215,7 +215,8 @@ class Enemy(Object):
|
|||||||
|
|
||||||
def attack(self, currentTime, player):
|
def attack(self, currentTime, player):
|
||||||
"""Perform attack on player"""
|
"""Perform attack on player"""
|
||||||
if player.isInvincible: return
|
if player.isInvincible:
|
||||||
|
return
|
||||||
self.lastAttackTime = currentTime
|
self.lastAttackTime = currentTime
|
||||||
# Play attack sound
|
# Play attack sound
|
||||||
attackSound = f"{self.enemyType}_attack"
|
attackSound = f"{self.enemyType}_attack"
|
||||||
@@ -223,7 +224,7 @@ class Enemy(Object):
|
|||||||
self.sounds[attackSound].play()
|
self.sounds[attackSound].play()
|
||||||
# Deal damage to player
|
# Deal damage to player
|
||||||
player.set_health(player.get_health() - self.damage)
|
player.set_health(player.get_health() - self.damage)
|
||||||
self.sounds['player_takes_damage'].play()
|
self.sounds["player_takes_damage"].play()
|
||||||
|
|
||||||
def take_damage(self, amount):
|
def take_damage(self, amount):
|
||||||
"""Handle enemy taking damage"""
|
"""Handle enemy taking damage"""
|
||||||
@@ -251,10 +252,11 @@ class Enemy(Object):
|
|||||||
# Award points
|
# Award points
|
||||||
self.level.levelScore += totalPoints
|
self.level.levelScore += totalPoints
|
||||||
|
|
||||||
# Play death sound if available using positional audio
|
# Create a DeathSound object to play death sound at fixed position
|
||||||
deathSound = f"{self.enemyType}_dies"
|
from src.die_monster_die import DeathSound
|
||||||
if deathSound in self.sounds:
|
|
||||||
self.channel = obj_play(self.sounds, deathSound, self.level.player.xPos, self.xPos, loop=False)
|
deathSoundObj = DeathSound(self.xPos, self.yPos, self.enemyType, self.sounds)
|
||||||
|
self.level.objects.append(deathSoundObj)
|
||||||
|
|
||||||
# Handle witch-specific drops
|
# Handle witch-specific drops
|
||||||
if self.enemyType == "witch":
|
if self.enemyType == "witch":
|
||||||
@@ -270,15 +272,9 @@ class Enemy(Object):
|
|||||||
dropX = self.xPos + (direction * dropDistance)
|
dropX = self.xPos + (direction * dropDistance)
|
||||||
|
|
||||||
droppedItem = PowerUp(
|
droppedItem = PowerUp(
|
||||||
dropX,
|
dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
|
||||||
self.yPos,
|
|
||||||
itemType,
|
|
||||||
self.sounds,
|
|
||||||
direction,
|
|
||||||
self.level.leftBoundary,
|
|
||||||
self.level.rightBoundary
|
|
||||||
)
|
)
|
||||||
self.level.bouncing_items.append(droppedItem)
|
self.level.bouncing_items.append(droppedItem)
|
||||||
|
|
||||||
# Update stats
|
# Update stats
|
||||||
self.level.player.stats.update_stat('Enemies killed', 1)
|
self.level.player.stats.update_stat("Enemies killed", 1)
|
||||||
|
|||||||
+42
-82
@@ -1,10 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import sys
|
||||||
import pygame
|
|
||||||
from os.path import isdir, join
|
from os.path import isdir, join
|
||||||
from libstormgames import speak
|
|
||||||
|
from libstormgames import speak, instruction_menu
|
||||||
|
|
||||||
|
|
||||||
def get_available_games():
|
def get_available_games():
|
||||||
"""Get list of available game directories in levels folder.
|
"""Get list of available game directories in levels folder.
|
||||||
@@ -13,12 +14,22 @@ def get_available_games():
|
|||||||
list: List of game directory names
|
list: List of game directory names
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return [d for d in os.listdir("levels") if isdir(join("levels", d))]
|
# Handle PyInstaller path issues
|
||||||
|
if hasattr(sys, "_MEIPASS"):
|
||||||
|
# Running as PyInstaller executable
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
# Running as script
|
||||||
|
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
levels_path = os.path.join(base_path, "levels")
|
||||||
|
return [d for d in os.listdir(levels_path) if isdir(join(levels_path, d)) and not d.endswith(".md")]
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def selection_menu(sounds, *options):
|
def selection_menu(sounds, *options):
|
||||||
"""Display level selection menu.
|
"""Display level selection menu using instruction_menu.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sounds (dict): Dictionary of loaded sound effects
|
sounds (dict): Dictionary of loaded sound effects
|
||||||
@@ -27,70 +38,8 @@ def selection_menu(sounds, *options):
|
|||||||
Returns:
|
Returns:
|
||||||
str: Selected option or None if cancelled
|
str: Selected option or None if cancelled
|
||||||
"""
|
"""
|
||||||
loop = True
|
return instruction_menu(sounds, "Select an adventure", *options)
|
||||||
pygame.mixer.stop()
|
|
||||||
i = 0
|
|
||||||
j = -1
|
|
||||||
|
|
||||||
# Clear any pending events
|
|
||||||
pygame.event.clear()
|
|
||||||
|
|
||||||
speak("Select an adventure")
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
while loop:
|
|
||||||
if i != j:
|
|
||||||
speak(options[i])
|
|
||||||
j = i
|
|
||||||
|
|
||||||
pygame.event.pump()
|
|
||||||
event = pygame.event.wait()
|
|
||||||
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if event.key == pygame.K_DOWN and i < len(options) - 1:
|
|
||||||
i = i + 1
|
|
||||||
try:
|
|
||||||
sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if event.key == pygame.K_UP and i > 0:
|
|
||||||
i = i - 1
|
|
||||||
try:
|
|
||||||
sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if event.key == pygame.K_HOME and i != 0:
|
|
||||||
i = 0
|
|
||||||
try:
|
|
||||||
sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if event.key == pygame.K_END and i != len(options) - 1:
|
|
||||||
i = len(options) - 1
|
|
||||||
try:
|
|
||||||
sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if event.key == pygame.K_RETURN:
|
|
||||||
try:
|
|
||||||
sounds['menu-select'].play()
|
|
||||||
time.sleep(sounds['menu-select'].get_length())
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return options[i]
|
|
||||||
elif event.type == pygame.QUIT:
|
|
||||||
return None
|
|
||||||
|
|
||||||
pygame.event.pump()
|
|
||||||
event = pygame.event.clear()
|
|
||||||
time.sleep(0.001)
|
|
||||||
|
|
||||||
def select_game(sounds):
|
def select_game(sounds):
|
||||||
"""Display game selection menu and return chosen game.
|
"""Display game selection menu and return chosen game.
|
||||||
@@ -101,37 +50,48 @@ def select_game(sounds):
|
|||||||
Returns:
|
Returns:
|
||||||
str: Selected game directory name or None if cancelled
|
str: Selected game directory name or None if cancelled
|
||||||
"""
|
"""
|
||||||
availableGames = get_available_games()
|
available_games = get_available_games()
|
||||||
|
|
||||||
if not availableGames:
|
if not available_games:
|
||||||
speak("No games found in levels directory!")
|
speak("No games found in levels directory!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Convert directory names to display names (replace underscores with spaces)
|
# Convert directory names to display names (replace underscores with spaces)
|
||||||
menuOptions = [game.replace("_", " ") for game in availableGames]
|
menu_options = [game.replace("_", " ") for game in available_games]
|
||||||
|
|
||||||
choice = selection_menu(sounds, *menuOptions)
|
choice = selection_menu(sounds, *menu_options)
|
||||||
|
|
||||||
if choice is None:
|
if choice is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Convert display name back to directory name if needed
|
# Convert display name back to directory name if needed
|
||||||
gameDir = choice.replace(" ", "_")
|
game_dir = choice.replace(" ", "_")
|
||||||
if gameDir not in availableGames:
|
if game_dir not in available_games:
|
||||||
gameDir = choice # Use original if conversion doesn't match
|
game_dir = choice # Use original if conversion doesn't match
|
||||||
|
|
||||||
return gameDir
|
return game_dir
|
||||||
|
|
||||||
def get_level_path(gameDir, levelNum):
|
|
||||||
|
def get_level_path(game_dir, level_num):
|
||||||
"""Get full path to level JSON file.
|
"""Get full path to level JSON file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
gameDir (str): Game directory name
|
game_dir (str): Game directory name
|
||||||
levelNum (int): Level number
|
level_num (int): Level number
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Full path to level JSON file
|
str: Full path to level JSON file
|
||||||
"""
|
"""
|
||||||
if gameDir is None:
|
if game_dir is None:
|
||||||
raise ValueError("gameDir cannot be None")
|
raise ValueError("game_dir cannot be None")
|
||||||
return os.path.join("levels", gameDir, f"{levelNum}.json")
|
|
||||||
|
# Handle PyInstaller path issues
|
||||||
|
if hasattr(sys, "_MEIPASS"):
|
||||||
|
# Running as PyInstaller executable
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
# Running as script
|
||||||
|
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
level_path = os.path.join(base_path, "levels", game_dir, f"{level_num}.json")
|
||||||
|
return level_path
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import pygame
|
|||||||
from libstormgames import *
|
from libstormgames import *
|
||||||
from src.object import Object
|
from src.object import Object
|
||||||
|
|
||||||
|
|
||||||
class GraspingHands(Object):
|
class GraspingHands(Object):
|
||||||
"""A hazard where the ground crumbles beneath the player as undead hands reach up."""
|
"""A hazard where the ground crumbles beneath the player as undead hands reach up."""
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ class GraspingHands(Object):
|
|||||||
"", # Empty string so no regular object sound plays
|
"", # Empty string so no regular object sound plays
|
||||||
isStatic=True,
|
isStatic=True,
|
||||||
isCollectible=False,
|
isCollectible=False,
|
||||||
isHazard=True
|
isHazard=True,
|
||||||
)
|
)
|
||||||
self.sounds = sounds
|
self.sounds = sounds
|
||||||
self.delay = delay # Delay in milliseconds before ground starts crumbling
|
self.delay = delay # Delay in milliseconds before ground starts crumbling
|
||||||
@@ -51,7 +52,7 @@ class GraspingHands(Object):
|
|||||||
self.isReset = False
|
self.isReset = False
|
||||||
|
|
||||||
# Play initial warning sound
|
# Play initial warning sound
|
||||||
play_sound(self.sounds['grasping_hands_start'])
|
play_sound(self.sounds["grasping_hands_start"])
|
||||||
speak("The ground crumbles as the dead reach for you.")
|
speak("The ground crumbles as the dead reach for you.")
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
@@ -67,7 +68,7 @@ class GraspingHands(Object):
|
|||||||
self.crumbleChannel = None
|
self.crumbleChannel = None
|
||||||
|
|
||||||
# Play the end sound
|
# Play the end sound
|
||||||
play_sound(self.sounds['grasping_hands_end'])
|
play_sound(self.sounds["grasping_hands_end"])
|
||||||
|
|
||||||
def update(self, currentTime, player):
|
def update(self, currentTime, player):
|
||||||
"""Update the grasping hands trap state"""
|
"""Update the grasping hands trap state"""
|
||||||
@@ -92,7 +93,7 @@ class GraspingHands(Object):
|
|||||||
# Manage the looping positional audio for the crumbling ground
|
# Manage the looping positional audio for the crumbling ground
|
||||||
if self.crumbleChannel is None or not self.crumbleChannel.get_busy():
|
if self.crumbleChannel is None or not self.crumbleChannel.get_busy():
|
||||||
# Start the sound if it's not playing
|
# Start the sound if it's not playing
|
||||||
self.crumbleChannel = obj_play(self.sounds, 'grasping_hands', player.xPos, self.crumblePosition)
|
self.crumbleChannel = obj_play(self.sounds, "grasping_hands", player.xPos, self.crumblePosition)
|
||||||
else:
|
else:
|
||||||
# Update the sound position
|
# Update the sound position
|
||||||
self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition)
|
self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition)
|
||||||
@@ -100,8 +101,9 @@ class GraspingHands(Object):
|
|||||||
# Check if player is caught by crumbling
|
# Check if player is caught by crumbling
|
||||||
playerCaught = False
|
playerCaught = False
|
||||||
if not player.isJumping:
|
if not player.isJumping:
|
||||||
if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or \
|
if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or (
|
||||||
(self.crumbleDirection < 0 and player.xPos >= self.crumblePosition):
|
self.crumbleDirection < 0 and player.xPos >= self.crumblePosition
|
||||||
|
):
|
||||||
playerCaught = True
|
playerCaught = True
|
||||||
|
|
||||||
if playerCaught:
|
if playerCaught:
|
||||||
@@ -122,6 +124,6 @@ class GraspingHands(Object):
|
|||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""Cleanup when object is destroyed"""
|
"""Cleanup when object is destroyed"""
|
||||||
# Ensure sound is stopped when object is destroyed
|
# Ensure sound is stopped when object is destroyed
|
||||||
if hasattr(self, 'crumbleChannel') and self.crumbleChannel:
|
if hasattr(self, "crumbleChannel") and self.crumbleChannel:
|
||||||
obj_stop(self.crumbleChannel)
|
obj_stop(self.crumbleChannel)
|
||||||
self.crumbleChannel = None
|
self.crumbleChannel = None
|
||||||
|
|||||||
+40
-8
@@ -8,14 +8,11 @@ from src.powerup import PowerUp
|
|||||||
class GraveObject(Object):
|
class GraveObject(Object):
|
||||||
def __init__(self, x, y, sounds, item=None, zombieSpawnChance=0):
|
def __init__(self, x, y, sounds, item=None, zombieSpawnChance=0):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
x, y, "grave",
|
x, y, "grave", isStatic=True, isCollectible=False, isHazard=True, zombieSpawnChance=zombieSpawnChance
|
||||||
isStatic=True,
|
|
||||||
isCollectible=False,
|
|
||||||
isHazard=True,
|
|
||||||
zombieSpawnChance=zombieSpawnChance
|
|
||||||
)
|
)
|
||||||
self.graveItem = item
|
self.graveItem = item
|
||||||
self.isCollected = False # Renamed to match style of isHazard, isStatic etc
|
self.isCollected = False # Renamed to match style of isHazard, isStatic etc
|
||||||
|
self.isFilled = False # Track if grave has been filled with shovel
|
||||||
self.sounds = sounds
|
self.sounds = sounds
|
||||||
|
|
||||||
def collect_grave_item(self, player):
|
def collect_grave_item(self, player):
|
||||||
@@ -28,9 +25,44 @@ class GraveObject(Object):
|
|||||||
if not self.graveItem or self.isCollected:
|
if not self.graveItem or self.isCollected:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Collect the item if player is ducking
|
# Collect the item if player is ducking, walking (not running), and wielding shovel
|
||||||
if player.isDucking:
|
if (
|
||||||
|
player.isDucking
|
||||||
|
and not player.isRunning
|
||||||
|
and player.currentWeapon
|
||||||
|
and player.currentWeapon.name == "rusty_shovel"
|
||||||
|
):
|
||||||
self.isCollected = True # Mark as collected when collection succeeds
|
self.isCollected = True # Mark as collected when collection succeeds
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def can_fill_grave(self, player):
|
||||||
|
"""Check if grave can be filled with shovel.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if grave can be filled, False otherwise
|
||||||
|
"""
|
||||||
|
# Can only fill empty graves (no item) that haven't been filled yet
|
||||||
|
if self.graveItem or self.isFilled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Must be ducking, walking (not running), and wielding shovel
|
||||||
|
return (
|
||||||
|
player.isDucking
|
||||||
|
and not player.isRunning
|
||||||
|
and player.currentWeapon
|
||||||
|
and player.currentWeapon.name == "rusty_shovel"
|
||||||
|
)
|
||||||
|
|
||||||
|
def fill_grave(self, player):
|
||||||
|
"""Fill the grave with dirt using shovel.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if grave was filled successfully
|
||||||
|
"""
|
||||||
|
if self.can_fill_grave(player):
|
||||||
|
self.isFilled = True
|
||||||
|
self.isHazard = False # No longer a hazard once filled
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|||||||
+4
-5
@@ -6,6 +6,7 @@ from enum import Enum, auto
|
|||||||
|
|
||||||
class ItemType(Enum):
|
class ItemType(Enum):
|
||||||
"""Defines available item types and their properties"""
|
"""Defines available item types and their properties"""
|
||||||
|
|
||||||
GUTS = auto()
|
GUTS = auto()
|
||||||
HAND_OF_GLORY = auto()
|
HAND_OF_GLORY = auto()
|
||||||
JACK_O_LANTERN = auto()
|
JACK_O_LANTERN = auto()
|
||||||
@@ -13,14 +14,12 @@ class ItemType(Enum):
|
|||||||
CAULDRON = auto()
|
CAULDRON = auto()
|
||||||
WITCH_BROOM = auto()
|
WITCH_BROOM = auto()
|
||||||
|
|
||||||
|
|
||||||
class ItemProperties:
|
class ItemProperties:
|
||||||
"""Manages item properties and availability"""
|
"""Manages item properties and availability"""
|
||||||
|
|
||||||
# Items that can appear in random drops
|
# Items that can appear in random drops
|
||||||
RANDOM_ELIGIBLE = {
|
RANDOM_ELIGIBLE = {ItemType.HAND_OF_GLORY: "hand_of_glory", ItemType.JACK_O_LANTERN: "jack_o_lantern"}
|
||||||
ItemType.HAND_OF_GLORY: "hand_of_glory",
|
|
||||||
ItemType.JACK_O_LANTERN: "jack_o_lantern"
|
|
||||||
}
|
|
||||||
|
|
||||||
# All possible items (including special ones)
|
# All possible items (including special ones)
|
||||||
ALL_ITEMS = {
|
ALL_ITEMS = {
|
||||||
@@ -29,7 +28,7 @@ class ItemProperties:
|
|||||||
ItemType.JACK_O_LANTERN: "jack_o_lantern",
|
ItemType.JACK_O_LANTERN: "jack_o_lantern",
|
||||||
ItemType.EXTRA_LIFE: "extra_life",
|
ItemType.EXTRA_LIFE: "extra_life",
|
||||||
ItemType.CAULDRON: "cauldron",
|
ItemType.CAULDRON: "cauldron",
|
||||||
ItemType.WITCH_BROOM: "witch_broom"
|
ItemType.WITCH_BROOM: "witch_broom",
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
+130
-82
@@ -16,15 +16,16 @@ from src.skull_storm import SkullStorm
|
|||||||
|
|
||||||
|
|
||||||
class Level:
|
class Level:
|
||||||
def __init__(self, levelData, sounds, player):
|
def __init__(self, levelData, sounds, player, levelPackName=None):
|
||||||
self.sounds = sounds
|
self.sounds = sounds
|
||||||
|
self.levelPackName = levelPackName
|
||||||
self.objects = []
|
self.objects = []
|
||||||
self.enemies = []
|
self.enemies = []
|
||||||
self.bouncing_items = []
|
self.bouncing_items = []
|
||||||
self.projectiles = [] # Track active projectiles
|
self.projectiles = [] # Track active projectiles
|
||||||
self.player = player
|
self.player = player
|
||||||
self.lastWarningTime = 0
|
self.lastWarningTime = 0
|
||||||
self.warningInterval = int(self.sounds['edge'].get_length() * 1000) # Convert seconds to milliseconds
|
self.warningInterval = int(self.sounds["edge"].get_length() * 1000) # Convert seconds to milliseconds
|
||||||
self.weapon_hit_channel = None
|
self.weapon_hit_channel = None
|
||||||
|
|
||||||
self.leftBoundary = levelData["boundaries"]["left"]
|
self.leftBoundary = levelData["boundaries"]["left"]
|
||||||
@@ -40,23 +41,35 @@ class Level:
|
|||||||
# Pass footstep sound to player
|
# Pass footstep sound to player
|
||||||
self.player.set_footstep_sound(self.footstepSound)
|
self.player.set_footstep_sound(self.footstepSound)
|
||||||
|
|
||||||
# Level intro message
|
# Level intro message (skip for survival mode)
|
||||||
levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. "
|
if levelData["level_id"] != 999: # 999 is survival mode
|
||||||
if self.isLocked:
|
levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. "
|
||||||
levelIntro += "This is a boss level. You must defeat all enemies before you can advance. "
|
if self.isLocked:
|
||||||
levelIntro += levelData['description']
|
levelIntro += "This is a boss level. You must defeat all enemies before you can advance. "
|
||||||
messagebox(levelIntro)
|
levelIntro += levelData["description"]
|
||||||
|
messagebox(levelIntro)
|
||||||
|
|
||||||
# Handle level music
|
# Handle level music
|
||||||
try:
|
try:
|
||||||
pygame.mixer.music.stop()
|
pygame.mixer.music.stop()
|
||||||
if "ambience" in levelData:
|
if "ambience" in levelData:
|
||||||
try:
|
ambientFile = levelData["ambience"]
|
||||||
pygame.mixer.music.load(f"sounds/ambience/{levelData['ambience']}")
|
|
||||||
pygame.mixer.music.play(-1) # Loop indefinitely
|
# Build list of paths to try (pack-specific first, then generic)
|
||||||
except:
|
ambiencePaths = []
|
||||||
pass
|
if self.levelPackName:
|
||||||
except:
|
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
|
pass
|
||||||
|
|
||||||
# Create end of level object at right boundary
|
# Create end of level object at right boundary
|
||||||
@@ -66,7 +79,7 @@ class Level:
|
|||||||
"end_of_level",
|
"end_of_level",
|
||||||
isStatic=True,
|
isStatic=True,
|
||||||
isCollectible=False,
|
isCollectible=False,
|
||||||
isHazard=False
|
isHazard=False,
|
||||||
)
|
)
|
||||||
self.objects.append(endLevel)
|
self.objects.append(endLevel)
|
||||||
|
|
||||||
@@ -85,7 +98,7 @@ class Level:
|
|||||||
obj["y"],
|
obj["y"],
|
||||||
self.sounds,
|
self.sounds,
|
||||||
fireInterval=obj.get("fireInterval", 5000),
|
fireInterval=obj.get("fireInterval", 5000),
|
||||||
firingRange=obj.get("range", 20)
|
firingRange=obj.get("range", 20),
|
||||||
)
|
)
|
||||||
self.objects.append(catapult)
|
self.objects.append(catapult)
|
||||||
# Check if this is grasping hands
|
# Check if this is grasping hands
|
||||||
@@ -95,7 +108,7 @@ class Level:
|
|||||||
obj["y"],
|
obj["y"],
|
||||||
self.sounds,
|
self.sounds,
|
||||||
delay=obj.get("delay", 1000),
|
delay=obj.get("delay", 1000),
|
||||||
crumble_speed=obj.get("crumble_speed", 0.03)
|
crumble_speed=obj.get("crumble_speed", 0.03),
|
||||||
)
|
)
|
||||||
self.objects.append(graspingHands)
|
self.objects.append(graspingHands)
|
||||||
# Check if this is a grave
|
# Check if this is a grave
|
||||||
@@ -105,7 +118,7 @@ class Level:
|
|||||||
obj["y"],
|
obj["y"],
|
||||||
self.sounds,
|
self.sounds,
|
||||||
item=obj.get("item", None),
|
item=obj.get("item", None),
|
||||||
zombieSpawnChance=obj.get("zombie_spawn_chance", 0)
|
zombieSpawnChance=obj.get("zombie_spawn_chance", 0),
|
||||||
)
|
)
|
||||||
self.objects.append(grave)
|
self.objects.append(grave)
|
||||||
# Check if this is a skull storm
|
# Check if this is a skull storm
|
||||||
@@ -117,7 +130,7 @@ class Level:
|
|||||||
obj.get("damage", 5),
|
obj.get("damage", 5),
|
||||||
obj.get("maximum_skulls", 3),
|
obj.get("maximum_skulls", 3),
|
||||||
obj.get("frequency", {}).get("min", 2),
|
obj.get("frequency", {}).get("min", 2),
|
||||||
obj.get("frequency", {}).get("max", 5)
|
obj.get("frequency", {}).get("max", 5),
|
||||||
)
|
)
|
||||||
self.objects.append(skullStorm)
|
self.objects.append(skullStorm)
|
||||||
# Check if this is a coffin
|
# Check if this is a coffin
|
||||||
@@ -127,7 +140,7 @@ class Level:
|
|||||||
obj["y"],
|
obj["y"],
|
||||||
self.sounds,
|
self.sounds,
|
||||||
self, # Pass level reference
|
self, # Pass level reference
|
||||||
item=obj.get("item", "random") # Get item type or default to random
|
item=obj.get("item", "random"), # Get item type or default to random
|
||||||
)
|
)
|
||||||
self.objects.append(coffin)
|
self.objects.append(coffin)
|
||||||
# Check if this is a spider web
|
# Check if this is a spider web
|
||||||
@@ -135,8 +148,7 @@ class Level:
|
|||||||
# Check distance from graves
|
# Check distance from graves
|
||||||
isValidPosition = True
|
isValidPosition = True
|
||||||
for existingObj in self.objects:
|
for existingObj in self.objects:
|
||||||
if (existingObj.soundName == "grave" and
|
if existingObj.soundName == "grave" and not hasattr(existingObj, "graveItem"):
|
||||||
not hasattr(existingObj, 'graveItem')):
|
|
||||||
distance = abs(obj["x"] - existingObj.xPos)
|
distance = abs(obj["x"] - existingObj.xPos)
|
||||||
if distance < 3:
|
if distance < 3:
|
||||||
isValidPosition = False
|
isValidPosition = False
|
||||||
@@ -163,7 +175,7 @@ class Level:
|
|||||||
damage=obj.get("damage", 1),
|
damage=obj.get("damage", 1),
|
||||||
attack_range=obj.get("attack_range", 1),
|
attack_range=obj.get("attack_range", 1),
|
||||||
movement_range=obj.get("movement_range", 5),
|
movement_range=obj.get("movement_range", 5),
|
||||||
attack_pattern=obj.get("attack_pattern", {'type': 'patrol'}),
|
attack_pattern=obj.get("attack_pattern", {"type": "patrol"}),
|
||||||
can_spawn=obj.get("can_spawn", False),
|
can_spawn=obj.get("can_spawn", False),
|
||||||
spawn_type=obj.get("spawn_type", "zombie"),
|
spawn_type=obj.get("spawn_type", "zombie"),
|
||||||
spawn_cooldown=obj.get("spawn_cooldown", 2000),
|
spawn_cooldown=obj.get("spawn_cooldown", 2000),
|
||||||
@@ -172,7 +184,7 @@ class Level:
|
|||||||
has_vulnerability=obj.get("has_vulnerability", False),
|
has_vulnerability=obj.get("has_vulnerability", False),
|
||||||
is_vulnerable=obj.get("is_vulnerable", False),
|
is_vulnerable=obj.get("is_vulnerable", False),
|
||||||
vulnerability_duration=obj.get("vulnerability_duration", 1000),
|
vulnerability_duration=obj.get("vulnerability_duration", 1000),
|
||||||
invulnerability_duration=obj.get("invulnerability_duration", 5000)
|
invulnerability_duration=obj.get("invulnerability_duration", 5000),
|
||||||
)
|
)
|
||||||
self.enemies.append(enemy)
|
self.enemies.append(enemy)
|
||||||
else:
|
else:
|
||||||
@@ -183,13 +195,13 @@ class Level:
|
|||||||
isStatic=obj.get("static", True),
|
isStatic=obj.get("static", True),
|
||||||
isCollectible=obj.get("collectible", False),
|
isCollectible=obj.get("collectible", False),
|
||||||
isHazard=obj.get("hazard", False),
|
isHazard=obj.get("hazard", False),
|
||||||
zombieSpawnChance=obj.get("zombieSpawnChance", 0)
|
zombieSpawnChance=obj.get("zombieSpawnChance", 0),
|
||||||
)
|
)
|
||||||
self.objects.append(gameObject)
|
self.objects.append(gameObject)
|
||||||
enemyCount = len(self.enemies)
|
enemyCount = len(self.enemies)
|
||||||
coffinCount = sum(1 for obj in self.objects if hasattr(obj, 'isBroken'))
|
coffinCount = sum(1 for obj in self.objects if hasattr(obj, "isBroken"))
|
||||||
player.stats.update_stat('Enemies remaining', enemyCount)
|
player.stats.update_stat("Enemies remaining", enemyCount)
|
||||||
player.stats.update_stat('Coffins remaining', coffinCount)
|
player.stats.update_stat("Coffins remaining", coffinCount)
|
||||||
|
|
||||||
def update_audio(self):
|
def update_audio(self):
|
||||||
"""Update all audio and entity state."""
|
"""Update all audio and entity state."""
|
||||||
@@ -201,9 +213,7 @@ class Level:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Check for potential zombie spawn from graves
|
# Check for potential zombie spawn from graves
|
||||||
if (obj.soundName == "grave" and
|
if obj.soundName == "grave" and obj.zombieSpawnChance > 0 and not obj.hasSpawned:
|
||||||
obj.zombieSpawnChance > 0 and
|
|
||||||
not obj.hasSpawned):
|
|
||||||
|
|
||||||
distance = abs(self.player.xPos - obj.xPos)
|
distance = abs(self.player.xPos - obj.xPos)
|
||||||
if distance < 6: # Within 6 tiles
|
if distance < 6: # Within 6 tiles
|
||||||
@@ -212,6 +222,10 @@ class Level:
|
|||||||
|
|
||||||
roll = random.randint(1, 100)
|
roll = random.randint(1, 100)
|
||||||
if roll <= obj.zombieSpawnChance:
|
if roll <= obj.zombieSpawnChance:
|
||||||
|
# Set behavior based on game mode
|
||||||
|
behavior = "hunter" if self.levelId == 999 else "patrol"
|
||||||
|
turn_rate = 2 if self.levelId == 999 else 8 # Faster turn rate for survival
|
||||||
|
|
||||||
zombie = Enemy(
|
zombie = Enemy(
|
||||||
[obj.xPos, obj.xPos],
|
[obj.xPos, obj.xPos],
|
||||||
obj.yPos,
|
obj.yPos,
|
||||||
@@ -220,7 +234,9 @@ class Level:
|
|||||||
self, # Pass the level reference
|
self, # Pass the level reference
|
||||||
health=3,
|
health=3,
|
||||||
damage=10,
|
damage=10,
|
||||||
attack_range=1
|
attack_range=1,
|
||||||
|
attack_pattern={"type": behavior},
|
||||||
|
turn_rate=turn_rate,
|
||||||
)
|
)
|
||||||
self.enemies.append(zombie)
|
self.enemies.append(zombie)
|
||||||
speak("A zombie emerges from the grave!")
|
speak("A zombie emerges from the grave!")
|
||||||
@@ -265,6 +281,16 @@ class Level:
|
|||||||
if caught:
|
if caught:
|
||||||
return # Stop if player is dead
|
return # Stop if player is dead
|
||||||
|
|
||||||
|
# Update death sound objects
|
||||||
|
from src.die_monster_die import DeathSound
|
||||||
|
|
||||||
|
for obj in self.objects:
|
||||||
|
if isinstance(obj, DeathSound):
|
||||||
|
obj.update(currentTime)
|
||||||
|
|
||||||
|
# Clean up inactive objects (including finished death sounds)
|
||||||
|
self.objects = [obj for obj in self.objects if obj.isActive]
|
||||||
|
|
||||||
# Update bouncing items
|
# Update bouncing items
|
||||||
for item in self.bouncing_items[:]: # Copy list to allow removal
|
for item in self.bouncing_items[:]: # Copy list to allow removal
|
||||||
if not item.update(currentTime, self.player.xPos):
|
if not item.update(currentTime, self.player.xPos):
|
||||||
@@ -275,8 +301,8 @@ class Level:
|
|||||||
|
|
||||||
# Check for item collection
|
# Check for item collection
|
||||||
if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping:
|
if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping:
|
||||||
play_sound(self.sounds[f'get_{item.soundName}'])
|
play_sound(self.sounds[f"get_{item.soundName}"])
|
||||||
item.apply_effect(self.player)
|
item.apply_effect(self.player, self)
|
||||||
self.levelScore += 1000 # All items collected points awarded
|
self.levelScore += 1000 # All items collected points awarded
|
||||||
item.isActive = False
|
item.isActive = False
|
||||||
self.bouncing_items.remove(item)
|
self.bouncing_items.remove(item)
|
||||||
@@ -297,11 +323,13 @@ class Level:
|
|||||||
|
|
||||||
# Check for coffin hits
|
# Check for coffin hits
|
||||||
for obj in self.objects:
|
for obj in self.objects:
|
||||||
if hasattr(obj, 'isBroken'): # Check if it's a coffin without using isinstance
|
if hasattr(obj, "isBroken"): # Check if it's a coffin without using isinstance
|
||||||
if (not obj.isBroken and
|
if (
|
||||||
obj.xPos >= attackRange[0] and
|
not obj.isBroken
|
||||||
obj.xPos <= attackRange[1] and
|
and obj.xPos >= attackRange[0]
|
||||||
self.player.isJumping): # Must be jumping to hit floating coffins
|
and obj.xPos <= attackRange[1]
|
||||||
|
and self.player.isJumping
|
||||||
|
): # Must be jumping to hit floating coffins
|
||||||
|
|
||||||
if obj.hit(self.player.xPos):
|
if obj.hit(self.player.xPos):
|
||||||
self.bouncing_items.append(obj.dropped_item)
|
self.bouncing_items.append(obj.dropped_item)
|
||||||
@@ -317,7 +345,7 @@ class Level:
|
|||||||
health=8,
|
health=8,
|
||||||
damage=8,
|
damage=8,
|
||||||
attack_range=1,
|
attack_range=1,
|
||||||
speed_multiplier=2.0
|
speed_multiplier=2.0,
|
||||||
)
|
)
|
||||||
self.enemies.append(spider)
|
self.enemies.append(spider)
|
||||||
|
|
||||||
@@ -336,62 +364,71 @@ class Level:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle grave edge warnings
|
# Handle grave edge warnings
|
||||||
if obj.isHazard and obj.soundName != "spiderweb" and not isinstance(obj, GraspingHands): # Exclude spiderwebs and grasping hands
|
if (
|
||||||
|
obj.isHazard and obj.soundName != "spiderweb" and not isinstance(obj, GraspingHands)
|
||||||
|
): # Exclude spiderwebs and grasping hands
|
||||||
distance = abs(self.player.xPos - obj.xPos)
|
distance = abs(self.player.xPos - obj.xPos)
|
||||||
currentTime = pygame.time.get_ticks()
|
currentTime = pygame.time.get_ticks()
|
||||||
if (distance <= 2 and not self.player.isJumping and not self.player.isInvincible
|
if (
|
||||||
and currentTime - self.lastWarningTime >= self.warningInterval):
|
distance <= 2
|
||||||
|
and not self.player.isJumping
|
||||||
|
and not self.player.isInvincible
|
||||||
|
and currentTime - self.lastWarningTime >= self.warningInterval
|
||||||
|
):
|
||||||
if isinstance(obj, GraveObject) and obj.graveItem and not obj.isCollected:
|
if isinstance(obj, GraveObject) and obj.graveItem and not obj.isCollected:
|
||||||
play_sound(self.sounds['_edge'])
|
play_sound(self.sounds["_edge"])
|
||||||
else:
|
else:
|
||||||
play_sound(self.sounds['edge'])
|
play_sound(self.sounds["edge"])
|
||||||
self.lastWarningTime = currentTime
|
self.lastWarningTime = currentTime
|
||||||
|
|
||||||
if not obj.is_in_range(self.player.xPos):
|
if not obj.is_in_range(self.player.xPos):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle collectibles
|
# Handle collectibles (but not if player died this frame)
|
||||||
if obj.isCollectible and self.player.isJumping:
|
if obj.isCollectible and self.player.isJumping and not self.player.diedThisFrame:
|
||||||
currentPos = round(self.player.xPos)
|
currentPos = round(self.player.xPos)
|
||||||
if currentPos not in obj.collectedPositions:
|
if currentPos not in obj.collectedPositions:
|
||||||
play_sound(self.sounds[f'get_{obj.soundName}'])
|
play_sound(self.sounds[f"get_{obj.soundName}"])
|
||||||
obj.collect_at_position(currentPos)
|
obj.collect_at_position(currentPos)
|
||||||
self.player.collectedItems.append(obj.soundName)
|
self.player.collectedItems.append(obj.soundName)
|
||||||
self.player.stats.update_stat('Items collected', 1)
|
self.player.stats.update_stat("Items collected", 1)
|
||||||
if obj.soundName == "bone_dust":
|
if obj.soundName == "bone_dust":
|
||||||
self.player._coins += 1
|
self.player._coins += 1
|
||||||
self.player.add_save_bone_dust(1) # Add to save bone dust counter too
|
self.player.add_save_bone_dust(1) # Add to save bone dust counter too
|
||||||
self.levelScore += 100
|
self.levelScore += 100
|
||||||
self.player.stats.update_stat('Bone dust', 1)
|
self.player.stats.update_stat("Bone dust", 1)
|
||||||
if self.player._coins % 5 == 0:
|
if self.player._coins % 5 == 0:
|
||||||
# Only heal if below max health
|
# Only heal if below max health
|
||||||
if self.player.get_health() < self.player.get_max_health():
|
if self.player.get_health() < self.player.get_max_health():
|
||||||
self.player.set_health(min(
|
self.player.set_health(min(self.player.get_health() + 1, self.player.get_max_health()))
|
||||||
self.player.get_health() + 1,
|
|
||||||
self.player.get_max_health()
|
|
||||||
))
|
|
||||||
|
|
||||||
if self.player._coins % 100 == 0:
|
if self.player._coins % 100 == 0:
|
||||||
# Extra life
|
# Only give extra lives in story mode, not survival mode (level_id 999)
|
||||||
self.player._coins = 0
|
if self.levelId != 999:
|
||||||
self.player._lives += 1
|
# Extra life
|
||||||
self.levelScore += 1000
|
self.player._coins = 0
|
||||||
play_sound(self.sounds['get_extra_life'])
|
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
|
continue
|
||||||
|
|
||||||
# Handle spiderweb - this should trigger for both walking and jumping if not ducking
|
# Handle spiderweb - this should trigger for both walking and jumping if not ducking
|
||||||
if obj.soundName == "spiderweb" and not self.player.isDucking:
|
if obj.soundName == "spiderweb" and not self.player.isDucking:
|
||||||
# Create and apply web effect
|
# Create and apply web effect
|
||||||
webEffect = PowerUp(
|
webEffect = PowerUp(
|
||||||
obj.xPos,
|
obj.xPos, obj.yPos, "spiderweb", self.sounds, 0 # No direction needed since it's just for effect
|
||||||
obj.yPos,
|
|
||||||
'spiderweb',
|
|
||||||
self.sounds,
|
|
||||||
0 # No direction needed since it's just for effect
|
|
||||||
)
|
)
|
||||||
webEffect.level = self # Pass level reference for spider spawning
|
webEffect.level = self # Pass level reference for spider spawning
|
||||||
play_sound(self.sounds['hit_spiderweb'])
|
play_sound(self.sounds["hit_spiderweb"])
|
||||||
webEffect.apply_effect(self.player)
|
webEffect.apply_effect(self.player, self)
|
||||||
|
|
||||||
# Deactivate web
|
# Deactivate web
|
||||||
obj.isActive = False
|
obj.isActive = False
|
||||||
@@ -401,16 +438,19 @@ class Level:
|
|||||||
# Handle graves and other hazards
|
# Handle graves and other hazards
|
||||||
if obj.isHazard and not self.player.isJumping:
|
if obj.isHazard and not self.player.isJumping:
|
||||||
if isinstance(obj, GraveObject):
|
if isinstance(obj, GraveObject):
|
||||||
can_collect = obj.collect_grave_item(self.player)
|
can_collect = obj.collect_grave_item(self.player) and not self.player.diedThisFrame
|
||||||
|
can_fill = obj.can_fill_grave(self.player)
|
||||||
|
|
||||||
if can_collect:
|
if can_collect:
|
||||||
# Successfully collected item while ducking
|
# Successfully collected item while ducking with shovel
|
||||||
play_sound(self.sounds[f'get_{obj.graveItem}'])
|
play_sound(self.sounds[f"get_{obj.graveItem}"])
|
||||||
self.player.stats.update_stat('Items collected', 1)
|
play_sound(self.sounds.get("fill_in_grave", "shovel_dig")) # Also play fill sound
|
||||||
|
self.player.stats.update_stat("Items collected", 1)
|
||||||
# Create PowerUp to handle the item effect
|
# Create PowerUp to handle the item effect
|
||||||
item = PowerUp(obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1,
|
item = PowerUp(
|
||||||
self.leftBoundary, self.rightBoundary)
|
obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1, self.leftBoundary, self.rightBoundary
|
||||||
item.apply_effect(self.player)
|
)
|
||||||
|
item.apply_effect(self.player, self)
|
||||||
# Stop grave's current audio channel
|
# Stop grave's current audio channel
|
||||||
if obj.channel:
|
if obj.channel:
|
||||||
obj_stop(obj.channel)
|
obj_stop(obj.channel)
|
||||||
@@ -419,6 +459,18 @@ class Level:
|
|||||||
obj.channel = None
|
obj.channel = None
|
||||||
obj.isActive = False # Mark the grave as inactive after collection
|
obj.isActive = False # Mark the grave as inactive after collection
|
||||||
continue
|
continue
|
||||||
|
elif can_fill and obj.fill_grave(self.player):
|
||||||
|
# Successfully filled empty grave with shovel
|
||||||
|
play_sound(
|
||||||
|
self.sounds.get("fill_in_grave", "shovel_dig")
|
||||||
|
) # Use fill_in_grave sound if available, fallback to shovel_dig
|
||||||
|
self.player.stats.update_stat("Graves filled", 1)
|
||||||
|
# Stop grave's current audio channel
|
||||||
|
if obj.channel:
|
||||||
|
obj_stop(obj.channel)
|
||||||
|
obj.channel = None
|
||||||
|
obj.isActive = False # Mark grave as inactive after filling
|
||||||
|
continue
|
||||||
elif not self.player.isInvincible:
|
elif not self.player.isInvincible:
|
||||||
# Kill player for normal graves or non-ducking collision
|
# Kill player for normal graves or non-ducking collision
|
||||||
play_sound(self.sounds[obj.soundName])
|
play_sound(self.sounds[obj.soundName])
|
||||||
@@ -440,14 +492,14 @@ class Level:
|
|||||||
# If level is locked, check for remaining enemies
|
# If level is locked, check for remaining enemies
|
||||||
if self.isLocked and any(enemy.isActive for enemy in self.enemies):
|
if self.isLocked and any(enemy.isActive for enemy in self.enemies):
|
||||||
speak("You must defeat all enemies before proceeding!")
|
speak("You must defeat all enemies before proceeding!")
|
||||||
play_sound(self.sounds['locked'])
|
play_sound(self.sounds["locked"])
|
||||||
# Push player back a bit
|
# Push player back a bit
|
||||||
self.player.xPos -= 5
|
self.player.xPos -= 5
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Level complete
|
# Level complete
|
||||||
pygame.mixer.stop()
|
pygame.mixer.stop()
|
||||||
play_sound(self.sounds['end_of_level'])
|
play_sound(self.sounds["end_of_level"])
|
||||||
self.levelScore += 10000
|
self.levelScore += 10000
|
||||||
# Actually update the scoreboard with level completion
|
# Actually update the scoreboard with level completion
|
||||||
self.player.scoreboard.increase_score(self.levelScore)
|
self.player.scoreboard.increase_score(self.levelScore)
|
||||||
@@ -470,7 +522,7 @@ class Level:
|
|||||||
# Calculate volume and pan for splat sound based on final position
|
# Calculate volume and pan for splat sound based on final position
|
||||||
volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x)
|
volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x)
|
||||||
if volume > 0: # Only play if within audible range
|
if volume > 0: # Only play if within audible range
|
||||||
obj_play(self.sounds, 'pumpkin_splat', self.player.xPos, proj.x, loop=False)
|
obj_play(self.sounds, "pumpkin_splat", self.player.xPos, proj.x, loop=False)
|
||||||
break
|
break
|
||||||
|
|
||||||
def throw_projectile(self):
|
def throw_projectile(self):
|
||||||
@@ -480,10 +532,6 @@ class Level:
|
|||||||
speak("No jack o'lanterns to throw!")
|
speak("No jack o'lanterns to throw!")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.projectiles.append(Projectile(
|
self.projectiles.append(Projectile(proj_info["type"], proj_info["start_x"], proj_info["direction"]))
|
||||||
proj_info['type'],
|
|
||||||
proj_info['start_x'],
|
|
||||||
proj_info['direction']
|
|
||||||
))
|
|
||||||
# Play throw sound
|
# Play throw sound
|
||||||
play_sound(self.sounds['throw_jack_o_lantern'])
|
play_sound(self.sounds["throw_jack_o_lantern"])
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from libstormgames import *
|
from libstormgames import *
|
||||||
|
|
||||||
|
|
||||||
class Object:
|
class Object:
|
||||||
def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False, zombieSpawnChance=0):
|
def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False, zombieSpawnChance=0):
|
||||||
# x can be either a single position or a range [start, end]
|
# x can be either a single position or a range [start, end]
|
||||||
|
|||||||
@@ -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.invincibilityStartTime = 0
|
||||||
self.invincibilityDuration = 10000 # 10 seconds of invincibility
|
self.invincibilityDuration = 10000 # 10 seconds of invincibility
|
||||||
|
|
||||||
|
# Death state tracking (to prevent revival after death in same frame)
|
||||||
|
self.diedThisFrame = False
|
||||||
|
|
||||||
# Initialize starting weapon (rusty shovel)
|
# Initialize starting weapon (rusty shovel)
|
||||||
self.add_weapon(Weapon(
|
self.add_weapon(
|
||||||
name="rusty_shovel",
|
Weapon(
|
||||||
damage=2,
|
name="rusty_shovel",
|
||||||
range=2,
|
damage=2,
|
||||||
attackSound="player_shovel_attack",
|
range=2,
|
||||||
hitSound="player_shovel_hit",
|
attackSound="player_shovel_attack",
|
||||||
attackDuration=200 # 200ms attack duration
|
hitSound="player_shovel_hit",
|
||||||
))
|
attackDuration=200, # 200ms attack duration
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.scoreboard = Scoreboard()
|
self.scoreboard = Scoreboard()
|
||||||
|
|
||||||
def should_play_footstep(self, currentTime):
|
def should_play_footstep(self, currentTime):
|
||||||
"""Check if it's time to play a footstep sound"""
|
"""Check if it's time to play a footstep sound"""
|
||||||
return (self.distanceSinceLastStep >= self.get_step_distance() and
|
return (
|
||||||
currentTime - self.lastStepTime >= self.get_step_interval())
|
self.distanceSinceLastStep >= self.get_step_distance()
|
||||||
|
and currentTime - self.lastStepTime >= self.get_step_interval()
|
||||||
|
)
|
||||||
|
|
||||||
def duck(self):
|
def duck(self):
|
||||||
"""Start ducking"""
|
"""Start ducking"""
|
||||||
if not self.isDucking and not self.isJumping: # Can't duck while jumping
|
if not self.isDucking and not self.isJumping: # Can't duck while jumping
|
||||||
self.isDucking = True
|
self.isDucking = True
|
||||||
play_sound(self.sounds['duck'])
|
play_sound(self.sounds["duck"])
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -88,11 +95,14 @@ class Player:
|
|||||||
"""Stop ducking state and play sound"""
|
"""Stop ducking state and play sound"""
|
||||||
if self.isDucking:
|
if self.isDucking:
|
||||||
self.isDucking = False
|
self.isDucking = False
|
||||||
play_sound(self.sounds['stand'])
|
play_sound(self.sounds["stand"])
|
||||||
|
|
||||||
def update(self, currentTime):
|
def update(self, currentTime):
|
||||||
"""Update player state"""
|
"""Update player state"""
|
||||||
if hasattr(self, 'webPenaltyEndTime'):
|
# Reset death flag at start of each frame
|
||||||
|
self.diedThisFrame = False
|
||||||
|
|
||||||
|
if hasattr(self, "webPenaltyEndTime"):
|
||||||
if currentTime >= self.webPenaltyEndTime:
|
if currentTime >= self.webPenaltyEndTime:
|
||||||
self.moveSpeed *= 2 # Restore speed
|
self.moveSpeed *= 2 # Restore speed
|
||||||
if self.currentWeapon:
|
if self.currentWeapon:
|
||||||
@@ -101,15 +111,17 @@ class Player:
|
|||||||
|
|
||||||
# Check invincibility status
|
# Check invincibility status
|
||||||
if self.isInvincible:
|
if self.isInvincible:
|
||||||
remaining_time = (self.invincibilityStartTime + self.invincibilityDuration - currentTime) / 1000 # Convert to seconds
|
remaining_time = (
|
||||||
|
self.invincibilityStartTime + self.invincibilityDuration - currentTime
|
||||||
|
) / 1000 # Convert to seconds
|
||||||
|
|
||||||
# Handle countdown sounds
|
# Handle countdown sounds
|
||||||
if not hasattr(self, '_last_countdown'):
|
if not hasattr(self, "_last_countdown"):
|
||||||
self._last_countdown = 4 # Start counting from 4 to catch 3,2,1
|
self._last_countdown = 4 # Start counting from 4 to catch 3,2,1
|
||||||
|
|
||||||
current_second = int(remaining_time)
|
current_second = int(remaining_time)
|
||||||
if current_second < self._last_countdown and current_second <= 3 and current_second > 0:
|
if current_second < self._last_countdown and current_second <= 3 and current_second > 0:
|
||||||
play_sound(self.sounds['end_of_invincibility_warning'])
|
play_sound(self.sounds["end_of_invincibility_warning"])
|
||||||
self._last_countdown = current_second
|
self._last_countdown = current_second
|
||||||
|
|
||||||
# Check if invincibility has expired
|
# Check if invincibility has expired
|
||||||
@@ -122,7 +134,7 @@ class Player:
|
|||||||
"""Activate invincibility from Hand of Glory"""
|
"""Activate invincibility from Hand of Glory"""
|
||||||
self.isInvincible = True
|
self.isInvincible = True
|
||||||
self.invincibilityStartTime = pygame.time.get_ticks()
|
self.invincibilityStartTime = pygame.time.get_ticks()
|
||||||
if hasattr(self, '_last_countdown'):
|
if hasattr(self, "_last_countdown"):
|
||||||
del self._last_countdown # Reset countdown if it exists
|
del self._last_countdown # Reset countdown if it exists
|
||||||
|
|
||||||
def extra_life(self):
|
def extra_life(self):
|
||||||
@@ -150,23 +162,27 @@ class Player:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
self._jack_o_lantern_count -= 1
|
self._jack_o_lantern_count -= 1
|
||||||
return {
|
return {"type": "jack_o_lantern", "start_x": self.xPos, "direction": 1 if self.facingRight else -1}
|
||||||
'type': 'jack_o_lantern',
|
|
||||||
'start_x': self.xPos,
|
|
||||||
'direction': 1 if self.facingRight else -1
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_step_distance(self):
|
def get_step_distance(self):
|
||||||
"""Get step distance based on current speed"""
|
"""Get step distance based on current speed"""
|
||||||
|
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
|
||||||
|
totalMultiplier = weaponBonus
|
||||||
|
|
||||||
if self.isRunning or self.isJumping:
|
if self.isRunning or self.isJumping:
|
||||||
return self.baseStepDistance / self.runMultiplier
|
totalMultiplier *= self.runMultiplier
|
||||||
return self.baseStepDistance
|
|
||||||
|
return self.baseStepDistance / totalMultiplier
|
||||||
|
|
||||||
def get_step_interval(self):
|
def get_step_interval(self):
|
||||||
"""Get minimum time between steps based on current speed"""
|
"""Get minimum time between steps based on current speed"""
|
||||||
|
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
|
||||||
|
totalMultiplier = weaponBonus
|
||||||
|
|
||||||
if self.isRunning or self.isJumping:
|
if self.isRunning or self.isJumping:
|
||||||
return self.baseStepInterval / self.runMultiplier
|
totalMultiplier *= self.runMultiplier
|
||||||
return self.baseStepInterval
|
|
||||||
|
return self.baseStepInterval / totalMultiplier
|
||||||
|
|
||||||
def get_health(self):
|
def get_health(self):
|
||||||
"""Get current health"""
|
"""Get current health"""
|
||||||
@@ -181,10 +197,18 @@ class Player:
|
|||||||
self._health = self._maxHealth
|
self._health = self._maxHealth
|
||||||
|
|
||||||
def get_current_speed(self):
|
def get_current_speed(self):
|
||||||
"""Calculate current speed based on state"""
|
"""Calculate current speed based on state and weapon"""
|
||||||
baseSpeed = self.moveSpeed
|
baseSpeed = self.moveSpeed
|
||||||
if self.isJumping or self.isRunning: return baseSpeed * self.runMultiplier
|
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
|
||||||
return baseSpeed
|
|
||||||
|
if self.isJumping or self.isRunning:
|
||||||
|
return baseSpeed * self.runMultiplier * weaponBonus
|
||||||
|
return baseSpeed * weaponBonus
|
||||||
|
|
||||||
|
def get_current_jump_duration(self):
|
||||||
|
"""Calculate current jump duration based on weapon bonus"""
|
||||||
|
weaponBonus = self.currentWeapon.jumpDurationBonus if self.currentWeapon else 1.0
|
||||||
|
return int(self.jumpDuration * weaponBonus)
|
||||||
|
|
||||||
def set_footstep_sound(self, soundName):
|
def set_footstep_sound(self, soundName):
|
||||||
"""Set the current footstep sound"""
|
"""Set the current footstep sound"""
|
||||||
@@ -196,20 +220,22 @@ class Player:
|
|||||||
|
|
||||||
# Oops, allow healing while invincible.
|
# Oops, allow healing while invincible.
|
||||||
if self.isInvincible and value < old_health:
|
if self.isInvincible and value < old_health:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._health = max(0, value) # Health can't go below 0
|
self._health = max(0, value) # Health can't go below 0
|
||||||
|
|
||||||
if self._health == 0 and old_health > 0:
|
if self._health == 0 and old_health > 0:
|
||||||
self._lives -= 1
|
self._lives -= 1
|
||||||
|
# Mark that player died this frame to prevent revival
|
||||||
|
self.diedThisFrame = True
|
||||||
# Stop all current sounds before playing death sound
|
# Stop all current sounds before playing death sound
|
||||||
pygame.mixer.stop()
|
pygame.mixer.stop()
|
||||||
try:
|
try:
|
||||||
pygame.mixer.music.stop()
|
pygame.mixer.music.stop()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
cut_scene(self.sounds, 'lose_a_life')
|
cut_scene(self.sounds, "lose_a_life")
|
||||||
|
|
||||||
def set_max_health(self, value):
|
def set_max_health(self, value):
|
||||||
"""Set max health"""
|
"""Set max health"""
|
||||||
@@ -253,6 +279,24 @@ class Player:
|
|||||||
if weapon in self.weapons:
|
if weapon in self.weapons:
|
||||||
self.currentWeapon = weapon
|
self.currentWeapon = weapon
|
||||||
|
|
||||||
|
def switch_to_weapon(self, weaponIndex):
|
||||||
|
"""Switch to weapon by index (1=shovel, 2=broom, 3=nunchucks)"""
|
||||||
|
weaponMap = {1: "rusty_shovel", 2: "witch_broom", 3: "nunchucks"}
|
||||||
|
|
||||||
|
targetWeaponName = weaponMap.get(weaponIndex)
|
||||||
|
if not targetWeaponName:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Find the weapon in player's inventory
|
||||||
|
for weapon in self.weapons:
|
||||||
|
if weapon.name == targetWeaponName:
|
||||||
|
self.equip_weapon(weapon)
|
||||||
|
speak(weapon.name.replace("_", " "))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Weapon not found in inventory
|
||||||
|
return False
|
||||||
|
|
||||||
def add_item(self, item):
|
def add_item(self, item):
|
||||||
"""Add an item to inventory"""
|
"""Add an item to inventory"""
|
||||||
self.inventory.append(item)
|
self.inventory.append(item)
|
||||||
|
|||||||
+49
-28
@@ -5,14 +5,10 @@ from libstormgames import *
|
|||||||
from src.object import Object
|
from src.object import Object
|
||||||
from src.weapon import Weapon
|
from src.weapon import Weapon
|
||||||
|
|
||||||
|
|
||||||
class PowerUp(Object):
|
class PowerUp(Object):
|
||||||
def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100):
|
def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100):
|
||||||
super().__init__(
|
super().__init__(x, y, item_type, isStatic=False, isCollectible=True, isHazard=False)
|
||||||
x, y, item_type,
|
|
||||||
isStatic=False,
|
|
||||||
isCollectible=True,
|
|
||||||
isHazard=False
|
|
||||||
)
|
|
||||||
self.sounds = sounds
|
self.sounds = sounds
|
||||||
self.direction = direction
|
self.direction = direction
|
||||||
self.speed = 0.049 # Base movement speed
|
self.speed = 0.049 # Base movement speed
|
||||||
@@ -56,35 +52,58 @@ class PowerUp(Object):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def apply_effect(self, player):
|
def apply_effect(self, player, level=None):
|
||||||
"""Apply the item's effect when collected"""
|
"""Apply the item's effect when collected"""
|
||||||
if self.item_type == 'hand_of_glory':
|
if self.item_type == "hand_of_glory":
|
||||||
player.start_invincibility()
|
player.start_invincibility()
|
||||||
elif self.item_type == 'cauldron':
|
elif self.item_type == "cauldron":
|
||||||
player.restore_health()
|
player.restore_health()
|
||||||
elif self.item_type == 'guts':
|
elif self.item_type == "guts":
|
||||||
player.add_guts()
|
player.add_guts()
|
||||||
player.collectedItems.append('guts')
|
player.collectedItems.append("guts")
|
||||||
self.check_for_nunchucks(player)
|
self.check_for_nunchucks(player)
|
||||||
elif self.item_type == 'jack_o_lantern':
|
elif self.item_type == "jack_o_lantern":
|
||||||
player.add_jack_o_lantern()
|
player.add_jack_o_lantern()
|
||||||
elif self.item_type == 'extra_life':
|
elif self.item_type == "extra_life":
|
||||||
player.extra_life()
|
# Don't give extra lives in survival mode
|
||||||
elif self.item_type == 'shin_bone': # Add shin bone handling
|
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.shinBoneCount += 1
|
||||||
player._coins += 5
|
player._coins += 5
|
||||||
player.add_save_bone_dust(5) # Add to save bone dust counter too
|
player.add_save_bone_dust(5) # Add to save bone dust counter too
|
||||||
if player.get_health() < player.get_max_health():
|
if player.get_health() < player.get_max_health():
|
||||||
player.set_health(min(
|
player.set_health(min(player.get_health() + 1, player.get_max_health()))
|
||||||
player.get_health() + 1,
|
|
||||||
player.get_max_health()
|
# Check for 100 coin bonus after adding shin bone coins
|
||||||
))
|
if player._coins >= 100:
|
||||||
|
# Only give extra lives in story mode, not survival mode (level_id 999)
|
||||||
|
if level.levelId != 999:
|
||||||
|
# Extra life
|
||||||
|
player._coins = 0
|
||||||
|
player._lives += 1
|
||||||
|
level.levelScore += 1000
|
||||||
|
play_sound(self.sounds["get_extra_life"])
|
||||||
|
else:
|
||||||
|
# In survival mode, reset coin counter but give bonus score instead
|
||||||
|
player._coins = 0
|
||||||
|
level.levelScore += 2000 # Double score bonus instead of extra life
|
||||||
|
speak("100 bone dust collected! Bonus score!")
|
||||||
|
play_sound(
|
||||||
|
self.sounds.get("survivor_bonus", "bone_dust")
|
||||||
|
) # Use survivor_bonus sound if available, fallback to bone_dust
|
||||||
|
|
||||||
self.check_for_nunchucks(player)
|
self.check_for_nunchucks(player)
|
||||||
elif self.item_type == 'witch_broom':
|
elif self.item_type == "witch_broom":
|
||||||
broomWeapon = Weapon.create_witch_broom()
|
broomWeapon = Weapon.create_witch_broom()
|
||||||
player.add_weapon(broomWeapon)
|
player.add_weapon(broomWeapon)
|
||||||
player.equip_weapon(broomWeapon)
|
player.equip_weapon(broomWeapon)
|
||||||
elif self.item_type == 'spiderweb':
|
elif self.item_type == "spiderweb":
|
||||||
# Bounce player back (happens even if invincible)
|
# Bounce player back (happens even if invincible)
|
||||||
player.xPos -= 3 if player.xPos > self.xPos else -3
|
player.xPos -= 3 if player.xPos > self.xPos else -3
|
||||||
|
|
||||||
@@ -98,7 +117,7 @@ class PowerUp(Object):
|
|||||||
player.webPenaltyEndTime = pygame.time.get_ticks() + 15000
|
player.webPenaltyEndTime = pygame.time.get_ticks() + 15000
|
||||||
|
|
||||||
# Tell level to spawn a spider
|
# Tell level to spawn a spider
|
||||||
if hasattr(self, 'level'):
|
if hasattr(self, "level"):
|
||||||
self.level.spawn_spider(self.xPos, self.yPos)
|
self.level.spawn_spider(self.xPos, self.yPos)
|
||||||
|
|
||||||
# Stop movement sound when collected
|
# Stop movement sound when collected
|
||||||
@@ -107,18 +126,20 @@ class PowerUp(Object):
|
|||||||
self.channel = None
|
self.channel = None
|
||||||
|
|
||||||
# Item tracking
|
# Item tracking
|
||||||
player.stats.update_stat('Items collected', 1)
|
player.stats.update_stat("Items collected", 1)
|
||||||
|
|
||||||
def check_for_nunchucks(self, player):
|
def check_for_nunchucks(self, player):
|
||||||
"""Check if player has materials for nunchucks and create if conditions are met"""
|
"""Check if player has materials for nunchucks and create if conditions are met"""
|
||||||
if (player.shinBoneCount >= 2 and
|
if (
|
||||||
'guts' in player.collectedItems and
|
player.shinBoneCount >= 2
|
||||||
not any(weapon.name == "nunchucks" for weapon in player.weapons)):
|
and "guts" in player.collectedItems
|
||||||
|
and not any(weapon.name == "nunchucks" for weapon in player.weapons)
|
||||||
|
):
|
||||||
nunchucksWeapon = Weapon.create_nunchucks()
|
nunchucksWeapon = Weapon.create_nunchucks()
|
||||||
player.add_weapon(nunchucksWeapon)
|
player.add_weapon(nunchucksWeapon)
|
||||||
player.equip_weapon(nunchucksWeapon)
|
player.equip_weapon(nunchucksWeapon)
|
||||||
basePoints = nunchucksWeapon.damage * 1000
|
basePoints = nunchucksWeapon.damage * 1000
|
||||||
rangeModifier = nunchucksWeapon.range * 500
|
rangeModifier = nunchucksWeapon.range * 500
|
||||||
player.scoreboard.increase_score(basePoints + rangeModifier)
|
player.scoreboard.increase_score(basePoints + rangeModifier)
|
||||||
play_sound(self.sounds['get_nunchucks'])
|
play_sound(self.sounds["get_nunchucks"])
|
||||||
player.stats.update_stat('Items collected', 1)
|
player.stats.update_stat("Items collected", 1)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
class Projectile:
|
class Projectile:
|
||||||
def __init__(self, projectile_type, start_x, direction):
|
def __init__(self, projectile_type, start_x, direction):
|
||||||
self.type = projectile_type
|
self.type = projectile_type
|
||||||
|
|||||||
+135
-116
@@ -11,8 +11,8 @@ class SaveManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize save manager with XDG-compliant save directory"""
|
"""Initialize save manager with XDG-compliant save directory"""
|
||||||
# Use XDG_CONFIG_HOME or default to ~/.config
|
# Use XDG_CONFIG_HOME or default to ~/.config
|
||||||
config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
|
config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
||||||
self.save_dir = Path(config_home) / 'storm-games' / 'wicked-quest'
|
self.save_dir = Path(config_home) / "storm-games" / "wicked-quest"
|
||||||
self.save_dir.mkdir(parents=True, exist_ok=True)
|
self.save_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.max_saves = 10
|
self.max_saves = 10
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class SaveManager:
|
|||||||
# Validate required parameters
|
# Validate required parameters
|
||||||
if current_game is None:
|
if current_game is None:
|
||||||
return False, "No game selected to save"
|
return False, "No game selected to save"
|
||||||
|
|
||||||
if current_level is None:
|
if current_level is None:
|
||||||
return False, "No current level to save"
|
return False, "No current level to save"
|
||||||
|
|
||||||
@@ -34,30 +34,30 @@ class SaveManager:
|
|||||||
|
|
||||||
# Create save data
|
# Create save data
|
||||||
save_data = {
|
save_data = {
|
||||||
'player_state': {
|
"player_state": {
|
||||||
'xPos': player.xPos,
|
"xPos": player.xPos,
|
||||||
'yPos': player.yPos,
|
"yPos": player.yPos,
|
||||||
'health': player._health,
|
"health": player._health,
|
||||||
'maxHealth': player._maxHealth,
|
"maxHealth": player._maxHealth,
|
||||||
'lives': player._lives,
|
"lives": player._lives,
|
||||||
'coins': player._coins,
|
"coins": player._coins,
|
||||||
'saveBoneDust': player._saveBoneDust,
|
"saveBoneDust": player._saveBoneDust,
|
||||||
'jackOLanternCount': player._jack_o_lantern_count,
|
"jackOLanternCount": player._jack_o_lantern_count,
|
||||||
'shinBoneCount': player.shinBoneCount,
|
"shinBoneCount": player.shinBoneCount,
|
||||||
'inventory': player.inventory,
|
"inventory": player.inventory,
|
||||||
'collectedItems': player.collectedItems,
|
"collectedItems": player.collectedItems,
|
||||||
'weapons': self._serialize_weapons(player.weapons),
|
"weapons": self._serialize_weapons(player.weapons),
|
||||||
'currentWeaponName': player.currentWeapon.name if player.currentWeapon else None,
|
"currentWeaponName": player.currentWeapon.name if player.currentWeapon else None,
|
||||||
'stats': self._serialize_stats(player.stats),
|
"stats": self._serialize_stats(player.stats),
|
||||||
'scoreboard': self._serialize_scoreboard(player.scoreboard)
|
"scoreboard": self._serialize_scoreboard(player.scoreboard),
|
||||||
},
|
},
|
||||||
'game_state': {
|
"game_state": {
|
||||||
'currentLevel': current_level,
|
"currentLevel": current_level,
|
||||||
'currentGame': current_game,
|
"currentGame": current_game,
|
||||||
'gameStartTime': game_start_time,
|
"gameStartTime": game_start_time,
|
||||||
'saveTime': datetime.now()
|
"saveTime": datetime.now(),
|
||||||
},
|
},
|
||||||
'version': '1.0'
|
"version": "1.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate filename with timestamp
|
# Generate filename with timestamp
|
||||||
@@ -67,26 +67,26 @@ class SaveManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Write to temporary file first, then rename for atomic operation
|
# Write to temporary file first, then rename for atomic operation
|
||||||
temp_filepath = filepath.with_suffix('.tmp')
|
temp_filepath = filepath.with_suffix(".tmp")
|
||||||
|
|
||||||
with open(temp_filepath, 'wb') as f:
|
with open(temp_filepath, "wb") as f:
|
||||||
pickle.dump(save_data, f)
|
pickle.dump(save_data, f)
|
||||||
f.flush() # Ensure data is written to disk
|
f.flush() # Ensure data is written to disk
|
||||||
os.fsync(f.fileno()) # Force write to disk
|
os.fsync(f.fileno()) # Force write to disk
|
||||||
|
|
||||||
# Atomic rename (replaces old file if it exists)
|
# Atomic rename (replaces old file if it exists)
|
||||||
temp_filepath.rename(filepath)
|
temp_filepath.rename(filepath)
|
||||||
|
|
||||||
# Clean up old saves if we exceed max_saves
|
# Clean up old saves if we exceed max_saves
|
||||||
self._cleanup_old_saves()
|
self._cleanup_old_saves()
|
||||||
|
|
||||||
return True, f"Game saved to {filename}"
|
return True, f"Game saved to {filename}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Clean up temp file if it exists
|
# Clean up temp file if it exists
|
||||||
if temp_filepath.exists():
|
if temp_filepath.exists():
|
||||||
try:
|
try:
|
||||||
temp_filepath.unlink()
|
temp_filepath.unlink()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return False, f"Failed to save game: {str(e)}"
|
return False, f"Failed to save game: {str(e)}"
|
||||||
|
|
||||||
@@ -94,119 +94,136 @@ class SaveManager:
|
|||||||
"""Serialize weapons for saving"""
|
"""Serialize weapons for saving"""
|
||||||
serialized = []
|
serialized = []
|
||||||
for weapon in weapons:
|
for weapon in weapons:
|
||||||
serialized.append({
|
serialized.append(
|
||||||
'name': weapon.name,
|
{
|
||||||
'damage': weapon.damage,
|
"name": weapon.name,
|
||||||
'range': weapon.range,
|
"damage": weapon.damage,
|
||||||
'attackSound': weapon.attackSound,
|
"range": weapon.range,
|
||||||
'hitSound': weapon.hitSound,
|
"attackSound": weapon.attackSound,
|
||||||
'attackDuration': weapon.attackDuration
|
"hitSound": weapon.hitSound,
|
||||||
})
|
"attackDuration": weapon.attackDuration,
|
||||||
|
"speedBonus": getattr(weapon, "speedBonus", 1.0),
|
||||||
|
"jumpDurationBonus": getattr(weapon, "jumpDurationBonus", 1.0),
|
||||||
|
}
|
||||||
|
)
|
||||||
return serialized
|
return serialized
|
||||||
|
|
||||||
def _deserialize_weapons(self, weapon_data):
|
def _deserialize_weapons(self, weapon_data):
|
||||||
"""Deserialize weapons from save data"""
|
"""Deserialize weapons from save data"""
|
||||||
from src.weapon import Weapon
|
from src.weapon import Weapon
|
||||||
|
|
||||||
weapons = []
|
weapons = []
|
||||||
for data in weapon_data:
|
for data in weapon_data:
|
||||||
|
# Handle backward compatibility for old saves
|
||||||
|
speedBonus = data.get("speedBonus", 1.0)
|
||||||
|
jumpDurationBonus = data.get("jumpDurationBonus", 1.0)
|
||||||
|
|
||||||
|
# For old saves, restore proper bonuses for specific weapons
|
||||||
|
if data["name"] == "witch_broom" and speedBonus == 1.0:
|
||||||
|
speedBonus = 1.17
|
||||||
|
jumpDurationBonus = 1.25
|
||||||
|
|
||||||
weapon = Weapon(
|
weapon = Weapon(
|
||||||
name=data['name'],
|
name=data["name"],
|
||||||
damage=data['damage'],
|
damage=data["damage"],
|
||||||
range=data['range'],
|
range=data["range"],
|
||||||
attackSound=data['attackSound'],
|
attackSound=data["attackSound"],
|
||||||
hitSound=data['hitSound'],
|
hitSound=data["hitSound"],
|
||||||
attackDuration=data['attackDuration']
|
attackDuration=data["attackDuration"],
|
||||||
|
speedBonus=speedBonus,
|
||||||
|
jumpDurationBonus=jumpDurationBonus,
|
||||||
)
|
)
|
||||||
weapons.append(weapon)
|
weapons.append(weapon)
|
||||||
return weapons
|
return weapons
|
||||||
|
|
||||||
def _serialize_stats(self, stats):
|
def _serialize_stats(self, stats):
|
||||||
"""Serialize stats for saving"""
|
"""Serialize stats for saving"""
|
||||||
return {
|
return {"total": stats.total.copy(), "level": stats.level.copy()}
|
||||||
'total': stats.total.copy(),
|
|
||||||
'level': stats.level.copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
def _deserialize_stats(self, stats_data):
|
def _deserialize_stats(self, stats_data):
|
||||||
"""Deserialize stats from save data"""
|
"""Deserialize stats from save data"""
|
||||||
from src.stat_tracker import StatTracker
|
from src.stat_tracker import StatTracker
|
||||||
|
|
||||||
stats = StatTracker()
|
stats = StatTracker()
|
||||||
if 'total' in stats_data:
|
if "total" in stats_data:
|
||||||
stats.total.update(stats_data['total'])
|
stats.total.update(stats_data["total"])
|
||||||
if 'level' in stats_data:
|
if "level" in stats_data:
|
||||||
stats.level.update(stats_data['level'])
|
stats.level.update(stats_data["level"])
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def _serialize_scoreboard(self, scoreboard):
|
def _serialize_scoreboard(self, scoreboard):
|
||||||
"""Serialize scoreboard for saving"""
|
"""Serialize scoreboard for saving"""
|
||||||
return {
|
return {
|
||||||
'currentScore': getattr(scoreboard, 'currentScore', 0),
|
"currentScore": getattr(scoreboard, "currentScore", 0),
|
||||||
'highScores': getattr(scoreboard, 'highScores', [])
|
"highScores": getattr(scoreboard, "highScores", []),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _deserialize_scoreboard(self, scoreboard_data):
|
def _deserialize_scoreboard(self, scoreboard_data):
|
||||||
"""Deserialize scoreboard from save data"""
|
"""Deserialize scoreboard from save data"""
|
||||||
from libstormgames import Scoreboard
|
from libstormgames import Scoreboard
|
||||||
|
|
||||||
scoreboard = Scoreboard()
|
scoreboard = Scoreboard()
|
||||||
if 'currentScore' in scoreboard_data:
|
if "currentScore" in scoreboard_data:
|
||||||
scoreboard.currentScore = scoreboard_data['currentScore']
|
scoreboard.currentScore = scoreboard_data["currentScore"]
|
||||||
if 'highScores' in scoreboard_data:
|
if "highScores" in scoreboard_data:
|
||||||
scoreboard.highScores = scoreboard_data['highScores']
|
scoreboard.highScores = scoreboard_data["highScores"]
|
||||||
return scoreboard
|
return scoreboard
|
||||||
|
|
||||||
def get_save_files(self):
|
def get_save_files(self):
|
||||||
"""Get list of save files with metadata"""
|
"""Get list of save files with metadata"""
|
||||||
save_files = []
|
save_files = []
|
||||||
pattern = str(self.save_dir / "save_*.pickle")
|
pattern = str(self.save_dir / "save_*.pickle")
|
||||||
|
|
||||||
for filepath in glob.glob(pattern):
|
for filepath in glob.glob(pattern):
|
||||||
try:
|
try:
|
||||||
with open(filepath, 'rb') as f:
|
with open(filepath, "rb") as f:
|
||||||
save_data = pickle.load(f)
|
save_data = pickle.load(f)
|
||||||
|
|
||||||
# Validate save data structure
|
# Validate save data structure
|
||||||
if not self._validate_save_data(save_data):
|
if not self._validate_save_data(save_data):
|
||||||
print(f"Invalid save file structure: {filepath}")
|
print(f"Invalid save file structure: {filepath}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract save info
|
# Extract save info
|
||||||
save_time = save_data['game_state']['saveTime']
|
save_time = save_data["game_state"]["saveTime"]
|
||||||
level = save_data['game_state']['currentLevel']
|
level = save_data["game_state"]["currentLevel"]
|
||||||
game_name = save_data['game_state']['currentGame']
|
game_name = save_data["game_state"]["currentGame"]
|
||||||
|
|
||||||
# Format display name
|
# Format display name
|
||||||
formatted_time = save_time.strftime("%B %d %I:%M%p")
|
formatted_time = save_time.strftime("%B %d %I:%M%p")
|
||||||
display_name = f"{formatted_time} {game_name} Level {level}"
|
display_name = f"{formatted_time} {game_name} Level {level}"
|
||||||
|
|
||||||
save_files.append({
|
save_files.append(
|
||||||
'filepath': filepath,
|
{
|
||||||
'display_name': display_name,
|
"filepath": filepath,
|
||||||
'save_time': save_time,
|
"display_name": display_name,
|
||||||
'level': level,
|
"save_time": save_time,
|
||||||
'game_name': game_name,
|
"level": level,
|
||||||
'save_data': save_data
|
"game_name": game_name,
|
||||||
})
|
"save_data": save_data,
|
||||||
|
}
|
||||||
|
)
|
||||||
except (pickle.PickleError, EOFError, OSError) as e:
|
except (pickle.PickleError, EOFError, OSError) as e:
|
||||||
print(f"Corrupted save file {filepath}: {e}")
|
print(f"Corrupted save file {filepath}: {e}")
|
||||||
# Try to remove corrupted save file
|
# Try to remove corrupted save file
|
||||||
try:
|
try:
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
print(f"Removed corrupted save file: {filepath}")
|
print(f"Removed corrupted save file: {filepath}")
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error reading save file {filepath}: {e}")
|
print(f"Error reading save file {filepath}: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Sort by save time (newest first)
|
# Sort by save time (newest first)
|
||||||
save_files.sort(key=lambda x: x['save_time'], reverse=True)
|
save_files.sort(key=lambda x: x["save_time"], reverse=True)
|
||||||
return save_files
|
return save_files
|
||||||
|
|
||||||
def load_save(self, filepath):
|
def load_save(self, filepath):
|
||||||
"""Load game state from save file"""
|
"""Load game state from save file"""
|
||||||
try:
|
try:
|
||||||
with open(filepath, 'rb') as f:
|
with open(filepath, "rb") as f:
|
||||||
save_data = pickle.load(f)
|
save_data = pickle.load(f)
|
||||||
return True, save_data
|
return True, save_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -214,55 +231,57 @@ class SaveManager:
|
|||||||
|
|
||||||
def restore_player_state(self, player, save_data):
|
def restore_player_state(self, player, save_data):
|
||||||
"""Restore player state from save data"""
|
"""Restore player state from save data"""
|
||||||
player_state = save_data['player_state']
|
player_state = save_data["player_state"]
|
||||||
|
|
||||||
# Restore basic attributes
|
# Restore basic attributes
|
||||||
player.xPos = player_state['xPos']
|
player.xPos = player_state["xPos"]
|
||||||
player.yPos = player_state['yPos']
|
player.yPos = player_state["yPos"]
|
||||||
player._health = player_state['health']
|
player._health = player_state["health"]
|
||||||
player._maxHealth = player_state['maxHealth']
|
player._maxHealth = player_state["maxHealth"]
|
||||||
player._lives = player_state['lives']
|
player._lives = player_state["lives"]
|
||||||
player._coins = player_state['coins']
|
player._coins = player_state["coins"]
|
||||||
player._saveBoneDust = player_state['saveBoneDust']
|
player._saveBoneDust = player_state["saveBoneDust"]
|
||||||
player._jack_o_lantern_count = player_state['jackOLanternCount']
|
player._jack_o_lantern_count = player_state["jackOLanternCount"]
|
||||||
player.shinBoneCount = player_state['shinBoneCount']
|
player.shinBoneCount = player_state["shinBoneCount"]
|
||||||
player.inventory = player_state['inventory']
|
player.inventory = player_state["inventory"]
|
||||||
player.collectedItems = player_state['collectedItems']
|
player.collectedItems = player_state["collectedItems"]
|
||||||
|
|
||||||
# Restore weapons
|
# Restore weapons
|
||||||
player.weapons = self._deserialize_weapons(player_state['weapons'])
|
player.weapons = self._deserialize_weapons(player_state["weapons"])
|
||||||
|
|
||||||
# Restore current weapon
|
# Restore current weapon
|
||||||
current_weapon_name = player_state.get('currentWeaponName')
|
current_weapon_name = player_state.get("currentWeaponName")
|
||||||
if current_weapon_name:
|
if current_weapon_name:
|
||||||
for weapon in player.weapons:
|
for weapon in player.weapons:
|
||||||
if weapon.name == current_weapon_name:
|
if weapon.name == current_weapon_name:
|
||||||
player.currentWeapon = weapon
|
player.currentWeapon = weapon
|
||||||
break
|
break
|
||||||
|
|
||||||
# Restore stats
|
# Restore stats
|
||||||
if 'stats' in player_state:
|
if "stats" in player_state:
|
||||||
player.stats = self._deserialize_stats(player_state['stats'])
|
player.stats = self._deserialize_stats(player_state["stats"])
|
||||||
else:
|
else:
|
||||||
from src.stat_tracker import StatTracker
|
from src.stat_tracker import StatTracker
|
||||||
|
|
||||||
player.stats = StatTracker()
|
player.stats = StatTracker()
|
||||||
|
|
||||||
# Restore scoreboard
|
# Restore scoreboard
|
||||||
if 'scoreboard' in player_state:
|
if "scoreboard" in player_state:
|
||||||
player.scoreboard = self._deserialize_scoreboard(player_state['scoreboard'])
|
player.scoreboard = self._deserialize_scoreboard(player_state["scoreboard"])
|
||||||
else:
|
else:
|
||||||
from libstormgames import Scoreboard
|
from libstormgames import Scoreboard
|
||||||
|
|
||||||
player.scoreboard = Scoreboard()
|
player.scoreboard = Scoreboard()
|
||||||
|
|
||||||
def _cleanup_old_saves(self):
|
def _cleanup_old_saves(self):
|
||||||
"""Remove old save files if we exceed max_saves"""
|
"""Remove old save files if we exceed max_saves"""
|
||||||
save_files = self.get_save_files()
|
save_files = self.get_save_files()
|
||||||
|
|
||||||
if len(save_files) > self.max_saves:
|
if len(save_files) > self.max_saves:
|
||||||
# Remove oldest saves
|
# Remove oldest saves
|
||||||
for save_file in save_files[self.max_saves:]:
|
for save_file in save_files[self.max_saves:]:
|
||||||
try:
|
try:
|
||||||
os.remove(save_file['filepath'])
|
os.remove(save_file["filepath"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error removing old save {save_file['filepath']}: {e}")
|
print(f"Error removing old save {save_file['filepath']}: {e}")
|
||||||
|
|
||||||
@@ -270,24 +289,24 @@ class SaveManager:
|
|||||||
"""Validate that save data has required structure"""
|
"""Validate that save data has required structure"""
|
||||||
try:
|
try:
|
||||||
# Check for required top-level keys
|
# Check for required top-level keys
|
||||||
required_keys = ['player_state', 'game_state', 'version']
|
required_keys = ["player_state", "game_state", "version"]
|
||||||
if not all(key in save_data for key in required_keys):
|
if not all(key in save_data for key in required_keys):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check player_state structure
|
# Check player_state structure
|
||||||
player_required = ['xPos', 'yPos', 'health', 'maxHealth', 'lives', 'coins', 'saveBoneDust']
|
player_required = ["xPos", "yPos", "health", "maxHealth", "lives", "coins", "saveBoneDust"]
|
||||||
if not all(key in save_data['player_state'] for key in player_required):
|
if not all(key in save_data["player_state"] for key in player_required):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check game_state structure
|
# Check game_state structure
|
||||||
game_required = ['currentLevel', 'currentGame', 'gameStartTime', 'saveTime']
|
game_required = ["currentLevel", "currentGame", "gameStartTime", "saveTime"]
|
||||||
if not all(key in save_data['game_state'] for key in game_required):
|
if not all(key in save_data["game_state"] for key in game_required):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_saves(self):
|
def has_saves(self):
|
||||||
"""Check if any save files exist"""
|
"""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 libstormgames import *
|
||||||
from src.object import Object
|
from src.object import Object
|
||||||
|
|
||||||
|
|
||||||
class SkullStorm(Object):
|
class SkullStorm(Object):
|
||||||
"""Handles falling skulls within a specified range."""
|
"""Handles falling skulls within a specified range."""
|
||||||
|
|
||||||
def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5):
|
def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
xRange,
|
xRange, y, "", isStatic=True, isCollectible=False, isHazard=False # No ambient sound for the skull storm
|
||||||
y,
|
|
||||||
"", # No ambient sound for the skull storm
|
|
||||||
isStatic=True,
|
|
||||||
isCollectible=False,
|
|
||||||
isHazard=False
|
|
||||||
)
|
)
|
||||||
self.sounds = sounds
|
self.sounds = sounds
|
||||||
self.damage = damage
|
self.damage = damage
|
||||||
@@ -37,7 +33,7 @@ class SkullStorm(Object):
|
|||||||
inRange = self.xRange[0] <= player.xPos <= self.xRange[1]
|
inRange = self.xRange[0] <= player.xPos <= self.xRange[1]
|
||||||
if inRange and not self.playerInRange:
|
if inRange and not self.playerInRange:
|
||||||
# Player just entered range - play the warning sound
|
# Player just entered range - play the warning sound
|
||||||
play_sound(self.sounds['skull_storm'])
|
play_sound(self.sounds["skull_storm"])
|
||||||
self.playerInRange = True
|
self.playerInRange = True
|
||||||
elif not inRange and self.playerInRange: # Only speak when actually leaving range
|
elif not inRange and self.playerInRange: # Only speak when actually leaving range
|
||||||
# Player just left range
|
# Player just left range
|
||||||
@@ -46,8 +42,8 @@ class SkullStorm(Object):
|
|||||||
|
|
||||||
# Clear any active skulls when player leaves the range
|
# Clear any active skulls when player leaves the range
|
||||||
for skull in self.activeSkulls[:]:
|
for skull in self.activeSkulls[:]:
|
||||||
if skull['channel']:
|
if skull["channel"]:
|
||||||
obj_stop(skull['channel'])
|
obj_stop(skull["channel"])
|
||||||
self.activeSkulls = [] # Reset the list of active skulls
|
self.activeSkulls = [] # Reset the list of active skulls
|
||||||
|
|
||||||
if not inRange:
|
if not inRange:
|
||||||
@@ -55,29 +51,28 @@ class SkullStorm(Object):
|
|||||||
|
|
||||||
# Update existing skulls
|
# Update existing skulls
|
||||||
for skull in self.activeSkulls[:]: # Copy list to allow removal
|
for skull in self.activeSkulls[:]: # Copy list to allow removal
|
||||||
if currentTime >= skull['land_time']:
|
if currentTime >= skull["land_time"]:
|
||||||
# Skull has landed
|
# Skull has landed
|
||||||
self.handle_landing(skull, player)
|
self.handle_landing(skull, player)
|
||||||
self.activeSkulls.remove(skull)
|
self.activeSkulls.remove(skull)
|
||||||
else:
|
else:
|
||||||
# Update falling sound
|
# Update falling sound
|
||||||
timeElapsed = currentTime - skull['start_time']
|
timeElapsed = currentTime - skull["start_time"]
|
||||||
fallProgress = timeElapsed / skull['fall_duration']
|
fallProgress = timeElapsed / skull["fall_duration"]
|
||||||
currentY = self.yPos * (1 - fallProgress)
|
currentY = self.yPos * (1 - fallProgress)
|
||||||
|
|
||||||
skull['channel'] = play_random_falling(
|
skull["channel"] = play_random_falling(
|
||||||
self.sounds,
|
self.sounds,
|
||||||
'falling_skull',
|
"falling_skull",
|
||||||
player.xPos,
|
player.xPos,
|
||||||
skull['x'],
|
skull["x"],
|
||||||
self.yPos,
|
self.yPos,
|
||||||
currentY,
|
currentY,
|
||||||
existingChannel=skull['channel']
|
existingChannel=skull["channel"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if we should spawn a new skull
|
# Check if we should spawn a new skull
|
||||||
if (len(self.activeSkulls) < self.maxSkulls and
|
if len(self.activeSkulls) < self.maxSkulls and currentTime - self.lastSkullTime >= self.nextSkullDelay:
|
||||||
currentTime - self.lastSkullTime >= self.nextSkullDelay):
|
|
||||||
self.spawn_skull(currentTime)
|
self.spawn_skull(currentTime)
|
||||||
|
|
||||||
def spawn_skull(self, currentTime):
|
def spawn_skull(self, currentTime):
|
||||||
@@ -91,11 +86,11 @@ class SkullStorm(Object):
|
|||||||
|
|
||||||
# Create new skull
|
# Create new skull
|
||||||
skull = {
|
skull = {
|
||||||
'x': random.uniform(self.xRange[0], self.xRange[1]),
|
"x": random.uniform(self.xRange[0], self.xRange[1]),
|
||||||
'start_time': currentTime,
|
"start_time": currentTime,
|
||||||
'fall_duration': fallDuration,
|
"fall_duration": fallDuration,
|
||||||
'land_time': currentTime + fallDuration,
|
"land_time": currentTime + fallDuration,
|
||||||
'channel': None
|
"channel": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.activeSkulls.append(skull)
|
self.activeSkulls.append(skull)
|
||||||
@@ -103,18 +98,18 @@ class SkullStorm(Object):
|
|||||||
def handle_landing(self, skull, player):
|
def handle_landing(self, skull, player):
|
||||||
"""Handle a skull landing."""
|
"""Handle a skull landing."""
|
||||||
# Stop falling sound
|
# Stop falling sound
|
||||||
if skull['channel']:
|
if skull["channel"]:
|
||||||
obj_stop(skull['channel'])
|
obj_stop(skull["channel"])
|
||||||
|
|
||||||
# Play landing sound with positional audio once
|
# Play landing sound with positional audio once
|
||||||
channel = pygame.mixer.find_channel(True) # Find an available channel
|
channel = pygame.mixer.find_channel(True) # Find an available channel
|
||||||
if channel:
|
if channel:
|
||||||
soundObj = self.sounds['skull_lands']
|
soundObj = self.sounds["skull_lands"]
|
||||||
obj_play(self.sounds, 'skull_lands', player.xPos, skull['x'], loop=False)
|
obj_play(self.sounds, "skull_lands", player.xPos, skull["x"], loop=False)
|
||||||
|
|
||||||
# Check if player was hit
|
# Check if player was hit
|
||||||
if abs(player.xPos - skull['x']) < 1: # Within 1 tile
|
if abs(player.xPos - skull["x"]) < 1: # Within 1 tile
|
||||||
if not player.isInvincible:
|
if not player.isInvincible:
|
||||||
player.set_health(player.get_health() - self.damage)
|
player.set_health(player.get_health() - self.damage)
|
||||||
self.sounds['player_takes_damage'].play()
|
self.sounds["player_takes_damage"].play()
|
||||||
speak("Hit by falling skull!")
|
speak("Hit by falling skull!")
|
||||||
|
|||||||
+3
-8
@@ -1,20 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
class StatTracker:
|
class StatTracker:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Base dictionary for tracking stats
|
# Base dictionary for tracking stats
|
||||||
self.total = {
|
self.total = {"Bone dust": 0, "Enemies killed": 0, "Coffins broken": 0, "Items collected": 0, "Total time": 0}
|
||||||
'Bone dust': 0,
|
|
||||||
'Enemies killed': 0,
|
|
||||||
'Coffins broken': 0,
|
|
||||||
'Items collected': 0,
|
|
||||||
'Total time': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create level stats from total (shallow copy is fine here)
|
# Create level stats from total (shallow copy is fine here)
|
||||||
self.level = self.total.copy()
|
self.level = self.total.copy()
|
||||||
|
|
||||||
self.total['levelsCompleted'] = 0
|
self.total["levelsCompleted"] = 0
|
||||||
|
|
||||||
def reset_level(self):
|
def reset_level(self):
|
||||||
"""Reset level stats based on variable type"""
|
"""Reset level stats based on variable type"""
|
||||||
|
|||||||
@@ -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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
class Weapon:
|
class Weapon:
|
||||||
def __init__(self, name, damage, range, attackSound, hitSound, cooldown=500, attackDuration=200):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name,
|
||||||
|
damage,
|
||||||
|
range,
|
||||||
|
attackSound,
|
||||||
|
hitSound,
|
||||||
|
cooldown=500,
|
||||||
|
attackDuration=200,
|
||||||
|
speedBonus=1.0,
|
||||||
|
jumpDurationBonus=1.0,
|
||||||
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.damage = damage
|
self.damage = damage
|
||||||
self.range = range # Range in tiles
|
self.range = range # Range in tiles
|
||||||
@@ -9,6 +21,8 @@ class Weapon:
|
|||||||
self.hitSound = hitSound
|
self.hitSound = hitSound
|
||||||
self.cooldown = cooldown # Milliseconds between attacks
|
self.cooldown = cooldown # Milliseconds between attacks
|
||||||
self.attackDuration = attackDuration # Milliseconds the attack is active
|
self.attackDuration = attackDuration # Milliseconds the attack is active
|
||||||
|
self.speedBonus = speedBonus # Speed multiplier when wielding this weapon
|
||||||
|
self.jumpDurationBonus = jumpDurationBonus # Jump duration multiplier when wielding this weapon
|
||||||
self.lastAttackTime = 0
|
self.lastAttackTime = 0
|
||||||
self.hitEnemies = set()
|
self.hitEnemies = set()
|
||||||
|
|
||||||
@@ -22,7 +36,7 @@ class Weapon:
|
|||||||
attackSound="player_nunchuck_attack",
|
attackSound="player_nunchuck_attack",
|
||||||
hitSound="player_nunchuck_hit",
|
hitSound="player_nunchuck_hit",
|
||||||
cooldown=250,
|
cooldown=250,
|
||||||
attackDuration=100
|
attackDuration=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -35,7 +49,9 @@ class Weapon:
|
|||||||
attackSound="player_broom_attack",
|
attackSound="player_broom_attack",
|
||||||
hitSound="player_broom_hit",
|
hitSound="player_broom_hit",
|
||||||
cooldown=500,
|
cooldown=500,
|
||||||
attackDuration=200
|
attackDuration=200,
|
||||||
|
speedBonus=1.17, # 17% speed bonus when wielding the broom
|
||||||
|
jumpDurationBonus=1.25, # 25% longer jump duration for better traversal
|
||||||
)
|
)
|
||||||
|
|
||||||
def can_attack(self, currentTime):
|
def can_attack(self, currentTime):
|
||||||
|
|||||||
+294
-65
@@ -5,25 +5,48 @@ import json
|
|||||||
import os
|
import os
|
||||||
import pygame
|
import pygame
|
||||||
from libstormgames import *
|
from libstormgames import *
|
||||||
|
from src.pack_sound_system import PackSoundSystem
|
||||||
from src.level import Level
|
from src.level import Level
|
||||||
from src.object import Object
|
from src.object import Object
|
||||||
from src.player import Player
|
from src.player import Player
|
||||||
from src.game_selection import select_game, get_level_path
|
from src.game_selection import select_game, get_level_path
|
||||||
from src.save_manager import SaveManager
|
from src.save_manager import SaveManager
|
||||||
|
from src.survival_generator import SurvivalGenerator
|
||||||
|
|
||||||
|
|
||||||
class WickedQuest:
|
class WickedQuest:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize game and load sounds."""
|
"""Initialize game and load sounds."""
|
||||||
self.sounds = initialize_gui("Wicked Quest")
|
self.sounds = initialize_gui("Wicked Quest")
|
||||||
|
self.soundSystem = None # Will be created when game is selected
|
||||||
self.currentLevel = None
|
self.currentLevel = None
|
||||||
self.gameStartTime = None
|
self.gameStartTime = None
|
||||||
self.lastThrowTime = 0
|
self.lastThrowTime = 0
|
||||||
self.throwDelay = 250
|
self.throwDelay = 250
|
||||||
|
self.lastWeaponSwitchTime = 0
|
||||||
|
self.weaponSwitchDelay = 200
|
||||||
self.player = None
|
self.player = None
|
||||||
self.currentGame = None
|
self.currentGame = None
|
||||||
self.runLock = False # Toggle behavior of the run keys
|
self.runLock = False # Toggle behavior of the run keys
|
||||||
self.saveManager = SaveManager()
|
self.saveManager = SaveManager()
|
||||||
|
self.survivalGenerator = None
|
||||||
|
self.lastBroomLandingTime = 0 # Timestamp to prevent ducking after broom landing
|
||||||
|
self.survivalWave = 1
|
||||||
|
self.survivalScore = 0
|
||||||
|
|
||||||
|
def initialize_pack_sounds(self):
|
||||||
|
"""Initialize pack-specific sound system after game selection."""
|
||||||
|
if self.currentGame:
|
||||||
|
self.soundSystem = PackSoundSystem(self.sounds, "sounds/", levelPackName=self.currentGame)
|
||||||
|
else:
|
||||||
|
self.soundSystem = PackSoundSystem(self.sounds, "sounds/")
|
||||||
|
|
||||||
|
def get_sounds(self):
|
||||||
|
"""Get the current sound system (pack-specific if available, otherwise original)."""
|
||||||
|
if self.soundSystem is None and self.currentGame is not None:
|
||||||
|
# Initialize pack sounds if not done yet
|
||||||
|
self.initialize_pack_sounds()
|
||||||
|
return self.soundSystem if self.soundSystem else self.sounds
|
||||||
|
|
||||||
def load_level(self, levelNumber):
|
def load_level(self, levelNumber):
|
||||||
"""Load a level from its JSON file."""
|
"""Load a level from its JSON file."""
|
||||||
@@ -36,7 +59,7 @@ class WickedQuest:
|
|||||||
if self.player is None:
|
if self.player is None:
|
||||||
self.player = Player(levelData["player_start"]["x"],
|
self.player = Player(levelData["player_start"]["x"],
|
||||||
levelData["player_start"]["y"],
|
levelData["player_start"]["y"],
|
||||||
self.sounds)
|
self.get_sounds())
|
||||||
else:
|
else:
|
||||||
# Reset player for new level.
|
# Reset player for new level.
|
||||||
self.player.isDucking = False
|
self.player.isDucking = False
|
||||||
@@ -56,7 +79,7 @@ class WickedQuest:
|
|||||||
|
|
||||||
# Pass existing player to new level
|
# Pass existing player to new level
|
||||||
pygame.event.clear()
|
pygame.event.clear()
|
||||||
self.currentLevel = Level(levelData, self.sounds, self.player)
|
self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -87,7 +110,7 @@ class WickedQuest:
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
def load_game_menu(self):
|
def load_game_menu(self):
|
||||||
"""Display load game menu with available saves"""
|
"""Display load game menu with available saves using instruction_menu"""
|
||||||
save_files = self.saveManager.get_save_files()
|
save_files = self.saveManager.get_save_files()
|
||||||
|
|
||||||
if not save_files:
|
if not save_files:
|
||||||
@@ -101,48 +124,24 @@ class WickedQuest:
|
|||||||
|
|
||||||
options.append("Cancel")
|
options.append("Cancel")
|
||||||
|
|
||||||
# Show menu
|
# Use instruction_menu for consistent behavior
|
||||||
currentIndex = 0
|
choice = instruction_menu(self.get_sounds(), "Select a save file to load:", *options)
|
||||||
lastSpoken = -1
|
|
||||||
|
|
||||||
messagebox("Select a save file to load:")
|
if choice == "Cancel" or choice is None:
|
||||||
|
return None
|
||||||
while True:
|
else:
|
||||||
if currentIndex != lastSpoken:
|
# Find the corresponding save file
|
||||||
speak(options[currentIndex])
|
for save_file in save_files:
|
||||||
lastSpoken = currentIndex
|
if save_file['display_name'] == choice:
|
||||||
|
return save_file
|
||||||
event = pygame.event.wait()
|
return None
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
return None
|
|
||||||
elif event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(options) - 1:
|
|
||||||
currentIndex += 1
|
|
||||||
try:
|
|
||||||
self.sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
elif event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
|
||||||
currentIndex -= 1
|
|
||||||
try:
|
|
||||||
self.sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
elif event.key == pygame.K_RETURN:
|
|
||||||
try:
|
|
||||||
self.sounds['menu-select'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if currentIndex == len(options) - 1: # Cancel
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return save_files[currentIndex]
|
|
||||||
|
|
||||||
pygame.event.clear()
|
|
||||||
|
|
||||||
def auto_save(self):
|
def auto_save(self):
|
||||||
"""Automatically save the game if player has enough bone dust"""
|
"""Automatically save the game if player has enough bone dust"""
|
||||||
|
# Don't save in survival mode
|
||||||
|
if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId == 999:
|
||||||
|
return False
|
||||||
|
|
||||||
if not self.player.can_save():
|
if not self.player.can_save():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -157,8 +156,8 @@ class WickedQuest:
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
try:
|
try:
|
||||||
if 'save' in self.sounds:
|
if 'save' in self.get_sounds():
|
||||||
play_sound(self.sounds['save'])
|
play_sound(self.get_sounds()['save'])
|
||||||
else:
|
else:
|
||||||
print("Save sound not found in sounds dictionary")
|
print("Save sound not found in sounds dictionary")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -179,10 +178,17 @@ class WickedQuest:
|
|||||||
currentTime = pygame.time.get_ticks()
|
currentTime = pygame.time.get_ticks()
|
||||||
|
|
||||||
# Update running and ducking states
|
# Update running and ducking states
|
||||||
if (keys[pygame.K_s] or keys[pygame.K_DOWN]) and not player.isDucking:
|
# Don't handle ducking if jumping (down key is used for instant landing with broom)
|
||||||
player.duck()
|
# Also prevent ducking for 250ms after broom landing to avoid conflict
|
||||||
elif (not keys[pygame.K_s] and not keys[pygame.K_DOWN]) and player.isDucking:
|
broomLandingGracePeriod = 250 # milliseconds
|
||||||
player.stand()
|
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:
|
if self.runLock:
|
||||||
player.isRunning = not (keys[pygame.K_SPACE] or keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT])
|
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:
|
if movementDistance > 0 and not player.isJumping:
|
||||||
player.distanceSinceLastStep += movementDistance
|
player.distanceSinceLastStep += movementDistance
|
||||||
if player.should_play_footstep(currentTime):
|
if player.should_play_footstep(currentTime):
|
||||||
play_sound(self.sounds[player.footstepSound])
|
play_sound(self.get_sounds()[player.footstepSound])
|
||||||
player.distanceSinceLastStep = 0
|
player.distanceSinceLastStep = 0
|
||||||
player.lastStepTime = currentTime
|
player.lastStepTime = currentTime
|
||||||
|
|
||||||
# Status queries
|
# Status queries
|
||||||
if keys[pygame.K_c]:
|
if keys[pygame.K_c]:
|
||||||
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]:
|
if keys[pygame.K_h]:
|
||||||
speak(f"{player.get_health()} health of {player.get_max_health()}")
|
speak(f"{player.get_health()} health of {player.get_max_health()}")
|
||||||
if keys[pygame.K_i]:
|
if keys[pygame.K_i]:
|
||||||
@@ -232,20 +242,44 @@ class WickedQuest:
|
|||||||
if keys[pygame.K_e]:
|
if keys[pygame.K_e]:
|
||||||
speak(f"Wielding {self.currentLevel.player.currentWeapon.name.replace('_', ' ')}")
|
speak(f"Wielding {self.currentLevel.player.currentWeapon.name.replace('_', ' ')}")
|
||||||
|
|
||||||
|
# Weapon switching (1=shovel, 2=broom, 3=nunchucks)
|
||||||
|
currentTime = pygame.time.get_ticks()
|
||||||
|
if currentTime - self.lastWeaponSwitchTime >= self.weaponSwitchDelay:
|
||||||
|
if keys[pygame.K_1]:
|
||||||
|
if player.switch_to_weapon(1):
|
||||||
|
self.lastWeaponSwitchTime = currentTime
|
||||||
|
elif keys[pygame.K_2]:
|
||||||
|
if player.switch_to_weapon(2):
|
||||||
|
self.lastWeaponSwitchTime = currentTime
|
||||||
|
elif keys[pygame.K_3]:
|
||||||
|
if player.switch_to_weapon(3):
|
||||||
|
self.lastWeaponSwitchTime = currentTime
|
||||||
|
|
||||||
# Handle attack with either CTRL key
|
# Handle attack with either CTRL key
|
||||||
if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime):
|
if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime):
|
||||||
play_sound(self.sounds[player.currentWeapon.attackSound])
|
play_sound(self.get_sounds()[player.currentWeapon.attackSound])
|
||||||
|
|
||||||
# Handle jumping
|
# Handle jumping
|
||||||
if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping:
|
if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping:
|
||||||
player.isJumping = True
|
player.isJumping = True
|
||||||
player.jumpStartTime = currentTime
|
player.jumpStartTime = currentTime
|
||||||
play_sound(self.sounds['jump'])
|
play_sound(self.get_sounds()['jump'])
|
||||||
|
|
||||||
# Check if jump should end
|
# Handle instant landing with broom (press down while jumping)
|
||||||
if player.isJumping and currentTime - player.jumpStartTime >= player.jumpDuration:
|
if (player.isJumping and (keys[pygame.K_s] or keys[pygame.K_DOWN]) and
|
||||||
|
player.currentWeapon and player.currentWeapon.name == "witch_broom"):
|
||||||
player.isJumping = False
|
player.isJumping = False
|
||||||
play_sound(self.sounds[player.footstepSound]) # Landing sound
|
play_sound(self.get_sounds()[player.footstepSound]) # Landing sound
|
||||||
|
# Reset step distance tracking after landing
|
||||||
|
player.distanceSinceLastStep = 0
|
||||||
|
player.lastStepTime = currentTime
|
||||||
|
# Set timestamp to prevent immediate ducking after broom landing
|
||||||
|
self.lastBroomLandingTime = currentTime
|
||||||
|
|
||||||
|
# Check if jump should end naturally
|
||||||
|
if player.isJumping and currentTime - player.jumpStartTime >= player.get_current_jump_duration():
|
||||||
|
player.isJumping = False
|
||||||
|
play_sound(self.get_sounds()[player.footstepSound]) # Landing sound
|
||||||
# Reset step distance tracking after landing
|
# Reset step distance tracking after landing
|
||||||
player.distanceSinceLastStep = 0
|
player.distanceSinceLastStep = 0
|
||||||
player.lastStepTime = currentTime
|
player.lastStepTime = currentTime
|
||||||
@@ -272,7 +306,7 @@ class WickedQuest:
|
|||||||
try:
|
try:
|
||||||
speak(f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}, complete!")
|
speak(f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}, complete!")
|
||||||
pygame.mixer.music.stop()
|
pygame.mixer.music.stop()
|
||||||
cut_scene(self.sounds, '_finish_level')
|
cut_scene(self.get_sounds(), '_finish_level')
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -298,7 +332,31 @@ class WickedQuest:
|
|||||||
pygame.event.clear()
|
pygame.event.clear()
|
||||||
self.player.scoreboard.add_high_score()
|
self.player.scoreboard.add_high_score()
|
||||||
|
|
||||||
cut_scene(self.sounds, "game_over")
|
cut_scene(self.get_sounds(), "game_over")
|
||||||
|
display_text(report)
|
||||||
|
|
||||||
|
def display_survival_stats(self, timeTaken):
|
||||||
|
"""Display survival mode completion statistics."""
|
||||||
|
# Convert time from milliseconds to minutes:seconds
|
||||||
|
minutes = timeTaken // 60000
|
||||||
|
seconds = (timeTaken % 60000) // 1000
|
||||||
|
|
||||||
|
report = [f"Survival Mode Complete!"]
|
||||||
|
report.append(f"Final Wave Reached: {self.survivalWave}")
|
||||||
|
report.append(f"Final Score: {self.survivalScore}")
|
||||||
|
report.append(f"Time Survived: {minutes} minutes and {seconds} seconds")
|
||||||
|
report.append("") # Blank line
|
||||||
|
|
||||||
|
# Add all total stats
|
||||||
|
for key in self.currentLevel.player.stats.total:
|
||||||
|
if key not in ['Total time', 'levelsCompleted']: # Skip these
|
||||||
|
report.append(f"Total {key}: {self.currentLevel.player.stats.get_total_stat(key)}")
|
||||||
|
|
||||||
|
if self.currentLevel.player.scoreboard.check_high_score():
|
||||||
|
pygame.event.clear()
|
||||||
|
self.currentLevel.player.scoreboard.add_high_score()
|
||||||
|
|
||||||
|
cut_scene(self.get_sounds(), "game_over")
|
||||||
display_text(report)
|
display_text(report)
|
||||||
|
|
||||||
def game_loop(self, startingLevelNum=1):
|
def game_loop(self, startingLevelNum=1):
|
||||||
@@ -389,8 +447,8 @@ class WickedQuest:
|
|||||||
for ext in ['.wav', '.ogg', '.mp3']:
|
for ext in ['.wav', '.ogg', '.mp3']:
|
||||||
endFile = os.path.join(gamePath, f'end{ext}')
|
endFile = os.path.join(gamePath, f'end{ext}')
|
||||||
if os.path.exists(endFile):
|
if os.path.exists(endFile):
|
||||||
self.sounds['end_scene'] = pygame.mixer.Sound(endFile)
|
self.get_sounds()['end_scene'] = pygame.mixer.Sound(endFile)
|
||||||
cut_scene(self.sounds, 'end_scene')
|
cut_scene(self.get_sounds(), 'end_scene')
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
messagebox("Congratulations! You've completed all available levels!")
|
messagebox("Congratulations! You've completed all available levels!")
|
||||||
@@ -414,7 +472,7 @@ class WickedQuest:
|
|||||||
if self.saveManager.has_saves():
|
if self.saveManager.has_saves():
|
||||||
custom_options.append("load_game")
|
custom_options.append("load_game")
|
||||||
|
|
||||||
choice = game_menu(self.sounds, None, *custom_options)
|
choice = game_menu(self.get_sounds(), None, *custom_options)
|
||||||
|
|
||||||
if choice == "exit":
|
if choice == "exit":
|
||||||
exit_game()
|
exit_game()
|
||||||
@@ -427,6 +485,8 @@ class WickedQuest:
|
|||||||
self.currentGame = save_data['game_state']['currentGame']
|
self.currentGame = save_data['game_state']['currentGame']
|
||||||
self.gameStartTime = save_data['game_state']['gameStartTime']
|
self.gameStartTime = save_data['game_state']['gameStartTime']
|
||||||
current_level = save_data['game_state']['currentLevel']
|
current_level = save_data['game_state']['currentLevel']
|
||||||
|
# Initialize pack-specific sound system
|
||||||
|
self.initialize_pack_sounds()
|
||||||
|
|
||||||
# Load the level
|
# Load the level
|
||||||
if self.load_level(current_level):
|
if self.load_level(current_level):
|
||||||
@@ -438,7 +498,11 @@ class WickedQuest:
|
|||||||
else:
|
else:
|
||||||
messagebox(f"Failed to load save: {save_data}")
|
messagebox(f"Failed to load save: {save_data}")
|
||||||
elif choice == "play":
|
elif choice == "play":
|
||||||
self.currentGame = select_game(self.sounds)
|
self.currentGame = select_game(self.get_sounds())
|
||||||
|
if self.currentGame is None:
|
||||||
|
continue # User cancelled game selection, return to main menu
|
||||||
|
# Initialize pack-specific sound system
|
||||||
|
self.initialize_pack_sounds()
|
||||||
# Validate level files before starting
|
# Validate level files before starting
|
||||||
errors = self.validate_levels()
|
errors = self.validate_levels()
|
||||||
if errors:
|
if errors:
|
||||||
@@ -448,10 +512,15 @@ class WickedQuest:
|
|||||||
display_text(errorLines)
|
display_text(errorLines)
|
||||||
continue
|
continue
|
||||||
if self.currentGame:
|
if self.currentGame:
|
||||||
self.player = None # Reset player for new game
|
# Ask player to choose game mode
|
||||||
self.gameStartTime = pygame.time.get_ticks()
|
mode_choice = game_mode_menu(self.get_sounds())
|
||||||
if self.load_level(1):
|
if mode_choice == "story":
|
||||||
self.game_loop()
|
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":
|
elif choice == "high_scores":
|
||||||
board = Scoreboard()
|
board = Scoreboard()
|
||||||
scores = board.get_high_scores()
|
scores = board.get_high_scores()
|
||||||
@@ -463,9 +532,169 @@ class WickedQuest:
|
|||||||
pygame.event.clear()
|
pygame.event.clear()
|
||||||
display_text(lines)
|
display_text(lines)
|
||||||
elif choice == "learn_sounds":
|
elif choice == "learn_sounds":
|
||||||
choice = learn_sounds(self.sounds)
|
choice = learn_sounds(self.get_sounds())
|
||||||
|
|
||||||
|
def start_survival_mode(self):
|
||||||
|
"""Initialize and start survival mode."""
|
||||||
|
self.survivalGenerator = SurvivalGenerator(self.currentGame)
|
||||||
|
self.survivalWave = 1
|
||||||
|
self.survivalScore = 0
|
||||||
|
self.player = Player(0, 0, self.get_sounds())
|
||||||
|
self.gameStartTime = pygame.time.get_ticks()
|
||||||
|
|
||||||
|
# Show intro message before level starts
|
||||||
|
messagebox(f"Survival Mode - Wave {self.survivalWave}! Survive as long as you can!")
|
||||||
|
|
||||||
|
# Generate first survival segment
|
||||||
|
levelData = self.survivalGenerator.generate_survival_level(self.survivalWave, 300)
|
||||||
|
self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame)
|
||||||
|
|
||||||
|
self.survival_loop()
|
||||||
|
|
||||||
|
def survival_loop(self):
|
||||||
|
"""Main survival mode game loop with endless level generation."""
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
currentTime = pygame.time.get_ticks()
|
||||||
|
pygame.event.pump()
|
||||||
|
|
||||||
|
# Handle events
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
# Check for Alt modifier
|
||||||
|
mods = pygame.key.get_mods()
|
||||||
|
altPressed = mods & pygame.KMOD_ALT
|
||||||
|
|
||||||
|
if event.key == pygame.K_ESCAPE:
|
||||||
|
# Stop all sounds before exiting
|
||||||
|
pygame.mixer.stop()
|
||||||
|
pygame.mixer.music.stop()
|
||||||
|
# Calculate survival time
|
||||||
|
survivalTime = pygame.time.get_ticks() - self.gameStartTime
|
||||||
|
self.display_survival_stats(survivalTime)
|
||||||
|
return
|
||||||
|
elif event.key in [pygame.K_CAPSLOCK, pygame.K_TAB]:
|
||||||
|
self.runLock = not self.runLock
|
||||||
|
speak("Run lock " + ("enabled." if self.runLock else "disabled."))
|
||||||
|
elif event.key == pygame.K_BACKSPACE:
|
||||||
|
pause_game()
|
||||||
|
# Volume controls (require Alt)
|
||||||
|
elif altPressed:
|
||||||
|
if event.key == pygame.K_PAGEUP:
|
||||||
|
adjust_master_volume(0.1)
|
||||||
|
elif event.key == pygame.K_PAGEDOWN:
|
||||||
|
adjust_master_volume(-0.1)
|
||||||
|
elif event.key == pygame.K_HOME:
|
||||||
|
adjust_bgm_volume(0.1)
|
||||||
|
elif event.key == pygame.K_END:
|
||||||
|
adjust_bgm_volume(-0.1)
|
||||||
|
elif event.key == pygame.K_INSERT:
|
||||||
|
adjust_sfx_volume(0.1)
|
||||||
|
elif event.key == pygame.K_DELETE:
|
||||||
|
adjust_sfx_volume(-0.1)
|
||||||
|
elif event.type == pygame.QUIT:
|
||||||
|
exit_game()
|
||||||
|
|
||||||
|
# Update game state (following main game_loop pattern)
|
||||||
|
self.currentLevel.player.update(currentTime)
|
||||||
|
self.handle_input()
|
||||||
|
self.currentLevel.update_audio()
|
||||||
|
|
||||||
|
# Handle combat and projectiles
|
||||||
|
self.currentLevel.handle_combat(currentTime)
|
||||||
|
self.currentLevel.handle_projectiles(currentTime)
|
||||||
|
|
||||||
|
# Handle collisions (including collecting items)
|
||||||
|
self.currentLevel.handle_collisions()
|
||||||
|
|
||||||
|
# Check if player reached end of segment - generate new one
|
||||||
|
if self.player.xPos >= self.currentLevel.rightBoundary - 20:
|
||||||
|
# Check lock system - only advance if no active enemies remain
|
||||||
|
if self.currentLevel.isLocked and any(enemy.isActive for enemy in self.currentLevel.enemies):
|
||||||
|
speak("You must defeat all enemies before proceeding to the next wave!")
|
||||||
|
play_sound(self.get_sounds()['locked'])
|
||||||
|
# Push player back a bit
|
||||||
|
self.player.xPos -= 5
|
||||||
|
else:
|
||||||
|
self.advance_survival_wave()
|
||||||
|
|
||||||
|
# Check for death first (following main game loop pattern)
|
||||||
|
if self.currentLevel.player.get_health() <= 0:
|
||||||
|
if self.currentLevel.player.get_lives() <= 0:
|
||||||
|
# Game over - stop all sounds
|
||||||
|
pygame.mixer.stop()
|
||||||
|
# Calculate survival time
|
||||||
|
survivalTime = pygame.time.get_ticks() - self.gameStartTime
|
||||||
|
self.display_survival_stats(survivalTime)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Player died but has lives left - respawn
|
||||||
|
pygame.mixer.stop()
|
||||||
|
self.currentLevel.player._health = self.currentLevel.player._maxHealth
|
||||||
|
# Reset player position to beginning of current segment
|
||||||
|
self.player.xPos = 10
|
||||||
|
|
||||||
|
# Update score based on survival time
|
||||||
|
self.survivalScore += 1
|
||||||
|
|
||||||
|
clock.tick(60) # 60 FPS
|
||||||
|
|
||||||
|
def advance_survival_wave(self):
|
||||||
|
"""Generate next wave/segment for survival mode."""
|
||||||
|
self.survivalWave += 1
|
||||||
|
|
||||||
|
# Clear any lingering projectiles/sounds from previous wave
|
||||||
|
if hasattr(self, 'currentLevel') and self.currentLevel:
|
||||||
|
self.currentLevel.projectiles.clear()
|
||||||
|
pygame.mixer.stop() # Stop any ongoing catapult/enemy sounds
|
||||||
|
|
||||||
|
# Check for all monsters unlocked bonus and prepare wave message
|
||||||
|
waveMessage = f"Wave {self.survivalWave}!"
|
||||||
|
if self.survivalGenerator:
|
||||||
|
maxLevel = max(self.survivalGenerator.levelData.keys()) if self.survivalGenerator.levelData else 1
|
||||||
|
if self.survivalWave == maxLevel + 1: # First wave after all levels unlocked
|
||||||
|
# Double the current score
|
||||||
|
self.survivalScore *= 2
|
||||||
|
play_sound(self.get_sounds().get("survivor_bonus", self.get_sounds()["get_extra_life"]))
|
||||||
|
waveMessage += " All monsters unlocked bonus! Score doubled!"
|
||||||
|
|
||||||
|
# Announce new wave (with bonus message if applicable)
|
||||||
|
speak(waveMessage)
|
||||||
|
|
||||||
|
# Generate new segment
|
||||||
|
segmentLength = min(500, 300 + (self.survivalWave * 20)) # Longer segments over time
|
||||||
|
levelData = self.survivalGenerator.generate_survival_level(self.survivalWave, segmentLength)
|
||||||
|
|
||||||
|
# Preserve player position but shift to start of new segment
|
||||||
|
playerX = 10
|
||||||
|
self.player.xPos = playerX
|
||||||
|
|
||||||
|
# Create new level
|
||||||
|
self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame)
|
||||||
|
|
||||||
|
|
||||||
|
def game_mode_menu(sounds):
|
||||||
|
"""Display game mode selection menu using instruction_menu.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of loaded sound effects
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Selected game mode or None if cancelled
|
||||||
|
"""
|
||||||
|
choice = instruction_menu(sounds, "Select game mode:", "Story", "Survival Mode")
|
||||||
|
|
||||||
|
if choice == "Story":
|
||||||
|
return "story"
|
||||||
|
elif choice == "Survival Mode":
|
||||||
|
return "survival"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
game = WickedQuest()
|
game = WickedQuest()
|
||||||
game.run()
|
game.run()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -1,11 +1,24 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Collect level directories dynamically
|
||||||
|
level_dirs = []
|
||||||
|
levels_path = 'levels'
|
||||||
|
if os.path.exists(levels_path):
|
||||||
|
for item in os.listdir(levels_path):
|
||||||
|
item_path = os.path.join(levels_path, item)
|
||||||
|
if os.path.isdir(item_path):
|
||||||
|
level_dirs.append((item_path, item_path))
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
['wicked_quest.py'],
|
['wicked_quest.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[],
|
datas=level_dirs + [
|
||||||
|
('sounds', 'sounds'),
|
||||||
|
('libstormgames', 'libstormgames'),
|
||||||
|
],
|
||||||
hiddenimports=[],
|
hiddenimports=[],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
|
|||||||
Reference in New Issue
Block a user