Fix up some inconsistancies in the code levels should only be needed in one directory now, so update the hook to move levels into place on first run if needed. Warning, this version breaks existing save files.

This commit is contained in:
Storm Dragon
2025-10-03 01:46:28 -04:00
parent 95fc94a507
commit dd67eb5e1d
14 changed files with 410 additions and 422 deletions
+22 -20
View File
@@ -5,53 +5,55 @@ import shutil
# Runtime hook to move directories and files from _internal to parent directory
if hasattr(sys, '_MEIPASS'):
# We're running from a PyInstaller bundle
bundle_dir = os.path.dirname(sys.executable)
internal_dir = sys._MEIPASS
bundleDir = os.path.dirname(sys.executable)
internalDir = sys._MEIPASS
# Directories to move from _internal to parent
dirs_to_move = ['sounds', 'libstormgames']
# All these use the parent directory for easier user customization
dirsToMove = ['sounds', 'libstormgames', 'levels']
# Directories to copy (keep in both locations)
dirs_to_copy = ['levels']
# None needed - everything now uses parent directory
dirsToCopy = []
# Files to move from _internal to parent
files_to_move = ['files', 'logo.png']
filesToMove = ['files', 'logo.png']
# Move directories
for dir_name in dirs_to_move:
internal_path = os.path.join(internal_dir, dir_name)
target_path = os.path.join(bundle_dir, dir_name)
for dir_name in dirsToMove:
internalPath = os.path.join(internalDir, dir_name)
targetPath = os.path.join(bundleDir, dir_name)
# Only move if source exists and target doesn't exist
if os.path.exists(internal_path) and not os.path.exists(target_path):
if os.path.exists(internalPath) and not os.path.exists(targetPath):
try:
shutil.move(internal_path, target_path)
shutil.move(internalPath, targetPath)
except Exception as e:
# Silently fail if we can't move - game will still work from _internal
pass
# Copy directories (keep in both locations)
for dir_name in dirs_to_copy:
internal_path = os.path.join(internal_dir, dir_name)
target_path = os.path.join(bundle_dir, dir_name)
for dir_name in dirsToCopy:
internalPath = os.path.join(internalDir, dir_name)
targetPath = os.path.join(bundleDir, dir_name)
# Only copy if source exists and target doesn't exist
if os.path.exists(internal_path) and not os.path.exists(target_path):
if os.path.exists(internalPath) and not os.path.exists(targetPath):
try:
shutil.copytree(internal_path, target_path)
shutil.copytree(internalPath, targetPath)
except Exception as e:
# Silently fail if we can't copy - game will still work from _internal
pass
# Move files
for file_name in files_to_move:
internal_path = os.path.join(internal_dir, file_name)
target_path = os.path.join(bundle_dir, file_name)
for file_name in filesToMove:
internalPath = os.path.join(internalDir, file_name)
targetPath = os.path.join(bundleDir, file_name)
# Only move if source exists and target doesn't exist
if os.path.exists(internal_path) and not os.path.exists(target_path):
if os.path.exists(internalPath) and not os.path.exists(targetPath):
try:
shutil.move(internal_path, target_path)
shutil.move(internalPath, targetPath)
except Exception as e:
# Silently fail if we can't move - game will still work from _internal at least enough to exit.
pass
BIN
View File
Binary file not shown.
+14 -14
View File
@@ -18,8 +18,8 @@ class CoffinObject(Object):
self.sounds = sounds
self.level = level
self.isBroken = False
self.dropped_item = None
self.specified_item = item
self.droppedItem = None
self.specifiedItem = item
def hit(self, player_pos):
"""Handle being hit by the player's weapon"""
@@ -38,32 +38,32 @@ class CoffinObject(Object):
self.isActive = False
# Determine item to drop
if self.specified_item == "random":
if self.specifiedItem == "random":
# Check if we're in survival mode (level_id 999)
is_survival_mode = hasattr(self.level, 'levelData') and self.level.levelData.get('level_id') == 999
item_type = ItemProperties.get_random_item(survival_mode=is_survival_mode)
isSurvivalMode = hasattr(self.level, 'levelData') and self.level.levelData.get('level_id') == 999
itemType = ItemProperties.get_random_item(survivalMode=isSurvivalMode)
else:
# Validate specified item
if ItemProperties.is_valid_item(self.specified_item):
item_type = self.specified_item
if ItemProperties.is_valid_item(self.specifiedItem):
itemType = self.specifiedItem
else:
# Fall back to random if invalid item specified
# Check if we're in survival mode (level_id 999)
is_survival_mode = hasattr(self.level, 'levelData') and self.level.levelData.get('level_id') == 999
item_type = ItemProperties.get_random_item(survival_mode=is_survival_mode)
isSurvivalMode = hasattr(self.level, 'levelData') and self.level.levelData.get('level_id') == 999
itemType = ItemProperties.get_random_item(survivalMode=isSurvivalMode)
# Create item 1-2 tiles away in random direction
direction = random.choice([-1, 1])
drop_distance = random.randint(1, 2)
drop_x = self.xPos + (direction * drop_distance)
dropDistance = random.randint(1, 2)
dropX = self.xPos + (direction * dropDistance)
self.dropped_item = PowerUp(
drop_x, self.yPos, item_type, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
self.droppedItem = PowerUp(
dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
)
# Apply sound override after creation (similar to how graves handle it)
if hasattr(self, 'itemSoundOverride'):
self.dropped_item.soundName = self.itemSoundOverride
self.droppedItem.soundName = self.itemSoundOverride
return True
return False
+16 -16
View File
@@ -19,7 +19,7 @@ class Enemy(Object):
self.level = level
self.health = kwargs.get("health", 5) # Default 5 HP
self.damage = kwargs.get("damage", 1) # Default 1 damage
self.attackRange = kwargs.get("attack_range", 1) # Default 1 tile range
self.attackRange = kwargs.get("attackRange", 1) # Default 1 tile range
self.sounds = sounds # Store reference to game sounds
# Movement and behavior properties
@@ -32,25 +32,25 @@ class Enemy(Object):
self._currentX = self.xRange[0] # Initialize current position
# Add spawn configuration
self.canSpawn = kwargs.get("can_spawn", False)
self.canSpawn = kwargs.get("canSpawn", False)
if self.canSpawn:
self.spawnCooldown = kwargs.get("spawn_cooldown", 2000)
self.spawnChance = kwargs.get("spawn_chance", 25)
self.spawnType = kwargs.get("spawn_type", "zombie") # Default to zombie for backward compatibility
self.spawnDistance = kwargs.get("spawn_distance", 5)
self.spawnCooldown = kwargs.get("spawnCooldown", 2000)
self.spawnChance = kwargs.get("spawnChance", 25)
self.spawnType = kwargs.get("spawnType", "zombie") # Default to zombie for backward compatibility
self.spawnDistance = kwargs.get("spawnDistance", 5)
self.lastSpawnTime = 0
# Attack pattern configuration
self.attackPattern = kwargs.get("attack_pattern", {"type": "patrol"})
self.attackPattern = kwargs.get("attackPattern", {"type": "patrol"})
self.turnThreshold = self.attackPattern.get("turn_threshold", 5)
# Initialize vulnerability system
self.hasVulnerabilitySystem = kwargs.get("has_vulnerability", False)
self.hasVulnerabilitySystem = kwargs.get("hasVulnerability", False)
if self.hasVulnerabilitySystem:
self.isVulnerable = False # Start invulnerable
self.vulnerabilityTimer = pygame.time.get_ticks()
self.vulnerabilityDuration = kwargs.get("vulnerability_duration", 2000)
self.invulnerabilityDuration = kwargs.get("invulnerability_duration", 5000)
self.vulnerabilityDuration = kwargs.get("vulnerabilityDuration", 2000)
self.invulnerabilityDuration = kwargs.get("invulnerabilityDuration", 5000)
soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType
self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos)
else:
@@ -63,7 +63,7 @@ class Enemy(Object):
self.health = 1 # Easy to kill
self.attackCooldown = 1500 # Slower attack rate
elif enemyType == "spider":
speedMultiplier = kwargs.get("speed_multiplier", 2.0)
speedMultiplier = kwargs.get("speedMultiplier", 2.0)
self.movementSpeed *= speedMultiplier # Spiders are faster
self.attackPattern = {"type": "hunter"} # Spiders actively hunt the player
self.turnThreshold = 3 # Spiders turn around quickly to chase player
@@ -162,7 +162,7 @@ class Enemy(Object):
# 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
turnRate = 2 if self.level.levelId == 999 else 8 # Faster turn rate for survival
# Create new enemy of specified type
spawned = Enemy(
@@ -173,9 +173,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_pattern={"type": behavior},
turn_rate=turn_rate,
attackRange=1, # Default range for spawned enemies
attackPattern={"type": behavior},
turnRate=turnRate,
)
# Add to level's enemies
@@ -273,7 +273,7 @@ class Enemy(Object):
droppedItem = PowerUp(
dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
)
self.level.bouncing_items.append(droppedItem)
self.level.bouncingItems.append(droppedItem)
# Update stats
self.level.player.stats.update_stat("Enemies killed", 1)
+28 -26
View File
@@ -15,7 +15,9 @@ def get_levels_base_path():
"""
if hasattr(sys, "_MEIPASS"):
# Running as PyInstaller executable
return sys._MEIPASS
# Use parent directory (where executable is) instead of _internal
# This allows users to add new level packs without recompiling
return os.path.dirname(sys.executable)
else:
# Running as script
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -28,9 +30,9 @@ def get_available_games():
list: List of game directory names
"""
try:
base_path = get_levels_base_path()
levels_path = os.path.join(base_path, "levels")
return [d for d in os.listdir(levels_path) if isdir(join(levels_path, d)) and not d.endswith(".md")]
basePath = get_levels_base_path()
levelsPath = os.path.join(basePath, "levels")
return [d for d in os.listdir(levelsPath) if isdir(join(levelsPath, d)) and not d.endswith(".md")]
except FileNotFoundError:
return []
@@ -57,57 +59,57 @@ def select_game(sounds):
Returns:
str: Selected game directory name or None if cancelled
"""
available_games = get_available_games()
availableGames = get_available_games()
if not available_games:
if not availableGames:
speak("No games found in levels directory!")
return None
# Convert directory names to display names (replace underscores with spaces)
menu_options = [game.replace("_", " ") for game in available_games]
menuOptions = [game.replace("_", " ") for game in availableGames]
choice = selection_menu(sounds, *menu_options)
choice = selection_menu(sounds, *menuOptions)
if choice is None:
return None
# Convert display name back to directory name if needed
game_dir = choice.replace(" ", "_")
if game_dir not in available_games:
game_dir = choice # Use original if conversion doesn't match
gameDir = choice.replace(" ", "_")
if gameDir not in availableGames:
gameDir = choice # Use original if conversion doesn't match
return game_dir
return gameDir
def get_level_path(game_dir, level_num):
def get_level_path(gameDir, levelNum):
"""Get full path to level JSON file.
Args:
game_dir (str): Game directory name
level_num (int): Level number
gameDir (str): Game directory name
levelNum (int): Level number
Returns:
str: Full path to level JSON file
"""
if game_dir is None:
raise ValueError("game_dir cannot be None")
if gameDir is None:
raise ValueError("gameDir cannot be None")
base_path = get_levels_base_path()
level_path = os.path.join(base_path, "levels", game_dir, f"{level_num}.json")
return level_path
basePath = get_levels_base_path()
levelPath = os.path.join(basePath, "levels", gameDir, f"{levelNum}.json")
return levelPath
def get_game_dir_path(game_dir):
def get_game_dir_path(gameDir):
"""Get full path to game directory for end.ogg and other game files.
Args:
game_dir (str): Game directory name
gameDir (str): Game directory name
Returns:
str: Full path to game directory
"""
if game_dir is None:
raise ValueError("game_dir cannot be None")
if gameDir is None:
raise ValueError("gameDir cannot be None")
base_path = get_levels_base_path()
return os.path.join(base_path, "levels", game_dir)
basePath = get_levels_base_path()
return os.path.join(basePath, "levels", gameDir)
+3 -3
View File
@@ -8,7 +8,7 @@ from src.object import Object
class GraspingHands(Object):
"""A hazard where the ground crumbles beneath the player as undead hands reach up."""
def __init__(self, xRange, y, sounds, delay=1000, crumble_speed=0.065):
def __init__(self, xRange, y, sounds, delay=1000, crumbleSpeed=0.065):
super().__init__(
xRange,
y,
@@ -19,7 +19,7 @@ class GraspingHands(Object):
)
self.sounds = sounds
self.delay = delay # Delay in milliseconds before ground starts crumbling
self.crumble_speed = crumble_speed # How fast the crumbling catches up (tiles per frame)
self.crumbleSpeed = crumbleSpeed # How fast the crumbling catches up (tiles per frame)
# Sound prefix for different sound effects (can be overridden)
self.soundPrefix = "grasping_hands"
@@ -95,7 +95,7 @@ class GraspingHands(Object):
# If triggered and delay has passed, start crumbling
if self.isTriggered and currentTime - self.triggerTime >= self.delay:
# Update crumble position based on direction
self.crumblePosition += self.crumble_speed * self.crumbleDirection
self.crumblePosition += self.crumbleSpeed * self.crumbleDirection
# Manage the looping positional audio for the crumbling ground
if self.crumbleChannel is None or not self.crumbleChannel.get_busy():
+13 -13
View File
@@ -45,28 +45,28 @@ class ItemProperties:
}
@staticmethod
def get_sound_name(item_type):
def get_sound_name(itemType):
"""Convert enum to sound/asset name"""
return ItemProperties.ALL_ITEMS.get(item_type)
return ItemProperties.ALL_ITEMS.get(itemType)
@staticmethod
def get_random_item(survival_mode=False):
def get_random_item(survivalMode=False):
"""Get a random item from eligible items"""
if survival_mode:
item_type = random.choice(list(ItemProperties.SURVIVAL_MODE_ELIGIBLE.keys()))
if survivalMode:
itemType = random.choice(list(ItemProperties.SURVIVAL_MODE_ELIGIBLE.keys()))
else:
item_type = random.choice(list(ItemProperties.STORY_MODE_ELIGIBLE.keys()))
return ItemProperties.get_sound_name(item_type)
itemType = random.choice(list(ItemProperties.STORY_MODE_ELIGIBLE.keys()))
return ItemProperties.get_sound_name(itemType)
@staticmethod
def is_valid_item(item_name):
def is_valid_item(itemName):
"""Check if an item name is valid"""
return item_name in [v for v in ItemProperties.ALL_ITEMS.values()]
return itemName in [v for v in ItemProperties.ALL_ITEMS.values()]
@staticmethod
def get_item_type(item_name):
def get_item_type(itemName):
"""Get ItemType enum from string name"""
for item_type, name in ItemProperties.ALL_ITEMS.items():
if name == item_name:
return item_type
for itemType, name in ItemProperties.ALL_ITEMS.items():
if name == itemName:
return itemType
return None
+58 -58
View File
@@ -22,12 +22,12 @@ class Level:
self.levelPackName = levelPackName
self.objects = []
self.enemies = []
self.bouncing_items = []
self.bouncingItems = []
self.projectiles = [] # Track active projectiles
self.player = player
self.lastWarningTime = 0
self.warningInterval = int(self.sounds["edge"].get_length() * 1000) # Convert seconds to milliseconds
self.weapon_hit_channel = None
self.weaponHitChannel = None
self.leftBoundary = levelData["boundaries"]["left"]
self.rightBoundary = levelData["boundaries"]["right"]
@@ -137,7 +137,7 @@ class Level:
obj["y"],
self.sounds,
delay=obj.get("delay", 1000),
crumble_speed=obj.get("crumble_speed", 0.03),
crumbleSpeed=obj.get("crumble_speed", 0.03),
)
# Apply sound overrides if specified
if "sound_overrides" in obj:
@@ -146,25 +146,25 @@ class Level:
# Check if this is a grave
elif obj.get("type") == "grave":
# Handle item spawning with chance (for survival mode)
grave_item = None
graveItem = None
if "item_spawn_chance" in obj:
# Survival mode: spawn item based on chance (random items already resolved)
spawn_chance = obj.get("item_spawn_chance", 0)
if random.randint(1, 100) <= spawn_chance:
grave_item = obj.get("item")
spawnChance = obj.get("item_spawn_chance", 0)
if random.randint(1, 100) <= spawnChance:
graveItem = obj.get("item")
else:
# Story mode: use item as specified
grave_item = obj.get("item", None)
if grave_item == "random":
graveItem = obj.get("item", None)
if graveItem == "random":
# Check if we're in survival mode (level_id 999)
is_survival_mode = self.levelData.get('level_id') == 999
grave_item = ItemProperties.get_random_item(survival_mode=is_survival_mode)
isSurvivalMode = self.levelData.get('level_id') == 999
graveItem = ItemProperties.get_random_item(survivalMode=isSurvivalMode)
grave = GraveObject(
xPos[0],
obj["y"],
self.sounds,
item=grave_item,
item=graveItem,
zombieSpawnChance=obj.get("zombie_spawn_chance", 0),
)
# Apply sound overrides if specified
@@ -232,18 +232,18 @@ class Level:
self, # Pass level reference
health=obj.get("health", 5),
damage=obj.get("damage", 1),
attack_range=obj.get("attack_range", 1),
movement_range=obj.get("movement_range", 5),
attack_pattern=obj.get("attack_pattern", {"type": "patrol"}),
can_spawn=obj.get("can_spawn", False),
spawn_type=obj.get("spawn_type", "zombie"),
spawn_cooldown=obj.get("spawn_cooldown", 2000),
spawn_chance=obj.get("spawn_chance", 25),
spawn_distance=obj.get("spawn_distance", 5),
has_vulnerability=obj.get("has_vulnerability", False),
is_vulnerable=obj.get("is_vulnerable", False),
vulnerability_duration=obj.get("vulnerability_duration", 1000),
invulnerability_duration=obj.get("invulnerability_duration", 5000),
attackRange=obj.get("attack_range", 1),
movementRange=obj.get("movement_range", 5),
attackPattern=obj.get("attack_pattern", {"type": "patrol"}),
canSpawn=obj.get("can_spawn", False),
spawnType=obj.get("spawn_type", "zombie"),
spawnCooldown=obj.get("spawn_cooldown", 2000),
spawnChance=obj.get("spawn_chance", 25),
spawnDistance=obj.get("spawn_distance", 5),
hasVulnerability=obj.get("has_vulnerability", False),
isVulnerable=obj.get("is_vulnerable", False),
vulnerabilityDuration=obj.get("vulnerability_duration", 1000),
invulnerabilityDuration=obj.get("invulnerability_duration", 5000),
)
self.enemies.append(enemy)
else:
@@ -286,7 +286,7 @@ class Level:
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
turnRate = 2 if self.levelId == 999 else 8 # Faster turn rate for survival
zombie = Enemy(
[obj.xPos, obj.xPos],
@@ -296,9 +296,9 @@ class Level:
self, # Pass the level reference
health=3,
damage=10,
attack_range=1,
attack_pattern={"type": behavior},
turn_rate=turn_rate,
attackRange=1,
attackPattern={"type": behavior},
turnRate=turnRate,
)
self.enemies.append(zombie)
speak("A zombie emerges from the grave!")
@@ -354,9 +354,9 @@ class Level:
self.objects = [obj for obj in self.objects if obj.isActive]
# Update bouncing items
for item in self.bouncing_items[:]: # Copy list to allow removal
for item in self.bouncingItems[:]: # Copy list to allow removal
if not item.update(currentTime, self.player.xPos):
self.bouncing_items.remove(item)
self.bouncingItems.remove(item)
if not item.isActive:
speak(f"{item.soundName} got away!")
continue
@@ -367,7 +367,7 @@ class Level:
item.apply_effect(self.player, self)
self.levelScore += 1000 # All items collected points awarded
item.isActive = False
self.bouncing_items.remove(item)
self.bouncingItems.remove(item)
def handle_combat(self, currentTime):
"""Handle combat interactions between player and enemies"""
@@ -394,7 +394,7 @@ class Level:
): # Must be jumping to hit floating coffins
if obj.hit(self.player.xPos):
self.bouncing_items.append(obj.dropped_item)
self.bouncingItems.append(obj.droppedItem)
def spawn_spider(self, xPos, yPos):
"""Spawn a spider at the given position"""
@@ -406,8 +406,8 @@ class Level:
self,
health=8,
damage=8,
attack_range=1,
speed_multiplier=2.0,
attackRange=1,
speedMultiplier=2.0,
)
self.enemies.append(spider)
@@ -417,20 +417,20 @@ class Level:
if enemyStats:
health = enemyStats.get("health", 8)
damage = enemyStats.get("damage", 2)
speed_multiplier = enemyStats.get("speed_multiplier", 1.0)
attack_range = enemyStats.get("attack_range", 1)
speedMultiplier = enemyStats.get("speed_multiplier", 1.0)
attackRange = enemyStats.get("attack_range", 1)
else:
# Default stats for common enemy types (backward compatibility)
if enemyType == "spider":
health, damage, speed_multiplier = 8, 8, 2.0
health, damage, speedMultiplier = 8, 8, 2.0
elif enemyType == "elf":
health, damage, speed_multiplier = 2, 1, 1.0
health, damage, speedMultiplier = 2, 1, 1.0
elif enemyType == "witch":
health, damage, speed_multiplier = 6, 2, 1.0
health, damage, speedMultiplier = 6, 2, 1.0
else:
# For any other enemy type, use reasonable defaults
health, damage, speed_multiplier = 6, 2, 1.0
attack_range = 1
health, damage, speedMultiplier = 6, 2, 1.0
attackRange = 1
enemy = Enemy(
[xPos - 5, xPos + 5], # Give enemy a patrol range
@@ -440,9 +440,9 @@ class Level:
self,
health=health,
damage=damage,
attack_range=attack_range,
speed_multiplier=speed_multiplier,
attack_pattern={"type": "hunter"}, # Hunt like original spiders
attackRange=attackRange,
speedMultiplier=speedMultiplier,
attackPattern={"type": "hunter"}, # Hunt like original spiders
)
self.enemies.append(enemy)
@@ -550,16 +550,16 @@ class Level:
# Handle graves and other hazards
if obj.isHazard and not self.player.isJumping:
if isinstance(obj, GraveObject):
can_collect = obj.collect_grave_item(self.player) and not self.player.diedThisFrame
can_fill = obj.can_fill_grave(self.player)
canCollect = obj.collect_grave_item(self.player) and not self.player.diedThisFrame
canFill = obj.can_fill_grave(self.player)
if can_collect:
if canCollect:
# Successfully collected item while ducking with shovel
# Check for item sound override
item_sound = obj.graveItem
itemSound = obj.graveItem
if hasattr(obj, 'itemSoundOverride'):
item_sound = obj.itemSoundOverride
play_sound(self.sounds[f"get_{item_sound}"])
itemSound = obj.itemSoundOverride
play_sound(self.sounds[f"get_{itemSound}"])
play_sound(self.sounds.get("fill_in_grave", "shovel_dig")) # Also play fill sound
self.player.stats.update_stat("Items collected", 1)
# Create PowerUp to handle the item effect
@@ -575,7 +575,7 @@ class Level:
obj.channel = None
obj.isActive = False # Mark the grave as inactive after collection
continue
elif can_fill and obj.fill_grave(self.player):
elif canFill and obj.fill_grave(self.player):
# Successfully filled empty grave with shovel
play_sound(
self.sounds.get("fill_in_grave", "shovel_dig")
@@ -643,12 +643,12 @@ class Level:
def throw_projectile(self):
"""Have player throw a projectile"""
proj_info = self.player.throw_projectile()
if proj_info is None:
projInfo = self.player.throw_projectile()
if projInfo is None:
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(projInfo["type"], projInfo["start_x"], projInfo["direction"]))
# Play throw sound
play_sound(self.sounds["throw_jack_o_lantern"])
@@ -663,11 +663,11 @@ class Level:
# Create projectile with weapon-specific properties
# Use a generic projectile type for weapon projectiles
projectile_type = f"weapon_{weapon.name}"
start_x = player.xPos + (direction * 1) # Start 1 tile away from player
projectileType = f"weapon_{weapon.name}"
startX = player.xPos + (direction * 1) # Start 1 tile away from player
# Create projectile with custom speed and damage from weapon
projectile = Projectile(projectile_type, start_x, direction)
projectile = Projectile(projectileType, startX, direction)
# Override projectile properties with weapon stats
projectile.damage = weapon.damage
@@ -822,7 +822,7 @@ class Level:
obj.itemSoundOverride = soundOverrides["item"]
# Handle item sound overrides for coffins
if hasattr(obj, 'specified_item') and obj.specified_item and obj.specified_item != "random" and "item" in soundOverrides:
if hasattr(obj, 'specifiedItem') and obj.specifiedItem and obj.specifiedItem != "random" and "item" in soundOverrides:
# Store the override for later use when coffin is broken
if not hasattr(obj, 'itemSoundOverride'):
obj.itemSoundOverride = soundOverrides["item"]
+20 -20
View File
@@ -45,7 +45,7 @@ class Player:
self.collectedItems = []
self._coins = 0 # Regular bone dust for extra lives
self._saveBoneDust = 0 # Separate bone dust counter for saves
self._jack_o_lantern_count = 0
self._jackOLanternCount = 0
self.shinBoneCount = 0
# Combat related attributes
@@ -115,7 +115,7 @@ class Player:
# Check invincibility status
if self.isInvincible:
remaining_time = (
remainingTime = (
self.invincibilityStartTime + self.invincibilityDuration - currentTime
) / 1000 # Convert to seconds
@@ -123,10 +123,10 @@ class Player:
if not hasattr(self, "_last_countdown"):
self._last_countdown = 4 # Start counting from 4 to catch 3,2,1
current_second = int(remaining_time)
if current_second < self._last_countdown and current_second <= 3 and current_second > 0:
currentSecond = int(remainingTime)
if currentSecond < self._last_countdown and currentSecond <= 3 and currentSecond > 0:
play_sound(self.sounds["end_of_invincibility_warning"])
self._last_countdown = current_second
self._last_countdown = currentSecond
# Check if invincibility has expired
if currentTime - self.invincibilityStartTime >= self.invincibilityDuration:
@@ -147,11 +147,11 @@ class Player:
def get_jack_o_lanterns(self):
"""Get number of jack o'lanterns"""
return self._jack_o_lantern_count
return self._jackOLanternCount
def add_jack_o_lantern(self):
"""Add a jack o'lantern"""
self._jack_o_lantern_count += 1
self._jackOLanternCount += 1
def add_guts(self):
"""Apply guts, increase max_health by 2 if less than 20 else restore health"""
@@ -165,7 +165,7 @@ class Player:
if self.get_jack_o_lanterns() <= 0:
return None
self._jack_o_lantern_count -= 1
self._jackOLanternCount -= 1
return {"type": "jack_o_lantern", "start_x": self.xPos, "direction": 1 if self.facingRight else -1}
def get_step_distance(self):
@@ -220,19 +220,19 @@ class Player:
def set_health(self, value):
"""Set health and handle death if needed."""
old_health = self._health
oldHealth = self._health
# Oops, allow healing while invincible.
if self.isInvincible and value < old_health:
if self.isInvincible and value < oldHealth:
return
# Check for god mode (prevents all damage)
if hasattr(self, '_godMode') and self._godMode and value < old_health:
if hasattr(self, '_godMode') and self._godMode and value < oldHealth:
return
self._health = max(0, value) # Health can't go below 0
if self._health == 0 and old_health > 0:
if self._health == 0 and oldHealth > 0:
self._lives -= 1
# Mark that player died this frame to prevent revival
self.diedThisFrame = True
@@ -379,7 +379,7 @@ class Player:
elif ammoType == "hand_of_glory":
return self.collectedItems.count("hand_of_glory") >= cost
elif ammoType == "jack_o_lantern":
return self._jack_o_lantern_count >= cost
return self._jackOLanternCount >= cost
else:
# Check for any other item type in collectedItems
return self.collectedItems.count(ammoType) >= cost
@@ -398,7 +398,7 @@ class Player:
for _ in range(min(cost, self.collectedItems.count("hand_of_glory"))):
self.collectedItems.remove("hand_of_glory")
elif ammoType == "jack_o_lantern":
self._jack_o_lantern_count = max(0, self._jack_o_lantern_count - cost)
self._jackOLanternCount = max(0, self._jackOLanternCount - cost)
else:
# Handle any other item type
for _ in range(min(cost, self.collectedItems.count(ammoType))):
@@ -409,18 +409,18 @@ class Player:
# Check for themed mappings in weapon overrides
if hasattr(self, 'weaponOverrides') and self.weaponOverrides:
# Define themed mappings (this could be extended or made configurable)
themed_mappings = {
themedMappings = {
"shin_bone": "candy_cane", # Christmas theme example
"guts": "reindeer_guts"
}
# Check if there's a themed equivalent and if the override exists
if ammoType in themed_mappings:
themed_name = themed_mappings[ammoType]
if ammoType in themedMappings:
themedName = themedMappings[ammoType]
# If the themed item exists in overrides, use the themed name
for override_key, override_data in self.weaponOverrides.items():
if isinstance(override_data, dict) and themed_name in str(override_data):
return themed_name.replace("_", " ")
for overrideKey, overrideData in self.weaponOverrides.items():
if isinstance(overrideData, dict) and themedName in str(overrideData):
return themedName.replace("_", " ")
# Return default name
return ammoType.replace("_", " ")
+29 -29
View File
@@ -7,43 +7,43 @@ from src.weapon import Weapon
class PowerUp(Object):
def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100):
super().__init__(x, y, item_type, isStatic=False, isCollectible=True, isHazard=False)
def __init__(self, x, y, itemType, sounds, direction, leftBoundary=1, rightBoundary=100):
super().__init__(x, y, itemType, isStatic=False, isCollectible=True, isHazard=False)
self.sounds = sounds
self.direction = direction
self.speed = 0.049 # Base movement speed
self.item_type = item_type
self.itemType = itemType
self.channel = None
self._currentX = x # Initialize the current x position
self.left_boundary = left_boundary
self.right_boundary = right_boundary
self.leftBoundary = leftBoundary
self.rightBoundary = rightBoundary
def update(self, current_time, player_pos):
def update(self, currentTime, playerPos):
"""Update item position"""
if not self.isActive:
return False
# Update position
new_x = self._currentX + self.direction * self.speed
newX = self._currentX + self.direction * self.speed
# Check boundaries and bounce if needed
if new_x < self.left_boundary:
self._currentX = self.left_boundary
if newX < self.leftBoundary:
self._currentX = self.leftBoundary
self.direction = 1 # Start moving right
elif new_x > self.right_boundary:
self._currentX = self.right_boundary
elif newX > self.rightBoundary:
self._currentX = self.rightBoundary
self.direction = -1 # Start moving left
else:
self._currentX = new_x
self._currentX = newX
# Update positional audio
if self.channel is None or not self.channel.get_busy():
self.channel = obj_play(self.sounds, "item_bounce", player_pos, self._currentX)
self.channel = obj_play(self.sounds, "item_bounce", playerPos, self._currentX)
else:
self.channel = obj_update(self.channel, player_pos, self._currentX)
self.channel = obj_update(self.channel, playerPos, self._currentX)
# Check if item is too far from player (12 tiles)
if abs(self._currentX - player_pos) > 12:
if abs(self._currentX - playerPos) > 12:
self.isActive = False
if self.channel:
self.channel.stop()
@@ -55,22 +55,22 @@ class PowerUp(Object):
def apply_effect(self, player, level=None):
"""Apply the item's effect when collected"""
# Map themed items to their original equivalents for game logic
original_item_type = self._get_original_item_type(self.item_type)
originalItemType = self._get_original_item_type(self.itemType)
if original_item_type == "hand_of_glory":
if originalItemType == "hand_of_glory":
player.start_invincibility()
self.check_for_custom_weapons(player)
elif original_item_type == "cauldron":
elif originalItemType == "cauldron":
player.restore_health()
elif original_item_type == "guts":
elif originalItemType == "guts":
player.add_guts()
player.collectedItems.append(original_item_type)
player.collectedItems.append(originalItemType)
self.check_for_nunchucks(player)
self.check_for_custom_weapons(player)
elif original_item_type == "jack_o_lantern":
elif originalItemType == "jack_o_lantern":
player.add_jack_o_lantern()
self.check_for_custom_weapons(player)
elif original_item_type == "extra_life":
elif originalItemType == "extra_life":
# Don't give extra lives in survival mode
if level and level.levelId == 999:
# In survival mode, give bonus score instead
@@ -79,7 +79,7 @@ class PowerUp(Object):
play_sound(self.sounds.get("survivor_bonus", "get_extra_life")) # Use survivor_bonus sound if available
else:
player.extra_life()
elif original_item_type == "shin_bone": # Add shin bone handling
elif originalItemType == "shin_bone": # Add shin bone handling
player.shinBoneCount += 1
player._coins += 5
player.add_save_bone_dust(5) # Add to save bone dust counter too
@@ -106,11 +106,11 @@ class PowerUp(Object):
self.check_for_nunchucks(player)
self.check_for_custom_weapons(player)
elif original_item_type == "witch_broom":
elif originalItemType == "witch_broom":
broomWeapon = Weapon.create_witch_broom()
player.add_weapon(broomWeapon)
player.equip_weapon(broomWeapon)
elif original_item_type == "spiderweb":
elif originalItemType == "spiderweb":
# Bounce player back (happens even if invincible)
player.xPos -= 3 if player.xPos > self.xPos else -3
@@ -193,7 +193,7 @@ class PowerUp(Object):
canCraft = False
break
elif itemType == "jack_o_lantern":
if player._jack_o_lantern_count < neededCount:
if player._jackOLanternCount < neededCount:
canCraft = False
break
else:
@@ -225,10 +225,10 @@ class PowerUp(Object):
player.stats.update_stat("Items collected", 1)
def _get_original_item_type(self, item_type):
def _get_original_item_type(self, itemType):
"""Map themed item names to their original equivalents for game logic."""
# Define themed equivalents that should behave like original items
themed_mappings = {
themedMappings = {
# Christmas theme
"candy_cane": "shin_bone",
"reindeer_guts": "guts",
@@ -237,4 +237,4 @@ class PowerUp(Object):
# "frozen_heart": "guts",
}
return themed_mappings.get(item_type, item_type)
return themedMappings.get(itemType, itemType)
+5 -5
View File
@@ -2,15 +2,15 @@
class Projectile:
def __init__(self, projectile_type, start_x, direction):
self.type = projectile_type
self.x = start_x
def __init__(self, projectileType, startX, direction):
self.type = projectileType
self.x = startX
self.direction = direction
self.speed = 0.2 # Projectiles move faster than player
self.isActive = True
self.damage = 5 # All projectiles do same damage for now
self.range = 12 # Maximum travel distance in tiles
self.start_x = start_x
self.startX = startX
def update(self):
"""Update projectile position and check if it should still exist"""
@@ -20,7 +20,7 @@ class Projectile:
self.x += self.direction * self.speed
# Check if projectile has gone too far
if abs(self.x - self.start_x) > self.range:
if abs(self.x - self.startX) > self.range:
self.isActive = False
return False
+76 -76
View File
@@ -11,32 +11,32 @@ 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"
self.save_dir.mkdir(parents=True, exist_ok=True)
self.max_saves = 10
configHome = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
self.saveDir = Path(configHome) / "storm-games" / "wicked-quest"
self.saveDir.mkdir(parents=True, exist_ok=True)
self.maxSaves = 10
def create_save(self, player, current_level, game_start_time, current_game, bypass_cost=False):
def create_save(self, player, currentLevel, gameStartTime, currentGame, bypassCost=False):
"""Create a save file with current game state"""
if not bypass_cost:
if not bypassCost:
if not player.can_save():
return False, "Not enough bone dust to save (need 200)"
# Validate required parameters
if current_game is None:
if currentGame is None:
return False, "No game selected to save"
if current_level is None:
if currentLevel is None:
return False, "No current level to save"
# Spend the bone dust (only if not bypassing cost)
if not bypass_cost:
if not bypassCost:
if not player.spend_save_bone_dust(200):
return False, "Failed to spend bone dust"
# Create save data
save_data = {
"player_state": {
saveData = {
"playerState": {
"xPos": player.xPos,
"yPos": player.yPos,
"health": player._health,
@@ -44,7 +44,7 @@ class SaveManager:
"lives": player._lives,
"coins": player._coins,
"saveBoneDust": player._saveBoneDust,
"jackOLanternCount": player._jack_o_lantern_count,
"jackOLanternCount": player._jackOLanternCount,
"shinBoneCount": player.shinBoneCount,
"inventory": player.inventory,
"collectedItems": player.collectedItems,
@@ -56,9 +56,9 @@ class SaveManager:
"scoreboard": self._serialize_scoreboard(player.scoreboard),
},
"game_state": {
"currentLevel": current_level,
"currentGame": current_game,
"gameStartTime": game_start_time,
"currentLevel": currentLevel,
"currentGame": currentGame,
"gameStartTime": gameStartTime,
"saveTime": datetime.now(),
},
"version": "1.0",
@@ -67,21 +67,21 @@ class SaveManager:
# Generate filename with timestamp
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"save_{timestamp}.pickle"
filepath = self.save_dir / filename
filepath = self.saveDir / filename
try:
# Write to temporary file first, then rename for atomic operation
temp_filepath = filepath.with_suffix(".tmp")
tempFilepath = filepath.with_suffix(".tmp")
with open(temp_filepath, "wb") as f:
pickle.dump(save_data, f)
with open(tempFilepath, "wb") as f:
pickle.dump(saveData, 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)
tempFilepath.rename(filepath)
# Clean up old saves if we exceed max_saves
# Clean up old saves if we exceed maxSaves
self._cleanup_old_saves()
return True, f"Game saved to {filename}"
@@ -205,38 +205,38 @@ class SaveManager:
scoreboard.highScores = scoreboard_data["highScores"]
return scoreboard
def get_save_files(self):
def get_saveFiles(self):
"""Get list of save files with metadata"""
save_files = []
pattern = str(self.save_dir / "save_*.pickle")
saveFiles = []
pattern = str(self.saveDir / "save_*.pickle")
for filepath in glob.glob(pattern):
try:
with open(filepath, "rb") as f:
save_data = pickle.load(f)
saveData = pickle.load(f)
# Validate save data structure
if not self._validate_save_data(save_data):
if not self._validate_saveData(saveData):
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"]
saveTime = saveData["game_state"]["saveTime"]
level = saveData["game_state"]["currentLevel"]
gameName = saveData["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}"
formattedTime = saveTime.strftime("%B %d %I:%M%p")
displayName = f"{formattedTime} {gameName} Level {level}"
save_files.append(
saveFiles.append(
{
"filepath": filepath,
"display_name": display_name,
"save_time": save_time,
"displayName": displayName,
"saveTime": saveTime,
"level": level,
"game_name": game_name,
"save_data": save_data,
"gameName": gameName,
"saveData": saveData,
}
)
except (pickle.PickleError, EOFError, OSError) as e:
@@ -253,94 +253,94 @@ class SaveManager:
continue
# Sort by save time (newest first)
save_files.sort(key=lambda x: x["save_time"], reverse=True)
return save_files
saveFiles.sort(key=lambda x: x["saveTime"], reverse=True)
return saveFiles
def load_save(self, filepath):
"""Load game state from save file"""
try:
with open(filepath, "rb") as f:
save_data = pickle.load(f)
return True, save_data
saveData = pickle.load(f)
return True, saveData
except Exception as e:
return False, f"Failed to load save: {str(e)}"
def restore_player_state(self, player, save_data):
def restore_playerState(self, player, saveData):
"""Restore player state from save data"""
player_state = save_data["player_state"]
playerState = saveData["playerState"]
# 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 = playerState["xPos"]
player.yPos = playerState["yPos"]
player._health = playerState["health"]
player._maxHealth = playerState["maxHealth"]
player._lives = playerState["lives"]
player._coins = playerState["coins"]
player._saveBoneDust = playerState["saveBoneDust"]
player._jackOLanternCount = playerState["jackOLanternCount"]
player.shinBoneCount = playerState["shinBoneCount"]
player.inventory = playerState["inventory"]
player.collectedItems = playerState["collectedItems"]
# Restore weapons
player.weapons = self._deserialize_weapons(player_state["weapons"])
player.weapons = self._deserialize_weapons(playerState["weapons"])
# Restore custom weapon tracking data (for backward compatibility, use get with defaults)
player.craftedCustomWeapons = set(player_state.get("craftedCustomWeapons", []))
player.customWeapons = player_state.get("customWeapons", [])
player.craftedCustomWeapons = set(playerState.get("craftedCustomWeapons", []))
player.customWeapons = playerState.get("customWeapons", [])
# Restore current weapon
current_weapon_name = player_state.get("currentWeaponName")
if current_weapon_name:
currentWeaponName = playerState.get("currentWeaponName")
if currentWeaponName:
for weapon in player.weapons:
if weapon.name == current_weapon_name:
if weapon.name == currentWeaponName:
player.currentWeapon = weapon
break
# Restore stats
if "stats" in player_state:
player.stats = self._deserialize_stats(player_state["stats"])
if "stats" in playerState:
player.stats = self._deserialize_stats(playerState["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 playerState:
player.scoreboard = self._deserialize_scoreboard(playerState["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()
"""Remove old save files if we exceed maxSaves"""
saveFiles = self.get_saveFiles()
if len(save_files) > self.max_saves:
if len(saveFiles) > self.maxSaves:
# Remove oldest saves
for save_file in save_files[self.max_saves:]:
for save_file in saveFiles[self.maxSaves:]:
try:
os.remove(save_file["filepath"])
except Exception as e:
print(f"Error removing old save {save_file['filepath']}: {e}")
def _validate_save_data(self, save_data):
def _validate_saveData(self, saveData):
"""Validate that save data has required structure"""
try:
# Check for required top-level keys
required_keys = ["player_state", "game_state", "version"]
if not all(key in save_data for key in required_keys):
requiredKeys = ["playerState", "game_state", "version"]
if not all(key in saveData for key in requiredKeys):
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):
# Check playerState structure
playerRequired = ["xPos", "yPos", "health", "maxHealth", "lives", "coins", "saveBoneDust"]
if not all(key in saveData["playerState"] for key in playerRequired):
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):
gameRequired = ["currentLevel", "currentGame", "gameStartTime", "saveTime"]
if not all(key in saveData["game_state"] for key in gameRequired):
return False
return True
@@ -349,4 +349,4 @@ class SaveManager:
def has_saves(self):
"""Check if any save files exist"""
return len(self.get_save_files()) > 0
return len(self.get_saveFiles()) > 0
+16 -16
View File
@@ -26,10 +26,10 @@ class SurvivalGenerator:
self.weaponOverrides = {}
self.customWeapons = [] # Custom weapons from level pack
self.availableItems = set() # Dynamically discovered items from containers
self.loadLevelData()
self.parseTemplates()
self.load_level_data()
self.parse_templates()
def loadLevelData(self):
def load_level_data(self):
"""Load all level JSON files from the game pack."""
levelFiles = []
packPath = os.path.join(get_levels_base_path(), "levels", self.gamePack)
@@ -49,7 +49,7 @@ class SurvivalGenerator:
levelNum = int(levelFile.split(".")[0])
self.levelData[levelNum] = json.load(f)
def parseTemplates(self):
def parse_templates(self):
"""Parse all level data to extract object templates by type."""
for levelNum, data in self.levelData.items():
# Store ambience and footstep sounds (remove duplicates)
@@ -74,14 +74,14 @@ class SurvivalGenerator:
# Discover items from containers (graves and coffins)
if obj.get("type") in ["grave", "coffin"] and "item" in obj:
item_name = obj["item"]
itemName = obj["item"]
# Check for sound override (themed equivalent)
if "sound_overrides" in obj and "item" in obj["sound_overrides"]:
item_name = obj["sound_overrides"]["item"]
itemName = obj["sound_overrides"]["item"]
# Exclude special items that should remain rare/exclusive
if item_name and item_name != "random" and item_name not in ["extra_life"]:
self.availableItems.add(item_name)
if itemName and itemName != "random" and itemName not in ["extra_life"]:
self.availableItems.add(itemName)
# Categorize objects
if "enemy_type" in obj:
@@ -154,16 +154,16 @@ class SurvivalGenerator:
# Guarantee at least one coffin per wave if coffins exist in templates
coffinTemplates = [obj for obj in self.objectTemplates if obj.get("type") == "coffin"]
coffin_placed = False
coffinPlaced = False
if coffinTemplates:
# Place guaranteed coffin in the first quarter of the level
coffin_x = random.randint(startBufferZone + 10, (segmentLength - endBufferZone) // 4)
coffin_template = random.choice(coffinTemplates)
coffin_obj = copy.deepcopy(coffin_template)
coffin_obj["item"] = "random" # Override any specified item
coffin_obj["x"] = coffin_x
levelData["objects"].append(coffin_obj)
coffin_placed = True
coffinX = random.randint(startBufferZone + 10, (segmentLength - endBufferZone) // 4)
coffinTemplate = random.choice(coffinTemplates)
coffinObj = copy.deepcopy(coffinTemplate)
coffinObj["item"] = "random" # Override any specified item
coffinObj["x"] = coffinX
levelData["objects"].append(coffinObj)
coffinPlaced = True
while currentX < segmentLength - endBufferZone:
# Determine what to place based on probability
+101 -117
View File
@@ -64,35 +64,35 @@ class WickedQuest:
return None
# Find active enemies
active_enemies = []
activeEnemies = []
for enemy in self.currentLevel.enemies:
if enemy.isActive:
distance = abs(enemy.xPos - self.player.xPos)
direction = "right" if enemy.xPos > self.player.xPos else "left"
active_enemies.append((enemy, distance, direction))
activeEnemies.append((enemy, distance, direction))
if not active_enemies:
if not activeEnemies:
return None
# Sort by distance and get closest
active_enemies.sort(key=lambda x: x[1])
enemy, distance, direction = active_enemies[0]
activeEnemies.sort(key=lambda x: x[1])
enemy, distance, direction = activeEnemies[0]
# Convert distance to natural language
if distance == 0:
return f"{enemy.enemyType} right on top of you"
elif distance <= 10:
distance_desc = "very close"
distanceDesc = "very close"
elif distance <= 30:
distance_desc = "close"
distanceDesc = "close"
elif distance <= 60:
distance_desc = "far"
distanceDesc = "far"
elif distance <= 100:
distance_desc = "very far"
distanceDesc = "very far"
else:
distance_desc = "extremely far"
distanceDesc = "extremely far"
return f"{enemy.enemyType} {distance_desc} to the {direction}"
return f"{enemy.enemyType} {distanceDesc} to the {direction}"
def process_console_command(self, command):
"""Process console commands and execute their effects."""
@@ -132,9 +132,9 @@ class WickedQuest:
enemyCount = len([enemy for enemy in self.currentLevel.enemies if enemy.isActive])
if enemyCount > 0:
speak(f"{enemyCount} enemies remaining on this level")
closest_enemy_info = self.get_closest_enemy_info()
if closest_enemy_info:
speak(f"Closest enemy: {closest_enemy_info}")
closestEnemyInfo = self.get_closest_enemy_info()
if closestEnemyInfo:
speak(f"Closest enemy: {closestEnemyInfo}")
else:
speak("No active enemies on this level")
else:
@@ -190,14 +190,14 @@ class WickedQuest:
elif command == "diemonsterdie":
# Give 100 jack o'lanterns
if self.player:
self.player._jack_o_lantern_count += 100
self.player._jackOLanternCount += 100
speak(f"100 jack o'lanterns granted. You now have {self.player.get_jack_o_lanterns()} jack o'lanterns")
if 'get_jack_o_lantern' in self.get_sounds():
play_sound(self.get_sounds()['get_jack_o_lantern'])
elif command == "murderland":
# Set jack o'lanterns to exactly 13 (the special number)
if self.player:
self.player._jack_o_lantern_count = 13
self.player._jackOLanternCount = 13
speak("13 jack o'lanterns, this power courses through my soul.")
if 'get_jack_o_lantern' in self.get_sounds():
play_sound(self.get_sounds()['get_jack_o_lantern'])
@@ -238,7 +238,7 @@ class WickedQuest:
self.currentLevel.levelId,
self.gameStartTime,
self.currentGame,
bypass_cost=True # Skip bone dust requirement
bypassCost=True # Skip bone dust requirement
)
if success:
@@ -273,7 +273,7 @@ class WickedQuest:
self.player._saveBoneDust = 999
# Max jack o'lanterns (set to 99 for practicality)
self.player._jack_o_lantern_count = 99
self.player._jackOLanternCount = 99
# Grant all weapons with level pack override support
weaponOverrides = getattr(self.currentLevel, 'weaponSoundOverrides', {}) if self.currentLevel else {}
@@ -438,16 +438,16 @@ class WickedQuest:
def load_game_menu(self):
"""Display load game menu with available saves using instruction_menu"""
save_files = self.saveManager.get_save_files()
saveFiles = self.saveManager.get_save_files()
if not save_files:
if not saveFiles:
messagebox("No save files found.")
return None
# Create menu options
options = []
for save_file in save_files:
options.append(save_file['display_name'])
for saveFile in saveFiles:
options.append(saveFile['display_name'])
options.append("Cancel")
@@ -458,9 +458,9 @@ class WickedQuest:
return None
else:
# Find the corresponding save file
for save_file in save_files:
if save_file['display_name'] == choice:
return save_file
for saveFile in saveFiles:
if saveFile['display_name'] == choice:
return saveFile
return None
def auto_save(self):
@@ -559,56 +559,56 @@ class WickedQuest:
elif keys[pygame.K_e]:
# Weapon and ammo status
if player.currentWeapon:
weapon_name = getattr(player.currentWeapon, 'displayName', player.currentWeapon.name.replace("_", " "))
status_message = f"Wielding {weapon_name}"
weaponName = getattr(player.currentWeapon, 'displayName', player.currentWeapon.name.replace("_", " "))
statusMessage = f"Wielding {weaponName}"
# Check if it's a projectile weapon - always show ammo for projectile weapons
weapon_type = getattr(player.currentWeapon, 'weaponType', 'melee')
if weapon_type == "projectile":
ammo_type = getattr(player.currentWeapon, 'ammoType', None)
if ammo_type:
ammo_count = 0
ammo_display_name = ammo_type.replace("_", " ") # Default fallback
weaponType = getattr(player.currentWeapon, 'weaponType', 'melee')
if weaponType == "projectile":
ammoType = getattr(player.currentWeapon, 'ammoType', None)
if ammoType:
ammoCount = 0
ammoDisplayName = ammoType.replace("_", " ") # Default fallback
# Get current ammo count based on ammo type
if ammo_type == "bone_dust":
ammo_count = player.get_coins()
ammo_display_name = "bone dust"
elif ammo_type == "shin_bone":
ammo_count = player.shinBoneCount
ammo_display_name = "shin bones"
elif ammo_type == "jack_o_lantern":
ammo_count = player._jack_o_lantern_count
ammo_display_name = "jack o'lanterns"
elif ammo_type == "guts":
ammo_count = player.collectedItems.count("guts")
ammo_display_name = "guts"
elif ammo_type == "hand_of_glory":
ammo_count = player.collectedItems.count("hand_of_glory")
ammo_display_name = "hands of glory"
if ammoType == "bone_dust":
ammoCount = player.get_coins()
ammoDisplayName = "bone dust"
elif ammoType == "shin_bone":
ammoCount = player.shinBoneCount
ammoDisplayName = "shin bones"
elif ammoType == "jack_o_lantern":
ammoCount = player._jackOLanternCount
ammoDisplayName = "jack o'lanterns"
elif ammoType == "guts":
ammoCount = player.collectedItems.count("guts")
ammoDisplayName = "guts"
elif ammoType == "hand_of_glory":
ammoCount = player.collectedItems.count("hand_of_glory")
ammoDisplayName = "hands of glory"
else:
# Check for any other item type in collectedItems
ammo_count = player.collectedItems.count(ammo_type)
ammoCount = player.collectedItems.count(ammoType)
status_message += f". {ammo_count} {ammo_display_name}"
statusMessage += f". {ammoCount} {ammoDisplayName}"
speak(status_message)
speak(statusMessage)
else:
speak("No weapon equipped")
elif keys[pygame.K_h]:
speak(f"{player.get_health()} health of {player.get_max_health()}")
elif keys[pygame.K_i]:
if self.currentLevel.levelId == 999:
base_info = f"Wave {self.survivalWave}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this wave so far. {player.get_lives()} lives remaining."
baseInfo = f"Wave {self.survivalWave}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this wave so far. {player.get_lives()} lives remaining."
else:
base_info = f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this level so far. {player.get_lives()} lives remaining."
baseInfo = f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this level so far. {player.get_lives()} lives remaining."
# Add closest enemy info
closest_enemy_info = self.get_closest_enemy_info()
if closest_enemy_info:
speak(f"{base_info} {closest_enemy_info}")
closestEnemyInfo = self.get_closest_enemy_info()
if closestEnemyInfo:
speak(f"{baseInfo} {closestEnemyInfo}")
else:
speak(base_info)
speak(baseInfo)
if keys[pygame.K_l]:
speak(f"{player.get_lives()} lives")
if keys[pygame.K_j]: # Check jack o'lanterns
@@ -898,39 +898,39 @@ class WickedQuest:
while True:
# Add load game option if saves exist
custom_options = []
customOptions = []
if self.saveManager.has_saves():
custom_options.append("load_game")
customOptions.append("load_game")
choice = game_menu(self.get_sounds(), None, *custom_options)
choice = game_menu(self.get_sounds(), None, *customOptions)
if choice == "exit":
exit_game()
elif choice == "load_game":
selected_save = self.load_game_menu()
if selected_save:
success, save_data = self.saveManager.load_save(selected_save['filepath'])
selectedSave = self.load_game_menu()
if selectedSave:
success, saveData = self.saveManager.load_save(selectedSave['filepath'])
if success:
# 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']
self.currentGame = saveData['game_state']['currentGame']
self.gameStartTime = saveData['game_state']['gameStartTime']
currentLevel = saveData['game_state']['currentLevel']
# Initialize pack-specific sound system
self.initialize_pack_sounds()
# Load the level
if self.load_level(current_level):
if self.load_level(currentLevel):
# Restore player state
self.saveManager.restore_player_state(self.player, save_data)
self.saveManager.restore_player_state(self.player, saveData)
# Re-apply weapon overrides after restoring player state to ensure
# sound/name overrides work with restored weapon properties
if hasattr(self.currentLevel, 'weaponOverrides') and self.currentLevel.weaponOverrides:
self.currentLevel._apply_weapon_overrides(self.currentLevel.weaponOverrides)
self.game_loop(current_level)
self.game_loop(currentLevel)
else:
messagebox("Failed to load saved level.")
else:
messagebox(f"Failed to load save: {save_data}")
messagebox(f"Failed to load save: {saveData}")
elif choice == "play":
self.currentGame = select_game(self.get_sounds())
if self.currentGame is None:
@@ -947,13 +947,13 @@ class WickedQuest:
continue
if self.currentGame:
# Ask player to choose game mode
mode_choice = game_mode_menu(self.get_sounds(), self.currentGame)
if mode_choice == "story":
modeChoice = game_mode_menu(self.get_sounds(), self.currentGame)
if modeChoice == "story":
self.player = None # Reset player for new game
self.gameStartTime = pygame.time.get_ticks()
if self.load_level(1):
self.game_loop()
elif mode_choice == "survival":
elif modeChoice == "survival":
self.start_survival_mode()
elif choice == "high_scores":
board = Scoreboard()
@@ -1116,12 +1116,12 @@ class WickedQuest:
self.currentLevel = Level(levelData, self.get_sounds(), self.player, self.currentGame)
def game_mode_menu(sounds, game_dir=None):
def game_mode_menu(sounds, gameDir=None):
"""Display game mode selection menu using instruction_menu.
Args:
sounds (dict): Dictionary of loaded sound effects
game_dir (str): Current game directory to check for instructions/credits
gameDir (str): Current game directory to check for instructions/credits
Returns:
str: Selected game mode or None if cancelled
@@ -1130,79 +1130,63 @@ def game_mode_menu(sounds, game_dir=None):
import os
# Build base menu options
menu_options = ["Story", "Survival Mode"]
menuOptions = ["Story", "Survival Mode"]
# Check for level pack specific files if game directory is provided
if game_dir:
if gameDir:
try:
game_path = get_game_dir_path(game_dir)
gamePath = get_game_dir_path(gameDir)
# Check for instructions.txt
instructions_path = os.path.join(game_path, "instructions.txt")
if os.path.exists(instructions_path):
menu_options.append("Instructions")
instructionsPath = os.path.join(gamePath, "instructions.txt")
if os.path.exists(instructionsPath):
menuOptions.append("Instructions")
# Check for credits.txt
credits_path = os.path.join(game_path, "credits.txt")
if os.path.exists(credits_path):
menu_options.append("Credits")
creditsPath = os.path.join(gamePath, "credits.txt")
if os.path.exists(creditsPath):
menuOptions.append("Credits")
except Exception:
# If there's any error checking files, just continue with basic menu
pass
while True:
choice = instruction_menu(sounds, "Select game mode:", *menu_options)
choice = instruction_menu(sounds, "Select game mode:", *menuOptions)
if choice == "Story":
return "story"
elif choice == "Survival Mode":
return "survival"
elif choice == "Instructions" and game_dir:
elif choice == "Instructions" and gameDir:
# Display instructions file
try:
game_path = get_game_dir_path(game_dir)
instructions_path = os.path.join(game_path, "instructions.txt")
print(f"DEBUG: Looking for instructions at: {instructions_path}")
if os.path.exists(instructions_path):
print("DEBUG: Instructions file found, loading content...")
with open(instructions_path, 'r', encoding='utf-8') as f:
instructions_content = f.read()
print(f"DEBUG: Content length: {len(instructions_content)} characters")
print("DEBUG: Calling display_text...")
gamePath = get_game_dir_path(gameDir)
instructionsPath = os.path.join(gamePath, "instructions.txt")
if os.path.exists(instructionsPath):
with open(instructionsPath, 'r', encoding='utf-8') as f:
instructionsContent = f.read()
# Convert string to list of lines for display_text
content_lines = instructions_content.split('\n')
print(f"DEBUG: Split into {len(content_lines)} lines")
display_text(content_lines)
print("DEBUG: display_text returned")
contentLines = instructionsContent.split('\n')
display_text(contentLines)
else:
print("DEBUG: Instructions file not found at expected path")
speak("Instructions file not found")
except Exception as e:
print(f"DEBUG: Error loading instructions: {str(e)}")
speak(f"Error loading instructions: {str(e)}")
elif choice == "Credits" and game_dir:
elif choice == "Credits" and gameDir:
# Display credits file
try:
game_path = get_game_dir_path(game_dir)
credits_path = os.path.join(game_path, "credits.txt")
print(f"DEBUG: Looking for credits at: {credits_path}")
if os.path.exists(credits_path):
print("DEBUG: Credits file found, loading content...")
with open(credits_path, 'r', encoding='utf-8') as f:
credits_content = f.read()
print(f"DEBUG: Content length: {len(credits_content)} characters")
print("DEBUG: Calling display_text...")
gamePath = get_game_dir_path(gameDir)
creditsPath = os.path.join(gamePath, "credits.txt")
if os.path.exists(creditsPath):
with open(creditsPath, 'r', encoding='utf-8') as f:
creditsContent = f.read()
# Convert string to list of lines for display_text
content_lines = credits_content.split('\n')
print(f"DEBUG: Split into {len(content_lines)} lines")
display_text(content_lines)
print("DEBUG: display_text returned")
contentLines = creditsContent.split('\n')
display_text(contentLines)
else:
print("DEBUG: Credits file not found at expected path")
speak("Credits file not found")
except Exception as e:
print(f"DEBUG: Error loading credits: {str(e)}")
speak(f"Error loading credits: {str(e)}")
else:
return None