Compare commits
11 Commits
10f859136b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 04b9ac5a51 | |||
| 275ead1e80 | |||
| dd67eb5e1d | |||
| 95fc94a507 | |||
| 6d49ea25c3 | |||
| 870ef051ba | |||
| fbbb970303 | |||
| a16b6f0b1f | |||
| 4681dc5a2e | |||
| 28856d2662 | |||
| 525c9d7563 |
@@ -6,6 +6,9 @@ https://git.stormux.org/storm/wicked-quest
|
||||
Angus Kola: Beta testing.
|
||||
https://angus.kola.casa
|
||||
|
||||
Chris Wright: Beta testing.
|
||||
https://www.youtube.com/channel/UCDWOwfwJ18lQiXRubVBEFkw
|
||||
|
||||
Deedra Waters: Beta testing.
|
||||
|
||||
Ember Wolfe: Sound effects, beta testing, and visual verification of messages.
|
||||
|
||||
@@ -819,5 +819,168 @@ This example demonstrates:
|
||||
- **Custom audio** (themed ambience and footsteps)
|
||||
- **Strategic design** (safe zones, risk/reward placement)
|
||||
|
||||
## Custom Weapons
|
||||
|
||||
Level packs can define custom weapons that players can craft and use alongside the standard weapons (1=shovel, 2=broom, 3=nunchucks). Custom weapons are bound to keys 4-0, giving you 7 additional weapon slots.
|
||||
|
||||
### Basic Structure
|
||||
|
||||
Add a `custom_weapons` array to your level JSON (typically level 1.json):
|
||||
|
||||
```json
|
||||
{
|
||||
"custom_weapons": [
|
||||
{
|
||||
"key": 4,
|
||||
"name": "weapon_internal_name",
|
||||
"display_name": "Player Visible Name",
|
||||
"damage": 6,
|
||||
"range": 3,
|
||||
"attack_sound": "sound_name",
|
||||
"hit_sound": "sound_name"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
- **`key`** - Key binding (4-9 or 0 for key 10)
|
||||
- **`name`** - Internal weapon name (for game logic)
|
||||
- **`damage`** - Weapon damage (integer)
|
||||
- **`range`** - Attack range in tiles (integer)
|
||||
- **`attack_sound`** - Sound when attacking
|
||||
- **`hit_sound`** - Sound when hitting enemies
|
||||
|
||||
### Optional Fields
|
||||
|
||||
- **`display_name`** - Name shown to player (defaults to `name`)
|
||||
- **`weapon_type`** - "melee" or "projectile" (defaults to "melee")
|
||||
- **`cooldown`** - Milliseconds between attacks (defaults to 500)
|
||||
- **`attack_duration`** - Milliseconds attack is active (defaults to 200)
|
||||
- **`speed_bonus`** - Movement speed multiplier (defaults to 1.0)
|
||||
- **`jump_bonus`** - Jump duration multiplier (defaults to 1.0)
|
||||
- **`requires`** - Crafting requirements (defaults to none)
|
||||
- **`craft_sound`** - Sound when weapon is crafted
|
||||
- **`ammo_type`** - Ammo type for projectile weapons
|
||||
- **`ammo_cost`** - Ammo consumed per shot (defaults to 1)
|
||||
- **`projectile_speed`** - Speed of projectiles (defaults to 0.2)
|
||||
|
||||
### Melee Weapon Example
|
||||
|
||||
```json
|
||||
{
|
||||
"custom_weapons": [
|
||||
{
|
||||
"key": 4,
|
||||
"name": "bone_hatchet",
|
||||
"display_name": "blood-stained hatchet",
|
||||
"damage": 6,
|
||||
"range": 2,
|
||||
"attack_sound": "player_hatchet_chop",
|
||||
"hit_sound": "player_hatchet_hit",
|
||||
"cooldown": 400,
|
||||
"speed_bonus": 1.1,
|
||||
"requires": {
|
||||
"shin_bone": 3,
|
||||
"hand_of_glory": 1
|
||||
},
|
||||
"craft_sound": "get_hatchet"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Projectile Weapon Example
|
||||
|
||||
```json
|
||||
{
|
||||
"custom_weapons": [
|
||||
{
|
||||
"key": 6,
|
||||
"name": "bone_crossbow",
|
||||
"display_name": "cursed crossbow",
|
||||
"weapon_type": "projectile",
|
||||
"damage": 12,
|
||||
"range": 8,
|
||||
"attack_sound": "crossbow_fire",
|
||||
"hit_sound": "crossbow_impact",
|
||||
"cooldown": 1500,
|
||||
"attack_duration": 100,
|
||||
"ammo_type": "shin_bone",
|
||||
"ammo_cost": 3,
|
||||
"projectile_speed": 0.3,
|
||||
"requires": {
|
||||
"shin_bone": 5,
|
||||
"guts": 2
|
||||
},
|
||||
"craft_sound": "get_crossbow"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Crafting System
|
||||
|
||||
Weapons with `requires` are automatically crafted when the player has the necessary materials:
|
||||
|
||||
**Supported Ammo/Material Types:**
|
||||
- `shin_bone` - Shin bones collected
|
||||
- `bone_dust` - Bone dust for extra lives
|
||||
- `guts` - Health/crafting items
|
||||
- `hand_of_glory` - Collectible items
|
||||
- `jack_o_lantern` - Throwable projectiles
|
||||
- Any other item type in `collectedItems`
|
||||
|
||||
**Crafting Behavior:**
|
||||
- Materials are NOT consumed (items keep their original functions)
|
||||
- Weapons auto-craft when requirements are met
|
||||
- Each weapon can only be crafted once per playthrough
|
||||
- Crafting plays the `craft_sound` if specified
|
||||
|
||||
### Projectile Weapons
|
||||
|
||||
Projectile weapons (`weapon_type: "projectile"`) fire projectiles instead of melee attacks:
|
||||
|
||||
- **Ammo Consumption** - Each shot consumes `ammo_cost` of `ammo_type`
|
||||
- **Out of Ammo** - Shows themed message: "Not enough candy canes"
|
||||
- **Inventory Display** - Press 'c' to see current ammo counts
|
||||
- **High Damage** - Usually more powerful than melee weapons
|
||||
- **Strategic Cost** - Must balance ammo usage vs. other needs
|
||||
|
||||
### Weapon Override Integration
|
||||
|
||||
Custom weapons work with the weapon override system. You can theme custom weapons just like standard ones:
|
||||
|
||||
```json
|
||||
{
|
||||
"weapon_sound_overrides": {
|
||||
"bone_hatchet": {
|
||||
"name": "ice hatchet",
|
||||
"attack_sound": "player_ice_hatchet_chop",
|
||||
"hit_sound": "player_ice_hatchet_hit"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Binding Reference
|
||||
|
||||
- **1-3**: Reserved for standard weapons (shovel, broom, nunchucks)
|
||||
- **4-9**: Custom weapon slots (6 weapons)
|
||||
- **0**: Custom weapon slot (key 10, for 7th weapon)
|
||||
|
||||
### Design Tips
|
||||
|
||||
**Balanced Progression:**
|
||||
- Early weapons: Low requirements, moderate power
|
||||
- Mid weapons: Medium requirements, good utility
|
||||
- Late weapons: High requirements, devastating power
|
||||
|
||||
**Ammo Economics:**
|
||||
- Cheap ammo: bone_dust (common)
|
||||
- Expensive ammo: shin_bone (valuable for lives)
|
||||
- Special ammo: guts, hand_of_glory (limited)
|
||||
|
||||
Check out the existing Wicked Quest levels for more examples and inspiration!
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import os
|
||||
import sys
|
||||
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
|
||||
bundleDir = os.path.dirname(sys.executable)
|
||||
internalDir = sys._MEIPASS
|
||||
|
||||
# Directories to move from _internal to parent
|
||||
# All these use the parent directory for easier user customization
|
||||
dirsToMove = ['sounds', 'libstormgames', 'levels']
|
||||
|
||||
# Directories to copy (keep in both locations)
|
||||
# None needed - everything now uses parent directory
|
||||
dirsToCopy = []
|
||||
|
||||
# Files to move from _internal to parent
|
||||
filesToMove = ['files', 'logo.png']
|
||||
|
||||
# Move directories
|
||||
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(internalPath) and not os.path.exists(targetPath):
|
||||
try:
|
||||
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 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(internalPath) and not os.path.exists(targetPath):
|
||||
try:
|
||||
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 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(internalPath) and not os.path.exists(targetPath):
|
||||
try:
|
||||
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
|
||||
LFS
BIN
Binary file not shown.
+18
-17
@@ -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,33 @@ 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
|
||||
)
|
||||
|
||||
# Apply sound override after creation (similar to how graves handle it)
|
||||
# Use themed item name if override exists (for proper themed item handling)
|
||||
actualItemType = itemType
|
||||
if hasattr(self, 'itemSoundOverride'):
|
||||
self.dropped_item.soundName = self.itemSoundOverride
|
||||
actualItemType = self.itemSoundOverride
|
||||
|
||||
self.droppedItem = PowerUp(
|
||||
dropX, self.yPos, actualItemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
|
||||
)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
+16
-16
@@ -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)
|
||||
|
||||
+44
-49
@@ -7,6 +7,22 @@ from os.path import isdir, join
|
||||
from libstormgames import speak, instruction_menu
|
||||
|
||||
|
||||
def get_levels_base_path():
|
||||
"""Get base path for levels directory, handling PyInstaller paths.
|
||||
|
||||
Returns:
|
||||
str: Base path where levels directory is located
|
||||
"""
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
# Running as PyInstaller executable
|
||||
# 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__)))
|
||||
|
||||
|
||||
def get_available_games():
|
||||
"""Get list of available game directories in levels folder.
|
||||
|
||||
@@ -14,16 +30,9 @@ def get_available_games():
|
||||
list: List of game directory names
|
||||
"""
|
||||
try:
|
||||
# Handle PyInstaller path issues
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
# Running as PyInstaller executable
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
# Running as script
|
||||
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
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 []
|
||||
|
||||
@@ -50,71 +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")
|
||||
|
||||
# Handle PyInstaller path issues
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
# Running as PyInstaller executable
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
# Running as script
|
||||
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
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")
|
||||
|
||||
# Handle PyInstaller path issues - same logic as get_level_path
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
# Running as PyInstaller executable
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
# Running as script
|
||||
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
return os.path.join(base_path, "levels", game_dir)
|
||||
if gameDir is None:
|
||||
raise ValueError("gameDir cannot be None")
|
||||
|
||||
basePath = get_levels_base_path()
|
||||
return os.path.join(basePath, "levels", gameDir)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+171
-73
@@ -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"]
|
||||
@@ -49,6 +49,11 @@ class Level:
|
||||
# Give player access to overrides for newly added weapons
|
||||
self.player.set_weapon_overrides(self.weaponOverrides)
|
||||
|
||||
# Load custom weapons if specified
|
||||
self.customWeapons = levelData.get("custom_weapons", [])
|
||||
if self.customWeapons:
|
||||
self._load_custom_weapons(self.customWeapons)
|
||||
|
||||
# Level intro message (skip for survival mode)
|
||||
if levelData["level_id"] != 999: # 999 is survival mode
|
||||
# Check if level uses new dialog format or old description format
|
||||
@@ -132,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:
|
||||
@@ -141,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
|
||||
@@ -227,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:
|
||||
@@ -281,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],
|
||||
@@ -291,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!")
|
||||
@@ -349,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
|
||||
@@ -362,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"""
|
||||
@@ -389,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"""
|
||||
@@ -401,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)
|
||||
|
||||
@@ -412,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
|
||||
@@ -435,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)
|
||||
|
||||
@@ -495,22 +500,22 @@ class Level:
|
||||
if self.player.get_health() < self.player.get_max_health():
|
||||
self.player.set_health(min(self.player.get_health() + 1, self.player.get_max_health()))
|
||||
|
||||
if self.player._coins % 100 == 0:
|
||||
# Only give extra lives in story mode, not survival mode (level_id 999)
|
||||
if self.levelId != 999:
|
||||
# Extra life
|
||||
self.player._coins = 0
|
||||
self.player._lives += 1
|
||||
self.levelScore += 1000
|
||||
play_sound(self.sounds["get_extra_life"])
|
||||
else:
|
||||
# In survival mode, reset coin counter but give bonus score instead
|
||||
self.player._coins = 0
|
||||
self.levelScore += 2000 # Double score bonus instead of extra life
|
||||
speak("100 bone dust collected! Bonus score!")
|
||||
play_sound(
|
||||
self.sounds.get("survivor_bonus", "bone_dust")
|
||||
) # Use survivor_bonus sound if available, fallback to bone_dust
|
||||
if self.player._coins >= 100:
|
||||
# Only give extra lives in story mode, not survival mode (level_id 999)
|
||||
if self.levelId != 999:
|
||||
# Extra life
|
||||
self.player._coins -= 100
|
||||
self.player._lives += 1
|
||||
self.levelScore += 1000
|
||||
play_sound(self.sounds["get_extra_life"])
|
||||
else:
|
||||
# In survival mode, reset coin counter but give bonus score instead
|
||||
self.player._coins = 0
|
||||
self.levelScore += 2000 # Double score bonus instead of extra life
|
||||
speak("100 bone dust collected! Bonus score!")
|
||||
play_sound(
|
||||
self.sounds.get("survivor_bonus", "bone_dust")
|
||||
) # Use survivor_bonus sound if available, fallback to bone_dust
|
||||
continue
|
||||
|
||||
# Handle spiderweb - collision depends on Y position
|
||||
@@ -545,21 +550,21 @@ 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
|
||||
# Create PowerUp to handle the item effect (use themed name if override exists)
|
||||
item = PowerUp(
|
||||
obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1, self.leftBoundary, self.rightBoundary
|
||||
obj.xPos, obj.yPos, itemSound, self.sounds, 1, self.leftBoundary, self.rightBoundary
|
||||
)
|
||||
item.apply_effect(self.player, self)
|
||||
# Stop grave's current audio channel
|
||||
@@ -570,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")
|
||||
@@ -638,15 +643,40 @@ 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"])
|
||||
|
||||
def create_weapon_projectile(self, player):
|
||||
"""Create a projectile from a projectile weapon attack"""
|
||||
weapon = player.currentWeapon
|
||||
if not weapon or not hasattr(weapon, 'weaponType') or weapon.weaponType != "projectile":
|
||||
return
|
||||
|
||||
# Determine projectile direction
|
||||
direction = 1 if player.facingRight else -1
|
||||
|
||||
# Create projectile with weapon-specific properties
|
||||
# Use a generic projectile type for weapon projectiles
|
||||
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(projectileType, startX, direction)
|
||||
|
||||
# Override projectile properties with weapon stats
|
||||
projectile.damage = weapon.damage
|
||||
projectile.speed = getattr(weapon, 'projectileSpeed', 0.2)
|
||||
projectile.weaponProjectile = True # Mark as weapon projectile
|
||||
projectile.hitSound = weapon.hitSound
|
||||
|
||||
self.projectiles.append(projectile)
|
||||
|
||||
def _apply_weapon_overrides(self, weaponOverrides):
|
||||
"""Apply sound and name overrides to player weapons based on level pack theme."""
|
||||
for weapon in self.player.weapons:
|
||||
@@ -672,6 +702,74 @@ class Level:
|
||||
if "hit_sound" in overrides and hasattr(weapon, 'hitSound') and weapon.hitSound != overrides["hit_sound"]:
|
||||
weapon.hitSound = overrides["hit_sound"]
|
||||
|
||||
def _load_custom_weapons(self, customWeapons):
|
||||
"""Load custom weapons from level data and make them available for crafting."""
|
||||
from src.weapon import Weapon
|
||||
|
||||
# Store custom weapons in player for crafting checks
|
||||
self.player.set_custom_weapons(customWeapons)
|
||||
|
||||
# Apply weapon overrides to custom weapons if they exist
|
||||
for weaponData in customWeapons:
|
||||
weaponName = weaponData["name"]
|
||||
if weaponName in self.weaponOverrides:
|
||||
overrides = self.weaponOverrides[weaponName]
|
||||
# Apply overrides to the weapon data before any weapons are created
|
||||
if "name" in overrides:
|
||||
weaponData["display_name"] = overrides["name"]
|
||||
if "attack_sound" in overrides:
|
||||
weaponData["attack_sound"] = overrides["attack_sound"]
|
||||
if "hit_sound" in overrides:
|
||||
weaponData["hit_sound"] = overrides["hit_sound"]
|
||||
|
||||
# Check for custom weapons with no requirements and add them immediately
|
||||
self._check_initial_custom_weapons()
|
||||
|
||||
def _check_initial_custom_weapons(self):
|
||||
"""Check for custom weapons with no requirements and add them immediately at level start."""
|
||||
if not hasattr(self.player, 'customWeapons') or not self.player.customWeapons:
|
||||
return
|
||||
|
||||
from src.weapon import Weapon
|
||||
|
||||
for weaponData in self.player.customWeapons:
|
||||
weaponName = weaponData["name"]
|
||||
|
||||
# Skip if weapon already crafted
|
||||
if hasattr(self.player, 'craftedCustomWeapons') and weaponName in self.player.craftedCustomWeapons:
|
||||
continue
|
||||
|
||||
# Skip if player already has this weapon
|
||||
if any(weapon.originalName == weaponName for weapon in self.player.weapons):
|
||||
continue
|
||||
|
||||
# Check if weapon has no requirements or empty requirements
|
||||
requirements = weaponData.get("requires", {})
|
||||
if not requirements: # No requirements or empty dict
|
||||
# Create and add the weapon immediately
|
||||
customWeapon = Weapon.create_from_json(weaponData)
|
||||
self.player.add_weapon(customWeapon)
|
||||
|
||||
# Mark as crafted to prevent duplicate creation
|
||||
if not hasattr(self.player, 'craftedCustomWeapons'):
|
||||
self.player.craftedCustomWeapons = set()
|
||||
self.player.craftedCustomWeapons.add(weaponName)
|
||||
|
||||
# Calculate score bonus
|
||||
basePoints = customWeapon.damage * 1000
|
||||
rangeModifier = customWeapon.range * 500
|
||||
self.player.scoreboard.increase_score(basePoints + rangeModifier)
|
||||
|
||||
# Play craft sound if specified
|
||||
craftSound = weaponData.get("craft_sound")
|
||||
if craftSound and craftSound in self.sounds:
|
||||
play_sound(self.sounds[craftSound])
|
||||
else:
|
||||
# Fallback to generic weapon craft sound
|
||||
play_sound(self.sounds.get("get_weapon", "get_nunchucks"))
|
||||
|
||||
self.player.stats.update_stat("Items collected", 1)
|
||||
|
||||
def _apply_object_sound_overrides(self, obj, soundOverrides):
|
||||
"""Apply sound overrides to an object based on level pack theme."""
|
||||
# Override base sound name if specified (originalSoundName remains unchanged for game logic)
|
||||
@@ -724,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"]
|
||||
|
||||
+127
-18
@@ -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
|
||||
@@ -53,6 +53,7 @@ class Player:
|
||||
self.currentWeapon = None
|
||||
self.isAttacking = False
|
||||
self.lastAttackTime = 0
|
||||
self.isProjectileAttack = False # Flag for projectile weapon attacks
|
||||
|
||||
# Power-up states
|
||||
self.isInvincible = False
|
||||
@@ -114,7 +115,7 @@ class Player:
|
||||
|
||||
# Check invincibility status
|
||||
if self.isInvincible:
|
||||
remaining_time = (
|
||||
remainingTime = (
|
||||
self.invincibilityStartTime + self.invincibilityDuration - currentTime
|
||||
) / 1000 # Convert to seconds
|
||||
|
||||
@@ -122,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:
|
||||
@@ -146,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"""
|
||||
@@ -164,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):
|
||||
@@ -219,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
|
||||
@@ -290,10 +291,29 @@ class Player:
|
||||
self.currentWeapon = weapon
|
||||
|
||||
def switch_to_weapon(self, weaponIndex):
|
||||
"""Switch to weapon by index (1=shovel, 2=broom, 3=nunchucks)"""
|
||||
"""Switch to weapon by index (1=shovel, 2=broom, 3=nunchucks, 4-0=custom weapons)"""
|
||||
# Standard weapons (1-3)
|
||||
weaponMap = {1: "rusty_shovel", 2: "witch_broom", 3: "nunchucks"}
|
||||
|
||||
targetWeaponName = weaponMap.get(weaponIndex)
|
||||
|
||||
# For keys 4-0, check for custom weapons
|
||||
if targetWeaponName is None and 4 <= weaponIndex <= 10:
|
||||
# Convert 0 key to index 10 for easier handling
|
||||
keyIndex = 10 if weaponIndex == 0 else weaponIndex
|
||||
|
||||
# Look for custom weapon with this key binding
|
||||
for weapon in self.weapons:
|
||||
if hasattr(weapon, 'keyBinding') and weapon.keyBinding == keyIndex:
|
||||
self.equip_weapon(weapon)
|
||||
# Use display name if available, otherwise format weapon name
|
||||
displayName = getattr(weapon, 'displayName', weapon.name.replace("_", " "))
|
||||
speak(displayName)
|
||||
return True
|
||||
# No weapon found for this key
|
||||
speak("Nothing here")
|
||||
return False
|
||||
|
||||
if not targetWeaponName:
|
||||
return False
|
||||
|
||||
@@ -307,12 +327,20 @@ class Player:
|
||||
return True
|
||||
|
||||
# Weapon not found in inventory
|
||||
speak("Nothing here")
|
||||
return False
|
||||
|
||||
def set_weapon_overrides(self, weaponOverrides):
|
||||
"""Store weapon overrides for applying to newly added weapons"""
|
||||
self.weaponOverrides = weaponOverrides
|
||||
|
||||
def set_custom_weapons(self, customWeapons):
|
||||
"""Store custom weapon definitions for crafting checks"""
|
||||
self.customWeapons = customWeapons
|
||||
# Initialize set to track which custom weapons have been crafted
|
||||
if not hasattr(self, 'craftedCustomWeapons'):
|
||||
self.craftedCustomWeapons = set()
|
||||
|
||||
def _apply_weapon_override(self, weapon):
|
||||
"""Apply weapon overrides to a single weapon"""
|
||||
if not hasattr(self, 'weaponOverrides') or not self.weaponOverrides:
|
||||
@@ -340,6 +368,63 @@ class Player:
|
||||
if "hit_sound" in overrides and hasattr(weapon, 'hitSound') and weapon.hitSound != overrides["hit_sound"]:
|
||||
weapon.hitSound = overrides["hit_sound"]
|
||||
|
||||
def has_ammo(self, ammoType, cost):
|
||||
"""Check if player has enough ammo for a projectile weapon"""
|
||||
if ammoType == "shin_bone":
|
||||
return self.shinBoneCount >= cost
|
||||
elif ammoType == "bone_dust":
|
||||
return self._coins >= cost
|
||||
elif ammoType == "guts":
|
||||
return self.collectedItems.count("guts") >= cost
|
||||
elif ammoType == "hand_of_glory":
|
||||
return self.collectedItems.count("hand_of_glory") >= cost
|
||||
elif ammoType == "jack_o_lantern":
|
||||
return self._jackOLanternCount >= cost
|
||||
else:
|
||||
# Check for any other item type in collectedItems
|
||||
return self.collectedItems.count(ammoType) >= cost
|
||||
|
||||
def consume_ammo(self, ammoType, cost):
|
||||
"""Consume ammo for a projectile weapon shot"""
|
||||
if ammoType == "shin_bone":
|
||||
self.shinBoneCount = max(0, self.shinBoneCount - cost)
|
||||
elif ammoType == "bone_dust":
|
||||
self._coins = max(0, self._coins - cost)
|
||||
elif ammoType == "guts":
|
||||
# Remove items from collectedItems list
|
||||
for _ in range(min(cost, self.collectedItems.count("guts"))):
|
||||
self.collectedItems.remove("guts")
|
||||
elif ammoType == "hand_of_glory":
|
||||
for _ in range(min(cost, self.collectedItems.count("hand_of_glory"))):
|
||||
self.collectedItems.remove("hand_of_glory")
|
||||
elif ammoType == "jack_o_lantern":
|
||||
self._jackOLanternCount = max(0, self._jackOLanternCount - cost)
|
||||
else:
|
||||
# Handle any other item type
|
||||
for _ in range(min(cost, self.collectedItems.count(ammoType))):
|
||||
self.collectedItems.remove(ammoType)
|
||||
|
||||
def get_ammo_display_name(self, ammoType):
|
||||
"""Get themed display name for ammo type"""
|
||||
# Check for themed mappings in weapon overrides
|
||||
if hasattr(self, 'weaponOverrides') and self.weaponOverrides:
|
||||
# Define themed mappings (this could be extended or made configurable)
|
||||
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 themedMappings:
|
||||
themedName = themedMappings[ammoType]
|
||||
# If the themed item exists in overrides, use the themed name
|
||||
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("_", " ")
|
||||
|
||||
def add_item(self, item):
|
||||
"""Add an item to inventory"""
|
||||
self.inventory.append(item)
|
||||
@@ -347,11 +432,35 @@ 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
|
||||
return False
|
||||
if not self.currentWeapon:
|
||||
return False
|
||||
|
||||
# Check if this is a projectile weapon
|
||||
if hasattr(self.currentWeapon, 'weaponType') and self.currentWeapon.weaponType == "projectile":
|
||||
# Check ammo availability
|
||||
if not self.has_ammo(self.currentWeapon.ammoType, self.currentWeapon.ammoCost):
|
||||
from libstormgames import speak
|
||||
ammoName = self.get_ammo_display_name(self.currentWeapon.ammoType)
|
||||
speak(f"Not enough {ammoName}")
|
||||
return False
|
||||
|
||||
# Consume ammo and mark as projectile attack
|
||||
if self.currentWeapon.start_attack(currentTime):
|
||||
self.consume_ammo(self.currentWeapon.ammoType, self.currentWeapon.ammoCost)
|
||||
self.isAttacking = True
|
||||
self.lastAttackTime = currentTime
|
||||
# Mark this as a projectile attack for the level to handle
|
||||
self.isProjectileAttack = True
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
# Standard melee weapon
|
||||
if self.currentWeapon.start_attack(currentTime):
|
||||
self.isAttacking = True
|
||||
self.lastAttackTime = currentTime
|
||||
self.isProjectileAttack = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_attack_range(self, currentTime):
|
||||
"""Get the current attack's range based on position and facing direction"""
|
||||
|
||||
+92
-28
@@ -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,19 +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()
|
||||
elif original_item_type == "cauldron":
|
||||
self.check_for_custom_weapons(player)
|
||||
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)
|
||||
elif original_item_type == "jack_o_lantern":
|
||||
self.check_for_custom_weapons(player)
|
||||
elif originalItemType == "jack_o_lantern":
|
||||
player.add_jack_o_lantern()
|
||||
elif original_item_type == "extra_life":
|
||||
self.check_for_custom_weapons(player)
|
||||
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
|
||||
@@ -76,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
|
||||
@@ -102,11 +105,12 @@ class PowerUp(Object):
|
||||
) # Use survivor_bonus sound if available, fallback to bone_dust
|
||||
|
||||
self.check_for_nunchucks(player)
|
||||
elif original_item_type == "witch_broom":
|
||||
self.check_for_custom_weapons(player)
|
||||
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
|
||||
|
||||
@@ -161,10 +165,70 @@ class PowerUp(Object):
|
||||
play_sound(self.sounds["get_nunchucks"])
|
||||
player.stats.update_stat("Items collected", 1)
|
||||
|
||||
def _get_original_item_type(self, item_type):
|
||||
def check_for_custom_weapons(self, player):
|
||||
"""Check if player has materials for any custom weapons and create if conditions are met"""
|
||||
if not hasattr(player, 'customWeapons') or not player.customWeapons:
|
||||
return
|
||||
|
||||
from src.weapon import Weapon
|
||||
|
||||
for weaponData in player.customWeapons:
|
||||
weaponName = weaponData["name"]
|
||||
|
||||
# Skip if weapon already crafted
|
||||
if weaponName in player.craftedCustomWeapons:
|
||||
continue
|
||||
|
||||
# Skip if player already has this weapon
|
||||
if any(weapon.originalName == weaponName for weapon in player.weapons):
|
||||
continue
|
||||
|
||||
# Check if all crafting requirements are met
|
||||
requirements = weaponData.get("requires", {})
|
||||
canCraft = True
|
||||
|
||||
for itemType, neededCount in requirements.items():
|
||||
if itemType == "shin_bone":
|
||||
if player.shinBoneCount < neededCount:
|
||||
canCraft = False
|
||||
break
|
||||
elif itemType == "jack_o_lantern":
|
||||
if player._jackOLanternCount < neededCount:
|
||||
canCraft = False
|
||||
break
|
||||
else:
|
||||
# Count items in collectedItems list
|
||||
playerCount = player.collectedItems.count(itemType)
|
||||
if playerCount < neededCount:
|
||||
canCraft = False
|
||||
break
|
||||
|
||||
if canCraft:
|
||||
# Create and add the weapon
|
||||
customWeapon = Weapon.create_from_json(weaponData)
|
||||
player.add_weapon(customWeapon)
|
||||
player.equip_weapon(customWeapon)
|
||||
player.craftedCustomWeapons.add(weaponName)
|
||||
|
||||
# Calculate score bonus
|
||||
basePoints = customWeapon.damage * 1000
|
||||
rangeModifier = customWeapon.range * 500
|
||||
player.scoreboard.increase_score(basePoints + rangeModifier)
|
||||
|
||||
# Play craft sound if specified
|
||||
craftSound = weaponData.get("craft_sound")
|
||||
if craftSound and craftSound in self.sounds:
|
||||
play_sound(self.sounds[craftSound])
|
||||
else:
|
||||
# Fallback to generic weapon craft sound
|
||||
play_sound(self.sounds.get("get_weapon", "get_nunchucks"))
|
||||
|
||||
player.stats.update_stat("Items collected", 1)
|
||||
|
||||
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",
|
||||
@@ -173,4 +237,4 @@ class PowerUp(Object):
|
||||
# "frozen_heart": "guts",
|
||||
}
|
||||
|
||||
return themed_mappings.get(item_type, item_type)
|
||||
return themedMappings.get(itemType, itemType)
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
|
||||
+104
-75
@@ -11,30 +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):
|
||||
def create_save(self, player, currentLevel, gameStartTime, currentGame, bypassCost=False):
|
||||
"""Create a save file with current game state"""
|
||||
if not player.can_save():
|
||||
return False, "Not enough bone dust to save (need 200)"
|
||||
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
|
||||
if not player.spend_save_bone_dust(200):
|
||||
return False, "Failed to spend bone dust"
|
||||
# Spend the bone dust (only if not bypassing 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,
|
||||
@@ -42,19 +44,21 @@ 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,
|
||||
"weapons": self._serialize_weapons(player.weapons),
|
||||
"currentWeaponName": player.currentWeapon.name if player.currentWeapon else None,
|
||||
"craftedCustomWeapons": list(getattr(player, 'craftedCustomWeapons', set())),
|
||||
"customWeapons": getattr(player, 'customWeapons', []),
|
||||
"stats": self._serialize_stats(player.stats),
|
||||
"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",
|
||||
@@ -63,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}"
|
||||
@@ -106,6 +110,15 @@ class SaveManager:
|
||||
"attackDuration": weapon.attackDuration,
|
||||
"speedBonus": getattr(weapon, "speedBonus", 1.0),
|
||||
"jumpDurationBonus": getattr(weapon, "jumpDurationBonus", 1.0),
|
||||
# Custom weapon attributes
|
||||
"keyBinding": getattr(weapon, "keyBinding", None),
|
||||
"displayName": getattr(weapon, "displayName", None),
|
||||
"weaponType": getattr(weapon, "weaponType", "melee"),
|
||||
"ammoType": getattr(weapon, "ammoType", None),
|
||||
"ammoCost": getattr(weapon, "ammoCost", 1),
|
||||
"projectileSpeed": getattr(weapon, "projectileSpeed", 0.2),
|
||||
"craftRequirements": getattr(weapon, "craftRequirements", {}),
|
||||
"craftSound": getattr(weapon, "craftSound", None),
|
||||
}
|
||||
)
|
||||
return serialized
|
||||
@@ -146,6 +159,18 @@ class SaveManager:
|
||||
)
|
||||
# Set originalName after creation for backward compatibility
|
||||
weapon.originalName = originalName
|
||||
|
||||
# Restore custom weapon attributes (for backward compatibility, use get with defaults)
|
||||
weapon.keyBinding = data.get("keyBinding", None)
|
||||
# Only set displayName if it exists and is not None
|
||||
if "displayName" in data and data["displayName"] is not None:
|
||||
weapon.displayName = data["displayName"]
|
||||
weapon.weaponType = data.get("weaponType", "melee")
|
||||
weapon.ammoType = data.get("ammoType", None)
|
||||
weapon.ammoCost = data.get("ammoCost", 1)
|
||||
weapon.projectileSpeed = data.get("projectileSpeed", 0.2)
|
||||
weapon.craftRequirements = data.get("craftRequirements", {})
|
||||
weapon.craftSound = data.get("craftSound", None)
|
||||
weapons.append(weapon)
|
||||
return weapons
|
||||
|
||||
@@ -184,36 +209,36 @@ class SaveManager:
|
||||
|
||||
def get_save_files(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:
|
||||
@@ -230,90 +255,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_player_state(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(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_save_files()
|
||||
|
||||
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
|
||||
|
||||
+27
-18
@@ -4,7 +4,7 @@ import json
|
||||
import os
|
||||
import random
|
||||
import copy
|
||||
from src.game_selection import get_level_path
|
||||
from src.game_selection import get_level_path, get_levels_base_path
|
||||
|
||||
|
||||
class SurvivalGenerator:
|
||||
@@ -24,14 +24,15 @@ class SurvivalGenerator:
|
||||
self.ambientSounds = []
|
||||
self.footstepSounds = []
|
||||
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("levels", self.gamePack)
|
||||
packPath = os.path.join(get_levels_base_path(), "levels", self.gamePack)
|
||||
|
||||
if not os.path.exists(packPath):
|
||||
raise FileNotFoundError(f"Game pack '{self.gamePack}' not found")
|
||||
@@ -48,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)
|
||||
@@ -61,6 +62,10 @@ class SurvivalGenerator:
|
||||
if "weapon_sound_overrides" in data:
|
||||
self.weaponOverrides.update(data["weapon_sound_overrides"])
|
||||
|
||||
# Collect custom weapons (typically from level 1, but merge from all levels)
|
||||
if "custom_weapons" in data:
|
||||
self.customWeapons.extend(data["custom_weapons"])
|
||||
|
||||
# Parse objects
|
||||
for obj in data.get("objects", []):
|
||||
objCopy = copy.deepcopy(obj)
|
||||
@@ -69,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:
|
||||
@@ -130,6 +135,10 @@ class SurvivalGenerator:
|
||||
if self.weaponOverrides:
|
||||
levelData["weapon_sound_overrides"] = self.weaponOverrides
|
||||
|
||||
# Include custom weapons if any were found
|
||||
if self.customWeapons:
|
||||
levelData["custom_weapons"] = self.customWeapons
|
||||
|
||||
# Calculate spawn rates based on difficulty
|
||||
# Ensure total probabilities stay under 1.0 to prevent graves being squeezed out
|
||||
collectibleDensity = max(0.05, 0.15 - (difficultyLevel * 0.01)) # Fewer collectibles over time
|
||||
@@ -145,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
|
||||
|
||||
@@ -55,6 +55,48 @@ class Weapon:
|
||||
jumpDurationBonus=1.25, # 25% longer jump duration for better traversal
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_from_json(cls, weaponData):
|
||||
"""Create a custom weapon from JSON data"""
|
||||
# Extract required fields
|
||||
name = weaponData["name"]
|
||||
damage = weaponData["damage"]
|
||||
range = weaponData["range"]
|
||||
attackSound = weaponData["attack_sound"]
|
||||
hitSound = weaponData["hit_sound"]
|
||||
|
||||
# Extract optional fields with defaults
|
||||
cooldown = weaponData.get("cooldown", 500)
|
||||
attackDuration = weaponData.get("attack_duration", 200)
|
||||
speedBonus = weaponData.get("speed_bonus", 1.0)
|
||||
jumpDurationBonus = weaponData.get("jump_bonus", 1.0)
|
||||
|
||||
weapon = cls(
|
||||
name=name,
|
||||
damage=damage,
|
||||
range=range,
|
||||
attackSound=attackSound,
|
||||
hitSound=hitSound,
|
||||
cooldown=cooldown,
|
||||
attackDuration=attackDuration,
|
||||
speedBonus=speedBonus,
|
||||
jumpDurationBonus=jumpDurationBonus,
|
||||
)
|
||||
|
||||
# Store additional custom weapon data
|
||||
weapon.keyBinding = weaponData.get("key", None)
|
||||
weapon.displayName = weaponData.get("display_name", name)
|
||||
weapon.craftRequirements = weaponData.get("requires", {})
|
||||
weapon.craftSound = weaponData.get("craft_sound", None)
|
||||
|
||||
# Projectile weapon support
|
||||
weapon.weaponType = weaponData.get("weapon_type", "melee") # "melee" or "projectile"
|
||||
weapon.ammoType = weaponData.get("ammo_type", None)
|
||||
weapon.ammoCost = weaponData.get("ammo_cost", 1)
|
||||
weapon.projectileSpeed = weaponData.get("projectile_speed", 0.2)
|
||||
|
||||
return weapon
|
||||
|
||||
def can_attack(self, currentTime):
|
||||
"""Check if enough time has passed since last attack"""
|
||||
return currentTime - self.lastAttackTime >= self.cooldown
|
||||
|
||||
+297
-109
@@ -25,6 +25,8 @@ class WickedQuest:
|
||||
self.throwDelay = 250
|
||||
self.lastWeaponSwitchTime = 0
|
||||
self.weaponSwitchDelay = 200
|
||||
self.lastStatusTime = 0
|
||||
self.statusDelay = 300 # 300ms between status checks
|
||||
self.player = None
|
||||
self.currentGame = None
|
||||
self.runLock = False # Toggle behavior of the run keys
|
||||
@@ -39,6 +41,9 @@ class WickedQuest:
|
||||
# Level tracking for proper progression
|
||||
self.currentLevelNum = 1
|
||||
|
||||
# Cheat save flag - allows one save per level/session
|
||||
self.canSave = True
|
||||
|
||||
def initialize_pack_sounds(self):
|
||||
"""Initialize pack-specific sound system after game selection."""
|
||||
if self.currentGame:
|
||||
@@ -53,6 +58,42 @@ class WickedQuest:
|
||||
self.initialize_pack_sounds()
|
||||
return self.soundSystem if self.soundSystem else self.sounds
|
||||
|
||||
def get_closest_enemy_info(self):
|
||||
"""Get information about the closest enemy, returns None if no enemies."""
|
||||
if not self.currentLevel or not self.currentLevel.enemies:
|
||||
return None
|
||||
|
||||
# Find 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"
|
||||
activeEnemies.append((enemy, distance, direction))
|
||||
|
||||
if not activeEnemies:
|
||||
return None
|
||||
|
||||
# Sort by distance and get closest
|
||||
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:
|
||||
distanceDesc = "very close"
|
||||
elif distance <= 30:
|
||||
distanceDesc = "close"
|
||||
elif distance <= 60:
|
||||
distanceDesc = "far"
|
||||
elif distance <= 100:
|
||||
distanceDesc = "very far"
|
||||
else:
|
||||
distanceDesc = "extremely far"
|
||||
|
||||
return f"{enemy.enemyType} {distanceDesc} to the {direction}"
|
||||
|
||||
def process_console_command(self, command):
|
||||
"""Process console commands and execute their effects."""
|
||||
command = command.lower().strip()
|
||||
@@ -78,32 +119,32 @@ class WickedQuest:
|
||||
if 'get_extra_life' in self.get_sounds():
|
||||
play_sound(self.get_sounds()['get_extra_life'])
|
||||
elif command == "nekromantix":
|
||||
# Give 100 bone dust
|
||||
# Give 100 bone dust (both types)
|
||||
if self.player:
|
||||
self.player._boneDust += 100
|
||||
speak(f"100 bone dust granted. You now have {self.player.get_coins()} bone dust")
|
||||
self.player._coins += 100
|
||||
self.player.add_save_bone_dust(100)
|
||||
speak(f"100 bone dust granted. You now have {self.player.get_coins()} bone dust and {self.player.get_save_bone_dust()} save bone dust")
|
||||
if 'coin' in self.get_sounds():
|
||||
play_sound(self.get_sounds()['coin'])
|
||||
elif command == "calabrese":
|
||||
# Reveal all enemies on current level
|
||||
# Reveal closest enemy on current level
|
||||
if self.currentLevel and self.currentLevel.enemies:
|
||||
enemyCount = len([enemy for enemy in self.currentLevel.enemies if enemy.isActive])
|
||||
if enemyCount > 0:
|
||||
speak(f"{enemyCount} enemies remaining on this level")
|
||||
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"
|
||||
speak(f"{enemy.enemyType} {distance} units to the {direction}")
|
||||
closestEnemyInfo = self.get_closest_enemy_info()
|
||||
if closestEnemyInfo:
|
||||
speak(f"Closest enemy: {closestEnemyInfo}")
|
||||
else:
|
||||
speak("No active enemies on this level")
|
||||
else:
|
||||
speak("No enemies found")
|
||||
elif command == "balzac":
|
||||
# Give 200 bone dust
|
||||
# Set bone dust to 200 (both types)
|
||||
if self.player:
|
||||
self.player._boneDust = 200
|
||||
speak(f"Bone dust set to 200")
|
||||
self.player._coins = 200
|
||||
self.player._saveBoneDust = 200
|
||||
speak(f"Bone dust set to 200. You now have {self.player.get_coins()} bone dust and {self.player.get_save_bone_dust()} save bone dust")
|
||||
if 'coin' in self.get_sounds():
|
||||
play_sound(self.get_sounds()['coin'])
|
||||
elif command == "blitzkid":
|
||||
@@ -135,7 +176,7 @@ class WickedQuest:
|
||||
if grantedWeapons:
|
||||
speak(f"Granted {', '.join(grantedWeapons)}")
|
||||
else:
|
||||
speak("You already have both weapons")
|
||||
speak("You already have all weapons")
|
||||
elif command == "creepshow":
|
||||
# Toggle god mode (invincibility)
|
||||
if self.player:
|
||||
@@ -149,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'])
|
||||
@@ -180,6 +221,91 @@ class WickedQuest:
|
||||
speak("Coffin spawned at your location")
|
||||
if 'coffin' in self.get_sounds():
|
||||
play_sound(self.get_sounds()['coffin'])
|
||||
elif command == "theother": # Save game on current level
|
||||
if not self.canSave:
|
||||
speak("Save already used for this level")
|
||||
return
|
||||
|
||||
# Don't save in survival mode
|
||||
if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId == 999:
|
||||
speak("Cannot save in survival mode")
|
||||
return
|
||||
|
||||
# Create save without bone dust requirement
|
||||
try:
|
||||
success, message = self.saveManager.create_save(
|
||||
self.player,
|
||||
self.currentLevel.levelId,
|
||||
self.gameStartTime,
|
||||
self.currentGame,
|
||||
bypassCost=True # Skip bone dust requirement
|
||||
)
|
||||
|
||||
if success:
|
||||
self.canSave = False # Disable further saves for this level
|
||||
speak("Game saved")
|
||||
try:
|
||||
if 'save' in self.get_sounds():
|
||||
play_sound(self.get_sounds()['save'])
|
||||
except Exception as e:
|
||||
print(f"Error playing save sound: {e}")
|
||||
pass
|
||||
else:
|
||||
speak(f"Save failed: {message}")
|
||||
|
||||
except Exception as e:
|
||||
speak("Save failed due to error")
|
||||
print(f"Error during cheat save: {e}")
|
||||
elif command == "wednesday13":
|
||||
# Max out all stats - the ultimate power-up
|
||||
if self.player:
|
||||
from src.weapon import Weapon
|
||||
|
||||
# Max health and restore to full
|
||||
self.player._maxHealth = 20
|
||||
self.player._health = 20
|
||||
|
||||
# Max lives (set to 13 for the theme)
|
||||
self.player._lives = 13
|
||||
|
||||
# Max bone dust (both types)
|
||||
self.player._coins = 999
|
||||
self.player._saveBoneDust = 999
|
||||
|
||||
# Max jack o'lanterns (set to 99 for practicality)
|
||||
self.player._jackOLanternCount = 99
|
||||
|
||||
# Grant all weapons with level pack override support
|
||||
weaponOverrides = getattr(self.currentLevel, 'weaponSoundOverrides', {}) if self.currentLevel else {}
|
||||
|
||||
# Check if player already has broom
|
||||
hasBroom = any(w.originalName == "witch_broom" for w in self.player.weapons)
|
||||
if not hasBroom:
|
||||
broom = Weapon.create_witch_broom()
|
||||
if weaponOverrides and "witch_broom" in weaponOverrides:
|
||||
override = weaponOverrides["witch_broom"]
|
||||
broom.name = override.get("name", broom.name)
|
||||
broom.attackSound = override.get("attack_sound", broom.attackSound)
|
||||
broom.hitSound = override.get("hit_sound", broom.hitSound)
|
||||
self.player.add_weapon(broom)
|
||||
|
||||
# Check if player already has nunchucks
|
||||
hasNunchucks = any(w.originalName == "nunchucks" for w in self.player.weapons)
|
||||
if not hasNunchucks:
|
||||
nunchucks = Weapon.create_nunchucks()
|
||||
if weaponOverrides and "nunchucks" in weaponOverrides:
|
||||
override = weaponOverrides["nunchucks"]
|
||||
nunchucks.name = override.get("name", nunchucks.name)
|
||||
nunchucks.attackSound = override.get("attack_sound", nunchucks.attackSound)
|
||||
nunchucks.hitSound = override.get("hit_sound", nunchucks.hitSound)
|
||||
self.player.add_weapon(nunchucks)
|
||||
|
||||
# Enable god mode for good measure
|
||||
self.player._godMode = True
|
||||
|
||||
speak("MAXIMUM POWER! All stats maxed, all weapons granted, god mode enabled")
|
||||
if 'get_extra_life' in self.get_sounds():
|
||||
play_sound(self.get_sounds()['get_extra_life'])
|
||||
else:
|
||||
speak("Unknown command")
|
||||
|
||||
@@ -229,6 +355,8 @@ class WickedQuest:
|
||||
if self.load_level(levelNum):
|
||||
# Update the current level counter for proper progression
|
||||
self.currentLevelNum = levelNum
|
||||
# Reset cheat save flag for warped level
|
||||
self.canSave = True
|
||||
speak(f"Warped to level {levelNum}")
|
||||
else:
|
||||
speak(f"Failed to load level {levelNum}")
|
||||
@@ -310,29 +438,29 @@ 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:
|
||||
saveFiles = self.saveManager.get_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['displayName'])
|
||||
|
||||
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 saveFile in saveFiles:
|
||||
if saveFile['displayName'] == choice:
|
||||
return saveFile
|
||||
return None
|
||||
|
||||
def auto_save(self):
|
||||
@@ -418,20 +546,69 @@ class WickedQuest:
|
||||
player.distanceSinceLastStep = 0
|
||||
player.lastStepTime = currentTime
|
||||
|
||||
# Status queries
|
||||
if keys[pygame.K_c]:
|
||||
# Different status message for survival vs story mode
|
||||
if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId == 999:
|
||||
speak(f"{player.get_coins()} bone dust collected")
|
||||
else:
|
||||
speak(f"{player.get_coins()} bone dust for extra lives, {player.get_save_bone_dust()} bone dust for saves")
|
||||
if keys[pygame.K_h]:
|
||||
speak(f"{player.get_health()} health of {player.get_max_health()}")
|
||||
if keys[pygame.K_i]:
|
||||
if self.currentLevel.levelId == 999:
|
||||
speak(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:
|
||||
speak(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.")
|
||||
# Status queries with debouncing
|
||||
if (keys[pygame.K_c] or keys[pygame.K_e] or keys[pygame.K_h] or keys[pygame.K_i]) and currentTime - self.lastStatusTime >= self.statusDelay:
|
||||
self.lastStatusTime = currentTime
|
||||
|
||||
if keys[pygame.K_c]:
|
||||
# Simplified bone dust status only
|
||||
if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId == 999:
|
||||
speak(f"{player.get_coins()} bone dust collected")
|
||||
else:
|
||||
speak(f"{player.get_coins()} bone dust for extra lives, {player.get_save_bone_dust()} bone dust for saves")
|
||||
elif keys[pygame.K_e]:
|
||||
# Weapon and ammo status
|
||||
if player.currentWeapon:
|
||||
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
|
||||
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 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
|
||||
ammoCount = player.collectedItems.count(ammoType)
|
||||
|
||||
statusMessage += f". {ammoCount} {ammoDisplayName}"
|
||||
|
||||
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:
|
||||
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:
|
||||
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
|
||||
closestEnemyInfo = self.get_closest_enemy_info()
|
||||
if closestEnemyInfo:
|
||||
speak(f"{baseInfo} {closestEnemyInfo}")
|
||||
else:
|
||||
speak(baseInfo)
|
||||
if keys[pygame.K_l]:
|
||||
speak(f"{player.get_lives()} lives")
|
||||
if keys[pygame.K_j]: # Check jack o'lanterns
|
||||
@@ -446,12 +623,11 @@ class WickedQuest:
|
||||
if currentTime - self.lastThrowTime >= self.throwDelay:
|
||||
self.currentLevel.throw_projectile()
|
||||
self.lastThrowTime = currentTime
|
||||
if keys[pygame.K_e]:
|
||||
speak(f"Wielding {self.currentLevel.player.currentWeapon.name.replace('_', ' ')}")
|
||||
|
||||
# Weapon switching (1=shovel, 2=broom, 3=nunchucks)
|
||||
# Weapon switching (1=shovel, 2=broom, 3=nunchucks, 4-0=custom weapons)
|
||||
currentTime = pygame.time.get_ticks()
|
||||
if currentTime - self.lastWeaponSwitchTime >= self.weaponSwitchDelay:
|
||||
# Check standard weapon keys (1-3)
|
||||
if keys[pygame.K_1]:
|
||||
if player.switch_to_weapon(1):
|
||||
self.lastWeaponSwitchTime = currentTime
|
||||
@@ -461,11 +637,37 @@ class WickedQuest:
|
||||
elif keys[pygame.K_3]:
|
||||
if player.switch_to_weapon(3):
|
||||
self.lastWeaponSwitchTime = currentTime
|
||||
# Check custom weapon keys (4-0)
|
||||
elif keys[pygame.K_4]:
|
||||
if player.switch_to_weapon(4):
|
||||
self.lastWeaponSwitchTime = currentTime
|
||||
elif keys[pygame.K_5]:
|
||||
if player.switch_to_weapon(5):
|
||||
self.lastWeaponSwitchTime = currentTime
|
||||
elif keys[pygame.K_6]:
|
||||
if player.switch_to_weapon(6):
|
||||
self.lastWeaponSwitchTime = currentTime
|
||||
elif keys[pygame.K_7]:
|
||||
if player.switch_to_weapon(7):
|
||||
self.lastWeaponSwitchTime = currentTime
|
||||
elif keys[pygame.K_8]:
|
||||
if player.switch_to_weapon(8):
|
||||
self.lastWeaponSwitchTime = currentTime
|
||||
elif keys[pygame.K_9]:
|
||||
if player.switch_to_weapon(9):
|
||||
self.lastWeaponSwitchTime = currentTime
|
||||
elif keys[pygame.K_0]:
|
||||
if player.switch_to_weapon(10): # 0 key maps to index 10
|
||||
self.lastWeaponSwitchTime = currentTime
|
||||
|
||||
# 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])
|
||||
|
||||
# If this was a projectile weapon attack, create a projectile
|
||||
if hasattr(player, 'isProjectileAttack') and player.isProjectileAttack:
|
||||
self.currentLevel.create_weapon_projectile(player)
|
||||
|
||||
# Handle jumping
|
||||
if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping and not player.isDucking:
|
||||
player.isJumping = True
|
||||
@@ -584,7 +786,7 @@ class WickedQuest:
|
||||
altPressed = mods & pygame.KMOD_ALT
|
||||
|
||||
# Console activation with grave accent key - only in story mode
|
||||
if event.key == pygame.K_BACKQUOTE: # Grave accent key (`)
|
||||
if event.key in (pygame.K_BACKQUOTE, pygame.K_QUOTE): # Grave accent key (`)
|
||||
# Open console (only in story mode, not survival)
|
||||
if hasattr(self, 'currentLevel') and self.currentLevel and self.currentLevel.levelId != 999:
|
||||
self.open_console()
|
||||
@@ -661,6 +863,8 @@ class WickedQuest:
|
||||
|
||||
self.currentLevelNum += 1
|
||||
if self.load_level(self.currentLevelNum):
|
||||
# Reset cheat save flag for new level
|
||||
self.canSave = True
|
||||
# Auto save at the beginning of new level if conditions are met
|
||||
self.auto_save()
|
||||
levelStartTime = pygame.time.get_ticks() # Reset level timer for new level
|
||||
@@ -669,9 +873,9 @@ class WickedQuest:
|
||||
# Game complete - use gameStartTime for total
|
||||
totalTime = pygame.time.get_ticks() - self.gameStartTime
|
||||
if self.player.xPos >= self.currentLevel.rightBoundary:
|
||||
# Check for end of game scene using relative path like other sounds
|
||||
# Check for end of game scene using unified path resolution
|
||||
for ext in ['.wav', '.ogg', '.mp3']:
|
||||
endFile = os.path.join("levels", self.currentGame, f'end{ext}')
|
||||
endFile = os.path.join(get_game_dir_path(self.currentGame), f'end{ext}')
|
||||
if os.path.exists(endFile):
|
||||
self.get_sounds()['end_scene'] = pygame.mixer.Sound(endFile)
|
||||
cut_scene(self.get_sounds(), 'end_scene')
|
||||
@@ -694,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")
|
||||
|
||||
choice = game_menu(self.get_sounds(), None, *custom_options)
|
||||
customOptions.append("load_game")
|
||||
|
||||
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:
|
||||
@@ -743,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()
|
||||
@@ -912,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
|
||||
@@ -926,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
|
||||
|
||||
+6
-12
@@ -1,15 +1,7 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
# Collect level directories dynamically
|
||||
level_dirs = []
|
||||
levels_path = 'levels'
|
||||
if os.path.exists(levels_path):
|
||||
for item in os.listdir(levels_path):
|
||||
item_path = os.path.join(levels_path, item)
|
||||
if os.path.isdir(item_path):
|
||||
level_dirs.append((item_path, item_path))
|
||||
# Include entire levels directory (simpler and more reliable)
|
||||
level_dirs = [('levels', 'levels')]
|
||||
|
||||
a = Analysis(
|
||||
['wicked_quest.py'],
|
||||
@@ -18,11 +10,13 @@ a = Analysis(
|
||||
datas=level_dirs + [
|
||||
('sounds', 'sounds'),
|
||||
('libstormgames', 'libstormgames'),
|
||||
('files', 'files'),
|
||||
('logo.png', '.'),
|
||||
],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
runtime_hooks=['move_dirs_hook.py'],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
@@ -39,7 +33,7 @@ exe = EXE(
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
|
||||
Reference in New Issue
Block a user