Improvements and lots of bug fixes to new weapon systems.

This commit is contained in:
Storm Dragon
2025-09-30 16:12:42 -04:00
parent 6d49ea25c3
commit 95fc94a507
8 changed files with 710 additions and 37 deletions
+163
View File
@@ -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!
+98
View File
@@ -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
@@ -647,6 +652,31 @@ class Level:
# 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
projectile_type = f"weapon_{weapon.name}"
start_x = player.xPos + (direction * 1) # Start 1 tile away from player
# Create projectile with custom speed and damage from weapon
projectile = Projectile(projectile_type, start_x, direction)
# 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)
+115 -6
View File
@@ -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
@@ -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._jack_o_lantern_count >= 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._jack_o_lantern_count = max(0, self._jack_o_lantern_count - 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)
themed_mappings = {
"shin_bone": "candy_cane", # Christmas theme example
"guts": "reindeer_guts"
}
# Check if there's a themed equivalent and if the override exists
if ammoType in themed_mappings:
themed_name = themed_mappings[ammoType]
# If the themed item exists in overrides, use the themed name
for override_key, override_data in self.weaponOverrides.items():
if isinstance(override_data, dict) and themed_name in str(override_data):
return themed_name.replace("_", " ")
# 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"""
+64
View File
@@ -59,14 +59,17 @@ class PowerUp(Object):
if original_item_type == "hand_of_glory":
player.start_invincibility()
self.check_for_custom_weapons(player)
elif original_item_type == "cauldron":
player.restore_health()
elif original_item_type == "guts":
player.add_guts()
player.collectedItems.append(original_item_type)
self.check_for_nunchucks(player)
self.check_for_custom_weapons(player)
elif original_item_type == "jack_o_lantern":
player.add_jack_o_lantern()
self.check_for_custom_weapons(player)
elif original_item_type == "extra_life":
# Don't give extra lives in survival mode
if level and level.levelId == 999:
@@ -102,6 +105,7 @@ class PowerUp(Object):
) # Use survivor_bonus sound if available, fallback to bone_dust
self.check_for_nunchucks(player)
self.check_for_custom_weapons(player)
elif original_item_type == "witch_broom":
broomWeapon = Weapon.create_witch_broom()
player.add_weapon(broomWeapon)
@@ -161,6 +165,66 @@ class PowerUp(Object):
play_sound(self.sounds["get_nunchucks"])
player.stats.update_stat("Items collected", 1)
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._jack_o_lantern_count < 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, item_type):
"""Map themed item names to their original equivalents for game logic."""
# Define themed equivalents that should behave like original items
+33 -6
View File
@@ -16,10 +16,11 @@ class SaveManager:
self.save_dir.mkdir(parents=True, exist_ok=True)
self.max_saves = 10
def create_save(self, player, current_level, game_start_time, current_game):
def create_save(self, player, current_level, game_start_time, current_game, bypass_cost=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 bypass_cost:
if not player.can_save():
return False, "Not enough bone dust to save (need 200)"
# Validate required parameters
if current_game is None:
@@ -28,9 +29,10 @@ class SaveManager:
if current_level 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 bypass_cost:
if not player.spend_save_bone_dust(200):
return False, "Failed to spend bone dust"
# Create save data
save_data = {
@@ -48,6 +50,8 @@ class SaveManager:
"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),
},
@@ -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,16 @@ 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)
weapon.displayName = data.get("displayName", None)
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
@@ -262,6 +285,10 @@ class SaveManager:
# Restore weapons
player.weapons = self._deserialize_weapons(player_state["weapons"])
# Restore custom weapon tracking data (for backward compatibility, use get with defaults)
player.craftedCustomWeapons = set(player_state.get("craftedCustomWeapons", []))
player.customWeapons = player_state.get("customWeapons", [])
# Restore current weapon
current_weapon_name = player_state.get("currentWeaponName")
if current_weapon_name:
+9
View File
@@ -24,6 +24,7 @@ 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()
@@ -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)
@@ -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
+42
View File
@@ -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
+186 -25
View File
@@ -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:
@@ -122,7 +127,7 @@ class WickedQuest:
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:
@@ -171,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:
@@ -216,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,
bypass_cost=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._jack_o_lantern_count = 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.weaponType == "broom" for w in self.player.weapons)
if not hasBroom:
broom = Weapon("broom", "witch broom", "broom_attack", "broom_hit", 7, 1.5, 2.0, 1.25)
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.weaponType == "nunchucks" for w in self.player.weapons)
if not hasNunchucks:
nunchucks = Weapon("nunchucks", "nunchucks", "nunchucks_attack", "nunchucks_hit", 10, 1.0, 1.0, 1.0)
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")
@@ -265,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}")
@@ -454,27 +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:
base_info = f"Wave {self.survivalWave}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this wave so far. {player.get_lives()} lives remaining."
else:
base_info = f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this level so far. {player.get_lives()} lives remaining."
# 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
# Add closest enemy info
closest_enemy_info = self.get_closest_enemy_info()
if closest_enemy_info:
speak(f"{base_info} {closest_enemy_info}")
else:
speak(base_info)
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:
weapon_name = getattr(player.currentWeapon, 'displayName', player.currentWeapon.name.replace("_", " "))
status_message = f"Wielding {weapon_name}"
# Check if it's a projectile weapon - always show ammo for projectile weapons
weapon_type = getattr(player.currentWeapon, 'weaponType', 'melee')
if weapon_type == "projectile":
ammo_type = getattr(player.currentWeapon, 'ammoType', None)
if ammo_type:
ammo_count = 0
ammo_display_name = ammo_type.replace("_", " ") # Default fallback
# Get current ammo count based on ammo type
if ammo_type == "bone_dust":
ammo_count = player.get_coins()
ammo_display_name = "bone dust"
elif ammo_type == "shin_bone":
ammo_count = player.shinBoneCount
ammo_display_name = "shin bones"
elif ammo_type == "jack_o_lantern":
ammo_count = player._jack_o_lantern_count
ammo_display_name = "jack o'lanterns"
elif ammo_type == "guts":
ammo_count = player.collectedItems.count("guts")
ammo_display_name = "guts"
elif ammo_type == "hand_of_glory":
ammo_count = player.collectedItems.count("hand_of_glory")
ammo_display_name = "hands of glory"
else:
# Check for any other item type in collectedItems
ammo_count = player.collectedItems.count(ammo_type)
status_message += f". {ammo_count} {ammo_display_name}"
speak(status_message)
else:
speak("No weapon equipped")
elif keys[pygame.K_h]:
speak(f"{player.get_health()} health of {player.get_max_health()}")
elif keys[pygame.K_i]:
if self.currentLevel.levelId == 999:
base_info = f"Wave {self.survivalWave}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this wave so far. {player.get_lives()} lives remaining."
else:
base_info = f"Level {self.currentLevel.levelId}, {self.currentLevel.levelName}. {player.get_health()} health of {player.get_max_health()}. {int(self.currentLevel.levelScore)} points on this level so far. {player.get_lives()} lives remaining."
# Add closest enemy info
closest_enemy_info = self.get_closest_enemy_info()
if closest_enemy_info:
speak(f"{base_info} {closest_enemy_info}")
else:
speak(base_info)
if keys[pygame.K_l]:
speak(f"{player.get_lives()} lives")
if keys[pygame.K_j]: # Check jack o'lanterns
@@ -489,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
@@ -504,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
@@ -704,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