Code cleanup, added more functionality. Floating coffins that spawn items, graves can spawn zombies, etc.

This commit is contained in:
Storm Dragon
2025-01-30 22:22:59 -05:00
parent 53009373c2
commit 6c000d78d8
10 changed files with 402 additions and 15 deletions

View File

@@ -28,7 +28,8 @@
"y": 0,
"hazard": true,
"sound": "grave",
"static": true
"static": true,
"zombie_spawn_chance": 100
}
],
"boundaries": {

40
src/coffin.py Normal file
View File

@@ -0,0 +1,40 @@
from libstormgames import *
from src.object import Object
from src.powerup import PowerUp
import random
class CoffinObject(Object):
def __init__(self, x, y, sounds):
super().__init__(
x, y, "coffin",
isStatic=True,
isCollectible=False,
isHazard=False
)
self.sounds = sounds
self.is_broken = False
self.dropped_item = None
def hit(self, player_pos):
"""Handle being hit by the player's weapon"""
if not self.is_broken:
self.is_broken = True
self.sounds['coffin_shatter'].play()
# Randomly choose item type
item_type = random.choice(['hand_of_glory', 'jack_o_lantern'])
# Create item 1-2 tiles away in random direction
direction = random.choice([-1, 1])
drop_distance = random.randint(1, 2)
drop_x = self.xPos + (direction * drop_distance)
self.dropped_item = PowerUp(
drop_x,
self.yPos,
item_type,
self.sounds,
direction
)
return True
return False

View File

@@ -24,12 +24,19 @@ class Enemy(Object):
# Movement and behavior properties
self.movingRight = True # Initial direction
self.movementSpeed = 0.03 # Slightly slower than player
self.movementSpeed = 0.03 # Base speed
self.patrolStart = self.xRange[0]
self.patrolEnd = self.xRange[0] + self.movementRange
self.lastAttackTime = 0
self.attackCooldown = 1000 # 1 second between attacks
# Enemy type specific adjustments
if enemyType == "zombie":
self.movementSpeed *= 0.6 # Zombies are slower
self.damage = 10 # Zombies do massive damage
self.health = 3 # Easier to kill than goblins
self.attackCooldown = 1500 # Slower attack rate
@property
def xPos(self):
"""Current x position"""
@@ -45,7 +52,17 @@ class Enemy(Object):
if not self.isActive or self.health <= 0:
return
# Update position based on patrol behavior
# Zombie behavior - always chase player
if self.enemyType == "zombie":
# Determine direction to player
if player.xPos > self.xPos:
self.movingRight = True
self.xPos += self.movementSpeed
else:
self.movingRight = False
self.xPos -= self.movementSpeed
else:
# Normal patrol behavior for other enemies
if self.movingRight:
self.xPos += self.movementSpeed
if self.xPos >= self.patrolEnd:
@@ -103,3 +120,7 @@ class Enemy(Object):
if self.channel:
obj_stop(self.channel)
self.channel = None
# Play death sound if available
deathSound = f"{self.enemyType}_death"
if deathSound in self.sounds:
self.sounds[deathSound].play()

89
src/item.py Normal file
View File

@@ -0,0 +1,89 @@
from libstormgames import *
from src.object import Object
import random
class CoffinObject(Object):
def __init__(self, x, y, sounds):
super().__init__(
x, y, "coffin",
isStatic=True,
isCollectible=False,
isHazard=False
)
self.sounds = sounds
self.is_broken = False
self.dropped_item = None
def hit(self, player_pos):
"""Handle being hit by the player's weapon"""
if not self.is_broken:
self.is_broken = True
self.sounds['coffin_shatter'].play()
# Randomly choose item type
item_type = random.choice(['hand_of_glory', 'jack_o_lantern'])
# Create item 1-2 tiles away in random direction
direction = random.choice([-1, 1])
drop_distance = random.randint(1, 2)
drop_x = self.xPos + (direction * drop_distance)
self.dropped_item = Item(
drop_x,
self.yPos,
item_type,
self.sounds,
direction
)
return True
return False
class Item(Object):
def __init__(self, x, y, item_type, sounds, direction):
super().__init__(
x, y, item_type,
isStatic=False,
isCollectible=True,
isHazard=False
)
self.sounds = sounds
self.direction = direction
self.speed = 0.05 # Base movement speed
self.item_type = item_type
self.channel = None
def update(self, current_time):
"""Update item position"""
if not self.isActive:
return False
# Update position
self._currentX += self.direction * self.speed
# Keep bounce sound playing while moving
if self.channel is None or not self.channel.get_busy():
self.channel = self.sounds['item_bounce'].play(-1)
# Check if item has gone too far (20 tiles)
if abs(self._currentX - self.xRange[0]) > 20:
self.isActive = False
if self.channel:
self.channel.stop()
self.channel = None
return False
return True
def apply_effect(self, player):
"""Apply the item's effect when collected"""
if self.item_type == 'hand_of_glory':
player.start_invincibility()
speak("Hand of Glory makes you invincible!")
elif self.item_type == 'jack_o_lantern':
player.add_projectile('jack_o_lantern')
speak("Gained a Jack-o'-lantern projectile!")
# Stop movement sound when collected
if self.channel:
self.channel.stop()
self.channel = None

View File

@@ -1,14 +1,20 @@
import pygame
import random
from libstormgames import *
from src.enemy import Enemy
from src.object import Object
from src.player import Player
from src.projectile import Projectile
from src.coffin import CoffinObject
from src.powerup import PowerUp
class Level:
def __init__(self, levelData, sounds):
self.sounds = sounds
self.objects = []
self.enemies = []
self.bouncing_items = []
self.projectiles = [] # Track active projectiles
self.player = Player(levelData["player_start"]["x"], levelData["player_start"]["y"])
# Load objects and enemies from level data
@@ -39,18 +45,45 @@ class Level:
obj["sound"],
isStatic=obj.get("static", True),
isCollectible=obj.get("collectible", False),
isHazard=obj.get("hazard", False)
isHazard=obj.get("hazard", False),
zombie_spawn_chance=obj.get("zombie_spawn_chance", 0)
)
self.objects.append(gameObject)
def update_audio(self):
currentTime = pygame.time.get_ticks()
# Update regular objects
# Update regular objects and check for zombie spawning
for obj in self.objects:
if not obj.isActive:
continue
# Check for potential zombie spawn from graves
if (obj.soundName == "grave" and
obj.zombie_spawn_chance > 0 and
not obj.has_spawned):
distance = abs(self.player.xPos - obj.xPos)
if distance < 6: # Within 6 tiles
# Mark as checked before doing anything else to prevent multiple checks
obj.has_spawned = True
roll = random.randint(1, 100)
speak(f"Near grave, chance to spawn zombie")
if roll <= obj.zombie_spawn_chance:
zombie = Enemy(
[obj.xPos, obj.xPos],
obj.yPos,
"zombie",
self.sounds,
health=3,
damage=10,
attack_range=1
)
self.enemies.append(zombie)
speak("A zombie emerges from the grave!")
# 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)
@@ -73,15 +106,43 @@ class Level:
if enemy.channel is not None:
enemy.channel = obj_update(enemy.channel, self.player.xPos, enemy.xPos)
# Update bouncing items
for item in self.bouncing_items[:]: # Copy list to allow removal
if not item.update(currentTime):
self.bouncing_items.remove(item)
if not item.isActive:
speak(f"{item.soundName} got away!")
continue
# Check for item collection
if abs(item.xPos - self.player.xPos) < 1 and self.player.isJumping:
self.sounds[f'get_{item.soundName}'].play()
item.apply_effect(self.player)
item.isActive = False
self.bouncing_items.remove(item)
def handle_combat(self, currentTime):
"""Handle combat interactions between player and enemies"""
attackRange = self.player.get_attack_range(currentTime)
if attackRange:
# Check for enemy hits
for enemy in self.enemies:
if enemy.isActive and enemy.xPos >= attackRange[0] and enemy.xPos <= attackRange[1]:
self.sounds[self.player.currentWeapon.hitSound].play()
enemy.take_damage(self.player.currentWeapon.damage)
# Check for coffin hits - only if we have any coffins
for obj in self.objects:
if hasattr(obj, 'is_broken'): # Check if it's a coffin without using isinstance
if (not obj.is_broken and
obj.xPos >= attackRange[0] and
obj.xPos <= attackRange[1] and
self.player.isJumping): # Must be jumping to hit floating coffins
if obj.hit(self.player.xPos):
self.bouncing_items.append(obj.dropped_item)
speak(f"{obj.dropped_item.item_type} falls out!")
def handle_collisions(self):
for obj in self.objects:
if not obj.isActive:
@@ -99,3 +160,30 @@ class Level:
self.sounds[obj.soundName].play()
speak("You fell in an open grave!")
self.player.set_health(0)
def handle_projectiles(self, currentTime):
"""Update projectiles and check for collisions"""
for proj in self.projectiles[:]: # Copy list to allow removal
if not proj.update():
self.projectiles.remove(proj)
continue
# Check for enemy hits
for enemy in self.enemies:
if enemy.isActive and abs(proj.x - enemy.xPos) < 1:
proj.hit_enemy(enemy)
self.projectiles.remove(proj)
break
def throw_projectile(self):
"""Have player throw a projectile"""
proj_info = self.player.throw_projectile()
if proj_info:
self.projectiles.append(Projectile(
proj_info['type'],
proj_info['start_x'],
proj_info['direction']
))
# Play throw sound
if f"{proj_info['type']}_throw" in self.sounds:
self.sounds[f"{proj_info['type']}_throw"].play()

