diff --git a/files/credits.txt b/files/credits.txt new file mode 100644 index 0000000..7743d5b --- /dev/null +++ b/files/credits.txt @@ -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 diff --git a/files/instructions.txt b/files/instructions.txt new file mode 100644 index 0000000..3e6739e --- /dev/null +++ b/files/instructions.txt @@ -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. diff --git a/levels/1.json b/levels/1.json index 9354900..4838f3e 100644 --- a/levels/1.json +++ b/levels/1.json @@ -1,7 +1,7 @@ { "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 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": { "x": 0, "y": 0 @@ -28,13 +28,15 @@ "static": true }, { - "x": 25, + "x_range": [25, 30], "y": 0, "enemy_type": "goblin", "health": 3, "damage": 1, "attack_range": 1, - "movement_range": 5 + "attack_pattern": { + "type": "patrol" + } }, { "x": 35, @@ -64,13 +66,15 @@ "type": "coffin" }, { - "x": 75, + "x_range": [75, 80], "y": 0, "enemy_type": "goblin", "health": 5, "damage": 2, "attack_range": 1, - "movement_range": 5 + "attack_pattern": { + "type": "patrol" + } }, { "x": 85, @@ -107,10 +111,18 @@ "direction": -1, "fire_interval": 5000, "range": 15 + }, + { + "x_range": [154, 159], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true } ], "boundaries": { "left": 0, "right": 160 - } + }, + "footstep_sound": "footstep_stone" } diff --git a/levels/2.json b/levels/2.json index 8bf925f..11db37c 100644 --- a/levels/2.json +++ b/levels/2.json @@ -21,13 +21,15 @@ "static": true }, { - "x": 25, + "x_range": [21, 29], "y": 0, "enemy_type": "goblin", "health": 4, "damage": 2, "attack_range": 1, - "movement_range": 5 + "attack_pattern": { + "type": "patrol" + } }, { "x": 35, @@ -62,13 +64,15 @@ "static": true }, { - "x": 75, + "x_range": [71, 79], "y": 0, "enemy_type": "goblin", "health": 5, "damage": 2, "attack_range": 1, - "movement_range": 6 + "attack_pattern": { + "type": "patrol" + } }, { "x": 85, @@ -109,10 +113,18 @@ "min": 2, "max": 5 } + }, + { + "x_range": [145, 150], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true } ], "boundaries": { "left": 0, "right": 170 - } + }, + "footstep_sound": "footstep_tall_grass" } diff --git a/levels/3.json b/levels/3.json new file mode 100644 index 0000000..97b43f0 --- /dev/null +++ b/levels/3.json @@ -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" +} diff --git a/sounds/footstep_stone.ogg b/sounds/footstep_stone.ogg new file mode 100644 index 0000000..5a2ab38 --- /dev/null +++ b/sounds/footstep_stone.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b7f975a7e72cd87276f4f5711c946fa6a4d0c8aee002f22accd6085e2699248 +size 4065 diff --git a/sounds/footstep_tall_grass.ogg b/sounds/footstep_tall_grass.ogg new file mode 100644 index 0000000..49c2148 --- /dev/null +++ b/sounds/footstep_tall_grass.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15e2a357d13b50c443ab26f1cb52332021e0dc23db1ed01a7119419811a734c0 +size 6297 diff --git a/sounds/ghoul.ogg b/sounds/ghoul.ogg new file mode 100644 index 0000000..6ef8403 --- /dev/null +++ b/sounds/ghoul.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:063e7bf25d3d68b53f8004e7ef1e663839210f9466e9a799f2b8a2f8cb4842d3 +size 23598 diff --git a/sounds/ghoul_dies.ogg b/sounds/ghoul_dies.ogg new file mode 100644 index 0000000..b93635d --- /dev/null +++ b/sounds/ghoul_dies.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:779575836ce5bb218f67c59e40bf61fbdaa27d62b84ec63dd22ef69fdee57227 +size 14286 diff --git a/sounds/zombie.ogg b/sounds/zombie.ogg index 1b0f984..33f5c7d 100644 --- a/sounds/zombie.ogg +++ b/sounds/zombie.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d615d072069190759c3c6babe05f99d813d7726772fd8c120a64f05ee2e9c1c -size 12276 +oid sha256:bf82061e0c6264361cec499c067916a40a0f0b448a035ec6657e2c8cfbfa01f4 +size 38072 diff --git a/sounds/zombie_dies.ogg b/sounds/zombie_dies.ogg new file mode 100644 index 0000000..b58926f --- /dev/null +++ b/sounds/zombie_dies.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9480cd218db9671883d8129993cec7ac34316c6d2d35ae628090ec150ef30c83 +size 22421 diff --git a/src/coffin.py b/src/coffin.py index 23cc8b9..d99cbbb 100644 --- a/src/coffin.py +++ b/src/coffin.py @@ -23,7 +23,6 @@ class CoffinObject(Object): self.is_broken = True play_sound(self.sounds['coffin_shatter']) self.level.player.stats.update_stat('Coffins broken', 1) - self.level.player.stats.update_stat('Coffins remaining', -1) # Stop the ongoing coffin sound if self.channel: diff --git a/src/enemy.py b/src/enemy.py index d5ef087..d69689f 100644 --- a/src/enemy.py +++ b/src/enemy.py @@ -2,7 +2,6 @@ from libstormgames import * from src.object import Object import pygame - class Enemy(Object): def __init__(self, xRange, y, enemyType, sounds, level, **kwargs): # Initialize base object properties @@ -20,16 +19,20 @@ class Enemy(Object): 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 # Base speed - self.patrolStart = self.xRange[0] - self.patrolEnd = self.xRange[0] + self.movementRange + self.patrolStart = self.xRange[0] # Use xRange directly for patrol boundaries + self.patrolEnd = self.xRange[1] self.lastAttackTime = 0 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 if enemyType == "zombie": @@ -41,21 +44,69 @@ class Enemy(Object): @property def xPos(self): """Current x position""" - return self._currentX if hasattr(self, '_currentX') else self.xRange[0] + return self._currentX @xPos.setter def xPos(self, value): """Set current x position""" 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): """Update enemy position and handle attacks""" if not self.isActive or self.health <= 0: return - # Zombie behavior - always chase player + # Handle movement based on enemy type if self.enemyType == "zombie": - # Determine direction to player + # Zombies always chase player if player.xPos > self.xPos: self.movingRight = True self.xPos += self.movementSpeed @@ -63,20 +114,13 @@ class Enemy(Object): 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 + # Other enemies use their attack pattern + self.update_movement(player) # 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 @@ -128,4 +172,3 @@ class Enemy(Object): # Update stats self.level.player.stats.update_stat('Enemies killed', 1) - self.level.player.stats.update_stat('Enemies remaining', -1) diff --git a/src/level.py b/src/level.py index 3ab1e97..101182b 100644 --- a/src/level.py +++ b/src/level.py @@ -25,6 +25,12 @@ class Level: self.rightBoundary = levelData["boundaries"]["right"] 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 endLevel = Object( self.rightBoundary, diff --git a/src/player.py b/src/player.py index 1594df9..ab7d9a2 100644 --- a/src/player.py +++ b/src/player.py @@ -23,6 +23,14 @@ class Player: self.distanceSinceLastStep = 0 self.stepDistance = 0.5 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 self.inventory = [] @@ -51,6 +59,11 @@ class Player: 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): """Update player state""" # Check if invincibility has expired @@ -91,6 +104,10 @@ class Player: """Get current max health""" return self._maxHealth + def set_footstep_sound(self, soundName): + """Set the current footstep sound""" + self.footstepSound = soundName + def set_health(self, value): """Set health and handle death if needed.""" if self.isInvincible: diff --git a/src/stat_tracker.py b/src/stat_tracker.py index 50dacbc..f6fb7c7 100644 --- a/src/stat_tracker.py +++ b/src/stat_tracker.py @@ -4,9 +4,7 @@ class StatTracker: self.total = { 'Bone dust': 0, 'Enemies killed': 0, - 'Enemies remaining': 0, 'Coffins broken': 0, - 'Coffins remaining': 0, 'Items collected': 0, 'Total time': 0 } diff --git a/wicked_quest.py b/wicked_quest.py index e9c59e5..34dc07b 100644 --- a/wicked_quest.py +++ b/wicked_quest.py @@ -65,6 +65,14 @@ class WickedQuest: player.xPos += currentSpeed 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 if keys[pygame.K_c]: 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): 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 if keys[pygame.K_w] and not player.isJumping: player.isJumping = True @@ -100,9 +101,10 @@ class WickedQuest: # Check if jump should end if player.isJumping and currentTime - player.jumpStartTime >= player.jumpDuration: player.isJumping = False - play_sound(self.sounds['footstep']) + play_sound(self.sounds[player.footstepSound]) # Landing sound # Reset step distance tracking after landing player.distanceSinceLastStep = 0 + player.lastStepTime = currentTime def display_level_stats(self, timeTaken): """Display level completion statistics.""" @@ -214,6 +216,8 @@ class WickedQuest: self.player = None # Reset player for new game if self.load_level(1): self.game_loop() + elif choice == "learn_sounds": + choice = learn_sounds(self.sounds) if __name__ == "__main__":