Allow for custom footsteps. Some work on footsteps to make the sound less crazy. Various other updates. Added initial documentation and credits files.

This commit is contained in:
Storm Dragon
2025-02-05 02:04:57 -05:00
parent 883dafaac0
commit c29bcd40b7
17 changed files with 431 additions and 43 deletions

4
files/credits.txt Normal file
View File

@@ -0,0 +1,4 @@
Billy Wolfe: Designer and coder.
https://social.wolfe.casa/storm
Source code is available at:
https://git.stormux.org/storm/wicked-quest

40
files/instructions.txt Normal file
View File

@@ -0,0 +1,40 @@
Welcome to Wicked Quest
For years, your bones have lain in disarray, scattered about the mausoleum where you were interred.
Some foolish mortal wandered in to your burial chamber and put you back together.
Now, you can wonder around spreading chaos and destruction where ever you go.
First, however, you have to make it past the dead so you can plague the living.
You will nead some kind of weapon to make it through.
You spot a grave digger's shovel and quickly grab it even though it's covered in rust.
You give an evil grin, oh wait, your a skeleton, your skull is always doing that.
Controls
a: move left
d: move right
w: jump
Control: attack
f: throw jack O'lantern
c: check bone dust
h check health
j: check jack O'lanterns
l: check lives remaining
Notes
Each 5 bone dust restores 1 health.
Each 100 bone dust gains an extra unlife.
Enemies
Goblin: Walks back and forth in his area trying to break your bones.
Ghoul: Same behavior as goblin, but if you enter his area he will actively chase you, staying close.
Pumpkin Catapult: Fires pumpkins at you in hopes of smashing you into bone dust.
Skull Storm: Screaming skulls rain down causing damage if they hit you.
Zombie: Slow moving creatures that have a chance to climb out of the open grave. They are slow moving but deadly. If they hit you you lose a life.
Bonuses and items
Bone dust: Currency of the game. Collect it to gain health and extra lives.
Hand of Glory: Grants invincibility for a short time.
Jack O'lantern: Throw these at your enemies to hit them from a distance.

View File

@@ -1,7 +1,7 @@
{ {
"level_id": 1, "level_id": 1,
"name": "The Mausoleum", "name": "The Mausoleum",
"description": "After years of existing as a pile of bones, someone was crazy enough to assemble your skeleton. Time to wreak some havoc! Use W to jump, A/D to move, and CTRL to attack with your shovel.", "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": { "player_start": {
"x": 0, "x": 0,
"y": 0 "y": 0
@@ -28,13 +28,15 @@
"static": true "static": true
}, },
{ {
"x": 25, "x_range": [25, 30],
"y": 0, "y": 0,
"enemy_type": "goblin", "enemy_type": "goblin",
"health": 3, "health": 3,
"damage": 1, "damage": 1,
"attack_range": 1, "attack_range": 1,
"movement_range": 5 "attack_pattern": {
"type": "patrol"
}
}, },
{ {
"x": 35, "x": 35,
@@ -64,13 +66,15 @@
"type": "coffin" "type": "coffin"
}, },
{ {
"x": 75, "x_range": [75, 80],
"y": 0, "y": 0,
"enemy_type": "goblin", "enemy_type": "goblin",
"health": 5, "health": 5,
"damage": 2, "damage": 2,
"attack_range": 1, "attack_range": 1,
"movement_range": 5 "attack_pattern": {
"type": "patrol"
}
}, },
{ {
"x": 85, "x": 85,
@@ -107,10 +111,18 @@
"direction": -1, "direction": -1,
"fire_interval": 5000, "fire_interval": 5000,
"range": 15 "range": 15
},
{
"x_range": [154, 159],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
} }
], ],
"boundaries": { "boundaries": {
"left": 0, "left": 0,
"right": 160 "right": 160
} },
"footstep_sound": "footstep_stone"
} }

View File

@@ -21,13 +21,15 @@
"static": true "static": true
}, },
{ {
"x": 25, "x_range": [21, 29],
"y": 0, "y": 0,
"enemy_type": "goblin", "enemy_type": "goblin",
"health": 4, "health": 4,
"damage": 2, "damage": 2,
"attack_range": 1, "attack_range": 1,
"movement_range": 5 "attack_pattern": {
"type": "patrol"
}
}, },
{ {
"x": 35, "x": 35,
@@ -62,13 +64,15 @@
"static": true "static": true
}, },
{ {
"x": 75, "x_range": [71, 79],
"y": 0, "y": 0,
"enemy_type": "goblin", "enemy_type": "goblin",
"health": 5, "health": 5,
"damage": 2, "damage": 2,
"attack_range": 1, "attack_range": 1,
"movement_range": 6 "attack_pattern": {
"type": "patrol"
}
}, },
{ {
"x": 85, "x": 85,
@@ -109,10 +113,18 @@
"min": 2, "min": 2,
"max": 5 "max": 5
} }
},
{
"x_range": [145, 150],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
} }
], ],
"boundaries": { "boundaries": {
"left": 0, "left": 0,
"right": 170 "right": 170
} },
"footstep_sound": "footstep_tall_grass"
} }

238
levels/3.json Normal file
View File

@@ -0,0 +1,238 @@
{
"level_id": 3,
"name": "Endless Graves",
"description": "Graves continue in all directions as far as you can see. The dead seem restless.",
"player_start": {
"x": 0,
"y": 0
},
"objects": [
{
"x_range": [1, 4],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 5,
"y": 3,
"sound": "coffin",
"type": "coffin"
},
{
"x_range": [6, 10],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 15,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 25
},
{
"x": 15,
"y": 3,
"sound": "coffin",
"type": "coffin"
},
{
"x_range": [25, 27],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [21, 31],
"y": 0,
"enemy_type": "goblin",
"health": 5,
"damage": 2,
"attack_range": 1,
"attack_pattern": {
"type": "patrol"
}
},
{
"x_range": [35, 50],
"y": 12,
"type": "skull_storm",
"damage": 3,
"maximum_skulls": 2,
"frequency": {
"min": 2,
"max": 5
}
},
{
"x_range": [42, 44],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 55,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 25
},
{
"x_range": [60, 70],
"y": 0,
"enemy_type": "goblin",
"health": 5,
"damage": 2,
"attack_range": 1,
"attack_pattern": {
"type": "patrol"
}
},
{
"x_range": [75, 77],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [71, 81],
"y": 0,
"enemy_type": "goblin",
"health": 5,
"damage": 2,
"attack_range": 1,
"attack_pattern": {
"type": "patrol"
}
},
{
"x": 85,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 28
},
{
"x": 85,
"y": 3,
"sound": "coffin",
"type": "coffin"
},
{
"x_range": [95, 120],
"y": 15,
"type": "skull_storm",
"damage": 3,
"maximum_skulls": 2,
"frequency": {
"min": 2,
"max": 4
}
},
{
"x_range": [105, 107],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [101, 111],
"y": 0,
"enemy_type": "goblin",
"health": 6,
"damage": 2,
"attack_range": 1,
"attack_pattern": {
"type": "patrol"
}
},
{
"x": 125,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 28
},
{
"x": 135,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 30
},
{
"x_range": [140, 150],
"y": 0,
"enemy_type": "goblin",
"health": 6,
"damage": 2,
"attack_range": 1,
"attack_pattern": {
"type": "patrol"
}
},
{
"x_range": [155, 157],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [146, 166],
"y": 0,
"enemy_type": "ghoul",
"health": 10,
"damage": 3,
"attack_range": 2,
"attack_pattern": {
"type": "hunter",
"turn_threshold": 5
}
},
{
"x_range": [165, 190],
"y": 15,
"type": "skull_storm",
"damage": 4,
"maximum_skulls": 3,
"frequency": {
"min": 2,
"max": 4
}
},
{
"x": 175,
"y": 0,
"type": "catapult",
"direction": -1,
"fire_interval": 4500,
"range": 20
},
{
"x_range": [173, 176],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
}
],
"boundaries": {
"left": 0,
"right": 200
},
"footstep_sound": "footstep_tall_grass"
}

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
sounds/zombie.ogg (Stored with Git LFS)

Binary file not shown.

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

Binary file not shown.

View File

@@ -23,7 +23,6 @@ class CoffinObject(Object):
self.is_broken = True self.is_broken = True
play_sound(self.sounds['coffin_shatter']) play_sound(self.sounds['coffin_shatter'])
self.level.player.stats.update_stat('Coffins broken', 1) self.level.player.stats.update_stat('Coffins broken', 1)
self.level.player.stats.update_stat('Coffins remaining', -1)
# Stop the ongoing coffin sound # Stop the ongoing coffin sound
if self.channel: if self.channel:

View File

@@ -2,7 +2,6 @@ from libstormgames import *
from src.object import Object from src.object import Object
import pygame import pygame
class Enemy(Object): class Enemy(Object):
def __init__(self, xRange, y, enemyType, sounds, level, **kwargs): def __init__(self, xRange, y, enemyType, sounds, level, **kwargs):
# Initialize base object properties # Initialize base object properties
@@ -20,16 +19,20 @@ class Enemy(Object):
self.health = kwargs.get('health', 5) # Default 5 HP self.health = kwargs.get('health', 5) # Default 5 HP
self.damage = kwargs.get('damage', 1) # Default 1 damage self.damage = kwargs.get('damage', 1) # Default 1 damage
self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range 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 self.sounds = sounds # Store reference to game sounds
# Movement and behavior properties # Movement and behavior properties
self.movingRight = True # Initial direction self.movingRight = True # Initial direction
self.movementSpeed = 0.03 # Base speed self.movementSpeed = 0.03 # Base speed
self.patrolStart = self.xRange[0] self.patrolStart = self.xRange[0] # Use xRange directly for patrol boundaries
self.patrolEnd = self.xRange[0] + self.movementRange self.patrolEnd = self.xRange[1]
self.lastAttackTime = 0 self.lastAttackTime = 0
self.attackCooldown = 1000 # 1 second between attacks self.attackCooldown = 1000 # 1 second between attacks
self._currentX = self.xRange[0] # Initialize current position
# Attack pattern configuration
self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'})
self.turnThreshold = self.attackPattern.get('turn_threshold', 5)
# Enemy type specific adjustments # Enemy type specific adjustments
if enemyType == "zombie": if enemyType == "zombie":
@@ -41,21 +44,69 @@ class Enemy(Object):
@property @property
def xPos(self): def xPos(self):
"""Current x position""" """Current x position"""
return self._currentX if hasattr(self, '_currentX') else self.xRange[0] return self._currentX
@xPos.setter @xPos.setter
def xPos(self, value): def xPos(self, value):
"""Set current x position""" """Set current x position"""
self._currentX = value self._currentX = value
def update_movement(self, player):
"""Update enemy movement based on attack pattern"""
if self.attackPattern['type'] == 'hunter':
# Calculate distance to player
distanceToPlayer = player.xPos - self.xPos
if abs(distanceToPlayer) <= (self.patrolEnd - self.patrolStart): # Use full range
# Player is within movement range
if self.movingRight:
# Moving right
if distanceToPlayer < -self.turnThreshold:
# Player is too far behind us, turn around
self.movingRight = False
else:
self.xPos += self.movementSpeed
else:
# Moving left
if distanceToPlayer > self.turnThreshold:
# Player is too far ahead of us, turn around
self.movingRight = True
else:
self.xPos -= self.movementSpeed
# Ensure we stay within our range
if self.xPos <= self.patrolStart:
self.xPos = self.patrolStart
self.movingRight = True
elif self.xPos >= self.patrolEnd:
self.xPos = self.patrolEnd
self.movingRight = False
else:
# Player out of range, return to normal patrol
self.patrol_movement()
else:
# Default patrol behavior
self.patrol_movement()
def patrol_movement(self):
"""Standard back-and-forth patrol movement"""
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
def update(self, currentTime, player): def update(self, currentTime, player):
"""Update enemy position and handle attacks""" """Update enemy position and handle attacks"""
if not self.isActive or self.health <= 0: if not self.isActive or self.health <= 0:
return return
# Zombie behavior - always chase player # Handle movement based on enemy type
if self.enemyType == "zombie": if self.enemyType == "zombie":
# Determine direction to player # Zombies always chase player
if player.xPos > self.xPos: if player.xPos > self.xPos:
self.movingRight = True self.movingRight = True
self.xPos += self.movementSpeed self.xPos += self.movementSpeed
@@ -63,20 +114,13 @@ class Enemy(Object):
self.movingRight = False self.movingRight = False
self.xPos -= self.movementSpeed self.xPos -= self.movementSpeed
else: else:
# Normal patrol behavior for other enemies # Other enemies use their attack pattern
if self.movingRight: self.update_movement(player)
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 # Check for attack opportunity
if self.can_attack(currentTime, player): if self.can_attack(currentTime, player):
self.attack(currentTime, player) self.attack(currentTime, player)
def can_attack(self, currentTime, player): def can_attack(self, currentTime, player):
"""Check if enemy can attack player""" """Check if enemy can attack player"""
# Must have cooled down from last attack # Must have cooled down from last attack
@@ -128,4 +172,3 @@ class Enemy(Object):
# Update stats # Update stats
self.level.player.stats.update_stat('Enemies killed', 1) self.level.player.stats.update_stat('Enemies killed', 1)
self.level.player.stats.update_stat('Enemies remaining', -1)

View File