View File

@@ -1,7 +1,7 @@
from libstormgames import *
class Object:
def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False):
def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False, zombie_spawn_chance=0):
# x can be either a single position or a range [start, end]
self.xRange = [x, x] if isinstance(x, (int, float)) else x
self.yPos = yPos
@@ -9,6 +9,8 @@ class Object:
self.isStatic = isStatic
self.isCollectible = isCollectible
self.isHazard = isHazard
self.zombie_spawn_chance = zombie_spawn_chance
self.has_spawned = False # Track if this object has spawned a zombie
self.channel = None # For tracking the sound channel
self.isActive = True
# For collectibles in a range, track which positions have been collected

View File

@@ -1,5 +1,4 @@
from src.weapon import Weapon
class Player:
def __init__(self, xPos, yPos):
# Movement attributes
@@ -28,6 +27,14 @@ class Player:
self.isAttacking = False
self.lastAttackTime = 0
# Power-up states
self.isInvincible = False
self.invincibilityStartTime = 0
self.invincibilityDuration = 5000 # 5 seconds of invincibility
# Projectiles
self.projectiles = [] # List of type and quantity tuples
# Initialize starting weapon (rusty shovel)
self.add_weapon(Weapon(
name="rusty_shovel",
@@ -38,12 +45,58 @@ class Player:
attackDuration=200 # 200ms attack duration
))
def update(self, currentTime):
"""Update player state"""
# Check if invincibility has expired
if self.isInvincible and currentTime - self.invincibilityStartTime >= self.invincibilityDuration:
self.isInvincible = False
speak("Invincibility wore off!")
def start_invincibility(self):
"""Activate invincibility from Hand of Glory"""
self.isInvincible = True
self.invincibilityStartTime = pygame.time.get_ticks()
def add_projectile(self, projectile_type):
"""Add a projectile to inventory"""
# Find if we already have this type
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
self.projectiles.append([projectile_type, 1])
def throw_projectile(self):
"""Throw the first available projectile"""
if not self.projectiles:
speak("No projectiles to throw!")
return None
# Get the first projectile type
projectile = self.projectiles[0]
projectile[1] -= 1 # Decrease quantity
if projectile[1] <= 0:
self.projectiles.pop(0) # Remove if none left
return {
'type': projectile[0],
'start_x': self.xPos,
'direction': 1 if self.facingRight else -1
}
def get_health(self):
"""Get current health"""
return self._health
def set_health(self, value):
"""Set health and handle death if needed"""
if self.isInvincible:
return # No damage while invincible
self._health = max(0, value) # Health can't go below 0
if self._health == 0:
self._lives -= 1

