More content added, weapons, coin collecting, basic combat.

This commit is contained in:
Storm Dragon
2025-01-30 19:11:33 -05:00
parent 0d115b2bef
commit 53009373c2
10 changed files with 344 additions and 63 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
filestructure.txt
__pycache__/
*\.pyc
sounds/rec.sh

View File

@@ -1,14 +1,14 @@
{
"level_id": 1,
"name": "The Mausoleum",
"description": "After years of existing as a pile of bones, someone was crazy enough to assemble your skeleton. Time to reak some havoc!",
"description": "After years of existing as a pile of bones, someone was crazy enough to assemble your skeleton. Time to wreak some havoc!",
"player_start": {
"x": 0,
"y": 0
},
"objects": [
{
"x": 5,
"x_range": [5, 7],
"y": 3,
"sound": "coin",
"collectible": true,
@@ -17,9 +17,11 @@
{
"x": 25,
"y": 0,
"sound": "goblin",
"hazard": true,
"static": false
"enemy_type": "goblin",
"health": 5,
"damage": 1,
"attack_range": 1,
"movement_range": 5
},
{
"x": 50,

BIN
sounds/player_shovel_attack.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
sounds/player_shovel_hit.ogg (Stored with Git LFS) Normal file

Binary file not shown.

105
src/enemy.py Normal file
View 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

View File

@@ -1,17 +1,40 @@
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"]:
# 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(
obj["x"],
xPos,
obj["y"],
obj["sound"],
isStatic=obj.get("static", True),
@@ -21,56 +44,58 @@ class Level:
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
elif obj.isHazard and not self.player.isJumping:
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
speak("You fell in an open grave!")
self.player.set_health(0)

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -27,34 +27,36 @@ class WickedQuest:
def handle_input(self):
keys = pygame.key.get_pressed()
player = self.currentLevel.player
currentTime = pygame.time.get_ticks()
# Calculate current speed based on jumping state
currentSpeed = player.moveSpeed * 1.5 if player.isJumping else player.moveSpeed
# Track movement distance for this frame
movement = 0
movementDistance = 0
# Horizontal movement
if keys[pygame.K_a]: # Left
movement = currentSpeed
movementDistance = currentSpeed
player.xPos -= currentSpeed
player.facingRight = False
elif keys[pygame.K_d]: # Right
movement = currentSpeed
movementDistance = currentSpeed
player.xPos += currentSpeed
player.facingRight = True
# 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()
# Play footstep sounds if moving and not jumping
if movement > 0 and not player.isJumping:
player.distanceSinceLastStep += movement
if movementDistance > 0 and not player.isJumping:
player.distanceSinceLastStep += movementDistance
if player.distanceSinceLastStep >= player.stepDistance:
self.sounds['footstep'].play()
player.distanceSinceLastStep = 0
# Handle jumping
currentTime = pygame.time.get_ticks()
# Start jump
if keys[pygame.K_w] and not player.isJumping:
player.isJumping = True
player.jumpStartTime = currentTime
@@ -69,7 +71,9 @@ class WickedQuest:
def game_loop(self):
clock = pygame.time.Clock()
while True:
while self.currentLevel.player.get_health() > 0 and self.currentLevel.player.get_lives() > 0:
currentTime = pygame.time.get_ticks()
if check_for_exit():
return
@@ -79,13 +83,21 @@ class WickedQuest:
self.currentLevel.update_audio()
self.currentLevel.handle_collisions()
# Handle combat interactions
self.currentLevel.handle_combat(currentTime)
clock.tick(60) # 60 FPS
# Player died or ran out of lives
speak("Game Over")
def run(self):
while True:
choice = game_menu(self.sounds, "play", "instructions", "learn_sounds", "credits", "donate")
choice = game_menu(self.sounds, "play", "instructions", "learn_sounds", "credits", "donate", "exit")
if choice == "play":
if choice == "exit":
exit_game()
elif choice == "play":
if self.load_level(1):
self.game_loop()