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)
|
- `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
@@ -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
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+85
-12
@@ -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":
|
|
||||||
return "story"
|
# Build base menu options
|
||||||
elif choice == "Survival Mode":
|
menu_options = ["Story", "Survival Mode"]
|
||||||
return "survival"
|
|
||||||
else:
|
# Check for level pack specific files if game directory is provided
|
||||||
return None
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user