Files
wicked-quest/src/player.py

359 lines
13 KiB
Python

# -*- coding: utf-8 -*-
import pygame
from libstormgames import *
from src.stat_tracker import StatTracker
from src.weapon import Weapon
class Player:
def __init__(self, xPos, yPos, sounds):
self.sounds = sounds
# Movement attributes
self.xPos = xPos
self.yPos = yPos
self.moveSpeed = 0.05
self.jumpDuration = 1000 # Jump duration in milliseconds
self.jumpStartTime = 0
self.isDucking = False
self.isJumping = False
self.isRunning = False
self.runMultiplier = 1.5 # Same multiplier as jumping
self.facingRight = True
# Stats and tracking
self._health = 10
self._maxHealth = 10
self._lives = 1
self.distanceSinceLastStep = 0
self.stepDistance = 0.5
self.stats = StatTracker()
self.sounds = sounds
# Footstep tracking
self.baseStepDistance = 0.8
self.baseStepInterval = 250
self.stepDistance = self.baseStepDistance
self.minStepInterval = self.baseStepInterval
self.distanceSinceLastStep = 0
self.lastStepTime = 0
self.isRunning = False
self.runMultiplier = 1.5
# Inventory system
self.inventory = []
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.shinBoneCount = 0
# Combat related attributes
self.weapons = []
self.currentWeapon = None
self.isAttacking = False
self.lastAttackTime = 0
# Power-up states
self.isInvincible = False
self.invincibilityStartTime = 0
self.invincibilityDuration = 10000 # 10 seconds of invincibility
# Death state tracking (to prevent revival after death in same frame)
self.diedThisFrame = False
# Weapon override storage
self.weaponOverrides = {}
# Initialize starting weapon (rusty shovel)
self.add_weapon(
Weapon(
name="rusty_shovel",
damage=2,
range=2,
attackSound="player_shovel_attack",
hitSound="player_shovel_hit",
attackDuration=200, # 200ms attack duration
)
)
self.scoreboard = Scoreboard()
def should_play_footstep(self, currentTime):
"""Check if it's time to play a footstep sound"""
return (
self.distanceSinceLastStep >= self.get_step_distance()
and currentTime - self.lastStepTime >= self.get_step_interval()
)
def duck(self):
"""Start ducking"""
if not self.isDucking and not self.isJumping: # Can't duck while jumping
self.isDucking = True
play_sound(self.sounds["duck"])
return True
return False
def stand(self):
"""Stop ducking state and play sound"""
if self.isDucking:
self.isDucking = False
play_sound(self.sounds["stand"])
def update(self, currentTime):
"""Update player state"""
# Reset death flag at start of each frame
self.diedThisFrame = False
if hasattr(self, "webPenaltyEndTime"):
if currentTime >= self.webPenaltyEndTime:
self.moveSpeed *= 2 # Restore speed
if self.currentWeapon:
self.currentWeapon.attackDuration *= 0.5 # Restore attack speed
del self.webPenaltyEndTime
# Check invincibility status
if self.isInvincible:
remaining_time = (
self.invincibilityStartTime + self.invincibilityDuration - currentTime
) / 1000 # Convert to seconds
# Handle countdown sounds
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:
play_sound(self.sounds["end_of_invincibility_warning"])
self._last_countdown = current_second
# Check if invincibility has expired
if currentTime - self.invincibilityStartTime >= self.invincibilityDuration:
self.isInvincible = False
speak("Invincibility wore off!")
del self._last_countdown # Clean up countdown tracker
def start_invincibility(self):
"""Activate invincibility from Hand of Glory"""
self.isInvincible = True
self.invincibilityStartTime = pygame.time.get_ticks()
if hasattr(self, "_last_countdown"):
del self._last_countdown # Reset countdown if it exists
def extra_life(self):
"""Increment lives by 1"""
self._lives += 1
def get_jack_o_lanterns(self):
"""Get number of jack o'lanterns"""
return self._jack_o_lantern_count
def add_jack_o_lantern(self):
"""Add a jack o'lantern"""
self._jack_o_lantern_count += 1
def add_guts(self):
"""Apply guts, increase max_health by 2 if less than 20 else restore health"""
if self._maxHealth < 20:
self._maxHealth += 2
else:
self._health = self._maxHealth
def throw_projectile(self):
"""Throw a jack o'lantern if we have any"""
if self.get_jack_o_lanterns() <= 0:
return None
self._jack_o_lantern_count -= 1
return {"type": "jack_o_lantern", "start_x": self.xPos, "direction": 1 if self.facingRight else -1}
def get_step_distance(self):
"""Get step distance based on current speed"""
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
totalMultiplier = weaponBonus
if self.isRunning or self.isJumping:
totalMultiplier *= self.runMultiplier
return self.baseStepDistance / totalMultiplier
def get_step_interval(self):
"""Get minimum time between steps based on current speed"""
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
totalMultiplier = weaponBonus
if self.isRunning or self.isJumping:
totalMultiplier *= self.runMultiplier
return self.baseStepInterval / totalMultiplier
def get_health(self):
"""Get current health"""
return self._health
def get_max_health(self):
"""Get current max health"""
return self._maxHealth
def restore_health(self):
"""Restore health to maximum"""
self._health = self._maxHealth
def get_current_speed(self):
"""Calculate current speed based on state and weapon"""
baseSpeed = self.moveSpeed
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
if self.isJumping or self.isRunning:
return baseSpeed * self.runMultiplier * weaponBonus
return baseSpeed * weaponBonus
def get_current_jump_duration(self):
"""Calculate current jump duration based on weapon bonus"""
weaponBonus = self.currentWeapon.jumpDurationBonus if self.currentWeapon else 1.0
return int(self.jumpDuration * weaponBonus)
def set_footstep_sound(self, soundName):
"""Set the current footstep sound"""
self.footstepSound = soundName
def set_health(self, value):
"""Set health and handle death if needed."""
old_health = self._health
# Oops, allow healing while invincible.
if self.isInvincible and value < old_health:
return
self._health = max(0, value) # Health can't go below 0
if self._health == 0 and old_health > 0:
self._lives -= 1
# Mark that player died this frame to prevent revival
self.diedThisFrame = True
# Record death time for delay before respawn/game over
self.deathTimestamp = pygame.time.get_ticks()
# Stop all current sounds before playing death sound
pygame.mixer.stop()
try:
pygame.mixer.music.stop()
except Exception:
pass
cut_scene(self.sounds, "lose_a_life")
def set_max_health(self, value):
"""Set max health"""
self._maxHealth = value
def get_coins(self):
"""Get remaining coins"""
return self._coins
def get_save_bone_dust(self):
"""Get bone dust available for saves"""
return self._saveBoneDust
def add_save_bone_dust(self, amount=1):
"""Add bone dust for saves (separate from extra life bone dust)"""
self._saveBoneDust += amount
def spend_save_bone_dust(self, amount):
"""Spend bone dust for saves"""
if self._saveBoneDust >= amount:
self._saveBoneDust -= amount
return True
return False
def can_save(self):
"""Check if player has enough bone dust to save"""
return self._saveBoneDust >= 200
def get_lives(self):
"""Get remaining lives"""
return self._lives
def add_weapon(self, weapon):
"""Add a new weapon to inventory and equip if first weapon"""
# Apply weapon overrides if they exist
self._apply_weapon_override(weapon)
self.weapons.append(weapon)
if len(self.weapons) == 1: # If this is our first weapon, equip it
self.equip_weapon(weapon)
def equip_weapon(self, weapon):
"""Equip a specific weapon"""
if weapon in self.weapons:
self.currentWeapon = weapon
def switch_to_weapon(self, weaponIndex):
"""Switch to weapon by index (1=shovel, 2=broom, 3=nunchucks)"""
weaponMap = {1: "rusty_shovel", 2: "witch_broom", 3: "nunchucks"}
targetWeaponName = weaponMap.get(weaponIndex)
if not targetWeaponName:
return False
# Find the weapon in player's inventory
for weapon in self.weapons:
# Check original name if it exists, fallback to current name
weaponKey = getattr(weapon, 'originalName', weapon.name)
if weaponKey == targetWeaponName:
self.equip_weapon(weapon)
speak(weapon.name.replace("_", " "))
return True
# Weapon not found in inventory
return False
def set_weapon_overrides(self, weaponOverrides):
"""Store weapon overrides for applying to newly added weapons"""
self.weaponOverrides = weaponOverrides
def _apply_weapon_override(self, weapon):
"""Apply weapon overrides to a single weapon"""
if not hasattr(self, 'weaponOverrides') or not self.weaponOverrides:
return
# Check if weapon needs override (use originalName if available, fallback to current name)
weaponKey = getattr(weapon, 'originalName', weapon.name)
if weaponKey in self.weaponOverrides:
overrides = self.weaponOverrides[weaponKey]
# Store original name on first override to enable future lookups
if not hasattr(weapon, 'originalName'):
weapon.originalName = weapon.name
# Override weapon name if specified and different from current
if "name" in overrides and weapon.name != overrides["name"]:
weapon.name = overrides["name"]
# Override attack sound if specified and different from current
if "attack_sound" in overrides and hasattr(weapon, 'attackSound') and weapon.attackSound != overrides["attack_sound"]:
weapon.attackSound = overrides["attack_sound"]
# Override hit sound if specified and different from current
if "hit_sound" in overrides and hasattr(weapon, 'hitSound') and weapon.hitSound != overrides["hit_sound"]:
weapon.hitSound = overrides["hit_sound"]
def add_item(self, item):
"""Add an item to inventory"""
self.inventory.append(item)
self.collectedItems.append(item)
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
def get_attack_range(self, currentTime):
"""Get the current attack's range based on position and facing direction"""
if not self.currentWeapon or not self.currentWeapon.is_attack_active(currentTime):
return None
return self.currentWeapon.get_attack_range(self.xPos, self.facingRight)