Fix up some inconsistancies in the code levels should only be needed in one directory now, so update the hook to move levels into place on first run if needed. Warning, this version breaks existing save files.

This commit is contained in:
Storm Dragon
2025-10-03 01:46:28 -04:00
parent 95fc94a507
commit dd67eb5e1d
14 changed files with 410 additions and 422 deletions
+22 -20
View File
@@ -5,53 +5,55 @@ import shutil
# Runtime hook to move directories and files from _internal to parent directory # Runtime hook to move directories and files from _internal to parent directory
if hasattr(sys, '_MEIPASS'): if hasattr(sys, '_MEIPASS'):
# We're running from a PyInstaller bundle # We're running from a PyInstaller bundle
bundle_dir = os.path.dirname(sys.executable) bundleDir = os.path.dirname(sys.executable)
internal_dir = sys._MEIPASS internalDir = sys._MEIPASS
# Directories to move from _internal to parent # Directories to move from _internal to parent
dirs_to_move = ['sounds', 'libstormgames'] # All these use the parent directory for easier user customization
dirsToMove = ['sounds', 'libstormgames', 'levels']
# Directories to copy (keep in both locations) # Directories to copy (keep in both locations)
dirs_to_copy = ['levels'] # None needed - everything now uses parent directory
dirsToCopy = []
# Files to move from _internal to parent # Files to move from _internal to parent
files_to_move = ['files', 'logo.png'] filesToMove = ['files', 'logo.png']
# Move directories # Move directories
for dir_name in dirs_to_move: for dir_name in dirsToMove:
internal_path = os.path.join(internal_dir, dir_name) internalPath = os.path.join(internalDir, dir_name)
target_path = os.path.join(bundle_dir, dir_name) targetPath = os.path.join(bundleDir, dir_name)
# Only move if source exists and target doesn't exist # Only move if source exists and target doesn't exist
if os.path.exists(internal_path) and not os.path.exists(target_path): if os.path.exists(internalPath) and not os.path.exists(targetPath):
try: try:
shutil.move(internal_path, target_path) shutil.move(internalPath, targetPath)
except Exception as e: except Exception as e:
# Silently fail if we can't move - game will still work from _internal # Silently fail if we can't move - game will still work from _internal
pass pass
# Copy directories (keep in both locations) # Copy directories (keep in both locations)
for dir_name in dirs_to_copy: for dir_name in dirsToCopy:
internal_path = os.path.join(internal_dir, dir_name) internalPath = os.path.join(internalDir, dir_name)
target_path = os.path.join(bundle_dir, dir_name) targetPath = os.path.join(bundleDir, dir_name)
# Only copy if source exists and target doesn't exist # Only copy if source exists and target doesn't exist
if os.path.exists(internal_path) and not os.path.exists(target_path): if os.path.exists(internalPath) and not os.path.exists(targetPath):
try: try:
shutil.copytree(internal_path, target_path) shutil.copytree(internalPath, targetPath)
except Exception as e: except Exception as e:
# Silently fail if we can't copy - game will still work from _internal # Silently fail if we can't copy - game will still work from _internal
pass pass
# Move files # Move files
for file_name in files_to_move: for file_name in filesToMove:
internal_path = os.path.join(internal_dir, file_name) internalPath = os.path.join(internalDir, file_name)
target_path = os.path.join(bundle_dir, file_name) targetPath = os.path.join(bundleDir, file_name)
# Only move if source exists and target doesn't exist # Only move if source exists and target doesn't exist
if os.path.exists(internal_path) and not os.path.exists(target_path): if os.path.exists(internalPath) and not os.path.exists(targetPath):
try: try:
shutil.move(internal_path, target_path) shutil.move(internalPath, targetPath)
except Exception as e: except Exception as e:
# Silently fail if we can't move - game will still work from _internal at least enough to exit. # Silently fail if we can't move - game will still work from _internal at least enough to exit.
pass pass
BIN
View File
Binary file not shown.
+14 -14
View File
@@ -18,8 +18,8 @@ class CoffinObject(Object):
self.sounds = sounds self.sounds = sounds
self.level = level self.level = level
self.isBroken = False self.isBroken = False
self.dropped_item = None self.droppedItem = None
self.specified_item = item self.specifiedItem = item
def hit(self, player_pos): def hit(self, player_pos):
"""Handle being hit by the player's weapon""" """Handle being hit by the player's weapon"""
@@ -38,32 +38,32 @@ class CoffinObject(Object):
self.isActive = False self.isActive = False
# Determine item to drop # Determine item to drop
if self.specified_item == "random": if self.specifiedItem == "random":
# Check if we're in survival mode (level_id 999) # Check if we're in survival mode (level_id 999)
is_survival_mode = hasattr(self.level, 'levelData') and self.level.levelData.get('level_id') == 999 isSurvivalMode = hasattr(self.level, 'levelData') and self.level.levelData.get('level_id') == 999
item_type = ItemProperties.get_random_item(survival_mode=is_survival_mode) itemType = ItemProperties.get_random_item(survivalMode=isSurvivalMode)
else: else:
# Validate specified item # Validate specified item
if ItemProperties.is_valid_item(self.specified_item): if ItemProperties.is_valid_item(self.specifiedItem):
item_type = self.specified_item itemType = self.specifiedItem
else: else:
# Fall back to random if invalid item specified # Fall back to random if invalid item specified
# Check if we're in survival mode (level_id 999) # Check if we're in survival mode (level_id 999)
is_survival_mode = hasattr(self.level, 'levelData') and self.level.levelData.get('level_id') == 999 isSurvivalMode = hasattr(self.level, 'levelData') and self.level.levelData.get('level_id') == 999
item_type = ItemProperties.get_random_item(survival_mode=is_survival_mode) itemType = ItemProperties.get_random_item(survivalMode=isSurvivalMode)
# Create item 1-2 tiles away in random direction # Create item 1-2 tiles away in random direction
direction = random.choice([-1, 1]) direction = random.choice([-1, 1])
drop_distance = random.randint(1, 2) dropDistance = random.randint(1, 2)
drop_x = self.xPos + (direction * drop_distance) dropX = self.xPos + (direction * dropDistance)
self.dropped_item = PowerUp( self.droppedItem = PowerUp(
drop_x, self.yPos, item_type, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
) )
# Apply sound override after creation (similar to how graves handle it) # Apply sound override after creation (similar to how graves handle it)
if hasattr(self, 'itemSoundOverride'): if hasattr(self, 'itemSoundOverride'):
self.dropped_item.soundName = self.itemSoundOverride self.droppedItem.soundName = self.itemSoundOverride
return True return True
return False return False
+16 -16
View File
@@ -19,7 +19,7 @@ class Enemy(Object):
self.level = level self.level = level
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("attackRange", 1) # Default 1 tile range
self.sounds = sounds # Store reference to game sounds self.sounds = sounds # Store reference to game sounds
# Movement and behavior properties # Movement and behavior properties
@@ -32,25 +32,25 @@ class Enemy(Object):
self._currentX = self.xRange[0] # Initialize current position self._currentX = self.xRange[0] # Initialize current position
# Add spawn configuration # Add spawn configuration
self.canSpawn = kwargs.get("can_spawn", False) self.canSpawn = kwargs.get("canSpawn", False)
if self.canSpawn: if self.canSpawn:
self.spawnCooldown = kwargs.get("spawn_cooldown", 2000) self.spawnCooldown = kwargs.get("spawnCooldown", 2000)
self.spawnChance = kwargs.get("spawn_chance", 25) self.spawnChance = kwargs.get("spawnChance", 25)
self.spawnType = kwargs.get("spawn_type", "zombie") # Default to zombie for backward compatibility self.spawnType = kwargs.get("spawnType", "zombie") # Default to zombie for backward compatibility
self.spawnDistance = kwargs.get("spawn_distance", 5) self.spawnDistance = kwargs.get("spawnDistance", 5)
self.lastSpawnTime = 0 self.lastSpawnTime = 0
# Attack pattern configuration # Attack pattern configuration
self.attackPattern = kwargs.get("attack_pattern", {"type": "patrol"}) self.attackPattern = kwargs.get("attackPattern", {"type": "patrol"})
self.turnThreshold = self.attackPattern.get("turn_threshold", 5) self.turnThreshold = self.attackPattern.get("turn_threshold", 5)
# Initialize vulnerability system # Initialize vulnerability system
self.hasVulnerabilitySystem = kwargs.get("has_vulnerability", False) self.hasVulnerabilitySystem = kwargs.get("hasVulnerability", False)
if self.hasVulnerabilitySystem: if self.hasVulnerabilitySystem:
self.isVulnerable = False # Start invulnerable self.isVulnerable = False # Start invulnerable
self.vulnerabilityTimer = pygame.time.get_ticks() self.vulnerabilityTimer = pygame.time.get_ticks()
self.vulnerabilityDuration = kwargs.get("vulnerability_duration", 2000) self.vulnerabilityDuration = kwargs.get("vulnerabilityDuration", 2000)
self.invulnerabilityDuration = kwargs.get("invulnerability_duration", 5000) self.invulnerabilityDuration = kwargs.get("invulnerabilityDuration", 5000)
soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType
self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos) self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos)
else: else:
@@ -63,7 +63,7 @@ class Enemy(Object):
self.health = 1 # Easy to kill self.health = 1 # Easy to kill
self.attackCooldown = 1500 # Slower attack rate self.attackCooldown = 1500 # Slower attack rate
elif enemyType == "spider": elif enemyType == "spider":
speedMultiplier = kwargs.get("speed_multiplier", 2.0) speedMultiplier = kwargs.get("speedMultiplier", 2.0)
self.movementSpeed *= speedMultiplier # Spiders are faster self.movementSpeed *= speedMultiplier # Spiders are faster
self.attackPattern = {"type": "hunter"} # Spiders actively hunt the player self.attackPattern = {"type": "hunter"} # Spiders actively hunt the player
self.turnThreshold = 3 # Spiders turn around quickly to chase player self.turnThreshold = 3 # Spiders turn around quickly to chase player
@@ -162,7 +162,7 @@ class Enemy(Object):
# Set behavior based on game mode # Set behavior based on game mode
behavior = "hunter" if self.level.levelId == 999 else "patrol" behavior = "hunter" if self.level.levelId == 999 else "patrol"
turn_rate = 2 if self.level.levelId == 999 else 8 # Faster turn rate for survival turnRate = 2 if self.level.levelId == 999 else 8 # Faster turn rate for survival
# Create new enemy of specified type # Create new enemy of specified type
spawned = Enemy( spawned = Enemy(
@@ -173,9 +173,9 @@ class Enemy(Object):
self.level, self.level,
health=4, # Default health for spawned enemies health=4, # Default health for spawned enemies
damage=2, # Default damage for spawned enemies damage=2, # Default damage for spawned enemies
attack_range=1, # Default range for spawned enemies attackRange=1, # Default range for spawned enemies
attack_pattern={"type": behavior}, attackPattern={"type": behavior},
turn_rate=turn_rate, turnRate=turnRate,
) )
# Add to level's enemies # Add to level's enemies
@@ -273,7 +273,7 @@ class Enemy(Object):
droppedItem = PowerUp( droppedItem = PowerUp(
dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
) )
self.level.bouncing_items.append(droppedItem) self.level.bouncingItems.append(droppedItem)
# Update stats # Update stats
self.level.player.stats.update_stat("Enemies killed", 1) self.level.player.stats.update_stat("Enemies killed", 1)
+28 -26
View File
@@ -15,7 +15,9 @@ def get_levels_base_path():
""" """
if hasattr(sys, "_MEIPASS"): if hasattr(sys, "_MEIPASS"):
# Running as PyInstaller executable # Running as PyInstaller executable
return sys._MEIPASS # Use parent directory (where executable is) instead of _internal
# This allows users to add new level packs without recompiling
return os.path.dirname(sys.executable)
else: else:
# Running as script # Running as script
return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -28,9 +30,9 @@ def get_available_games():
list: List of game directory names list: List of game directory names
""" """
try: try:
base_path = get_levels_base_path() basePath = get_levels_base_path()
levels_path = os.path.join(base_path, "levels") levelsPath = os.path.join(basePath, "levels")
return [d for d in os.listdir(levels_path) if isdir(join(levels_path, d)) and not d.endswith(".md")] return [d for d in os.listdir(levelsPath) if isdir(join(levelsPath, d)) and not d.endswith(".md")]
except FileNotFoundError: except FileNotFoundError:
return [] return []
@@ -57,57 +59,57 @@ def select_game(sounds):
Returns: Returns:
str: Selected game directory name or None if cancelled str: Selected game directory name or None if cancelled
""" """
available_games = get_available_games() availableGames = get_available_games()
if not available_games: if not availableGames:
speak("No games found in levels directory!") speak("No games found in levels directory!")
return None return None
# Convert directory names to display names (replace underscores with spaces) # Convert directory names to display names (replace underscores with spaces)
menu_options = [game.replace("_", " ") for game in available_games] menuOptions = [game.replace("_", " ") for game in availableGames]
choice = selection_menu(sounds, *menu_options) choice = selection_menu(sounds, *menuOptions)
if choice is None: if choice is None:
return None return None
# Convert display name back to directory name if needed # Convert display name back to directory name if needed
game_dir = choice.replace(" ", "_") gameDir = choice.replace(" ", "_")
if game_dir not in available_games: if gameDir not in availableGames:
game_dir = choice # Use original if conversion doesn't match gameDir = choice # Use original if conversion doesn't match
return game_dir return gameDir
def get_level_path(game_dir, level_num): def get_level_path(gameDir, levelNum):
"""Get full path to level JSON file. """Get full path to level JSON file.
Args: Args:
game_dir (str): Game directory name gameDir (str): Game directory name
level_num (int): Level number levelNum (int): Level number
Returns: Returns:
str: Full path to level JSON file str: Full path to level JSON file
""" """
if game_dir is None: if gameDir is None:
raise ValueError("game_dir cannot be None") raise ValueError("gameDir cannot be None")
base_path = get_levels_base_path() basePath = get_levels_base_path()
level_path = os.path.join(base_path, "levels", game_dir, f"{level_num}.json") levelPath = os.path.join(basePath, "levels", gameDir, f"{levelNum}.json")
return level_path return levelPath
def get_game_dir_path(game_dir): def get_game_dir_path(gameDir):
"""Get full path to game directory for end.ogg and other game files. """Get full path to game directory for end.ogg and other game files.
Args: Args:
game_dir (str): Game directory name gameDir (str): Game directory name
Returns: Returns:
str: Full path to game directory str: Full path to game directory
""" """
if game_dir is None: if gameDir is None:
raise ValueError("game_dir cannot be None") raise ValueError("gameDir cannot be None")
base_path = get_levels_base_path() basePath = get_levels_base_path()
return os.path.join(base_path, "levels", game_dir) return os.path.join(basePath, "levels", gameDir)
+3 -3
View File
@@ -8,7 +8,7 @@ from src.object import Object
class GraspingHands(Object): class GraspingHands(Object):
"""A hazard where the ground crumbles beneath the player as undead hands reach up.""" """A hazard where the ground crumbles beneath the player as undead hands reach up."""
def __init__(self, xRange, y, sounds, delay=1000, crumble_speed=0.065): def __init__(self, xRange, y, sounds, delay=1000, crumbleSpeed=0.065):
super().__init__( super().__init__(
xRange, xRange,
y, y,
@@ -19,7 +19,7 @@ class GraspingHands(Object):
) )
self.sounds = sounds self.sounds = sounds
self.delay = delay # Delay in milliseconds before ground starts crumbling self.delay = delay # Delay in milliseconds before ground starts crumbling
self.crumble_speed = crumble_speed # How fast the crumbling catches up (tiles per frame) self.crumbleSpeed = crumbleSpeed # How fast the crumbling catches up (tiles per frame)
# Sound prefix for different sound effects (can be overridden) # Sound prefix for different sound effects (can be overridden)
self.soundPrefix = "grasping_hands" self.soundPrefix = "grasping_hands"
@@ -95,7 +95,7 @@ class GraspingHands(Object):
# If triggered and delay has passed, start crumbling # If triggered and delay has passed, start crumbling
if self.isTriggered and currentTime - self.triggerTime >= self.delay: if self.isTriggered and currentTime - self.triggerTime >= self.delay:
# Update crumble position based on direction # Update crumble position based on direction
self.crumblePosition += self.crumble_speed * self.crumbleDirection self.crumblePosition += self.crumbleSpeed * self.crumbleDirection
# Manage the looping positional audio for the crumbling ground # Manage the looping positional audio for the crumbling ground
if self.crumbleChannel is None or not self.crumbleChannel.get_busy(): if self.crumbleChannel is None or not self.crumbleChannel.get_busy():
+13 -13
View File
@@ -45,28 +45,28 @@ class ItemProperties:
} }
@staticmethod @staticmethod
def get_sound_name(item_type): def get_sound_name(itemType):
"""Convert enum to sound/asset name""" """Convert enum to sound/asset name"""
return ItemProperties.ALL_ITEMS.get(item_type) return ItemProperties.ALL_ITEMS.get(itemType)
@staticmethod @staticmethod
def get_random_item(survival_mode=False): def get_random_item(survivalMode=False):
"""Get a random item from eligible items""" """Get a random item from eligible items"""
if survival_mode: if survivalMode:
item_type = random.choice(list(ItemProperties.SURVIVAL_MODE_ELIGIBLE.keys())) itemType = random.choice(list(ItemProperties.SURVIVAL_MODE_ELIGIBLE.keys()))
else: else:
item_type = random.choice(list(ItemProperties.STORY_MODE_ELIGIBLE.keys())) itemType = random.choice(list(ItemProperties.STORY_MODE_ELIGIBLE.keys()))
return ItemProperties.get_sound_name(item_type) return ItemProperties.get_sound_name(itemType)
@staticmethod @staticmethod
def is_valid_item(item_name): def is_valid_item(itemName):
"""Check if an item name is valid""" """Check if an item name is valid"""
return item_name in [v for v in ItemProperties.ALL_ITEMS.values()] return itemName in [v for v in ItemProperties.ALL_ITEMS.values()]
@staticmethod @staticmethod
def get_item_type(item_name): def get_item_type(itemName):
"""Get ItemType enum from string name""" """Get ItemType enum from string name"""
for item_type, name in ItemProperties.ALL_ITEMS.items(): for itemType, name in ItemProperties.ALL_ITEMS.items():
if name == item_name: if name == itemName:
return item_type return itemType
return None return None
+58 -58
View File
@@ -22,12 +22,12 @@ class Level:
self.levelPackName = levelPackName self.levelPackName = levelPackName
self.objects = [] self.objects = []
self.enemies = [] self.enemies = []
self.bouncing_items = [] self.bouncingItems = []
self.projectiles = [] # Track active projectiles self.projectiles = [] # Track active projectiles
self.player = player self.player = player
self.lastWarningTime = 0 self.lastWarningTime = 0
self.warningInterval = int(self.sounds["edge"].get_length() * 1000) # Convert seconds to milliseconds self.warningInterval = int(self.sounds["edge"].get_length() * 1000) # Convert seconds to milliseconds
self.weapon_hit_channel = None self.weaponHitChannel = None
self.leftBoundary = levelData["boundaries"]["left"] self.leftBoundary = levelData["boundaries"]["left"]
self.rightBoundary = levelData["boundaries"]["right"] self.rightBoundary = levelData["boundaries"]["right"]
@@ -137,7 +137,7 @@ class Level:
obj["y"], obj["y"],
self.sounds, self.sounds,
delay=obj.get("delay", 1000), delay=obj.get("delay", 1000),
crumble_speed=obj.get("crumble_speed", 0.03), crumbleSpeed=obj.get("crumble_speed", 0.03),
) )
# Apply sound overrides if specified # Apply sound overrides if specified
if "sound_overrides" in obj: if "sound_overrides" in obj:
@@ -146,25 +146,25 @@ class Level:
# Check if this is a grave # Check if this is a grave
elif obj.get("type") == "grave": elif obj.get("type") == "grave":
# Handle item spawning with chance (for survival mode) # Handle item spawning with chance (for survival mode)
grave_item = None graveItem = None
if "item_spawn_chance" in obj: if "item_spawn_chance" in obj:
# Survival mode: spawn item based on chance (random items already resolved) # Survival mode: spawn item based on chance (random items already resolved)
spawn_chance = obj.get("item_spawn_chance", 0) spawnChance = obj.get("item_spawn_chance", 0)
if random.randint(1, 100) <= spawn_chance: if random.randint(1, 100) <= spawnChance:
grave_item = obj.get("item") graveItem = obj.get("item")
else: else:
# Story mode: use item as specified # Story mode: use item as specified
grave_item = obj.get("item", None) graveItem = obj.get("item", None)
if grave_item == "random": if graveItem == "random":
# Check if we're in survival mode (level_id 999) # Check if we're in survival mode (level_id 999)
is_survival_mode = self.levelData.get('level_id') == 999 isSurvivalMode = self.levelData.get('level_id') == 999
grave_item = ItemProperties.get_random_item(survival_mode=is_survival_mode) graveItem = ItemProperties.get_random_item(survivalMode=isSurvivalMode)
grave = GraveObject( grave = GraveObject(
xPos[0], xPos[0],
obj["y"], obj["y"],
self.sounds, self.sounds,
item=grave_item, item=graveItem,
zombieSpawnChance=obj.get("zombie_spawn_chance", 0), zombieSpawnChance=obj.get("zombie_spawn_chance", 0),
) )
# Apply sound overrides if specified # Apply sound overrides if specified
@@ -232,18 +232,18 @@ class Level:
self, # Pass level reference self, # Pass level reference
health=obj.get("health", 5), health=obj.get("health", 5),
damage=obj.get("damage", 1), damage=obj.get("damage", 1),
attack_range=obj.get("attack_range", 1), attackRange=obj.get("attack_range", 1),
movement_range=obj.get("movement_range", 5), movementRange=obj.get("movement_range", 5),
attack_pattern=obj.get("attack_pattern", {"type": "patrol"}), attackPattern=obj.get("attack_pattern", {"type": "patrol"}),
can_spawn=obj.get("can_spawn", False), canSpawn=obj.get("can_spawn", False),
spawn_type=obj.get("spawn_type", "zombie"), spawnType=obj.get("spawn_type", "zombie"),
spawn_cooldown=obj.get("spawn_cooldown", 2000), spawnCooldown=obj.get("spawn_cooldown", 2000),
spawn_chance=obj.get("spawn_chance", 25), spawnChance=obj.get("spawn_chance", 25),
spawn_distance=obj.get("spawn_distance", 5), spawnDistance=obj.get("spawn_distance", 5),
has_vulnerability=obj.get("has_vulnerability", False), hasVulnerability=obj.get("has_vulnerability", False),
is_vulnerable=obj.get("is_vulnerable", False), isVulnerable=obj.get("is_vulnerable", False),
vulnerability_duration=obj.get("vulnerability_duration", 1000), vulnerabilityDuration=obj.get("vulnerability_duration", 1000),
invulnerability_duration=obj.get("invulnerability_duration", 5000), invulnerabilityDuration=obj.get("invulnerability_duration", 5000),
) )
self.enemies.append(enemy) self.enemies.append(enemy)
else: else:
@@ -286,7 +286,7 @@ class Level:
if roll <= obj.zombieSpawnChance: if roll <= obj.zombieSpawnChance:
# Set behavior based on game mode # Set behavior based on game mode
behavior = "hunter" if self.levelId == 999 else "patrol" behavior = "hunter" if self.levelId == 999 else "patrol"
turn_rate = 2 if self.levelId == 999 else 8 # Faster turn rate for survival turnRate = 2 if self.levelId == 999 else 8 # Faster turn rate for survival
zombie = Enemy( zombie = Enemy(
[obj.xPos, obj.xPos], [obj.xPos, obj.xPos],
@@ -296,9 +296,9 @@ class Level:
self, # Pass the level reference self, # Pass the level reference
health=3, health=3,
damage=10, damage=10,
attack_range=1, attackRange=1,
attack_pattern={"type": behavior}, attackPattern={"type": behavior},
turn_rate=turn_rate, turnRate=turnRate,
) )
self.enemies.append(zombie) self.enemies.append(zombie)
speak("A zombie emerges from the grave!") speak("A zombie emerges from the grave!")
@@ -354,9 +354,9 @@ class Level:
self.objects = [obj for obj in self.objects if obj.isActive] self.objects = [obj for obj in self.objects if obj.isActive]
# Update bouncing items # Update bouncing items
for item in self.bouncing_items[:]: # Copy list to allow removal for item in self.bouncingItems[:]: # Copy list to allow removal
if not item.update(currentTime, self.player.xPos): if not item.update(currentTime, self.player.xPos):
self.bouncing_items.remove(item) self.bouncingItems.remove(item)
if not item.isActive: if not item.isActive:
speak(f"{item.soundName} got away!") speak(f"{item.soundName} got away!")
continue continue
@@ -367,7 +367,7 @@ class Level:
item.apply_effect(self.player, self) item.apply_effect(self.player, self)
self.levelScore += 1000 # All items collected points awarded self.levelScore += 1000 # All items collected points awarded
item.isActive = False item.isActive = False
self.bouncing_items.remove(item) self.bouncingItems.remove(item)
def handle_combat(self, currentTime): def handle_combat(self, currentTime):
"""Handle combat interactions between player and enemies""" """Handle combat interactions between player and enemies"""
@@ -394,7 +394,7 @@ class Level:
): # Must be jumping to hit floating coffins ): # Must be jumping to hit floating coffins
if obj.hit(self.player.xPos): if obj.hit(self.player.xPos):
self.bouncing_items.append(obj.dropped_item) self.bouncingItems.append(obj.droppedItem)
def spawn_spider(self, xPos, yPos): def spawn_spider(self, xPos, yPos):
"""Spawn a spider at the given position""" """Spawn a spider at the given position"""
@@ -406,8 +406,8 @@ class Level:
self, self,
health=8, health=8,
damage=8, damage=8,
attack_range=1, attackRange=1,
speed_multiplier=2.0, speedMultiplier=2.0,
) )
self.enemies.append(spider) self.enemies.append(spider)
@@ -417,20 +417,20 @@ class Level:
if enemyStats: if enemyStats:
health = enemyStats.get("health", 8) health = enemyStats.get("health", 8)
damage = enemyStats.get("damage", 2) damage = enemyStats.get("damage", 2)
speed_multiplier = enemyStats.get("speed_multiplier", 1.0) speedMultiplier = enemyStats.get("speed_multiplier", 1.0)
attack_range = enemyStats.get("attack_range", 1) attackRange = enemyStats.get("attack_range", 1)
else: else:
# Default stats for common enemy types (backward compatibility) # Default stats for common enemy types (backward compatibility)
if enemyType == "spider": if enemyType == "spider":
health, damage, speed_multiplier = 8, 8, 2.0 health, damage, speedMultiplier = 8, 8, 2.0
elif enemyType == "elf": elif enemyType == "elf":
health, damage, speed_multiplier = 2, 1, 1.0 health, damage, speedMultiplier = 2, 1, 1.0
elif enemyType == "witch": elif enemyType == "witch":
health, damage, speed_multiplier = 6, 2, 1.0 health, damage, speedMultiplier = 6, 2, 1.0
else: else:
# For any other enemy type, use reasonable defaults # For any other enemy type, use reasonable defaults
health, damage, speed_multiplier = 6, 2, 1.0 health, damage, speedMultiplier = 6, 2, 1.0
attack_range = 1 attackRange = 1
enemy = Enemy( enemy = Enemy(
[xPos - 5, xPos + 5], # Give enemy a patrol range [xPos - 5, xPos + 5], # Give enemy a patrol range
@@ -440,9 +440,9 @@ class Level:
self, self,
health=health, health=health,
damage=damage, damage=damage,
attack_range=attack_range, attackRange=attackRange,
speed_multiplier=speed_multiplier, speedMultiplier=speedMultiplier,
attack_pattern={"type": "hunter"}, # Hunt like original spiders attackPattern={"type": "hunter"}, # Hunt like original spiders
) )
self.enemies.append(enemy) self.enemies.append(enemy)
@@ -550,16 +550,16 @@ class Level:
# Handle graves and other hazards # Handle graves and other hazards
if obj.isHazard and not self.player.isJumping: if obj.isHazard and not self.player.isJumping:
if isinstance(obj, GraveObject): if isinstance(obj, GraveObject):
can_collect = obj.collect_grave_item(self.player) and not self.player.diedThisFrame canCollect = obj.collect_grave_item(self.player) and not self.player.diedThisFrame
can_fill = obj.can_fill_grave(self.player) canFill = obj.can_fill_grave(self.player)
if can_collect: if canCollect:
# Successfully collected item while ducking with shovel # Successfully collected item while ducking with shovel
# Check for item sound override # Check for item sound override
item_sound = obj.graveItem itemSound = obj.graveItem
if hasattr(obj, 'itemSoundOverride'): if hasattr(obj, 'itemSoundOverride'):
item_sound = obj.itemSoundOverride itemSound = obj.itemSoundOverride
play_sound(self.sounds[f"get_{item_sound}"]) play_sound(self.sounds[f"get_{itemSound}"])
play_sound(self.sounds.get("fill_in_grave", "shovel_dig")) # Also play fill sound play_sound(self.sounds.get("fill_in_grave", "shovel_dig")) # Also play fill sound
self.player.stats.update_stat("Items collected", 1) self.player.stats.update_stat("Items collected", 1)
# Create PowerUp to handle the item effect # Create PowerUp to handle the item effect
@@ -575,7 +575,7 @@ class Level:
obj.channel = None obj.channel = None
obj.isActive = False # Mark the grave as inactive after collection obj.isActive = False # Mark the grave as inactive after collection
continue continue
elif can_fill and obj.fill_grave(self.player): elif canFill and obj.fill_grave(self.player):
# Successfully filled empty grave with shovel # Successfully filled empty grave with shovel
play_sound( play_sound(
self.sounds.get("fill_in_grave", "shovel_dig") self.sounds.get("fill_in_grave", "shovel_dig")
@@ -643,12 +643,12 @@ class Level:
def throw_projectile(self): def throw_projectile(self):
"""Have player throw a projectile""" """Have player throw a projectile"""
proj_info = self.player.throw_projectile() projInfo = self.player.throw_projectile()
if proj_info is None: if projInfo is None:
speak("No jack o'lanterns to throw!") speak("No jack o'lanterns to throw!")
return return
self.projectiles.append(Projectile(proj_info["type"], proj_info["start_x"], proj_info["direction"])) self.projectiles.append(Projectile(projInfo["type"], projInfo["start_x"], projInfo["direction"]))
# Play throw sound # Play throw sound
play_sound(self.sounds["throw_jack_o_lantern"]) play_sound(self.sounds["throw_jack_o_lantern"])
@@ -663,11 +663,11 @@ class Level:
# Create projectile with weapon-specific properties # Create projectile with weapon-specific properties
# Use a generic projectile type for weapon projectiles # Use a generic projectile type for weapon projectiles
projectile_type = f"weapon_{weapon.name}" projectileType = f"weapon_{weapon.name}"
start_x = player.xPos + (direction * 1) # Start 1 tile away from player startX = player.xPos + (direction * 1) # Start 1 tile away from player
# Create projectile with custom speed and damage from weapon # Create projectile with custom speed and damage from weapon
projectile = Projectile(projectile_type, start_x, direction) projectile = Projectile(projectileType, startX, direction)
# Override projectile properties with weapon stats # Override projectile properties with weapon stats
projectile.damage = weapon.damage projectile.damage = weapon.damage
@@ -822,7 +822,7 @@ class Level:
obj.itemSoundOverride = soundOverrides["item"] obj.itemSoundOverride = soundOverrides["item"]
# Handle item sound overrides for coffins # Handle item sound overrides for coffins
if hasattr(obj, 'specified_item') and obj.specified_item and obj.specified_item != "random" and "item" in soundOverrides: if hasattr(obj, 'specifiedItem') and obj.specifiedItem and obj.specifiedItem != "random" and "item" in soundOverrides:
# Store the override for later use when coffin is broken # Store the override for later use when coffin is broken
if not hasattr(obj, 'itemSoundOverride'): if not hasattr(obj, 'itemSoundOverride'):
obj.itemSoundOverride = soundOverrides["item"] obj.itemSoundOverride = soundOverrides["item"]
+20 -20
View File
@@ -45,7 +45,7 @@ class Player:
self.collectedItems = [] self.collectedItems = []
self._coins = 0 # Regular bone dust for extra lives self._coins = 0 # Regular bone dust for extra lives
self._saveBoneDust = 0 # Separate bone dust counter for saves self._saveBoneDust = 0 # Separate bone dust counter for saves
self._jack_o_lantern_count = 0 self._jackOLanternCount = 0
self.shinBoneCount = 0 self.shinBoneCount = 0
# Combat related attributes # Combat related attributes
@@ -115,7 +115,7 @@ class Player:
# Check invincibility status # Check invincibility status
if self.isInvincible: if self.isInvincible:
remaining_time = ( remainingTime = (
self.invincibilityStartTime + self.invincibilityDuration - currentTime self.invincibilityStartTime + self.invincibilityDuration - currentTime
) / 1000 # Convert to seconds ) / 1000 # Convert to seconds
@@ -123,10 +123,10 @@ class Player:
if not hasattr(self, "_last_countdown"): if not hasattr(self, "_last_countdown"):
self._last_countdown = 4 # Start counting from 4 to catch 3,2,1 self._last_countdown = 4 # Start counting from 4 to catch 3,2,1
current_second = int(remaining_time) currentSecond = int(remainingTime)
if current_second < self._last_countdown and current_second <= 3 and current_second > 0: if currentSecond < self._last_countdown and currentSecond <= 3 and currentSecond > 0:
play_sound(self.sounds["end_of_invincibility_warning"]) play_sound(self.sounds["end_of_invincibility_warning"])
self._last_countdown = current_second self._last_countdown = currentSecond
# Check if invincibility has expired # Check if invincibility has expired
if currentTime - self.invincibilityStartTime >= self.invincibilityDuration: if currentTime - self.invincibilityStartTime >= self.invincibilityDuration:
@@ -147,11 +147,11 @@ class Player:
def get_jack_o_lanterns(self): def get_jack_o_lanterns(self):
"""Get number of jack o'lanterns""" """Get number of jack o'lanterns"""
return self._jack_o_lantern_count return self._jackOLanternCount
def add_jack_o_lantern(self): def add_jack_o_lantern(self):
"""Add a jack o'lantern""" """Add a jack o'lantern"""
self._jack_o_lantern_count += 1 self._jackOLanternCount += 1
def add_guts(self): def add_guts(self):
"""Apply guts, increase max_health by 2 if less than 20 else restore health""" """Apply guts, increase max_health by 2 if less than 20 else restore health"""
@@ -165,7 +165,7 @@ class Player:
if self.get_jack_o_lanterns() <= 0: if self.get_jack_o_lanterns() <= 0:
return None return None
self._jack_o_lantern_count -= 1 self._jackOLanternCount -= 1
return {"type": "jack_o_lantern", "start_x": self.xPos, "direction": 1 if self.facingRight else -1} return {"type": "jack_o_lantern", "start_x": self.xPos, "direction": 1 if self.facingRight else -1}
def get_step_distance(self): def get_step_distance(self):
@@ -220,19 +220,19 @@ class Player:
def set_health(self, value): def set_health(self, value):
"""Set health and handle death if needed.""" """Set health and handle death if needed."""
old_health = self._health oldHealth = self._health
# Oops, allow healing while invincible. # Oops, allow healing while invincible.
if self.isInvincible and value < old_health: if self.isInvincible and value < oldHealth:
return return
# Check for god mode (prevents all damage) # Check for god mode (prevents all damage)
if hasattr(self, '_godMode') and self._godMode and value < old_health: if hasattr(self, '_godMode') and self._godMode and value < oldHealth:
return return
self._health = max(0, value) # Health can't go below 0 self._health = max(0, value) # Health can't go below 0
if self._health == 0 and old_health > 0: if self._health == 0 and oldHealth > 0:
self._lives -= 1 self._lives -= 1
# Mark that player died this frame to prevent revival # Mark that player died this frame to prevent revival
self.diedThisFrame = True self.diedThisFrame = True
@@ -379,7 +379,7 @@ class Player:
elif ammoType == "hand_of_glory": elif ammoType == "hand_of_glory":
return self.collectedItems.count("hand_of_glory") >= cost return self.collectedItems.count("hand_of_glory") >= cost
elif ammoType == "jack_o_lantern": elif ammoType == "jack_o_lantern":
return self._jack_o_lantern_count >= cost return self._jackOLanternCount >= cost
else: else:
# Check for any other item type in collectedItems # Check for any other item type in collectedItems
return self.collectedItems.count(ammoType) >= cost return self.collectedItems.count(ammoType) >= cost
@@ -398,7 +398,7 @@ class Player:
for _ in range(min(cost, self.collectedItems.count("hand_of_glory"))): for _ in range(min(cost, self.collectedItems.count("hand_of_glory"))):
self.collectedItems.remove("hand_of_glory") self.collectedItems.remove("hand_of_glory")
elif ammoType == "jack_o_lantern": elif ammoType == "jack_o_lantern":
self._jack_o_lantern_count = max(0, self._jack_o_lantern_count - cost) self._jackOLanternCount = max(0, self._jackOLanternCount - cost)
else: else:
# Handle any other item type # Handle any other item type
for _ in range(min(cost, self.collectedItems.count(ammoType))): for _ in range(min(cost, self.collectedItems.count(ammoType))):
@@ -409,18 +409,18 @@ class Player:
# Check for themed mappings in weapon overrides # Check for themed mappings in weapon overrides
if hasattr(self, 'weaponOverrides') and self.weaponOverrides: if hasattr(self, 'weaponOverrides') and self.weaponOverrides:
# Define themed mappings (this could be extended or made configurable) # Define themed mappings (this could be extended or made configurable)
themed_mappings = { themedMappings = {
"shin_bone": "candy_cane", # Christmas theme example "shin_bone": "candy_cane", # Christmas theme example
"guts": "reindeer_guts" "guts": "reindeer_guts"
} }
# Check if there's a themed equivalent and if the override exists # Check if there's a themed equivalent and if the override exists
if ammoType in themed_mappings: if ammoType in themedMappings:
themed_name = themed_mappings[ammoType] themedName = themedMappings[ammoType]
# If the themed item exists in overrides, use the themed name # If the themed item exists in overrides, use the themed name
for override_key, override_data in self.weaponOverrides.items(): for overrideKey, overrideData in self.weaponOverrides.items():
if isinstance(override_data, dict) and themed_name in str(override_data): if isinstance(overrideData, dict) and themedName in str(overrideData):
return themed_name.replace("_", " ") return themedName.replace("_", " ")
# Return default name # Return default name
return ammoType.replace("_", " ") return ammoType.replace("_", " ")
+29 -29
View File
@@ -7,43 +7,43 @@ from src.weapon import Weapon
class PowerUp(Object): class PowerUp(Object):
def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100): def __init__(self, x, y, itemType, sounds, direction, leftBoundary=1, rightBoundary=100):
super().__init__(x, y, item_type, isStatic=False, isCollectible=True, isHazard=False) super().__init__(x, y, itemType, isStatic=False, isCollectible=True, isHazard=False)
self.sounds = sounds self.sounds = sounds
self.direction = direction self.direction = direction
self.speed = 0.049 # Base movement speed self.speed = 0.049 # Base movement speed
self.item_type = item_type self.itemType = itemType
self.channel = None self.channel = None
self._currentX = x # Initialize the current x position self._currentX = x # Initialize the current x position
self.left_boundary = left_boundary self.leftBoundary = leftBoundary
self.right_boundary = right_boundary self.rightBoundary = rightBoundary
def update(self, current_time, player_pos): def update(self, currentTime, playerPos):
"""Update item position""" """Update item position"""
if not self.isActive: if not self.isActive:
return False return False
# Update position # Update position
new_x = self._currentX + self.direction * self.speed newX = self._currentX + self.direction * self.speed
# Check boundaries and bounce if needed # Check boundaries and bounce if needed
if new_x < self.left_boundary: if newX < self.leftBoundary:
self._currentX = self.left_boundary self._currentX = self.leftBoundary
self.direction = 1 # Start moving right self.direction = 1 # Start moving right
elif new_x > self.right_boundary: elif newX > self.rightBoundary:
self._currentX = self.right_boundary self._currentX = self.rightBoundary
self.direction = -1 # Start moving left self.direction = -1 # Start moving left
else: else:
self._currentX = new_x self._currentX = newX
# Update positional audio # Update positional audio
if self.channel is None or not self.channel.get_busy(): if self.channel is None or not self.channel.get_busy():
self.channel = obj_play(self.sounds, "item_bounce", player_pos, self._currentX) self.channel = obj_play(self.sounds, "item_bounce", playerPos, self._currentX)
else: else:
self.channel = obj_update(self.channel, player_pos, self._currentX) self.channel = obj_update(self.channel, playerPos, self._currentX)
# Check if item is too far from player (12 tiles) # Check if item is too far from player (12 tiles)
if abs(self._currentX - player_pos) > 12: if abs(self._currentX - playerPos) > 12:
self.isActive = False self.isActive = False
if self.channel: if self.channel:
self.channel.stop() self.channel.stop()
@@ -55,22 +55,22 @@ class PowerUp(Object):
def apply_effect(self, player, level=None): def apply_effect(self, player, level=None):
"""Apply the item's effect when collected""" """Apply the item's effect when collected"""
# Map themed items to their original equivalents for game logic # Map themed items to their original equivalents for game logic
original_item_type = self._get_original_item_type(self.item_type) originalItemType = self._get_original_item_type(self.itemType)
if original_item_type == "hand_of_glory": if originalItemType == "hand_of_glory":
player.start_invincibility() player.start_invincibility()
self.check_for_custom_weapons(player) self.check_for_custom_weapons(player)
elif original_item_type == "cauldron": elif originalItemType == "cauldron":
player.restore_health() player.restore_health()
elif original_item_type == "guts": elif originalItemType == "guts":
player.add_guts() player.add_guts()
player.collectedItems.append(original_item_type) player.collectedItems.append(originalItemType)
self.check_for_nunchucks(player) self.check_for_nunchucks(player)
self.check_for_custom_weapons(player) self.check_for_custom_weapons(player)
elif original_item_type == "jack_o_lantern": elif originalItemType == "jack_o_lantern":
player.add_jack_o_lantern() player.add_jack_o_lantern()
self.check_for_custom_weapons(player) self.check_for_custom_weapons(player)
elif original_item_type == "extra_life": elif originalItemType == "extra_life":
# Don't give extra lives in survival mode # Don't give extra lives in survival mode
if level and level.levelId == 999: if level and level.levelId == 999:
# In survival mode, give bonus score instead # In survival mode, give bonus score instead
@@ -79,7 +79,7 @@ class PowerUp(Object):
play_sound(self.sounds.get("survivor_bonus", "get_extra_life")) # Use survivor_bonus sound if available play_sound(self.sounds.get("survivor_bonus", "get_extra_life")) # Use survivor_bonus sound if available
else: else:
player.extra_life() player.extra_life()
elif original_item_type == "shin_bone": # Add shin bone handling elif originalItemType == "shin_bone": # Add shin bone handling
player.shinBoneCount += 1 player.shinBoneCount += 1
player._coins += 5 player._coins += 5
player.add_save_bone_dust(5) # Add to save bone dust counter too player.add_save_bone_dust(5) # Add to save bone dust counter too
@@ -106,11 +106,11 @@ class PowerUp(Object):
self.check_for_nunchucks(player) self.check_for_nunchucks(player)
self.check_for_custom_weapons(player) self.check_for_custom_weapons(player)
elif original_item_type == "witch_broom": elif originalItemType == "witch_broom":
broomWeapon = Weapon.create_witch_broom() broomWeapon = Weapon.create_witch_broom()
player.add_weapon(broomWeapon) player.add_weapon(broomWeapon)
player.equip_weapon(broomWeapon) player.equip_weapon(broomWeapon)
elif original_item_type == "spiderweb": elif originalItemType == "spiderweb":
# Bounce player back (happens even if invincible) # Bounce player back (happens even if invincible)
player.xPos -= 3 if player.xPos > self.xPos else -3 player.xPos -= 3 if player.xPos > self.xPos else -3
@@ -193,7 +193,7 @@ class PowerUp(Object):
canCraft = False canCraft = False
break break
elif itemType == "jack_o_lantern": elif itemType == "jack_o_lantern":
if player._jack_o_lantern_count < neededCount: if player._jackOLanternCount < neededCount:
canCraft = False canCraft = False
break break
else: else:
@@ -225,10 +225,10 @@ class PowerUp(Object):
player.stats.update_stat("Items collected", 1) player.stats.update_stat("Items collected", 1)
def _get_original_item_type(self, item_type): def _get_original_item_type(self, itemType):
"""Map themed item names to their original equivalents for game logic.""" """Map themed item names to their original equivalents for game logic."""
# Define themed equivalents that should behave like original items # Define themed equivalents that should behave like original items
themed_mappings = { themedMappings = {
# Christmas theme # Christmas theme
"candy_cane": "shin_bone", "candy_cane": "shin_bone",
"reindeer_guts": "guts", "reindeer_guts": "guts",
@@ -237,4 +237,4 @@ class PowerUp(Object):
# "frozen_heart": "guts", # "frozen_heart": "guts",
} }
return themed_mappings.get(item_type, item_type) return themedMappings.get(itemType, itemType)
+5 -5
View File
@@ -2,15 +2,15 @@
class Projectile: class Projectile:
def __init__(self, projectile_type, start_x, direction): def __init__(self, projectileType, startX, direction):
self.type = projectile_type self.type = projectileType
self.x = start_x self.x = startX
self.direction = direction self.direction = direction
self.speed = 0.2 # Projectiles move faster than player self.speed = 0.2 # Projectiles move faster than player
self.isActive = True self.isActive = True
self.damage = 5 # All projectiles do same damage for now self.damage = 5 # All projectiles do same damage for now
self.range = 12 # Maximum travel distance in tiles self.range = 12 # Maximum travel distance in tiles
self.start_x = start_x self.startX = startX
def update(self): def update(self):
"""Update projectile position and check if it should still exist""" """Update projectile position and check if it should still exist"""
@@ -20,7 +20,7 @@ class Projectile:
self.x += self.direction * self.speed self.x += self.direction * self.speed
# Check if projectile has gone too far # Check if projectile has gone too far
if abs(self.x - self.start_x) > self.range: if abs(self.x - self.startX) > self.range:
self.isActive = False self.isActive = False
return False return False
+76 -76
View File
@@ -11,32 +11,32 @@ class SaveManager:
def __init__(self): def __init__(self):
"""Initialize save manager with XDG-compliant save directory""" """Initialize save manager with XDG-compliant save directory"""
# Use XDG_CONFIG_HOME or default to ~/.config # Use XDG_CONFIG_HOME or default to ~/.config
config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) configHome = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
self.save_dir = Path(config_home) / "storm-games" / "wicked-quest" self.saveDir = Path(configHome) / "storm-games" / "wicked-quest"
self.save_dir.mkdir(parents=True, exist_ok=True) self.saveDir.mkdir(parents=True, exist_ok=True)
self.max_saves = 10 self.maxSaves = 10
def create_save(self, player, current_level, game_start_time, current_game, bypass_cost=False): def create_save(self, player, currentLevel, gameStartTime, currentGame, bypassCost=False):
"""Create a save file with current game state""" """Create a save file with current game state"""
if not bypass_cost: if not bypassCost:
if not player.can_save(): if not player.can_save():
return False, "Not enough bone dust to save (need 200)" return False, "Not enough bone dust to save (need 200)"
# Validate required parameters # Validate required parameters
if current_game is None: if currentGame is None:
return False, "No game selected to save" return False, "No game selected to save"
if current_level is None: if currentLevel is None:
return False, "No current level to save" return False, "No current level to save"
# Spend the bone dust (only if not bypassing cost) # Spend the bone dust (only if not bypassing cost)
if not bypass_cost: if not bypassCost:
if not player.spend_save_bone_dust(200): if not player.spend_save_bone_dust(200):
return False, "Failed to spend bone dust" return False, "Failed to spend bone dust"
# Create save data # Create save data
save_data = { saveData = {
"player_state": { "playerState": {
"xPos": player.xPos, "xPos": player.xPos,
"yPos": player.yPos, "yPos": player.yPos,
"health": player._health, "health": player._health,
@@ -44,7 +44,7 @@ class SaveManager:
"lives": player._lives, "lives": player._lives,
"coins": player._coins, "coins": player._coins,
"saveBoneDust": player._saveBoneDust, "saveBoneDust": player._saveBoneDust,
"jackOLanternCount": player._jack_o_lantern_count, "jackOLanternCount": player._jackOLanternCount,
"shinBoneCount": player.shinBoneCount, "shinBoneCount": player.shinBoneCount,
"inventory": player.inventory, "inventory": player.inventory,
"collectedItems": player.collectedItems, "collectedItems": player.collectedItems,
@@ -56,9 +56,9 @@ class SaveManager:
"scoreboard": self._serialize_scoreboard(player.scoreboard), "scoreboard": self._serialize_scoreboard(player.scoreboard),
}, },
"game_state": { "game_state": {
"currentLevel": current_level, "currentLevel": currentLevel,
"currentGame": current_game, "currentGame": currentGame,
"gameStartTime": game_start_time, "gameStartTime": gameStartTime,
"saveTime": datetime.now(), "saveTime": datetime.now(),
}, },
"version": "1.0", "version": "1.0",
@@ -67,21 +67,21 @@ class SaveManager:
# Generate filename with timestamp # Generate filename with timestamp
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"save_{timestamp}.pickle" filename = f"save_{timestamp}.pickle"
filepath = self.save_dir / filename filepath = self.saveDir / filename
try: try:
# Write to temporary file first, then rename for atomic operation # Write to temporary file first, then rename for atomic operation
temp_filepath = filepath.with_suffix(".tmp") tempFilepath = filepath.with_suffix(".tmp")
with open(temp_filepath, "wb") as f: with open(tempFilepath, "wb") as f:
pickle.dump(save_data, f) pickle.dump(saveData, f)
f.flush() # Ensure data is written to disk f.flush() # Ensure data is written to disk
os.fsync(f.fileno()) # Force write to disk os.fsync(f.fileno()) # Force write to disk
# Atomic rename (replaces old file if it exists) # Atomic rename (replaces old file if it exists)
temp_filepath.rename(filepath) tempFilepath.rename(filepath)
# Clean up old saves if we exceed max_saves # Clean up old saves if we exceed maxSaves
self._cleanup_old_saves() self._cleanup_old_saves()
return True, f"Game saved to {filename}" return True, f"Game saved to {filename}"
@@ -205,38 +205,38 @@ class SaveManager:
scoreboard.highScores = scoreboard_data["highScores"] scoreboard.highScores = scoreboard_data["highScores"]
return scoreboard return scoreboard
def get_save_files(self): def get_saveFiles(self):
"""Get list of save files with metadata""" """Get list of save files with metadata"""
save_files = [] saveFiles = []
pattern = str(self.save_dir / "save_*.pickle") pattern = str(self.saveDir / "save_*.pickle")
for filepath in glob.glob(pattern): for filepath in glob.glob(pattern):
try: try:
with open(filepath, "rb") as f: with open(filepath, "rb") as f:
save_data = pickle.load(f) saveData = pickle.load(f)
# Validate save data structure # Validate save data structure
if not self._validate_save_data(save_data): if not self._validate_saveData(saveData):
print(f"Invalid save file structure: {filepath}") print(f"Invalid save file structure: {filepath}")
continue continue
# Extract save info # Extract save info
save_time = save_data["game_state"]["saveTime"] saveTime = saveData["game_state"]["saveTime"]
level = save_data["game_state"]["currentLevel"] level = saveData["game_state"]["currentLevel"]
game_name = save_data["game_state"]["currentGame"] gameName = saveData["game_state"]["currentGame"]
# Format display name # Format display name
formatted_time = save_time.strftime("%B %d %I:%M%p") formattedTime = saveTime.strftime("%B %d %I:%M%p")
display_name = f"{formatted_time} {game_name} Level {level}" displayName = f"{formattedTime} {gameName} Level {level}"
save_files.append( saveFiles.append(
{ {
"filepath": filepath, "filepath": filepath,
"display_name": display_name, "displayName": displayName,
"save_time": save_time, "saveTime": saveTime,
"level": level, "level": level,
"game_name": game_name, "gameName": gameName,
"save_data": save_data, "saveData": saveData,
} }
) )
except (pickle.PickleError, EOFError, OSError) as e: except (pickle.PickleError, EOFError, OSError) as e:
@@ -253,94 +253,94 @@ class SaveManager:
continue continue
# Sort by save time (newest first) # Sort by save time (newest first)
save_files.sort(key=lambda x: x["save_time"], reverse=True) saveFiles.sort(key=lambda x: x["saveTime"], reverse=True)
return save_files return saveFiles
def load_save(self, filepath): def load_save(self, filepath):
"""Load game state from save file""" """Load game state from save file"""
try: try:
with open(filepath, "rb") as f: with open(filepath, "rb") as f:
save_data = pickle.load(f) saveData = pickle.load(f)
return True, save_data return True, saveData
except Exception as e: except Exception as e:
return False, f"Failed to load save: {str(e)}" return False, f"Failed to load save: {str(e)}"
def restore_player_state(self, player, save_data): def restore_playerState(self, player, saveData):
"""Restore player state from save data""" """Restore player state from save data"""
player_state = save_data["player_state"] playerState = saveData["playerState"]
# Restore basic attributes # Restore basic attributes
player.xPos = player_state["xPos"] player.xPos = playerState["xPos"]
player.yPos = player_state["yPos"] player.yPos = playerState["yPos"]
player._health = player_state["health"] player._health = playerState["health"]
player._maxHealth = player_state["maxHealth"] player._maxHealth = playerState["maxHealth"]
player._lives = player_state["lives"] player._lives = playerState["lives"]
player._coins = player_state["coins"] player._coins = playerState["coins"]
player._saveBoneDust = player_state["saveBoneDust"] player._saveBoneDust = playerState["saveBoneDust"]
player._jack_o_lantern_count = player_state["jackOLanternCount"] player._jackOLanternCount = playerState["jackOLanternCount"]
player.shinBoneCount = player_state["shinBoneCount"] player.shinBoneCount = playerState["shinBoneCount"]
player.inventory = player_state["inventory"] player.inventory = playerState["inventory"]
player.collectedItems = player_state["collectedItems"] player.collectedItems = playerState["collectedItems"]
# Restore weapons # Restore weapons
player.weapons = self._deserialize_weapons(player_state["weapons"]) player.weapons = self._deserialize_weapons(playerState["weapons"])
# Restore custom weapon tracking data (for backward compatibility, use get with defaults) # Restore custom weapon tracking data (for backward compatibility, use get with defaults)
player.craftedCustomWeapons = set(player_state.get("craftedCustomWeapons", [])) player.craftedCustomWeapons = set(playerState.get("craftedCustomWeapons", []))
player.customWeapons = player_state.get("customWeapons", []) player.customWeapons = playerState.get("customWeapons", [])
# Restore current weapon # Restore current weapon
current_weapon_name = player_state.get("currentWeaponName") currentWeaponName = playerState.get("currentWeaponName")
if current_weapon_name: if currentWeaponName:
for weapon in player.weapons: for weapon in player.weapons:
if weapon.name == current_weapon_name: if weapon.name == currentWeaponName:
player.currentWeapon = weapon player.currentWeapon = weapon
break break
# Restore stats # Restore stats
if "stats" in player_state: if "stats" in playerState:
player.stats = self._deserialize_stats(player_state["stats"]) player.stats = self._deserialize_stats(playerState["stats"])
else: else:
from src.stat_tracker import StatTracker from src.stat_tracker import StatTracker
player.stats = StatTracker() player.stats = StatTracker()
# Restore scoreboard # Restore scoreboard
if "scoreboard" in player_state: if "scoreboard" in playerState:
player.scoreboard = self._deserialize_scoreboard(player_state["scoreboard"]) player.scoreboard = self._deserialize_scoreboard(playerState["scoreboard"])
else: else:
from libstormgames import Scoreboard from libstormgames import Scoreboard
player.scoreboard = Scoreboard() player.scoreboard = Scoreboard()
def _cleanup_old_saves(self): def _cleanup_old_saves(self):
"""Remove old save files if we exceed max_saves""" """Remove old save files if we exceed maxSaves"""
save_files = self.get_save_files() saveFiles = self.get_saveFiles()
if len(save_files) > self.max_saves: if len(saveFiles) > self.maxSaves:
# Remove oldest saves # Remove oldest saves
for save_file in save_files[self.max_saves:]: for save_file in saveFiles[self.maxSaves:]:
try: try:
os.remove(save_file["filepath"]) os.remove(save_file["filepath"])
except Exception as e: except Exception as e:
print(f"Error removing old save {save_file['filepath']}: {e}") print(f"Error removing old save {save_file['filepath']}: {e}")
def _validate_save_data(self, save_data): def _validate_saveData(self, saveData):
"""Validate that save data has required structure""" """Validate that save data has required structure"""
try: try:
# Check for required top-level keys # Check for required top-level keys
required_keys = ["player_state", "game_state", "version"] requiredKeys = ["playerState", "game_state", "version"]
if not all(key in save_data for key in required_keys): if not all(key in saveData for key in requiredKeys):
return False return False
# Check player_state structure # Check playerState structure
player_required = ["xPos", "yPos", "health", "maxHealth", "lives", "coins", "saveBoneDust"] playerRequired = ["xPos", "yPos", "health", "maxHealth", "lives", "coins", "saveBoneDust"]
if not all(key in save_data["player_state"] for key in player_required): if not all(key in saveData["playerState"] for key in playerRequired):
return False return False
# Check game_state structure # Check game_state structure
game_required = ["currentLevel", "currentGame", "gameStartTime", "saveTime"] gameRequired = ["currentLevel", "currentGame", "gameStartTime", "saveTime"]
if not all(key in save_data["game_state"] for key in game_required): if not all(key in saveData["game_state"] for key in gameRequired):
return False return False
return True return True
@@ -349,4 +349,4 @@ class SaveManager:
def has_saves(self): def has_saves(self):
"""Check if any save files exist""" """Check if any save files exist"""
return len(self.get_save_files()) > 0 return len(self.get_saveFiles()) > 0
+16 -16
View File
@@ -26,10 +26,10 @@ class SurvivalGenerator:
self.weaponOverrides = {} self.weaponOverrides = {}
self.customWeapons = [] # Custom weapons from level pack self.customWeapons = [] # Custom weapons from level pack
self.availableItems = set() # Dynamically discovered items from containers self.availableItems = set() # Dynamically discovered items from containers
self.loadLevelData() self.load_level_data()
self.parseTemplates() self.parse_templates()
def loadLevelData(self): def load_level_data(self):
"""Load all level JSON files from the game pack.""" """Load all level JSON files from the game pack."""
levelFiles = [] levelFiles = []
packPath = os.path.join(get_levels_base_path(), "levels", self.gamePack) packPath = os.path.join(get_levels_base_path(), "levels", self.gamePack)
@@ -49,7 +49,7 @@ class SurvivalGenerator:
levelNum = int(levelFile.split(".")[0]) levelNum = int(levelFile.split(".")[0])
self.levelData[levelNum] = json.load(f) self.levelData[levelNum] = json.load(f)
def parseTemplates(self): def parse_templates(self):
"""Parse all level data to extract object templates by type.""" """Parse all level data to extract object templates by type."""
for levelNum, data in self.levelData.items(): for levelNum, data in self.levelData.items():
# Store ambience and footstep sounds (remove duplicates) # Store ambience and footstep sounds (remove duplicates)
@@ -74,14 +74,14 @@ class SurvivalGenerator:
# Discover items from containers (graves and coffins) # Discover items from containers (graves and coffins)
if obj.get("type") in ["grave", "coffin"] and "item" in obj: if obj.get("type") in ["grave", "coffin"] and "item" in obj:
item_name = obj["item"] itemName = obj["item"]
# Check for sound override (themed equivalent) # Check for sound override (themed equivalent)
if "sound_overrides" in obj and "item" in obj["sound_overrides"]: if "sound_overrides" in obj and "item" in obj["sound_overrides"]:
item_name = obj["sound_overrides"]["item"] itemName = obj["sound_overrides"]["item"]
# Exclude special items that should remain rare/exclusive # Exclude special items that should remain rare/exclusive
if item_name and item_name != "random" and item_name not in ["extra_life"]: if itemName and itemName != "random" and itemName not in ["extra_life"]:
self.availableItems.add(item_name) self.availableItems.add(itemName)
# Categorize objects # Categorize objects
if "enemy_type" in obj: if "enemy_type" in obj:
@@ -154,16 +154,16 @@ class SurvivalGenerator:
# Guarantee at least one coffin per wave if coffins exist in templates # Guarantee at least one coffin per wave if coffins exist in templates
coffinTemplates = [obj for obj in self.objectTemplates if obj.get("type") == "coffin"] coffinTemplates = [obj for obj in self.objectTemplates if obj.get("type") == "coffin"]
coffin_placed = False coffinPlaced = False
if coffinTemplates: if coffinTemplates:
# Place guaranteed coffin in the first quarter of the level # Place guaranteed coffin in the first quarter of the level
coffin_x = random.randint(startBufferZone + 10, (segmentLength - endBufferZone) // 4) coffinX = random.randint(startBufferZone + 10, (segmentLength - endBufferZone) // 4)
coffin_template = random.choice(coffinTemplates) coffinTemplate = random.choice(coffinTemplates)
coffin_obj = copy.deepcopy(coffin_template) coffinObj = copy.deepcopy(coffinTemplate)
coffin_obj["item"] = "random" # Override any specified item coffinObj["item"] = "random" # Override any specified item
coffin_obj["x"] = coffin_x coffinObj["x"] = coffinX
levelData["objects"].append(coffin_obj) levelData["objects"].append(coffinObj)
coffin_placed = True coffinPlaced = True
while currentX < segmentLength - endBufferZone: while currentX < segmentLength - endBufferZone:
# Determine what to place based on probability # Determine what to place based on probability
+101 -117
View File
@@ -64,35 +64,35 @@ class WickedQuest:
return None return None
# Find active enemies # Find active enemies
active_enemies = [] activeEnemies = []
for enemy in self.currentLevel.enemies: for enemy in self.currentLevel.enemies:
if enemy.isActive: if enemy.isActive:
distance = abs(enemy.xPos - self.player.xPos) distance = abs(enemy.xPos - self.player.xPos)
direction = "right" if enemy.xPos > self.player.xPos else "left" direction = "right" if enemy.xPos > self.player.xPos else "left"
active_enemies.append((enemy, distance, direction)) activeEnemies.append((enemy, distance, direction))
if not active_enemies: if not activeEnemies:
return None return None
# Sort by distance and get closest # Sort by distance and get closest
active_enemies.sort(key=lambda x: x[1]) activeEnemies.sort(key=lambda x: x[1])
enemy, distance, direction = active_enemies[0] enemy, distance, direction = activeEnemies[0]
# Convert distance to natural language # Convert distance to natural language
if distance == 0: if distance == 0:
return f"{enemy.enemyType} right on top of you" return f"{enemy.enemyType} right on top of you"
elif distance <= 10: elif distance <= 10:
distance_desc = "very close" distanceDesc = "very close"
elif distance <= 30: elif distance <= 30:
distance_desc = "close" distanceDesc = "close"
elif distance <= 60: elif distance <= 60:
distance_desc = "far" distanceDesc = "far"
elif distance <= 100: elif distance <= 100:
distance_desc = "very far" distanceDesc = "very far"
else: else:
distance_desc = "extremely far" distanceDesc = "extremely far"
return f"{enemy.enemyType} {distance_desc} to the {direction}" return f"{enemy.enemyType} {distanceDesc} to the {direction}"
def process_console_command(self, command): def process_console_command(self, command):
"""Process console commands and execute their effects.""" """Process console commands and execute their effects."""
@@ -132,9 +132,9 @@ class WickedQuest:
enemyCount = len([enemy for enemy in self.currentLevel.enemies if enemy.isActive]) enemyCount = len([enemy for enemy in self.currentLevel.enemies if enemy.isActive])
if enemyCount > 0: if enemyCount > 0:
speak(f"{enemyCount} enemies remaining on this level") speak(f"{enemyCount} enemies remaining on this level")
closest_enemy_info = self.get_closest_enemy_info() closestEnemyInfo = self.get_closest_enemy_info()
if closest_enemy_info: if closestEnemyInfo:
speak(f"Closest enemy: {closest_enemy_info}") speak(f"Closest enemy: {closestEnemyInfo}")
else: else:
speak("No active enemies on this level") speak("No active enemies on this level")
else: else:
@@ -190,14 +190,14 @@ class WickedQuest:
elif command == "diemonsterdie": elif command == "diemonsterdie":
# Give 100 jack o'lanterns # Give 100 jack o'lanterns
if self.player: if self.player:
self.player._jack_o_lantern_count += 100 self.player._jackOLanternCount += 100
speak(f"100 jack o'lanterns granted. You now have {self.player.get_jack_o_lanterns()} jack o'lanterns") speak(f"100 jack o'lanterns granted. You now have {self.player.get_jack_o_lanterns()} jack o'lanterns")
if 'get_jack_o_lantern' in self.get_sounds(): if 'get_jack_o_lantern' in self.get_sounds():
play_sound(self.get_sounds()['get_jack_o_lantern']) play_sound(self.get_sounds()['get_jack_o_lantern'])
elif command == "murderland": elif command == "murderland":
# Set jack o'lanterns to exactly 13 (the special number) # Set jack o'lanterns to exactly 13 (the special number)
if self.player: if self.player:
self.player._jack_o_lantern_count = 13 self.player._jackOLanternCount = 13
speak("13 jack o'lanterns, this power courses through my soul.") speak("13 jack o'lanterns, this power courses through my soul.")
if 'get_jack_o_lantern' in self.get_sounds(): if 'get_jack_o_lantern' in self.get_sounds():
play_sound(self.get_sounds()['get_jack_o_lantern']) play_sound(self.get_sounds()['get_jack_o_lantern'])
@@ -238,7 +238,7 @@ class WickedQuest:
self.currentLevel.levelId, self.currentLevel.levelId,
self.gameStartTime, self.gameStartTime,
self.currentGame, self.currentGame,
bypass_cost=True # Skip bone dust requirement bypassCost=True # Skip bone dust requirement
) )
if success: if success:
@@ -273,7 +273,7 @@ class WickedQuest:
self.player._saveBoneDust = 999 self.player._saveBoneDust = 999
# Max jack o'lanterns (set to 99 for practicality) # Max jack o'lanterns (set to 99 for practicality)
self.player._jack_o_lantern_count = 99 self.player._jackOLanternCount = 99
# Grant all weapons with level pack override support # Grant all weapons with level pack override support
weaponOverrides = getattr(self.currentLevel, 'weaponSoundOverrides', {}) if self.currentLevel else {} weaponOverrides = getattr(self.currentLevel, 'weaponSoundOverrides', {}) if self.currentLevel else {}
@@ -438,16 +438,16 @@ class WickedQuest:
def load_game_menu(self): def load_game_menu(self):
"""Display load game menu with available saves using instruction_menu""" """Display load game menu with available saves using instruction_menu"""
save_files = self.saveManager.get_save_files() saveFiles = self.saveManager.get_save_files()
if not save_files: if not saveFiles:
messagebox("No save files found.") messagebox("No save files found.")
return None return None
# Create menu options # Create menu options
options = [] options = []
for save_file in save_files: for saveFile in saveFiles:
options.append(save_file['display_name']) options.append(saveFile['display_name'])
options.append("Cancel") options.append("Cancel")
@@ -458,9 +458,9 @@ class WickedQuest:
return None return None
else: else:
# Find the corresponding save file # Find the corresponding save file
for save_file in save_files: for saveFile in saveFiles:
if save_file['display_name'] == choice: if saveFile['display_name'] == choice:
return save_file return saveFile
return None return None
def auto_save(self): def auto_save(self):
@@ -559,56 +559,56 @@ class WickedQuest:
elif keys[pygame.K_e]: elif keys[pygame.K_e]:
# Weapon and ammo status # Weapon and ammo status
if player.currentWeapon: if player.currentWeapon:
weapon_name = getattr(player.currentWeapon, 'displayName', player.currentWeapon.name.replace("_", " ")) weaponName = getattr(player.currentWeapon, 'displayName', player.currentWeapon.name.replace("_", " "))
status_message = f"Wielding {weapon_name}" statusMessage = f"Wielding {weaponName}"
# Check if it's a projectile weapon - always show ammo for projectile weapons # Check if it's a projectile weapon - always show ammo for projectile weapons
weapon_type = getattr(player.currentWeapon, 'weaponType', 'melee') weaponType = getattr(player.currentWeapon, 'weaponType', 'melee')
if weapon_type == "projectile": if weaponType == "projectile":
ammo_type = getattr(player.currentWeapon, 'ammoType', None) ammoType = getattr(player.currentWeapon, 'ammoType', None)
if ammo_type: if ammoType:
ammo_count = 0 ammoCount = 0
ammo_display_name = ammo_type.replace("_", " ") # Default fallback ammoDisplayName = ammoType.replace("_", " ") # Default fallback
# Get current ammo count based on ammo type # Get current ammo count based on ammo type
if ammo_type == "bone_dust": if ammoType == "bone_dust":
ammo_count = player.get_coins() ammoCount = player.get_coins()
ammo_display_name = "bone dust" ammoDisplayName = "bone dust"
elif ammo_type == "shin_bone": elif ammoType == "shin_bone":
ammo_count = player.shinBoneCount ammoCount = player.shinBoneCount
ammo_display_name = "shin bones" ammoDisplayName = "shin bones"
elif ammo_type == "jack_o_lantern": elif ammoType == "jack_o_lantern":
ammo_count = player._jack_o_lantern_count ammoCount = player._jackOLanternCount
ammo_display_name = "jack o'lanterns" ammoDisplayName = "jack o'lanterns"
elif ammo_type == "guts": elif ammoType == "guts":
ammo_count = player.collectedItems.count("guts") ammoCount = player.collectedItems.count("guts")
ammo_display_name = "guts" ammoDisplayName = "guts"
elif ammo_type == "hand_of_glory": elif ammoType == "hand_of_glory":
ammo_count = player.collectedItems.count("hand_of_glory") ammoCount = player.collectedItems.count("hand_of_glory")
ammo_display_name = "hands of glory" ammoDisplayName = "hands of glory"
else: else:
# Check for any other item type in collectedItems # Check for any other item type in collectedItems
ammo_count = player.collectedItems.count(ammo_type) ammoCount = player.collectedItems.count(ammoType)
status_message += f". {ammo_count} {ammo_display_name}" statusMessage += f". {ammoCount} {ammoDisplayName}"
speak(status_message) speak(statusMessage)
else: else:
speak("No weapon equipped") speak("No weapon equipped")
elif keys[pygame.K_h]: elif keys[pygame.K_h]:
speak(f"{player.get_health()} health of {player.get_max_health()}") speak(f"{player.get_health()} health of {player.get_max_health()}")
elif keys[pygame.K_i]: elif keys[pygame.K_i]:
if self.currentLevel.levelId == 999: if self.currentLevel.levelId == 999:
base_info = f"Wave {self.survivalWave}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this wave so far. {player.get_lives()} lives remaining." baseInfo = f"Wave {self.survivalWave}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this wave so far. {player.get_lives()} lives remaining."
else: else:
base_info = f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this level so far. {player.get_lives()} lives remaining." baseInfo = f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this level so far. {player.get_lives()} lives remaining."
# Add closest enemy info # Add closest enemy info
closest_enemy_info = self.get_closest_enemy_info() closestEnemyInfo = self.get_closest_enemy_info()
if closest_enemy_info: if closestEnemyInfo:
speak(f"{base_info} {closest_enemy_info}") speak(f"{baseInfo} {closestEnemyInfo}")
else: else:
speak(base_info) speak(baseInfo)
if keys[pygame.K_l]: if keys[pygame.K_l]:
speak(f"{player.get_lives()} lives") speak(f"{player.get_lives()} lives")
if keys[pygame.K_j]: # Check jack o'lanterns if keys[pygame.K_j]: # Check jack o'lanterns
@@ -898,39 +898,39 @@ class WickedQuest:
while True: while True:
# Add load game option if saves exist # Add load game option if saves exist
custom_options = [] customOptions = []
if self.saveManager.has_saves(): if self.saveManager.has_saves():
custom_options.append("load_game") customOptions.append("load_game")
choice = game_menu(self.get_sounds(), None, *custom_options) choice = game_menu(self.get_sounds(), None, *customOptions)
if choice == "exit": if choice == "exit":
exit_game() exit_game()
elif choice == "load_game": elif choice == "load_game":
selected_save = self.load_game_menu() selectedSave = self.load_game_menu()
if selected_save: if selectedSave:
success, save_data = self.saveManager.load_save(selected_save['filepath']) success, saveData = self.saveManager.load_save(selectedSave['filepath'])
if success: if success:
# Load the saved game # Load the saved game
self.currentGame = save_data['game_state']['currentGame'] self.currentGame = saveData['game_state']['currentGame']
self.gameStartTime = save_data['game_state']['gameStartTime'] self.gameStartTime = saveData['game_state']['gameStartTime']
current_level = save_data['game_state']['currentLevel'] currentLevel = saveData['game_state']['currentLevel']
# Initialize pack-specific sound system # Initialize pack-specific sound system
self.initialize_pack_sounds() self.initialize_pack_sounds()
# Load the level # Load the level
if self.load_level(current_level): if self.load_level(currentLevel):
# Restore player state # Restore player state
self.saveManager.restore_player_state(self.player, save_data) self.saveManager.restore_player_state(self.player, saveData)
# Re-apply weapon overrides after restoring player state to ensure # Re-apply weapon overrides after restoring player state to ensure
# sound/name overrides work with restored weapon properties # sound/name overrides work with restored weapon properties
if hasattr(self.currentLevel, 'weaponOverrides') and self.currentLevel.weaponOverrides: if hasattr(self.currentLevel, 'weaponOverrides') and self.currentLevel.weaponOverrides:
self.currentLevel._apply_weapon_overrides(self.currentLevel.weaponOverrides) self.currentLevel._apply_weapon_overrides(self.currentLevel.weaponOverrides)
self.game_loop(current_level) self.game_loop(currentLevel)
else: else:
messagebox("Failed to load saved level.") messagebox("Failed to load saved level.")
else: else:
messagebox(f"Failed to load save: {save_data}") messagebox(f"Failed to load save: {saveData}")
elif choice == "play": elif choice == "play":
self.currentGame = select_game(self.get_sounds()) self.currentGame = select_game(self.get_sounds())
if self.currentGame is None: if self.currentGame is None:
@@ -947,13 +947,13 @@ class WickedQuest:
continue continue
if self.currentGame: if self.currentGame:
# Ask player to choose game mode # Ask player to choose game mode
mode_choice = game_mode_menu(self.get_sounds(), self.currentGame) modeChoice = game_mode_menu(self.get_sounds(), self.currentGame)
if mode_choice == "story": if modeChoice == "story":
self.player = None # Reset player for new game self.player = None # Reset player for new game
self.gameStartTime = pygame.time.get_ticks() self.gameStartTime = pygame.time.get_ticks()
if self.load_level(1): if self.load_level(1):
self.game_loop() self.game_loop()
elif mode_choice == "survival": elif modeChoice == "survival":
self.start_survival_mode() self.start_survival_mode()
elif choice == "high_scores": elif choice == "high_scores":
board = Scoreboard() board = Scoreboard()
@@ -1116,12 +1116,12 @@ class WickedQuest:
self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame) self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame)
def game_mode_menu(sounds, game_dir=None): def game_mode_menu(sounds, gameDir=None):
"""Display game mode selection menu using instruction_menu. """Display game mode selection menu using instruction_menu.
Args: Args:
sounds (dict): Dictionary of loaded sound effects sounds (dict): Dictionary of loaded sound effects
game_dir (str): Current game directory to check for instructions/credits gameDir (str): Current game directory to check for instructions/credits
Returns: Returns:
str: Selected game mode or None if cancelled str: Selected game mode or None if cancelled
@@ -1130,79 +1130,63 @@ def game_mode_menu(sounds, game_dir=None):
import os import os
# Build base menu options # Build base menu options
menu_options = ["Story", "Survival Mode"] menuOptions = ["Story", "Survival Mode"]
# Check for level pack specific files if game directory is provided # Check for level pack specific files if game directory is provided
if game_dir: if gameDir:
try: try:
game_path = get_game_dir_path(game_dir) gamePath = get_game_dir_path(gameDir)
# Check for instructions.txt # Check for instructions.txt
instructions_path = os.path.join(game_path, "instructions.txt") instructionsPath = os.path.join(gamePath, "instructions.txt")
if os.path.exists(instructions_path): if os.path.exists(instructionsPath):
menu_options.append("Instructions") menuOptions.append("Instructions")
# Check for credits.txt # Check for credits.txt
credits_path = os.path.join(game_path, "credits.txt") creditsPath = os.path.join(gamePath, "credits.txt")
if os.path.exists(credits_path): if os.path.exists(creditsPath):
menu_options.append("Credits") menuOptions.append("Credits")
except Exception: except Exception:
# If there's any error checking files, just continue with basic menu # If there's any error checking files, just continue with basic menu
pass pass
while True: while True:
choice = instruction_menu(sounds, "Select game mode:", *menu_options) choice = instruction_menu(sounds, "Select game mode:", *menuOptions)
if choice == "Story": if choice == "Story":
return "story" return "story"
elif choice == "Survival Mode": elif choice == "Survival Mode":
return "survival" return "survival"
elif choice == "Instructions" and game_dir: elif choice == "Instructions" and gameDir:
# Display instructions file # Display instructions file
try: try:
game_path = get_game_dir_path(game_dir) gamePath = get_game_dir_path(gameDir)
instructions_path = os.path.join(game_path, "instructions.txt") instructionsPath = os.path.join(gamePath, "instructions.txt")
print(f"DEBUG: Looking for instructions at: {instructions_path}") if os.path.exists(instructionsPath):
if os.path.exists(instructions_path): with open(instructionsPath, 'r', encoding='utf-8') as f:
print("DEBUG: Instructions file found, loading content...") instructionsContent = f.read()
with open(instructions_path, 'r', encoding='utf-8') as f:
instructions_content = f.read()
print(f"DEBUG: Content length: {len(instructions_content)} characters")
print("DEBUG: Calling display_text...")
# Convert string to list of lines for display_text # Convert string to list of lines for display_text
content_lines = instructions_content.split('\n') contentLines = instructionsContent.split('\n')
print(f"DEBUG: Split into {len(content_lines)} lines") display_text(contentLines)
display_text(content_lines)
print("DEBUG: display_text returned")
else: else:
print("DEBUG: Instructions file not found at expected path")
speak("Instructions file not found") speak("Instructions file not found")
except Exception as e: except Exception as e:
print(f"DEBUG: Error loading instructions: {str(e)}")
speak(f"Error loading instructions: {str(e)}") speak(f"Error loading instructions: {str(e)}")
elif choice == "Credits" and game_dir: elif choice == "Credits" and gameDir:
# Display credits file # Display credits file
try: try:
game_path = get_game_dir_path(game_dir) gamePath = get_game_dir_path(gameDir)
credits_path = os.path.join(game_path, "credits.txt") creditsPath = os.path.join(gamePath, "credits.txt")
print(f"DEBUG: Looking for credits at: {credits_path}") if os.path.exists(creditsPath):
if os.path.exists(credits_path): with open(creditsPath, 'r', encoding='utf-8') as f:
print("DEBUG: Credits file found, loading content...") creditsContent = f.read()
with open(credits_path, 'r', encoding='utf-8') as f:
credits_content = f.read()
print(f"DEBUG: Content length: {len(credits_content)} characters")
print("DEBUG: Calling display_text...")
# Convert string to list of lines for display_text # Convert string to list of lines for display_text
content_lines = credits_content.split('\n') contentLines = creditsContent.split('\n')
print(f"DEBUG: Split into {len(content_lines)} lines") display_text(contentLines)
display_text(content_lines)
print("DEBUG: display_text returned")
else: else:
print("DEBUG: Credits file not found at expected path")
speak("Credits file not found") speak("Credits file not found")
except Exception as e: except Exception as e:
print(f"DEBUG: Error loading credits: {str(e)}")
speak(f"Error loading credits: {str(e)}") speak(f"Error loading credits: {str(e)}")
else: else:
return None return None