Working? MVP of the minigame

This commit is contained in:
Michal Pikulski
2025-12-07 20:34:12 +01:00
parent 421c4d5cbd
commit ffdde1f5e1
67 changed files with 8370 additions and 192 deletions

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 71a8e50c7218456c96ceb54cd1140918
timeCreated: 1764975940

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

View File

@@ -2,6 +2,8 @@ using System;
using System.Collections;
using Core;
using Core.Lifecycle;
using Minigames.Airplane.Abilities;
using Minigames.Airplane.Data;
using UnityEngine;
namespace Minigames.Airplane.Core
@@ -44,10 +46,6 @@ namespace Minigames.Airplane.Core
[Tooltip("Gravity multiplier for arc calculation")]
[SerializeField] private float gravity = 9.81f;
[Header("Visual")]
[Tooltip("Should airplane rotate to face velocity direction?")]
[SerializeField] private bool rotateToVelocity = true;
[Header("Debug")]
[SerializeField] private bool showDebugLogs = false;
@@ -65,9 +63,14 @@ namespace Minigames.Airplane.Core
private float mass;
private float maxFlightTime;
// Ability system
private BaseAirplaneAbility currentAbility;
public bool IsFlying => isFlying;
public Vector2 CurrentVelocity => rb2D != null ? rb2D.linearVelocity : Vector2.zero;
public string LastHitTarget => lastHitTarget;
public BaseAirplaneAbility CurrentAbility => currentAbility;
public bool RotateToVelocity { get; set; } = true; // Made public for ability access
#endregion
@@ -166,12 +169,18 @@ namespace Minigames.Airplane.Core
while (isFlying)
{
// Rotate to face velocity direction (visual only)
if (rotateToVelocity && rb2D != null && rb2D.linearVelocity.magnitude > 0.1f)
if (RotateToVelocity && rb2D != null && rb2D.linearVelocity.magnitude > 0.1f)
{
float angle = Mathf.Atan2(rb2D.linearVelocity.y, rb2D.linearVelocity.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0, 0, angle);
}
// Update ability cooldown
if (currentAbility != null)
{
currentAbility.UpdateCooldown(Time.deltaTime);
}
// Update flight timer
flightTimer += Time.deltaTime;
@@ -275,12 +284,97 @@ namespace Minigames.Airplane.Core
#endregion
#region Ability System
/// <summary>
/// Initialize airplane with ability type from settings.
/// Called before launch to setup airplane properties.
/// </summary>
public void Initialize(AirplaneAbilityType abilityType)
{
// Get settings
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IAirplaneSettings>();
if (settings == null)
{
Logging.Error("[AirplaneController] Cannot initialize - settings not found!");
return;
}
// Get airplane config
var config = settings.GetAirplaneConfig(abilityType);
// Create ability from settings
currentAbility = Abilities.AbilityFactory.CreateAbility(abilityType, settings);
if (currentAbility != null)
{
currentAbility.Initialize(this);
}
// Apply physics overrides
if (rb2D != null)
{
if (config.overrideMass)
{
rb2D.mass = config.mass;
mass = config.mass;
}
if (config.overrideGravityScale)
rb2D.gravityScale = config.gravityScale;
if (config.overrideDrag)
rb2D.linearDamping = config.drag;
}
if (showDebugLogs)
{
Logging.Debug($"[AirplaneController] Initialized with type: {config.displayName}");
}
}
/// <summary>
/// Activate the airplane's special ability.
/// Called by UI button or input system.
/// </summary>
public void ActivateAbility()
{
if (currentAbility != null && currentAbility.CanActivate)
{
currentAbility.Execute();
}
else if (showDebugLogs)
{
Logging.Debug("[AirplaneController] Cannot activate ability - not ready or no ability assigned");
}
}
/// <summary>
/// Deactivate the airplane's special ability (for sustained abilities).
/// Called when releasing hold button.
/// </summary>
public void DeactivateAbility()
{
if (currentAbility != null && currentAbility.IsActive)
{
currentAbility.Deactivate();
}
}
#endregion
#region Cleanup
internal override void OnManagedDestroy()
{
base.OnManagedDestroy();
// Cleanup ability
if (currentAbility != null)
{
currentAbility.Cleanup();
currentAbility = null;
}
// Stop any coroutines
StopAllCoroutines();
}

View File

@@ -30,9 +30,9 @@ namespace Minigames.Airplane.Core
[SerializeField] private AirplaneTargetValidator targetValidator;
[SerializeField] private AirplaneSpawnManager spawnManager;
[Header("Targets")]
[Tooltip("All targets in the scene (for highlighting)")]
[SerializeField] private Targets.AirplaneTarget[] allTargets;
[Header("Airplane Type Selection")]
[SerializeField] private UI.AirplaneSelectionUI selectionUI;
[SerializeField] private UI.AirplaneAbilityButton abilityButton;
[Header("Debug")]
[SerializeField] private bool showDebugLogs = true;
@@ -65,7 +65,7 @@ namespace Minigames.Airplane.Core
#region State
private AirplaneGameState _currentState = AirplaneGameState.Intro;
private AirplaneGameState _currentState = AirplaneGameState.AirplaneSelection;
private Person _currentPerson;
private Person _previousPerson;
private AirplaneController _currentAirplane;
@@ -73,6 +73,7 @@ namespace Minigames.Airplane.Core
private int _successCount;
private int _failCount;
private int _totalTurns;
private AirplaneAbilityType _selectedAirplaneType;
public AirplaneGameState CurrentState => _currentState;
public Person CurrentPerson => _currentPerson;
@@ -175,9 +176,17 @@ namespace Minigames.Airplane.Core
Logging.Error("[AirplaneGameManager] AirplaneSpawnManager not assigned!");
}
if (allTargets == null || allTargets.Length == 0)
// Validate airplane selection system
if (selectionUI == null)
{
Logging.Warning("[AirplaneGameManager] No targets assigned!");
Logging.Warning("[AirplaneGameManager] ⚠️ SelectionUI not assigned! Player will not be able to choose airplane type.");
Logging.Warning("[AirplaneGameManager] → Assign AirplaneSelectionUI GameObject in Inspector under 'Airplane Type Selection'");
}
if (abilityButton == null)
{
Logging.Warning("[AirplaneGameManager] ⚠️ AbilityButton not assigned! Player will not be able to use abilities.");
Logging.Warning("[AirplaneGameManager] → Assign AirplaneAbilityButton GameObject in Inspector under 'Airplane Type Selection'");
}
}
@@ -200,21 +209,85 @@ namespace Minigames.Airplane.Core
#region Game Flow
/// <summary>
/// Start the game
/// Start the game - begins with intro sequence
/// </summary>
public void StartGame()
{
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] ===== GAME STARTING =====");
ChangeState(AirplaneGameState.Intro);
// Start with intro camera blend, THEN show selection UI
StartCoroutine(IntroSequence());
}
/// <summary>
/// Intro sequence: blend to intro camera, greet all people, blend to aiming camera
/// Airplane selection sequence: show selection UI, wait for player choice
/// Called AFTER intro camera blend
/// </summary>
private IEnumerator AirplaneSelectionSequence()
{
ChangeState(AirplaneGameState.AirplaneSelection);
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] === AIRPLANE SELECTION STARTING ===");
// Show selection UI
if (selectionUI != null)
{
if (showDebugLogs)
{
Logging.Debug($"[AirplaneGameManager] SelectionUI found! GameObject: {selectionUI.gameObject.name}, Active: {selectionUI.gameObject.activeSelf}");
}
selectionUI.Show();
if (showDebugLogs)
{
Logging.Debug($"[AirplaneGameManager] Called selectionUI.Show(). GameObject now active: {selectionUI.gameObject.activeSelf}");
}
// Wait for player to select and confirm
yield return new WaitUntil(() => selectionUI.HasSelectedType);
_selectedAirplaneType = selectionUI.GetSelectedType();
selectionUI.Hide();
if (showDebugLogs)
{
Logging.Debug($"[AirplaneGameManager] Selected airplane: {_selectedAirplaneType}");
}
}
else
{
Logging.Warning("[AirplaneGameManager] ⚠️ selectionUI is NULL! Cannot show selection UI. Check Inspector.");
Logging.Warning("[AirplaneGameManager] Using default airplane type from settings as fallback.");
// Fallback: use default type from settings
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IAirplaneSettings>();
if (settings != null)
{
_selectedAirplaneType = settings.DefaultAirplaneType;
if (showDebugLogs)
{
Logging.Debug($"[AirplaneGameManager] No selection UI, using default: {_selectedAirplaneType}");
}
}
else
{
_selectedAirplaneType = AirplaneAbilityType.Jet; // Ultimate fallback
}
}
// Continue with hellos after selection
yield return StartCoroutine(IntroHellosSequence());
}
/// <summary>
/// Intro sequence: blend to intro camera, THEN show airplane selection
/// </summary>
private IEnumerator IntroSequence()
{
ChangeState(AirplaneGameState.Intro);
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Playing intro sequence...");
// 1. Blend to intro camera
@@ -224,7 +297,21 @@ namespace Minigames.Airplane.Core
yield return new WaitForSeconds(0.5f); // Camera blend time
}
// 2. Iterate over each person and allow them to say their hellos
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Intro camera ready. Now showing airplane selection...");
// 2. Show airplane selection UI and wait for player choice
yield return StartCoroutine(AirplaneSelectionSequence());
}
/// <summary>
/// Hellos sequence: all people greet, then blend to aiming camera
/// Called AFTER airplane selection is complete
/// </summary>
private IEnumerator IntroHellosSequence()
{
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Starting hellos sequence...");
// 1. Iterate over each person and allow them to say their hellos
if (personQueue != null && personQueue.HasMorePeople())
{
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Introducing all people...");
@@ -243,7 +330,7 @@ namespace Minigames.Airplane.Core
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] All introductions complete");
}
// 3. Blend to aiming camera (first person's turn will start)
// 2. Blend to aiming camera (first person's turn will start)
if (cameraManager != null)
{
cameraManager.SwitchToState(AirplaneCameraState.Aiming);
@@ -278,6 +365,34 @@ namespace Minigames.Airplane.Core
if (cameraManager != null)
{
cameraManager.SwitchToState(AirplaneCameraState.NextPerson);
// Wait for camera blend to complete before cleanup and reaction
yield return new WaitForSeconds(0.5f); // Camera blend time
}
// NOW cleanup spawned objects after camera has blended (camera shows scene before cleanup)
if (spawnManager != null)
{
if (_lastShotHit)
{
// Success: Full cleanup - destroy all spawned objects and target
spawnManager.CleanupSpawnedObjects();
if (showDebugLogs)
{
Logging.Debug("[AirplaneGameManager] Cleaned up spawned objects after successful shot");
}
}
else
{
// Failure: Keep spawned objects for retry, just reset tracking state
spawnManager.ResetForRetry();
if (showDebugLogs)
{
Logging.Debug("[AirplaneGameManager] Kept spawned objects for retry after miss");
}
}
}
// Handle the previous person's reaction (celebrate/disappointment), removal (if hit), and shuffle
@@ -295,21 +410,25 @@ namespace Minigames.Airplane.Core
yield break;
}
// Check if this is a NEW person (different from previous) or a retry (same person)
bool isNewPerson = _previousPerson != _currentPerson;
if (showDebugLogs)
{
Logging.Debug($"[AirplaneGameManager] === Turn {_totalTurns}: {_currentPerson.PersonName} ===" +
string turnType = isNewPerson ? "NEW PERSON" : "RETRY";
Logging.Debug($"[AirplaneGameManager] === Turn {_totalTurns}: {_currentPerson.PersonName} ({turnType}) ===" +
$"\n Target: {_currentPerson.TargetName}");
}
OnPersonStartTurn?.Invoke(_currentPerson);
// Introduce the new person (unless it's the first turn - they already greeted in intro)
if (_previousPerson != null)
// Only introduce if this is a NEW person
if (isNewPerson && _previousPerson != null)
{
// Subsequent turns - person says hello
// Switching to a new person (after success) - they say hello
yield return StartCoroutine(personQueue.IntroduceNextPerson());
}
else
else if (_previousPerson == null)
{
// First turn - they already said hello during intro, just brief camera pause
if (cameraManager != null)
@@ -318,11 +437,14 @@ namespace Minigames.Airplane.Core
yield return new WaitForSeconds(0.5f);
}
}
// else: Same person retry (after failure) - skip introduction, go straight to aiming
// Initialize spawn manager for this person's target
if (spawnManager != null)
{
spawnManager.InitializeForGame(_currentPerson.TargetName);
// Pass retry flag: true if same person, false if new person
bool isRetry = !isNewPerson;
spawnManager.InitializeForGame(_currentPerson.TargetName, isRetry);
}
// Queue done - continue game flow
@@ -331,9 +453,6 @@ namespace Minigames.Airplane.Core
targetValidator.SetExpectedTarget(_currentPerson.TargetName);
}
// Highlight the target
HighlightTarget(_currentPerson.TargetName);
// Enter aiming state
EnterAimingState();
}
@@ -353,6 +472,17 @@ namespace Minigames.Airplane.Core
cameraManager.SwitchToState(AirplaneCameraState.Aiming);
}
// Spawn airplane at slingshot with selected type
if (launchController != null && _selectedAirplaneType != AirplaneAbilityType.None)
{
launchController.SetAirplaneType(_selectedAirplaneType);
if (showDebugLogs)
{
Logging.Debug($"[AirplaneGameManager] Spawned airplane at slingshot: {_selectedAirplaneType}");
}
}
// Show target UI
if (spawnManager != null)
{
@@ -387,6 +517,17 @@ namespace Minigames.Airplane.Core
ChangeState(AirplaneGameState.Flying);
// Show ability button if airplane has an ability
if (abilityButton != null && airplane.CurrentAbility != null)
{
abilityButton.Setup(airplane, airplane.CurrentAbility);
if (showDebugLogs)
{
Logging.Debug($"[AirplaneGameManager] Ability button shown: {airplane.CurrentAbility.AbilityName}");
}
}
// Start following airplane with camera
if (cameraManager != null)
{
@@ -501,6 +642,17 @@ namespace Minigames.Airplane.Core
{
ChangeState(AirplaneGameState.Evaluating);
// Hide ability button
if (abilityButton != null)
{
abilityButton.Hide();
if (showDebugLogs)
{
Logging.Debug("[AirplaneGameManager] Ability button hidden");
}
}
// Stop following airplane
if (cameraManager != null)
{
@@ -528,6 +680,9 @@ namespace Minigames.Airplane.Core
OnPersonFinishTurn?.Invoke(_currentPerson, success);
// Store success state for later use
_lastShotHit = success;
// Wait for evaluation display (stub)
yield return new WaitForSeconds(1f);
@@ -538,20 +693,14 @@ namespace Minigames.Airplane.Core
_currentAirplane = null;
}
// Clean up spawned objects
if (spawnManager != null)
{
spawnManager.CleanupSpawnedObjects();
}
// Clear launch controller reference
if (launchController != null)
{
launchController.ClearActiveAirplane();
}
// Clear target highlighting
ClearAllTargetHighlights();
// NOTE: Spawned objects cleanup moved to SetupNextPerson() to happen AFTER camera blend
// This ensures camera shows the scene before cleanup and person reaction
// Move to next person
StartCoroutine(SetupNextPerson());
@@ -581,45 +730,7 @@ namespace Minigames.Airplane.Core
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Game complete");
}
#endregion
#region Target Management
/// <summary>
/// Highlight a specific target by name
/// </summary>
private void HighlightTarget(string targetName)
{
if (allTargets == null) return;
foreach (var target in allTargets)
{
if (target != null)
{
bool isActive = string.Equals(target.TargetName, targetName, StringComparison.OrdinalIgnoreCase);
target.SetAsActiveTarget(isActive);
}
}
if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Highlighted target: {targetName}");
}
/// <summary>
/// Clear all target highlights
/// </summary>
private void ClearAllTargetHighlights()
{
if (allTargets == null) return;
foreach (var target in allTargets)
{
if (target != null)
{
target.SetAsActiveTarget(false);
}
}
}
#endregion
#region Public Query Methods

View File

@@ -2,6 +2,7 @@ using System;
using AppleHills.Core.Settings;
using Common.Input;
using Core;
using Minigames.Airplane.Data;
using UnityEngine;
namespace Minigames.Airplane.Core
@@ -64,6 +65,7 @@ namespace Minigames.Airplane.Core
#region State
private AirplaneController _activeAirplane;
private AirplaneAbilityType _selectedAirplaneType;
public AirplaneController ActiveAirplane => _activeAirplane;
@@ -116,18 +118,48 @@ namespace Minigames.Airplane.Core
#endregion
#region Launch
#region Airplane Type System
protected override void PerformLaunch(Vector2 direction, float force)
/// <summary>
/// Set the airplane type and spawn it at slingshot (before aiming).
/// </summary>
public void SetAirplaneType(Data.AirplaneAbilityType abilityType)
{
if (airplanePrefab == null)
_selectedAirplaneType = abilityType;
SpawnAirplaneAtSlingshot();
}
/// <summary>
/// Spawn airplane at slingshot anchor (pre-launch).
/// </summary>
private void SpawnAirplaneAtSlingshot()
{
// Clear existing
if (_activeAirplane != null)
{
Logging.Error("[AirplaneLaunchController] Cannot launch - airplane prefab not assigned!");
Destroy(_activeAirplane.gameObject);
_activeAirplane = null;
}
// Get settings and airplane config
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IAirplaneSettings>();
if (settings == null)
{
Logging.Error("[AirplaneLaunchController] Cannot spawn - settings not found!");
return;
}
// Spawn airplane at launch anchor
GameObject airplaneObj = Instantiate(airplanePrefab, launchAnchor.position, Quaternion.identity);
var config = settings.GetAirplaneConfig(_selectedAirplaneType);
GameObject prefab = config.prefab ?? airplanePrefab;
if (prefab == null)
{
Logging.Error("[AirplaneLaunchController] No airplane prefab available!");
return;
}
// Instantiate at launch anchor
GameObject airplaneObj = Instantiate(prefab, launchAnchor.position, Quaternion.identity);
_activeAirplane = airplaneObj.GetComponent<AirplaneController>();
if (_activeAirplane == null)
@@ -137,6 +169,41 @@ namespace Minigames.Airplane.Core
return;
}
// Initialize with ability type
_activeAirplane.Initialize(_selectedAirplaneType);
// Set kinematic until launch
var rb = _activeAirplane.GetComponent<Rigidbody2D>();
if (rb != null)
{
rb.bodyType = RigidbodyType2D.Kinematic;
}
if (showDebugLogs)
{
Logging.Debug($"[AirplaneLaunchController] Spawned airplane at slingshot: {config.displayName}");
}
}
#endregion
#region Launch
protected override void PerformLaunch(Vector2 direction, float force)
{
if (_activeAirplane == null)
{
Logging.Error("[AirplaneLaunchController] No airplane to launch! Call SetAirplaneType first.");
return;
}
// Set dynamic before launch
var rb = _activeAirplane.GetComponent<Rigidbody2D>();
if (rb != null)
{
rb.bodyType = RigidbodyType2D.Dynamic;
}
// Launch the airplane
_activeAirplane.Launch(direction, force);

View File

@@ -92,6 +92,10 @@ namespace Minigames.Airplane.Core
private int _positiveSpawnCount;
private int _negativeSpawnCount;
// Adaptive spawn distance (persistent across retries)
private float _furthestReachedX;
private bool _isRetryAttempt;
// Cached dictionaries
private Dictionary<string, GameObject> _targetPrefabDict;
private IAirplaneSettings _settings;
@@ -121,6 +125,12 @@ namespace Minigames.Airplane.Core
float planeX = _planeTransform.position.x;
// Track furthest X position reached
if (planeX > _furthestReachedX)
{
_furthestReachedX = planeX;
}
// Check if target should be spawned (when plane gets within spawn distance)
if (!_hasSpawnedTarget && _targetPrefabToSpawn != null)
{
@@ -149,22 +159,28 @@ namespace Minigames.Airplane.Core
}
}
// If past threshold, handle spawning
// If past threshold, handle spawning (only if we're going further than before)
if (_hasPassedThreshold)
{
// Spawn objects at intervals
if (Time.time >= _nextObjectSpawnTime)
{
SpawnRandomObject();
ScheduleNextObjectSpawn();
}
// Only spawn new content if plane is beyond previous furthest point (for retries)
bool shouldSpawnNewContent = !_isRetryAttempt || planeX > (_furthestReachedX - _settings.SpawnDistanceAhead);
// Spawn ground tiles ahead of plane
float groundSpawnTargetX = planeX + GetGroundSpawnAheadDistance();
while (_nextGroundSpawnX < groundSpawnTargetX)
if (shouldSpawnNewContent)
{
SpawnGroundTile();
_nextGroundSpawnX += _settings.GroundSpawnInterval;
// Spawn objects at intervals
if (Time.time >= _nextObjectSpawnTime)
{
SpawnRandomObject();
ScheduleNextObjectSpawn();
}
// Spawn ground tiles ahead of plane
float groundSpawnTargetX = planeX + GetGroundSpawnAheadDistance();
while (_nextGroundSpawnX < groundSpawnTargetX)
{
SpawnGroundTile();
_nextGroundSpawnX += _settings.GroundSpawnInterval;
}
}
}
}
@@ -179,22 +195,38 @@ namespace Minigames.Airplane.Core
/// Target will spawn when plane gets within spawn distance.
/// </summary>
/// <param name="targetKey">Key of the target to spawn</param>
public void InitializeForGame(string targetKey)
/// <param name="isRetry">True if this is a retry attempt (keeps existing spawned objects and target position)</param>
public void InitializeForGame(string targetKey, bool isRetry = false)
{
_currentTargetKey = targetKey;
_isSpawningActive = false;
_hasPassedThreshold = false;
_hasSpawnedTarget = false;
_positiveSpawnCount = 0;
_negativeSpawnCount = 0;
_isRetryAttempt = isRetry;
// Determine target spawn distance
_targetDistance = Random.Range((float)_settings.TargetMinDistance, (float)_settings.TargetMaxDistance);
_targetSpawnPosition = new Vector3(_targetDistance, 0f, 0f);
if (showDebugLogs)
// Only reset target and spawn state if NOT a retry
if (!isRetry)
{
Logging.Debug($"[SpawnManager] Initialized for target '{targetKey}' at distance {_targetDistance:F2}");
_hasSpawnedTarget = false;
_positiveSpawnCount = 0;
_negativeSpawnCount = 0;
_furthestReachedX = 0f;
// Determine NEW target spawn distance
_targetDistance = Random.Range((float)_settings.TargetMinDistance, (float)_settings.TargetMaxDistance);
_targetSpawnPosition = new Vector3(_targetDistance, 0f, 0f);
if (showDebugLogs)
{
Logging.Debug($"[SpawnManager] Initialized NEW turn for target '{targetKey}' at distance {_targetDistance:F2}");
}
}
else
{
// Retry: Keep existing target position and spawned objects
if (showDebugLogs)
{
Logging.Debug($"[SpawnManager] Initialized RETRY for target '{targetKey}' at distance {_targetDistance:F2}, furthest reached: {_furthestReachedX:F2}");
}
}
// Find target prefab and extract icon WITHOUT spawning
@@ -296,7 +328,8 @@ namespace Minigames.Airplane.Core
}
/// <summary>
/// Clean up all spawned objects (call on game restart/cleanup).
/// Clean up all spawned objects (call on successful shot or game restart).
/// Destroys all spawned content including target, objects, and ground tiles.
/// </summary>
public void CleanupSpawnedObjects()
{
@@ -321,6 +354,33 @@ namespace Minigames.Airplane.Core
Destroy(_spawnedTarget);
_spawnedTarget = null;
}
// Reset all spawn state
_hasSpawnedTarget = false;
_hasPassedThreshold = false;
_furthestReachedX = 0f;
_positiveSpawnCount = 0;
_negativeSpawnCount = 0;
if (showDebugLogs)
{
Logging.Debug("[SpawnManager] Full cleanup completed (success or game restart)");
}
}
/// <summary>
/// Reset tracking state for retry attempt (keeps spawned objects).
/// Call this when player fails and will retry the same shot.
/// </summary>
public void ResetForRetry()
{
// Don't destroy anything - keep all spawned objects and target
// Just reset the tracking state so spawning can continue if plane goes further
if (showDebugLogs)
{
Logging.Debug($"[SpawnManager] Reset for retry (keeping spawned objects, furthest reached: {_furthestReachedX:F2})");
}
}
/// <summary>
@@ -559,11 +619,29 @@ namespace Minigames.Airplane.Core
/// <summary>
/// Spawn a random positive or negative object.
/// Uses weighted randomness to maintain target ratio.
/// Avoids spawning near target position to prevent obscuring it.
/// </summary>
private void SpawnRandomObject()
{
if (_planeTransform == null) return;
// Calculate spawn X position ahead of plane
float spawnX = _planeTransform.position.x + _settings.SpawnDistanceAhead;
// Check if spawn position is too close to target (avoid obscuring it)
float distanceToTarget = Mathf.Abs(spawnX - _targetSpawnPosition.x);
float targetClearanceZone = 10f; // Don't spawn within 10 units of target
if (distanceToTarget < targetClearanceZone)
{
// Too close to target, skip this spawn
if (showDebugLogs)
{
Logging.Debug($"[SpawnManager] Skipped object spawn at X={spawnX:F2} (too close to target at X={_targetSpawnPosition.x:F2})");
}
return;
}
// Determine if spawning positive or negative based on weighted ratio
bool spawnPositive = ShouldSpawnPositive();
@@ -588,9 +666,7 @@ namespace Minigames.Airplane.Core
if (prefabToSpawn == null) return;
// Calculate spawn X position ahead of plane
float spawnX = _planeTransform.position.x + _settings.SpawnDistanceAhead;
// Spawn object at temporary position
Vector3 tempPosition = new Vector3(spawnX, 0f, 0f);
GameObject spawnedObject = Instantiate(prefabToSpawn, tempPosition, Quaternion.identity);

View File

@@ -100,7 +100,7 @@ namespace Minigames.Airplane.Core
/// Shows debug text, waits, then hides it. Cancels any previous debug display.
/// Awaitable so callers can yield return this coroutine.
/// </summary>
public IEnumerator PrintDebugText(string inputText, float duration = 2.0f)
public IEnumerator PrintDebugText(string inputText, float duration = 0.5f)
{
if (debugText != null)
{

View File

@@ -165,7 +165,8 @@ namespace Minigames.Airplane.Core
}
/// <summary>
/// Remove the current person from the queue after their turn
/// Remove the current person from the queue after their turn.
/// Destroys the person's GameObject.
/// </summary>
public void RemoveCurrentPerson()
{
@@ -179,6 +180,17 @@ namespace Minigames.Airplane.Core
Logging.Debug($"[PersonQueue] Removed {removedPerson.PersonName} from queue. " +
$"Remaining: {RemainingPeople}");
}
// Destroy the person's GameObject
if (removedPerson != null && removedPerson.gameObject != null)
{
Destroy(removedPerson.gameObject);
if (showDebugLogs)
{
Logging.Debug($"[PersonQueue] Destroyed GameObject for {removedPerson.PersonName}");
}
}
}
#endregion
@@ -227,14 +239,14 @@ namespace Minigames.Airplane.Core
if (showDebugLogs) Logging.Debug("[PersonQueue] Success! Removing person and shuffling queue...");
// Store position before removal for shuffle animation
Vector3 removedPosition = currentPerson.PersonTransform.position;
// Remember the first person's position BEFORE removing them
Vector3 firstPersonPosition = currentPerson.PersonTransform.position;
// Remove successful person from queue
// Remove successful person from queue (they're no longer in peopleInQueue)
RemoveCurrentPerson();
// Shuffle remaining people toward the removed person's position
yield return StartCoroutine(ShuffleTransition(removedPosition));
// Shuffle remaining people forward to fill the first person's spot
yield return StartCoroutine(ShuffleToPosition(firstPersonPosition));
}
else
{
@@ -271,47 +283,64 @@ namespace Minigames.Airplane.Core
}
/// <summary>
/// Shuffle remaining people toward a target position (visual transition).
/// Shuffle remaining people forward to fill the first person's spot.
/// The next person (now at index 0) moves to the first person's position.
/// All other people move forward proportionally.
/// Only tweens X position to prevent characters with foot pivots from floating.
/// </summary>
private IEnumerator ShuffleTransition(Vector3 targetPosition)
private IEnumerator ShuffleToPosition(Vector3 firstPersonPosition)
{
if (peopleInQueue.Count == 0)
{
yield break; // No one left to shuffle
}
if (showDebugLogs) Logging.Debug($"[PersonQueue] Shuffling {peopleInQueue.Count} people");
if (showDebugLogs) Logging.Debug($"[PersonQueue] Shuffling {peopleInQueue.Count} people forward to first position");
// Store starting positions
List<Vector3> startPositions = new List<Vector3>();
foreach (var person in peopleInQueue)
// Store starting X positions and calculate target X positions
List<float> startXPositions = new List<float>();
List<float> targetXPositions = new List<float>();
for (int i = 0; i < peopleInQueue.Count; i++)
{
startPositions.Add(person.PersonTransform.position);
Person person = peopleInQueue[i];
startXPositions.Add(person.PersonTransform.position.x);
if (i == 0)
{
// Next person (index 0) moves to first person's X position
targetXPositions.Add(firstPersonPosition.x);
}
else
{
// Everyone else moves to the X position of the person ahead of them
targetXPositions.Add(peopleInQueue[i - 1].PersonTransform.position.x);
}
}
// Animate shuffle
// Animate shuffle (only X axis)
float elapsed = 0f;
while (elapsed < shuffleDuration)
{
elapsed += Time.deltaTime;
float t = elapsed / shuffleDuration;
// Move each person toward the left (toward removed person's spot)
// Smoothly lerp each person's X position only (preserve Y and Z)
for (int i = 0; i < peopleInQueue.Count; i++)
{
Vector3 start = startPositions[i];
Vector3 end = start + Vector3.left * shuffleDistance;
peopleInQueue[i].PersonTransform.position = Vector3.Lerp(start, end, t);
float newX = Mathf.Lerp(startXPositions[i], targetXPositions[i], t);
Vector3 currentPos = peopleInQueue[i].PersonTransform.position;
peopleInQueue[i].PersonTransform.position = new Vector3(newX, currentPos.y, currentPos.z);
}
yield return null;
}
// Ensure final positions are exact
// Ensure final X positions are exact (preserve Y and Z)
for (int i = 0; i < peopleInQueue.Count; i++)
{
Vector3 finalPos = startPositions[i] + Vector3.left * shuffleDistance;
peopleInQueue[i].PersonTransform.position = finalPos;
Vector3 currentPos = peopleInQueue[i].PersonTransform.position;
peopleInQueue[i].PersonTransform.position = new Vector3(targetXPositions[i], currentPos.y, currentPos.z);
}
if (showDebugLogs) Logging.Debug("[PersonQueue] Shuffle complete");

View File

@@ -0,0 +1,108 @@
using UnityEngine;
namespace Minigames.Airplane.Data
{
/// <summary>
/// Configuration for Jet Plane ability.
/// </summary>
[System.Serializable]
public class JetAbilityConfig
{
[Header("Jet Ability")]
[Tooltip("Display name")]
public string abilityName = "Jet Boost";
[Tooltip("Icon for ability button")]
public Sprite abilityIcon;
[Tooltip("Cooldown duration in seconds")]
public float cooldownDuration = 5f;
[Tooltip("Speed while ability is active")]
public float jetSpeed = 15f;
[Tooltip("Direction angle (0 = right, 90 = up)")]
public float jetAngle = 0f;
}
/// <summary>
/// Configuration for Bobbing Plane ability.
/// </summary>
[System.Serializable]
public class BobbingAbilityConfig
{
[Header("Bobbing Ability")]
[Tooltip("Display name")]
public string abilityName = "Air Hop";
[Tooltip("Icon for ability button")]
public Sprite abilityIcon;
[Tooltip("Cooldown duration in seconds")]
public float cooldownDuration = 3f;
[Tooltip("Force applied on activation (X = forward, Y = upward)")]
public Vector2 bobForce = new Vector2(7f, 10f);
}
/// <summary>
/// Configuration for Drop Plane ability.
/// </summary>
[System.Serializable]
public class DropAbilityConfig
{
[Header("Drop Ability")]
[Tooltip("Display name")]
public string abilityName = "Dive Bomb";
[Tooltip("Icon for ability button")]
public Sprite abilityIcon;
[Tooltip("Cooldown duration in seconds")]
public float cooldownDuration = 4f;
[Tooltip("Downward force applied")]
public float dropForce = 20f;
[Tooltip("Distance to drop before returning to normal flight")]
public float dropDistance = 5f;
[Tooltip("Should horizontal velocity be zeroed during drop?")]
public bool zeroHorizontalVelocity = true;
}
/// <summary>
/// Configuration for an airplane type with visual and physics properties.
/// </summary>
[System.Serializable]
public class AirplaneTypeConfig
{
[Header("Identity")]
[Tooltip("Display name for UI")]
public string displayName = "Airplane";
[Tooltip("Airplane prefab")]
public GameObject prefab;
[Tooltip("Preview sprite for selection UI")]
public Sprite previewSprite;
[Header("Ability")]
[Tooltip("Which ability this airplane uses")]
public AirplaneAbilityType abilityType = AirplaneAbilityType.Jet;
[Header("Physics Overrides (Optional)")]
[Tooltip("Override default mass")]
public bool overrideMass;
public float mass = 1f;
[Tooltip("Override default gravity scale")]
public bool overrideGravityScale;
public float gravityScale = 1f;
[Tooltip("Override default drag")]
public bool overrideDrag;
public float drag = 0f;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 57bed242caa44ef5bbcd33348d5c908f
timeCreated: 1764977731

View File

@@ -0,0 +1,14 @@
namespace Minigames.Airplane.Data
{
/// <summary>
/// Types of special abilities available for airplanes.
/// </summary>
public enum AirplaneAbilityType
{
None,
Jet, // Hold to fly straight without gravity
Bobbing, // Tap to jump upward, reduces speed
Drop // Swipe down to drop straight down
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 944943da845c403b8b43aeab3c9ef696
timeCreated: 1764975919

View File

@@ -5,6 +5,7 @@ namespace Minigames.Airplane.Data
/// </summary>
public enum AirplaneGameState
{
AirplaneSelection, // Player selecting airplane type
Intro, // Intro sequence
NextPerson, // Introducing the next person
Aiming, // Player is aiming the airplane

View File

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

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

View File

@@ -11,7 +11,40 @@ namespace Minigames.Airplane.Settings
[CreateAssetMenu(fileName = "AirplaneSettings", menuName = "AppleHills/Settings/Airplane", order = 9)]
public class AirplaneSettings : BaseSettings, IAirplaneSettings
{
[Header("Slingshot Configuration")]
[Header("=== AIRPLANE TYPES ===")]
[Header("Jet Plane")]
[SerializeField] private Data.AirplaneTypeConfig jetPlaneConfig = new Data.AirplaneTypeConfig
{
displayName = "Jet Plane",
abilityType = Data.AirplaneAbilityType.Jet
};
[SerializeField] private Data.JetAbilityConfig jetAbilityConfig = new Data.JetAbilityConfig();
[Header("Bobbing Plane")]
[SerializeField] private Data.AirplaneTypeConfig bobbingPlaneConfig = new Data.AirplaneTypeConfig
{
displayName = "Bobbing Plane",
abilityType = Data.AirplaneAbilityType.Bobbing
};
[SerializeField] private Data.BobbingAbilityConfig bobbingAbilityConfig = new Data.BobbingAbilityConfig();
[Header("Drop Plane")]
[SerializeField] private Data.AirplaneTypeConfig dropPlaneConfig = new Data.AirplaneTypeConfig
{
displayName = "Drop Plane",
abilityType = Data.AirplaneAbilityType.Drop
};
[SerializeField] private Data.DropAbilityConfig dropAbilityConfig = new Data.DropAbilityConfig();
[Header("Default Selection")]
[Tooltip("Which airplane type is selected by default")]
[SerializeField] private Data.AirplaneAbilityType defaultAirplaneType = Data.AirplaneAbilityType.Jet;
[Header("=== SLINGSHOT ===")]
[SerializeField] private SlingshotConfig slingshotSettings = new SlingshotConfig
{
maxDragDistance = 5f,
@@ -84,6 +117,21 @@ namespace Minigames.Airplane.Settings
#region IAirplaneSettings Implementation
public Data.AirplaneTypeConfig GetAirplaneConfig(Data.AirplaneAbilityType type)
{
return type switch
{
Data.AirplaneAbilityType.Jet => jetPlaneConfig,
Data.AirplaneAbilityType.Bobbing => bobbingPlaneConfig,
Data.AirplaneAbilityType.Drop => dropPlaneConfig,
_ => jetPlaneConfig // Fallback to jet
};
}
public Data.JetAbilityConfig JetAbilityConfig => jetAbilityConfig;
public Data.BobbingAbilityConfig BobbingAbilityConfig => bobbingAbilityConfig;
public Data.DropAbilityConfig DropAbilityConfig => dropAbilityConfig;
public Data.AirplaneAbilityType DefaultAirplaneType => defaultAirplaneType;
public SlingshotConfig SlingshotSettings => slingshotSettings;
public float AirplaneMass => airplaneMass;
public float MaxFlightTime => maxFlightTime;

View File

@@ -7,7 +7,11 @@ namespace Minigames.Airplane.Targets
{
/// <summary>
/// Represents a target in the airplane minigame.
/// Detects airplane collisions and can be highlighted when active.
/// Detects airplane collisions.
///
/// NOTE: Active/inactive highlighting is deprecated - targets now spawn dynamically per person,
/// so only one target exists in the scene at a time (no need for highlighting multiple targets).
/// The SetAsActiveTarget() method is kept for backward compatibility but is no longer used.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class AirplaneTarget : ManagedBehaviour
@@ -86,34 +90,6 @@ namespace Minigames.Airplane.Targets
}
}
internal override void OnManagedStart()
{
base.OnManagedStart();
// Start as inactive
SetAsActiveTarget(false);
}
#endregion
#region Active State
/// <summary>
/// Set this target as active (highlighted) or inactive
/// </summary>
public void SetAsActiveTarget(bool active)
{
_isActive = active;
// Update visual feedback
if (spriteRenderer != null)
{
spriteRenderer.color = active ? activeColor : inactiveColor;
}
if (showDebugLogs) Logging.Debug($"[AirplaneTarget] {targetName} set to {(active ? "active" : "inactive")}");
}
#endregion
#region Collision Detection
@@ -134,18 +110,6 @@ namespace Minigames.Airplane.Targets
}
#endregion
#region Public Methods
/// <summary>
/// Reset target to original state
/// </summary>
public void Reset()
{
SetAsActiveTarget(false);
}
#endregion
}
}

View File

@@ -0,0 +1,300 @@
using Core;
using Input;
using Minigames.Airplane.Abilities;
using Minigames.Airplane.Core;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Minigames.Airplane.UI
{
/// <summary>
/// UI button for activating airplane special abilities.
/// Handles input, visual feedback, and cooldown display.
/// Implements ITouchInputConsumer to properly handle hold/release for Jet ability.
/// </summary>
public class AirplaneAbilityButton : MonoBehaviour, ITouchInputConsumer
{
#region Inspector References
[Header("UI Components")]
[SerializeField] private Button button;
[SerializeField] private Image abilityIcon;
[SerializeField] private Image cooldownFill;
[SerializeField] private TextMeshProUGUI cooldownText;
[Header("Debug")]
[SerializeField] private bool showDebugLogs;
#endregion
#region State
private BaseAirplaneAbility currentAbility;
private AirplaneController currentAirplane;
private bool isHoldAbility; // Jet plane needs hold mechanic
private bool isHolding; // Track if button is currently being held
#endregion
#region Lifecycle
private void Awake()
{
if (button != null)
{
button.onClick.AddListener(OnButtonClick);
}
// Hide by default
gameObject.SetActive(false);
}
private void Update()
{
if (currentAbility == null) return;
// Update cooldown display
if (currentAbility.IsOnCooldown)
{
// Fill starts at 1 and reduces to 0 over cooldown duration
float fillAmount = currentAbility.CooldownRemaining / currentAbility.CooldownDuration;
if (cooldownFill != null)
{
cooldownFill.fillAmount = fillAmount;
}
// Show timer text
if (cooldownText != null)
{
cooldownText.text = $"{currentAbility.CooldownRemaining:F1}s";
}
}
else
{
// Cooldown complete - fill at 0, no text
if (cooldownFill != null)
cooldownFill.fillAmount = 0f;
if (cooldownText != null)
cooldownText.text = "";
}
}
private void OnDestroy()
{
// Unsubscribe from events
if (currentAbility != null)
{
currentAbility.OnAbilityActivated -= HandleAbilityActivated;
currentAbility.OnAbilityDeactivated -= HandleAbilityDeactivated;
currentAbility.OnCooldownChanged -= HandleCooldownChanged;
}
// Unregister from input system
if (InputManager.Instance != null)
{
InputManager.Instance.UnregisterOverrideConsumer(this);
}
}
#endregion
#region Public API
/// <summary>
/// Setup button with airplane and ability reference.
/// </summary>
public void Setup(AirplaneController airplane, BaseAirplaneAbility ability)
{
currentAirplane = airplane;
currentAbility = ability;
isHolding = false;
// Set icon and show immediately
if (abilityIcon != null && ability != null)
{
abilityIcon.sprite = ability.AbilityIcon;
abilityIcon.enabled = true;
}
// Initialize cooldown display
if (cooldownFill != null)
{
cooldownFill.fillAmount = 0f;
}
if (cooldownText != null)
{
cooldownText.text = "";
}
// Check if this is a hold ability (Jet)
isHoldAbility = ability is JetAbility;
// Subscribe to ability events
if (ability != null)
{
ability.OnAbilityActivated += HandleAbilityActivated;
ability.OnAbilityDeactivated += HandleAbilityDeactivated;
ability.OnCooldownChanged += HandleCooldownChanged;
if (showDebugLogs)
{
Logging.Debug($"[AirplaneAbilityButton] Subscribed to ability events for: {ability.AbilityName}");
}
}
// Show UI
gameObject.SetActive(true);
if (showDebugLogs)
{
Logging.Debug($"[AirplaneAbilityButton] Setup complete with ability: {ability?.AbilityName ?? "None"}, Hold: {isHoldAbility}");
}
}
/// <summary>
/// Hide and cleanup button.
/// </summary>
public void Hide()
{
if (currentAbility != null)
{
currentAbility.OnAbilityActivated -= HandleAbilityActivated;
currentAbility.OnAbilityDeactivated -= HandleAbilityDeactivated;
currentAbility.OnCooldownChanged -= HandleCooldownChanged;
}
// Unregister from input system
if (InputManager.Instance != null)
{
InputManager.Instance.UnregisterOverrideConsumer(this);
}
currentAbility = null;
currentAirplane = null;
isHolding = false;
gameObject.SetActive(false);
}
#endregion
#region Input Handling
private void OnButtonClick()
{
if (currentAirplane == null || currentAbility == null) return;
if (!currentAbility.CanActivate) return;
// Activate ability
currentAirplane.ActivateAbility();
// For hold abilities (Jet), mark as holding and register for input
if (isHoldAbility)
{
isHolding = true;
// Register as override consumer to receive hold/release events
if (InputManager.Instance != null)
{
InputManager.Instance.RegisterOverrideConsumer(this);
}
if (showDebugLogs)
{
Logging.Debug("[AirplaneAbilityButton] Started holding ability, registered for input");
}
}
// For non-hold abilities (Bobbing, Drop), this is all we need
}
#endregion
#region Event Handlers
private void HandleAbilityActivated(BaseAirplaneAbility ability)
{
if (showDebugLogs)
{
Logging.Debug($"[AirplaneAbilityButton] Ability activated: {ability.AbilityName}");
}
}
private void HandleAbilityDeactivated(BaseAirplaneAbility ability)
{
if (showDebugLogs)
{
Logging.Debug($"[AirplaneAbilityButton] Ability deactivated: {ability.AbilityName}");
}
}
private void HandleCooldownChanged(float remaining, float total)
{
if (showDebugLogs)
{
Logging.Debug($"[AirplaneAbilityButton] OnCooldownChanged: remaining={remaining:F2}, total={total:F2}");
}
// When cooldown starts (remaining == total), set fill to 1
if (remaining >= total - 0.01f && cooldownFill != null)
{
cooldownFill.fillAmount = 1f;
if (showDebugLogs)
{
Logging.Debug($"[AirplaneAbilityButton] Cooldown started: {total}s, fill set to 1");
}
}
}
#endregion
#region ITouchInputConsumer Implementation
public void OnTap(Vector2 position)
{
// If Jet ability is active (holding), next tap anywhere deactivates it
if (isHoldAbility && isHolding)
{
isHolding = false;
currentAirplane?.DeactivateAbility();
// Unregister from input system after tap
if (InputManager.Instance != null)
{
InputManager.Instance.UnregisterOverrideConsumer(this);
}
if (showDebugLogs)
{
Logging.Debug("[AirplaneAbilityButton] Tap detected - deactivated Jet ability, unregistered");
}
}
// Handle as button click for non-hold abilities
else if (!isHoldAbility)
{
OnButtonClick();
}
}
public void OnHoldStart(Vector2 position)
{
// Not used - button click handles activation, tap handles deactivation
}
public void OnHoldMove(Vector2 position)
{
// Not used
}
public void OnHoldEnd(Vector2 position)
{
// Not used - tap handles deactivation for Jet ability
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 44f826b6d40c47c0b5a9985f7f793278
timeCreated: 1764976132

View File

@@ -0,0 +1,38 @@
using UnityEngine;
using UnityEngine.UI;
namespace Minigames.Airplane.UI
{
/// <summary>
/// Component for individual airplane selection buttons.
/// Handles visual highlight feedback via show/hide of a highlight image.
/// </summary>
public class AirplaneSelectionButton : MonoBehaviour
{
[Header("Highlight Visual")]
[SerializeField] private Image highlightImage;
/// <summary>
/// Show the highlight visual.
/// </summary>
public void HighlightStart()
{
if (highlightImage != null)
{
highlightImage.gameObject.SetActive(true);
}
}
/// <summary>
/// Hide the highlight visual.
/// </summary>
public void HighlightEnd()
{
if (highlightImage != null)
{
highlightImage.gameObject.SetActive(false);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4ccf530e55324aec8dc6e09eb827f123
timeCreated: 1765132601

View File

@@ -0,0 +1,297 @@
using System;
using Core;
using Minigames.Airplane.Data;
using UnityEngine;
using UnityEngine.UI;
namespace Minigames.Airplane.UI
{
/// <summary>
/// UI for selecting airplane type before game starts.
/// Displays buttons for each available airplane type.
/// </summary>
public class AirplaneSelectionUI : MonoBehaviour
{
#region Inspector References
[Header("UI References")]
[SerializeField] private Button jetPlaneButton;
[SerializeField] private Button bobbingPlaneButton;
[SerializeField] private Button dropPlaneButton;
[SerializeField] private Button confirmButton;
[Header("Debug")]
[SerializeField] private bool showDebugLogs;
#endregion
#region State
private AirplaneAbilityType selectedType;
private AirplaneSelectionButton selectedButtonComponent;
private bool hasConfirmed;
public bool HasSelectedType => hasConfirmed;
#endregion
#region Events
public event Action<AirplaneAbilityType> OnTypeSelected;
public event Action<AirplaneAbilityType> OnConfirmed;
#endregion
#region Lifecycle
private void Awake()
{
// Setup button listeners
if (jetPlaneButton != null)
jetPlaneButton.onClick.AddListener(() => SelectType(AirplaneAbilityType.Jet, jetPlaneButton));
if (bobbingPlaneButton != null)
bobbingPlaneButton.onClick.AddListener(() => SelectType(AirplaneAbilityType.Bobbing, bobbingPlaneButton));
if (dropPlaneButton != null)
dropPlaneButton.onClick.AddListener(() => SelectType(AirplaneAbilityType.Drop, dropPlaneButton));
if (confirmButton != null)
{
confirmButton.onClick.AddListener(ConfirmSelection);
confirmButton.interactable = false; // Disabled until selection made
}
// Hide by default (deactivate container child, not root)
if (transform.childCount > 0)
{
Transform container = transform.GetChild(0);
container.gameObject.SetActive(false);
}
}
#endregion
#region Public API
/// <summary>
/// Show the selection UI.
/// Activates the immediate child container.
/// Script should be on Root, with UI elements under a Container child.
/// </summary>
public void Show()
{
selectedType = AirplaneAbilityType.None;
selectedButtonComponent = null;
hasConfirmed = false;
if (confirmButton != null)
confirmButton.interactable = false;
// Reset all button highlights
ResetButtonHighlights();
// Populate icons from settings
PopulateButtonIcons();
// Activate the container (immediate child)
if (transform.childCount > 0)
{
Transform container = transform.GetChild(0);
container.gameObject.SetActive(true);
if (showDebugLogs)
{
Logging.Debug($"[AirplaneSelectionUI] Shown. Activated container: {container.name}");
}
}
else
{
Logging.Error("[AirplaneSelectionUI] No child container found! Expected structure: Root(script)->Container->UI Elements");
}
}
/// <summary>
/// Hide the selection UI.
/// Deactivates the immediate child container.
/// </summary>
public void Hide()
{
// Deactivate the container (immediate child)
if (transform.childCount > 0)
{
Transform container = transform.GetChild(0);
container.gameObject.SetActive(false);
if (showDebugLogs)
{
Logging.Debug($"[AirplaneSelectionUI] Hidden. Deactivated container: {container.name}");
}
}
}
/// <summary>
/// Get the selected airplane type.
/// </summary>
public AirplaneAbilityType GetSelectedType()
{
return selectedType;
}
#endregion
#region Private Methods
private void SelectType(AirplaneAbilityType type, Button button)
{
if (type == AirplaneAbilityType.None)
{
Logging.Warning("[AirplaneSelectionUI] Attempted to select None type!");
return;
}
selectedType = type;
// Get the AirplaneSelectionButton component (on same GameObject as Button)
var buttonComponent = button.GetComponent<AirplaneSelectionButton>();
if (buttonComponent == null)
{
Logging.Warning($"[AirplaneSelectionUI] Button {button.name} is missing AirplaneSelectionButton component!");
return;
}
selectedButtonComponent = buttonComponent;
// Update visual feedback
ResetButtonHighlights();
HighlightButton(buttonComponent);
// Enable confirm button
if (confirmButton != null)
confirmButton.interactable = true;
// Fire event
OnTypeSelected?.Invoke(type);
if (showDebugLogs)
{
Logging.Debug($"[AirplaneSelectionUI] Selected type: {type}");
}
}
private void ConfirmSelection()
{
if (selectedType == AirplaneAbilityType.None)
{
Logging.Warning("[AirplaneSelectionUI] Cannot confirm - no type selected!");
return;
}
hasConfirmed = true;
// Fire event
OnConfirmed?.Invoke(selectedType);
// Hide UI
Hide();
}
private void ResetButtonHighlights()
{
// End highlight on all buttons
if (jetPlaneButton != null)
{
var component = jetPlaneButton.GetComponent<AirplaneSelectionButton>();
if (component != null) component.HighlightEnd();
}
if (bobbingPlaneButton != null)
{
var component = bobbingPlaneButton.GetComponent<AirplaneSelectionButton>();
if (component != null) component.HighlightEnd();
}
if (dropPlaneButton != null)
{
var component = dropPlaneButton.GetComponent<AirplaneSelectionButton>();
if (component != null) component.HighlightEnd();
}
}
private void HighlightButton(AirplaneSelectionButton buttonComponent)
{
if (buttonComponent != null)
{
buttonComponent.HighlightStart();
}
}
/// <summary>
/// Populate button icons from airplane settings.
/// Assumes Image component is on the same GameObject as the Button.
/// </summary>
private void PopulateButtonIcons()
{
// Get airplane settings
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IAirplaneSettings>();
if (settings == null)
{
if (showDebugLogs)
{
Logging.Warning("[AirplaneSelectionUI] Could not load airplane settings for icons");
}
return;
}
// Populate Jet button icon
if (jetPlaneButton != null)
{
var jetConfig = settings.GetAirplaneConfig(AirplaneAbilityType.Jet);
if (jetConfig != null && jetConfig.previewSprite != null)
{
var image = jetPlaneButton.GetComponent<Image>();
if (image != null)
{
image.sprite = jetConfig.previewSprite;
}
}
}
// Populate Bobbing button icon
if (bobbingPlaneButton != null)
{
var bobbingConfig = settings.GetAirplaneConfig(AirplaneAbilityType.Bobbing);
if (bobbingConfig != null && bobbingConfig.previewSprite != null)
{
var image = bobbingPlaneButton.GetComponent<Image>();
if (image != null)
{
image.sprite = bobbingConfig.previewSprite;
}
}
}
// Populate Drop button icon
if (dropPlaneButton != null)
{
var dropConfig = settings.GetAirplaneConfig(AirplaneAbilityType.Drop);
if (dropConfig != null && dropConfig.previewSprite != null)
{
var image = dropPlaneButton.GetComponent<Image>();
if (image != null)
{
image.sprite = dropConfig.previewSprite;
}
}
}
if (showDebugLogs)
{
Logging.Debug("[AirplaneSelectionUI] Populated airplane icons from settings");
}
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6463ce42d43142878816170f53a0f5bd
timeCreated: 1764976150

View File

@@ -66,7 +66,9 @@ namespace Minigames.Airplane.UI
private void Update()
{
if (!_isActive || _planeTransform == null) return;
// Only update if active and we have at least one transform to calculate from
if (!_isActive) return;
if (_planeTransform == null && _launchPointTransform == null) return;
// Update distance at specified interval
_frameCounter++;
@@ -83,6 +85,7 @@ namespace Minigames.Airplane.UI
/// <summary>
/// Setup the target display with icon and target position.
/// Activates tracking using launch point for distance calculation.
/// </summary>
/// <param name="targetSprite">Sprite to display as target icon</param>
/// <param name="targetPosition">World position of the target</param>
@@ -99,17 +102,22 @@ namespace Minigames.Airplane.UI
targetIcon.enabled = true;
}
// Activate tracking so distance updates even before plane spawns
_isActive = true;
_frameCounter = 0;
// Update distance immediately using launch point
UpdateDistance();
if (showDebugLogs)
{
Logging.Debug($"[TargetDisplayUI] Setup with target at {targetPosition}");
Logging.Debug($"[TargetDisplayUI] Setup with target at {targetPosition}, launch point at {launchPoint?.position ?? Vector3.zero}");
}
}
/// <summary>
/// Start tracking the airplane and updating distance.
/// Switches distance calculation from launch point to airplane position.
/// Note: Does not automatically show UI - call Show() separately.
/// </summary>
/// <param name="planeTransform">Transform of the airplane to track</param>
@@ -119,7 +127,7 @@ namespace Minigames.Airplane.UI
_isActive = true;
_frameCounter = 0;
// Update distance immediately if visible
// Update distance immediately if visible (now using plane position)
if (gameObject.activeSelf)
{
UpdateDistance();
@@ -133,16 +141,24 @@ namespace Minigames.Airplane.UI
/// <summary>
/// Stop tracking the airplane.
/// Reverts to using launch point for distance calculation if available.
/// Note: Does not automatically hide UI - call Hide() separately.
/// </summary>
public void StopTracking()
{
_isActive = false;
_planeTransform = null;
// Keep _isActive true so we can show distance from launch point
// Will be set false when Hide() is called
// Update immediately to show launch point distance again
if (_launchPointTransform != null && gameObject.activeSelf)
{
UpdateDistance();
}
if (showDebugLogs)
{
Logging.Debug("[TargetDisplayUI] Stopped tracking");
Logging.Debug("[TargetDisplayUI] Stopped tracking airplane, reverted to launch point");
}
}
@@ -155,10 +171,11 @@ namespace Minigames.Airplane.UI
}
/// <summary>
/// Hide the UI.
/// Hide the UI and deactivate tracking.
/// </summary>
public void Hide()
{
_isActive = false;
gameObject.SetActive(false);
}