From 6d4080438d5d3c09cf1b0d3d742928e78f4cc93d Mon Sep 17 00:00:00 2001 From: Michal Pikulski Date: Thu, 4 Dec 2025 15:10:20 +0100 Subject: [PATCH] Working MVP code for Valentines --- Assets/Scenes/MiniGames/FortFight.unity | 16 +- .../MiniGames/ValentineNoteDelivery.unity | 125 +--- Assets/Scripts/Common.meta | 3 + Assets/Scripts/Common/Camera.meta | 3 + .../Common/Camera/CameraStateManager.cs | 169 ++++++ .../Common/Camera/CameraStateManager.cs.meta | 3 + Assets/Scripts/Common/Input.meta | 3 + .../Common/Input/DragLaunchController.cs | 373 ++++++++++++ .../Common/Input/DragLaunchController.cs.meta | 3 + .../Scripts/Common/Input/SlingshotConfig.cs | 59 ++ .../Common/Input/SlingshotConfig.cs.meta | 3 + Assets/Scripts/Core/GameManager.cs | 14 +- .../Core/Settings/SettingsInterfaces.cs | 28 + Assets/Scripts/Input/InputManager.cs | 2 +- Assets/Scripts/Minigames/Airplane.meta | 3 + Assets/Scripts/Minigames/Airplane/Core.meta | 3 + .../Airplane/Core/AirplaneCameraManager.cs | 160 +++++ .../Core/AirplaneCameraManager.cs.meta | 3 + .../Airplane/Core/AirplaneController.cs | 281 +++++++++ .../Airplane/Core/AirplaneController.cs.meta | 3 + .../Airplane/Core/AirplaneGameManager.cs | 553 ++++++++++++++++++ .../Airplane/Core/AirplaneGameManager.cs.meta | 3 + .../Airplane/Core/AirplaneLaunchController.cs | 252 ++++++++ .../Core/AirplaneLaunchController.cs.meta | 3 + .../Airplane/Core/AirplaneTargetValidator.cs | 186 ++++++ .../Core/AirplaneTargetValidator.cs.meta | 3 + .../Minigames/Airplane/Core/PersonQueue.cs | 197 +++++++ .../Airplane/Core/PersonQueue.cs.meta | 3 + Assets/Scripts/Minigames/Airplane/Data.meta | 3 + .../Airplane/Data/AirplaneCameraState.cs | 14 + .../Airplane/Data/AirplaneCameraState.cs.meta | 3 + .../Airplane/Data/AirplaneGameState.cs | 16 + .../Airplane/Data/AirplaneGameState.cs.meta | 3 + .../Minigames/Airplane/Data/PersonData.cs | 52 ++ .../Airplane/Data/PersonData.cs.meta | 3 + .../Scripts/Minigames/Airplane/Settings.meta | 3 + .../Airplane/Settings/AirplaneSettings.cs | 70 +++ .../Settings/AirplaneSettings.cs.meta | 3 + .../Scripts/Minigames/Airplane/Targets.meta | 3 + .../Airplane/Targets/AirplaneTarget.cs | 151 +++++ .../Airplane/Targets/AirplaneTarget.cs.meta | 3 + .../FortFight/Core/FortFightSettings.cs | 16 + .../FortFight/Core/SlingshotController.cs | 214 ++----- Assets/Settings/FortFightSettings.asset | 11 + 44 files changed, 2731 insertions(+), 294 deletions(-) create mode 100644 Assets/Scripts/Common.meta create mode 100644 Assets/Scripts/Common/Camera.meta create mode 100644 Assets/Scripts/Common/Camera/CameraStateManager.cs create mode 100644 Assets/Scripts/Common/Camera/CameraStateManager.cs.meta create mode 100644 Assets/Scripts/Common/Input.meta create mode 100644 Assets/Scripts/Common/Input/DragLaunchController.cs create mode 100644 Assets/Scripts/Common/Input/DragLaunchController.cs.meta create mode 100644 Assets/Scripts/Common/Input/SlingshotConfig.cs create mode 100644 Assets/Scripts/Common/Input/SlingshotConfig.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneController.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneController.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneLaunchController.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneLaunchController.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneTargetValidator.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/AirplaneTargetValidator.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Data.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Data/AirplaneGameState.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Data/AirplaneGameState.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Data/PersonData.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Data/PersonData.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Settings.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Targets.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Targets/AirplaneTarget.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Targets/AirplaneTarget.cs.meta diff --git a/Assets/Scenes/MiniGames/FortFight.unity b/Assets/Scenes/MiniGames/FortFight.unity index 324ce617..ed953a92 100644 --- a/Assets/Scenes/MiniGames/FortFight.unity +++ b/Assets/Scenes/MiniGames/FortFight.unity @@ -2874,9 +2874,11 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: fc81b72132764f09a0ba180c90b432cf, type: 3} m_Name: m_EditorClassIdentifier: AppleHillsScripts::Minigames.FortFight.Core.SlingshotController - maxDragDistance: 5 - projectileSpawnPoint: {fileID: 1668202570} - trajectoryPreview: {fileID: 0} + maxDragDistanceOverride: 0 + maxForceOverride: 0 + launchAnchor: {fileID: 1668202570} + showDebugLogs: 1 + trajectoryPreview: {fileID: 841922115} --- !u!1 &846792101 GameObject: m_ObjectHideFlags: 0 @@ -4525,9 +4527,11 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: fc81b72132764f09a0ba180c90b432cf, type: 3} m_Name: m_EditorClassIdentifier: AppleHillsScripts::Minigames.FortFight.Core.SlingshotController - maxDragDistance: 5 - projectileSpawnPoint: {fileID: 497509525} - trajectoryPreview: {fileID: 0} + maxDragDistanceOverride: 0 + maxForceOverride: 0 + launchAnchor: {fileID: 497509525} + showDebugLogs: 1 + trajectoryPreview: {fileID: 1460473368} --- !u!1 &1543340062 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity b/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity index bddd4be5..655ca311 100644 --- a/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity +++ b/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity @@ -119,85 +119,6 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} ---- !u!1 &580848252 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 580848255} - - component: {fileID: 580848254} - - component: {fileID: 580848253} - m_Layer: 0 - m_Name: EventSystem - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &580848253 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 580848252} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3} - m_Name: - m_EditorClassIdentifier: - m_SendPointerHoverToParent: 1 - m_MoveRepeatDelay: 0.5 - m_MoveRepeatRate: 0.1 - m_XRTrackingOrigin: {fileID: 0} - m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_DeselectOnBackgroundClick: 1 - m_PointerBehavior: 0 - m_CursorLockBehavior: 0 - m_ScrollDeltaPerTick: 6 ---- !u!114 &580848254 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 580848252} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} - m_Name: - m_EditorClassIdentifier: - m_FirstSelected: {fileID: 0} - m_sendNavigationEvents: 1 - m_DragThreshold: 10 ---- !u!4 &580848255 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 580848252} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1810521056 GameObject: m_ObjectHideFlags: 0 @@ -362,7 +283,7 @@ Transform: m_GameObject: {fileID: 1810521056} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -34.3, y: -36.3, z: -10} + m_LocalPosition: {x: 0, y: 0, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -378,8 +299,6 @@ GameObject: m_Component: - component: {fileID: 2103114178} - component: {fileID: 2103114177} - - component: {fileID: 2103114176} - - component: {fileID: 2103114175} m_Layer: 0 m_Name: CinemachineCamera m_TagString: Untagged @@ -387,45 +306,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &2103114175 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2103114174} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f453f694addf4275988fac205bc91968, type: 3} - m_Name: - m_EditorClassIdentifier: - BoundingShape2D: {fileID: 0} - Damping: 3 - SlowingDistance: 20 - OversizeWindow: - Enabled: 0 - MaxWindowSize: 0 - Padding: 0 - m_LegacyMaxWindowSize: -2 ---- !u!114 &2103114176 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2103114174} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: b617507da6d07e749b7efdb34e1173e1, type: 3} - m_Name: - m_EditorClassIdentifier: - TrackerSettings: - BindingMode: 4 - PositionDamping: {x: 2, y: 0.5, z: 1} - AngularDampingMode: 0 - RotationDamping: {x: 1, y: 1, z: 1} - QuaternionDamping: 1 - FollowOffset: {x: 0, y: 0, z: -10} --- !u!114 &2103114177 MonoBehaviour: m_ObjectHideFlags: 0 @@ -478,7 +358,7 @@ Transform: m_GameObject: {fileID: 2103114174} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -34.3, y: -36.3, z: -10} + m_LocalPosition: {x: 0, y: 0, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -489,5 +369,4 @@ SceneRoots: m_ObjectHideFlags: 0 m_Roots: - {fileID: 1810521061} - - {fileID: 580848255} - {fileID: 2103114178} diff --git a/Assets/Scripts/Common.meta b/Assets/Scripts/Common.meta new file mode 100644 index 00000000..9163167c --- /dev/null +++ b/Assets/Scripts/Common.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 70833f6496d94acab58cfe981c757d2d +timeCreated: 1764851204 \ No newline at end of file diff --git a/Assets/Scripts/Common/Camera.meta b/Assets/Scripts/Common/Camera.meta new file mode 100644 index 00000000..c9babf97 --- /dev/null +++ b/Assets/Scripts/Common/Camera.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 44c4b5c8fcd54d1887fb05ca65a9bb20 +timeCreated: 1764851223 \ No newline at end of file diff --git a/Assets/Scripts/Common/Camera/CameraStateManager.cs b/Assets/Scripts/Common/Camera/CameraStateManager.cs new file mode 100644 index 00000000..dc6e39cd --- /dev/null +++ b/Assets/Scripts/Common/Camera/CameraStateManager.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using Core; +using Core.Lifecycle; +using Unity.Cinemachine; +using UnityEngine; + +namespace Common.Camera +{ + /// + /// Generic state-based camera controller using Cinemachine. + /// Manages camera transitions by setting priorities on virtual cameras. + /// Type parameter TState must be an enum representing camera states. + /// + public abstract class CameraStateManager : ManagedBehaviour where TState : Enum + { + #region Configuration + + [Header("Camera Priority Settings")] + [Tooltip("Priority for inactive cameras")] + [SerializeField] protected int inactivePriority = 10; + + [Tooltip("Priority for the active camera")] + [SerializeField] protected int activePriority = 20; + + [Header("Debug")] + [SerializeField] protected bool showDebugLogs = false; + + #endregion + + #region State + + private Dictionary _cameraMap = new Dictionary(); + private TState _currentState; + private bool _isInitialized = false; + + public TState CurrentState => _currentState; + + #endregion + + #region Events + + /// + /// Fired when camera state changes. Parameters: (TState oldState, TState newState) + /// + public event Action OnStateChanged; + + #endregion + + #region Initialization + + /// + /// Register a camera for a specific state. + /// Call this in subclass OnManagedAwake to set up the camera map. + /// + protected void RegisterCamera(TState state, CinemachineCamera pCamera) + { + if (pCamera == null) + { + Logging.Warning($"[{GetType().Name}] Attempted to register null camera for state {state}"); + return; + } + + if (_cameraMap.ContainsKey(state)) + { + Logging.Warning($"[{GetType().Name}] Camera for state {state} already registered, overwriting"); + } + + _cameraMap[state] = pCamera; + + // Set all cameras to inactive priority initially + pCamera.Priority.Value = inactivePriority; + + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Registered camera '{pCamera.gameObject.name}' for state {state}"); + } + + /// + /// Finalize initialization after all cameras are registered. + /// Call this at the end of subclass OnManagedAwake. + /// + protected void FinalizeInitialization() + { + _isInitialized = true; + + if (_cameraMap.Count == 0) + { + Logging.Warning($"[{GetType().Name}] No cameras registered!"); + } + + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Initialized with {_cameraMap.Count} cameras"); + } + + #endregion + + #region State Management + + /// + /// Switch to a specific camera state + /// + public virtual void SwitchToState(TState newState) + { + if (!_isInitialized) + { + Logging.Error($"[{GetType().Name}] Cannot switch state - not initialized!"); + return; + } + + if (!_cameraMap.ContainsKey(newState)) + { + Logging.Error($"[{GetType().Name}] No camera registered for state {newState}!"); + return; + } + + TState oldState = _currentState; + _currentState = newState; + + // Set all cameras to inactive priority + foreach (var kvp in _cameraMap) + { + kvp.Value.Priority.Value = inactivePriority; + } + + // Set target camera to active priority + _cameraMap[newState].Priority.Value = activePriority; + + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Switched from {oldState} to {newState} (camera: {_cameraMap[newState].gameObject.name})"); + + OnStateChanged?.Invoke(oldState, newState); + } + + /// + /// Get the camera for a specific state + /// + public CinemachineCamera GetCamera(TState state) + { + if (_cameraMap.TryGetValue(state, out CinemachineCamera pCamera)) + { + return pCamera; + } + + Logging.Warning($"[{GetType().Name}] No camera found for state {state}"); + return null; + } + + /// + /// Check if a camera is registered for a state + /// + public bool HasCamera(TState state) + { + return _cameraMap.ContainsKey(state); + } + + #endregion + + #region Validation + + /// + /// Validate that all required states have cameras registered. + /// Subclasses can override to add custom validation. + /// + protected virtual void ValidateCameras() + { + // Subclasses should implement specific validation + } + + #endregion + } +} + diff --git a/Assets/Scripts/Common/Camera/CameraStateManager.cs.meta b/Assets/Scripts/Common/Camera/CameraStateManager.cs.meta new file mode 100644 index 00000000..c0633017 --- /dev/null +++ b/Assets/Scripts/Common/Camera/CameraStateManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c4fc438e61b94c529f7d1e8fe9fb70fa +timeCreated: 1764851223 \ No newline at end of file diff --git a/Assets/Scripts/Common/Input.meta b/Assets/Scripts/Common/Input.meta new file mode 100644 index 00000000..327b6514 --- /dev/null +++ b/Assets/Scripts/Common/Input.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 35838202f1ac4fa4b606b0582fa4e439 +timeCreated: 1764851204 \ No newline at end of file diff --git a/Assets/Scripts/Common/Input/DragLaunchController.cs b/Assets/Scripts/Common/Input/DragLaunchController.cs new file mode 100644 index 00000000..95b2554f --- /dev/null +++ b/Assets/Scripts/Common/Input/DragLaunchController.cs @@ -0,0 +1,373 @@ +using System; +using Core; +using Core.Lifecycle; +using Input; +using UnityEngine; + +namespace Common.Input +{ + /// + /// Base class for drag-to-launch mechanics (Angry Birds style). + /// Provides core drag logic, force calculation, and input handling. + /// Uses SlingshotConfig for all settings - fully configuration-driven. + /// Subclasses implement visual feedback and specific launch behavior. + /// + public abstract class DragLaunchController : ManagedBehaviour, ITouchInputConsumer + { + #region Events + + /// + /// Fired when drag starts. Parameters: (Vector2 startPosition) + /// + public event Action OnDragStart; + + /// + /// Fired during drag update. Parameters: (Vector2 currentPosition, Vector2 direction, float force) + /// + public event Action OnDragUpdate; + + /// + /// Fired when drag ends. Parameters: (Vector2 endPosition, Vector2 direction, float force) + /// + public event Action OnDragEnd; + + /// + /// Fired when launch occurs. Parameters: (Vector2 direction, float force) + /// + public event Action OnLaunch; + + #endregion + + #region Settings + + private SlingshotConfig _config; + + protected SlingshotConfig Config + { + get + { + if (_config == null) + { + _config = GetSlingshotConfig(); + } + return _config; + } + } + + /// + /// Subclasses implement to return their slingshot configuration + /// from their specific settings object + /// + protected abstract SlingshotConfig GetSlingshotConfig(); + + /// + /// Subclasses implement to return the projectile prefab that will be launched. + /// Used for reading Rigidbody2D properties (mass, gravityScale) for trajectory calculations. + /// + protected abstract GameObject GetProjectilePrefab(); + + #endregion + + #region Inspector Properties + + [Header("Launch Settings Overrides (leave 0 to use config)")] + [Tooltip("Override max drag distance (0 = use config)")] + [SerializeField] protected float maxDragDistanceOverride = 0f; + + [Tooltip("Override max force (0 = use config)")] + [SerializeField] protected float maxForceOverride = 0f; + + [Header("References")] + [Tooltip("Launch anchor point (spawn/slingshot position)")] + [SerializeField] protected Transform launchAnchor; + + [Header("Debug")] + [SerializeField] protected bool showDebugLogs; + + #endregion + + #region Computed Properties + + protected float MaxDragDistance => maxDragDistanceOverride > 0 ? maxDragDistanceOverride : Config?.maxDragDistance ?? 5f; + protected float MaxForce => maxForceOverride > 0 ? maxForceOverride : Config?.baseLaunchForce ?? 20f; + + #endregion + + #region State + + private bool _isDragging; + private Vector2 _dragStartPosition; + private bool _isEnabled = false; + private bool _isRegistered = false; + + public bool IsDragging => _isDragging; + public bool IsEnabled => _isEnabled; + + #endregion + + #region Lifecycle + + internal override void OnManagedAwake() + { + base.OnManagedAwake(); + + if (launchAnchor == null) + { + launchAnchor = transform; + } + } + + #endregion + + #region Enable/Disable + + /// + /// Enable the launch controller and register with InputManager + /// + public virtual void Enable() + { + _isEnabled = true; + + // Register with InputManager as override consumer + if (InputManager.Instance != null && !_isRegistered) + { + InputManager.Instance.RegisterOverrideConsumer(this); + _isRegistered = true; + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Registered with InputManager"); + } + + // Show preview visuals + ShowPreview(); + + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Enabled"); + } + + /// + /// Disable the launch controller and unregister from InputManager + /// + public virtual void Disable() + { + _isEnabled = false; + _isDragging = false; + + // Unregister from InputManager + if (InputManager.Instance != null && _isRegistered) + { + InputManager.Instance.UnregisterOverrideConsumer(this); + _isRegistered = false; + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Unregistered from InputManager"); + } + + // Hide preview visuals + HidePreview(); + + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Disabled"); + } + + #endregion + + #region ITouchInputConsumer Implementation + + public void OnTap(Vector2 worldPosition) + { + // Drag-to-launch uses hold/drag, not tap + } + + public void OnHoldStart(Vector2 worldPosition) + { + if (!_isEnabled) return; + StartDrag(worldPosition); + } + + public void OnHoldMove(Vector2 worldPosition) + { + if (!_isEnabled || !_isDragging) return; + UpdateDrag(worldPosition); + } + + public void OnHoldEnd(Vector2 worldPosition) + { + if (!_isEnabled || !_isDragging) return; + EndDrag(worldPosition); + } + + #endregion + + #region Drag Handling + + /// + /// Start drag operation + /// + protected virtual void StartDrag(Vector2 worldPosition) + { + _isDragging = true; + // Use launch anchor as the reference point (like Angry Birds) + _dragStartPosition = launchAnchor.position; + + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Started drag at {worldPosition}, anchor at {_dragStartPosition}"); + + OnDragStart?.Invoke(worldPosition); + } + + /// + /// Update drag operation + /// + protected virtual void UpdateDrag(Vector2 currentWorldPosition) + { + // Calculate drag vector from anchor to current drag position + // Pull back (away from anchor) = launch forward (toward anchor direction) + Vector2 dragVector = _dragStartPosition - currentWorldPosition; + + // Calculate force and direction + float dragDistance = dragVector.magnitude; + float dragRatio = Mathf.Clamp01(dragDistance / MaxDragDistance); + + // Use config to calculate force with multipliers + float force = Config?.CalculateForce(dragDistance, dragRatio) ?? (dragRatio * MaxForce); + Vector2 direction = dragVector.normalized; + float mass = GetProjectileMass(); + + // Warn if mass is zero or invalid + if (mass <= 0f && showDebugLogs) + { + Logging.Warning($"[{GetType().Name}] Projectile mass is {mass}! Trajectory calculation will be inaccurate. Override GetProjectileMass()."); + } + + // Update visuals with mass parameter + UpdateVisuals(currentWorldPosition, direction, force, dragDistance, mass); + + OnDragUpdate?.Invoke(currentWorldPosition, direction, force); + } + + /// + /// End drag operation and potentially launch + /// + protected virtual void EndDrag(Vector2 currentWorldPosition) + { + _isDragging = false; + + // Hide preview + HidePreview(); + + // Calculate final launch parameters + Vector2 dragVector = _dragStartPosition - currentWorldPosition; + float dragDistance = dragVector.magnitude; + float dragRatio = Mathf.Clamp01(dragDistance / MaxDragDistance); + + // Use config to calculate force with multipliers + float force = Config?.CalculateForce(dragDistance, dragRatio) ?? (dragRatio * MaxForce); + Vector2 direction = dragVector.normalized; + + // Get minimum force from config + float minForce = Config?.GetMinForce() ?? (MaxForce * 0.1f); + + if (showDebugLogs) + Logging.Debug($"[{GetType().Name}] Drag ended - Force: {force:F2}, Min: {minForce:F2}, Distance: {dragDistance:F2}"); + + OnDragEnd?.Invoke(currentWorldPosition, direction, force); + + // Launch if force exceeds minimum + if (force >= minForce) + { + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Launching with force {force:F2}"); + PerformLaunch(direction, force); + OnLaunch?.Invoke(direction, force); + } + else + { + if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Drag too short - force {force:F2} < min {minForce:F2}"); + } + } + + #endregion + + #region Abstract Methods - Subclass Implementation + + /// + /// Update visual feedback during drag (trajectory preview, rubber band, etc.) + /// + protected abstract void UpdateVisuals(Vector2 currentPosition, Vector2 direction, float force, float dragDistance, float mass); + + /// + /// Show preview visuals when controller is enabled + /// + protected abstract void ShowPreview(); + + /// + /// Hide preview visuals when controller is disabled + /// + protected abstract void HidePreview(); + + /// + /// Perform the actual launch (spawn projectile/airplane, apply force, etc.) + /// + protected abstract void PerformLaunch(Vector2 direction, float force); + + #endregion + + #region Virtual Methods - Optional Override + + /// + /// Get projectile mass for trajectory calculation. + /// Reads from the prefab's Rigidbody2D component. + /// Subclasses can override for custom behavior (e.g., if mass changes dynamically). + /// + protected virtual float GetProjectileMass() + { + GameObject prefab = GetProjectilePrefab(); + if (prefab == null) + { + if (showDebugLogs) + Logging.Warning($"[{GetType().Name}] GetProjectilePrefab() returned null!"); + return 0f; + } + + var rb = prefab.GetComponent(); + if (rb == null) + { + if (showDebugLogs) + Logging.Warning($"[{GetType().Name}] Projectile prefab '{prefab.name}' has no Rigidbody2D!"); + return 0f; + } + + return rb.mass; + } + + /// + /// Get gravity value for trajectory calculation. + /// Uses Physics2D.gravity.magnitude * prefab's Rigidbody2D gravityScale. + /// + protected virtual float GetGravity() + { + GameObject prefab = GetProjectilePrefab(); + if (prefab == null) + { + // Fallback to project gravity + return Physics2D.gravity.magnitude; + } + + var rb = prefab.GetComponent(); + float gravityScale = rb != null ? rb.gravityScale : 1f; + + return Physics2D.gravity.magnitude * gravityScale; + } + + #endregion + + #region Cleanup + + internal override void OnManagedDestroy() + { + base.OnManagedDestroy(); + + // Ensure we unregister from InputManager + if (_isRegistered && InputManager.Instance != null) + { + InputManager.Instance.UnregisterOverrideConsumer(this); + } + } + + #endregion + } +} + diff --git a/Assets/Scripts/Common/Input/DragLaunchController.cs.meta b/Assets/Scripts/Common/Input/DragLaunchController.cs.meta new file mode 100644 index 00000000..e09d590c --- /dev/null +++ b/Assets/Scripts/Common/Input/DragLaunchController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 44e042d1338149f6bb8adf6129e1c6c2 +timeCreated: 1764851204 \ No newline at end of file diff --git a/Assets/Scripts/Common/Input/SlingshotConfig.cs b/Assets/Scripts/Common/Input/SlingshotConfig.cs new file mode 100644 index 00000000..d69854d2 --- /dev/null +++ b/Assets/Scripts/Common/Input/SlingshotConfig.cs @@ -0,0 +1,59 @@ +using System; +using UnityEngine; + +namespace Common.Input +{ + /// + /// Configuration for slingshot launch mechanics. + /// Can be embedded in any minigame settings that use drag-to-launch. + /// + [Serializable] + public class SlingshotConfig + { + [Header("Drag & Force Settings")] + [Tooltip("Distance to reach max force")] + public float maxDragDistance = 5f; + + [Tooltip("Base force value")] + public float baseLaunchForce = 20f; + + [Tooltip("Minimum threshold (0-1)")] + [Range(0f, 1f)] + public float minForceMultiplier = 0.1f; + + [Tooltip("Maximum cap (0-2, usually 1)")] + [Range(0f, 2f)] + public float maxForceMultiplier = 1f; + + [Header("Trajectory Settings")] + [Tooltip("Number of preview points")] + public int trajectoryPoints = 50; + + [Tooltip("Time between points")] + public float trajectoryTimeStep = 0.1f; + + [Tooltip("Show trajectory after launch (seconds, 0 = no lock)")] + public float trajectoryLockDuration = 2f; + + [Header("Input")] + [Tooltip("Auto-register with InputManager on Enable()")] + public bool autoRegisterInput = true; + + /// + /// Calculate force from drag parameters using configured multipliers + /// + public float CalculateForce(float dragDistance, float dragRatio) + { + return dragRatio * maxForceMultiplier * baseLaunchForce; + } + + /// + /// Calculate minimum force threshold + /// + public float GetMinForce() + { + return baseLaunchForce * minForceMultiplier; + } + } +} + diff --git a/Assets/Scripts/Common/Input/SlingshotConfig.cs.meta b/Assets/Scripts/Common/Input/SlingshotConfig.cs.meta new file mode 100644 index 00000000..5fa8c7d5 --- /dev/null +++ b/Assets/Scripts/Common/Input/SlingshotConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: be4f5d5fd7084425a7bf28a1fadf125e +timeCreated: 1764854225 \ No newline at end of file diff --git a/Assets/Scripts/Core/GameManager.cs b/Assets/Scripts/Core/GameManager.cs index e238829f..545e3370 100644 --- a/Assets/Scripts/Core/GameManager.cs +++ b/Assets/Scripts/Core/GameManager.cs @@ -5,6 +5,7 @@ using AppleHills.Core.Settings; using Core.Lifecycle; using Core.Settings; using Input; +using Minigames.Airplane.Settings; using Minigames.FortFight.Core; using UnityEngine; @@ -175,6 +176,7 @@ namespace Core var birdPooperSettings = SettingsProvider.Instance.LoadSettingsSynchronous(); var statueDressupSettings = SettingsProvider.Instance.LoadSettingsSynchronous(); var fortFightSettings = SettingsProvider.Instance.LoadSettingsSynchronous(); + var airplaneSettings = SettingsProvider.Instance.LoadSettingsSynchronous(); // Register settings with service locator @@ -257,11 +259,21 @@ namespace Core { Debug.LogError("Failed to load FortFightSettings"); } + + if (airplaneSettings != null) + { + ServiceLocator.Register(airplaneSettings); + Logging.Debug("AirplaneSettings registered successfully"); + } + else + { + Debug.LogError("Failed to load AirplaneSettings"); + } // Log success _settingsLoaded = playerSettings != null && interactionSettings != null && minigameSettings != null && cardSystemSettings != null && birdPooperSettings != null && statueDressupSettings != null - && fortFightSettings != null; + && fortFightSettings != null && sortingGameSettings != null && airplaneSettings != null; if (_settingsLoaded) { Logging.Debug("All settings loaded and registered with ServiceLocator"); diff --git a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs index 1f3084f2..4b38f2da 100644 --- a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs +++ b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs @@ -219,6 +219,9 @@ namespace AppleHills.Core.Settings /// public interface IFortFightSettings { + // Slingshot Configuration + Common.Input.SlingshotConfig SlingshotSettings { get; } + // Block configurations System.Collections.Generic.List MaterialConfigs { get; } System.Collections.Generic.List SizeConfigs { get; } @@ -279,4 +282,29 @@ namespace AppleHills.Core.Settings Minigames.FortFight.Settings.BlockMaterialConfig GetMaterialConfig(Minigames.FortFight.Data.BlockMaterial material); Minigames.FortFight.Settings.BlockSizeConfig GetSizeConfig(Minigames.FortFight.Data.BlockSize size); } + + /// + /// Interface for Airplane minigame settings + /// + public interface IAirplaneSettings + { + // Slingshot Configuration + Common.Input.SlingshotConfig SlingshotSettings { get; } + + // Flight Settings + float AirplaneMass { get; } + float MaxFlightTime { get; } + + // Camera Settings + float CameraFollowSmoothing { get; } + float FlightCameraZoom { get; } + + // Timing + float IntroDuration { get; } + float PersonIntroDuration { get; } + float EvaluationDuration { get; } + + // Debug + bool ShowDebugLogs { get; } + } } diff --git a/Assets/Scripts/Input/InputManager.cs b/Assets/Scripts/Input/InputManager.cs index ec1f7903..93580401 100644 --- a/Assets/Scripts/Input/InputManager.cs +++ b/Assets/Scripts/Input/InputManager.cs @@ -15,7 +15,7 @@ namespace Input UI, GameAndUI, InputDisabled - } + } /// /// Handles input events and dispatches them to the appropriate ITouchInputConsumer. diff --git a/Assets/Scripts/Minigames/Airplane.meta b/Assets/Scripts/Minigames/Airplane.meta new file mode 100644 index 00000000..5553dbda --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4cdcfc21e5ec473dafc45f1ae16624b2 +timeCreated: 1764851234 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core.meta b/Assets/Scripts/Minigames/Airplane/Core.meta new file mode 100644 index 00000000..fc37aad0 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 48e6932cbd9645bfac8add678e705033 +timeCreated: 1764851249 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs new file mode 100644 index 00000000..d48dba7f --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs @@ -0,0 +1,160 @@ +using Common.Camera; +using Core; +using Minigames.Airplane.Data; +using Unity.Cinemachine; +using UnityEngine; + +namespace Minigames.Airplane.Core +{ + /// + /// Manages camera states for the airplane minigame. + /// Handles transitions between Intro, NextPerson, Aiming, and Flight cameras. + /// Flight camera includes follow functionality for tracking airplanes. + /// + public class AirplaneCameraManager : CameraStateManager + { + #region Singleton + + private static AirplaneCameraManager _instance; + public static AirplaneCameraManager Instance => _instance; + + #endregion + + #region Inspector References + + [Header("Cinemachine Cameras")] + [Tooltip("Camera for intro sequence")] + [SerializeField] private CinemachineCamera introCamera; + + [Tooltip("Camera for showing the next person")] + [SerializeField] private CinemachineCamera nextPersonCamera; + + [Tooltip("Camera for aiming view")] + [SerializeField] private CinemachineCamera aimingCamera; + + [Tooltip("Camera that follows the airplane (should have CinemachineFollow)")] + [SerializeField] private CinemachineCamera flightCamera; + + #endregion + + #region Lifecycle + + internal override void OnManagedAwake() + { + base.OnManagedAwake(); + + // Set singleton + if (_instance != null && _instance != this) + { + Logging.Warning("[AirplaneCameraManager] Multiple instances detected! Destroying duplicate."); + Destroy(gameObject); + return; + } + _instance = this; + + // Register cameras + RegisterCamera(AirplaneCameraState.Intro, introCamera); + RegisterCamera(AirplaneCameraState.NextPerson, nextPersonCamera); + RegisterCamera(AirplaneCameraState.Aiming, aimingCamera); + RegisterCamera(AirplaneCameraState.Flight, flightCamera); + + // Finalize initialization + FinalizeInitialization(); + + // Validate + ValidateCameras(); + } + + internal override void OnManagedDestroy() + { + base.OnManagedDestroy(); + + if (_instance == this) + { + _instance = null; + } + } + + #endregion + + #region Validation + + protected override void ValidateCameras() + { + if (introCamera == null) + { + Logging.Error("[AirplaneCameraManager] Intro camera not assigned!"); + } + + if (nextPersonCamera == null) + { + Logging.Error("[AirplaneCameraManager] Next person camera not assigned!"); + } + + if (aimingCamera == null) + { + Logging.Error("[AirplaneCameraManager] Aiming camera not assigned!"); + } + + if (flightCamera == null) + { + Logging.Error("[AirplaneCameraManager] Flight camera not assigned!"); + } + else + { + // Verify flight camera has follow component + var followComponent = flightCamera.GetComponent(); + if (followComponent == null) + { + Logging.Warning("[AirplaneCameraManager] Flight camera missing CinemachineFollow component!"); + } + } + } + + #endregion + + #region Flight Camera Follow + + /// + /// Start following an airplane with the flight camera + /// + public void StartFollowingAirplane(Transform airplaneTransform) + { + if (flightCamera == null) + { + Logging.Warning("[AirplaneCameraManager] Cannot follow airplane - flight camera not assigned!"); + return; + } + + if (airplaneTransform == null) + { + Logging.Warning("[AirplaneCameraManager] Cannot follow null airplane transform!"); + return; + } + + // Set the follow target on the flight camera + flightCamera.Target.TrackingTarget = airplaneTransform; + + // Switch to flight camera + SwitchToState(AirplaneCameraState.Flight); + + if (showDebugLogs) Logging.Debug($"[AirplaneCameraManager] Now following airplane: {airplaneTransform.gameObject.name}"); + } + + /// + /// Stop following the airplane and clear the target + /// + public void StopFollowingAirplane() + { + if (flightCamera == null) return; + + // Clear the follow target + flightCamera.Target.TrackingTarget = null; + + if (showDebugLogs) Logging.Debug("[AirplaneCameraManager] Stopped following airplane"); + } + + #endregion + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs.meta new file mode 100644 index 00000000..bcc7d549 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 34b856742e12475793b85a0a3019d67b +timeCreated: 1764851249 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneController.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneController.cs new file mode 100644 index 00000000..6d04d750 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneController.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections; +using Core; +using Core.Lifecycle; +using UnityEngine; + +namespace Minigames.Airplane.Core +{ + /// + /// Controls airplane movement using calculated (non-physics-based) flight. + /// Uses Rigidbody2D for velocity application but not for simulation. + /// Follows an arc trajectory based on launch parameters. + /// + [RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))] + public class AirplaneController : ManagedBehaviour + { + #region Events + + /// + /// Fired when airplane is launched. Parameters: (AirplaneController airplane) + /// + public event Action OnLaunched; + + /// + /// Fired when airplane lands/stops. Parameters: (AirplaneController airplane) + /// + public event Action OnLanded; + + /// + /// Fired when airplane hits a target. Parameters: (AirplaneController airplane, string targetName) + /// + public event Action OnTargetHit; + + /// + /// Fired when airplane times out. Parameters: (AirplaneController airplane) + /// + public event Action OnTimeout; + + #endregion + + #region Inspector Properties + + [Header("Flight Settings")] + [Tooltip("Gravity multiplier for arc calculation")] + [SerializeField] private float gravity = 9.81f; + + [Tooltip("Mass of the airplane")] + [SerializeField] private float mass = 1f; + + [Tooltip("Maximum flight time before timeout (seconds)")] + [SerializeField] private float maxFlightTime = 10f; + + [Header("Visual")] + [Tooltip("Should airplane rotate to face velocity direction?")] + [SerializeField] private bool rotateToVelocity = true; + + [Header("Debug")] + [SerializeField] private bool showDebugLogs = false; + + #endregion + + #region State + + private Rigidbody2D rb2D; + private Collider2D airplaneCollider; + private Vector2 currentVelocity; + private bool isFlying = false; + private float flightTimer = 0f; + private string lastHitTarget = null; + + public bool IsFlying => isFlying; + public Vector2 CurrentVelocity => currentVelocity; + public string LastHitTarget => lastHitTarget; + + #endregion + + #region Lifecycle + + internal override void OnManagedAwake() + { + base.OnManagedAwake(); + + // Cache components + rb2D = GetComponent(); + airplaneCollider = GetComponent(); + + // Configure Rigidbody2D + if (rb2D != null) + { + rb2D.isKinematic = true; // Not physics-simulated + rb2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous; + } + + // Configure Collider2D as trigger + if (airplaneCollider != null) + { + airplaneCollider.isTrigger = true; + } + } + + #endregion + + #region Launch + + /// + /// Launch the airplane with calculated velocity + /// + public void Launch(Vector2 direction, float force) + { + if (isFlying) + { + Logging.Warning($"[AirplaneController] {gameObject.name} already flying!"); + return; + } + + // Calculate initial velocity from force and mass + float initialSpeed = force / mass; + currentVelocity = direction.normalized * initialSpeed; + + isFlying = true; + flightTimer = 0f; + lastHitTarget = null; + + if (showDebugLogs) + { + Logging.Debug($"[AirplaneController] Launched - Force: {force:F2}, Mass: {mass:F2}, " + + $"Initial Speed: {initialSpeed:F2}, Direction: {direction}"); + } + + OnLaunched?.Invoke(this); + + // Start flight update + StartCoroutine(FlightUpdateCoroutine()); + } + + #endregion + + #region Flight Update + + /// + /// Update airplane flight physics each frame + /// + private IEnumerator FlightUpdateCoroutine() + { + while (isFlying) + { + float deltaTime = Time.fixedDeltaTime; + + // Apply gravity to velocity + currentVelocity.y -= gravity * deltaTime; + + // Apply velocity to rigidbody + if (rb2D != null) + { + rb2D.linearVelocity = currentVelocity; + } + + // Rotate to face velocity direction + if (rotateToVelocity && currentVelocity.magnitude > 0.1f) + { + float angle = Mathf.Atan2(currentVelocity.y, currentVelocity.x) * Mathf.Rad2Deg; + transform.rotation = Quaternion.Euler(0, 0, angle); + } + + // Update flight timer + flightTimer += deltaTime; + + // Check for timeout + if (flightTimer >= maxFlightTime) + { + if (showDebugLogs) Logging.Debug("[AirplaneController] Flight timeout reached"); + HandleTimeout(); + yield break; + } + + // Check if airplane has landed (velocity near zero or hit ground) + if (currentVelocity.y < -0.1f && transform.position.y < -10f) // Below screen + { + if (showDebugLogs) Logging.Debug("[AirplaneController] Airplane went off screen"); + HandleLanding(); + yield break; + } + + yield return new WaitForFixedUpdate(); + } + } + + #endregion + + #region Collision Detection + + /// + /// Detect trigger collisions with targets + /// + private void OnTriggerEnter2D(Collider2D other) + { + if (!isFlying) return; + + // Check if it's a target + var target = other.GetComponent(); + if (target != null) + { + lastHitTarget = target.TargetName; + + if (showDebugLogs) Logging.Debug($"[AirplaneController] Hit target: {lastHitTarget}"); + + OnTargetHit?.Invoke(this, lastHitTarget); + + // Land after hitting target + HandleLanding(); + } + } + + #endregion + + #region Landing and Timeout + + /// + /// Handle airplane landing + /// + private void HandleLanding() + { + if (!isFlying) return; + + isFlying = false; + currentVelocity = Vector2.zero; + + if (rb2D != null) + { + rb2D.linearVelocity = Vector2.zero; + } + + if (showDebugLogs) Logging.Debug("[AirplaneController] Airplane landed"); + + OnLanded?.Invoke(this); + } + + /// + /// Handle airplane timeout + /// + private void HandleTimeout() + { + if (!isFlying) return; + + isFlying = false; + currentVelocity = Vector2.zero; + + if (rb2D != null) + { + rb2D.linearVelocity = Vector2.zero; + } + + if (showDebugLogs) Logging.Debug("[AirplaneController] Airplane timed out"); + + OnTimeout?.Invoke(this); + } + + /// + /// Public method to force stop the airplane + /// + public void ForceStop() + { + HandleLanding(); + } + + #endregion + + #region Cleanup + + internal override void OnManagedDestroy() + { + base.OnManagedDestroy(); + + // Stop any coroutines + StopAllCoroutines(); + } + + #endregion + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneController.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/AirplaneController.cs.meta new file mode 100644 index 00000000..8460dff2 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0cdaac23e969495d8c0deeaf236c259e +timeCreated: 1764851277 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs new file mode 100644 index 00000000..46b41dee --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs @@ -0,0 +1,553 @@ +using System; +using System.Collections; +using Core; +using Core.Lifecycle; +using Minigames.Airplane.Data; +using UnityEngine; + +namespace Minigames.Airplane.Core +{ + /// + /// Main game manager for the airplane minigame. + /// Orchestrates game flow through state machine with distinct phases: + /// Intro -> NextPerson -> Aiming -> Flying -> Evaluating -> (repeat or GameOver) + /// + public class AirplaneGameManager : ManagedBehaviour + { + #region Singleton + + private static AirplaneGameManager _instance; + public static AirplaneGameManager Instance => _instance; + + #endregion + + #region Inspector References + + [Header("Core Systems")] + [SerializeField] private PersonQueue personQueue; + [SerializeField] private AirplaneCameraManager cameraManager; + [SerializeField] private AirplaneLaunchController launchController; + [SerializeField] private AirplaneTargetValidator targetValidator; + + [Header("Targets")] + [Tooltip("All targets in the scene (for highlighting)")] + [SerializeField] private Targets.AirplaneTarget[] allTargets; + + [Header("Debug")] + [SerializeField] private bool showDebugLogs = true; + + #endregion + + #region Events + + /// + /// Fired when game state changes. Parameters: (AirplaneGameState oldState, AirplaneGameState newState) + /// + public event Action OnStateChanged; + + /// + /// Fired when a person starts their turn. Parameters: (PersonData person) + /// + public event Action OnPersonStartTurn; + + /// + /// Fired when a person finishes their turn. Parameters: (PersonData person, bool success) + /// + public event Action OnPersonFinishTurn; + + /// + /// Fired when game completes + /// + public event Action OnGameComplete; + + #endregion + + #region State + + private AirplaneGameState _currentState = AirplaneGameState.Intro; + private PersonData _currentPerson; + private AirplaneController _currentAirplane; + private int _successCount; + private int _failCount; + private int _totalTurns; + + public AirplaneGameState CurrentState => _currentState; + public PersonData CurrentPerson => _currentPerson; + public int SuccessCount => _successCount; + public int FailCount => _failCount; + + #endregion + + #region Lifecycle + + internal override void OnManagedAwake() + { + base.OnManagedAwake(); + + // Set singleton + if (_instance != null && _instance != this) + { + Logging.Warning("[AirplaneGameManager] Multiple instances detected! Destroying duplicate."); + Destroy(gameObject); + return; + } + _instance = this; + + // Validate references + ValidateReferences(); + } + + internal override void OnManagedStart() + { + base.OnManagedStart(); + + // Subscribe to events + if (launchController != null) + { + launchController.OnAirplaneLaunched += HandleAirplaneLaunched; + } + + if (targetValidator != null) + { + targetValidator.OnCorrectTargetHit += HandleCorrectTargetHit; + targetValidator.OnWrongTargetHit += HandleWrongTargetHit; + targetValidator.OnMissedAllTargets += HandleMissedTargets; + } + + // Start the game + StartGame(); + } + + internal override void OnManagedDestroy() + { + base.OnManagedDestroy(); + + // Unsubscribe from events + if (launchController != null) + { + launchController.OnAirplaneLaunched -= HandleAirplaneLaunched; + } + + if (targetValidator != null) + { + targetValidator.OnCorrectTargetHit -= HandleCorrectTargetHit; + targetValidator.OnWrongTargetHit -= HandleWrongTargetHit; + targetValidator.OnMissedAllTargets -= HandleMissedTargets; + } + + if (_instance == this) + { + _instance = null; + } + } + + #endregion + + #region Validation + + private void ValidateReferences() + { + if (personQueue == null) + { + Logging.Error("[AirplaneGameManager] PersonQueue not assigned!"); + } + + if (cameraManager == null) + { + Logging.Error("[AirplaneGameManager] AirplaneCameraManager not assigned!"); + } + + if (launchController == null) + { + Logging.Error("[AirplaneGameManager] AirplaneLaunchController not assigned!"); + } + + if (targetValidator == null) + { + Logging.Error("[AirplaneGameManager] AirplaneTargetValidator not assigned!"); + } + + if (allTargets == null || allTargets.Length == 0) + { + Logging.Warning("[AirplaneGameManager] No targets assigned!"); + } + } + + #endregion + + #region State Management + + private void ChangeState(AirplaneGameState newState) + { + AirplaneGameState oldState = _currentState; + _currentState = newState; + + if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] State: {oldState} -> {newState}"); + + OnStateChanged?.Invoke(oldState, newState); + } + + #endregion + + #region Game Flow + + /// + /// Start the game + /// + public void StartGame() + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] ===== GAME STARTING ====="); + + ChangeState(AirplaneGameState.Intro); + StartCoroutine(IntroSequence()); + } + + /// + /// Intro sequence (stub for MVP) + /// + private IEnumerator IntroSequence() + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Playing intro sequence..."); + + // Switch to intro camera + if (cameraManager != null) + { + cameraManager.SwitchToState(AirplaneCameraState.Intro); + } + + // Wait for intro duration (stub) + yield return new WaitForSeconds(1f); + + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Intro complete"); + + // Move to first person + StartCoroutine(SetupNextPerson()); + } + + /// + /// Setup the next person's turn + /// + private IEnumerator SetupNextPerson() + { + // Check if there are more people + if (personQueue == null || !personQueue.HasMorePeople()) + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] No more people, ending game"); + StartCoroutine(GameOver()); + yield break; + } + + ChangeState(AirplaneGameState.NextPerson); + + // Pop next person + _currentPerson = personQueue.PopNextPerson(); + _totalTurns++; + + if (_currentPerson == null) + { + Logging.Error("[AirplaneGameManager] Failed to get next person!"); + yield break; + } + + if (showDebugLogs) + { + Logging.Debug($"[AirplaneGameManager] === Turn {_totalTurns}: {_currentPerson.personName} ===" + + $"\n Target: {_currentPerson.targetName}"); + } + + OnPersonStartTurn?.Invoke(_currentPerson); + + // Switch to next person camera + if (cameraManager != null) + { + cameraManager.SwitchToState(AirplaneCameraState.NextPerson); + } + + // Wait for person introduction (stub) + yield return new WaitForSeconds(1f); + + // Set expected target + if (targetValidator != null) + { + targetValidator.SetExpectedTarget(_currentPerson.targetName); + } + + // Highlight the target + HighlightTarget(_currentPerson.targetName); + + // Enter aiming state + EnterAimingState(); + } + + /// + /// Enter aiming state - player can aim and launch + /// + private void EnterAimingState() + { + ChangeState(AirplaneGameState.Aiming); + + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Ready to aim and launch!"); + + // Switch to aiming camera + if (cameraManager != null) + { + cameraManager.SwitchToState(AirplaneCameraState.Aiming); + } + + // Enable launch controller + if (launchController != null) + { + launchController.Enable(); + } + } + + #endregion + + #region Event Handlers + + /// + /// Handle airplane launched event + /// + private void HandleAirplaneLaunched(AirplaneController airplane) + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Airplane launched!"); + + _currentAirplane = airplane; + + // Disable launch controller + if (launchController != null) + { + launchController.Disable(); + } + + ChangeState(AirplaneGameState.Flying); + + // Start following airplane with camera + if (cameraManager != null) + { + cameraManager.StartFollowingAirplane(airplane.transform); + } + + // Subscribe to airplane events + airplane.OnTargetHit += HandleAirplaneHitTarget; + airplane.OnLanded += HandleAirplaneLanded; + airplane.OnTimeout += HandleAirplaneTimeout; + } + + /// + /// Handle airplane hitting a target + /// + private void HandleAirplaneHitTarget(AirplaneController airplane, string targetName) + { + if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Airplane hit target: {targetName}"); + + // Validate the hit + if (targetValidator != null) + { + targetValidator.ValidateHit(targetName); + } + } + + /// + /// Handle airplane landing + /// + private void HandleAirplaneLanded(AirplaneController airplane) + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Airplane landed"); + + // If no target was hit, count as miss + if (targetValidator != null && !targetValidator.HasValidated) + { + targetValidator.HandleMiss(); + } + + // Evaluate result + StartCoroutine(EvaluateResult()); + } + + /// + /// Handle airplane timeout + /// + private void HandleAirplaneTimeout(AirplaneController airplane) + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Airplane timed out"); + + // Count as miss + if (targetValidator != null && !targetValidator.HasValidated) + { + targetValidator.HandleMiss(); + } + + // Evaluate result + StartCoroutine(EvaluateResult()); + } + + /// + /// Handle correct target hit + /// + private void HandleCorrectTargetHit(string targetName) + { + _successCount++; + if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] ✓ SUCCESS! Hit correct target: {targetName}"); + } + + /// + /// Handle wrong target hit + /// + private void HandleWrongTargetHit(string expectedTarget, string actualTarget) + { + _failCount++; + if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] ✗ FAIL! Expected: {expectedTarget}, Hit: {actualTarget}"); + } + + /// + /// Handle missed all targets + /// + private void HandleMissedTargets() + { + _failCount++; + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] ✗ MISS! Didn't hit any target"); + } + + #endregion + + #region Evaluation and Cleanup + + /// + /// Evaluate the result of the turn + /// + private IEnumerator EvaluateResult() + { + ChangeState(AirplaneGameState.Evaluating); + + // Stop following airplane + if (cameraManager != null) + { + cameraManager.StopFollowingAirplane(); + } + + // Determine success/failure + bool success = targetValidator != null && + targetValidator.HasValidated && + _currentAirplane != null && + !string.IsNullOrEmpty(_currentAirplane.LastHitTarget) && + targetValidator.IsExpectedTarget(_currentAirplane.LastHitTarget); + + if (showDebugLogs) + { + Logging.Debug($"[AirplaneGameManager] Turn result: {(success ? "SUCCESS" : "FAILURE")}" + + $"\n Score: {_successCount} / {_totalTurns}"); + } + + OnPersonFinishTurn?.Invoke(_currentPerson, success); + + // Wait for evaluation display (stub) + yield return new WaitForSeconds(1f); + + // Clean up airplane + if (_currentAirplane != null) + { + Destroy(_currentAirplane.gameObject); + _currentAirplane = null; + } + + // Clear launch controller reference + if (launchController != null) + { + launchController.ClearActiveAirplane(); + } + + // Clear target highlighting + ClearAllTargetHighlights(); + + // Move to next person + StartCoroutine(SetupNextPerson()); + } + + /// + /// Game over - no more people + /// + private IEnumerator GameOver() + { + ChangeState(AirplaneGameState.GameOver); + + if (showDebugLogs) + { + Logging.Debug($"[AirplaneGameManager] ===== GAME OVER =====" + + $"\n Total Turns: {_totalTurns}" + + $"\n Success: {_successCount}" + + $"\n Failures: {_failCount}" + + $"\n Success Rate: {(_totalTurns > 0 ? (_successCount * 100f / _totalTurns) : 0):F1}%"); + } + + OnGameComplete?.Invoke(); + + // Stub: Show game over UI + yield return new WaitForSeconds(2f); + + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Game complete"); + } + + #endregion + + #region Target Management + + /// + /// Highlight a specific target by name + /// + 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}"); + } + + /// + /// Clear all target highlights + /// + private void ClearAllTargetHighlights() + { + if (allTargets == null) return; + + foreach (var target in allTargets) + { + if (target != null) + { + target.SetAsActiveTarget(false); + } + } + } + + #endregion + + #region Public Query Methods + + /// + /// Get current game statistics + /// + public (int total, int success, int fail) GetStatistics() + { + return (_totalTurns, _successCount, _failCount); + } + + /// + /// Check if game is active + /// + public bool IsGameActive() + { + return _currentState != AirplaneGameState.Intro && _currentState != AirplaneGameState.GameOver; + } + + #endregion + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs.meta new file mode 100644 index 00000000..732e5a27 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fd2c6d27dee546479b16d0dfd8c3b2ee +timeCreated: 1764851399 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneLaunchController.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneLaunchController.cs new file mode 100644 index 00000000..57ad3647 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneLaunchController.cs @@ -0,0 +1,252 @@ +using System; +using AppleHills.Core.Settings; +using Common.Input; +using Core; +using Core.Lifecycle; +using Minigames.Airplane.Data; +using UnityEngine; + +namespace Minigames.Airplane.Core +{ + /// + /// Launch controller for the airplane minigame. + /// Extends DragLaunchController with airplane-specific behavior. + /// Spawns and launches airplanes on release. + /// + public class AirplaneLaunchController : DragLaunchController + { + #region Events + + /// + /// Fired when airplane is launched. Parameters: (AirplaneController airplane) + /// + public event Action OnAirplaneLaunched; + + #endregion + + #region Settings + + protected override SlingshotConfig GetSlingshotConfig() + { + return GameManager.GetSettingsObject()?.SlingshotSettings; + } + + protected override GameObject GetProjectilePrefab() + { + return airplanePrefab; + } + + #endregion + + #region Inspector Properties + + [Header("Airplane Setup")] + [Tooltip("Airplane prefab to spawn")] + [SerializeField] private GameObject airplanePrefab; + + [Header("Visual Feedback")] + [Tooltip("Line renderer for trajectory preview (optional)")] + [SerializeField] private LineRenderer trajectoryLine; + + [Tooltip("Visual indicator for launch anchor (optional)")] + [SerializeField] private GameObject anchorVisual; + + #endregion + + #region State + + private AirplaneController _activeAirplane; + + public AirplaneController ActiveAirplane => _activeAirplane; + + #endregion + + #region Lifecycle + + internal override void OnManagedAwake() + { + base.OnManagedAwake(); + + // Validate airplane prefab + if (airplanePrefab == null) + { + Logging.Error("[AirplaneLaunchController] Airplane prefab not assigned!"); + } + else + { + // Verify airplane has AirplaneController + if (airplanePrefab.GetComponent() == null) + { + Logging.Error("[AirplaneLaunchController] Airplane prefab missing AirplaneController component!"); + } + } + + // Setup trajectory line + if (trajectoryLine != null) + { + trajectoryLine.enabled = false; + } + + // Hide anchor visual initially + if (anchorVisual != null) + { + anchorVisual.SetActive(false); + } + } + + #endregion + + #region Visual Feedback + + protected override void ShowPreview() + { + // Show anchor visual + if (anchorVisual != null) + { + anchorVisual.SetActive(true); + } + + // Show trajectory line (will be updated during drag) + if (trajectoryLine != null) + { + trajectoryLine.enabled = false; // Only show during drag + } + + if (showDebugLogs) Logging.Debug("[AirplaneLaunchController] Preview shown"); + } + + protected override void HidePreview() + { + // Hide anchor visual + if (anchorVisual != null) + { + anchorVisual.SetActive(false); + } + + // Hide trajectory line + if (trajectoryLine != null) + { + trajectoryLine.enabled = false; + } + + if (showDebugLogs) Logging.Debug("[AirplaneLaunchController] Preview hidden"); + } + + protected override void UpdateVisuals(Vector2 currentPosition, Vector2 direction, float force, float dragDistance, float mass) + { + // Show trajectory line during drag + if (trajectoryLine != null && trajectoryLine.enabled == false && dragDistance > 0.1f) + { + trajectoryLine.enabled = true; + } + + // Update trajectory preview + if (trajectoryLine != null && trajectoryLine.enabled) + { + UpdateTrajectoryPreview(direction, force, mass); + } + } + + /// + /// Update the trajectory preview line + /// + private void UpdateTrajectoryPreview(Vector2 direction, float force, float mass) + { + if (trajectoryLine == null) return; + + var config = Config; + if (config == null) return; + + if (mass <= 0f) + { + if (showDebugLogs) Logging.Warning("[AirplaneLaunchController] Cannot calculate trajectory with zero mass!"); + return; + } + + Vector2 startPos = launchAnchor.position; + float initialSpeed = force / mass; + Vector2 velocity = direction * initialSpeed; + + // Get gravity from prefab's Rigidbody2D (Physics2D.gravity.magnitude * rb.gravityScale) + float gravity = GetGravity(); + + trajectoryLine.positionCount = config.trajectoryPoints; + + // Calculate trajectory points using config values + for (int i = 0; i < config.trajectoryPoints; i++) + { + float time = i * config.trajectoryTimeStep; + + // Calculate position at this time + float x = startPos.x + velocity.x * time; + float y = startPos.y + velocity.y * time - 0.5f * gravity * time * time; + + trajectoryLine.SetPosition(i, new Vector3(x, y, 0)); + } + } + + #endregion + + #region Launch + + protected override void PerformLaunch(Vector2 direction, float force) + { + if (airplanePrefab == null) + { + Logging.Error("[AirplaneLaunchController] Cannot launch - airplane prefab not assigned!"); + return; + } + + // Spawn airplane at launch anchor + GameObject airplaneObj = Instantiate(airplanePrefab, launchAnchor.position, Quaternion.identity); + _activeAirplane = airplaneObj.GetComponent(); + + if (_activeAirplane == null) + { + Logging.Error("[AirplaneLaunchController] Spawned airplane missing AirplaneController!"); + Destroy(airplaneObj); + return; + } + + // Launch the airplane + _activeAirplane.Launch(direction, force); + + // Hide trajectory preview + if (trajectoryLine != null) + { + trajectoryLine.enabled = false; + } + + if (showDebugLogs) + { + Logging.Debug($"[AirplaneLaunchController] Launched airplane with force {force:F2}, direction {direction}"); + } + + // Fire event + OnAirplaneLaunched?.Invoke(_activeAirplane); + } + + #endregion + + #region Public Methods + + /// + /// Get reference to the currently active airplane (if any) + /// + public AirplaneController GetActiveAirplane() + { + return _activeAirplane; + } + + /// + /// Clear reference to active airplane (called after airplane is destroyed) + /// + public void ClearActiveAirplane() + { + _activeAirplane = null; + } + + #endregion + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneLaunchController.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/AirplaneLaunchController.cs.meta new file mode 100644 index 00000000..3b329113 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneLaunchController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a819923cb68240d494bdcf6d5ecf6b9b +timeCreated: 1764851349 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneTargetValidator.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneTargetValidator.cs new file mode 100644 index 00000000..7f51d478 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneTargetValidator.cs @@ -0,0 +1,186 @@ +using System; +using Core; +using Core.Lifecycle; +using UnityEngine; + +namespace Minigames.Airplane.Core +{ + /// + /// Validates whether the airplane hit the correct target. + /// Singleton for easy access throughout the minigame. + /// + public class AirplaneTargetValidator : ManagedBehaviour + { + #region Singleton + + private static AirplaneTargetValidator _instance; + public static AirplaneTargetValidator Instance => _instance; + + #endregion + + #region Events + + /// + /// Fired when correct target is hit. Parameters: (string targetName) + /// + public event Action OnCorrectTargetHit; + + /// + /// Fired when wrong target is hit. Parameters: (string expectedTarget, string actualTarget) + /// + public event Action OnWrongTargetHit; + + /// + /// Fired when no target is hit + /// + public event Action OnMissedAllTargets; + + #endregion + + #region State + + private string _expectedTargetName = null; + private bool _hasValidatedCurrentShot = false; + + public string ExpectedTargetName => _expectedTargetName; + + #endregion + + #region Configuration + + [Header("Debug")] + [SerializeField] private bool showDebugLogs = false; + + #endregion + + #region Lifecycle + + internal override void OnManagedAwake() + { + base.OnManagedAwake(); + + // Set singleton + if (_instance != null && _instance != this) + { + Logging.Warning("[AirplaneTargetValidator] Multiple instances detected! Destroying duplicate."); + Destroy(gameObject); + return; + } + _instance = this; + } + + internal override void OnManagedDestroy() + { + base.OnManagedDestroy(); + + if (_instance == this) + { + _instance = null; + } + } + + #endregion + + #region Target Setting + + /// + /// Set the expected target for the current shot + /// + public void SetExpectedTarget(string targetName) + { + _expectedTargetName = targetName; + _hasValidatedCurrentShot = false; + + if (showDebugLogs) Logging.Debug($"[AirplaneTargetValidator] Expected target set to: {targetName}"); + } + + /// + /// Clear the expected target + /// + public void ClearExpectedTarget() + { + _expectedTargetName = null; + _hasValidatedCurrentShot = false; + + if (showDebugLogs) Logging.Debug("[AirplaneTargetValidator] Expected target cleared"); + } + + #endregion + + #region Validation + + /// + /// Validate if the hit target matches the expected target + /// + public bool ValidateHit(string hitTargetName) + { + // Prevent multiple validations for the same shot + if (_hasValidatedCurrentShot) + { + if (showDebugLogs) Logging.Debug("[AirplaneTargetValidator] Already validated this shot"); + return false; + } + + _hasValidatedCurrentShot = true; + + if (string.IsNullOrEmpty(_expectedTargetName)) + { + Logging.Warning("[AirplaneTargetValidator] No expected target set!"); + return false; + } + + bool isCorrect = string.Equals(hitTargetName, _expectedTargetName, StringComparison.OrdinalIgnoreCase); + + if (isCorrect) + { + if (showDebugLogs) Logging.Debug($"[AirplaneTargetValidator] ✓ Correct! Hit target: {hitTargetName}"); + OnCorrectTargetHit?.Invoke(hitTargetName); + } + else + { + if (showDebugLogs) Logging.Debug($"[AirplaneTargetValidator] ✗ Wrong! Expected: {_expectedTargetName}, Hit: {hitTargetName}"); + OnWrongTargetHit?.Invoke(_expectedTargetName, hitTargetName); + } + + return isCorrect; + } + + /// + /// Handle case where airplane didn't hit any target + /// + public void HandleMiss() + { + // Prevent multiple validations for the same shot + if (_hasValidatedCurrentShot) + { + return; + } + + _hasValidatedCurrentShot = true; + + if (showDebugLogs) Logging.Debug($"[AirplaneTargetValidator] ✗ Missed! Expected target: {_expectedTargetName}"); + + OnMissedAllTargets?.Invoke(); + } + + #endregion + + #region Query Methods + + /// + /// Check if a target name matches the expected target + /// + public bool IsExpectedTarget(string targetName) + { + return string.Equals(targetName, _expectedTargetName, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Check if validation has been done for current shot + /// + public bool HasValidated => _hasValidatedCurrentShot; + + #endregion + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneTargetValidator.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/AirplaneTargetValidator.cs.meta new file mode 100644 index 00000000..9eb19f7d --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneTargetValidator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3cf7815b220240e090fb5cba4fc7414f +timeCreated: 1764851309 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs b/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs new file mode 100644 index 00000000..0b6b2999 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs @@ -0,0 +1,197 @@ +using System.Collections.Generic; +using Core; +using Core.Lifecycle; +using Minigames.Airplane.Data; +using UnityEngine; + +namespace Minigames.Airplane.Core +{ + /// + /// Manages the queue of people waiting to launch airplanes. + /// Provides methods to pop the next person and track remaining people. + /// + public class PersonQueue : ManagedBehaviour + { + #region Inspector Properties + + [Header("Person Setup")] + [Tooltip("List of people in the queue (order matters)")] + [SerializeField] private List peopleInQueue = new List(); + + [Header("Debug")] + [SerializeField] private bool showDebugLogs = false; + + #endregion + + #region State + + private int _currentTurnNumber = 1; + + public int TotalPeople => peopleInQueue.Count; + public int RemainingPeople => peopleInQueue.Count; + + #endregion + + #region Lifecycle + + internal override void OnManagedAwake() + { + base.OnManagedAwake(); + + ValidateQueue(); + } + + internal override void OnManagedStart() + { + base.OnManagedStart(); + + if (showDebugLogs) + { + Logging.Debug($"[PersonQueue] Initialized with {TotalPeople} people"); + foreach (var person in peopleInQueue) + { + Logging.Debug($" - {person.personName} -> Target: {person.targetName}"); + } + } + } + + #endregion + + #region Validation + + /// + /// Validate the queue setup + /// + private void ValidateQueue() + { + if (peopleInQueue.Count == 0) + { + Logging.Warning("[PersonQueue] No people in queue! Add people in the inspector."); + return; + } + + // Check for missing data + for (int i = 0; i < peopleInQueue.Count; i++) + { + var person = peopleInQueue[i]; + + if (string.IsNullOrEmpty(person.personName)) + { + Logging.Warning($"[PersonQueue] Person at index {i} has no name!"); + } + + if (string.IsNullOrEmpty(person.targetName)) + { + Logging.Warning($"[PersonQueue] Person '{person.personName}' at index {i} has no target assigned!"); + } + + if (person.personTransform == null) + { + Logging.Warning($"[PersonQueue] Person '{person.personName}' at index {i} has no transform reference!"); + } + } + } + + #endregion + + #region Queue Management + + /// + /// Check if there are more people in the queue + /// + public bool HasMorePeople() + { + return peopleInQueue.Count > 0; + } + + /// + /// Get the next person without removing them from the queue + /// + public PersonData PeekNextPerson() + { + if (peopleInQueue.Count == 0) + { + if (showDebugLogs) Logging.Debug("[PersonQueue] Queue is empty!"); + return null; + } + + return peopleInQueue[0]; + } + + /// + /// Pop the next person from the queue + /// + public PersonData PopNextPerson() + { + if (peopleInQueue.Count == 0) + { + if (showDebugLogs) Logging.Debug("[PersonQueue] Queue is empty!"); + return null; + } + + // Get first person + PersonData nextPerson = peopleInQueue[0]; + + // Assign turn number + nextPerson.turnNumber = _currentTurnNumber; + _currentTurnNumber++; + + // Remove from queue + peopleInQueue.RemoveAt(0); + + if (showDebugLogs) + { + Logging.Debug($"[PersonQueue] Popped person: {nextPerson.personName} (Turn {nextPerson.turnNumber}), " + + $"Remaining: {RemainingPeople}"); + } + + return nextPerson; + } + + /// + /// Reset the queue (for testing or replay) + /// + public void ResetQueue(List newQueue) + { + peopleInQueue.Clear(); + peopleInQueue.AddRange(newQueue); + _currentTurnNumber = 1; + + if (showDebugLogs) Logging.Debug($"[PersonQueue] Reset queue with {TotalPeople} people"); + } + + /// + /// Clear the queue + /// + public void Clear() + { + peopleInQueue.Clear(); + _currentTurnNumber = 1; + + if (showDebugLogs) Logging.Debug("[PersonQueue] Queue cleared"); + } + + #endregion + + #region Query Methods + + /// + /// Get count of people still in queue + /// + public int GetRemainingCount() + { + return peopleInQueue.Count; + } + + /// + /// Get the current turn number + /// + public int GetCurrentTurnNumber() + { + return _currentTurnNumber; + } + + #endregion + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs.meta new file mode 100644 index 00000000..7a9cf704 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 77964ec3bd5848a6b947ed4ac9b0ee3f +timeCreated: 1764851326 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Data.meta b/Assets/Scripts/Minigames/Airplane/Data.meta new file mode 100644 index 00000000..41610835 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4d58653664484f58be14ab8089e22ce3 +timeCreated: 1764851234 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs b/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs new file mode 100644 index 00000000..3068533f --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs @@ -0,0 +1,14 @@ +namespace Minigames.Airplane.Data +{ + /// + /// Camera states for the airplane minigame + /// + public enum AirplaneCameraState + { + Intro, // Intro sequence camera + NextPerson, // Camera focusing on the next person + Aiming, // Camera for aiming the airplane + Flight // Camera following the airplane in flight + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs.meta b/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs.meta new file mode 100644 index 00000000..f0d441d3 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f5b6a3623e7040be9dfeac6ee8e195cf +timeCreated: 1764851235 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Data/AirplaneGameState.cs b/Assets/Scripts/Minigames/Airplane/Data/AirplaneGameState.cs new file mode 100644 index 00000000..da80a4cb --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/AirplaneGameState.cs @@ -0,0 +1,16 @@ +namespace Minigames.Airplane.Data +{ + /// + /// Game states for the airplane minigame + /// + public enum AirplaneGameState + { + Intro, // Intro sequence + NextPerson, // Introducing the next person + Aiming, // Player is aiming the airplane + Flying, // Airplane is in flight + Evaluating, // Evaluating the result of the flight + GameOver // All people have had their turn + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Data/AirplaneGameState.cs.meta b/Assets/Scripts/Minigames/Airplane/Data/AirplaneGameState.cs.meta new file mode 100644 index 00000000..aa89a34f --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/AirplaneGameState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 59636bd1dbca4575b431820510da201f +timeCreated: 1764851235 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Data/PersonData.cs b/Assets/Scripts/Minigames/Airplane/Data/PersonData.cs new file mode 100644 index 00000000..2aafc422 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/PersonData.cs @@ -0,0 +1,52 @@ +using UnityEngine; + +namespace Minigames.Airplane.Data +{ + /// + /// Data for a person participating in the airplane minigame. + /// Contains their name, target assignment, and scene reference. + /// + [System.Serializable] + public class PersonData + { + [Tooltip("Name of the person")] + public string personName; + + [Tooltip("Target name they need to hit")] + public string targetName; + + [Tooltip("Transform reference to the person in the scene")] + public Transform personTransform; + + [Tooltip("Turn number (assigned at runtime)")] + public int turnNumber; + + /// + /// Constructor for creating person data + /// + public PersonData(string name, string target, Transform transform, int turn = 0) + { + personName = name; + targetName = target; + personTransform = transform; + turnNumber = turn; + } + + /// + /// Default constructor for serialization + /// + public PersonData() + { + personName = "Unknown"; + targetName = "Unknown"; + personTransform = null; + turnNumber = 0; + } + + public override string ToString() + { + return $"Person: {personName}, Target: {targetName}, Turn: {turnNumber}"; + } + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Data/PersonData.cs.meta b/Assets/Scripts/Minigames/Airplane/Data/PersonData.cs.meta new file mode 100644 index 00000000..6f2eb0b3 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/PersonData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b9a03de5cfa64dadaf6c53b8f3935d3e +timeCreated: 1764851235 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Settings.meta b/Assets/Scripts/Minigames/Airplane/Settings.meta new file mode 100644 index 00000000..8676bb48 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Settings.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 81b8f6aeeaf946cea5f5338a9127ae74 +timeCreated: 1764851415 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs b/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs new file mode 100644 index 00000000..56a7c0c6 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs @@ -0,0 +1,70 @@ +using AppleHills.Core.Settings; +using Common.Input; +using UnityEngine; + +namespace Minigames.Airplane.Settings +{ + /// + /// Settings for the airplane minigame. + /// Create via Assets > Create > AppleHills > Settings > Airplane + /// + [CreateAssetMenu(fileName = "AirplaneSettings", menuName = "AppleHills/Settings/Airplane", order = 9)] + public class AirplaneSettings : BaseSettings, IAirplaneSettings + { + [Header("Slingshot Configuration")] + [SerializeField] private SlingshotConfig slingshotSettings = new SlingshotConfig + { + maxDragDistance = 5f, + baseLaunchForce = 20f, + minForceMultiplier = 0.1f, + maxForceMultiplier = 1f, + trajectoryPoints = 20, + trajectoryTimeStep = 0.1f, + trajectoryLockDuration = 0f, // No locking for airplane + autoRegisterInput = true // Direct registration + }; + + [Header("Flight Settings")] + [Tooltip("Mass of the airplane")] + [SerializeField] private float airplaneMass = 1f; + + [Tooltip("Maximum flight time before timeout (seconds)")] + [SerializeField] private float maxFlightTime = 10f; + + [Header("Camera Settings")] + [Tooltip("Camera follow smoothness (higher = smoother but more lag)")] + [SerializeField] private float cameraFollowSmoothing = 5f; + + [Tooltip("Camera zoom level during flight")] + [SerializeField] private float flightCameraZoom = 5f; + + [Header("Timing")] + [Tooltip("Duration of intro sequence (seconds)")] + [SerializeField] private float introDuration = 1f; + + [Tooltip("Duration of person introduction (seconds)")] + [SerializeField] private float personIntroDuration = 1f; + + [Tooltip("Duration of result evaluation (seconds)")] + [SerializeField] private float evaluationDuration = 1f; + + [Header("Debug")] + [Tooltip("Show debug logs in console")] + [SerializeField] private bool showDebugLogs; + + #region IAirplaneSettings Implementation + + public SlingshotConfig SlingshotSettings => slingshotSettings; + public float AirplaneMass => airplaneMass; + public float MaxFlightTime => maxFlightTime; + public float CameraFollowSmoothing => cameraFollowSmoothing; + public float FlightCameraZoom => flightCameraZoom; + public float IntroDuration => introDuration; + public float PersonIntroDuration => personIntroDuration; + public float EvaluationDuration => evaluationDuration; + public bool ShowDebugLogs => showDebugLogs; + + #endregion + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs.meta b/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs.meta new file mode 100644 index 00000000..80964634 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1c277e2fec3d42e2b3b0bed1b8a33beb +timeCreated: 1764851415 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Targets.meta b/Assets/Scripts/Minigames/Airplane/Targets.meta new file mode 100644 index 00000000..927293c5 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Targets.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bef822469ac14cedad520c7d8f01562a +timeCreated: 1764851291 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Targets/AirplaneTarget.cs b/Assets/Scripts/Minigames/Airplane/Targets/AirplaneTarget.cs new file mode 100644 index 00000000..376d72b1 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Targets/AirplaneTarget.cs @@ -0,0 +1,151 @@ +using System; +using Core; +using Core.Lifecycle; +using UnityEngine; + +namespace Minigames.Airplane.Targets +{ + /// + /// Represents a target in the airplane minigame. + /// Detects airplane collisions and can be highlighted when active. + /// + [RequireComponent(typeof(Collider2D))] + public class AirplaneTarget : ManagedBehaviour + { + #region Events + + /// + /// Fired when this target is hit. Parameters: (AirplaneTarget target, GameObject airplane) + /// + public event Action OnTargetHit; + + #endregion + + #region Inspector Properties + + [Header("Target Configuration")] + [Tooltip("Name of this target (for validation)")] + [SerializeField] private string targetName = "Target"; + + [Header("Visual Feedback")] + [Tooltip("Sprite renderer for visual feedback (optional)")] + [SerializeField] private SpriteRenderer spriteRenderer; + + [Tooltip("Color when target is active")] + [SerializeField] private Color activeColor = Color.yellow; + + [Tooltip("Color when target is inactive")] + [SerializeField] private Color inactiveColor = Color.white; + + [Header("Debug")] + [SerializeField] private bool showDebugLogs = false; + + #endregion + + #region Properties + + public string TargetName => targetName; + + private bool _isActive = false; + public bool IsActive => _isActive; + + #endregion + + #region State + + private Collider2D _targetCollider; + private Color _originalColor; + + #endregion + + #region Lifecycle + + internal override void OnManagedAwake() + { + base.OnManagedAwake(); + + // Cache components + _targetCollider = GetComponent(); + + // Configure collider as trigger + if (_targetCollider != null) + { + _targetCollider.isTrigger = true; + } + + // Cache sprite renderer if not assigned + if (spriteRenderer == null) + { + spriteRenderer = GetComponent(); + } + + // Store original color + if (spriteRenderer != null) + { + _originalColor = spriteRenderer.color; + } + } + + internal override void OnManagedStart() + { + base.OnManagedStart(); + + // Start as inactive + SetAsActiveTarget(false); + } + + #endregion + + #region Active State + + /// + /// Set this target as active (highlighted) or inactive + /// + 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 + + /// + /// Detect when airplane enters trigger + /// + private void OnTriggerEnter2D(Collider2D other) + { + // Check if it's an airplane + var airplane = other.GetComponent(); + if (airplane != null) + { + if (showDebugLogs) Logging.Debug($"[AirplaneTarget] {targetName} hit by airplane: {other.gameObject.name}"); + + OnTargetHit?.Invoke(this, other.gameObject); + } + } + + #endregion + + #region Public Methods + + /// + /// Reset target to original state + /// + public void Reset() + { + SetAsActiveTarget(false); + } + + #endregion + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Targets/AirplaneTarget.cs.meta b/Assets/Scripts/Minigames/Airplane/Targets/AirplaneTarget.cs.meta new file mode 100644 index 00000000..87f94ebb --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Targets/AirplaneTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 53e3dae13bb14c109a038bb5a84bd941 +timeCreated: 1764851291 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/FortFight/Core/FortFightSettings.cs b/Assets/Scripts/Minigames/FortFight/Core/FortFightSettings.cs index acece465..f9eb1fb8 100644 --- a/Assets/Scripts/Minigames/FortFight/Core/FortFightSettings.cs +++ b/Assets/Scripts/Minigames/FortFight/Core/FortFightSettings.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using AppleHills.Core.Settings; +using Common.Input; using Minigames.FortFight.Data; using Minigames.FortFight.Settings; using UnityEngine; @@ -14,6 +15,19 @@ namespace Minigames.FortFight.Core [CreateAssetMenu(fileName = "FortFightSettings", menuName = "AppleHills/Settings/Fort Fight", order = 8)] public class FortFightSettings : BaseSettings, IFortFightSettings { + [Header("Slingshot Configuration")] + [SerializeField] private SlingshotConfig slingshotSettings = new SlingshotConfig + { + maxDragDistance = 5f, + baseLaunchForce = 20f, + minForceMultiplier = 0.1f, + maxForceMultiplier = 1f, + trajectoryPoints = 50, + trajectoryTimeStep = 0.1f, + trajectoryLockDuration = 2f, + autoRegisterInput = false // TurnManager handles registration + }; + [Header("Block Material Configurations")] [Tooltip("HP and mass configurations for each material type")] [SerializeField] private List materialConfigs = new List @@ -142,6 +156,8 @@ namespace Minigames.FortFight.Core #region IFortFightSettings Implementation + public SlingshotConfig SlingshotSettings => slingshotSettings; + public List MaterialConfigs => materialConfigs; public List SizeConfigs => sizeConfigs; diff --git a/Assets/Scripts/Minigames/FortFight/Core/SlingshotController.cs b/Assets/Scripts/Minigames/FortFight/Core/SlingshotController.cs index d50599a6..ad8c03a4 100644 --- a/Assets/Scripts/Minigames/FortFight/Core/SlingshotController.cs +++ b/Assets/Scripts/Minigames/FortFight/Core/SlingshotController.cs @@ -1,7 +1,7 @@ using System; using AppleHills.Core.Settings; +using Common.Input; using Core; -using Core.Lifecycle; using Minigames.FortFight.Data; using Minigames.FortFight.Projectiles; using UnityEngine; @@ -9,22 +9,14 @@ using UnityEngine; namespace Minigames.FortFight.Core { /// - /// Controls slingshot aiming and projectile launching. - /// Angry Birds-style drag-to-aim mechanic with trajectory preview. - /// Implements ITouchInputConsumer for InputManager integration. + /// Controls slingshot aiming and projectile launching for FortFight. + /// Extends DragLaunchController with FortFight-specific ammo management and trajectory preview. /// - public class SlingshotController : ManagedBehaviour, ITouchInputConsumer + public class SlingshotController : DragLaunchController { #region Inspector Properties - [Header("Launch Settings")] - [Tooltip("Drag distance to reach max force")] - [SerializeField] private float maxDragDistance = 5f; - - [Tooltip("Spawn point for projectiles")] - [SerializeField] private Transform projectileSpawnPoint; - - [Header("References")] + [Header("FortFight Specific")] [Tooltip("Trajectory preview component")] [SerializeField] private TrajectoryPreview trajectoryPreview; @@ -58,8 +50,15 @@ namespace Minigames.FortFight.Core } } - private float MaxForce => CachedSettings?.BaseLaunchForce ?? 20f; - private bool ShowDebugLogs => CachedDevSettings?.SlingshotShowDebugLogs ?? false; + protected override SlingshotConfig GetSlingshotConfig() + { + return CachedSettings?.SlingshotSettings; + } + + protected override GameObject GetProjectilePrefab() + { + return _currentAmmo?.prefab; + } #endregion @@ -74,13 +73,10 @@ namespace Minigames.FortFight.Core #region State - private bool _isDragging; - private Vector2 _dragStartPosition; private ProjectileConfig _currentAmmo; private ProjectileBase _activeProjectile; - public bool IsDragging => _isDragging; - public bool IsEnabled { get; private set; } = true; + public ProjectileBase ActiveProjectile => _activeProjectile; #endregion @@ -90,15 +86,15 @@ namespace Minigames.FortFight.Core { base.OnManagedAwake(); - if (projectileSpawnPoint == null) - { - projectileSpawnPoint = transform; - } + // Base class handles launchAnchor (previously projectileSpawnPoint) if (trajectoryPreview == null) { trajectoryPreview = GetComponent(); } + + // Set debug logging from developer settings + showDebugLogs = CachedDevSettings?.SlingshotShowDebugLogs ?? false; } internal override void OnManagedStart() @@ -114,132 +110,51 @@ namespace Minigames.FortFight.Core #endregion - #region ITouchInputConsumer Implementation + #region Override Methods - public void OnTap(Vector2 worldPosition) + protected override float GetProjectileMass() { - // Slingshot uses hold/drag, not tap + return _currentAmmo?.GetMass() ?? base.GetProjectileMass(); } - public void OnHoldStart(Vector2 worldPosition) + protected override void StartDrag(Vector2 worldPosition) { - if (!IsEnabled) return; - StartDrag(worldPosition); - } - - public void OnHoldMove(Vector2 worldPosition) - { - if (!IsEnabled || !_isDragging) return; - UpdateDrag(worldPosition); - } - - public void OnHoldEnd(Vector2 worldPosition) - { - if (!IsEnabled || !_isDragging) return; - EndDrag(worldPosition); + // Check ammo before starting drag + if (_currentAmmo == null) + { + if (showDebugLogs) Logging.Warning("[SlingshotController] No ammo selected!"); + return; + } + + base.StartDrag(worldPosition); } #endregion - #region Drag Handling + #region Abstract Method Implementations - private void StartDrag(Vector2 worldPosition) + protected override void ShowPreview() { - if (_currentAmmo == null) - { - if (ShowDebugLogs) Logging.Warning("[SlingshotController] No ammo selected!"); - return; - } - - _isDragging = true; - // Use the projectile spawn point as the anchor, not the touch position - // This makes it work like Angry Birds - pull back from slingshot to launch forward - _dragStartPosition = projectileSpawnPoint.position; - - // Show trajectory preview - if (trajectoryPreview != null) - { - trajectoryPreview.Show(); - } - - if (ShowDebugLogs) Logging.Debug($"[SlingshotController] Started drag at {worldPosition}, anchor at spawn point {_dragStartPosition}"); + trajectoryPreview?.Show(); } - private void UpdateDrag(Vector2 currentWorldPosition) + protected override void HidePreview() { - // Calculate drag vector from spawn point to current drag position - // Pull back (away from spawn) = launch forward (toward spawn direction) - Vector2 dragVector = _dragStartPosition - currentWorldPosition; - - // Calculate force and direction - float dragDistance = dragVector.magnitude; - float dragRatio = Mathf.Clamp01(dragDistance / maxDragDistance); - - // Apply configurable max force multiplier - float maxMultiplier = CachedSettings?.MaxForceMultiplier ?? 1f; - float forceMultiplier = dragRatio * maxMultiplier; - float force = forceMultiplier * MaxForce; - - Vector2 direction = dragVector.normalized; - - // Update trajectory preview with projectile mass - if (trajectoryPreview != null && _currentAmmo != null) + trajectoryPreview?.Hide(); + } + + protected override void UpdateVisuals(Vector2 currentPosition, Vector2 direction, + float force, float dragDistance, float mass) + { + if (trajectoryPreview != null) { - Vector2 worldStartPos = projectileSpawnPoint.position; - float mass = _currentAmmo.GetMass(); - - // Debug: Log trajectory calculation (uncomment for debugging) - // if (showDebugLogs && Time.frameCount % 30 == 0) // Log every 30 frames to avoid spam - // { - // Logging.Debug($"[Slingshot] Preview - Force: {force:F2}, Mass: {mass:F2}, Velocity: {force/mass:F2}, Dir: {direction}"); - // } - - trajectoryPreview.UpdateTrajectory(worldStartPos, direction, force, mass); + trajectoryPreview.UpdateTrajectory(launchAnchor.position, direction, force, mass); } } - private void EndDrag(Vector2 currentWorldPosition) + protected override void PerformLaunch(Vector2 direction, float force) { - _isDragging = false; - - // Hide trajectory - if (trajectoryPreview != null) - { - trajectoryPreview.Hide(); - } - - // Calculate final launch parameters from spawn point to final drag position - Vector2 dragVector = _dragStartPosition - currentWorldPosition; - float dragDistance = dragVector.magnitude; - float dragRatio = Mathf.Clamp01(dragDistance / maxDragDistance); - - // Apply configurable max force multiplier - float maxMultiplier = CachedSettings?.MaxForceMultiplier ?? 1f; - float forceMultiplier = dragRatio * maxMultiplier; - float force = forceMultiplier * MaxForce; - - Vector2 direction = dragVector.normalized; - - // Check against configurable minimum force threshold - float minMultiplier = CachedSettings?.MinForceMultiplier ?? 0.1f; - float minForce = minMultiplier * MaxForce; - - // Launch projectile if force exceeds minimum - if (force >= minForce) - { - if (ShowDebugLogs && _currentAmmo != null) - { - float mass = _currentAmmo.GetMass(); - float velocity = force / mass; - Logging.Debug($"[Slingshot] Launch - Force: {force:F2}, Mass: {mass:F2}, Velocity: {velocity:F2}, Dir: {direction}"); - } - - LaunchProjectile(direction, force); - } - else - { - if (ShowDebugLogs) Logging.Debug($"[SlingshotController] Drag too short - force {force:F2} < min {minForce:F2}"); - } + LaunchProjectile(direction, force); } #endregion @@ -252,7 +167,7 @@ namespace Minigames.FortFight.Core public void SetAmmo(ProjectileConfig ammoConfig) { _currentAmmo = ammoConfig; - if (ShowDebugLogs) Logging.Debug($"[SlingshotController] Ammo set to: {ammoConfig?.displayName ?? "null"}"); + if (showDebugLogs) Logging.Debug($"[SlingshotController] Ammo set to: {ammoConfig?.displayName ?? "null"}"); } /// @@ -266,8 +181,8 @@ namespace Minigames.FortFight.Core return; } - // Spawn projectile - GameObject projectileObj = Instantiate(_currentAmmo.prefab, projectileSpawnPoint.position, Quaternion.identity); + // Spawn projectile at launch anchor + GameObject projectileObj = Instantiate(_currentAmmo.prefab, launchAnchor.position, Quaternion.identity); _activeProjectile = projectileObj.GetComponent(); if (_activeProjectile == null) @@ -290,7 +205,7 @@ namespace Minigames.FortFight.Core trajectoryPreview.LockTrajectory(lockDuration); } - if (ShowDebugLogs) Logging.Debug($"[SlingshotController] Launched {_currentAmmo?.displayName ?? "projectile"} with force {force}"); + if (showDebugLogs) Logging.Debug($"[SlingshotController] Launched {_currentAmmo?.displayName ?? "projectile"} with force {force}"); // Fire event OnProjectileLaunched?.Invoke(_activeProjectile); @@ -324,7 +239,7 @@ namespace Minigames.FortFight.Core float speed = velocity.magnitude; float force = mass * speed; - if (ShowDebugLogs) + if (showDebugLogs) { Logging.Debug($"[Slingshot] LaunchWithVelocity - Velocity: {velocity}, Mass: {mass:F2}, Force: {force:F2}"); } @@ -341,34 +256,7 @@ namespace Minigames.FortFight.Core } #endregion - - #region Enable/Disable - /// - /// Enable slingshot (allow aiming/launching) - /// - public void Enable() - { - IsEnabled = true; - if (ShowDebugLogs) Logging.Debug("[SlingshotController] Enabled"); - } - - /// - /// Disable slingshot (prevent aiming/launching) - /// - public void Disable() - { - IsEnabled = false; - _isDragging = false; - - if (trajectoryPreview != null) - { - trajectoryPreview.Hide(); - } - - if (ShowDebugLogs) Logging.Debug("[SlingshotController] Disabled"); - } - - #endregion + // Note: Enable/Disable methods now handled by base DragLaunchController class } } diff --git a/Assets/Settings/FortFightSettings.asset b/Assets/Settings/FortFightSettings.asset index 1ee88337..dd3e15f9 100644 --- a/Assets/Settings/FortFightSettings.asset +++ b/Assets/Settings/FortFightSettings.asset @@ -12,6 +12,17 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: eaaa527529c5438f80d27ff95c7c7930, type: 3} m_Name: FortFightSettings m_EditorClassIdentifier: AppleHillsScripts::Minigames.FortFight.Core.FortFightSettings + slingshotSettings: + maxDragDistance: 5 + baseLaunchForce: 125 + minForceMultiplier: 0.1 + maxForceMultiplier: 1 + trajectoryPoints: 50 + trajectoryTimeStep: 0.1 + trajectoryLockDuration: 2 + gravity: 9.81 + defaultProjectileMass: 1 + autoRegisterInput: 0 materialConfigs: - material: 0 baseHp: 20