More content added, weapons, coin collecting, basic combat.
This commit is contained in:
		
							
								
								
									
										105
									
								
								src/enemy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/enemy.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| from libstormgames import * | ||||
| from src.object import Object | ||||
| import pygame | ||||
|  | ||||
|  | ||||
| class Enemy(Object): | ||||
|     def __init__(self, xRange, y, enemyType, sounds, **kwargs): | ||||
|         # Initialize base object properties | ||||
|         super().__init__( | ||||
|             xRange,  | ||||
|             y,  | ||||
|             f"{enemyType}",  # Base sound for ambient noise | ||||
|             isStatic=False, | ||||
|             isHazard=True | ||||
|         ) | ||||
|          | ||||
|         # Enemy specific properties | ||||
|         self.enemyType = enemyType | ||||
|         self.health = kwargs.get('health', 5)  # Default 5 HP | ||||
|         self.damage = kwargs.get('damage', 1)  # Default 1 damage | ||||
|         self.attackRange = kwargs.get('attack_range', 1)  # Default 1 tile range | ||||
|         self.movementRange = kwargs.get('movement_range', 5)  # Default 5 tile patrol | ||||
|         self.sounds = sounds  # Store reference to game sounds | ||||
|          | ||||
|         # Movement and behavior properties | ||||
|         self.movingRight = True  # Initial direction | ||||
|         self.movementSpeed = 0.03  # Slightly slower than player | ||||
|         self.patrolStart = self.xRange[0] | ||||
|         self.patrolEnd = self.xRange[0] + self.movementRange | ||||
|         self.lastAttackTime = 0 | ||||
|         self.attackCooldown = 1000  # 1 second between attacks | ||||
|          | ||||
|     @property | ||||
|     def xPos(self): | ||||
|         """Current x position""" | ||||
|         return self._currentX if hasattr(self, '_currentX') else self.xRange[0] | ||||
|          | ||||
|     @xPos.setter | ||||
|     def xPos(self, value): | ||||
|         """Set current x position""" | ||||
|         self._currentX = value | ||||
|          | ||||
|     def update(self, currentTime, player): | ||||
|         """Update enemy position and handle attacks""" | ||||
|         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: | ||||
|                 self.movingRight = True | ||||
|                  | ||||
|         # Check for attack opportunity | ||||
|         if self.can_attack(currentTime, player): | ||||
|             self.attack(currentTime, player) | ||||
|              | ||||
|     def can_attack(self, currentTime, player): | ||||
|         """Check if enemy can attack player""" | ||||
|         # Must have cooled down from last attack | ||||
|         if currentTime - self.lastAttackTime < self.attackCooldown: | ||||
|             return False | ||||
|              | ||||
|         # Don't attack if player is jumping | ||||
|         if player.isJumping: | ||||
|             return False | ||||
|              | ||||
|         # Check if player is in range and on same side we're facing | ||||
|         distance = abs(player.xPos - self.xPos) | ||||
|         tolerance = 0.5  # Same tolerance as we used for the grave | ||||
|          | ||||
|         if distance <= (self.attackRange + tolerance): | ||||
|             # Only attack if we're facing the right way | ||||
|             playerOnRight = player.xPos > self.xPos | ||||
|             return playerOnRight == self.movingRight | ||||
|              | ||||
|         return False | ||||
|          | ||||
|     def attack(self, currentTime, player): | ||||
|         """Perform attack on player""" | ||||
|         self.lastAttackTime = currentTime | ||||
|         # Play attack sound | ||||
|         attackSound = f"{self.enemyType}_attack" | ||||
|         if attackSound in self.sounds: | ||||
|             self.sounds[attackSound].play() | ||||
|         # Deal damage to player | ||||
|         player.set_health(player.get_health() - self.damage) | ||||
|         speak(f"The {self.enemyType} hits you!") | ||||
|          | ||||
|     def take_damage(self, amount): | ||||
|         """Handle enemy taking damage""" | ||||
|         self.health -= amount | ||||
|         if self.health <= 0: | ||||
|             self.die() | ||||
|              | ||||
|     def die(self): | ||||
|         """Handle enemy death""" | ||||
|         self.isActive = False | ||||
|         if self.channel: | ||||
|             obj_stop(self.channel) | ||||
|             self.channel = None | ||||
							
								
								
									
										109
									
								
								src/level.py
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								src/level.py
									
									
									
									
									
								
							| @@ -1,76 +1,101 @@ | ||||