52
src/powerup.py Normal file
View File

@@ -0,0 +1,52 @@
from libstormgames import *
from src.object import Object
class PowerUp(Object):
def __init__(self, x, y, item_type, sounds, direction):
super().__init__(
x, y, item_type,
isStatic=False,
isCollectible=True,
isHazard=False
)
self.sounds = sounds
self.direction = direction
self.speed = 0.05 # Base movement speed
self.item_type = item_type
self.channel = None
def update(self, current_time):
"""Update item position"""
if not self.isActive:
return False
# Update position
self._currentX += self.direction * self.speed
# Keep bounce sound playing while moving
if self.channel is None or not self.channel.get_busy():
self.channel = self.sounds['item_bounce'].play(-1)
# Check if item has gone too far (20 tiles)
if abs(self._currentX - self.xRange[0]) > 20:
self.isActive = False
if self.channel:
self.channel.stop()
self.channel = None
return False
return True
def apply_effect(self, player):
"""Apply the item's effect when collected"""
if self.item_type == 'hand_of_glory':
player.start_invincibility()
speak("Hand of Glory makes you invincible!")
elif self.item_type == 'jack_o_lantern':
player.add_projectile('jack_o_lantern')
speak("Gained a Jack-o'-lantern projectile!")
# Stop movement sound when collected
if self.channel:
self.channel.stop()
self.channel = None

