More content added, weapons, coin collecting, basic combat.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
filestructure.txt
|
||||
__pycache__/
|
||||
*\.pyc
|
||||
sounds/rec.sh
|
||||
|
@@ -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
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
BIN
sounds/player_shovel_hit.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
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
|
91
src/level.py
91
src/level.py
@@ -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)
|
||||
|
@@ -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
|
@@ -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()
|
||||
|
||||
|
Reference in New Issue
Block a user