| import pygame | ||||
| from libstormgames import * | ||||
| from src.player import Player | ||||
| from src.enemy import Enemy | ||||
| from src.object import Object | ||||
| from src.player import Player | ||||
|  | ||||
| class Level: | ||||
|     def __init__(self, levelData, sounds): | ||||
|         self.sounds = sounds | ||||
|         self.objects = [] | ||||
|         self.enemies = [] | ||||
|         self.player = Player(levelData["player_start"]["x"], levelData["player_start"]["y"]) | ||||
|          | ||||
|         # Load objects from level data | ||||
|         # Load objects and enemies from level data | ||||
|         for obj in levelData["objects"]: | ||||
|             gameObject = Object( | ||||
|                 obj["x"], | ||||
|                 obj["y"], | ||||
|                 obj["sound"], | ||||
|                 isStatic=obj.get("static", True), | ||||
|                 isCollectible=obj.get("collectible", False), | ||||
|                 isHazard=obj.get("hazard", False) | ||||
|             ) | ||||
|             self.objects.append(gameObject) | ||||
|             # Handle x position or range | ||||
|             if "x_range" in obj: | ||||
|                 xPos = obj["x_range"] | ||||
|             else: | ||||
|                 xPos = [obj["x"], obj["x"]]  # Single position as range | ||||
|                  | ||||
|             # Check if this is an enemy | ||||
|             if "enemy_type" in obj: | ||||
|                 enemy = Enemy( | ||||
|                     xPos, | ||||
|                     obj["y"], | ||||
|                     obj["enemy_type"], | ||||
|                     self.sounds, | ||||
|                     health=obj.get("health", 5), | ||||
|                     damage=obj.get("damage", 1), | ||||
|                     attack_range=obj.get("attack_range", 1), | ||||
|                     movement_range=obj.get("movement_range", 5) | ||||
|                 ) | ||||
|                 self.enemies.append(enemy) | ||||
|             else: | ||||
|                 gameObject = Object( | ||||
|                     xPos, | ||||
|                     obj["y"], | ||||
|                     obj["sound"], | ||||
|                     isStatic=obj.get("static", True), | ||||
|                     isCollectible=obj.get("collectible", False), | ||||
|                     isHazard=obj.get("hazard", False) | ||||
|                 ) | ||||
|                 self.objects.append(gameObject) | ||||
|  | ||||
|     def update_audio(self): | ||||
|         # Update all object sounds based on player position | ||||
|         currentTime = pygame.time.get_ticks() | ||||
|          | ||||
|         # Update regular objects | ||||
|         for obj in self.objects: | ||||
|             if not obj.isActive: | ||||
|                 continue | ||||
|                  | ||||
|             # For non-static objects like goblins, ensure continuous sound | ||||
|             if not obj.isStatic: | ||||
|                 # If channel doesn't exist or sound stopped playing, restart it | ||||
|                 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: | ||||
|                 # Original logic for static objects | ||||
|                 if obj.channel is None: | ||||
|                     obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos) | ||||
|                      | ||||
|             # Update position-based audio for all objects | ||||
|             if obj.channel is not None: | ||||
|                 obj.channel = obj_update(obj.channel, self.player.xPos, obj.xPos) | ||||
|                  | ||||
|         # Update enemies | ||||
|         for enemy in self.enemies: | ||||
|             if not enemy.isActive: | ||||
|                 continue | ||||
|                  | ||||
|             enemy.update(currentTime, self.player) | ||||
|              | ||||
|             if enemy.channel is None or not enemy.channel.get_busy(): | ||||
|                 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) | ||||
|  | ||||
|     def handle_combat(self, currentTime): | ||||
|         """Handle combat interactions between player and enemies""" | ||||
|         attackRange = self.player.get_attack_range(currentTime) | ||||
|         if attackRange: | ||||
|             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) | ||||
|  | ||||
|     def handle_collisions(self): | ||||
|         for obj in self.objects: | ||||
|             if not obj.isActive: | ||||
|                 continue | ||||
|              | ||||
|             # Single-tile collision detection (more precise) | ||||
|             collision_threshold = 0.5  # Half a tile width from center | ||||
|             distance = abs(self.player.xPos - obj.xPos) | ||||
|          | ||||
|             if distance <= collision_threshold: | ||||
|                 if obj.isCollectible: | ||||
|                     # Coins must be collected while jumping | ||||
|                     if self.player.isJumping: | ||||
|             if obj.is_in_range(self.player.xPos): | ||||
|                 if obj.isCollectible and self.player.isJumping: | ||||
|                     currentPos = round(self.player.xPos) | ||||
|                     if currentPos not in obj.collectedPositions: | ||||
|                         self.sounds[f'get_{obj.soundName}'].play() | ||||
|                         speak(f"Collected {obj.soundName}") | ||||
|                         obj.isActive = False | ||||
|                         obj_stop(obj.channel) | ||||
|                         obj.collect_at_position(currentPos) | ||||
|                         self.player.collectedItems.append(obj.soundName) | ||||
|                 elif obj.isHazard: | ||||
|                     # Only affect player if they're not jumping (for ground-based hazards) | ||||
|                     if not self.player.isJumping: | ||||
|                         if obj.soundName == "grave": | ||||
|                             self.sounds['grave'].play() | ||||
|                             speak("You fell into a pit!") | ||||
|                         elif obj.soundName == "goblin": | ||||
|                             self.sounds['goblin'].play() | ||||
|                             speak("A goblin got you!") | ||||
|                         else:  # Other hazards | ||||
|                             self.sounds[obj.soundName].play() | ||||
|                             speak(f"A {obj.soundName} got you!") | ||||
|                          | ||||
|                         # Apply knockback for any hazard hit | ||||
|                         knockback = 2 | ||||
|                         self.player.xPos = self.player.xPos - knockback if self.player.facingRight else self.player.xPos + knockback | ||||
|                 elif obj.isHazard and not self.player.isJumping: | ||||
|                     self.sounds[obj.soundName].play() | ||||
|                     speak("You fell in an open grave!") | ||||
|                     self.player.set_health(0) | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| from libstormgames import * | ||||
|  | ||||
| class Object: | ||||
|     def __init__(self, xPos, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False): | ||||
|         self.xPos = xPos | ||||
|     def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False): | ||||
|         # 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 | ||||
|         self.soundName = soundName | ||||
|         self.isStatic = isStatic | ||||
| @@ -8,3 +11,25 @@ class Object: | ||||
|         self.isHazard = isHazard | ||||
|         self.channel = None  # For tracking the sound channel | ||||
|         self.isActive = True | ||||
|         # For collectibles in a range, track which positions have been collected | ||||
|         self.collectedPositions = set() | ||||
|          | ||||
|     @property | ||||
|     def xPos(self): | ||||
|         """Return center of range for audio positioning""" | ||||
|         return (self.xRange[0] + self.xRange[1]) / 2 | ||||
|      | ||||
|     def is_in_range(self, x): | ||||
|         """Check if a given x position is within this object's range""" | ||||
|         tolerance = 0.5  # Half a unit tolerance | ||||
|         return (self.xRange[0] - tolerance) <= x <= (self.xRange[1] + tolerance) | ||||
|      | ||||
|     def collect_at_position(self, x): | ||||
|         """Mark a specific position in the range as collected""" | ||||
|         self.collectedPositions.add(x) | ||||
|         # If all positions in range are collected, deactivate the object and stop sound | ||||
|         if len(self.collectedPositions) == int(self.xRange[1] - self.xRange[0] + 1): | ||||
|             self.isActive = False | ||||
|             if self.channel: | ||||
|                 obj_stop(self.channel) | ||||
|                 self.channel = None | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| from src.weapon import Weapon | ||||
|  | ||||
| class Player: | ||||
|     def __init__(self, xPos, yPos): | ||||
|         # Movement attributes | ||||
|         self.xPos = xPos | ||||
|         self.yPos = yPos | ||||
|         self.moveSpeed = 0.05 | ||||
| @@ -7,7 +10,76 @@ class Player: | ||||
|         self.jumpStartTime = 0 | ||||
|         self.isJumping = False | ||||
|         self.facingRight = True | ||||
|         self.collectedItems = [] | ||||
|         # Track distance for footsteps | ||||
|          | ||||
|         # Stats and tracking | ||||
|         self._health = 10 | ||||
|         self._lives = 1 | ||||
|         self.distanceSinceLastStep = 0 | ||||
|         self.stepDistance = 0.5  # Play a step sound every 0.5 units moved | ||||
|         self.stepDistance = 0.5 | ||||
|          | ||||
|         # Inventory system | ||||
|         self.inventory = [] | ||||
|         self.collectedItems = [] | ||||
|         self.coins = 0 | ||||
|          | ||||
|         # Combat related attributes | ||||
|         self.weapons = [] | ||||
|         self.currentWeapon = None | ||||
|         self.isAttacking = False | ||||
|         self.lastAttackTime = 0 | ||||
|          | ||||
|         # Initialize starting weapon (rusty shovel) | ||||
|         self.add_weapon(Weapon( | ||||
|             name="rusty_shovel", | ||||
|             damage=2, | ||||
|             range=2, | ||||
|             attackSound="player_shovel_attack", | ||||
|             hitSound="player_shovel_hit", | ||||
|             attackDuration=200  # 200ms attack duration | ||||
|         )) | ||||
|          | ||||
|     def get_health(self): | ||||
|         """Get current health""" | ||||
|         return self._health | ||||
|          | ||||
|     def set_health(self, value): | ||||
|         """Set health and handle death if needed""" | ||||
|         self._health = max(0, value)  # Health can't go below 0 | ||||
|         if self._health == 0: | ||||
|             self._lives -= 1 | ||||
|             if self._lives > 0: | ||||
|                 self._health = 10  # Reset health if we still have lives | ||||
|                  | ||||
|     def get_lives(self): | ||||
|         """Get remaining lives""" | ||||
|         return self._lives | ||||
|              | ||||
|     def add_weapon(self, weapon): | ||||
|         """Add a new weapon to inventory and equip if first weapon""" | ||||
|         self.weapons.append(weapon) | ||||
|         if len(self.weapons) == 1:  # If this is our first weapon, equip it | ||||
|             self.equip_weapon(weapon) | ||||
|              | ||||
|     def equip_weapon(self, weapon): | ||||
|         """Equip a specific weapon""" | ||||
|         if weapon in self.weapons: | ||||
|             self.currentWeapon = weapon | ||||
|              | ||||
|     def add_item(self, item): | ||||
|         """Add an item to inventory""" | ||||
|         self.inventory.append(item) | ||||
|         self.collectedItems.append(item) | ||||
|          | ||||
|     def start_attack(self, currentTime): | ||||
|         """Attempt to start an attack with the current weapon""" | ||||
|         if self.currentWeapon and self.currentWeapon.start_attack(currentTime): | ||||
|             self.isAttacking = True | ||||
|             self.lastAttackTime = currentTime | ||||
|             return True | ||||
|         return False | ||||
|              | ||||
|     def get_attack_range(self, currentTime): | ||||
|         """Get the current attack's range based on position and facing direction""" | ||||
|         if not self.currentWeapon or not self.currentWeapon.is_attack_active(currentTime): | ||||
|             return None | ||||
|         return self.currentWeapon.get_attack_range(self.xPos, self.facingRight) | ||||
|   | ||||
							
								
								
									
										33
									
								
								src/weapon.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/weapon.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| class Weapon: | ||||
