MVP of the plane throwing game (#77)

Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #77
This commit is contained in:
2025-12-07 19:36:57 +00:00
parent ad8338f37e
commit c27f22ef0a
128 changed files with 15474 additions and 1589 deletions

View File

@@ -0,0 +1,123 @@
using UnityEngine;
namespace Minigames.Airplane.Interactive
{
/// <summary>
/// Bounces airplanes on collision with configurable bounce multiplier.
/// Can be used for trampolines, bounce pads, or elastic surfaces.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class AirplaneBouncySurface : MonoBehaviour
{
[Header("Bounce Configuration")]
[SerializeField] private float bounceMultiplier = 1.5f;
[SerializeField] private Vector2 bounceDirection = Vector2.up;
[SerializeField] private bool useReflection = true;
[Header("Optional Modifiers")]
[SerializeField] private float minBounceVelocity = 2f;
[SerializeField] private float maxBounceVelocity = 20f;
[Header("Visual Feedback (Optional)")]
[SerializeField] private Animator bounceAnimator;
[SerializeField] private string bounceTrigger = "Bounce";
[SerializeField] private AudioSource bounceSound;
[Header("Debug")]
[SerializeField] private bool showDebugLogs;
private void Awake()
{
// Ensure collider is trigger
var collider = GetComponent<Collider2D>();
if (collider != null)
{
collider.isTrigger = true;
}
}
private void OnTriggerEnter2D(Collider2D other)
{
// Check if it's an airplane
var airplane = other.GetComponent<Core.AirplaneController>();
if (airplane == null || !airplane.IsFlying) return;
var rb = other.GetComponent<Rigidbody2D>();
if (rb == null) return;
// Calculate bounce velocity
Vector2 newVelocity;
if (useReflection)
{
// Reflect velocity around bounce direction (normal)
Vector2 normal = bounceDirection.normalized;
newVelocity = Vector2.Reflect(rb.linearVelocity, normal) * bounceMultiplier;
}
else
{
// Direct bounce in specified direction
float speed = rb.linearVelocity.magnitude * bounceMultiplier;
newVelocity = bounceDirection.normalized * speed;
}
// Clamp velocity
float magnitude = newVelocity.magnitude;
if (magnitude < minBounceVelocity)
{
newVelocity = newVelocity.normalized * minBounceVelocity;
}
else if (magnitude > maxBounceVelocity)
{
newVelocity = newVelocity.normalized * maxBounceVelocity;
}
// Apply bounce
rb.linearVelocity = newVelocity;
// Visual/audio feedback
PlayBounceEffects();
if (showDebugLogs)
{
Debug.Log($"[AirplaneBouncySurface] Bounced {other.name}: velocity={newVelocity}");
}
}
private void PlayBounceEffects()
{
// Trigger animation
if (bounceAnimator != null && !string.IsNullOrEmpty(bounceTrigger))
{
bounceAnimator.SetTrigger(bounceTrigger);
}
// Play sound
if (bounceSound != null)
{
bounceSound.Play();
}
}
private void OnDrawGizmos()
{
// Visualize bounce direction in editor
Gizmos.color = Color.cyan;
var collider = GetComponent<Collider2D>();
if (collider != null)
{
Vector3 center = collider.bounds.center;
Vector3 direction = bounceDirection.normalized;
// Draw arrow showing bounce direction
Gizmos.DrawRay(center, direction * 2f);
Gizmos.DrawWireSphere(center + direction * 2f, 0.3f);
// Draw surface bounds
Gizmos.DrawWireCube(collider.bounds.center, collider.bounds.size);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6f1ff69bae8e49188f439a8e5cdb7dfc
timeCreated: 1765135371

View File

@@ -0,0 +1,148 @@
using UnityEngine;
namespace Minigames.Airplane.Interactive
{
/// <summary>
/// Gravity well that pulls airplanes toward its center.
/// Creates challenging "danger zones" that players must avoid or escape from.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class AirplaneGravityWell : MonoBehaviour
{
[Header("Gravity Configuration")]
[SerializeField] private float pullStrength = 5f;
[SerializeField] private bool useInverseSquare = false;
[SerializeField] private float minPullDistance = 0.5f;
[Header("Optional Modifiers")]
[SerializeField] private float maxPullForce = 15f;
[SerializeField] private AnimationCurve pullFalloff = AnimationCurve.Linear(0, 1, 1, 0);
[Header("Visual Feedback (Optional)")]
[SerializeField] private ParticleSystem gravityParticles;
[SerializeField] private SpriteRenderer centerSprite;
[SerializeField] private float rotationSpeed = 90f;
[Header("Debug")]
[SerializeField] private bool showDebugLogs;
[SerializeField] private bool drawDebugLines = true;
private Vector2 centerPosition;
private void Awake()
{
// Ensure collider is trigger
var collider = GetComponent<Collider2D>();
if (collider != null)
{
collider.isTrigger = true;
}
centerPosition = transform.position;
}
private void Update()
{
// Rotate center visual if present
if (centerSprite != null)
{
centerSprite.transform.Rotate(0, 0, rotationSpeed * Time.deltaTime);
}
}
private void OnTriggerStay2D(Collider2D other)
{
// Check if it's an airplane
var airplane = other.GetComponent<Core.AirplaneController>();
if (airplane == null || !airplane.IsFlying) return;
var rb = other.GetComponent<Rigidbody2D>();
if (rb == null) return;
// Calculate direction and distance to center
Vector2 airplanePos = rb.position;
Vector2 toCenter = centerPosition - airplanePos;
float distance = toCenter.magnitude;
// Prevent division by zero
if (distance < minPullDistance)
{
distance = minPullDistance;
}
// Calculate pull force
float forceMagnitude;
if (useInverseSquare)
{
// Realistic gravity-like force (inverse square law)
forceMagnitude = pullStrength / (distance * distance);
}
else
{
// Linear falloff based on distance
var collider = GetComponent<Collider2D>();
float maxDistance = collider != null ? collider.bounds.extents.magnitude : 5f;
float normalizedDistance = Mathf.Clamp01(distance / maxDistance);
float falloff = pullFalloff.Evaluate(1f - normalizedDistance);
forceMagnitude = pullStrength * falloff;
}
// Clamp force
forceMagnitude = Mathf.Min(forceMagnitude, maxPullForce);
// Apply force toward center
Vector2 pullForce = toCenter.normalized * forceMagnitude;
rb.AddForce(pullForce, ForceMode2D.Force);
if (showDebugLogs && Time.frameCount % 30 == 0) // Log every 30 frames
{
Debug.Log($"[AirplaneGravityWell] Pulling {other.name}: force={forceMagnitude:F2}, distance={distance:F2}");
}
}
private void OnDrawGizmos()
{
// Visualize gravity well in editor
Gizmos.color = new Color(1f, 0f, 1f, 0.3f); // Magenta transparent
var collider = GetComponent<Collider2D>();
if (collider != null)
{
// Draw zone bounds
Gizmos.DrawWireSphere(collider.bounds.center, collider.bounds.extents.magnitude);
// Draw center point
Gizmos.color = Color.magenta;
Gizmos.DrawWireSphere(transform.position, 0.5f);
// Draw pull strength indicator
Gizmos.DrawRay(transform.position, Vector3.up * pullStrength * 0.2f);
}
}
private void OnDrawGizmosSelected()
{
if (!drawDebugLines) return;
// Draw pull force visualization at multiple points
var collider = GetComponent<Collider2D>();
if (collider == null) return;
float radius = collider.bounds.extents.magnitude;
int samples = 8;
for (int i = 0; i < samples; i++)
{
float angle = (i / (float)samples) * 360f * Mathf.Deg2Rad;
Vector2 offset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
Vector3 samplePoint = transform.position + (Vector3)offset;
Vector3 direction = (transform.position - samplePoint).normalized;
Gizmos.color = Color.yellow;
Gizmos.DrawLine(samplePoint, samplePoint + direction * 2f);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f6c5008f2782416095c5b3f5092843a9
timeCreated: 1765135424

View File

@@ -0,0 +1,122 @@
using UnityEngine;
namespace Minigames.Airplane.Interactive
{
/// <summary>
/// Speed boost ring that increases airplane velocity when passed through.
/// Can be used as collectible power-ups or checkpoint rings.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class AirplaneSpeedRing : MonoBehaviour
{
[Header("Boost Configuration")]
[SerializeField] private float velocityMultiplier = 1.5f;
[SerializeField] private float boostDuration = 1f;
[SerializeField] private bool oneTimeUse = true;
[Header("Optional Constraints")]
[SerializeField] private float minResultSpeed = 5f;
[SerializeField] private float maxResultSpeed = 25f;
[Header("Visual Feedback")]
[SerializeField] private GameObject ringVisual;
[SerializeField] private ParticleSystem collectEffect;
[SerializeField] private AudioSource collectSound;
[Header("Debug")]
[SerializeField] private bool showDebugLogs;
private bool hasBeenUsed;
private void Awake()
{
// Ensure collider is trigger
var collider = GetComponent<Collider2D>();
if (collider != null)
{
collider.isTrigger = true;
}
}
private void OnTriggerEnter2D(Collider2D other)
{
// Check if already used
if (oneTimeUse && hasBeenUsed) return;
// Check if it's an airplane
var airplane = other.GetComponent<Core.AirplaneController>();
if (airplane == null || !airplane.IsFlying) return;
var rb = other.GetComponent<Rigidbody2D>();
if (rb == null) return;
// Apply speed boost
Vector2 boostedVelocity = rb.linearVelocity * velocityMultiplier;
// Clamp to constraints
float speed = boostedVelocity.magnitude;
if (speed < minResultSpeed)
{
boostedVelocity = boostedVelocity.normalized * minResultSpeed;
}
else if (speed > maxResultSpeed)
{
boostedVelocity = boostedVelocity.normalized * maxResultSpeed;
}
rb.linearVelocity = boostedVelocity;
// Mark as used
hasBeenUsed = true;
// Trigger effects
PlayCollectEffects();
if (showDebugLogs)
{
Debug.Log($"[AirplaneSpeedRing] Boosted {other.name}: velocity={boostedVelocity.magnitude:F1}");
}
// Hide or destroy ring
if (oneTimeUse)
{
if (ringVisual != null)
{
ringVisual.SetActive(false);
}
else
{
Destroy(gameObject, collectEffect != null ? collectEffect.main.duration : 0.5f);
}
}
}
private void PlayCollectEffects()
{
// Play particle effect
if (collectEffect != null)
{
collectEffect.Play();
}
// Play sound
if (collectSound != null)
{
collectSound.Play();
}
}
private void OnDrawGizmos()
{
// Visualize ring in editor
Gizmos.color = hasBeenUsed ? Color.gray : Color.yellow;
var collider = GetComponent<Collider2D>();
if (collider != null)
{
Gizmos.DrawWireSphere(collider.bounds.center, collider.bounds.extents.magnitude);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 30fadeee96664cf28e3e2e562c99db26
timeCreated: 1765135387

View File

@@ -0,0 +1,107 @@
using UnityEngine;
namespace Minigames.Airplane.Interactive
{
/// <summary>
/// Turbulence zone that applies random chaotic forces to airplanes.
/// Creates unpredictable movement, adding challenge to navigation.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class AirplaneTurbulenceZone : MonoBehaviour
{
[Header("Turbulence Configuration")]
[SerializeField] private float turbulenceStrength = 3f;
[SerializeField] private float changeFrequency = 0.1f;
[Header("Optional Modifiers")]
[SerializeField] private bool preventUpwardForce = false;
[SerializeField] private float maxTotalForce = 10f;
[Header("Visual Feedback (Optional)")]
[SerializeField] private ParticleSystem turbulenceParticles;
[Header("Debug")]
[SerializeField] private bool showDebugLogs;
private float nextChangeTime;
private Vector2 currentTurbulenceDirection;
private void Awake()
{
// Ensure collider is trigger
var collider = GetComponent<Collider2D>();
if (collider != null)
{
collider.isTrigger = true;
}
// Initialize random direction
UpdateTurbulenceDirection();
}
private void Update()
{
// Change turbulence direction periodically
if (Time.time >= nextChangeTime)
{
UpdateTurbulenceDirection();
nextChangeTime = Time.time + changeFrequency;
}
}
private void UpdateTurbulenceDirection()
{
currentTurbulenceDirection = Random.insideUnitCircle.normalized;
// Prevent upward force if configured
if (preventUpwardForce && currentTurbulenceDirection.y > 0)
{
currentTurbulenceDirection.y = -currentTurbulenceDirection.y;
}
}
private void OnTriggerStay2D(Collider2D other)
{
// Check if it's an airplane
var airplane = other.GetComponent<Core.AirplaneController>();
if (airplane == null || !airplane.IsFlying) return;
var rb = other.GetComponent<Rigidbody2D>();
if (rb == null) return;
// Apply turbulence force
Vector2 turbulenceForce = currentTurbulenceDirection * turbulenceStrength;
// Clamp total force
if (turbulenceForce.magnitude > maxTotalForce)
{
turbulenceForce = turbulenceForce.normalized * maxTotalForce;
}
rb.AddForce(turbulenceForce, ForceMode2D.Force);
if (showDebugLogs && Time.frameCount % 30 == 0) // Log every 30 frames to avoid spam
{
Debug.Log($"[AirplaneTurbulenceZone] Applied turbulence: {turbulenceForce} to {other.name}");
}
}
private void OnDrawGizmos()
{
// Visualize turbulence zone in editor
Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); // Orange transparent
var collider = GetComponent<Collider2D>();
if (collider != null)
{
Gizmos.DrawCube(collider.bounds.center, collider.bounds.size);
// Draw current direction
Gizmos.color = Color.red;
Vector3 center = collider.bounds.center;
Gizmos.DrawRay(center, (Vector3)currentTurbulenceDirection * 2f);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ae20630c0dc74174ae4d851d97d101c0
timeCreated: 1765135403

View File

@@ -0,0 +1,70 @@
using UnityEngine;
namespace Minigames.Airplane.Interactive
{
/// <summary>
/// Applies a constant force to airplanes passing through this zone.
/// Can be used for updrafts, downdrafts, or crosswinds.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class AirplaneWindZone : MonoBehaviour
{
[Header("Wind Configuration")]
[SerializeField] private Vector2 windForce = new Vector2(0, 5f);
[SerializeField] private bool isWorldSpace = true;
[Header("Visual Feedback (Optional)")]
[SerializeField] private ParticleSystem windParticles;
[Header("Debug")]
[SerializeField] private bool showDebugLogs;
private void Awake()
{
// Ensure collider is trigger
var collider = GetComponent<Collider2D>();
if (collider != null)
{
collider.isTrigger = true;
}
}
private void OnTriggerStay2D(Collider2D other)
{
// Check if it's an airplane
var airplane = other.GetComponent<Core.AirplaneController>();
if (airplane == null || !airplane.IsFlying) return;
// Apply wind force
var rb = other.GetComponent<Rigidbody2D>();
if (rb != null)
{
Vector2 force = isWorldSpace ? windForce : transform.TransformDirection(windForce);
rb.AddForce(force * Time.fixedDeltaTime, ForceMode2D.Force);
if (showDebugLogs)
{
Debug.Log($"[AirplaneWindZone] Applied force: {force} to {other.name}");
}
}
}
private void OnDrawGizmos()
{
// Visualize wind direction in editor
Gizmos.color = windForce.y > 0 ? Color.green : Color.red;
var collider = GetComponent<Collider2D>();
if (collider != null)
{
Vector3 center = collider.bounds.center;
Vector2 direction = isWorldSpace ? windForce : transform.TransformDirection(windForce);
// Draw arrow showing wind direction
Gizmos.DrawRay(center, direction.normalized * 2f);
Gizmos.DrawWireSphere(center + (Vector3)(direction.normalized * 2f), 0.3f);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dc3242fd3fe042919496d71933a760a5
timeCreated: 1765135354