Updated libstormgames submodule. Added skull storms. Fixed no jack-o-lanterns reported when there actually were. Fixed player being recreated on each new level thus resetting all stats.
This commit is contained in:
Submodule libstormgames updated: d5c79c0770...658709ebce
BIN
sounds/falling_skull1.ogg
(Stored with Git LFS)
Normal file
BIN
sounds/falling_skull1.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
sounds/skull_lands.ogg
(Stored with Git LFS)
Normal file
BIN
sounds/skull_lands.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
sounds/skull_storm.ogg
(Stored with Git LFS)
Normal file
BIN
sounds/skull_storm.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
sounds/throw_jack_o_lantern.ogg
(Stored with Git LFS)
Normal file
BIN
sounds/throw_jack_o_lantern.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
61
src/level.py
61
src/level.py
@@ -8,15 +8,17 @@ from src.object import Object
|
|||||||
from src.player import Player
|
from src.player import Player
|
||||||
from src.projectile import Projectile
|
from src.projectile import Projectile
|
||||||
from src.powerup import PowerUp
|
from src.powerup import PowerUp
|
||||||
|
from src.skull_storm import SkullStorm
|
||||||
|
|
||||||
|
|
||||||
class Level:
|
class Level:
|
||||||
def __init__(self, levelData, sounds):
|
def __init__(self, levelData, sounds, player):
|
||||||
self.sounds = sounds
|
self.sounds = sounds
|
||||||
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(levelData["player_start"]["x"], levelData["player_start"]["y"], sounds)
|
self.player = player
|
||||||
self.edge_warning_channel = None
|
self.edge_warning_channel = None
|
||||||
self.weapon_hit_channel = None
|
self.weapon_hit_channel = None
|
||||||
self.leftBoundary = levelData["boundaries"]["left"]
|
self.leftBoundary = levelData["boundaries"]["left"]
|
||||||
@@ -53,6 +55,18 @@ class Level:
|
|||||||
firingRange=obj.get("range", 20)
|
firingRange=obj.get("range", 20)
|
||||||
)
|
)
|
||||||
self.objects.append(catapult)
|
self.objects.append(catapult)
|
||||||
|
# Check if this is a skull storm
|
||||||
|
elif obj.get("type") == "skull_storm":
|
||||||
|
skullStorm = SkullStorm(
|
||||||
|
xPos,
|
||||||
|
obj["y"],
|
||||||
|
self.sounds,
|
||||||
|
obj.get("damage", 5),
|
||||||
|
obj.get("maximum_skulls", 3),
|
||||||
|
obj.get("frequency", {}).get("min", 2),
|
||||||
|
obj.get("frequency", {}).get("max", 5)
|
||||||
|
)
|
||||||
|
self.objects.append(skullStorm)
|
||||||
# Check if this is a coffin
|
# Check if this is a coffin
|
||||||
elif obj.get("type") == "coffin":
|
elif obj.get("type") == "coffin":
|
||||||
coffin = CoffinObject(
|
coffin = CoffinObject(
|
||||||
@@ -120,15 +134,13 @@ class Level:
|
|||||||
speak("A zombie emerges from the grave!")
|
speak("A zombie emerges from the grave!")
|
||||||
|
|
||||||
# Handle object audio
|
# Handle object audio
|
||||||
if not obj.isStatic:
|
|
||||||
if obj.channel is None or not obj.channel.get_busy():
|
|
||||||
obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos)
|
|
||||||
else:
|
|
||||||
if obj.channel is None:
|
|
||||||
obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos)
|
|
||||||
|
|
||||||
if obj.channel is not None:
|
if obj.channel is not None:
|
||||||
obj.channel = obj_update(obj.channel, self.player.xPos, obj.xPos)
|
obj.channel = obj_update(obj.channel, self.player.xPos, obj.xPos)
|
||||||
|
elif obj.soundName: # Only try to play sound if soundName is not empty
|
||||||
|
if not obj.isStatic:
|
||||||
|
obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos)
|
||||||
|
else:
|
||||||
|
obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos)
|
||||||
|
|
||||||
# Update enemies
|
# Update enemies
|
||||||
for enemy in self.enemies:
|
for enemy in self.enemies:
|
||||||
@@ -147,6 +159,11 @@ class Level:
|
|||||||
if isinstance(obj, Catapult):
|
if isinstance(obj, Catapult):
|
||||||
obj.update(currentTime, self.player)
|
obj.update(currentTime, self.player)
|
||||||
|
|
||||||
|
# Update skull storms
|
||||||
|
for obj in self.objects:
|
||||||
|
if isinstance(obj, SkullStorm):
|
||||||
|
obj.update(currentTime, self.player)
|
||||||
|
|
||||||
# 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):
|
if not item.update(currentTime):
|
||||||
@@ -256,17 +273,25 @@ class Level:
|
|||||||
if enemy.isActive and abs(proj.x - enemy.xPos) < 1:
|
if enemy.isActive and abs(proj.x - enemy.xPos) < 1:
|
||||||
proj.hit_enemy(enemy)
|
proj.hit_enemy(enemy)
|
||||||
self.projectiles.remove(proj)
|
self.projectiles.remove(proj)
|
||||||
|
# Calculate volume and pan for splat sound based on final position
|
||||||
|
volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x)
|
||||||
|
if volume > 0: # Only play if within audible range
|
||||||
|
channel = self.sounds["pumpkin_splat"].play()
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(volume * left, volume * right)
|
||||||
break
|
break
|
||||||
|
|
||||||
def throw_projectile(self):
|
def throw_projectile(self):
|
||||||
"""Have player throw a projectile"""
|
"""Have player throw a projectile"""
|
||||||
proj_info = self.player.throw_projectile()
|
proj_info = self.player.throw_projectile()
|
||||||
if proj_info:
|
if proj_info is None:
|
||||||
self.projectiles.append(Projectile(
|
speak("No jack o'lanterns to throw!")
|
||||||
proj_info['type'],
|
return
|
||||||
proj_info['start_x'],
|
|
||||||
proj_info['direction']
|
self.projectiles.append(Projectile(
|
||||||
))
|
proj_info['type'],
|
||||||
# Play throw sound
|
proj_info['start_x'],
|
||||||
if f"{proj_info['type']}_throw" in self.sounds:
|
proj_info['direction']
|
||||||
self.sounds[f"{proj_info['type']}_throw"].play()
|
))
|
||||||
|
# Play throw sound
|
||||||
|
self.sounds['throw_jack_o_lantern'].play()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class Player:
|
|||||||
self.inventory = []
|
self.inventory = []
|
||||||
self.collectedItems = []
|
self.collectedItems = []
|
||||||
self._coins = 0
|
self._coins = 0
|
||||||
|
self._jack_o_lantern_count = 0
|
||||||
|
|
||||||
# Combat related attributes
|
# Combat related attributes
|
||||||
self.weapons = []
|
self.weapons = []
|
||||||
@@ -38,9 +39,6 @@ class Player:
|
|||||||
self.invincibilityStartTime = 0
|
self.invincibilityStartTime = 0
|
||||||
self.invincibilityDuration = 5000 # 5 seconds of invincibility
|
self.invincibilityDuration = 5000 # 5 seconds of invincibility
|
||||||
|
|
||||||
# Projectiles
|
|
||||||
self.projectiles = [] # List of type and quantity tuples
|
|
||||||
|
|
||||||
# Initialize starting weapon (rusty shovel)
|
# Initialize starting weapon (rusty shovel)
|
||||||
self.add_weapon(Weapon(
|
self.add_weapon(Weapon(
|
||||||
name="rusty_shovel",
|
name="rusty_shovel",
|
||||||
@@ -63,33 +61,22 @@ class Player:
|
|||||||
self.isInvincible = True
|
self.isInvincible = True
|
||||||
self.invincibilityStartTime = pygame.time.get_ticks()
|
self.invincibilityStartTime = pygame.time.get_ticks()
|
||||||
|
|
||||||
def add_projectile(self, projectile_type):
|
def get_jack_o_lanterns(self):
|
||||||
"""Add a projectile to inventory"""
|
"""Get number of jack o'lanterns"""
|
||||||
# Find if we already have this type
|
return self._jack_o_lantern_count
|
||||||
for proj in self.projectiles:
|
|
||||||
if proj[0] == projectile_type:
|
|
||||||
proj[1] += 1 # Increase quantity
|
|
||||||
speak(f"Now have {proj[1]} {projectile_type}s")
|
|
||||||
return
|
|
||||||
|
|
||||||
# If not found, add new type with quantity 1
|
def add_jack_o_lantern(self):
|
||||||
self.projectiles.append([projectile_type, 1])
|
"""Add a jack o'lantern"""
|
||||||
|
self._jack_o_lantern_count += 1
|
||||||
|
|
||||||
def throw_projectile(self):
|
def throw_projectile(self):
|
||||||
"""Throw the first available projectile"""
|
"""Throw a jack o'lantern if we have any"""
|
||||||
if not self.projectiles:
|
if self.get_jack_o_lanterns() <= 0:
|
||||||
speak("No projectiles to throw!")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get the first projectile type
|
self._jack_o_lantern_count -= 1
|
||||||
projectile = self.projectiles[0]
|
|
||||||
projectile[1] -= 1 # Decrease quantity
|
|
||||||
|
|
||||||
if projectile[1] <= 0:
|
|
||||||
self.projectiles.pop(0) # Remove if none left
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'type': projectile[0],
|
'type': 'jack_o_lantern',
|
||||||
'start_x': self.xPos,
|
'start_x': self.xPos,
|
||||||
'direction': 1 if self.facingRight else -1
|
'direction': 1 if self.facingRight else -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ class PowerUp(Object):
|
|||||||
player.start_invincibility()
|
player.start_invincibility()
|
||||||
speak("Hand of Glory makes you invincible!")
|
speak("Hand of Glory makes you invincible!")
|
||||||
elif self.item_type == 'jack_o_lantern':
|
elif self.item_type == 'jack_o_lantern':
|
||||||
player.add_projectile('jack_o_lantern')
|
player.add_jack_o_lantern()
|
||||||
speak("Gained a Jack-o'-lantern projectile!")
|
speak("Gained a Jack-o'-lantern!")
|
||||||
|
|
||||||
# Stop movement sound when collected
|
# Stop movement sound when collected
|
||||||
if self.channel:
|
if self.channel:
|
||||||
|
|||||||
112
src/skull_storm.py
Normal file
112
src/skull_storm.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
from libstormgames import *
|
||||||
|
from src.object import Object
|
||||||
|
|
||||||
|
class SkullStorm(Object):
|
||||||
|
"""Handles falling skulls within a specified range."""
|
||||||
|
|
||||||
|
def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5):
|
||||||
|
super().__init__(
|
||||||
|
xRange,
|
||||||
|
y,
|
||||||
|
"", # No ambient sound for the skull storm
|
||||||
|
isStatic=True,
|
||||||
|
isCollectible=False,
|
||||||
|
isHazard=False
|
||||||
|
)
|
||||||
|
self.sounds = sounds
|
||||||
|
self.damage = damage
|
||||||
|
self.maxSkulls = maxSkulls
|
||||||
|
self.minFreq = minFreq * 1000 # Convert to milliseconds
|
||||||
|
self.maxFreq = maxFreq * 1000
|
||||||
|
|
||||||
|
self.activeSkulls = [] # List of currently falling skulls
|
||||||
|
self.lastSkullTime = 0
|
||||||
|
self.nextSkullDelay = random.randint(self.minFreq, self.maxFreq)
|
||||||
|
self.playerInRange = False
|
||||||
|
|
||||||
|
def update(self, currentTime, player):
|
||||||
|
"""Update all active skulls and potentially spawn new ones."""
|
||||||
|
if not self.isActive:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if player has entered range
|
||||||
|
inRange = self.xRange[0] <= player.xPos <= self.xRange[1]
|
||||||
|
if inRange and not self.playerInRange:
|
||||||
|
# Player just entered range - play the warning sound
|
||||||
|
self.sounds['skull_storm'].play()
|
||||||
|
self.playerInRange = True
|
||||||
|
elif not inRange and self.playerInRange: # Only speak when actually leaving range
|
||||||
|
# Player just left range
|
||||||
|
self.playerInRange = False
|
||||||
|
speak("Skull storm ended.")
|
||||||
|
|
||||||
|
if not inRange:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update existing skulls
|
||||||
|
for skull in self.activeSkulls[:]: # Copy list to allow removal
|
||||||
|
if currentTime >= skull['land_time']:
|
||||||
|
# Skull has landed
|
||||||
|
self.handle_landing(skull, player)
|
||||||
|
self.activeSkulls.remove(skull)
|
||||||
|
else:
|
||||||
|
# Update falling sound
|
||||||
|
timeElapsed = currentTime - skull['start_time']
|
||||||
|
fallProgress = timeElapsed / skull['fall_duration']
|
||||||
|
currentY = self.yPos * (1 - fallProgress)
|
||||||
|
|
||||||
|
if skull['channel'] is None or not skull['channel'].get_busy():
|
||||||
|
skull['channel'] = play_random_falling(
|
||||||
|
self.sounds,
|
||||||
|
'falling_skull',
|
||||||
|
player.xPos,
|
||||||
|
skull['x'],
|
||||||
|
self.yPos,
|
||||||
|
currentY
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if we should spawn a new skull
|
||||||
|
if (len(self.activeSkulls) < self.maxSkulls and
|
||||||
|
currentTime - self.lastSkullTime >= self.nextSkullDelay):
|
||||||
|
self.spawn_skull(currentTime)
|
||||||
|
|
||||||
|
def spawn_skull(self, currentTime):
|
||||||
|
"""Spawn a new falling skull at a random position within range."""
|
||||||
|
# Reset timing
|
||||||
|
self.lastSkullTime = currentTime
|
||||||
|
self.nextSkullDelay = random.randint(self.minFreq, self.maxFreq)
|
||||||
|
|
||||||
|
# Calculate fall duration based on height (higher = longer fall)
|
||||||
|
fallDuration = self.yPos * 100 # 100ms per unit of height
|
||||||
|
|
||||||
|
# Create new skull
|
||||||
|
skull = {
|
||||||
|
'x': random.uniform(self.xRange[0], self.xRange[1]),
|
||||||
|
'start_time': currentTime,
|
||||||
|
'fall_duration': fallDuration,
|
||||||
|
'land_time': currentTime + fallDuration,
|
||||||
|
'channel': None
|
||||||
|
}
|
||||||
|
|
||||||
|
self.activeSkulls.append(skull)
|
||||||
|
|
||||||
|
def handle_landing(self, skull, player):
|
||||||
|
"""Handle a skull landing."""
|
||||||
|
# Stop falling sound
|
||||||
|
if skull['channel']:
|
||||||
|
obj_stop(skull['channel'])
|
||||||
|
|
||||||
|
# Play landing sound with positional audio once
|
||||||
|
channel = pygame.mixer.find_channel(True) # Find an available channel
|
||||||
|
if channel:
|
||||||
|
soundObj = self.sounds['skull_lands']
|
||||||
|
channel.play(soundObj, 0) # Play once (0 = no loops)
|
||||||
|
# Apply positional audio
|
||||||
|
volume, left, right = calculate_volume_and_pan(player.xPos, skull['x'])
|
||||||
|
channel.set_volume(volume * left, volume * right)
|
||||||
|
|
||||||
|
# Check if player was hit
|
||||||
|
if abs(player.xPos - skull['x']) < 1: # Within 1 tile
|
||||||
|
if not player.isJumping: # Only hit if not jumping
|
||||||
|
player.set_health(player.get_health() - self.damage)
|
||||||
|
speak("Hit by falling skull!")
|
||||||
Reference in New Issue
Block a user