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, "y": 0,
"hazard": true, "hazard": true,
"sound": "grave", "sound": "grave",
"static": true "static": true,
"zombie_spawn_chance": 100
} }
], ],
"boundaries": { "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 # Movement and behavior properties
self.movingRight = True # Initial direction 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.patrolStart = self.xRange[0]
self.patrolEnd = self.xRange[0] + self.movementRange self.patrolEnd = self.xRange[0] + self.movementRange
self.lastAttackTime = 0 self.lastAttackTime = 0
self.attackCooldown = 1000 # 1 second between attacks 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 @property
def xPos(self): def xPos(self):
"""Current x position""" """Current x position"""
@@ -45,16 +52,26 @@ class Enemy(Object):
if not self.isActive or self.health <= 0: if not self.isActive or self.health <= 0:
return return
# Update position based on patrol behavior # Zombie behavior - always chase player
if self.movingRight: if self.enemyType == "zombie":
self.xPos += self.movementSpeed # Determine direction to player
if self.xPos >= self.patrolEnd: if player.xPos > self.xPos:
self.movingRight = False
else:
self.xPos -= self.movementSpeed
if self.xPos <= self.patrolStart:
self.movingRight = True 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:
self.movingRight = False
else:
self.xPos -= self.movementSpeed
if self.xPos <= self.patrolStart:
self.movingRight = True
# Check for attack opportunity # Check for attack opportunity
if self.can_attack(currentTime, player): if self.can_attack(currentTime, player):
self.attack(currentTime, player) self.attack(currentTime, player)
@@ -103,3 +120,7 @@ class Enemy(Object):
if self.channel: if self.channel:
obj_stop(self.channel) obj_stop(self.channel)
self.channel = None 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 pygame
import random
from libstormgames import * from libstormgames import *
from src.enemy import Enemy from src.enemy import Enemy
from src.object import Object from src.object import Object
from src.player import Player from src.player import Player
from src.projectile import Projectile
from src.coffin import CoffinObject
from src.powerup import PowerUp
class Level: class Level:
def __init__(self, levelData, sounds): def __init__(self, levelData, sounds):
self.sounds = sounds self.sounds = sounds
self.objects = [] self.objects = []
self.enemies = [] self.enemies = []
self.bouncing_items = []
self.projectiles = [] # Track active projectiles
self.player = Player(levelData["player_start"]["x"], levelData["player_start"]["y"]) self.player = Player(levelData["player_start"]["x"], levelData["player_start"]["y"])
# Load objects and enemies from level data # Load objects and enemies from level data
@@ -39,18 +45,45 @@ class Level:
obj["sound"], obj["sound"],
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),
zombie_spawn_chance=obj.get("zombie_spawn_chance", 0)
) )
self.objects.append(gameObject) self.objects.append(gameObject)
def update_audio(self): def update_audio(self):
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
# Update regular objects # Update regular objects and check for zombie spawning
for obj in self.objects: for obj in self.objects:
if not obj.isActive: if not obj.isActive:
continue 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 not obj.isStatic:
if obj.channel is None or not obj.channel.get_busy(): if obj.channel is None or not obj.channel.get_busy():
obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos) obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos)
@@ -72,15 +105,43 @@ class Level:
enemy.channel = obj_play(self.sounds, enemy.enemyType, self.player.xPos, enemy.xPos) enemy.channel = obj_play(self.sounds, enemy.enemyType, self.player.xPos, enemy.xPos)
if enemy.channel is not None: if enemy.channel is not None:
enemy.channel = obj_update(enemy.channel, self.player.xPos, enemy.xPos) 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): def handle_combat(self, currentTime):
"""Handle combat interactions between player and enemies""" """Handle combat interactions between player and enemies"""
attackRange = self.player.get_attack_range(currentTime) attackRange = self.player.get_attack_range(currentTime)
if attackRange: if attackRange:
# Check for enemy hits
for enemy in self.enemies: for enemy in self.enemies:
if enemy.isActive and enemy.xPos >= attackRange[0] and enemy.xPos <= attackRange[1]: if enemy.isActive and enemy.xPos >= attackRange[0] and enemy.xPos <= attackRange[1]:
self.sounds[self.player.currentWeapon.hitSound].play() self.sounds[self.player.currentWeapon.hitSound].play()
enemy.take_damage(self.player.currentWeapon.damage) 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): def handle_collisions(self):
for obj in self.objects: for obj in self.objects:
@@ -99,3 +160,30 @@ class Level:
self.sounds[obj.soundName].play() self.sounds[obj.soundName].play()
speak("You fell in an open grave!") speak("You fell in an open grave!")
self.player.set_health(0) 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 * from libstormgames import *
class Object: 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] # x can be either a single position or a range [start, end]
self.xRange = [x, x] if isinstance(x, (int, float)) else x self.xRange = [x, x] if isinstance(x, (int, float)) else x
self.yPos = yPos self.yPos = yPos
@@ -9,6 +9,8 @@ class Object:
self.isStatic = isStatic self.isStatic = isStatic
self.isCollectible = isCollectible self.isCollectible = isCollectible
self.isHazard = isHazard 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.channel = None # For tracking the sound channel
self.isActive = True self.isActive = True
# For collectibles in a range, track which positions have been collected # For collectibles in a range, track which positions have been collected

View File

@@ -1,5 +1,4 @@
from src.weapon import Weapon from src.weapon import Weapon
class Player: class Player:
def __init__(self, xPos, yPos): def __init__(self, xPos, yPos):
# Movement attributes # Movement attributes
@@ -28,6 +27,14 @@ class Player:
self.isAttacking = False self.isAttacking = False
self.lastAttackTime = 0 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) # Initialize starting weapon (rusty shovel)
self.add_weapon(Weapon( self.add_weapon(Weapon(
name="rusty_shovel", name="rusty_shovel",
@@ -38,12 +45,58 @@ class Player:
attackDuration=200 # 200ms attack duration 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): def get_health(self):
"""Get current health""" """Get current health"""
return self._health return self._health
def set_health(self, value): def set_health(self, value):
"""Set health and handle death if needed""" """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 self._health = max(0, value) # Health can't go below 0
if self._health == 0: if self._health == 0:
self._lives -= 1 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.xPos += currentSpeed
player.facingRight = True 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 # 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):
self.sounds[player.currentWeapon.attackSound].play() self.sounds[player.currentWeapon.attackSound].play()
@@ -77,6 +83,9 @@ class WickedQuest:
if check_for_exit(): if check_for_exit():
return return
# Update player state (including power-ups)
self.currentLevel.player.update(currentTime)
self.handle_input() self.handle_input()
# Update audio positioning and handle collisions # Update audio positioning and handle collisions
@@ -86,6 +95,9 @@ class WickedQuest:
# Handle combat interactions # Handle combat interactions
self.currentLevel.handle_combat(currentTime) self.currentLevel.handle_combat(currentTime)
# Update projectiles
self.currentLevel.handle_projectiles(currentTime)
clock.tick(60) # 60 FPS clock.tick(60) # 60 FPS
# Player died or ran out of lives # Player died or ran out of lives