Improvements and lots of bug fixes to new weapon systems.
This commit is contained in:
@@ -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!
|
||||
|
||||
|
||||
@@ -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
@@ -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"""
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user