Compare commits
2 Commits
cc0f0437f5
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c442261d9 | ||
|
|
d83e7db248 |
@@ -31,7 +31,7 @@ l: Check lives remaining.
|
||||
Alt+PageDown: Master volume decrease.
|
||||
Alt+PageUp: Master volume increase.
|
||||
Alt+End: Ambience volume decrease.
|
||||
Alt+Home: Game volume increase.
|
||||
Alt+Home: Ambience volume increase.
|
||||
Alt+Delete: Game sounds volume decrease.
|
||||
Alt+Insert: Game sounds volume increase.
|
||||
Backspace: Pause or resume the game.
|
||||
@@ -44,22 +44,23 @@ Spider webs can be passed by ducking as you move by them.
|
||||
If you hit a spiderweb, a spider spawns, and you are slowed for 15 seconds. If you are invincible, you are not slowed, but the spider still appears.
|
||||
Running and jumping both move you at 1.5 your normal speed.
|
||||
Items bounce away from you when they are freed from the coffin. You must jump to catch them when they are in range.
|
||||
You can switch between weapons you've collected using the number keys (1, 2, 3). This allows strategic combat - use different weapons for different situations based on their damage, range, and speed.
|
||||
You can switch between weapons you've collected using the number keys 1, 2, and 3. This allows strategic combat use different weapons for different situations based on their damage, range, and speed.
|
||||
The shovel is a weak weapon, but it can fill in graves. While wielding the shovel hold down and walk (do not run) over the grave. If you run you will trip and fall in.
|
||||
The witch's broom isn't in good enough condition from bashing enemies to be used for true flight, but it does grant a speed bonus and also allows you to land at any point while jumping, just press down.
|
||||
The game automatically saves your progress when you have collected 200 bone dust. You will hear a message saying the game has been saved, and a wolf will howl. To load a game, use the load option in the main menu. Load only appears if you have saved games. The first save should happen around level 6.
|
||||
The game automatically saves your progress when you have collected 200 bone dust. You will hear a message saying the game has been saved, and a wolf will howl. To load a game, use the load option in the main menu. Load only appears if you have saved games.
|
||||
|
||||
Game Modes
|
||||
|
||||
Story Mode: Traditional level-by-level progression through predefined stages. Your progress is saved, and you can collect extra lives by gathering bone dust.
|
||||
|
||||
Survival Mode: An endless challenge where you face wave after wave of increasingly difficult enemies. Each wave gets progressively harder with stronger enemies, faster spawn rates, and longer levels. In survival mode:
|
||||
Survival Mode: An endless challenge where you face wave after wave of increasingly difficult enemies. Each wave gets progressively harder with stronger enemies, faster spawn rates, and longer levels. Level length starts at 300 units and increases by 20 units per wave (capped at 500 units). In survival mode:
|
||||
No saving or loading - each run is a fresh start
|
||||
No extra lives are awarded - you rely on your starting lives only
|
||||
All enemies use aggressive "hunter" behavior instead of patrol patterns
|
||||
No extra lives are awarded
|
||||
Each wave must be completed by defeating all enemies before you can advance
|
||||
Bone dust still provides health bonuses but gives bonus score instead of extra lives
|
||||
All coffin contents are randomized to ensure fair gameplay
|
||||
Bone dust still provides health bonuses but collecting 100 bone dust gives 2000 bonus score instead of extra lives
|
||||
Monsters unlock per wave that matches the level in which they are found
|
||||
Once all monsters are unlocked your score is doubled
|
||||
All coffin contents are randomized
|
||||
A detailed statistics report shows your performance at the end of each run
|
||||
|
||||
Enemies
|
||||
@@ -80,7 +81,7 @@ Bonuses and items
|
||||
|
||||
Bone dust: Currency of the game. Collect it to gain health and extra lives.
|
||||
Cauldron: Restores you to full health.
|
||||
Witch's broom: A weapon gained from witches. Stronger than the grave digger's rusty shovel.
|
||||
Witch's broom: A weapon gained from witches. Stronger than the grave digger's rusty shovel and grants extra speed and jump time.
|
||||
Coffin: Contains items. Be quick, they try to escape.
|
||||
Guts: Adds 2 health to your maximum unlife if your unlife is below 20 points otherwise it restores your health.
|
||||
Hand of Glory: Grants invincibility for a short time.
|
||||
|
||||
36
src/level.py
36
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"])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -57,6 +57,8 @@ class SurvivalGenerator:
|
||||
# Parse objects
|
||||
for obj in data.get("objects", []):
|
||||
objCopy = copy.deepcopy(obj)
|
||||
# Add source level information to track difficulty progression
|
||||
objCopy["source_level"] = levelNum
|
||||
|
||||
# Categorize objects
|
||||
if "enemy_type" in obj:
|
||||
@@ -157,16 +159,15 @@ class SurvivalGenerator:
|
||||
|
||||
def place_enemy(self, xPos, difficultyLevel):
|
||||
"""Place an enemy at the given position with scaled difficulty."""
|
||||
# Filter out boss enemies for early waves
|
||||
bossEnemies = ["witch", "boogie_man", "revenant", "ghost", "headless_horseman"]
|
||||
|
||||
if difficultyLevel < 3: # Waves 1-2: no bosses
|
||||
availableEnemies = [e for e in self.enemyTemplates if e.get("enemy_type") not in bossEnemies]
|
||||
elif difficultyLevel < 5: # Waves 3-4: exclude the hardest bosses
|
||||
hardestBosses = ["revenant", "ghost", "headless_horseman"]
|
||||
availableEnemies = [e for e in self.enemyTemplates if e.get("enemy_type") not in hardestBosses]
|
||||
else: # Wave 5+: all enemies allowed
|
||||
# Level-based filtering: allow enemies from levels 1 through current wave
|
||||
maxLevel = max(self.levelData.keys()) if self.levelData else 1
|
||||
allowedLevels = list(range(1, min(difficultyLevel, maxLevel) + 1))
|
||||
|
||||
# If we're past the max level, allow all enemies
|
||||
if difficultyLevel > maxLevel:
|
||||
availableEnemies = self.enemyTemplates
|
||||
else:
|
||||
availableEnemies = [e for e in self.enemyTemplates if e.get("source_level", 1) in allowedLevels]
|
||||
|
||||
# Fallback to all enemies if filtering removed everything
|
||||
if not availableEnemies:
|
||||
|
||||
241
wicked_quest.py
241
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:
|
||||
@@ -649,8 +784,18 @@ 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}!")
|
||||
# Check for all monsters unlocked bonus and prepare wave message
|
||||
waveMessage = f"Wave {self.survivalWave}!"
|
||||
if self.survivalGenerator:
|
||||
maxLevel = max(self.survivalGenerator.levelData.keys()) if self.survivalGenerator.levelData else 1
|
||||
if self.survivalWave == maxLevel + 1: # First wave after all levels unlocked
|
||||
# Double the current score
|
||||
self.survivalScore *= 2
|
||||
play_sound(self.get_sounds().get("survivor_bonus", self.get_sounds()["get_extra_life"]))
|
||||
waveMessage += " All monsters unlocked bonus! Score doubled!"
|
||||
|
||||
# Announce new wave (with bonus message if applicable)
|
||||
speak(waveMessage)
|
||||
|
||||
# Generate new segment
|
||||
segmentLength = min(500, 300 + (self.survivalWave * 20)) # Longer segments over time
|
||||
|
||||
Reference in New Issue
Block a user