From 5dd78a168769d4b0e4dfb624824abf15930d72e9 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 20 Sep 2025 04:10:32 -0400 Subject: [PATCH] Added override capabilities for lots of items, weapons, and hazards. --- levels/README.md | 180 +++++++++++++++++++++++++++++++++++++++++- src/catapult.py | 3 +- src/enemy.py | 4 +- src/grasping_hands.py | 8 +- src/grave.py | 4 +- src/level.py | 88 +++++++++++++++++++-- src/object.py | 1 + src/player.py | 2 +- src/powerup.py | 37 ++++++--- src/save_manager.py | 10 ++- src/weapon.py | 1 + wicked_quest.py | 97 ++++++++++++++++++++--- 12 files changed, 392 insertions(+), 43 deletions(-) diff --git a/levels/README.md b/levels/README.md index 86a5d28..6b19d1a 100644 --- a/levels/README.md +++ b/levels/README.md @@ -81,7 +81,7 @@ Instead of using a simple `"description"` field, levels can now include interact - `narrative`: Set to `true` for descriptive text entries (no speaker) - `sound`: Optional sound file to play with this dialogue entry. If no sound is specified, the system will automatically play `sounds/dialogue.ogg` if it exists. -**Note:** Levels can use either `"description"` (traditional format) or `"dialog"` (new interactive format), but not both. The dialogue system takes precedence if both are present. +**Note:** Levels can include both `"description"` and `"dialog"`. When both are present, the dialogue plays first, followed by the standard level description message format ("Level X, Name. Description."). ## Adding Objects @@ -385,13 +385,187 @@ sounds/Samhain Showdown/ambience ├── howling_winds.ogg ``` -### Sound Override System +### Advanced Sound Override System + +Beyond simple file replacement, Wicked Quest now supports granular sound overrides directly in your level JSON files. This allows thematic consistency where a catapult becomes a "snowball launcher" or grasping hands become an "avalanche" - same mechanics, different sounds and feel. + +#### Weapon Sound Overrides + +Override weapon sounds and names globally for an entire level: + +```json +{ + "level_id": 1, + "name": "Winter Wonderland", + "weapon_sound_overrides": { + "rusty_shovel": { + "name": "rusty snow shovel", + "attack_sound": "player_snow_shovel_attack", + "hit_sound": "player_snow_shovel_hit" + }, + "nunchucks": { + "name": "ice sickles", + "attack_sound": "player_ice_sickles_attack", + "hit_sound": "player_ice_sickles_hit" + }, + "witch_broom": { + "name": "snow broom", + "attack_sound": "player_snow_broom_attack", + "hit_sound": "player_snow_broom_hit" + } + } +} +``` + +**Weapon Override Properties:** +- `name`: Display name for the weapon (e.g., "ice sickles" instead of "nunchucks") +- `attack_sound`: Sound played when attacking +- `hit_sound`: Sound played when hitting an enemy + +#### Object Sound Overrides + +Override sounds for individual objects in your level: + +```json +{ + "x": 25, + "y": 0, + "type": "catapult", + "fire_interval": 3000, + "range": 30, + "sound_overrides": { + "base": "snowball_launcher", + "launch": "snowball_launcher_launch" + } +} +``` + +```json +{ + "x_range": [40, 60], + "y": 0, + "type": "grasping_hands", + "delay": 1000, + "sound_overrides": { + "base": "avalanche" + } +} +``` + +```json +{ + "x": 35, + "y": 0, + "type": "grave", + "item": "shin_bone", + "sound_overrides": { + "base": "snow_mound", + "item": "candy_cane" + } +} +``` + +**Object Sound Override Properties:** +- `base`: Override the main ambient sound (e.g., "catapult" → "snowball_launcher") +- `launch`: Override launch sound for catapults (e.g., "catapult_launch" → "snowball_launcher_launch") +- `item`: Override pickup sound for grave items (e.g., "get_shin_bone.ogg" → "get_candy_cane.ogg") +- `warning_message`: Override warning message for grasping hands (e.g., "The ground crumbles as snow begins to avalanche!") +- `death_message`: Override death message for grasping hands (e.g., "You vanish under tons of snow!") + +#### Themed Item Equivalents + +The sound override system includes intelligent item mapping for crafting consistency. Certain themed items automatically behave like their original counterparts: + +**Christmas Theme:** +- `"candy_cane"` → Functions as `"shin_bone"` (increments shin bone count) +- `"reindeer_guts"` → Functions as `"guts"` (enables nunchucks crafting) + +**Result:** Collecting 2 candy canes + reindeer guts = nunchucks (can be renamed to "ice sickles") + +This system allows complete thematic consistency where players collect "2 Candy Canes + Reindeer Guts = Ice Sickles" while preserving all original game mechanics. The mapping works automatically across any level pack - simply use themed item names and they'll function correctly. + +**Adding New Themed Equivalents:** +To add your own themed items, modify the `themed_mappings` in `src/powerup.py`: +```python +themed_mappings = { + "your_bone_item": "shin_bone", + "your_guts_item": "guts", +} +``` + +#### Complete Thematic Example + +Here's how to transform a Halloween level into a Christmas level using sound overrides: + +```json +{ + "level_id": 1, + "name": "Winter Siege", + "description": "Santa's workshop is under attack by snow witches!", + "weapon_sound_overrides": { + "rusty_shovel": { + "name": "snow shovel", + "attack_sound": "player_snow_shovel_attack", + "hit_sound": "player_snow_shovel_hit" + } + }, + "objects": [ + { + "x": 25, + "y": 0, + "type": "catapult", + "sound_overrides": { + "base": "snowball_launcher", + "launch": "snowball_launcher_launch" + } + }, + { + "x_range": [40, 60], + "y": 12, + "type": "skull_storm", + "sound_overrides": { + "base": "snowball_storm" + } + }, + { + "x": 75, + "y": 0, + "type": "grave", + "item": "shin_bone", + "sound_overrides": { + "base": "snow_pile", + "item": "candy_cane" + } + }, + { + "x_range": [90, 110], + "y": 0, + "type": "grasping_hands", + "sound_overrides": { + "base": "avalanche", + "warning_message": "The ground crumbles as snow begins to avalanche!", + "death_message": "You vanish under tons of snow!" + } + } + ] +} +``` + +**Result:** +- Weapons sound winter-themed when attacking +- "Catapult" becomes "Snowball Launcher" with appropriate launch sounds +- "Skull Storm" becomes "Snowball Storm" +- "Graves" become "Snow Piles" containing "Candy Canes" instead of "Shin Bones" +- "Grasping Hands" becomes "Avalanche" with snow-themed death messages +- All mechanics remain identical - only audio and messaging changes + +#### Legacy Sound Override System - **Custom ambience:** Place in `sounds/[Pack Name]/ambience/` - **Custom enemy sounds:** Place in `sounds/[Pack Name]/` - **Custom footsteps:** Reference in level JSON as `"footstep_sound"` - **Ending scene:** Add `end.ogg` in the level pack directory -This system allows complete audio customization. For example, skull storms could become firestorms just by replacing the skull storm sounds in your pack's sound directory. +This legacy system allows complete audio customization through file replacement. For example, skull storms could become firestorms just by replacing the skull storm sounds in your pack's sound directory. ## Complete Example Level diff --git a/src/catapult.py b/src/catapult.py index ab25efb..3a3e720 100644 --- a/src/catapult.py +++ b/src/catapult.py @@ -73,13 +73,14 @@ class Catapult(Object): self.launchDelay = 900 # Time between launch sound and pumpkin firing self.pendingPumpkin = None # Store pending pumpkin data self.pumpkinLaunchTime = 0 # When to launch the pending pumpkin + self.launchSound = "catapult_launch" # Configurable launch sound def fire(self, currentTime, player): """Start the firing sequence""" self.lastFireTime = currentTime # Play launch sound using directional audio - play_directional_sound(self.sounds, "catapult_launch", player.xPos, self.xPos) + play_directional_sound(self.sounds, self.launchSound, player.xPos, self.xPos) # Set up pending pumpkin isHigh = random.choice([True, False]) diff --git a/src/enemy.py b/src/enemy.py index 0093d88..239cd5d 100644 --- a/src/enemy.py +++ b/src/enemy.py @@ -261,8 +261,8 @@ class Enemy(Object): # Handle witch-specific drops if self.enemyType == "witch": # Determine which item to drop - hasBroom = any(weapon.name == "witch_broom" for weapon in self.level.player.weapons) - hasNunchucks = any(weapon.name == "nunchucks" for weapon in self.level.player.weapons) + hasBroom = any(weapon.originalName == "witch_broom" for weapon in self.level.player.weapons) + hasNunchucks = any(weapon.originalName == "nunchucks" for weapon in self.level.player.weapons) # Drop witch_broom only if player has neither broom nor nunchucks itemType = "witch_broom" if not (hasBroom or hasNunchucks) else "cauldron" diff --git a/src/grasping_hands.py b/src/grasping_hands.py index f36d9ae..c222d09 100644 --- a/src/grasping_hands.py +++ b/src/grasping_hands.py @@ -21,6 +21,10 @@ class GraspingHands(Object): self.delay = delay # Delay in milliseconds before ground starts crumbling self.crumble_speed = crumble_speed # How fast the crumbling catches up (tiles per frame) + # Default messages (can be overridden by sound override system) + self.warningMessage = "The ground crumbles as the dead reach for you." + self.deathMessage = "The hands of the dead drag you down!" + # State tracking self.isTriggered = False # Has the player entered the zone? self.triggerTime = 0 # When did the player enter the zone? @@ -53,7 +57,7 @@ class GraspingHands(Object): # Play initial warning sound play_sound(self.sounds["grasping_hands_start"]) - speak("The ground crumbles as the dead reach for you.") + speak(self.warningMessage) def reset(self): """Reset the trap when player leaves the range""" @@ -114,7 +118,7 @@ class GraspingHands(Object): obj_stop(self.crumbleChannel) self.crumbleChannel = None - speak("The hands of the dead drag you down!") + speak(self.deathMessage) player.set_health(0) return True # Player is invincible - no warning needed diff --git a/src/grave.py b/src/grave.py index 07270b8..1d3e7e0 100644 --- a/src/grave.py +++ b/src/grave.py @@ -30,7 +30,7 @@ class GraveObject(Object): player.isDucking and not player.isRunning and player.currentWeapon - and player.currentWeapon.name == "rusty_shovel" + and player.currentWeapon.originalName == "rusty_shovel" ): self.isCollected = True # Mark as collected when collection succeeds return True @@ -52,7 +52,7 @@ class GraveObject(Object): player.isDucking and not player.isRunning and player.currentWeapon - and player.currentWeapon.name == "rusty_shovel" + and player.currentWeapon.originalName == "rusty_shovel" ) def fill_grave(self, player): diff --git a/src/level.py b/src/level.py index b8163b7..fd65da3 100644 --- a/src/level.py +++ b/src/level.py @@ -41,12 +41,23 @@ class Level: # Pass footstep sound to player self.player.set_footstep_sound(self.footstepSound) + # Apply weapon sound overrides if specified + if "weapon_sound_overrides" in levelData: + self._apply_weapon_overrides(levelData["weapon_sound_overrides"]) + # Level intro message (skip for survival mode) if levelData["level_id"] != 999: # 999 is survival mode # Check if level uses new dialog format or old description format if "dialog" in levelData: # Use new dialog system with sound support messagebox(levelData["dialog"], self.sounds) + # After dialogue, show standard level description if present + if "description" in levelData: + levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. " + if self.isLocked: + levelIntro += "This is a boss level. You must defeat all enemies before you can advance. " + levelIntro += levelData["description"] + messagebox(levelIntro) elif "description" in levelData: # Use traditional description format levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. " @@ -106,6 +117,9 @@ class Level: fireInterval=obj.get("fireInterval", 5000), firingRange=obj.get("range", 20), ) + # Apply sound overrides if specified + if "sound_overrides" in obj: + self._apply_object_sound_overrides(catapult, obj["sound_overrides"]) self.objects.append(catapult) # Check if this is grasping hands elif obj.get("type") == "grasping_hands": @@ -116,6 +130,9 @@ class Level: delay=obj.get("delay", 1000), crumble_speed=obj.get("crumble_speed", 0.03), ) + # Apply sound overrides if specified + if "sound_overrides" in obj: + self._apply_object_sound_overrides(graspingHands, obj["sound_overrides"]) self.objects.append(graspingHands) # Check if this is a grave elif obj.get("type") == "grave": @@ -126,6 +143,9 @@ class Level: item=obj.get("item", None), zombieSpawnChance=obj.get("zombie_spawn_chance", 0), ) + # Apply sound overrides if specified + if "sound_overrides" in obj: + self._apply_object_sound_overrides(grave, obj["sound_overrides"]) self.objects.append(grave) # Check if this is a skull storm elif obj.get("type") == "skull_storm": @@ -138,6 +158,9 @@ class Level: obj.get("frequency", {}).get("min", 2), obj.get("frequency", {}).get("max", 5), ) + # Apply sound overrides if specified + if "sound_overrides" in obj: + self._apply_object_sound_overrides(skullStorm, obj["sound_overrides"]) self.objects.append(skullStorm) # Check if this is a coffin elif obj.get("type") == "coffin": @@ -154,7 +177,7 @@ class Level: # Check distance from graves isValidPosition = True for existingObj in self.objects: - if existingObj.soundName == "grave" and not hasattr(existingObj, "graveItem"): + if existingObj.originalSoundName == "grave" and not hasattr(existingObj, "graveItem"): distance = abs(obj["x"] - existingObj.xPos) if distance < 3: isValidPosition = False @@ -203,6 +226,9 @@ class Level: isHazard=obj.get("hazard", False), zombieSpawnChance=obj.get("zombieSpawnChance", 0), ) + # Apply sound overrides if specified + if "sound_overrides" in obj: + self._apply_object_sound_overrides(gameObject, obj["sound_overrides"]) self.objects.append(gameObject) enemyCount = len(self.enemies) coffinCount = sum(1 for obj in self.objects if hasattr(obj, "isBroken")) @@ -219,7 +245,7 @@ class Level: continue # Check for potential zombie spawn from graves - if obj.soundName == "grave" and obj.zombieSpawnChance > 0 and not obj.hasSpawned: + if obj.originalSoundName == "grave" and obj.zombieSpawnChance > 0 and not obj.hasSpawned: distance = abs(self.player.xPos - obj.xPos) if distance < 6: # Within 6 tiles @@ -371,7 +397,7 @@ class Level: # Handle grave edge warnings if ( - obj.isHazard and obj.soundName != "spiderweb" and not isinstance(obj, GraspingHands) + obj.isHazard and obj.originalSoundName != "spiderweb" and not isinstance(obj, GraspingHands) ): # Exclude spiderwebs and grasping hands distance = abs(self.player.xPos - obj.xPos) currentTime = pygame.time.get_ticks() @@ -396,9 +422,9 @@ class Level: if currentPos not in obj.collectedPositions: play_sound(self.sounds[f"get_{obj.soundName}"]) obj.collect_at_position(currentPos) - self.player.collectedItems.append(obj.soundName) + self.player.collectedItems.append(obj.originalSoundName) self.player.stats.update_stat("Items collected", 1) - if obj.soundName == "bone_dust": + if obj.originalSoundName == "bone_dust": self.player._coins += 1 self.player.add_save_bone_dust(1) # Add to save bone dust counter too self.levelScore += 100 @@ -427,7 +453,7 @@ class Level: continue # Handle spiderweb - this should trigger for both walking and jumping if not ducking - if obj.soundName == "spiderweb" and not self.player.isDucking: + if obj.originalSoundName == "spiderweb" and not self.player.isDucking: # Create and apply web effect webEffect = PowerUp( obj.xPos, obj.yPos, "spiderweb", self.sounds, 0 # No direction needed since it's just for effect @@ -449,7 +475,11 @@ class Level: if can_collect: # Successfully collected item while ducking with shovel - play_sound(self.sounds[f"get_{obj.graveItem}"]) + # Check for item sound override + item_sound = obj.graveItem + if hasattr(obj, 'itemSoundOverride'): + item_sound = obj.itemSoundOverride + play_sound(self.sounds[f"get_{item_sound}"]) play_sound(self.sounds.get("fill_in_grave", "shovel_dig")) # Also play fill sound self.player.stats.update_stat("Items collected", 1) # Create PowerUp to handle the item effect @@ -492,7 +522,7 @@ class Level: # Check for level completion - takes precedence over everything except death if self.player.get_health() > 0: for obj in self.objects: - if obj.soundName == "end_of_level": + if obj.originalSoundName == "end_of_level": # Check if player has reached or passed the end marker if self.player.xPos >= obj.xPos: # If level is locked, check for remaining enemies @@ -541,3 +571,45 @@ class Level: self.projectiles.append(Projectile(proj_info["type"], proj_info["start_x"], proj_info["direction"])) # Play throw sound play_sound(self.sounds["throw_jack_o_lantern"]) + + def _apply_weapon_overrides(self, weaponOverrides): + """Apply sound and name overrides to player weapons based on level pack theme.""" + for weapon in self.player.weapons: + if weapon.name in weaponOverrides: + overrides = weaponOverrides[weapon.name] + + # Override weapon name if specified + if "name" in overrides: + weapon.name = overrides["name"] + + # Override attack sound if specified + if "attack_sound" in overrides: + weapon.attackSound = overrides["attack_sound"] + + # Override hit sound if specified + if "hit_sound" in overrides: + weapon.hitSound = overrides["hit_sound"] + + def _apply_object_sound_overrides(self, obj, soundOverrides): + """Apply sound overrides to an object based on level pack theme.""" + # Override base sound name if specified (originalSoundName remains unchanged for game logic) + if "base" in soundOverrides: + obj.soundName = soundOverrides["base"] + + # Handle special object-specific overrides + # For catapults, check for launch sound override + if hasattr(obj, 'launchSound') and "launch" in soundOverrides: + obj.launchSound = soundOverrides["launch"] + + # For grasping hands, check for message overrides + if hasattr(obj, 'warningMessage') and "warning_message" in soundOverrides: + obj.warningMessage = soundOverrides["warning_message"] + if hasattr(obj, 'deathMessage') and "death_message" in soundOverrides: + obj.deathMessage = soundOverrides["death_message"] + + # Handle item sound overrides for graves + if hasattr(obj, 'graveItem') and obj.graveItem and "item" in soundOverrides: + # This would need to be handled in the item collection logic + # Store the override for later use + if not hasattr(obj, 'itemSoundOverride'): + obj.itemSoundOverride = soundOverrides["item"] diff --git a/src/object.py b/src/object.py index 531bf00..64d064c 100644 --- a/src/object.py +++ b/src/object.py @@ -9,6 +9,7 @@ class Object: self.xRange = [x, x] if isinstance(x, (int, float)) else x self.yPos = yPos self.soundName = soundName + self.originalSoundName = soundName # Store original sound name for game logic checks self.isStatic = isStatic self.isCollectible = isCollectible self.isHazard = isHazard diff --git a/src/player.py b/src/player.py index 02237d9..34a3278 100644 --- a/src/player.py +++ b/src/player.py @@ -289,7 +289,7 @@ class Player: # Find the weapon in player's inventory for weapon in self.weapons: - if weapon.name == targetWeaponName: + if weapon.originalName == targetWeaponName: self.equip_weapon(weapon) speak(weapon.name.replace("_", " ")) return True diff --git a/src/powerup.py b/src/powerup.py index d16168e..1497e8b 100644 --- a/src/powerup.py +++ b/src/powerup.py @@ -54,17 +54,20 @@ class PowerUp(Object): def apply_effect(self, player, level=None): """Apply the item's effect when collected""" - if self.item_type == "hand_of_glory": + # Map themed items to their original equivalents for game logic + original_item_type = self._get_original_item_type(self.item_type) + + if original_item_type == "hand_of_glory": player.start_invincibility() - elif self.item_type == "cauldron": + elif original_item_type == "cauldron": player.restore_health() - elif self.item_type == "guts": + elif original_item_type == "guts": player.add_guts() - player.collectedItems.append("guts") + player.collectedItems.append(original_item_type) self.check_for_nunchucks(player) - elif self.item_type == "jack_o_lantern": + elif original_item_type == "jack_o_lantern": player.add_jack_o_lantern() - elif self.item_type == "extra_life": + elif original_item_type == "extra_life": # Don't give extra lives in survival mode if level and level.levelId == 999: # In survival mode, give bonus score instead @@ -73,7 +76,7 @@ class PowerUp(Object): play_sound(self.sounds.get("survivor_bonus", "get_extra_life")) # Use survivor_bonus sound if available else: player.extra_life() - elif self.item_type == "shin_bone": # Add shin bone handling + elif original_item_type == "shin_bone": # Add shin bone handling player.shinBoneCount += 1 player._coins += 5 player.add_save_bone_dust(5) # Add to save bone dust counter too @@ -99,11 +102,11 @@ class PowerUp(Object): ) # Use survivor_bonus sound if available, fallback to bone_dust self.check_for_nunchucks(player) - elif self.item_type == "witch_broom": + elif original_item_type == "witch_broom": broomWeapon = Weapon.create_witch_broom() player.add_weapon(broomWeapon) player.equip_weapon(broomWeapon) - elif self.item_type == "spiderweb": + elif original_item_type == "spiderweb": # Bounce player back (happens even if invincible) player.xPos -= 3 if player.xPos > self.xPos else -3 @@ -133,7 +136,7 @@ class PowerUp(Object): if ( player.shinBoneCount >= 2 and "guts" in player.collectedItems - and not any(weapon.name == "nunchucks" for weapon in player.weapons) + and not any(weapon.originalName == "nunchucks" for weapon in player.weapons) ): nunchucksWeapon = Weapon.create_nunchucks() player.add_weapon(nunchucksWeapon) @@ -143,3 +146,17 @@ class PowerUp(Object): player.scoreboard.increase_score(basePoints + rangeModifier) play_sound(self.sounds["get_nunchucks"]) player.stats.update_stat("Items collected", 1) + + def _get_original_item_type(self, item_type): + """Map themed item names to their original equivalents for game logic.""" + # Define themed equivalents that should behave like original items + themed_mappings = { + # Christmas theme + "candy_cane": "shin_bone", + "reindeer_guts": "guts", + # Future themes can be added here + # "ice_crystal": "shin_bone", + # "frozen_heart": "guts", + } + + return themed_mappings.get(item_type, item_type) diff --git a/src/save_manager.py b/src/save_manager.py index a7ed89b..413c5d0 100644 --- a/src/save_manager.py +++ b/src/save_manager.py @@ -97,6 +97,7 @@ class SaveManager: serialized.append( { "name": weapon.name, + "originalName": getattr(weapon, "originalName", weapon.name), "damage": weapon.damage, "range": weapon.range, "attackSound": weapon.attackSound, @@ -120,13 +121,16 @@ class SaveManager: jumpDurationBonus = data.get("jumpDurationBonus", 1.0) cooldown = data.get("cooldown", 500) # Default cooldown for old saves + # Get originalName (for backward compatibility with old saves) + originalName = data.get("originalName", data["name"]) + # For old saves, restore proper bonuses and cooldowns for specific weapons - if data["name"] == "witch_broom" and speedBonus == 1.0: + if originalName == "witch_broom" and speedBonus == 1.0: speedBonus = 1.17 jumpDurationBonus = 1.25 # Restore proper cooldown for nunchucks in old saves - if data["name"] == "nunchucks" and cooldown == 500: + if originalName == "nunchucks" and cooldown == 500: cooldown = 250 weapon = Weapon( @@ -140,6 +144,8 @@ class SaveManager: speedBonus=speedBonus, jumpDurationBonus=jumpDurationBonus, ) + # Set originalName after creation for backward compatibility + weapon.originalName = originalName weapons.append(weapon) return weapons diff --git a/src/weapon.py b/src/weapon.py index 199d795..a787061 100644 --- a/src/weapon.py +++ b/src/weapon.py @@ -15,6 +15,7 @@ class Weapon: jumpDurationBonus=1.0, ): self.name = name + self.originalName = name # Store original name for game logic checks self.damage = damage self.range = range # Range in tiles self.attackSound = attackSound diff --git a/wicked_quest.py b/wicked_quest.py index 0f4d018..08dc7a8 100755 --- a/wicked_quest.py +++ b/wicked_quest.py @@ -525,7 +525,7 @@ class WickedQuest: continue if self.currentGame: # Ask player to choose game mode - mode_choice = game_mode_menu(self.get_sounds()) + mode_choice = game_mode_menu(self.get_sounds(), self.currentGame) if mode_choice == "story": self.player = None # Reset player for new game self.gameStartTime = pygame.time.get_ticks() @@ -686,23 +686,96 @@ class WickedQuest: self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame) -def game_mode_menu(sounds): +def game_mode_menu(sounds, game_dir=None): """Display game mode selection menu using instruction_menu. - + Args: sounds (dict): Dictionary of loaded sound effects - + game_dir (str): Current game directory to check for instructions/credits + Returns: str: Selected game mode or None if cancelled """ - choice = instruction_menu(sounds, "Select game mode:", "Story", "Survival Mode") - - if choice == "Story": - return "story" - elif choice == "Survival Mode": - return "survival" - else: - return None + from src.game_selection import get_game_dir_path + import os + + # Build base menu options + menu_options = ["Story", "Survival Mode"] + + # Check for level pack specific files if game directory is provided + if game_dir: + try: + game_path = get_game_dir_path(game_dir) + + # Check for instructions.txt + instructions_path = os.path.join(game_path, "instructions.txt") + if os.path.exists(instructions_path): + menu_options.append("Instructions") + + # Check for credits.txt + credits_path = os.path.join(game_path, "credits.txt") + if os.path.exists(credits_path): + menu_options.append("Credits") + + except Exception: + # If there's any error checking files, just continue with basic menu + pass + + while True: + choice = instruction_menu(sounds, "Select game mode:", *menu_options) + + if choice == "Story": + return "story" + elif choice == "Survival Mode": + return "survival" + elif choice == "Instructions" and game_dir: + # Display instructions file + try: + game_path = get_game_dir_path(game_dir) + instructions_path = os.path.join(game_path, "instructions.txt") + print(f"DEBUG: Looking for instructions at: {instructions_path}") + if os.path.exists(instructions_path): + print("DEBUG: Instructions file found, loading content...") + 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 + content_lines = instructions_content.split('\n') + print(f"DEBUG: Split into {len(content_lines)} lines") + display_text(content_lines) + print("DEBUG: display_text returned") + else: + print("DEBUG: Instructions file not found at expected path") + speak("Instructions file not found") + except Exception as e: + print(f"DEBUG: Error loading instructions: {str(e)}") + speak(f"Error loading instructions: {str(e)}") + elif choice == "Credits" and game_dir: + # Display credits file + try: + game_path = get_game_dir_path(game_dir) + credits_path = os.path.join(game_path, "credits.txt") + print(f"DEBUG: Looking for credits at: {credits_path}") + if os.path.exists(credits_path): + print("DEBUG: Credits file found, loading content...") + 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 + content_lines = credits_content.split('\n') + print(f"DEBUG: Split into {len(content_lines)} lines") + display_text(content_lines) + print("DEBUG: display_text returned") + else: + print("DEBUG: Credits file not found at expected path") + speak("Credits file not found") + except Exception as e: + print(f"DEBUG: Error loading credits: {str(e)}") + speak(f"Error loading credits: {str(e)}") + else: + return None if __name__ == "__main__":