Implement Fort Fight minigame (#75)

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #75
This commit is contained in:
2025-12-04 01:18:29 +00:00
parent bb8d600af2
commit e60d516e7e
127 changed files with 21544 additions and 128 deletions

View File

@@ -0,0 +1,183 @@
using System.Collections;
using Core;
using UnityEngine;
namespace Minigames.FortFight.Projectiles
{
/// <summary>
/// Ceiling Fan projectile - drops straight down when player taps screen.
/// Implements ITouchInputConsumer to capture tap input mid-flight.
/// </summary>
public class CeilingFanProjectile : ProjectileBase, ITouchInputConsumer
{
[Header("Ceiling Fan Specific")]
[Tooltip("Visual indicator showing drop is available (arrow down)")]
[SerializeField] private GameObject indicator;
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()
{
base.ActivateAbility();
if (AbilityActivated)
{
Logging.Debug("[CeilingFanProjectile] Ability activated - dropping straight down");
StartCoroutine(DropCoroutine());
}
}
private IEnumerator DropCoroutine()
{
isDropping = true;
// Stop all velocity
if (rb2D != null)
{
rb2D.linearVelocity = Vector2.zero;
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);
// Drop straight down
if (rb2D != null)
{
rb2D.linearVelocity = Vector2.down * dropSpeed;
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();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e10ba9bd4bcd40da87ecb3efe5b78467
timeCreated: 1764682337

View File

@@ -0,0 +1,381 @@
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
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 70f37c48406847cdabd5589910220fdf
timeCreated: 1764682302

View File

@@ -0,0 +1,39 @@
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
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
Logging.Debug("[ToasterProjectile] Toaster has no special ability");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6ecf658b5965496abda845de1a28e227
timeCreated: 1764682312

View File

@@ -0,0 +1,107 @@
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("Prefab for individual trash pieces (small debris)")]
[SerializeField] private GameObject trashPiecePrefab;
protected override void OnHit(Collision2D collision)
{
// 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}");
}
// 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 cone away from the hit surface.
/// Uses hit normal + projectile momentum for realistic splash effect.
/// </summary>
private void SpawnTrashPieces(Vector2 impactPoint, Vector2 hitNormal)
{
if (trashPiecePrefab == null)
{
Logging.Warning("[TrashBagProjectile] No trash piece prefab assigned!");
return;
}
// 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)
{
incomingDirection = LaunchDirection;
}
// 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 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 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)
{
// 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));
}
Logging.Debug($"[TrashBagProjectile] Spawned trash piece {i} in direction {pieceDirection}");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b0996e59b91e48f8a542ab9294b11a74
timeCreated: 1764682467

View File

@@ -0,0 +1,84 @@
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);
}
// Destroy trash piece immediately after dealing damage
Destroy(gameObject);
}
}
/// <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;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 516daf2ce7384aaa94fd5e0f7a3cf078
timeCreated: 1764756385

View File

@@ -0,0 +1,105 @@
using Core;
using UnityEngine;
namespace Minigames.FortFight.Projectiles
{
/// <summary>
/// Vacuum projectile - high mass, slides along ground after landing.
/// On floor/block impact: applies constant force to the right and destroys blocks.
/// </summary>
public class VacuumProjectile : ProjectileBase
{
private bool isSliding = false;
private int blocksDestroyed = 0;
private int maxBlocksToDestroy = 3;
private Vector2 slideDirection;
protected override void OnHit(Collision2D collision)
{
// If already sliding, count block destruction
if (isSliding)
{
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;
}
// 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 surface
/// </summary>
private void StartSliding()
{
if (isSliding) return;
isSliding = true;
// Get settings
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
if (settings != null)
{
maxBlocksToDestroy = settings.VacuumDestroyBlockCount;
}
// 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 void FixedUpdate()
{
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>
/// Clean up when destroyed
/// </summary>
protected override void DestroyProjectile()
{
isSliding = false;
base.DestroyProjectile();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bb85d181808c411b8bd1335aa7d35257
timeCreated: 1764682326