using System; using Core; using Core.Lifecycle; using Minigames.FortFight.Fort; using UnityEngine; namespace Minigames.FortFight.Projectiles { /// /// Base class for all projectile types in Fort Fight. /// Handles physics, collision, and basic damage dealing. /// Subclasses override ActivateAbility() and OnHit() for unique behaviors. /// [RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))] public abstract class ProjectileBase : ManagedBehaviour { #region Inspector Properties [Header("Visuals")] [Tooltip("Sprite renderer for projectile")] [SerializeField] protected SpriteRenderer spriteRenderer; [Header("Effects")] [Tooltip("Particle effect on impact (optional)")] [SerializeField] protected GameObject impactEffectPrefab; #endregion #region Events /// /// Fired when projectile is launched. Parameters: (ProjectileBase projectile) /// public event Action OnLaunched; /// /// Fired when projectile hits something. Parameters: (ProjectileBase projectile, Collider2D hit) /// public event Action OnImpact; /// /// Fired when projectile is destroyed. Parameters: (ProjectileBase projectile) /// public event Action OnDestroyed; #endregion #region Properties public float Damage { get; protected set; } public float Mass { get; protected set; } public Data.ProjectileType ProjectileType { get; protected set; } public bool IsLaunched { get; protected set; } public bool AbilityActivated { get; protected set; } public Vector2 LaunchDirection { get; protected set; } public float LaunchForce { get; protected set; } #endregion #region Timeout private const float ProjectileTimeout = 10f; // Destroy projectile after 10 seconds if stuck/off-map private Coroutine timeoutCoroutine; #endregion #region Components protected Rigidbody2D rb2D; protected Collider2D projectileCollider; #endregion #region Lifecycle /// /// Initialize the projectile with its type and load stats from settings. /// Must be called after instantiation, before Launch. /// public void Initialize(Data.ProjectileType projectileType) { ProjectileType = projectileType; // Load damage and mass from settings var settings = GameManager.GetSettingsObject(); if (settings != null) { var config = settings.GetProjectileConfig(projectileType); if (config != null) { Damage = config.damage; Mass = config.mass; // Update rigidbody mass if already initialized if (rb2D != null) { rb2D.mass = Mass; } Logging.Debug($"[ProjectileBase] Initialized {projectileType} - Damage: {Damage}, Mass: {Mass}"); } else { Logging.Warning($"[ProjectileBase] No config found for {projectileType}, using defaults"); Damage = 20f; Mass = 1f; } } else { Logging.Warning($"[ProjectileBase] Settings not found, using default damage and mass"); Damage = 20f; Mass = 1f; } } internal override void OnManagedAwake() { base.OnManagedAwake(); // Automatically assign projectile to correct layer from settings var settings = GameManager.GetSettingsObject(); if (settings != null && settings.ProjectileLayer >= 0 && gameObject.layer != settings.ProjectileLayer) { gameObject.layer = settings.ProjectileLayer; Logging.Debug($"[ProjectileBase] Assigned {gameObject.name} to layer {LayerMask.LayerToName(settings.ProjectileLayer)}"); } // Cache components rb2D = GetComponent(); projectileCollider = GetComponent(); if (spriteRenderer == null) { spriteRenderer = GetComponent(); } // Configure rigidbody (mass will be set by Initialize if called, otherwise use defaults) if (rb2D != null) { // If Initialize hasn't been called yet, use default mass if (Mass == 0f) { Mass = 1f; Damage = 20f; } rb2D.mass = Mass; rb2D.gravityScale = settings?.ProjectileGravityScale ?? 1f; rb2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous; } } #endregion #region Launch /// /// Launch the projectile with given direction and force. /// Called by SlingshotController. /// public virtual void Launch(Vector2 direction, float force) { if (IsLaunched) { Logging.Warning($"[ProjectileBase] {gameObject.name} already launched!"); return; } LaunchDirection = direction.normalized; LaunchForce = force; IsLaunched = true; // Apply physics impulse if (rb2D != null) { rb2D.AddForce(LaunchDirection * LaunchForce, ForceMode2D.Impulse); // Debug: Log actual mass and resulting velocity for trajectory verification float actualMass = rb2D.mass; float expectedVelocity = LaunchForce / actualMass; Logging.Debug($"[Projectile] Launched {gameObject.name} - Force: {LaunchForce:F2}, Mass: {actualMass:F2}, Expected Velocity: {expectedVelocity:F2}, Dir: {LaunchDirection}"); // After physics applies, log actual velocity (next frame would show it, but we log expectation) // Note: Actual velocity will be set by Unity physics engine as: velocity = impulse / mass } // Fire event OnLaunched?.Invoke(this); // Start timeout - destroy projectile after configured time if it hasn't been destroyed StartTimeoutTimer(); } #endregion #region Timeout /// /// Start timeout timer. Projectile will auto-destroy after timeout to prevent stuck/lost projectiles. /// private void StartTimeoutTimer() { if (timeoutCoroutine != null) { StopCoroutine(timeoutCoroutine); } timeoutCoroutine = StartCoroutine(TimeoutCoroutine()); } /// /// Timeout coroutine - destroys projectile after configured time /// private System.Collections.IEnumerator TimeoutCoroutine() { yield return new WaitForSeconds(ProjectileTimeout); // Only destroy if still exists (might have been destroyed by collision already) if (this != null && gameObject != null) { Logging.Debug($"[ProjectileBase] {gameObject.name} timed out after {ProjectileTimeout}s, destroying..."); DestroyProjectile(); } } #endregion #region Ability /// /// Activate projectile's special ability (mid-flight). /// Override in subclasses for unique behaviors. /// Called when player taps screen during flight. /// public virtual void ActivateAbility() { if (!IsLaunched) { Logging.Warning($"[ProjectileBase] Cannot activate ability - projectile not launched yet!"); return; } if (AbilityActivated) { Logging.Warning($"[ProjectileBase] Ability already activated!"); return; } AbilityActivated = true; Logging.Debug($"[ProjectileBase] {gameObject.name} ability activated"); // Subclasses override this for special behavior } #endregion #region Collision private void OnCollisionEnter2D(Collision2D collision) { if (!IsLaunched) return; Logging.Debug($"[ProjectileBase] {gameObject.name} hit {collision.gameObject.name}"); // Fire impact event OnImpact?.Invoke(this, collision.collider); // Delegate to subclass - they handle everything (damage, effects, destruction) OnHit(collision); } /// /// Called when projectile hits something. /// Override in subclasses to implement full projectile behavior. /// Default implementation: Deal damage to blocks and destroy projectile. /// Subclasses should call DestroyProjectile() when they want to be destroyed. /// /// Collision data including contact points and normals protected virtual void OnHit(Collision2D collision) { // Default behavior: Deal damage to blocks and destroy FortBlock block = collision.gameObject.GetComponent(); if (block != null) { block.TakeDamage(Damage); Logging.Debug($"[ProjectileBase] Dealt {Damage} damage to {block.gameObject.name}"); } // Default: Destroy on hit DestroyProjectile(); } #endregion #region Effects /// /// Spawn impact particle effect /// protected void SpawnImpactEffect(Vector2 position) { if (impactEffectPrefab != null) { GameObject effect = Instantiate(impactEffectPrefab, position, Quaternion.identity); // Dynamically determine cleanup time from particle system float lifetime = GetEffectLifetime(effect); Destroy(effect, lifetime); } } /// /// Get the lifetime of an effect by reading particle system StartLifetime. /// Falls back to 2 seconds if no particle system found. /// private float GetEffectLifetime(GameObject effect) { // Try to read from ParticleSystem ParticleSystem ps = effect.GetComponent(); if (ps != null) { return ps.main.startLifetime.constantMax + 0.5f; // Add small buffer } // Try to read from child particle systems ParticleSystem childPs = effect.GetComponentInChildren(); if (childPs != null) { return childPs.main.startLifetime.constantMax + 0.5f; } // Fallback for non-particle effects (sprites, etc.) return 2f; } #endregion #region Destruction /// /// Destroy the projectile. /// Can be overridden by subclasses for delayed destruction. /// protected virtual void DestroyProjectile() { Logging.Debug($"[ProjectileBase] Destroying {gameObject.name}"); // Stop timeout coroutine if running if (timeoutCoroutine != null) { StopCoroutine(timeoutCoroutine); timeoutCoroutine = null; } // Fire destroyed event OnDestroyed?.Invoke(this); // Destroy GameObject Destroy(gameObject); } #endregion #region Debug private void OnDrawGizmos() { if (IsLaunched && Application.isPlaying) { // Draw launch direction Gizmos.color = Color.yellow; Gizmos.DrawLine(transform.position, transform.position + (Vector3)(LaunchDirection * 2f)); } } #endregion } }