29
src/projectile.py Normal file
View File

@@ -0,0 +1,29 @@
class Projectile:
def __init__(self, projectile_type, start_x, direction):
self.type = projectile_type
self.x = start_x
self.direction = direction
self.speed = 0.2 # Projectiles move faster than player
self.isActive = True
self.damage = 5 # All projectiles do same damage for now
self.range = 10 # Maximum travel distance in tiles
self.start_x = start_x
def update(self):
"""Update projectile position and check if it should still exist"""
if not self.isActive:
return False
self.x += self.direction * self.speed
# Check if projectile has gone too far
if abs(self.x - self.start_x) > self.range:
self.isActive = False
return False
return True
def hit_enemy(self, enemy):
"""Handle hitting an enemy"""
enemy.take_damage(self.damage)
self.isActive = False # Projectile is destroyed on hit

View File

@@ -45,6 +45,12 @@ class WickedQuest:
player.xPos += currentSpeed
player.facingRight = True
if keys[pygame.K_h]:
speak(f"{player.get_health()} HP")
if keys[pygame.K_f]: # Throw projectile
self.currentLevel.throw_projectile()
# Handle attack with either CTRL key
if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime):
self.sounds[player.currentWeapon.attackSound].play()
@@ -77,6 +83,9 @@ class WickedQuest:
if check_for_exit():
return
# Update player state (including power-ups)
self.currentLevel.player.update(currentTime)
self.handle_input()
# Update audio positioning and handle collisions
@@ -86,6 +95,9 @@ class WickedQuest:
# Handle combat interactions
self.currentLevel.handle_combat(currentTime)
# Update projectiles
self.currentLevel.handle_projectiles(currentTime)
clock.tick(60) # 60 FPS
# Player died or ran out of lives