Stash work
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
using System.Collections;
|
||||
using Core;
|
||||
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.
|
||||
/// </summary>
|
||||
public class CeilingFanProjectile : ProjectileBase
|
||||
{
|
||||
[Header("Ceiling Fan Specific")]
|
||||
[Tooltip("Speed of downward drop")]
|
||||
[SerializeField] private float dropSpeed = 20f;
|
||||
|
||||
[Tooltip("Delay before dropping")]
|
||||
[SerializeField] private float dropDelay = 0.2f;
|
||||
|
||||
public override void ActivateAbility()
|
||||
{
|
||||
base.ActivateAbility();
|
||||
|
||||
if (AbilityActivated)
|
||||
{
|
||||
Logging.Debug("[CeilingFanProjectile] Ability activated - dropping straight down");
|
||||
StartCoroutine(DropCoroutine());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DropCoroutine()
|
||||
{
|
||||
// Stop all velocity
|
||||
if (rb2D != null)
|
||||
{
|
||||
rb2D.linearVelocity = Vector2.zero;
|
||||
rb2D.angularVelocity = 0f;
|
||||
}
|
||||
|
||||
// Wait brief moment
|
||||
yield return new WaitForSeconds(dropDelay);
|
||||
|
||||
// Drop straight down
|
||||
if (rb2D != null)
|
||||
{
|
||||
rb2D.linearVelocity = Vector2.down * dropSpeed;
|
||||
Logging.Debug($"[CeilingFanProjectile] Dropping with velocity: {rb2D.linearVelocity}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e10ba9bd4bcd40da87ecb3efe5b78467
|
||||
timeCreated: 1764682337
|
||||
250
Assets/Scripts/Minigames/FortFight/Projectiles/ProjectileBase.cs
Normal file
250
Assets/Scripts/Minigames/FortFight/Projectiles/ProjectileBase.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
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("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;
|
||||
|
||||
[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 => damage;
|
||||
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 Components
|
||||
|
||||
protected Rigidbody2D rb2D;
|
||||
protected Collider2D projectileCollider;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Cache components
|
||||
rb2D = GetComponent<Rigidbody2D>();
|
||||
projectileCollider = GetComponent<Collider2D>();
|
||||
|
||||
if (spriteRenderer == null)
|
||||
{
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
// Configure rigidbody
|
||||
if (rb2D != null)
|
||||
{
|
||||
rb2D.mass = mass;
|
||||
rb2D.gravityScale = 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);
|
||||
Logging.Debug($"[ProjectileBase] Launched {gameObject.name} with force {LaunchForce} in direction {LaunchDirection}");
|
||||
}
|
||||
|
||||
// Fire event
|
||||
OnLaunched?.Invoke(this);
|
||||
}
|
||||
|
||||
#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}");
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when projectile hits something.
|
||||
/// Override in subclasses for projectile-specific behavior.
|
||||
/// </summary>
|
||||
protected virtual void OnHit(Collider2D hit)
|
||||
{
|
||||
// Subclasses override for special behavior
|
||||
// e.g., Vacuum continues sliding, TrashBag splits, etc.
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Effects
|
||||
|
||||
/// <summary>
|
||||
/// Spawn impact particle effect
|
||||
/// </summary>
|
||||
protected void SpawnImpactEffect(Vector2 position)
|
||||
{
|
||||
if (impactEffectPrefab != null)
|
||||
{
|
||||
GameObject effect = Instantiate(impactEffectPrefab, position, Quaternion.identity);
|
||||
Destroy(effect, 2f); // Auto-cleanup
|
||||
}
|
||||
}
|
||||
|
||||
#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}");
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70f37c48406847cdabd5589910220fdf
|
||||
timeCreated: 1764682302
|
||||
@@ -0,0 +1,22 @@
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.FortFight.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Standard projectile - no special ability.
|
||||
/// Moderate damage, standard physics arc.
|
||||
/// </summary>
|
||||
public class ToasterProjectile : ProjectileBase
|
||||
{
|
||||
// Toaster is the basic projectile - uses base class behavior
|
||||
// No special ability needed
|
||||
|
||||
public override void ActivateAbility()
|
||||
{
|
||||
// Toaster has no special ability
|
||||
Logging.Debug("[ToasterProjectile] Toaster has no special ability");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ecf658b5965496abda845de1a28e227
|
||||
timeCreated: 1764682312
|
||||
@@ -0,0 +1,120 @@
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.FortFight.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Trash Bag projectile - splits into multiple smaller pieces on impact.
|
||||
/// Deals AOE damage in a forward cone.
|
||||
/// </summary>
|
||||
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")]
|
||||
[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)
|
||||
{
|
||||
base.OnHit(hit);
|
||||
|
||||
Logging.Debug($"[TrashBagProjectile] Splitting into {trashPieceCount} pieces");
|
||||
SpawnTrashPieces(hit.transform.position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn multiple trash pieces in a forward cone
|
||||
/// </summary>
|
||||
private void SpawnTrashPieces(Vector2 impactPoint)
|
||||
{
|
||||
if (trashPiecePrefab == null)
|
||||
{
|
||||
Logging.Warning("[TrashBagProjectile] No trash piece prefab assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate forward direction from velocity
|
||||
Vector2 forwardDirection = rb2D.linearVelocity.normalized;
|
||||
if (forwardDirection == Vector2.zero)
|
||||
{
|
||||
forwardDirection = LaunchDirection;
|
||||
}
|
||||
|
||||
// Spawn pieces in a cone
|
||||
for (int i = 0; i < trashPieceCount; 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;
|
||||
|
||||
Vector2 pieceDirection = new Vector2(Mathf.Cos(angleRadians), Mathf.Sin(angleRadians));
|
||||
|
||||
// Spawn trash piece
|
||||
GameObject piece = Instantiate(trashPiecePrefab, impactPoint, Quaternion.identity);
|
||||
|
||||
// Setup trash piece physics
|
||||
Rigidbody2D pieceRb = piece.GetComponent<Rigidbody2D>();
|
||||
if (pieceRb != null)
|
||||
{
|
||||
pieceRb.AddForce(pieceDirection * pieceForce, ForceMode2D.Impulse);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0996e59b91e48f8a542ab9294b11a74
|
||||
timeCreated: 1764682467
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Collections;
|
||||
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.
|
||||
/// </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;
|
||||
|
||||
protected override void OnHit(Collider2D hit)
|
||||
{
|
||||
// Check if hit ground
|
||||
if (((1 << hit.gameObject.layer) & groundLayer) != 0)
|
||||
{
|
||||
Logging.Debug("[VacuumProjectile] Hit ground - starting slide");
|
||||
StartSliding();
|
||||
}
|
||||
// If hit wall or fort block, destroy immediately (handled by base class)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start sliding behavior after hitting ground
|
||||
/// </summary>
|
||||
private void StartSliding()
|
||||
{
|
||||
if (isSliding) return;
|
||||
|
||||
isSliding = true;
|
||||
|
||||
// Disable gravity
|
||||
if (rb2D != 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}");
|
||||
}
|
||||
|
||||
// Destroy after slide duration
|
||||
StartCoroutine(SlideCoroutine());
|
||||
}
|
||||
|
||||
private IEnumerator SlideCoroutine()
|
||||
{
|
||||
yield return new WaitForSeconds(slideDuration);
|
||||
|
||||
Logging.Debug("[VacuumProjectile] Slide duration ended - destroying");
|
||||
DestroyProjectile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to NOT destroy immediately on ground hit
|
||||
/// </summary>
|
||||
protected override void DestroyProjectile()
|
||||
{
|
||||
// Only destroy if we're done sliding or hit a wall
|
||||
if (!isSliding || rb2D.linearVelocity.magnitude < 1f)
|
||||
{
|
||||
base.DestroyProjectile();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb85d181808c411b8bd1335aa7d35257
|
||||
timeCreated: 1764682326
|
||||
Reference in New Issue
Block a user