From 1c442261d9ed642fd766efdaf4cc170be7073883 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 16 Sep 2025 01:51:36 -0400 Subject: [PATCH] Migrate to libstormgames systems. --- src/level.py | 36 +++++--- src/player.py | 41 +++++---- src/powerup.py | 26 ++++-- wicked_quest.py | 227 ++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 247 insertions(+), 83 deletions(-) diff --git a/src/level.py b/src/level.py index e179ce1..3cbf458 100644 --- a/src/level.py +++ b/src/level.py @@ -10,7 +10,6 @@ from src.grasping_hands import GraspingHands from src.grave import GraveObject from src.object import Object from src.player import Player -from src.projectile import Projectile from src.powerup import PowerUp from src.skull_storm import SkullStorm @@ -200,8 +199,8 @@ class Level: self.objects.append(gameObject) enemyCount = len(self.enemies) coffinCount = sum(1 for obj in self.objects if hasattr(obj, "isBroken")) - player.stats.update_stat("Enemies remaining", enemyCount) - player.stats.update_stat("Coffins remaining", coffinCount) + player.stats.set_stat("Enemies remaining", enemyCount, level_only=True) + player.stats.set_stat("Coffins remaining", coffinCount, level_only=True) def update_audio(self): """Update all audio and entity state.""" @@ -310,16 +309,22 @@ class Level: def handle_combat(self, currentTime): """Handle combat interactions between player and enemies""" # Only get attack range if attack is active - if self.player.currentWeapon and self.player.currentWeapon.is_attack_active(currentTime): - attackRange = self.player.currentWeapon.get_attack_range(self.player.xPos, self.player.facingRight) + if self.player.currentWeapon and self.player.currentWeapon.is_attack_active(): + # Calculate attack range manually + if self.player.facingRight: + attackRange = (self.player.xPos, self.player.xPos + self.player.currentWeapon.range_value) + else: + attackRange = (self.player.xPos - self.player.currentWeapon.range_value, self.player.xPos) # Check for enemy hits for enemy in self.enemies: if enemy.isActive and enemy.xPos >= attackRange[0] and enemy.xPos <= attackRange[1]: - # Only damage and play sound if this is a new hit for this attack - if self.player.currentWeapon.register_hit(enemy, currentTime): - play_sound(self.sounds[self.player.currentWeapon.hitSound]) - enemy.take_damage(self.player.currentWeapon.damage) + # Use libstormgames weapon hit detection + if self.player.currentWeapon.can_hit_target(enemy.xPos, self.player.xPos, self.player.facingRight, id(enemy)): + damage = self.player.currentWeapon.hit_target(id(enemy)) + if damage > 0: + play_sound(self.sounds[self.player.currentWeapon.hit_sound]) + enemy.take_damage(damage) # Check for coffin hits for obj in self.objects: @@ -516,13 +521,16 @@ class Level: # Check for enemy hits for enemy in self.enemies: - if enemy.isActive and abs(proj.x - enemy.xPos) < 1: - proj.hit_enemy(enemy) + if enemy.isActive and proj.check_collision(enemy.xPos, 1.0): + damage = proj.hit() + enemy.take_damage(damage) self.projectiles.remove(proj) # Calculate volume and pan for splat sound based on final position - volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x) + proj_pos = proj.get_position() + proj_x = proj_pos if isinstance(proj_pos, (int, float)) else proj_pos[0] + volume, left, right = calculate_volume_and_pan(self.player.xPos, proj_x) if volume > 0: # Only play if within audible range - obj_play(self.sounds, "pumpkin_splat", self.player.xPos, proj.x, loop=False) + obj_play(self.sounds, "pumpkin_splat", self.player.xPos, proj_x, loop=False) break def throw_projectile(self): @@ -532,6 +540,6 @@ class Level: speak("No jack o'lanterns to throw!") return - 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"], speed=0.2, damage=5, max_range=12)) # Play throw sound play_sound(self.sounds["throw_jack_o_lantern"]) diff --git a/src/player.py b/src/player.py index 02237d9..fe29c02 100644 --- a/src/player.py +++ b/src/player.py @@ -2,8 +2,6 @@ import pygame from libstormgames import * -from src.stat_tracker import StatTracker -from src.weapon import Weapon class Player: @@ -27,7 +25,7 @@ class Player: self._lives = 1 self.distanceSinceLastStep = 0 self.stepDistance = 0.5 - self.stats = StatTracker() + self.stats = StatTracker({"Bone dust": 0, "Enemies killed": 0, "Coffins broken": 0, "Items collected": 0, "Total time": 0, "levelsCompleted": 0}) self.sounds = sounds # Footstep tracking @@ -67,10 +65,10 @@ class Player: Weapon( name="rusty_shovel", damage=2, - range=2, - attackSound="player_shovel_attack", - hitSound="player_shovel_hit", - attackDuration=200, # 200ms attack duration + range_value=2, + attack_sound="player_shovel_attack", + hit_sound="player_shovel_hit", + attack_duration=200, # 200ms attack duration ) ) @@ -106,7 +104,7 @@ class Player: if currentTime >= self.webPenaltyEndTime: self.moveSpeed *= 2 # Restore speed if self.currentWeapon: - self.currentWeapon.attackDuration *= 0.5 # Restore attack speed + self.currentWeapon.attack_duration *= 0.5 # Restore attack speed del self.webPenaltyEndTime # Check invincibility status @@ -166,7 +164,7 @@ class Player: def get_step_distance(self): """Get step distance based on current speed""" - weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0 + weaponBonus = self.currentWeapon.stat_bonuses.get("speed", 1.0) if self.currentWeapon else 1.0 totalMultiplier = weaponBonus if self.isRunning or self.isJumping: @@ -176,7 +174,7 @@ class Player: def get_step_interval(self): """Get minimum time between steps based on current speed""" - weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0 + weaponBonus = self.currentWeapon.stat_bonuses.get("speed", 1.0) if self.currentWeapon else 1.0 totalMultiplier = weaponBonus if self.isRunning or self.isJumping: @@ -199,7 +197,7 @@ class Player: def get_current_speed(self): """Calculate current speed based on state and weapon""" baseSpeed = self.moveSpeed - weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0 + weaponBonus = self.currentWeapon.stat_bonuses.get("speed", 1.0) if self.currentWeapon else 1.0 if self.isJumping or self.isRunning: return baseSpeed * self.runMultiplier * weaponBonus @@ -207,7 +205,7 @@ class Player: def get_current_jump_duration(self): """Calculate current jump duration based on weapon bonus""" - weaponBonus = self.currentWeapon.jumpDurationBonus if self.currentWeapon else 1.0 + weaponBonus = self.currentWeapon.stat_bonuses.get("jump_duration", 1.0) if self.currentWeapon else 1.0 return int(self.jumpDuration * weaponBonus) def set_footstep_sound(self, soundName): @@ -304,14 +302,21 @@ class Player: def start_attack(self, currentTime): """Attempt to start an attack with the current weapon""" - if self.currentWeapon and self.currentWeapon.start_attack(currentTime): - self.isAttacking = True - self.lastAttackTime = currentTime - return True + if self.currentWeapon and self.currentWeapon.can_attack(): + damage = self.currentWeapon.attack() + if damage > 0: + self.isAttacking = True + self.lastAttackTime = currentTime + return True return False def get_attack_range(self, currentTime): """Get the current attack's range based on position and facing direction""" - if not self.currentWeapon or not self.currentWeapon.is_attack_active(currentTime): + if not self.currentWeapon or not self.currentWeapon.is_attack_active(): return None - return self.currentWeapon.get_attack_range(self.xPos, self.facingRight) + + # Calculate attack range based on position and facing direction + if self.facingRight: + return (self.xPos, self.xPos + self.currentWeapon.range_value) + else: + return (self.xPos - self.currentWeapon.range_value, self.xPos) diff --git a/src/powerup.py b/src/powerup.py index d16168e..f06ac31 100644 --- a/src/powerup.py +++ b/src/powerup.py @@ -3,7 +3,6 @@ import pygame from libstormgames import * from src.object import Object -from src.weapon import Weapon class PowerUp(Object): @@ -100,7 +99,16 @@ class PowerUp(Object): self.check_for_nunchucks(player) elif self.item_type == "witch_broom": - broomWeapon = Weapon.create_witch_broom() + broomWeapon = Weapon( + name="witch_broom", + damage=3, + range_value=3, + attack_sound="player_broom_attack", + hit_sound="player_broom_hit", + cooldown=500, + attack_duration=200, + stat_bonuses={"speed": 1.17, "jump_duration": 1.25} + ) player.add_weapon(broomWeapon) player.equip_weapon(broomWeapon) elif self.item_type == "spiderweb": @@ -112,7 +120,7 @@ class PowerUp(Object): # Half speed and double attack time for 15 seconds player.moveSpeed *= 0.5 if player.currentWeapon: - player.currentWeapon.attackDuration *= 2 + player.currentWeapon.attack_duration *= 2 # Set timer for penalty removal player.webPenaltyEndTime = pygame.time.get_ticks() + 15000 @@ -135,11 +143,19 @@ class PowerUp(Object): and "guts" in player.collectedItems and not any(weapon.name == "nunchucks" for weapon in player.weapons) ): - nunchucksWeapon = Weapon.create_nunchucks() + nunchucksWeapon = Weapon( + name="nunchucks", + damage=6, + range_value=4, + attack_sound="player_nunchuck_attack", + hit_sound="player_nunchuck_hit", + cooldown=250, + attack_duration=100 + ) player.add_weapon(nunchucksWeapon) player.equip_weapon(nunchucksWeapon) basePoints = nunchucksWeapon.damage * 1000 - rangeModifier = nunchucksWeapon.range * 500 + rangeModifier = nunchucksWeapon.range_value * 500 player.scoreboard.increase_score(basePoints + rangeModifier) play_sound(self.sounds["get_nunchucks"]) player.stats.update_stat("Items collected", 1) diff --git a/wicked_quest.py b/wicked_quest.py index 2bd4aca..af4fcc4 100755 --- a/wicked_quest.py +++ b/wicked_quest.py @@ -10,7 +10,6 @@ from src.level import Level from src.object import Object from src.player import Player from src.game_selection import select_game, get_level_path -from src.save_manager import SaveManager from src.survival_generator import SurvivalGenerator @@ -28,7 +27,7 @@ class WickedQuest: self.player = None self.currentGame = None self.runLock = False # Toggle behavior of the run keys - self.saveManager = SaveManager() + self.saveManager = SaveManager("wicked-quest") self.survivalGenerator = None self.lastBroomLandingTime = 0 # Timestamp to prevent ducking after broom landing self.survivalWave = 1 @@ -75,7 +74,7 @@ class WickedQuest: del self.player.webPenaltyEndTime # Remove the penalty timer self.player.moveSpeed *= 2 # Restore normal speed if self.player.currentWeapon: - self.player.currentWeapon.attackDuration *= 0.5 # Restore normal attack speed + self.player.currentWeapon.attack_duration *= 0.5 # Restore normal attack speed # Pass existing player to new level pygame.event.clear() @@ -112,28 +111,32 @@ class WickedQuest: def load_game_menu(self): """Display load game menu with available saves using instruction_menu""" save_files = self.saveManager.get_save_files() - + if not save_files: messagebox("No save files found.") return None - - # Create menu options + + # Create menu options with save info options = [] + save_infos = [] for save_file in save_files: - options.append(save_file['display_name']) - + save_info = self.saveManager.get_save_info(save_file) + save_infos.append(save_info) + display_name = save_info.get("metadata", {}).get("display_name", "Unknown Save") + options.append(display_name) + options.append("Cancel") - + # Use instruction_menu for consistent behavior choice = instruction_menu(self.get_sounds(), "Select a save file to load:", *options) - + if choice == "Cancel" or choice is None: return None else: # Find the corresponding save file - for save_file in save_files: - if save_file['display_name'] == choice: - return save_file + for i, option in enumerate(options[:-1]): # Exclude "Cancel" + if option == choice: + return save_infos[i] return None def auto_save(self): @@ -141,36 +144,167 @@ class WickedQuest: # 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 - + + # Spend the bone dust + if not self.player.spend_save_bone_dust(200): + return False + # Automatically create save try: - success, message = self.saveManager.create_save( - self.player, - self.currentLevel.levelId, - self.gameStartTime, - self.currentGame - ) - - if success: - try: - if 'save' in self.get_sounds(): - play_sound(self.get_sounds()['save']) - else: - print("Save sound not found in sounds dictionary") - except Exception as e: - print(f"Error playing save sound: {e}") - pass # Continue if save sound fails to play - else: - print(f"Save failed: {message}") - - return success + # Create save data structure + save_data = { + "player_state": self._serialize_player_state(), + "game_state": { + "currentLevel": self.currentLevel.levelId, + "currentGame": self.currentGame, + "gameStartTime": self.gameStartTime, + }, + } + + # Create metadata for display + metadata = { + "display_name": f"{self.currentGame} Level {self.currentLevel.levelId}", + "level": self.currentLevel.levelId, + "game": self.currentGame + } + + save_path = self.saveManager.create_save(save_data, metadata) + + try: + if 'save' in self.get_sounds(): + play_sound(self.get_sounds()['save']) + else: + print("Save sound not found in sounds dictionary") + except Exception as e: + print(f"Error playing save sound: {e}") + pass # Continue if save sound fails to play + + return True except Exception as e: print(f"Error during save: {e}") return False + def _serialize_player_state(self): + """Serialize player state for saving""" + return { + "xPos": self.player.xPos, + "yPos": self.player.yPos, + "health": self.player._health, + "maxHealth": self.player._maxHealth, + "lives": self.player._lives, + "coins": self.player._coins, + "saveBoneDust": self.player._saveBoneDust, + "jackOLanternCount": self.player._jack_o_lantern_count, + "shinBoneCount": self.player.shinBoneCount, + "inventory": self.player.inventory, + "collectedItems": self.player.collectedItems, + "weapons": self._serialize_weapons(self.player.weapons), + "currentWeaponName": self.player.currentWeapon.name if self.player.currentWeapon else None, + "stats": self.player.stats.to_dict(), + "scoreboard": self._serialize_scoreboard(self.player.scoreboard), + } + + def _serialize_weapons(self, weapons): + """Serialize weapons for saving""" + serialized = [] + for weapon in weapons: + serialized.append({ + "name": weapon.name, + "damage": weapon.damage, + "range": weapon.range, + "attackSound": weapon.attack_sound, + "hitSound": weapon.hit_sound, + "attackDuration": weapon.attack_duration, + "speedBonus": getattr(weapon, "speedBonus", 1.0), + "jumpDurationBonus": getattr(weapon, "jumpDurationBonus", 1.0), + }) + return serialized + + def _serialize_scoreboard(self, scoreboard): + """Serialize scoreboard for saving""" + return { + "currentScore": getattr(scoreboard, "currentScore", 0), + "highScores": getattr(scoreboard, "highScores", []), + } + + def _restore_player_state(self, player_state): + """Restore player state from save data""" + # Restore basic attributes + self.player.xPos = player_state["xPos"] + self.player.yPos = player_state["yPos"] + self.player._health = player_state["health"] + self.player._maxHealth = player_state["maxHealth"] + self.player._lives = player_state["lives"] + self.player._coins = player_state["coins"] + self.player._saveBoneDust = player_state["saveBoneDust"] + self.player._jack_o_lantern_count = player_state["jackOLanternCount"] + self.player.shinBoneCount = player_state["shinBoneCount"] + self.player.inventory = player_state["inventory"] + self.player.collectedItems = player_state["collectedItems"] + + # Restore weapons + self.player.weapons = self._deserialize_weapons(player_state["weapons"]) + + # Restore current weapon + current_weapon_name = player_state.get("currentWeaponName") + if current_weapon_name: + for weapon in self.player.weapons: + if weapon.name == current_weapon_name: + self.player.currentWeapon = weapon + break + + # Restore stats + if "stats" in player_state: + self.player.stats = StatTracker.from_dict(player_state["stats"]) + else: + self.player.stats = StatTracker() + + # Restore scoreboard + if "scoreboard" in player_state: + self.player.scoreboard = self._deserialize_scoreboard(player_state["scoreboard"]) + else: + self.player.scoreboard = Scoreboard() + + def _deserialize_weapons(self, weapon_data): + """Deserialize weapons from save data""" + from src.weapon import Weapon + + weapons = [] + for data in weapon_data: + # Handle backward compatibility for old saves + speedBonus = data.get("speedBonus", 1.0) + jumpDurationBonus = data.get("jumpDurationBonus", 1.0) + + # For old saves, restore proper bonuses for specific weapons + if data["name"] == "witch_broom" and speedBonus == 1.0: + speedBonus = 1.17 + jumpDurationBonus = 1.25 + + weapon = Weapon( + name=data["name"], + damage=data["damage"], + range=data["range"], + attackSound=data["attackSound"], + hitSound=data["hitSound"], + attackDuration=data["attackDuration"], + speedBonus=speedBonus, + jumpDurationBonus=jumpDurationBonus, + ) + weapons.append(weapon) + return weapons + + def _deserialize_scoreboard(self, scoreboard_data): + """Deserialize scoreboard from save data""" + scoreboard = Scoreboard() + if "currentScore" in scoreboard_data: + scoreboard.currentScore = scoreboard_data["currentScore"] + if "highScores" in scoreboard_data: + scoreboard.highScores = scoreboard_data["highScores"] + return scoreboard + def handle_input(self): """Process keyboard input for player actions.""" keys = pygame.key.get_pressed() @@ -257,7 +391,7 @@ class WickedQuest: # Handle attack with either CTRL key if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime): - play_sound(self.get_sounds()[player.currentWeapon.attackSound]) + play_sound(self.get_sounds()[player.currentWeapon.attack_sound]) # Handle jumping if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping: @@ -291,14 +425,14 @@ class WickedQuest: seconds = (timeTaken % 60000) // 1000 # Update time in stats - self.currentLevel.player.stats.update_stat('Total time', timeTaken, levelOnly=True) + self.currentLevel.player.stats.set_stat('Total time', timeTaken, level_only=True) report = [f"Time taken: {minutes} minutes and {seconds} seconds"] # Add all level stats for key in self.currentLevel.player.stats.level: if key != 'Total time': # Skip time since we already displayed it - report.append(f"{key}: {self.currentLevel.player.stats.get_level_stat(key)}") + report.append(f"{key}: {self.currentLevel.player.stats.get_stat(key)}") report.append(f"Score: {int(self.currentLevel.levelScore)}") @@ -324,7 +458,7 @@ class WickedQuest: # 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)}") + report.append(f"Total {key}: {self.currentLevel.player.stats.get_stat(key, from_total=True)}") report.append(f"Final Score: {self.player.scoreboard.get_score()}") @@ -350,7 +484,7 @@ class WickedQuest: # 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)}") + report.append(f"Total {key}: {self.currentLevel.player.stats.get_stat(key, from_total=True)}") if self.currentLevel.player.scoreboard.check_high_score(): pygame.event.clear() @@ -479,24 +613,25 @@ class WickedQuest: elif choice == "load_game": selected_save = self.load_game_menu() if selected_save: - success, save_data = self.saveManager.load_save(selected_save['filepath']) - if success: + try: + save_data, metadata = self.saveManager.load_save(selected_save['filepath']) + # Load the saved game self.currentGame = save_data['game_state']['currentGame'] self.gameStartTime = save_data['game_state']['gameStartTime'] current_level = save_data['game_state']['currentLevel'] # Initialize pack-specific sound system self.initialize_pack_sounds() - + # Load the level if self.load_level(current_level): # Restore player state - self.saveManager.restore_player_state(self.player, save_data) + self._restore_player_state(save_data['player_state']) self.game_loop(current_level) else: messagebox("Failed to load saved level.") - else: - messagebox(f"Failed to load save: {save_data}") + except Exception as e: + messagebox(f"Failed to load save: {e}") elif choice == "play": self.currentGame = select_game(self.get_sounds()) if self.currentGame is None: