Make the game more level creator friendly by separating levels into adventures. Menu for choosing which adventure you want.

This commit is contained in:
Storm Dragon
2025-02-08 23:22:38 -05:00
parent f751d99553
commit 0f0d719578
5 changed files with 154 additions and 440 deletions

View File

@@ -1,178 +0,0 @@
{
"level_id": 2,
"name": "The Graveyard",
"description": "The mausoleum led to an ancient graveyard. Watch out for falling skulls!",
"player_start": {
"x": 0,
"y": 0
},
"objects": [
{
"x": 5,
"y": 3,
"sound": "coffin",
"type": "coffin"
},
{
"x_range": [15, 20],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [21, 29],
"y": 0,
"enemy_type": "goblin",
"health": 2,
"damage": 2,
"attack_range": 1,
"attack_pattern": {
"type": "patrol"
}
},
{
"x": 35,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 10
},
{
"x_range": [33, 38],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [45, 60],
"y": 12,
"type": "skull_storm",
"damage": 3,
"maximum_skulls": 2,
"frequency": {
"min": 3,
"max": 6
}
},
{
"x": 55,
"y": 3,
"sound": "coffin",
"type": "coffin",
"item": "guts"
},
{
"x_range": [65, 70],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [71, 79],
"y": 0,
"enemy_type": "goblin",
"health": 2,
"damage": 2,
"attack_range": 1,
"attack_pattern": {
"type": "patrol"
}
},
{
"x": 85,
"y": 3,
"sound": "coffin",
"type": "coffin"
},
{
"x": 95,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 10
},
{
"x_range": [105, 108],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 120,
"y": 0,
"type": "catapult",
"fire_interval": 5000,
"range": 15
},
{
"x_range": [130, 165],
"y": 15,
"type": "skull_storm",
"damage": 4,
"maximum_skulls": 3,
"frequency": {
"min": 2,
"max": 5
}
},
{
"x_range": [145, 150],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 160,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 10
},
{
"x": 170,
"y": 3,
"sound": "coffin",
"type": "coffin",
"item": "extra_life"
},
{
"x_range": [175, 180],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 185,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 10
},
{
"x_range": [189, 198],
"y": 0,
"enemy_type": "witch",
"health": 2,
"damage": 2,
"attack_range": 1.5,
"attack_pattern": {
"type": "patrol"
}
}
],
"boundaries": {
"left": 0,
"right": 200
},
"footstep_sound": "footstep_tall_grass"
}

View File

@@ -1,252 +0,0 @@
{
"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_range": [25, 28],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [21, 31],
"y": 0,
"enemy_type": "goblin",
"health": 3,
"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": [40, 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": 3,
"damage": 2,
"attack_range": 1,
"attack_pattern": {
"type": "patrol"
}
},
{
"x_range": [75, 78],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [71, 81],
"y": 0,
"enemy_type": "goblin",
"health": 3,
"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, 115],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x_range": [101, 111],
"y": 0,
"enemy_type": "witch",
"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": 3,
"damage": 2,
"attack_range": 1,
"attack_pattern": {
"type": "patrol"
}
},
{
"x_range": [155, 158],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 145,
"y": 3,
"item": "hand_of_glory",
"sound": "coffin",
"type": "coffin"
},
{
"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",
"fire_interval": 4500,
"range": 20
},
{
"x_range": [173, 176],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 185,
"y": 3,
"item": "extra_life",
"sound": "coffin",
"type": "coffin"
},
{
"x_range": [190, 195],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
}
],
"boundaries": {
"left": 0,
"right": 200
},
"footstep_sound": "footstep_tall_grass"
}

133
src/game_selection.py Normal file
View File

@@ -0,0 +1,133 @@
import os
import time
import pygame
from os.path import isdir, join
from libstormgames import speak
def get_available_games():
"""Get list of available game directories in levels folder.
Returns:
list: List of game directory names
"""
try:
return [d for d in os.listdir("levels") if isdir(join("levels", d))]
except FileNotFoundError:
return []
def selection_menu(sounds, *options):
"""Display level selection menu.
Args:
sounds (dict): Dictionary of loaded sound effects
*options: Variable number of menu options
Returns:
str: Selected option or None if cancelled
"""
loop = True
pygame.mixer.stop()
i = 0
j = -1
# Clear any pending events
pygame.event.clear()
speak("Select an adventure")
time.sleep(1.0)
while loop:
if i != j:
speak(options[i])
j = i
pygame.event.pump()
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
return None
if event.key == pygame.K_DOWN and i < len(options) - 1:
i = i + 1
try:
sounds['menu-move'].play()
except:
pass
if event.key == pygame.K_UP and i > 0:
i = i - 1
try:
sounds['menu-move'].play()
except:
pass
if event.key == pygame.K_HOME and i != 0:
i = 0
try:
sounds['menu-move'].play()
except:
pass
if event.key == pygame.K_END and i != len(options) - 1:
i = len(options) - 1
try:
sounds['menu-move'].play()
except:
pass
if event.key == pygame.K_RETURN:
try:
sounds['menu-select'].play()
time.sleep(sounds['menu-select'].get_length())
except:
pass
return options[i]
elif event.type == pygame.QUIT:
return None
pygame.event.pump()
event = pygame.event.clear()
time.sleep(0.001)
def select_game(sounds):
"""Display game selection menu and return chosen game.
Args:
sounds (dict): Dictionary of loaded sound effects
Returns:
str: Selected game directory name or None if cancelled
"""
availableGames = get_available_games()
if not availableGames:
speak("No games found in levels directory!")
return None
# Convert directory names to display names (replace underscores with spaces)
menuOptions = [game.replace("_", " ") for game in availableGames]
choice = selection_menu(sounds, *menuOptions)
if choice is None:
return None
# Convert display name back to directory name if needed
gameDir = choice.replace(" ", "_")
if gameDir not in availableGames:
gameDir = choice # Use original if conversion doesn't match
return gameDir
def get_level_path(gameDir, levelNum):
"""Get full path to level JSON file.
Args:
gameDir (str): Game directory name
levelNum (int): Level number
Returns:
str: Full path to level JSON file
"""
return os.path.join("levels", gameDir, f"{levelNum}.json")

