From 2bc27c0e2803febcf2314878c05e3a01eca7aac5 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 7 Sep 2025 11:13:01 -0400 Subject: [PATCH] More work on survival mode. --- src/__init__.py | 1 + src/catapult.py | 1 + src/coffin.py | 1 + src/enemy.py | 9 ++++- src/game_selection.py | 1 + src/grasping_hands.py | 1 + src/grave.py | 1 + src/item_types.py | 1 + src/level.py | 33 +++++++++++++----- src/object.py | 1 + src/player.py | 1 + src/powerup.py | 11 ++++-- src/projectile.py | 1 + src/save_manager.py | 2 +- src/skull_storm.py | 1 + src/stat_tracker.py | 1 + src/survival_generator.py | 56 ++++++++++++++++++++---------- src/weapon.py | 1 + wicked_quest.py | 71 +++++++++++++++++++++++++++++++++------ 19 files changed, 154 insertions(+), 41 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index e69de29..8b13789 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -0,0 +1 @@ + diff --git a/src/catapult.py b/src/catapult.py index a68eb14..d0ae2ab 100644 --- a/src/catapult.py +++ b/src/catapult.py @@ -141,3 +141,4 @@ class Catapult(Object): self.activePumpkins.remove(pumpkin) if not player.isInvincible: self.sounds['player_takes_damage'].play() + diff --git a/src/coffin.py b/src/coffin.py index 34f3375..9137ec4 100644 --- a/src/coffin.py +++ b/src/coffin.py @@ -65,3 +65,4 @@ class CoffinObject(Object): return True return False + diff --git a/src/enemy.py b/src/enemy.py index 157b084..6d6cc55 100644 --- a/src/enemy.py +++ b/src/enemy.py @@ -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) + diff --git a/src/game_selection.py b/src/game_selection.py index 1bc300b..2dcc4a8 100644 --- a/src/game_selection.py +++ b/src/game_selection.py @@ -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 + diff --git a/src/grasping_hands.py b/src/grasping_hands.py index b6b9798..a899fda 100644 --- a/src/grasping_hands.py +++ b/src/grasping_hands.py @@ -125,3 +125,4 @@ class GraspingHands(Object): if hasattr(self, 'crumbleChannel') and self.crumbleChannel: obj_stop(self.crumbleChannel) self.crumbleChannel = None + diff --git a/src/grave.py b/src/grave.py index 12c1847..225f6de 100644 --- a/src/grave.py +++ b/src/grave.py @@ -34,3 +34,4 @@ class GraveObject(Object): return True return False + diff --git a/src/item_types.py b/src/item_types.py index 5676f66..0eebd98 100644 --- a/src/item_types.py +++ b/src/item_types.py @@ -55,3 +55,4 @@ class ItemProperties: if name == item_name: return item_type return None + diff --git a/src/level.py b/src/level.py index e4b4b1d..3a88210 100644 --- a/src/level.py +++ b/src/level.py @@ -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']) + diff --git a/src/object.py b/src/object.py index b720d12..01fbe0b 100644 --- a/src/object.py +++ b/src/object.py @@ -37,3 +37,4 @@ class Object: if self.channel: obj_stop(self.channel) self.channel = None + diff --git a/src/player.py b/src/player.py index b0fed65..bdfc2d2 100644 --- a/src/player.py +++ b/src/player.py @@ -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) + diff --git a/src/powerup.py b/src/powerup.py index 91d5546..ef2a2e6 100644 --- a/src/powerup.py +++ b/src/powerup.py @@ -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) + diff --git a/src/projectile.py b/src/projectile.py index 783e193..1314510 100644 --- a/src/projectile.py +++ b/src/projectile.py @@ -29,3 +29,4 @@ class Projectile: """Handle hitting an enemy""" enemy.take_damage(self.damage) self.isActive = False # Projectile is destroyed on hit + diff --git a/src/save_manager.py b/src/save_manager.py index d3432ef..d3d2a16 100644 --- a/src/save_manager.py +++ b/src/save_manager.py @@ -290,4 +290,4 @@ class SaveManager: def has_saves(self): """Check if any save files exist""" - return len(self.get_save_files()) > 0 \ No newline at end of file + return len(self.get_save_files()) > 0 diff --git a/src/skull_storm.py b/src/skull_storm.py index ca79c92..dd3646c 100644 --- a/src/skull_storm.py +++ b/src/skull_storm.py @@ -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!") + diff --git a/src/stat_tracker.py b/src/stat_tracker.py index 457f12b..a788271 100644 --- a/src/stat_tracker.py +++ b/src/stat_tracker.py @@ -42,3 +42,4 @@ class StatTracker: def get_total_stat(self, statName): """Get a total stat""" return self.total.get(statName, 0) + diff --git a/src/survival_generator.py b/src/survival_generator.py index 15c6342..1c0f6ba 100644 --- a/src/survival_generator.py +++ b/src/survival_generator.py @@ -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 \ No newline at end of file + return obj diff --git a/src/weapon.py b/src/weapon.py index 8c43f45..eff9a51 100644 --- a/src/weapon.py +++ b/src/weapon.py @@ -69,3 +69,4 @@ class Weapon: self.hitEnemies.add(enemy) return True return False + diff --git a/wicked_quest.py b/wicked_quest.py index 0d3f350..106a01b 100755 --- a/wicked_quest.py +++ b/wicked_quest.py @@ -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() +