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,69 @@
using AppleHills.Core.Settings;
using Core;
using Minigames.Airplane.Data;
namespace Minigames.Airplane.Abilities
{
/// <summary>
/// Factory for creating airplane abilities from settings configuration.
/// </summary>
public static class AbilityFactory
{
/// <summary>
/// Create an ability instance based on type and settings.
/// </summary>
public static BaseAirplaneAbility CreateAbility(AirplaneAbilityType type, IAirplaneSettings settings)
{
if (settings == null)
{
Logging.Error("[AbilityFactory] Settings is null!");
return null;
}
return type switch
{
AirplaneAbilityType.Jet => CreateJetAbility(settings),
AirplaneAbilityType.Bobbing => CreateBobbingAbility(settings),
AirplaneAbilityType.Drop => CreateDropAbility(settings),
_ => null
};
}
private static JetAbility CreateJetAbility(IAirplaneSettings settings)
{
var config = settings.JetAbilityConfig;
return new JetAbility(
config.abilityName,
config.abilityIcon,
config.cooldownDuration,
config.jetSpeed,
config.jetAngle
);
}
private static BobbingAbility CreateBobbingAbility(IAirplaneSettings settings)
{
var config = settings.BobbingAbilityConfig;
return new BobbingAbility(
config.abilityName,
config.abilityIcon,
config.cooldownDuration,
config.bobForce
);
}
private static DropAbility CreateDropAbility(IAirplaneSettings settings)
{
var config = settings.DropAbilityConfig;
return new DropAbility(
config.abilityName,
config.abilityIcon,
config.cooldownDuration,
config.dropForce,
config.dropDistance,
config.zeroHorizontalVelocity
);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6668ddc48c30428f98d780700e93cab5
timeCreated: 1764977809

View File

@@ -0,0 +1,232 @@
using System;
using Core;
using UnityEngine;
namespace Minigames.Airplane.Abilities
{
/// <summary>
/// Abstract base class for airplane special abilities.
/// Each ability defines its own execution logic, input handling, and cooldown.
/// Subclasses override Execute() to implement specific ability behavior.
/// Created from settings configuration at runtime.
/// </summary>
[System.Serializable]
public abstract class BaseAirplaneAbility
{
#region Configuration
protected readonly string abilityName;
protected readonly Sprite abilityIcon;
protected readonly float cooldownDuration;
protected readonly bool canReuse;
protected bool showDebugLogs;
#endregion
#region Constructor
/// <summary>
/// Base constructor for abilities. Called by subclasses.
/// </summary>
protected BaseAirplaneAbility(string name, Sprite icon, float cooldown, bool reusable = true)
{
abilityName = name;
abilityIcon = icon;
cooldownDuration = cooldown;
canReuse = reusable;
showDebugLogs = false;
}
#endregion
#region Properties
public string AbilityName => abilityName;
public Sprite AbilityIcon => abilityIcon;
public float CooldownDuration => cooldownDuration;
public bool CanReuse => canReuse;
#endregion
#region State (Runtime)
protected Core.AirplaneController currentAirplane;
protected bool isActive;
protected bool isOnCooldown;
protected float cooldownTimer;
public bool IsActive => isActive;
public bool IsOnCooldown => isOnCooldown;
public float CooldownRemaining => cooldownTimer;
public bool CanActivate => !isOnCooldown && !isActive && currentAirplane != null && currentAirplane.IsFlying;
#endregion
#region Events
public event Action<BaseAirplaneAbility> OnAbilityActivated;
public event Action<BaseAirplaneAbility> OnAbilityDeactivated;
public event Action<float, float> OnCooldownChanged; // (remaining, total)
#endregion
#region Lifecycle
/// <summary>
/// Initialize ability with airplane reference.
/// Called when airplane is spawned.
/// </summary>
public virtual void Initialize(Core.AirplaneController airplane)
{
currentAirplane = airplane;
isActive = false;
isOnCooldown = false;
cooldownTimer = 0f;
if (showDebugLogs)
{
Logging.Debug($"[{abilityName}] Initialized with airplane");
}
}
/// <summary>
/// Update cooldown timer. Called every frame by ability manager.
/// </summary>
public virtual void UpdateCooldown(float deltaTime)
{
if (isOnCooldown)
{
cooldownTimer -= deltaTime;
if (cooldownTimer <= 0f)
{
cooldownTimer = 0f;
isOnCooldown = false;
if (showDebugLogs)
{
Logging.Debug($"[{abilityName}] Cooldown complete");
}
}
OnCooldownChanged?.Invoke(cooldownTimer, cooldownDuration);
}
}
/// <summary>
/// Cleanup when airplane is destroyed or flight ends.
/// </summary>
public virtual void Cleanup()
{
if (isActive)
{
Deactivate();
}
currentAirplane = null;
isActive = false;
isOnCooldown = false;
cooldownTimer = 0f;
if (showDebugLogs)
{
Logging.Debug($"[{abilityName}] Cleaned up");
}
}
#endregion
#region Abstract Methods (Must Override)
/// <summary>
/// Execute the ability effect.
/// Override to implement specific ability behavior.
/// </summary>
public abstract void Execute();
/// <summary>
/// Stop the ability effect (for sustained abilities).
/// Override if ability can be deactivated.
/// </summary>
public virtual void Deactivate()
{
if (!isActive) return;
isActive = false;
OnAbilityDeactivated?.Invoke(this);
if (showDebugLogs)
{
Logging.Debug($"[{abilityName}] Deactivated");
}
}
#endregion
#region Protected Helpers
/// <summary>
/// Start ability activation (called by subclasses).
/// </summary>
protected virtual void StartActivation()
{
if (!CanActivate)
{
if (showDebugLogs)
{
Logging.Warning($"[{abilityName}] Cannot activate - IsOnCooldown: {isOnCooldown}, IsActive: {isActive}, CanFly: {currentAirplane?.IsFlying}");
}
return;
}
isActive = true;
OnAbilityActivated?.Invoke(this);
if (showDebugLogs)
{
Logging.Debug($"[{abilityName}] Activated");
}
}
/// <summary>
/// Start cooldown timer (called by subclasses after execution).
/// </summary>
protected virtual void StartCooldown()
{
isOnCooldown = true;
cooldownTimer = cooldownDuration;
OnCooldownChanged?.Invoke(cooldownTimer, cooldownDuration);
if (showDebugLogs)
{
Logging.Debug($"[{abilityName}] Cooldown started: {cooldownDuration}s");
}
}
/// <summary>
/// Check if airplane reference is valid.
/// </summary>
protected bool ValidateAirplane()
{
if (currentAirplane == null)
{
Logging.Warning($"[{abilityName}] Cannot execute - airplane reference is null!");
return false;
}
if (!currentAirplane.IsFlying)
{
if (showDebugLogs)
{
Logging.Debug($"[{abilityName}] Cannot execute - airplane is not flying!");
}
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9b5ef9d7a9ce48ddb98de1e974e1d496
timeCreated: 1764975940

View File

@@ -0,0 +1,63 @@
using Core;
using UnityEngine;
namespace Minigames.Airplane.Abilities
{
/// <summary>
/// Bobbing Plane Ability: Tap to jump upward and forward.
/// Instant ability - activates once, then cooldown.
/// Applies diagonal impulse (forward + upward) to maintain airborne momentum.
/// Configuration loaded from settings at runtime.
/// </summary>
public class BobbingAbility : BaseAirplaneAbility
{
#region Configuration
private readonly Vector2 bobForce;
#endregion
#region Constructor
/// <summary>
/// Create bobbing ability with configuration from settings.
/// </summary>
public BobbingAbility(string name, Sprite icon, float cooldown, Vector2 force)
: base(name, icon, cooldown)
{
bobForce = force;
}
#endregion
#region Override Methods
public override void Execute()
{
if (!ValidateAirplane()) return;
if (!CanActivate) return;
StartActivation();
var rb = currentAirplane.GetComponent<Rigidbody2D>();
if (rb != null)
{
// Apply configured forward and upward impulse
// X = forward momentum, Y = upward lift
rb.AddForce(bobForce, ForceMode2D.Impulse);
if (showDebugLogs)
{
Logging.Debug($"[BobbingAbility] Executed - Force: {bobForce} (forward: {bobForce.x:F1}, upward: {bobForce.y:F1})");
}
}
// Instant ability - deactivate immediately and start cooldown
base.Deactivate();
StartCooldown();
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cc60dfa311424a7a9f2fdfe19eda8639
timeCreated: 1764975962

View File

@@ -0,0 +1,140 @@
using System.Collections;
using Core;
using UnityEngine;
namespace Minigames.Airplane.Abilities
{
/// <summary>
/// Drop Plane Ability: Swipe down to drop straight down.
/// Sustained ability - drops for fixed duration/distance.
/// Good for precision strikes on targets.
/// Configuration loaded from settings at runtime.
/// </summary>
public class DropAbility : BaseAirplaneAbility
{
#region Configuration
private readonly float dropForce;
private readonly float dropDistance;
private readonly bool zeroHorizontalVelocity;
#endregion
#region Constructor
/// <summary>
/// Create drop ability with configuration from settings.
/// </summary>
public DropAbility(string name, Sprite icon, float cooldown, float force, float distance, bool zeroHorizontal = true)
: base(name, icon, cooldown)
{
dropForce = force;
dropDistance = distance;
zeroHorizontalVelocity = zeroHorizontal;
}
#endregion
#region State
private float originalXVelocity;
private Vector3 dropStartPosition;
private Coroutine dropCoroutine;
#endregion
#region Override Methods
public override void Execute()
{
if (!ValidateAirplane()) return;
if (!CanActivate) return;
StartActivation();
var rb = currentAirplane.GetComponent<Rigidbody2D>();
if (rb != null)
{
// Store original velocity
originalXVelocity = rb.linearVelocity.x;
// Zero horizontal velocity if configured
if (zeroHorizontalVelocity)
{
rb.linearVelocity = new Vector2(0f, rb.linearVelocity.y);
}
// Apply strong downward force
rb.AddForce(Vector2.down * dropForce, ForceMode2D.Impulse);
// Track drop distance
dropStartPosition = currentAirplane.transform.position;
// Start monitoring drop distance
dropCoroutine = currentAirplane.StartCoroutine(MonitorDropDistance());
}
if (showDebugLogs)
{
Logging.Debug($"[DropAbility] Activated - Force: {dropForce}, Distance: {dropDistance}");
}
}
public override void Deactivate()
{
if (!isActive) return;
// Stop monitoring
if (dropCoroutine != null && currentAirplane != null)
{
currentAirplane.StopCoroutine(dropCoroutine);
dropCoroutine = null;
}
// Restore horizontal velocity (optional)
if (currentAirplane != null)
{
var rb = currentAirplane.GetComponent<Rigidbody2D>();
if (rb != null && zeroHorizontalVelocity)
{
Vector2 currentVel = rb.linearVelocity;
rb.linearVelocity = new Vector2(originalXVelocity * 0.5f, currentVel.y); // Resume at reduced speed
}
}
base.Deactivate();
// Start cooldown
StartCooldown();
if (showDebugLogs)
{
Logging.Debug("[DropAbility] Deactivated, cooldown started");
}
}
#endregion
#region Drop Monitoring
private IEnumerator MonitorDropDistance()
{
while (isActive && currentAirplane != null)
{
float distanceDropped = Mathf.Abs(dropStartPosition.y - currentAirplane.transform.position.y);
if (distanceDropped >= dropDistance)
{
// Drop distance reached - deactivate
Deactivate();
yield break;
}
yield return null;
}
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3b79dff7e24b4167af7631351242d500
timeCreated: 1764975977

View File

@@ -0,0 +1,104 @@
using Core;
using UnityEngine;
namespace Minigames.Airplane.Abilities
{
/// <summary>
/// Jet Plane Ability: Hold to fly straight without gravity.
/// Sustained ability - active while button held, deactivates on release.
/// </summary>
public class JetAbility : BaseAirplaneAbility
{
#region Configuration
private readonly float jetSpeed;
private readonly float jetAngle;
#endregion
#region Constructor
/// <summary>
/// Create jet ability with configuration from settings.
/// </summary>
public JetAbility(string name, Sprite icon, float cooldown, float speed, float angle)
: base(name, icon, cooldown)
{
jetSpeed = speed;
jetAngle = angle;
}
#endregion
#region State
private float originalGravityScale;
private bool originalRotateToVelocity;
#endregion
#region Override Methods
public override void Execute()
{
if (!ValidateAirplane()) return;
if (!CanActivate) return;
StartActivation();
// Store original physics values
var rb = currentAirplane.GetComponent<Rigidbody2D>();
if (rb != null)
{
originalGravityScale = rb.gravityScale;
// Disable gravity
rb.gravityScale = 0f;
// Set constant velocity in forward direction
Vector2 direction = Quaternion.Euler(0, 0, jetAngle) * Vector2.right;
rb.linearVelocity = direction.normalized * jetSpeed;
}
// Disable rotation to velocity (maintain straight angle)
originalRotateToVelocity = currentAirplane.RotateToVelocity;
currentAirplane.RotateToVelocity = false;
if (showDebugLogs)
{
Logging.Debug($"[JetAbility] Activated - Speed: {jetSpeed}, Angle: {jetAngle}");
}
}
public override void Deactivate()
{
if (!isActive) return;
// Restore original physics
if (currentAirplane != null)
{
var rb = currentAirplane.GetComponent<Rigidbody2D>();
if (rb != null)
{
rb.gravityScale = originalGravityScale;
}
// Restore rotation behavior
currentAirplane.RotateToVelocity = originalRotateToVelocity;
}
base.Deactivate();
// Start cooldown after deactivation
StartCooldown();
if (showDebugLogs)
{
Logging.Debug("[JetAbility] Deactivated, cooldown started");
}
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1175e6da9b23482c8ca74e18b35a82e4
timeCreated: 1764975953