lint and spacing fixes. Also fixed problem with levels not showing up in compiled version (hopefully).
This commit is contained in:
@@ -11,8 +11,8 @@ class SaveManager:
|
||||
def __init__(self):
|
||||
"""Initialize save manager with XDG-compliant save directory"""
|
||||
# Use XDG_CONFIG_HOME or default to ~/.config
|
||||
config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
|
||||
self.save_dir = Path(config_home) / 'storm-games' / 'wicked-quest'
|
||||
config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
||||
self.save_dir = Path(config_home) / "storm-games" / "wicked-quest"
|
||||
self.save_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.max_saves = 10
|
||||
|
||||
@@ -24,7 +24,7 @@ class SaveManager:
|
||||
# Validate required parameters
|
||||
if current_game is None:
|
||||
return False, "No game selected to save"
|
||||
|
||||
|
||||
if current_level is None:
|
||||
return False, "No current level to save"
|
||||
|
||||
@@ -34,30 +34,30 @@ class SaveManager:
|
||||
|
||||
# Create save data
|
||||
save_data = {
|
||||
'player_state': {
|
||||
'xPos': player.xPos,
|
||||
'yPos': player.yPos,
|
||||
'health': player._health,
|
||||
'maxHealth': player._maxHealth,
|
||||
'lives': player._lives,
|
||||
'coins': player._coins,
|
||||
'saveBoneDust': player._saveBoneDust,
|
||||
'jackOLanternCount': player._jack_o_lantern_count,
|
||||
'shinBoneCount': player.shinBoneCount,
|
||||
'inventory': player.inventory,
|
||||
'collectedItems': player.collectedItems,
|
||||
'weapons': self._serialize_weapons(player.weapons),
|
||||
'currentWeaponName': player.currentWeapon.name if player.currentWeapon else None,
|
||||
'stats': self._serialize_stats(player.stats),
|
||||
'scoreboard': self._serialize_scoreboard(player.scoreboard)
|
||||
"player_state": {
|
||||
"xPos": player.xPos,
|
||||
"yPos": player.yPos,
|
||||
"health": player._health,
|
||||
"maxHealth": player._maxHealth,
|
||||
"lives": player._lives,
|
||||
"coins": player._coins,
|
||||
"saveBoneDust": player._saveBoneDust,
|
||||
"jackOLanternCount": player._jack_o_lantern_count,
|
||||
"shinBoneCount": player.shinBoneCount,
|
||||
"inventory": player.inventory,
|
||||
"collectedItems": player.collectedItems,
|
||||
"weapons": self._serialize_weapons(player.weapons),
|
||||
"currentWeaponName": player.currentWeapon.name if player.currentWeapon else None,
|
||||
"stats": self._serialize_stats(player.stats),
|
||||
"scoreboard": self._serialize_scoreboard(player.scoreboard),
|
||||
},
|
||||
'game_state': {
|
||||
'currentLevel': current_level,
|
||||
'currentGame': current_game,
|
||||
'gameStartTime': game_start_time,
|
||||
'saveTime': datetime.now()
|
||||
"game_state": {
|
||||
"currentLevel": current_level,
|
||||
"currentGame": current_game,
|
||||
"gameStartTime": game_start_time,
|
||||
"saveTime": datetime.now(),
|
||||
},
|
||||
'version': '1.0'
|
||||
"version": "1.0",
|
||||
}
|
||||
|
||||
# Generate filename with timestamp
|
||||
@@ -67,26 +67,26 @@ class SaveManager:
|
||||
|
||||
try:
|
||||
# Write to temporary file first, then rename for atomic operation
|
||||
temp_filepath = filepath.with_suffix('.tmp')
|
||||
|
||||
with open(temp_filepath, 'wb') as f:
|
||||
temp_filepath = filepath.with_suffix(".tmp")
|
||||
|
||||
with open(temp_filepath, "wb") as f:
|
||||
pickle.dump(save_data, f)
|
||||
f.flush() # Ensure data is written to disk
|
||||
os.fsync(f.fileno()) # Force write to disk
|
||||
|
||||
|
||||
# Atomic rename (replaces old file if it exists)
|
||||
temp_filepath.rename(filepath)
|
||||
|
||||
|
||||
# Clean up old saves if we exceed max_saves
|
||||
self._cleanup_old_saves()
|
||||
|
||||
|
||||
return True, f"Game saved to {filename}"
|
||||
except Exception as e:
|
||||
# Clean up temp file if it exists
|
||||
if temp_filepath.exists():
|
||||
try:
|
||||
temp_filepath.unlink()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return False, f"Failed to save game: {str(e)}"
|
||||
|
||||
@@ -94,132 +94,136 @@ class SaveManager:
|
||||
"""Serialize weapons for saving"""
|
||||
serialized = []
|
||||
for weapon in weapons:
|
||||
serialized.append({
|
||||
'name': weapon.name,
|
||||
'damage': weapon.damage,
|
||||
'range': weapon.range,
|
||||
'attackSound': weapon.attackSound,
|
||||
'hitSound': weapon.hitSound,
|
||||
'attackDuration': weapon.attackDuration,
|
||||
'speedBonus': getattr(weapon, 'speedBonus', 1.0),
|
||||
'jumpDurationBonus': getattr(weapon, 'jumpDurationBonus', 1.0)
|
||||
})
|
||||
serialized.append(
|
||||
{
|
||||
"name": weapon.name,
|
||||
"damage": weapon.damage,
|
||||
"range": weapon.range,
|
||||
"attackSound": weapon.attackSound,
|
||||
"hitSound": weapon.hitSound,
|
||||
"attackDuration": weapon.attackDuration,
|
||||
"speedBonus": getattr(weapon, "speedBonus", 1.0),
|
||||
"jumpDurationBonus": getattr(weapon, "jumpDurationBonus", 1.0),
|
||||
}
|
||||
)
|
||||
return serialized
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
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'],
|
||||
name=data["name"],
|
||||
damage=data["damage"],
|
||||
range=data["range"],
|
||||
attackSound=data["attackSound"],
|
||||
hitSound=data["hitSound"],
|
||||
attackDuration=data["attackDuration"],
|
||||
speedBonus=speedBonus,
|
||||
jumpDurationBonus=jumpDurationBonus
|
||||
jumpDurationBonus=jumpDurationBonus,
|
||||
)
|
||||
weapons.append(weapon)
|
||||
return weapons
|
||||
|
||||
def _serialize_stats(self, stats):
|
||||
"""Serialize stats for saving"""
|
||||
return {
|
||||
'total': stats.total.copy(),
|
||||
'level': stats.level.copy()
|
||||
}
|
||||
return {"total": stats.total.copy(), "level": stats.level.copy()}
|
||||
|
||||
def _deserialize_stats(self, stats_data):
|
||||
"""Deserialize stats from save data"""
|
||||
from src.stat_tracker import StatTracker
|
||||
|
||||
stats = StatTracker()
|
||||
if 'total' in stats_data:
|
||||
stats.total.update(stats_data['total'])
|
||||
if 'level' in stats_data:
|
||||
stats.level.update(stats_data['level'])
|
||||
if "total" in stats_data:
|
||||
stats.total.update(stats_data["total"])
|
||||
if "level" in stats_data:
|
||||
stats.level.update(stats_data["level"])
|
||||
return stats
|
||||
|
||||
def _serialize_scoreboard(self, scoreboard):
|
||||
"""Serialize scoreboard for saving"""
|
||||
return {
|
||||
'currentScore': getattr(scoreboard, 'currentScore', 0),
|
||||
'highScores': getattr(scoreboard, 'highScores', [])
|
||||
"currentScore": getattr(scoreboard, "currentScore", 0),
|
||||
"highScores": getattr(scoreboard, "highScores", []),
|
||||
}
|
||||
|
||||
def _deserialize_scoreboard(self, scoreboard_data):
|
||||
"""Deserialize scoreboard from save data"""
|
||||
from libstormgames import Scoreboard
|
||||
|
||||
scoreboard = Scoreboard()
|
||||
if 'currentScore' in scoreboard_data:
|
||||
scoreboard.currentScore = scoreboard_data['currentScore']
|
||||
if 'highScores' in scoreboard_data:
|
||||
scoreboard.highScores = scoreboard_data['highScores']
|
||||
if "currentScore" in scoreboard_data:
|
||||
scoreboard.currentScore = scoreboard_data["currentScore"]
|
||||
if "highScores" in scoreboard_data:
|
||||
scoreboard.highScores = scoreboard_data["highScores"]
|
||||
return scoreboard
|
||||
|
||||
def get_save_files(self):
|
||||
"""Get list of save files with metadata"""
|
||||
save_files = []
|
||||
pattern = str(self.save_dir / "save_*.pickle")
|
||||
|
||||
|
||||
for filepath in glob.glob(pattern):
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
with open(filepath, "rb") as f:
|
||||
save_data = pickle.load(f)
|
||||
|
||||
|
||||
# Validate save data structure
|
||||
if not self._validate_save_data(save_data):
|
||||
print(f"Invalid save file structure: {filepath}")
|
||||
continue
|
||||
|
||||
|
||||
# Extract save info
|
||||
save_time = save_data['game_state']['saveTime']
|
||||
level = save_data['game_state']['currentLevel']
|
||||
game_name = save_data['game_state']['currentGame']
|
||||
|
||||
save_time = save_data["game_state"]["saveTime"]
|
||||
level = save_data["game_state"]["currentLevel"]
|
||||
game_name = save_data["game_state"]["currentGame"]
|
||||
|
||||
# Format display name
|
||||
formatted_time = save_time.strftime("%B %d %I:%M%p")
|
||||
display_name = f"{formatted_time} {game_name} Level {level}"
|
||||
|
||||
save_files.append({
|
||||
'filepath': filepath,
|
||||
'display_name': display_name,
|
||||
'save_time': save_time,
|
||||
'level': level,
|
||||
'game_name': game_name,
|
||||
'save_data': save_data
|
||||
})
|
||||
|
||||
save_files.append(
|
||||
{
|
||||
"filepath": filepath,
|
||||
"display_name": display_name,
|
||||
"save_time": save_time,
|
||||
"level": level,
|
||||
"game_name": game_name,
|
||||
"save_data": save_data,
|
||||
}
|
||||
)
|
||||
except (pickle.PickleError, EOFError, OSError) as e:
|
||||
print(f"Corrupted save file {filepath}: {e}")
|
||||
# Try to remove corrupted save file
|
||||
try:
|
||||
os.remove(filepath)
|
||||
print(f"Removed corrupted save file: {filepath}")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Error reading save file {filepath}: {e}")
|
||||
continue
|
||||
|
||||
|
||||
# Sort by save time (newest first)
|
||||
save_files.sort(key=lambda x: x['save_time'], reverse=True)
|
||||
save_files.sort(key=lambda x: x["save_time"], reverse=True)
|
||||
return save_files
|
||||
|
||||
def load_save(self, filepath):
|
||||
"""Load game state from save file"""
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
with open(filepath, "rb") as f:
|
||||
save_data = pickle.load(f)
|
||||
return True, save_data
|
||||
except Exception as e:
|
||||
@@ -227,55 +231,57 @@ class SaveManager:
|
||||
|
||||
def restore_player_state(self, player, save_data):
|
||||
"""Restore player state from save data"""
|
||||
player_state = save_data['player_state']
|
||||
|
||||
player_state = save_data["player_state"]
|
||||
|
||||
# Restore basic attributes
|
||||
player.xPos = player_state['xPos']
|
||||
player.yPos = player_state['yPos']
|
||||
player._health = player_state['health']
|
||||
player._maxHealth = player_state['maxHealth']
|
||||
player._lives = player_state['lives']
|
||||
player._coins = player_state['coins']
|
||||
player._saveBoneDust = player_state['saveBoneDust']
|
||||
player._jack_o_lantern_count = player_state['jackOLanternCount']
|
||||
player.shinBoneCount = player_state['shinBoneCount']
|
||||
player.inventory = player_state['inventory']
|
||||
player.collectedItems = player_state['collectedItems']
|
||||
|
||||
player.xPos = player_state["xPos"]
|
||||
player.yPos = player_state["yPos"]
|
||||
player._health = player_state["health"]
|
||||
player._maxHealth = player_state["maxHealth"]
|
||||
player._lives = player_state["lives"]
|
||||
player._coins = player_state["coins"]
|
||||
player._saveBoneDust = player_state["saveBoneDust"]
|
||||
player._jack_o_lantern_count = player_state["jackOLanternCount"]
|
||||
player.shinBoneCount = player_state["shinBoneCount"]
|
||||
player.inventory = player_state["inventory"]
|
||||
player.collectedItems = player_state["collectedItems"]
|
||||
|
||||
# Restore weapons
|
||||
player.weapons = self._deserialize_weapons(player_state['weapons'])
|
||||
|
||||
player.weapons = self._deserialize_weapons(player_state["weapons"])
|
||||
|
||||
# Restore current weapon
|
||||
current_weapon_name = player_state.get('currentWeaponName')
|
||||
current_weapon_name = player_state.get("currentWeaponName")
|
||||
if current_weapon_name:
|
||||
for weapon in player.weapons:
|
||||
if weapon.name == current_weapon_name:
|
||||
player.currentWeapon = weapon
|
||||
break
|
||||
|
||||
|
||||
# Restore stats
|
||||
if 'stats' in player_state:
|
||||
player.stats = self._deserialize_stats(player_state['stats'])
|
||||
if "stats" in player_state:
|
||||
player.stats = self._deserialize_stats(player_state["stats"])
|
||||
else:
|
||||
from src.stat_tracker import StatTracker
|
||||
|
||||
player.stats = StatTracker()
|
||||
|
||||
|
||||
# Restore scoreboard
|
||||
if 'scoreboard' in player_state:
|
||||
player.scoreboard = self._deserialize_scoreboard(player_state['scoreboard'])
|
||||
if "scoreboard" in player_state:
|
||||
player.scoreboard = self._deserialize_scoreboard(player_state["scoreboard"])
|
||||
else:
|
||||
from libstormgames import Scoreboard
|
||||
|
||||
player.scoreboard = Scoreboard()
|
||||
|
||||
def _cleanup_old_saves(self):
|
||||
"""Remove old save files if we exceed max_saves"""
|
||||
save_files = self.get_save_files()
|
||||
|
||||
|
||||
if len(save_files) > self.max_saves:
|
||||
# Remove oldest saves
|
||||
for save_file in save_files[self.max_saves:]:
|
||||
try:
|
||||
os.remove(save_file['filepath'])
|
||||
os.remove(save_file["filepath"])
|
||||
except Exception as e:
|
||||
print(f"Error removing old save {save_file['filepath']}: {e}")
|
||||
|
||||
@@ -283,25 +289,24 @@ class SaveManager:
|
||||
"""Validate that save data has required structure"""
|
||||
try:
|
||||
# Check for required top-level keys
|
||||
required_keys = ['player_state', 'game_state', 'version']
|
||||
required_keys = ["player_state", "game_state", "version"]
|
||||
if not all(key in save_data for key in required_keys):
|
||||
return False
|
||||
|
||||
|
||||
# Check player_state structure
|
||||
player_required = ['xPos', 'yPos', 'health', 'maxHealth', 'lives', 'coins', 'saveBoneDust']
|
||||
if not all(key in save_data['player_state'] for key in player_required):
|
||||
player_required = ["xPos", "yPos", "health", "maxHealth", "lives", "coins", "saveBoneDust"]
|
||||
if not all(key in save_data["player_state"] for key in player_required):
|
||||
return False
|
||||
|
||||
|
||||
# Check game_state structure
|
||||
game_required = ['currentLevel', 'currentGame', 'gameStartTime', 'saveTime']
|
||||
if not all(key in save_data['game_state'] for key in game_required):
|
||||
game_required = ["currentLevel", "currentGame", "gameStartTime", "saveTime"]
|
||||
if not all(key in save_data["game_state"] for key in game_required):
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def has_saves(self):
|
||||
"""Check if any save files exist"""
|
||||
return len(self.get_save_files()) > 0
|
||||
|
||||
|
Reference in New Issue
Block a user