From 6c000d78d8330a4628931ba0f36858d2e21b6221 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 30 Jan 2025 22:22:59 -0500 Subject: [PATCH] Code cleanup, added more functionality. Floating coffins that spawn items, graves can spawn zombies, etc. --- levels/1.json | 3 +- src/coffin.py | 40 +++++++++++++++++++++ src/enemy.py | 41 +++++++++++++++------ src/item.py | 89 +++++++++++++++++++++++++++++++++++++++++++++ src/level.py | 92 +++++++++++++++++++++++++++++++++++++++++++++-- src/object.py | 4 ++- src/player.py | 55 +++++++++++++++++++++++++++- src/powerup.py | 52 +++++++++++++++++++++++++++ src/projectile.py | 29 +++++++++++++++ wicked_quest.py | 12 +++++++ 10 files changed, 402 insertions(+), 15 deletions(-) create mode 100644 src/coffin.py create mode 100644 src/item.py create mode 100644 src/powerup.py create mode 100644 src/projectile.py diff --git a/levels/1.json b/levels/1.json index 1e40bda..08005c6 100644 --- a/levels/1.json +++ b/levels/1.json @@ -28,7 +28,8 @@ "y": 0, "hazard": true, "sound": "grave", - "static": true + "static": true, + "zombie_spawn_chance": 100 } ], "boundaries": { diff --git a/src/coffin.py b/src/coffin.py new file mode 100644 index 0000000..452327e --- /dev/null +++ b/src/coffin.py @@ -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 diff --git a/src/enemy.py b/src/enemy.py index b67914a..c79ed7a 100644 --- a/src/enemy.py +++ b/src/enemy.py @@ -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,16 +52,26 @@ class Enemy(Object): if not self.isActive or self.health <= 0: return - # Update position based on patrol behavior - 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: + # 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: + self.movingRight = False + else: + self.xPos -= self.movementSpeed + if self.xPos <= self.patrolStart: + self.movingRight = True + # Check for attack opportunity if self.can_attack(currentTime, player): self.attack(currentTime, player) @@ -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() diff --git a/src/item.py b/src/item.py new file mode 100644 index 0000000..a01ba94 --- /dev/null +++ b/src/item.py @@ -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 diff --git a/src/level.py b/src/level.py index 2a3544f..8133378 100644 --- a/src/level.py +++ b/src/level.py @@ -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) @@ -72,15 +105,43 @@ class Level: enemy.channel = obj_play(self.sounds, enemy.enemyType, self.player.xPos, enemy.xPos) 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: @@ -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() diff --git a/src/object.py b/src/object.py index 7a8e91e..4e3c54b 100644 --- a/src/object.py +++ b/src/object.py @@ -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 diff --git a/src/player.py b/src/player.py index 4302ebe..db3fab5 100644 --- a/src/player.py +++ b/src/player.py @@ -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 diff --git a/src/powerup.py b/src/powerup.py new file mode 100644 index 0000000..f9cf272 --- /dev/null +++ b/src/powerup.py @@ -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 diff --git a/src/projectile.py b/src/projectile.py new file mode 100644 index 0000000..5f4105a --- /dev/null +++ b/src/projectile.py @@ -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 diff --git a/wicked_quest.py b/wicked_quest.py index 9902f83..91a4452 100644 --- a/wicked_quest.py +++ b/wicked_quest.py @@ -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