359 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			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)
 |