Working MVP
This commit is contained in:
@@ -5,17 +5,59 @@ using UnityEngine;
|
||||
namespace Minigames.FortFight.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Ceiling Fan projectile - drops straight down when ability is activated.
|
||||
/// Player taps screen mid-flight to activate drop.
|
||||
/// Ceiling Fan projectile - drops straight down when player taps screen.
|
||||
/// Implements ITouchInputConsumer to capture tap input mid-flight.
|
||||
/// </summary>
|
||||
public class CeilingFanProjectile : ProjectileBase
|
||||
public class CeilingFanProjectile : ProjectileBase, ITouchInputConsumer
|
||||
{
|
||||
[Header("Ceiling Fan Specific")]
|
||||
[Tooltip("Speed of downward drop")]
|
||||
[SerializeField] private float dropSpeed = 20f;
|
||||
[Tooltip("Visual indicator showing drop is available (arrow down)")]
|
||||
[SerializeField] private GameObject indicator;
|
||||
|
||||
[Tooltip("Delay before dropping")]
|
||||
[SerializeField] private float dropDelay = 0.2f;
|
||||
private bool isDropping = false;
|
||||
private bool inputEnabled = false;
|
||||
|
||||
public override void Launch(Vector2 direction, float force)
|
||||
{
|
||||
base.Launch(direction, force);
|
||||
|
||||
// Hide indicator initially
|
||||
if (indicator != null)
|
||||
{
|
||||
indicator.SetActive(false);
|
||||
}
|
||||
|
||||
// Start activation delay coroutine
|
||||
StartCoroutine(ActivationDelayCoroutine());
|
||||
}
|
||||
|
||||
private IEnumerator ActivationDelayCoroutine()
|
||||
{
|
||||
// Get activation delay from settings
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
float activationDelay = settings?.CeilingFanActivationDelay ?? 0.5f;
|
||||
|
||||
// Wait for delay
|
||||
yield return new WaitForSeconds(activationDelay);
|
||||
|
||||
// Enable input and show indicator (if not already dropped)
|
||||
if (!isDropping && !AbilityActivated)
|
||||
{
|
||||
inputEnabled = true;
|
||||
|
||||
if (indicator != null)
|
||||
{
|
||||
indicator.SetActive(true);
|
||||
}
|
||||
|
||||
// Register with InputManager to capture tap-to-drop
|
||||
if (Input.InputManager.Instance != null)
|
||||
{
|
||||
Input.InputManager.Instance.RegisterOverrideConsumer(this);
|
||||
Logging.Debug("[CeilingFanProjectile] Tap-to-drop now available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void ActivateAbility()
|
||||
{
|
||||
@@ -30,6 +72,8 @@ namespace Minigames.FortFight.Projectiles
|
||||
|
||||
private IEnumerator DropCoroutine()
|
||||
{
|
||||
isDropping = true;
|
||||
|
||||
// Stop all velocity
|
||||
if (rb2D != null)
|
||||
{
|
||||
@@ -37,6 +81,11 @@ namespace Minigames.FortFight.Projectiles
|
||||
rb2D.angularVelocity = 0f;
|
||||
}
|
||||
|
||||
// Get drop configuration from settings
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
float dropDelay = settings?.CeilingFanDropDelay ?? 0.2f;
|
||||
float dropSpeed = settings?.CeilingFanDropSpeed ?? 20f;
|
||||
|
||||
// Wait brief moment
|
||||
yield return new WaitForSeconds(dropDelay);
|
||||
|
||||
@@ -47,6 +96,88 @@ namespace Minigames.FortFight.Projectiles
|
||||
Logging.Debug($"[CeilingFanProjectile] Dropping with velocity: {rb2D.linearVelocity}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnHit(Collision2D collision)
|
||||
{
|
||||
// Spawn impact effect only if dropped (not on normal arc hit)
|
||||
if (isDropping)
|
||||
{
|
||||
SpawnImpactEffect(collision.contacts[0].point);
|
||||
}
|
||||
|
||||
// Deal damage to blocks
|
||||
var block = collision.gameObject.GetComponent<Fort.FortBlock>();
|
||||
if (block != null)
|
||||
{
|
||||
block.TakeDamage(Damage);
|
||||
Logging.Debug($"[CeilingFanProjectile] Dealt {Damage} damage to {block.gameObject.name}");
|
||||
}
|
||||
|
||||
// Destroy projectile
|
||||
DestroyProjectile();
|
||||
}
|
||||
|
||||
#region ITouchInputConsumer Implementation
|
||||
|
||||
public void OnTap(Vector2 worldPosition)
|
||||
{
|
||||
// Only respond if input is enabled
|
||||
if (inputEnabled && !AbilityActivated && !isDropping)
|
||||
{
|
||||
Logging.Debug("[CeilingFanProjectile] Tap detected - activating drop");
|
||||
|
||||
// Hide indicator
|
||||
if (indicator != null)
|
||||
{
|
||||
indicator.SetActive(false);
|
||||
}
|
||||
|
||||
ActivateAbility();
|
||||
|
||||
// Unregister immediately after tap
|
||||
UnregisterFromInput();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnHoldStart(Vector2 worldPosition)
|
||||
{
|
||||
// Not used for ceiling fan
|
||||
}
|
||||
|
||||
public void OnHoldMove(Vector2 worldPosition)
|
||||
{
|
||||
// Not used for ceiling fan
|
||||
}
|
||||
|
||||
public void OnHoldEnd(Vector2 worldPosition)
|
||||
{
|
||||
// Not used for ceiling fan
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void UnregisterFromInput()
|
||||
{
|
||||
inputEnabled = false;
|
||||
|
||||
if (indicator != null)
|
||||
{
|
||||
indicator.SetActive(false);
|
||||
}
|
||||
|
||||
if (Input.InputManager.Instance != null)
|
||||
{
|
||||
Input.InputManager.Instance.UnregisterOverrideConsumer(this);
|
||||
Logging.Debug("[CeilingFanProjectile] Unregistered from input");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DestroyProjectile()
|
||||
{
|
||||
// Make sure we unregister when destroyed
|
||||
UnregisterFromInput();
|
||||
base.DestroyProjectile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,6 @@ namespace Minigames.FortFight.Projectiles
|
||||
{
|
||||
#region Inspector Properties
|
||||
|
||||
[Header("Projectile Stats")]
|
||||
[Tooltip("Base damage dealt on impact")]
|
||||
[SerializeField] protected float damage = 20f;
|
||||
|
||||
[Tooltip("Mass for physics (affects trajectory)")]
|
||||
[SerializeField] protected float mass = 1f;
|
||||
|
||||
[Header("Visuals")]
|
||||
[Tooltip("Sprite renderer for projectile")]
|
||||
[SerializeField] protected SpriteRenderer spriteRenderer;
|
||||
@@ -54,7 +47,10 @@ namespace Minigames.FortFight.Projectiles
|
||||
|
||||
#region Properties
|
||||
|
||||
public float Damage => damage;
|
||||
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; }
|
||||
@@ -71,10 +67,59 @@ namespace Minigames.FortFight.Projectiles
|
||||
|
||||
#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>();
|
||||
@@ -84,11 +129,18 @@ namespace Minigames.FortFight.Projectiles
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
// Configure rigidbody
|
||||
// Configure rigidbody (mass will be set by Initialize if called, otherwise use defaults)
|
||||
if (rb2D != null)
|
||||
{
|
||||
rb2D.mass = mass;
|
||||
rb2D.gravityScale = 1f;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -117,7 +169,14 @@ namespace Minigames.FortFight.Projectiles
|
||||
if (rb2D != null)
|
||||
{
|
||||
rb2D.AddForce(LaunchDirection * LaunchForce, ForceMode2D.Impulse);
|
||||
Logging.Debug($"[ProjectileBase] Launched {gameObject.name} with force {LaunchForce} in direction {LaunchDirection}");
|
||||
|
||||
// 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
|
||||
@@ -163,36 +222,32 @@ namespace Minigames.FortFight.Projectiles
|
||||
|
||||
Logging.Debug($"[ProjectileBase] {gameObject.name} hit {collision.gameObject.name}");
|
||||
|
||||
// Check if hit a fort block
|
||||
FortBlock block = collision.gameObject.GetComponent<FortBlock>();
|
||||
if (block != null)
|
||||
{
|
||||
// Deal damage to block
|
||||
block.TakeDamage(damage);
|
||||
Logging.Debug($"[ProjectileBase] Dealt {damage} damage to {block.gameObject.name}");
|
||||
}
|
||||
|
||||
// Spawn impact effect
|
||||
SpawnImpactEffect(collision.contacts[0].point);
|
||||
|
||||
// Fire impact event
|
||||
OnImpact?.Invoke(this, collision.collider);
|
||||
|
||||
// Call subclass-specific hit behavior
|
||||
OnHit(collision.collider);
|
||||
|
||||
// Destroy projectile after hit (subclasses can override)
|
||||
DestroyProjectile();
|
||||
// Delegate to subclass - they handle everything (damage, effects, destruction)
|
||||
OnHit(collision);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when projectile hits something.
|
||||
/// Override in subclasses for projectile-specific behavior.
|
||||
/// 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>
|
||||
protected virtual void OnHit(Collider2D hit)
|
||||
/// <param name="collision">Collision data including contact points and normals</param>
|
||||
protected virtual void OnHit(Collision2D collision)
|
||||
{
|
||||
// Subclasses override for special behavior
|
||||
// e.g., Vacuum continues sliding, TrashBag splits, etc.
|
||||
// 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
|
||||
@@ -207,10 +262,37 @@ namespace Minigames.FortFight.Projectiles
|
||||
if (impactEffectPrefab != null)
|
||||
{
|
||||
GameObject effect = Instantiate(impactEffectPrefab, position, Quaternion.identity);
|
||||
Destroy(effect, 2f); // Auto-cleanup
|
||||
|
||||
// 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
|
||||
|
||||
@@ -12,6 +12,23 @@ namespace Minigames.FortFight.Projectiles
|
||||
// Toaster is the basic projectile - uses base class behavior
|
||||
// No special ability needed
|
||||
|
||||
protected override void OnHit(Collision2D collision)
|
||||
{
|
||||
// Spawn impact effect
|
||||
SpawnImpactEffect(collision.contacts[0].point);
|
||||
|
||||
// Deal damage to blocks
|
||||
var block = collision.gameObject.GetComponent<Fort.FortBlock>();
|
||||
if (block != null)
|
||||
{
|
||||
block.TakeDamage(Damage);
|
||||
Logging.Debug($"[ToasterProjectile] Dealt {Damage} damage to {block.gameObject.name}");
|
||||
}
|
||||
|
||||
// Destroy projectile
|
||||
DestroyProjectile();
|
||||
}
|
||||
|
||||
public override void ActivateAbility()
|
||||
{
|
||||
// Toaster has no special ability
|
||||
|
||||
@@ -10,33 +10,41 @@ namespace Minigames.FortFight.Projectiles
|
||||
public class TrashBagProjectile : ProjectileBase
|
||||
{
|
||||
[Header("Trash Bag Specific")]
|
||||
[Tooltip("Number of trash pieces to spawn on impact")]
|
||||
[SerializeField] private int trashPieceCount = 6;
|
||||
|
||||
[Tooltip("Prefab for individual trash pieces")]
|
||||
[Tooltip("Prefab for individual trash pieces (small debris)")]
|
||||
[SerializeField] private GameObject trashPiecePrefab;
|
||||
|
||||
[Tooltip("Force applied to each trash piece")]
|
||||
[SerializeField] private float pieceForce = 8f;
|
||||
|
||||
[Tooltip("Spread angle for trash pieces (degrees)")]
|
||||
[SerializeField] private float spreadAngle = 30f;
|
||||
|
||||
[Tooltip("Damage dealt by each trash piece")]
|
||||
[SerializeField] private float pieceDamage = 8f;
|
||||
|
||||
protected override void OnHit(Collider2D hit)
|
||||
protected override void OnHit(Collision2D collision)
|
||||
{
|
||||
base.OnHit(hit);
|
||||
// Deal initial damage from trash bag itself
|
||||
var block = collision.gameObject.GetComponent<Fort.FortBlock>();
|
||||
if (block != null)
|
||||
{
|
||||
block.TakeDamage(Damage);
|
||||
Logging.Debug($"[TrashBagProjectile] Dealt {Damage} damage to {block.gameObject.name}");
|
||||
}
|
||||
|
||||
Logging.Debug($"[TrashBagProjectile] Splitting into {trashPieceCount} pieces");
|
||||
SpawnTrashPieces(hit.transform.position);
|
||||
// Get settings for trash pieces
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
int pieceCount = settings?.TrashBagPieceCount ?? 8;
|
||||
|
||||
Logging.Debug($"[TrashBagProjectile] Splitting into {pieceCount} pieces");
|
||||
|
||||
// Get contact normal and impact point
|
||||
Vector2 hitNormal = collision.contacts[0].normal;
|
||||
Vector2 impactPoint = collision.contacts[0].point;
|
||||
|
||||
// Spawn trash pieces (NOT parented, so they persist as debris)
|
||||
SpawnTrashPieces(impactPoint, hitNormal);
|
||||
|
||||
// Destroy trash bag after spawning pieces
|
||||
DestroyProjectile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn multiple trash pieces in a forward cone
|
||||
/// Spawn multiple trash pieces in a cone away from the hit surface.
|
||||
/// Uses hit normal + projectile momentum for realistic splash effect.
|
||||
/// </summary>
|
||||
private void SpawnTrashPieces(Vector2 impactPoint)
|
||||
private void SpawnTrashPieces(Vector2 impactPoint, Vector2 hitNormal)
|
||||
{
|
||||
if (trashPiecePrefab == null)
|
||||
{
|
||||
@@ -44,77 +52,56 @@ namespace Minigames.FortFight.Projectiles
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate forward direction from velocity
|
||||
Vector2 forwardDirection = rb2D.linearVelocity.normalized;
|
||||
if (forwardDirection == Vector2.zero)
|
||||
// Get settings
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
int pieceCount = settings?.TrashBagPieceCount ?? 8;
|
||||
float pieceForce = settings?.TrashBagPieceForce ?? 10f;
|
||||
float spreadAngle = settings?.TrashBagSpreadAngle ?? 60f;
|
||||
|
||||
// Calculate projectile's incoming direction (momentum)
|
||||
Vector2 incomingDirection = rb2D.linearVelocity.normalized;
|
||||
if (incomingDirection == Vector2.zero)
|
||||
{
|
||||
forwardDirection = LaunchDirection;
|
||||
incomingDirection = LaunchDirection;
|
||||
}
|
||||
|
||||
// Spawn pieces in a cone
|
||||
for (int i = 0; i < trashPieceCount; i++)
|
||||
// Calculate reflection direction from hit normal
|
||||
// This creates a bounce-like effect
|
||||
Vector2 reflectDirection = Vector2.Reflect(incomingDirection, hitNormal);
|
||||
|
||||
// Blend between reflection and pure normal for more variety
|
||||
// 70% normal (splash away from surface) + 30% reflection (maintain some momentum direction)
|
||||
Vector2 baseDirection = (hitNormal * 0.7f + reflectDirection * 0.3f).normalized;
|
||||
|
||||
// Spawn pieces in a cone around the base direction
|
||||
for (int i = 0; i < pieceCount; i++)
|
||||
{
|
||||
// Calculate angle for this piece
|
||||
float angleOffset = Mathf.Lerp(-spreadAngle, spreadAngle, i / (float)(trashPieceCount - 1));
|
||||
float angleRadians = Mathf.Atan2(forwardDirection.y, forwardDirection.x) + angleOffset * Mathf.Deg2Rad;
|
||||
// Calculate angle offset for this piece within the spread cone
|
||||
float t = pieceCount > 1 ? i / (float)(pieceCount - 1) : 0.5f;
|
||||
float angleOffset = Mathf.Lerp(-spreadAngle / 2f, spreadAngle / 2f, t);
|
||||
float angleRadians = Mathf.Atan2(baseDirection.y, baseDirection.x) + angleOffset * Mathf.Deg2Rad;
|
||||
|
||||
Vector2 pieceDirection = new Vector2(Mathf.Cos(angleRadians), Mathf.Sin(angleRadians));
|
||||
|
||||
// Spawn trash piece
|
||||
GameObject piece = Instantiate(trashPiecePrefab, impactPoint, Quaternion.identity);
|
||||
// Spawn trash piece slightly offset from impact point
|
||||
Vector2 spawnOffset = pieceDirection * 0.2f; // Small offset to prevent clipping
|
||||
GameObject piece = Instantiate(trashPiecePrefab, (Vector2)impactPoint + spawnOffset, Quaternion.identity);
|
||||
|
||||
// Setup trash piece physics
|
||||
Rigidbody2D pieceRb = piece.GetComponent<Rigidbody2D>();
|
||||
if (pieceRb != null)
|
||||
{
|
||||
pieceRb.AddForce(pieceDirection * pieceForce, ForceMode2D.Impulse);
|
||||
// Apply force with some randomness for more natural spread
|
||||
float randomForce = pieceForce * Random.Range(0.8f, 1.2f);
|
||||
pieceRb.AddForce(pieceDirection * randomForce, ForceMode2D.Impulse);
|
||||
|
||||
// Add some random spin
|
||||
pieceRb.AddTorque(Random.Range(-100f, 100f));
|
||||
}
|
||||
|
||||
// Setup trash piece damage (if it has a component)
|
||||
TrashPiece trashPieceComponent = piece.GetComponent<TrashPiece>();
|
||||
if (trashPieceComponent != null)
|
||||
{
|
||||
trashPieceComponent.Initialize(pieceDamage);
|
||||
}
|
||||
|
||||
// Auto-destroy after 3 seconds
|
||||
Destroy(piece, 3f);
|
||||
|
||||
Logging.Debug($"[TrashBagProjectile] Spawned trash piece {i} in direction {pieceDirection}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Component for individual trash pieces spawned by TrashBagProjectile.
|
||||
/// Deals damage on collision.
|
||||
/// </summary>
|
||||
public class TrashPiece : MonoBehaviour
|
||||
{
|
||||
private float damage;
|
||||
private bool hasHit = false;
|
||||
|
||||
public void Initialize(float pieceDamage)
|
||||
{
|
||||
this.damage = pieceDamage;
|
||||
}
|
||||
|
||||
private void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
if (hasHit) return;
|
||||
hasHit = true;
|
||||
|
||||
// Check if hit a fort block
|
||||
var block = collision.gameObject.GetComponent<Fort.FortBlock>();
|
||||
if (block != null)
|
||||
{
|
||||
block.TakeDamage(damage);
|
||||
Logging.Debug($"[TrashPiece] Dealt {damage} damage to {block.gameObject.name}");
|
||||
}
|
||||
|
||||
// Destroy this piece
|
||||
Destroy(gameObject, 0.1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
81
Assets/Scripts/Minigames/FortFight/Projectiles/TrashPiece.cs
Normal file
81
Assets/Scripts/Minigames/FortFight/Projectiles/TrashPiece.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.FortFight.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Component for individual trash pieces spawned by TrashBagProjectile.
|
||||
/// Deals pre-configured damage on collision with blocks, spawns impact effect, then auto-cleans up after timeout.
|
||||
/// </summary>
|
||||
public class TrashPiece : MonoBehaviour
|
||||
{
|
||||
[Header("Visual Effects")]
|
||||
[Tooltip("Impact effect prefab spawned on block collision")]
|
||||
[SerializeField] private GameObject impactEffectPrefab;
|
||||
|
||||
private float damage;
|
||||
private bool hasHit = false;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Get configuration from settings
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
damage = settings?.TrashPieceDamage ?? 5f;
|
||||
float lifetime = settings?.TrashPieceLifetime ?? 5f;
|
||||
|
||||
// Auto-cleanup after configured timeout
|
||||
Destroy(gameObject, lifetime);
|
||||
}
|
||||
|
||||
private void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
if (hasHit) return;
|
||||
|
||||
// Check if hit a fort block
|
||||
var block = collision.gameObject.GetComponent<Fort.FortBlock>();
|
||||
if (block != null)
|
||||
{
|
||||
hasHit = true;
|
||||
|
||||
// Deal damage
|
||||
block.TakeDamage(damage);
|
||||
Logging.Debug($"[TrashPiece] Dealt {damage} damage to {block.gameObject.name}");
|
||||
|
||||
// Spawn impact effect at collision point
|
||||
if (impactEffectPrefab != null && collision.contacts.Length > 0)
|
||||
{
|
||||
Vector2 impactPoint = collision.contacts[0].point;
|
||||
GameObject effect = Instantiate(impactEffectPrefab, impactPoint, 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
|
||||
return 2f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 516daf2ce7384aaa94fd5e0f7a3cf078
|
||||
timeCreated: 1764756385
|
||||
@@ -1,40 +1,59 @@
|
||||
using System.Collections;
|
||||
using Core;
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.FortFight.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Vacuum projectile - high mass, slides along ground after landing.
|
||||
/// On ground impact: disables gravity, moves horizontally for 2 seconds.
|
||||
/// On floor/block impact: applies constant force to the right and destroys blocks.
|
||||
/// </summary>
|
||||
public class VacuumProjectile : ProjectileBase
|
||||
{
|
||||
[Header("Vacuum Specific")]
|
||||
[Tooltip("Speed when sliding on ground")]
|
||||
[SerializeField] private float slideSpeed = 10f;
|
||||
|
||||
[Tooltip("How long to slide before destroying")]
|
||||
[SerializeField] private float slideDuration = 2f;
|
||||
|
||||
[Tooltip("Layer mask for ground detection")]
|
||||
[SerializeField] private LayerMask groundLayer = 1; // Default layer
|
||||
|
||||
private bool isSliding = false;
|
||||
private int blocksDestroyed = 0;
|
||||
private int maxBlocksToDestroy = 3;
|
||||
private Vector2 slideDirection;
|
||||
|
||||
protected override void OnHit(Collider2D hit)
|
||||
protected override void OnHit(Collision2D collision)
|
||||
{
|
||||
// Check if hit ground
|
||||
if (((1 << hit.gameObject.layer) & groundLayer) != 0)
|
||||
// If already sliding, count block destruction
|
||||
if (isSliding)
|
||||
{
|
||||
Logging.Debug("[VacuumProjectile] Hit ground - starting slide");
|
||||
StartSliding();
|
||||
var block = collision.gameObject.GetComponent<Fort.FortBlock>();
|
||||
if (block != null)
|
||||
{
|
||||
// Spawn impact effect on each block hit
|
||||
SpawnImpactEffect(collision.contacts[0].point);
|
||||
|
||||
// Get damage from settings
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
float blockDamage = settings?.VacuumBlockDamage ?? 999f;
|
||||
|
||||
// Deal high damage to destroy block instantly
|
||||
block.TakeDamage(blockDamage);
|
||||
blocksDestroyed++;
|
||||
|
||||
Logging.Debug($"[VacuumProjectile] Destroyed block {blocksDestroyed}/{maxBlocksToDestroy}");
|
||||
|
||||
if (blocksDestroyed >= maxBlocksToDestroy)
|
||||
{
|
||||
Logging.Debug("[VacuumProjectile] Destroyed max blocks - stopping");
|
||||
DestroyProjectile();
|
||||
}
|
||||
}
|
||||
// Don't destroy - keep sliding
|
||||
return;
|
||||
}
|
||||
// If hit wall or fort block, destroy immediately (handled by base class)
|
||||
|
||||
// First hit - spawn impact effect and start sliding
|
||||
SpawnImpactEffect(collision.contacts[0].point);
|
||||
Logging.Debug("[VacuumProjectile] Hit surface - starting slide");
|
||||
StartSliding();
|
||||
// Don't destroy - keep sliding!
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start sliding behavior after hitting ground
|
||||
/// Start sliding behavior after hitting surface
|
||||
/// </summary>
|
||||
private void StartSliding()
|
||||
{
|
||||
@@ -42,40 +61,44 @@ namespace Minigames.FortFight.Projectiles
|
||||
|
||||
isSliding = true;
|
||||
|
||||
// Disable gravity
|
||||
if (rb2D != null)
|
||||
// Get settings
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
if (settings != null)
|
||||
{
|
||||
rb2D.gravityScale = 0f;
|
||||
|
||||
// Set velocity to horizontal only (preserve direction)
|
||||
Vector2 horizontalVelocity = new Vector2(rb2D.linearVelocity.x, 0f).normalized * slideSpeed;
|
||||
rb2D.linearVelocity = horizontalVelocity;
|
||||
|
||||
Logging.Debug($"[VacuumProjectile] Sliding with velocity: {horizontalVelocity}");
|
||||
maxBlocksToDestroy = settings.VacuumDestroyBlockCount;
|
||||
}
|
||||
|
||||
// Destroy after slide duration
|
||||
StartCoroutine(SlideCoroutine());
|
||||
// Determine slide direction based on horizontal velocity (preserve launch direction)
|
||||
if (rb2D != null)
|
||||
{
|
||||
slideDirection = rb2D.linearVelocity.x >= 0 ? Vector2.right : Vector2.left;
|
||||
|
||||
rb2D.gravityScale = 0f;
|
||||
rb2D.linearVelocity = Vector2.zero; // Stop all momentum
|
||||
|
||||
Logging.Debug($"[VacuumProjectile] Started sliding in direction: {slideDirection}");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SlideCoroutine()
|
||||
private void FixedUpdate()
|
||||
{
|
||||
yield return new WaitForSeconds(slideDuration);
|
||||
|
||||
Logging.Debug("[VacuumProjectile] Slide duration ended - destroying");
|
||||
DestroyProjectile();
|
||||
if (isSliding && rb2D != null)
|
||||
{
|
||||
// Set constant velocity in slide direction
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
float slideSpeed = settings?.VacuumSlideSpeed ?? 10f;
|
||||
|
||||
rb2D.linearVelocity = slideDirection * slideSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to NOT destroy immediately on ground hit
|
||||
/// Clean up when destroyed
|
||||
/// </summary>
|
||||
protected override void DestroyProjectile()
|
||||
{
|
||||
// Only destroy if we're done sliding or hit a wall
|
||||
if (!isSliding || rb2D.linearVelocity.magnitude < 1f)
|
||||
{
|
||||
base.DestroyProjectile();
|
||||
}
|
||||
isSliding = false;
|
||||
base.DestroyProjectile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user