Added override capabilities for lots of items, weapons, and hazards.

This commit is contained in:
Storm Dragon
2025-09-20 04:10:32 -04:00
parent 91eecae786
commit 5dd78a1687
12 changed files with 392 additions and 43 deletions
+177 -3
View File
@@ -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) - `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. - `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 ## Adding Objects
@@ -385,13 +385,187 @@ sounds/Samhain Showdown/ambience
├── howling_winds.ogg ├── 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 ambience:** Place in `sounds/[Pack Name]/ambience/`
- **Custom enemy sounds:** Place in `sounds/[Pack Name]/` - **Custom enemy sounds:** Place in `sounds/[Pack Name]/`
- **Custom footsteps:** Reference in level JSON as `"footstep_sound"` - **Custom footsteps:** Reference in level JSON as `"footstep_sound"`
- **Ending scene:** Add `end.ogg` in the level pack directory - **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 ## Complete Example Level
+2 -1
View File
@@ -73,13 +73,14 @@ class Catapult(Object):
self.launchDelay = 900 # Time between launch sound and pumpkin firing self.launchDelay = 900 # Time between launch sound and pumpkin firing
self.pendingPumpkin = None # Store pending pumpkin data self.pendingPumpkin = None # Store pending pumpkin data
self.pumpkinLaunchTime = 0 # When to launch the pending pumpkin self.pumpkinLaunchTime = 0 # When to launch the pending pumpkin
self.launchSound = "catapult_launch" # Configurable launch sound
def fire(self, currentTime, player): def fire(self, currentTime, player):
"""Start the firing sequence""" """Start the firing sequence"""
self.lastFireTime = currentTime self.lastFireTime = currentTime
# Play launch sound using directional audio # 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 # Set up pending pumpkin
isHigh = random.choice([True, False]) isHigh = random.choice([True, False])
+2 -2
View File
@@ -261,8 +261,8 @@ class Enemy(Object):
# Handle witch-specific drops # Handle witch-specific drops
if self.enemyType == "witch": if self.enemyType == "witch":
# Determine which item to drop # Determine which item to drop
hasBroom = any(weapon.name == "witch_broom" for weapon in self.level.player.weapons) hasBroom = any(weapon.originalName == "witch_broom" for weapon in self.level.player.weapons)
hasNunchucks = any(weapon.name == "nunchucks" 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 # Drop witch_broom only if player has neither broom nor nunchucks
itemType = "witch_broom" if not (hasBroom or hasNunchucks) else "cauldron" itemType = "witch_broom" if not (hasBroom or hasNunchucks) else "cauldron"
+6 -2
View File
@@ -21,6 +21,10 @@ class GraspingHands(Object):
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.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 # State tracking
self.isTriggered = False # Has the player entered the zone? self.isTriggered = False # Has the player entered the zone?
self.triggerTime = 0 # When did the player enter the zone? self.triggerTime = 0 # When did the player enter the zone?
@@ -53,7 +57,7 @@ class GraspingHands(Object):
# Play initial warning sound # Play initial warning sound
play_sound(self.sounds["grasping_hands_start"]) play_sound(self.sounds["grasping_hands_start"])
speak("The ground crumbles as the dead reach for you.") speak(self.warningMessage)
def reset(self): def reset(self):
"""Reset the trap when player leaves the range""" """Reset the trap when player leaves the range"""
@@ -114,7 +118,7 @@ class GraspingHands(Object):
obj_stop(self.crumbleChannel) obj_stop(self.crumbleChannel)
self.crumbleChannel = None self.crumbleChannel = None
speak("The hands of the dead drag you down!") speak(self.deathMessage)
player.set_health(0) player.set_health(0)
return True return True
# Player is invincible - no warning needed # Player is invincible - no warning needed
+2 -2
View File
@@ -30,7 +30,7 @@ class GraveObject(Object):
player.isDucking player.isDucking
and not player.isRunning and not player.isRunning
and player.currentWeapon and player.currentWeapon
and player.currentWeapon.name == "rusty_shovel" and player.currentWeapon.originalName == "rusty_shovel"
): ):
self.isCollected = True # Mark as collected when collection succeeds self.isCollected = True # Mark as collected when collection succeeds
return True return True
@@ -52,7 +52,7 @@ class GraveObject(Object):
player.isDucking player.isDucking
and not player.isRunning and not player.isRunning
and player.currentWeapon and player.currentWeapon
and player.currentWeapon.name == "rusty_shovel" and player.currentWeapon.originalName == "rusty_shovel"
) )
def fill_grave(self, player): def fill_grave(self, player):
+80 -8
View File
@@ -41,12 +41,23 @@ class Level:
# Pass footstep sound to player # Pass footstep sound to player
self.player.set_footstep_sound(self.footstepSound) 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) # Level intro message (skip for survival mode)
if levelData["level_id"] != 999: # 999 is survival mode if levelData["level_id"] != 999: # 999 is survival mode
# Check if level uses new dialog format or old description format # Check if level uses new dialog format or old description format
if "dialog" in levelData: if "dialog" in levelData:
# Use new dialog system with sound support # Use new dialog system with sound support
messagebox(levelData["dialog"], self.sounds) 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: elif "description" in levelData:
# Use traditional description format # Use traditional description format
levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. " levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. "
@@ -106,6 +117,9 @@ class Level:
fireInterval=obj.get("fireInterval", 5000), fireInterval=obj.get("fireInterval", 5000),
firingRange=obj.get("range", 20), 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) self.objects.append(catapult)
# Check if this is grasping hands # Check if this is grasping hands
elif obj.get("type") == "grasping_hands": elif obj.get("type") == "grasping_hands":
@@ -116,6 +130,9 @@ class Level:
delay=obj.get("delay", 1000), delay=obj.get("delay", 1000),
crumble_speed=obj.get("crumble_speed", 0.03), 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) self.objects.append(graspingHands)
# Check if this is a grave # Check if this is a grave
elif obj.get("type") == "grave": elif obj.get("type") == "grave":
@@ -126,6 +143,9 @@ class Level:
item=obj.get("item", None), item=obj.get("item", None),
zombieSpawnChance=obj.get("zombie_spawn_chance", 0), 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) self.objects.append(grave)
# Check if this is a skull storm # Check if this is a skull storm
elif obj.get("type") == "skull_storm": elif obj.get("type") == "skull_storm":
@@ -138,6 +158,9 @@ class Level:
obj.get("frequency", {}).get("min", 2), obj.get("frequency", {}).get("min", 2),
obj.get("frequency", {}).get("max", 5), 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) self.objects.append(skullStorm)
# Check if this is a coffin # Check if this is a coffin
elif obj.get("type") == "coffin": elif obj.get("type") == "coffin":
@@ -154,7 +177,7 @@ class Level:
# Check distance from graves # Check distance from graves
isValidPosition = True isValidPosition = True
for existingObj in self.objects: 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) distance = abs(obj["x"] - existingObj.xPos)
if distance < 3: if distance < 3:
isValidPosition = False isValidPosition = False
@@ -203,6 +226,9 @@ class Level:
isHazard=obj.get("hazard", False), isHazard=obj.get("hazard", False),
zombieSpawnChance=obj.get("zombieSpawnChance", 0), 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) self.objects.append(gameObject)
enemyCount = len(self.enemies) enemyCount = len(self.enemies)
coffinCount = sum(1 for obj in self.objects if hasattr(obj, "isBroken")) coffinCount = sum(1 for obj in self.objects if hasattr(obj, "isBroken"))
@@ -219,7 +245,7 @@ class Level:
continue continue
# Check for potential zombie spawn from graves # 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) distance = abs(self.player.xPos - obj.xPos)
if distance < 6: # Within 6 tiles if distance < 6: # Within 6 tiles
@@ -371,7 +397,7 @@ class Level:
# Handle grave edge warnings # Handle grave edge warnings
if ( 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 ): # Exclude spiderwebs and grasping hands
distance = abs(self.player.xPos - obj.xPos) distance = abs(self.player.xPos - obj.xPos)
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
@@ -396,9 +422,9 @@ class Level:
if currentPos not in obj.collectedPositions: if currentPos not in obj.collectedPositions:
play_sound(self.sounds[f"get_{obj.soundName}"]) play_sound(self.sounds[f"get_{obj.soundName}"])
obj.collect_at_position(currentPos) 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) self.player.stats.update_stat("Items collected", 1)
if obj.soundName == "bone_dust": if obj.originalSoundName == "bone_dust":
self.player._coins += 1 self.player._coins += 1
self.player.add_save_bone_dust(1) # Add to save bone dust counter too self.player.add_save_bone_dust(1) # Add to save bone dust counter too
self.levelScore += 100 self.levelScore += 100
@@ -427,7 +453,7 @@ class Level:
continue continue
# Handle spiderweb - this should trigger for both walking and jumping if not ducking # 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 # Create and apply web effect
webEffect = PowerUp( webEffect = PowerUp(
obj.xPos, obj.yPos, "spiderweb", self.sounds, 0 # No direction needed since it's just for effect 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: if can_collect:
# Successfully collected item while ducking with shovel # 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 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
@@ -492,7 +522,7 @@ class Level:
# Check for level completion - takes precedence over everything except death # Check for level completion - takes precedence over everything except death
if self.player.get_health() > 0: if self.player.get_health() > 0:
for obj in self.objects: 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 # Check if player has reached or passed the end marker
if self.player.xPos >= obj.xPos: if self.player.xPos >= obj.xPos:
# If level is locked, check for remaining enemies # 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"])) self.projectiles.append(Projectile(proj_info["type"], proj_info["start_x"], proj_info["direction"]))
# Play throw sound # Play throw sound
play_sound(self.sounds["throw_jack_o_lantern"]) 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"]
+1
View File
@@ -9,6 +9,7 @@ class Object:
self.xRange = [x, x] if isinstance(x, (int, float)) else x self.xRange = [x, x] if isinstance(x, (int, float)) else x
self.yPos = yPos self.yPos = yPos
self.soundName = soundName self.soundName = soundName
self.originalSoundName = soundName # Store original sound name for game logic checks
self.isStatic = isStatic self.isStatic = isStatic
self.isCollectible = isCollectible self.isCollectible = isCollectible
self.isHazard = isHazard self.isHazard = isHazard
+1 -1
View File
@@ -289,7 +289,7 @@ class Player:
# Find the weapon in player's inventory # Find the weapon in player's inventory
for weapon in self.weapons: for weapon in self.weapons:
if weapon.name == targetWeaponName: if weapon.originalName == targetWeaponName:
self.equip_weapon(weapon) self.equip_weapon(weapon)
speak(weapon.name.replace("_", " ")) speak(weapon.name.replace("_", " "))
return True return True
+27 -10
View File
@@ -54,17 +54,20 @@ 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"""
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() player.start_invincibility()
elif self.item_type == "cauldron": elif original_item_type == "cauldron":
player.restore_health() player.restore_health()
elif self.item_type == "guts": elif original_item_type == "guts":
player.add_guts() player.add_guts()
player.collectedItems.append("guts") player.collectedItems.append(original_item_type)
self.check_for_nunchucks(player) self.check_for_nunchucks(player)
elif self.item_type == "jack_o_lantern": elif original_item_type == "jack_o_lantern":
player.add_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 # 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
@@ -73,7 +76,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 self.item_type == "shin_bone": # Add shin bone handling elif original_item_type == "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
@@ -99,11 +102,11 @@ class PowerUp(Object):
) # Use survivor_bonus sound if available, fallback to bone_dust ) # Use survivor_bonus sound if available, fallback to bone_dust
self.check_for_nunchucks(player) self.check_for_nunchucks(player)
elif self.item_type == "witch_broom": elif original_item_type == "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 self.item_type == "spiderweb": elif original_item_type == "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
@@ -133,7 +136,7 @@ class PowerUp(Object):
if ( if (
player.shinBoneCount >= 2 player.shinBoneCount >= 2
and "guts" in player.collectedItems 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() nunchucksWeapon = Weapon.create_nunchucks()
player.add_weapon(nunchucksWeapon) player.add_weapon(nunchucksWeapon)
@@ -143,3 +146,17 @@ class PowerUp(Object):
player.scoreboard.increase_score(basePoints + rangeModifier) player.scoreboard.increase_score(basePoints + rangeModifier)
play_sound(self.sounds["get_nunchucks"]) play_sound(self.sounds["get_nunchucks"])
player.stats.update_stat("Items collected", 1) 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)
+8 -2
View File
@@ -97,6 +97,7 @@ class SaveManager:
serialized.append( serialized.append(
{ {
"name": weapon.name, "name": weapon.name,
"originalName": getattr(weapon, "originalName", weapon.name),
"damage": weapon.damage, "damage": weapon.damage,
"range": weapon.range, "range": weapon.range,
"attackSound": weapon.attackSound, "attackSound": weapon.attackSound,
@@ -120,13 +121,16 @@ class SaveManager:
jumpDurationBonus = data.get("jumpDurationBonus", 1.0) jumpDurationBonus = data.get("jumpDurationBonus", 1.0)
cooldown = data.get("cooldown", 500) # Default cooldown for old saves 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 # 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 speedBonus = 1.17
jumpDurationBonus = 1.25 jumpDurationBonus = 1.25
# Restore proper cooldown for nunchucks in old saves # Restore proper cooldown for nunchucks in old saves
if data["name"] == "nunchucks" and cooldown == 500: if originalName == "nunchucks" and cooldown == 500:
cooldown = 250 cooldown = 250
weapon = Weapon( weapon = Weapon(
@@ -140,6 +144,8 @@ class SaveManager:
speedBonus=speedBonus, speedBonus=speedBonus,
jumpDurationBonus=jumpDurationBonus, jumpDurationBonus=jumpDurationBonus,
) )
# Set originalName after creation for backward compatibility
weapon.originalName = originalName
weapons.append(weapon) weapons.append(weapon)
return weapons return weapons
+1
View File
@@ -15,6 +15,7 @@ class Weapon:
jumpDurationBonus=1.0, jumpDurationBonus=1.0,
): ):
self.name = name self.name = name
self.originalName = name # Store original name for game logic checks
self.damage = damage self.damage = damage
self.range = range # Range in tiles self.range = range # Range in tiles
self.attackSound = attackSound self.attackSound = attackSound
+82 -9
View File
@@ -525,7 +525,7 @@ 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()) mode_choice = game_mode_menu(self.get_sounds(), self.currentGame)
if mode_choice == "story": if mode_choice == "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()
@@ -686,23 +686,96 @@ 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): def game_mode_menu(sounds, game_dir=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
Returns: Returns:
str: Selected game mode or None if cancelled str: Selected game mode or None if cancelled
""" """
choice = instruction_menu(sounds, "Select game mode:", "Story", "Survival Mode") from src.game_selection import get_game_dir_path
import os
if choice == "Story": # Build base menu options
return "story" menu_options = ["Story", "Survival Mode"]
elif choice == "Survival Mode":
return "survival" # Check for level pack specific files if game directory is provided
else: if game_dir:
return None 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__": if __name__ == "__main__":