382 lines
13 KiB
C#
382 lines
13 KiB
C#
using System;
|
|
using Core;
|
|
using Core.Lifecycle;
|
|
using Minigames.FortFight.Fort;
|
|
using UnityEngine;
|
|
|
|
namespace Minigames.FortFight.Projectiles
|
|
{
|
|
/// <summary>
|
|
/// Base class for all projectile types in Fort Fight.
|
|
/// Handles physics, collision, and basic damage dealing.
|
|
/// Subclasses override ActivateAbility() and OnHit() for unique behaviors.
|
|
/// </summary>
|
|
[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
|
|
|
|
/// <summary>
|
|
/// Fired when projectile is launched. Parameters: (ProjectileBase projectile)
|
|
/// </summary>
|
|
public event Action<ProjectileBase> OnLaunched;
|
|
|
|
/// <summary>
|
|
/// Fired when projectile hits something. Parameters: (ProjectileBase projectile, Collider2D hit)
|
|
/// </summary>
|
|
public event Action<ProjectileBase, Collider2D> OnImpact;
|
|
|
|
/// <summary>
|
|
/// Fired when projectile is destroyed. Parameters: (ProjectileBase projectile)
|
|
/// </summary>
|
|
public event Action<ProjectileBase> 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
|
|
|
|
/// <summary>
|
|
/// Initialize the projectile with its type and load stats from settings.
|
|
/// Must be called after instantiation, before Launch.
|
|
/// </summary>
|
|
public void Initialize(Data.ProjectileType projectileType)
|
|
{
|
|
ProjectileType = projectileType;
|
|
|
|
// Load damage and mass from settings
|
|
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
|
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<AppleHills.Core.Settings.IFortFightSettings>();
|
|
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<Rigidbody2D>();
|
|
projectileCollider = GetComponent<Collider2D>();
|
|
|
|
if (spriteRenderer == null)
|
|
{
|
|
spriteRenderer = GetComponent<SpriteRenderer>();
|
|
}
|
|
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Launch the projectile with given direction and force.
|
|
/// Called by SlingshotController.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Start timeout timer. Projectile will auto-destroy after timeout to prevent stuck/lost projectiles.
|
|
/// </summary>
|
|
private void StartTimeoutTimer()
|
|
{
|
|
if (timeoutCoroutine != null)
|
|
{
|
|
StopCoroutine(timeoutCoroutine);
|
|
}
|
|
|
|
timeoutCoroutine = StartCoroutine(TimeoutCoroutine());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Timeout coroutine - destroys projectile after configured time
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Activate projectile's special ability (mid-flight).
|
|
/// Override in subclasses for unique behaviors.
|
|
/// Called when player taps screen during flight.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="collision">Collision data including contact points and normals</param>
|
|
protected virtual void OnHit(Collision2D collision)
|
|
{
|
|
// Default behavior: Deal damage to blocks and destroy
|
|
FortBlock block = collision.gameObject.GetComponent<FortBlock>();
|
|
if (block != null)
|
|
{
|
|
block.TakeDamage(Damage);
|
|
Logging.Debug($"[ProjectileBase] Dealt {Damage} damage to {block.gameObject.name}");
|
|
}
|
|
|
|
// Default: Destroy on hit
|
|
DestroyProjectile();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Effects
|
|
|
|
/// <summary>
|
|
/// Spawn impact particle effect
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the lifetime of an effect by reading particle system StartLifetime.
|
|
/// Falls back to 2 seconds if no particle system found.
|
|
/// </summary>
|
|
private float GetEffectLifetime(GameObject effect)
|
|
{
|
|
// Try to read from ParticleSystem
|
|
ParticleSystem ps = effect.GetComponent<ParticleSystem>();
|
|
if (ps != null)
|
|
{
|
|
return ps.main.startLifetime.constantMax + 0.5f; // Add small buffer
|
|
}
|
|
|
|
// Try to read from child particle systems
|
|
ParticleSystem childPs = effect.GetComponentInChildren<ParticleSystem>();
|
|
if (childPs != null)
|
|
{
|
|
return childPs.main.startLifetime.constantMax + 0.5f;
|
|
}
|
|
|
|
// Fallback for non-particle effects (sprites, etc.)
|
|
return 2f;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Destruction
|
|
|
|
/// <summary>
|
|
/// Destroy the projectile.
|
|
/// Can be overridden by subclasses for delayed destruction.
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|
|
|