More work on survival mode.

This commit is contained in:
Storm Dragon
2025-09-07 11:13:01 -04:00
parent ce353d0ed9
commit 2bc27c0e28
19 changed files with 154 additions and 41 deletions
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -141,3 +141,4 @@ class Catapult(Object):
self.activePumpkins.remove(pumpkin)
if not player.isInvincible:
self.sounds['player_takes_damage'].play()
+1
View File
@@ -65,3 +65,4 @@ class CoffinObject(Object):
return True
return False
+8 -1
View File
@@ -166,6 +166,10 @@ class Enemy(Object):
# Ensure spawn point is within level boundaries
spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary))
# Set behavior based on game mode
behavior = 'hunter' if self.level.levelId == 999 else 'patrol'
turn_rate = 2 if self.level.levelId == 999 else 8 # Faster turn rate for survival
# Create new enemy of specified type
spawned = Enemy(
[spawnX, spawnX], # Single point range for spawn
@@ -175,7 +179,9 @@ class Enemy(Object):
self.level,
health=4, # Default health for spawned enemies
damage=2, # Default damage for spawned enemies
attack_range=1 # Default range for spawned enemies
attack_range=1, # Default range for spawned enemies
attack_pattern={'type': behavior},
turn_rate=turn_rate
)
# Add to level's enemies
@@ -282,3 +288,4 @@ class Enemy(Object):
# Update stats
self.level.player.stats.update_stat('Enemies killed', 1)
+1
View File
@@ -92,3 +92,4 @@ def get_level_path(gameDir, levelNum):
level_path = os.path.join(base_path, "levels", gameDir, f"{levelNum}.json")
return level_path
+1
View File
@@ -125,3 +125,4 @@ class GraspingHands(Object):
if hasattr(self, 'crumbleChannel') and self.crumbleChannel:
obj_stop(self.crumbleChannel)
self.crumbleChannel = None
+1
View File
@@ -34,3 +34,4 @@ class GraveObject(Object):
return True
return False
+1
View File
@@ -55,3 +55,4 @@ class ItemProperties:
if name == item_name:
return item_type
return None
+24 -9
View File
@@ -213,6 +213,10 @@ class Level:
roll = random.randint(1, 100)
if roll <= obj.zombieSpawnChance:
# Set behavior based on game mode
behavior = 'hunter' if self.levelId == 999 else 'patrol'
turn_rate = 2 if self.levelId == 999 else 8 # Faster turn rate for survival
zombie = Enemy(
[obj.xPos, obj.xPos],
obj.yPos,
@@ -221,7 +225,9 @@ class Level:
self, # Pass the level reference
health=3,
damage=10,
attack_range=1
attack_range=1,
attack_pattern={'type': behavior},
turn_rate=turn_rate
)
self.enemies.append(zombie)
speak("A zombie emerges from the grave!")
@@ -277,7 +283,7 @@ class Level:
# Check for item collection
if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping:
play_sound(self.sounds[f'get_{item.soundName}'])
item.apply_effect(self.player)
item.apply_effect(self.player, self)
self.levelScore += 1000 # All items collected points awarded
item.isActive = False
self.bouncing_items.remove(item)
@@ -373,11 +379,19 @@ class Level:
))
if self.player._coins % 100 == 0:
# Extra life
self.player._coins = 0
self.player._lives += 1
self.levelScore += 1000
play_sound(self.sounds['get_extra_life'])
# Only give extra lives in story mode, not survival mode (level_id 999)
if self.levelId != 999:
# Extra life
self.player._coins = 0
self.player._lives += 1
self.levelScore += 1000
play_sound(self.sounds['get_extra_life'])
else:
# In survival mode, reset coin counter but give bonus score instead
self.player._coins = 0
self.levelScore += 2000 # Double score bonus instead of extra life
speak("100 bone dust collected! Bonus score!")
play_sound(self.sounds['bone_dust'])
continue
# Handle spiderweb - this should trigger for both walking and jumping if not ducking
@@ -392,7 +406,7 @@ class Level:
)
webEffect.level = self # Pass level reference for spider spawning
play_sound(self.sounds['hit_spiderweb'])
webEffect.apply_effect(self.player)
webEffect.apply_effect(self.player, self)
# Deactivate web
obj.isActive = False
@@ -411,7 +425,7 @@ class Level:
# Create PowerUp to handle the item effect
item = PowerUp(obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1,
self.leftBoundary, self.rightBoundary)
item.apply_effect(self.player)
item.apply_effect(self.player, self)
# Stop grave's current audio channel
if obj.channel:
obj_stop(obj.channel)
@@ -488,3 +502,4 @@ class Level:
))
# Play throw sound
play_sound(self.sounds['throw_jack_o_lantern'])
+1
View File
@@ -37,3 +37,4 @@ class Object:
if self.channel:
obj_stop(self.channel)
self.channel = None
+1
View File
@@ -271,3 +271,4 @@ class Player:
if not self.currentWeapon or not self.currentWeapon.is_attack_active(currentTime):
return None
return self.currentWeapon.get_attack_range(self.xPos, self.facingRight)
+9 -2
View File
@@ -56,7 +56,7 @@ class PowerUp(Object):
return True
def apply_effect(self, player):
def apply_effect(self, player, level=None):
"""Apply the item's effect when collected"""
if self.item_type == 'hand_of_glory':
player.start_invincibility()
@@ -69,7 +69,13 @@ class PowerUp(Object):
elif self.item_type == 'jack_o_lantern':
player.add_jack_o_lantern()
elif self.item_type == 'extra_life':
player.extra_life()
# Don't give extra lives in survival mode
if level and level.levelId == 999:
# In survival mode, give bonus score instead
level.levelScore += 2000
speak("Extra life found! Bonus score in survival mode!")
else:
player.extra_life()
elif self.item_type == 'shin_bone': # Add shin bone handling
player.shinBoneCount += 1
player._coins += 5
@@ -122,3 +128,4 @@ class PowerUp(Object):
player.scoreboard.increase_score(basePoints + rangeModifier)
play_sound(self.sounds['get_nunchucks'])
player.stats.update_stat('Items collected', 1)
+1
View File
@@ -29,3 +29,4 @@ class Projectile:
"""Handle hitting an enemy"""
enemy.take_damage(self.damage)
self.isActive = False # Projectile is destroyed on hit
+1 -1
View File
@@ -290,4 +290,4 @@ class SaveManager:
def has_saves(self):
"""Check if any save files exist"""
return len(self.get_save_files()) > 0
return len(self.get_save_files()) > 0
+1
View File
@@ -118,3 +118,4 @@ class SkullStorm(Object):
player.set_health(player.get_health() - self.damage)
self.sounds['player_takes_damage'].play()
speak("Hit by falling skull!")
+1
View File
@@ -42,3 +42,4 @@ class StatTracker:
def get_total_stat(self, statName):
"""Get a total stat"""
return self.total.get(statName, 0)
+39 -17
View File
@@ -48,10 +48,10 @@ class SurvivalGenerator:
def parseTemplates(self):
"""Parse all level data to extract object templates by type."""
for levelNum, data in self.levelData.items():
# Store ambience and footstep sounds
if 'ambience' in data:
# Store ambience and footstep sounds (remove duplicates)
if 'ambience' in data and data['ambience'] not in self.ambientSounds:
self.ambientSounds.append(data['ambience'])
if 'footstep_sound' in data:
if 'footstep_sound' in data and data['footstep_sound'] not in self.footstepSounds:
self.footstepSounds.append(data['footstep_sound'])
# Parse objects
@@ -86,16 +86,16 @@ class SurvivalGenerator:
"player_start": {"x": 0, "y": 0},
"objects": [],
"boundaries": {"left": 0, "right": segmentLength},
"locked": True, # Enable lock system for survival mode
"ambience": "Escaping the Grave.ogg", # Will be overridden below
"footstep_sound": "footstep_stone" # Will be overridden below
}
# Choose random music and footstep from any level
randomLevel = random.choice(list(self.levelData.values()))
if 'ambience' in randomLevel and randomLevel['ambience']:
levelData["ambience"] = randomLevel['ambience']
if 'footstep_sound' in randomLevel and randomLevel['footstep_sound']:
levelData["footstep_sound"] = randomLevel['footstep_sound']
# Choose random music and footstep from collected unique tracks
if self.ambientSounds:
levelData["ambience"] = random.choice(self.ambientSounds)
if self.footstepSounds:
levelData["footstep_sound"] = random.choice(self.footstepSounds)
# Calculate spawn rates based on difficulty
collectibleDensity = max(0.1, 0.3 - (difficultyLevel * 0.02)) # Fewer collectibles over time
@@ -103,10 +103,12 @@ class SurvivalGenerator:
hazardDensity = min(0.4, 0.1 + (difficultyLevel * 0.03)) # More hazards over time
objectDensity = max(0.1, 0.2 - (difficultyLevel * 0.01)) # Fewer misc objects over time
# Generate objects across the segment
currentX = 10 # Start placing objects at x=10
# Generate objects across the segment with buffer zones
startBufferZone = 25 # Safe zone at start of each wave
endBufferZone = 30 # Safe zone at end of each wave
currentX = startBufferZone # Start placing objects after start buffer zone
while currentX < segmentLength - 10:
while currentX < segmentLength - endBufferZone:
# Determine what to place based on probability
rand = random.random()
@@ -129,6 +131,14 @@ class SurvivalGenerator:
if obj:
levelData["objects"].append(obj)
# Add end-of-level marker at the end, within the end buffer zone
endMarker = {
"x": segmentLength - (endBufferZone // 2), # Place marker in middle of end buffer
"y": 0,
"sound": "end_of_level"
}
levelData["objects"].append(endMarker)
return levelData
def place_collectible(self, xPos, difficultyLevel):
@@ -167,13 +177,21 @@ class SurvivalGenerator:
template = random.choice(availableEnemies)
obj = copy.deepcopy(template)
# Scale enemy stats based on difficulty
healthMultiplier = 1 + (difficultyLevel * 0.15)
damageMultiplier = 1 + (difficultyLevel * 0.1)
# Dynamic health scaling: random between wave/2 and wave
minHealth = max(1, difficultyLevel // 2)
maxHealth = max(1, difficultyLevel)
obj['health'] = random.randint(minHealth, maxHealth)
obj['health'] = int(obj.get('health', 1) * healthMultiplier)
# Damage scaling (keep existing logic)
damageMultiplier = 1 + (difficultyLevel * 0.1)
obj['damage'] = max(1, int(obj.get('damage', 1) * damageMultiplier))
# Set all enemies to hunter mode for survival
obj['behavior'] = 'hunter'
# Progressive turn rate reduction: start at 6, decrease to 1
obj['turn_rate'] = max(1, 7 - difficultyLevel)
# Handle x_range vs single x
if 'x_range' in obj:
rangeSize = obj['x_range'][1] - obj['x_range'][0]
@@ -214,6 +232,10 @@ class SurvivalGenerator:
baseChance = obj.get('zombie_spawn_chance', 0)
obj['zombie_spawn_chance'] = min(50, baseChance + (difficultyLevel * 2))
# Handle coffins - make all items random in survival mode
if obj.get('type') == 'coffin':
obj['item'] = 'random' # Override any specified item
# Handle x_range vs single x
if 'x_range' in obj:
rangeSize = obj['x_range'][1] - obj['x_range'][0]
@@ -221,4 +243,4 @@ class SurvivalGenerator:
else:
obj['x'] = xPos
return obj
return obj
+1
View File
@@ -69,3 +69,4 @@ class Weapon:
self.hitEnemies.add(enemy)
return True
return False
+60 -11
View File
@@ -119,6 +119,10 @@ class WickedQuest:
def auto_save(self):
"""Automatically save the game if player has enough bone dust"""
# Don't save in survival mode
if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId == 999:
return False
if not self.player.can_save():
return False
@@ -191,7 +195,11 @@ class WickedQuest:
# Status queries
if keys[pygame.K_c]:
speak(f"{player.get_coins()} bone dust for extra lives, {player.get_save_bone_dust()} bone dust for saves")
# Different status message for survival vs story mode
if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId == 999:
speak(f"{player.get_coins()} bone dust collected")
else:
speak(f"{player.get_coins()} bone dust for extra lives, {player.get_save_bone_dust()} bone dust for saves")
if keys[pygame.K_h]:
speak(f"{player.get_health()} health of {player.get_max_health()}")
if keys[pygame.K_i]:
@@ -277,6 +285,29 @@ class WickedQuest:
cut_scene(self.sounds, "game_over")
display_text(report)
def display_survival_stats(self, timeTaken):
"""Display survival mode completion statistics."""
# Convert time from milliseconds to minutes:seconds
minutes = timeTaken // 60000
seconds = (timeTaken % 60000) // 1000
report = [f"Survival Mode Complete!"]
report.append(f"Final Wave Reached: {self.survivalWave}")
report.append(f"Final Score: {self.survivalScore}")
report.append(f"Time Survived: {minutes} minutes and {seconds} seconds")
report.append("") # Blank line
# Add all total stats
for key in self.currentLevel.player.stats.total:
if key not in ['Total time', 'levelsCompleted']: # Skip these
report.append(f"Total {key}: {self.currentLevel.player.stats.get_total_stat(key)}")
if self.currentLevel.player.scoreboard.check_high_score():
pygame.event.clear()
self.currentLevel.player.scoreboard.add_high_score()
display_text(report)
def game_loop(self, startingLevelNum=1):
"""Main game loop handling updates and state changes."""
clock = pygame.time.Clock()
@@ -426,7 +457,7 @@ class WickedQuest:
if self.currentGame:
# Ask player to choose game mode
mode_choice = game_mode_menu(self.sounds)
if mode_choice == "campaign":
if mode_choice == "story":
self.player = None # Reset player for new game
self.gameStartTime = pygame.time.get_ticks()
if self.load_level(1):
@@ -454,11 +485,13 @@ class WickedQuest:
self.player = Player(0, 0, self.sounds)
self.gameStartTime = pygame.time.get_ticks()
# Show intro message before level starts
messagebox(f"Survival Mode - Wave {self.survivalWave}! Survive as long as you can!")
# Generate first survival segment
levelData = self.survivalGenerator.generate_survival_level(self.survivalWave, 300)
self.currentLevel = Level(levelData, self.sounds, self.player)
messagebox(f"Survival Mode - Wave {self.survivalWave}! Survive as long as you can!")
self.survival_loop()
def survival_loop(self):
@@ -473,7 +506,12 @@ class WickedQuest:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
messagebox(f"Survival ended! Final score: {self.survivalScore}")
# Stop all sounds before exiting
pygame.mixer.stop()
pygame.mixer.music.stop()
# Calculate survival time
survivalTime = pygame.time.get_ticks() - self.gameStartTime
self.display_survival_stats(survivalTime)
return
elif event.key in [pygame.K_CAPSLOCK, pygame.K_TAB]:
self.runLock = not self.runLock
@@ -497,14 +535,23 @@ class WickedQuest:
# Check if player reached end of segment - generate new one
if self.player.xPos >= self.currentLevel.rightBoundary - 20:
self.advance_survival_wave()
# Check lock system - only advance if no active enemies remain
if self.currentLevel.isLocked and any(enemy.isActive for enemy in self.currentLevel.enemies):
speak("You must defeat all enemies before proceeding to the next wave!")
play_sound(self.sounds['locked'])
# Push player back a bit
self.player.xPos -= 5
else:
self.advance_survival_wave()
# Check for death first (following main game loop pattern)
if self.currentLevel.player.get_health() <= 0:
if self.currentLevel.player.get_lives() <= 0:
# Game over - stop all sounds
pygame.mixer.stop()
messagebox(f"Game Over! Final wave: {self.survivalWave}, Final score: {self.survivalScore}")
# Calculate survival time
survivalTime = pygame.time.get_ticks() - self.gameStartTime
self.display_survival_stats(survivalTime)
return
else:
# Player died but has lives left - respawn
@@ -527,6 +574,9 @@ class WickedQuest:
self.currentLevel.projectiles.clear()
pygame.mixer.stop() # Stop any ongoing catapult/enemy sounds
# Announce new wave before starting level
speak(f"Wave {self.survivalWave}!")
# Generate new segment
segmentLength = min(500, 300 + (self.survivalWave * 20)) # Longer segments over time
levelData = self.survivalGenerator.generate_survival_level(self.survivalWave, segmentLength)
@@ -537,8 +587,6 @@ class WickedQuest:
# Create new level
self.currentLevel = Level(levelData, self.sounds, self.player)
speak(f"Wave {self.survivalWave}! Difficulty increased!")
def game_mode_menu(sounds):
@@ -550,10 +598,10 @@ def game_mode_menu(sounds):
Returns:
str: Selected game mode or None if cancelled
"""
choice = instruction_menu(sounds, "Select game mode:", "Campaign", "Survival Mode")
choice = instruction_menu(sounds, "Select game mode:", "Story", "Survival Mode")
if choice == "Campaign":
return "campaign"
if choice == "Story":
return "story"
elif choice == "Survival Mode":
return "survival"
else:
@@ -563,3 +611,4 @@ def game_mode_menu(sounds):
if __name__ == "__main__":
game = WickedQuest()
game.run()