Code cleanup, added more functionality. Floating coffins that spawn items, graves can spawn zombies, etc.
This commit is contained in:
		
							
								
								
									
										40
									
								
								src/coffin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/coffin.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										41
									
								
								src/enemy.py
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								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() | ||||
|   | ||||
							
								
								
									
										89
									
								
								src/item.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/item.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										92
									
								
								src/level.py
									
									
									
									
									
								
							
							
						
						
									
										92
									
								
								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() | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										52
									
								
								src/powerup.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										29
									
								
								src/projectile.py
									
									
									
									
									
										Normal 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 | ||||
		Reference in New Issue
	
	Block a user