Compare commits

1 Commits

Author SHA1 Message Date
Storm Dragon
1c442261d9 Migrate to libstormgames systems. 2025-09-16 01:51:36 -04:00
4 changed files with 247 additions and 83 deletions

View File

@@ -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"])

View File

@@ -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)

View File

@@ -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)

View File

@@ -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: