More work on survival mode.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -141,3 +141,4 @@ class Catapult(Object):
|
||||
self.activePumpkins.remove(pumpkin)
|
||||
if not player.isInvincible:
|
||||
self.sounds['player_takes_damage'].play()
|
||||
|
||||
|
||||
@@ -65,3 +65,4 @@ class CoffinObject(Object):
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
+8
-1
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -125,3 +125,4 @@ class GraspingHands(Object):
|
||||
if hasattr(self, 'crumbleChannel') and self.crumbleChannel:
|
||||
obj_stop(self.crumbleChannel)
|
||||
self.crumbleChannel = None
|
||||
|
||||
|
||||
@@ -34,3 +34,4 @@ class GraveObject(Object):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -55,3 +55,4 @@ class ItemProperties:
|
||||
if name == item_name:
|
||||
return item_type
|
||||
return None
|
||||
|
||||
|
||||
+24
-9
@@ -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'])
|
||||
|
||||
|
||||
@@ -37,3 +37,4 @@ class Object:
|
||||
if self.channel:
|
||||
obj_stop(self.channel)
|
||||
self.channel = None
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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!")
|
||||
|
||||
|
||||
@@ -42,3 +42,4 @@ class StatTracker:
|
||||
def get_total_stat(self, statName):
|
||||
"""Get a total stat"""
|
||||
return self.total.get(statName, 0)
|
||||
|
||||
|
||||
+39
-17
@@ -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
|
||||
|
||||
@@ -69,3 +69,4 @@ class Weapon:
|
||||
self.hitEnemies.add(enemy)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
+60
-11
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user