#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Combat system for Storm Games. Provides weapon and projectile systems with cooldowns, hit detection, stat bonuses, and flexible configuration options. """ import time import math from typing import Dict, Any, Optional, Tuple, Union, Callable, List import copy class Weapon: """Generic weapon system with cooldown, range, and stat bonuses. Features: - Cooldown-based attack system - Hit detection with attack duration - Configurable stat bonuses (speed, jump, etc.) - Serialization support for save/load - Factory methods for common weapon types - Sound integration (optional) Example usage: # Create a sword sword = Weapon("Iron Sword", damage=10, range_value=2, cooldown=500) # Add stat bonuses sword.stat_bonuses = {"speed": 1.1, "critical_chance": 0.05} # Use in combat if sword.can_attack(): if sword.can_hit_target(enemy_pos, player_pos, facing_right): damage_dealt = sword.attack() """ def __init__( self, name: str, damage: Union[int, float], range_value: Union[int, float], attack_sound: Optional[str] = None, hit_sound: Optional[str] = None, cooldown: int = 500, attack_duration: int = 200, stat_bonuses: Optional[Dict[str, Union[int, float]]] = None ): """Initialize weapon. Args: name: Display name of the weapon damage: Base damage value range_value: Attack range attack_sound: Sound key for attack (optional) hit_sound: Sound key for successful hit (optional) cooldown: Milliseconds between attacks attack_duration: Milliseconds the attack window stays open stat_bonuses: Dictionary of stat multipliers/bonuses """ self.name = name self.damage = damage self.range_value = range_value self.attack_sound = attack_sound self.hit_sound = hit_sound self.cooldown = cooldown self.attack_duration = attack_duration self.stat_bonuses = stat_bonuses or {} # Attack state tracking self.last_attack_time = 0 self.attack_start_time = 0 self.is_attacking = False self.hit_targets = set() # Track what we've hit during current attack def can_attack(self) -> bool: """Check if weapon can currently attack (not on cooldown). Returns: True if weapon can attack, False if on cooldown """ current_time = time.time() * 1000 # Convert to milliseconds return current_time - self.last_attack_time >= self.cooldown def attack(self) -> Union[int, float]: """Initiate an attack. Returns: Damage value if attack is successful, 0 if on cooldown """ if not self.can_attack(): return 0 current_time = time.time() * 1000 self.last_attack_time = current_time self.attack_start_time = current_time self.is_attacking = True self.hit_targets.clear() return self.damage def is_attack_active(self) -> bool: """Check if the attack window is currently active. Returns: True if within attack duration, False otherwise """ if not self.is_attacking: return False current_time = time.time() * 1000 elapsed = current_time - self.attack_start_time if elapsed > self.attack_duration: self.is_attacking = False return False return True def can_hit_target( self, target_pos: Union[Tuple[float, float], float], attacker_pos: Union[Tuple[float, float], float], facing_right: bool = True, target_id: Any = None ) -> bool: """Check if target is within range and can be hit. Args: target_pos: Position of target (x, y) or just x for 1D attacker_pos: Position of attacker (x, y) or just x for 1D facing_right: Direction attacker is facing (for 1D games) target_id: Optional identifier to prevent multiple hits Returns: True if target can be hit, False otherwise """ # Check if attack is active if not self.is_attack_active(): return False # Check if we've already hit this target if target_id is not None and target_id in self.hit_targets: return False # Calculate distance if isinstance(target_pos, (tuple, list)) and isinstance(attacker_pos, (tuple, list)): # 2D distance calculation distance = math.sqrt( (target_pos[0] - attacker_pos[0])**2 + (target_pos[1] - attacker_pos[1])**2 ) else: # 1D distance calculation target_x = target_pos if isinstance(target_pos, (int, float)) else target_pos[0] attacker_x = attacker_pos if isinstance(attacker_pos, (int, float)) else attacker_pos[0] distance = abs(target_x - attacker_x) # For 1D, also check direction if facing_right and target_x <= attacker_x: return False elif not facing_right and target_x >= attacker_x: return False return distance <= self.range_value def hit_target(self, target_id: Any = None) -> Union[int, float]: """Mark a target as hit and return damage. Args: target_id: Optional identifier for the target Returns: Damage value if hit is valid, 0 if target already hit """ if target_id is not None: if target_id in self.hit_targets: return 0 self.hit_targets.add(target_id) return self.damage def apply_stat_bonuses(self, base_stats: Dict[str, Union[int, float]]) -> Dict[str, Union[int, float]]: """Apply weapon's stat bonuses to base stats. Args: base_stats: Dictionary of base stat values Returns: Dictionary with stat bonuses applied """ modified_stats = base_stats.copy() for stat_name, bonus in self.stat_bonuses.items(): if stat_name in modified_stats: if isinstance(bonus, (int, float)) and bonus > 0: if bonus > 1: # Multiplicative bonus (e.g., 1.2 = 20% increase) modified_stats[stat_name] *= bonus else: # Additive bonus (e.g., 0.05 = +5%) modified_stats[stat_name] += bonus return modified_stats def to_dict(self) -> Dict[str, Any]: """Serialize weapon to dictionary for saving. Returns: Dictionary representation of weapon """ return { "name": self.name, "damage": self.damage, "range_value": self.range_value, "attack_sound": self.attack_sound, "hit_sound": self.hit_sound, "cooldown": self.cooldown, "attack_duration": self.attack_duration, "stat_bonuses": self.stat_bonuses.copy() } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Weapon': """Create weapon from dictionary data. Args: data: Dictionary containing weapon data Returns: New Weapon instance """ return cls( name=data.get("name", "Unknown Weapon"), damage=data.get("damage", 1), range_value=data.get("range_value", 1), attack_sound=data.get("attack_sound"), hit_sound=data.get("hit_sound"), cooldown=data.get("cooldown", 500), attack_duration=data.get("attack_duration", 200), stat_bonuses=data.get("stat_bonuses", {}) ) @classmethod def create_from_config(cls, config_dict: Dict[str, Any]) -> 'Weapon': """Create weapon from configuration dictionary. Args: config_dict: Configuration dictionary Returns: New Weapon instance """ return cls.from_dict(config_dict) # Factory methods for common weapon types @classmethod def create_sword(cls, name: str = "Sword", damage: Union[int, float] = 10) -> 'Weapon': """Create a standard sword weapon. Args: name: Name of the sword damage: Damage value Returns: Sword weapon instance """ return cls( name=name, damage=damage, range_value=2, attack_sound="sword_swing", hit_sound="sword_hit", cooldown=800, attack_duration=300 ) @classmethod def create_dagger(cls, name: str = "Dagger", damage: Union[int, float] = 6) -> 'Weapon': """Create a fast dagger weapon. Args: name: Name of the dagger damage: Damage value Returns: Dagger weapon instance """ return cls( name=name, damage=damage, range_value=1, attack_sound="dagger_stab", hit_sound="dagger_hit", cooldown=400, attack_duration=150, stat_bonuses={"speed": 1.1} ) @classmethod def create_staff(cls, name: str = "Staff", damage: Union[int, float] = 8) -> 'Weapon': """Create a magical staff weapon. Args: name: Name of the staff damage: Damage value Returns: Staff weapon instance """ return cls( name=name, damage=damage, range_value=3, attack_sound="staff_cast", hit_sound="magic_hit", cooldown=1200, attack_duration=400 ) def __str__(self) -> str: """String representation of weapon.""" return f"{self.name} (Damage: {self.damage}, Range: {self.range_value})" def __repr__(self) -> str: """Detailed string representation.""" return f"Weapon(name='{self.name}', damage={self.damage}, range={self.range_value})" class Projectile: """Generic projectile system for ranged combat. Features: - Position-based movement with direction vectors - Range limiting and collision detection - Configurable damage, speed, and behavior - 1D and 2D movement support - Hit callbacks for custom effects Example usage: # Create an arrow arrow = Projectile("arrow", (10, 5), (1, 0), speed=0.3, damage=8) # Update in game loop while arrow.is_active(): arrow.update() # Check collision with target if arrow.check_collision(target_pos, target_size): damage = arrow.hit() break """ def __init__( self, projectile_type: str, start_pos: Union[Tuple[float, float], float], direction: Union[Tuple[float, float], float], speed: float = 0.2, damage: Union[int, float] = 5, max_range: Union[int, float] = 12, on_hit_callback: Optional[Callable] = None ): """Initialize projectile. Args: projectile_type: Type identifier for the projectile start_pos: Starting position (x, y) or just x for 1D direction: Direction vector (dx, dy) or just direction for 1D speed: Movement speed per update damage: Damage value max_range: Maximum travel distance on_hit_callback: Optional function called when projectile hits """ self.projectile_type = projectile_type self.damage = damage self.max_range = max_range self.speed = speed self.on_hit_callback = on_hit_callback # Position tracking if isinstance(start_pos, (tuple, list)): self.position = list(start_pos) self.start_position = list(start_pos) self.is_2d = True else: self.position = float(start_pos) self.start_position = float(start_pos) self.is_2d = False # Direction handling if self.is_2d and isinstance(direction, (tuple, list)): self.direction = list(direction) # Normalize direction vector magnitude = math.sqrt(direction[0]**2 + direction[1]**2) if magnitude > 0: self.direction = [direction[0]/magnitude, direction[1]/magnitude] elif not self.is_2d: self.direction = float(direction) else: raise ValueError("Direction format must match position format (1D or 2D)") # State tracking self.active = True self.has_hit = False self.distance_traveled = 0.0 def update(self) -> bool: """Update projectile position. Returns: True if projectile is still active, False if it should be removed """ if not self.active: return False # Move projectile if self.is_2d: old_pos = self.position.copy() self.position[0] += self.direction[0] * self.speed self.position[1] += self.direction[1] * self.speed # Calculate distance traveled dx = self.position[0] - old_pos[0] dy = self.position[1] - old_pos[1] self.distance_traveled += math.sqrt(dx**2 + dy**2) else: old_pos = self.position self.position += self.direction * self.speed self.distance_traveled += abs(self.position - old_pos) # Check if projectile has exceeded range if self.distance_traveled >= self.max_range: self.active = False return self.active def check_collision( self, target_pos: Union[Tuple[float, float], float], target_size: Union[Tuple[float, float], float] = 1.0 ) -> bool: """Check if projectile collides with a target. Args: target_pos: Target position (x, y) or just x for 1D target_size: Target size (width, height) or just width for 1D Returns: True if collision detected, False otherwise """ if not self.active or self.has_hit: return False if self.is_2d and isinstance(target_pos, (tuple, list)): # 2D collision detection (simple rectangle/circle) target_width = target_size[0] if isinstance(target_size, (tuple, list)) else target_size target_height = target_size[1] if isinstance(target_size, (tuple, list)) else target_size # Simple bounding box collision distance_x = abs(self.position[0] - target_pos[0]) distance_y = abs(self.position[1] - target_pos[1]) return distance_x <= target_width / 2 and distance_y <= target_height / 2 else: # 1D collision detection target_x = target_pos if isinstance(target_pos, (int, float)) else target_pos[0] proj_x = self.position if isinstance(self.position, (int, float)) else self.position[0] target_width = target_size if isinstance(target_size, (int, float)) else target_size[0] return abs(proj_x - target_x) <= target_width / 2 def hit(self) -> Union[int, float]: """Mark projectile as having hit a target. Returns: Damage value of the projectile """ if self.has_hit: return 0 self.has_hit = True self.active = False # Call hit callback if provided if self.on_hit_callback: try: self.on_hit_callback(self) except Exception as e: print(f"Warning: Hit callback failed: {e}") return self.damage def is_active(self) -> bool: """Check if projectile is still active. Returns: True if projectile is active, False otherwise """ return self.active and not self.has_hit def get_position(self) -> Union[Tuple[float, float], float]: """Get current position of projectile. Returns: Current position (x, y) or just x for 1D """ if self.is_2d: return tuple(self.position) else: return self.position def get_distance_traveled(self) -> float: """Get total distance traveled by projectile. Returns: Distance traveled from start position """ return self.distance_traveled def to_dict(self) -> Dict[str, Any]: """Serialize projectile to dictionary. Returns: Dictionary representation of projectile """ return { "projectile_type": self.projectile_type, "position": self.position, "start_position": self.start_position, "direction": self.direction, "speed": self.speed, "damage": self.damage, "max_range": self.max_range, "distance_traveled": self.distance_traveled, "active": self.active, "has_hit": self.has_hit, "is_2d": self.is_2d } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Projectile': """Create projectile from dictionary data. Args: data: Dictionary containing projectile data Returns: New Projectile instance """ projectile = cls( projectile_type=data.get("projectile_type", "projectile"), start_pos=data.get("start_position", (0, 0)), direction=data.get("direction", (1, 0)), speed=data.get("speed", 0.2), damage=data.get("damage", 5), max_range=data.get("max_range", 12) ) # Restore state projectile.position = data.get("position", projectile.position) projectile.distance_traveled = data.get("distance_traveled", 0.0) projectile.active = data.get("active", True) projectile.has_hit = data.get("has_hit", False) return projectile # Factory methods for common projectile types @classmethod def create_arrow(cls, start_pos: Union[Tuple[float, float], float], direction: Union[Tuple[float, float], float]) -> 'Projectile': """Create a standard arrow projectile. Args: start_pos: Starting position direction: Direction vector Returns: Arrow projectile instance """ return cls( projectile_type="arrow", start_pos=start_pos, direction=direction, speed=0.4, damage=7, max_range=15 ) @classmethod def create_fireball(cls, start_pos: Union[Tuple[float, float], float], direction: Union[Tuple[float, float], float]) -> 'Projectile': """Create a fireball projectile with area damage. Args: start_pos: Starting position direction: Direction vector Returns: Fireball projectile instance """ return cls( projectile_type="fireball", start_pos=start_pos, direction=direction, speed=0.3, damage=12, max_range=20 ) @classmethod def create_bullet(cls, start_pos: Union[Tuple[float, float], float], direction: Union[Tuple[float, float], float]) -> 'Projectile': """Create a fast bullet projectile. Args: start_pos: Starting position direction: Direction vector Returns: Bullet projectile instance """ return cls( projectile_type="bullet", start_pos=start_pos, direction=direction, speed=0.8, damage=5, max_range=25 ) def __str__(self) -> str: """String representation of projectile.""" return f"{self.projectile_type} at {self.position} (Damage: {self.damage})" def __repr__(self) -> str: """Detailed string representation.""" return f"Projectile(type='{self.projectile_type}', pos={self.position}, damage={self.damage})"