@@ -25,6 +25,12 @@ class Level:
self.rightBoundary = levelData["boundaries"]["right"] self.rightBoundary = levelData["boundaries"]["right"]
self.levelId = levelData["level_id"] self.levelId = levelData["level_id"]
# Get footstep sound for this level, default to 'footstep' if not specified
self.footstepSound = levelData.get("footstep_sound", "footstep")
# Pass footstep sound to player
self.player.set_footstep_sound(self.footstepSound)
# Create end of level object at right boundary # Create end of level object at right boundary
endLevel = Object( endLevel = Object(
self.rightBoundary, self.rightBoundary,

View File

@@ -23,6 +23,14 @@ class Player:
self.distanceSinceLastStep = 0 self.distanceSinceLastStep = 0
self.stepDistance = 0.5 self.stepDistance = 0.5
self.stats = StatTracker() self.stats = StatTracker()
self.sounds = sounds
# Footstep tracking
self.distanceSinceLastStep = 0
self.stepDistance = 0.8
self.lastStepTime = 0
self.minStepInterval = 250 # Minimum milliseconds between steps
self.footstepSound = "footstep"
# Inventory system # Inventory system
self.inventory = [] self.inventory = []
@@ -51,6 +59,11 @@ class Player:
attackDuration=200 # 200ms attack duration attackDuration=200 # 200ms attack duration
)) ))
def should_play_footstep(self, currentTime):
"""Check if it's time to play a footstep sound"""
return (self.distanceSinceLastStep >= self.stepDistance and
currentTime - self.lastStepTime >= self.minStepInterval)
def update(self, currentTime): def update(self, currentTime):
"""Update player state""" """Update player state"""
# Check if invincibility has expired # Check if invincibility has expired
@@ -91,6 +104,10 @@ class Player:
"""Get current max health""" """Get current max health"""
return self._maxHealth return self._maxHealth
def set_footstep_sound(self, soundName):
"""Set the current footstep sound"""
self.footstepSound = soundName
def set_health(self, value): def set_health(self, value):
"""Set health and handle death if needed.""" """Set health and handle death if needed."""
if self.isInvincible: if self.isInvincible:

View File

@@ -4,9 +4,7 @@ class StatTracker:
self.total = { self.total = {
'Bone dust': 0, 'Bone dust': 0,
'Enemies killed': 0, 'Enemies killed': 0,
'Enemies remaining': 0,
'Coffins broken': 0, 'Coffins broken': 0,
'Coffins remaining': 0,
'Items collected': 0, 'Items collected': 0,
'Total time': 0 'Total time': 0
} }

View File

@@ -65,6 +65,14 @@ class WickedQuest:
player.xPos += currentSpeed player.xPos += currentSpeed
player.facingRight = True player.facingRight = True
# Handle footsteps
if movementDistance > 0 and not player.isJumping:
player.distanceSinceLastStep += movementDistance
if player.should_play_footstep(currentTime):
play_sound(self.sounds[player.footstepSound])
player.distanceSinceLastStep = 0
player.lastStepTime = currentTime
# Status queries # Status queries
if keys[pygame.K_c]: if keys[pygame.K_c]:
speak(f"{player.get_coins()} gbone dust") speak(f"{player.get_coins()} gbone dust")
@@ -84,13 +92,6 @@ class WickedQuest:
if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime): if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime):
play_sound(self.sounds[player.currentWeapon.attackSound]) play_sound(self.sounds[player.currentWeapon.attackSound])
# Play footstep sounds if moving and not jumping
if movementDistance > 0 and not player.isJumping:
player.distanceSinceLastStep += movementDistance
if player.distanceSinceLastStep >= player.stepDistance:
play_sound(self.sounds['footstep'])
player.distanceSinceLastStep = 0
# Handle jumping # Handle jumping
if keys[pygame.K_w] and not player.isJumping: if keys[pygame.K_w] and not player.isJumping:
player.isJumping = True player.isJumping = True
@@ -100,9 +101,10 @@ class WickedQuest:
# Check if jump should end # Check if jump should end
if player.isJumping and currentTime - player.jumpStartTime >= player.jumpDuration: if player.isJumping and currentTime - player.jumpStartTime >= player.jumpDuration:
player.isJumping = False player.isJumping = False
play_sound(self.sounds['footstep']) play_sound(self.sounds[player.footstepSound]) # Landing sound
# Reset step distance tracking after landing # Reset step distance tracking after landing
player.distanceSinceLastStep = 0 player.distanceSinceLastStep = 0
player.lastStepTime = currentTime
def display_level_stats(self, timeTaken): def display_level_stats(self, timeTaken):
"""Display level completion statistics.""" """Display level completion statistics."""
@@ -214,6 +216,8 @@ class WickedQuest:
self.player = None # Reset player for new game self.player = None # Reset player for new game
if self.load_level(1): if self.load_level(1):
self.game_loop() self.game_loop()
elif choice == "learn_sounds":
choice = learn_sounds(self.sounds)
if __name__ == "__main__": if __name__ == "__main__":