Added override capabilities for lots of items, weapons, and hazards.
This commit is contained in:
+177
-3
@@ -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
|
||||
|
||||
|
||||
+2
-1
@@ -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])
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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):
|
||||
|
||||
+80
-8
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+27
-10
@@ -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)
|
||||
|
||||
+8
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+85
-12
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user