|     def __init__(self, name, damage, range, attackSound, hitSound, cooldown=500, attackDuration=200): | ||||
|         self.name = name | ||||
|         self.damage = damage | ||||
|         self.range = range  # Range in tiles | ||||
|         self.attackSound = attackSound | ||||
|         self.hitSound = hitSound | ||||
|         self.cooldown = cooldown  # Milliseconds between attacks | ||||
|         self.attackDuration = attackDuration  # Milliseconds the attack is active | ||||
|         self.lastAttackTime = 0 | ||||
|          | ||||
|     def can_attack(self, currentTime): | ||||
|         """Check if enough time has passed since last attack""" | ||||
|         return currentTime - self.lastAttackTime >= self.cooldown | ||||
|          | ||||
|     def get_attack_range(self, playerPos, facingRight): | ||||
|         """Calculate the area that this attack would hit""" | ||||
|         if facingRight: | ||||
|             return (playerPos, playerPos + self.range) | ||||
|         else: | ||||
|             return (playerPos - self.range, playerPos) | ||||
|  | ||||
|     def start_attack(self, currentTime): | ||||
|         """Begin an attack and return True if allowed""" | ||||
|         if self.can_attack(currentTime): | ||||
|             self.lastAttackTime = currentTime | ||||
|             return True | ||||
|         return False | ||||
|          | ||||
|     def is_attack_active(self, currentTime): | ||||
|         """Check if the attack is still in its active frames""" | ||||
|         timeSinceAttack = currentTime - self.lastAttackTime | ||||
|         return 0 <= timeSinceAttack <= self.attackDuration | ||||
		Reference in New Issue
	
	Block a user