View File

@@ -5,6 +5,7 @@ from libstormgames import *
from src.level import Level from src.level import Level
from src.object import Object from src.object import Object
from src.player import Player from src.player import Player
from src.game_selection import select_game, get_level_path
class WickedQuest: class WickedQuest:
@@ -15,24 +16,29 @@ class WickedQuest:
self.gameStartTime = None self.gameStartTime = None
self.lastThrowTime = 0 self.lastThrowTime = 0
self.throwDelay = 250 self.throwDelay = 250
self.player = None # Will be initialized when first level loads self.player = None
self.currentGame = None
def load_level(self, levelNumber): def load_level(self, levelNumber):
"""Load a level from its JSON file.""" """Load a level from its JSON file."""
levelFile = f"levels/{levelNumber}.json" levelFile = get_level_path(self.currentGame, levelNumber)
pygame.event.pump()
try: try:
with open(levelFile, 'r') as f: with open(levelFile, 'r') as f:
levelData = json.load(f) levelData = json.load(f)
# Create player if this is the first level # Create player if this is the first level
if self.player is None: if self.player is None:
self.player = Player(levelData["player_start"]["x"], levelData["player_start"]["y"], self.sounds) self.player = Player(levelData["player_start"]["x"],
levelData["player_start"]["y"],
self.sounds)
else: else:
# Just update player position for new level # Just update player position for new level
self.player.xPos = levelData["player_start"]["x"] self.player.xPos = levelData["player_start"]["x"]
self.player.yPos = levelData["player_start"]["y"] self.player.yPos = levelData["player_start"]["y"]
# Pass existing player to new level # Pass existing player to new level
pygame.event.pump()
self.currentLevel = Level(levelData, self.sounds, self.player) self.currentLevel = Level(levelData, self.sounds, self.player)
# Announce level details # Announce level details
@@ -49,6 +55,7 @@ class WickedQuest:
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
player = self.currentLevel.player player = self.currentLevel.player
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
pygame.event.pump()
# Update running and ducking states # Update running and ducking states
if keys[pygame.K_s] and not player.isDucking: if keys[pygame.K_s] and not player.isDucking:
@@ -162,18 +169,19 @@ class WickedQuest:
while True: while True:
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
pygame.event.pump()
# Game volume controls # Game volume controls
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.KEYDOWN: if event.type == pygame.KEYDOWN:
# Check for Alt modifier # Check for Alt modifier
mods = pygame.key.get_mods() mods = pygame.key.get_mods()
alt_pressed = mods & pygame.KMOD_ALT altPressed = mods & pygame.KMOD_ALT
if event.key == pygame.K_ESCAPE: if event.key == pygame.K_ESCAPE:
return return
# Volume controls (require Alt) # Volume controls (require Alt)
elif alt_pressed: elif altPressed:
if event.key == pygame.K_PAGEUP: if event.key == pygame.K_PAGEUP:
adjust_master_volume(0.1) adjust_master_volume(0.1)
elif event.key == pygame.K_PAGEDOWN: elif event.key == pygame.K_PAGEDOWN:
@@ -233,15 +241,18 @@ class WickedQuest:
def run(self): def run(self):
"""Main game loop with menu system.""" """Main game loop with menu system."""
while True: while True:
choice = game_menu(self.sounds, "play", "instructions", "learn_sounds", "credits", "donate", "exit") choice = game_menu(self.sounds, "play", "instructions", "learn_sounds",
"credits", "donate", "exit")
if choice == "exit": if choice == "exit":
exit_game() exit_game()
elif choice == "play": elif choice == "play":
self.player = None # Reset player for new game self.currentGame = select_game(self.sounds)
self.gameStartTime = pygame.time.get_ticks() # Set game start time here if self.currentGame:
if self.load_level(1): self.player = None # Reset player for new game
self.game_loop() self.gameStartTime = pygame.time.get_ticks()
if self.load_level(1):
self.game_loop()
elif choice == "learn_sounds": elif choice == "learn_sounds":
choice = learn_sounds(self.sounds) choice = learn_sounds(self.sounds)