diff --git a/Assets/Prefabs/Minigames/Airplane/AirplaneGameBlends.asset b/Assets/Prefabs/Minigames/Airplane/AirplaneGameBlends.asset index f203d504..c01400cc 100644 --- a/Assets/Prefabs/Minigames/Airplane/AirplaneGameBlends.asset +++ b/Assets/Prefabs/Minigames/Airplane/AirplaneGameBlends.asset @@ -24,3 +24,25 @@ MonoBehaviour: m_PreInfinity: 0 m_PostInfinity: 0 m_RotationOrder: 0 + - From: '**ANY CAMERA**' + To: TargetCamera + Blend: + Style: 6 + Time: 3 + CustomCurve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 0 + m_PostInfinity: 0 + m_RotationOrder: 0 + - From: TargetCamera + To: '**ANY CAMERA**' + Blend: + Style: 1 + Time: 1 + CustomCurve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 0 + m_PostInfinity: 0 + m_RotationOrder: 0 diff --git a/Assets/Scenes/MiniGames/BirdPoop.unity b/Assets/Scenes/MiniGames/BirdPoop.unity index 9d5faabc..4ca4923d 100644 --- a/Assets/Scenes/MiniGames/BirdPoop.unity +++ b/Assets/Scenes/MiniGames/BirdPoop.unity @@ -285,6 +285,7 @@ GameObject: m_Component: - component: {fileID: 128829408} - component: {fileID: 128829407} + - component: {fileID: 128829409} m_Layer: 0 m_Name: MinigameManager m_TagString: Untagged @@ -307,6 +308,7 @@ MonoBehaviour: player: {fileID: 941621859} obstacleSpawner: {fileID: 938885957} targetSpawner: {fileID: 1838778561} + tapToStartController: {fileID: 128829409} gameOverScreen: {fileID: 81231374} poopPrefab: {fileID: 5552423787977869117, guid: 066f9990a9b1f5547b387633d5d204c0, type: 3} poopCooldown: 0.5 @@ -325,6 +327,23 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &128829409 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 128829406} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a6ee5aca3ca423c82b57e16c0b2cca3, type: 3} + m_Name: + m_EditorClassIdentifier: AppleHillsScripts::Minigames.BirdPooper.TapToStartController + fingerContainer: {fileID: 2064126204} + fingerImage: {fileID: 1481863789} + blinkDuration: 1.5 + minAlpha: 0.3 + maxAlpha: 1 --- !u!1 &402349268 GameObject: m_ObjectHideFlags: 0 @@ -771,53 +790,6 @@ MonoBehaviour: despawnPoint: {fileID: 938473626} referenceMarker: {fileID: 1143700529} cameraAdapter: {fileID: 2103114179} - obstaclePrefabs: - - {fileID: 8855270423038321603, guid: 20ae02a8f50484045aaf3dcee33fb9a2, type: 3} - - {fileID: 2514399078413048981, guid: ee834e7efcf7d8749881f71f8b0da99c, type: 3} - - {fileID: 842802843766402460, guid: cdc806fd167bba3488797031a28657fa, type: 3} - - {fileID: 4239333156730914246, guid: 332d8cce2ed99054c83ecf84fbfa14c8, type: 3} - - {fileID: 6660502783540694524, guid: 5d42fc70e5838544ab654e30aa4b0c48, type: 3} - - {fileID: 2421410811796775077, guid: 371a09b68a5c0654bac9ba58ad3bcbe5, type: 3} - - {fileID: 1408173265900928789, guid: 871373a85e5da0e4cafdf0e47496e105, type: 3} - - {fileID: 1408173265900928789, guid: d2998934362713545a040d7017a1bd36, type: 3} - - {fileID: 1408173265900928789, guid: 146d99398c0e7964dbed504e256adab7, type: 3} - - {fileID: 1408173265900928789, guid: dc8a19e9a4d30b44596237d915b3b73f, type: 3} - - {fileID: 1408173265900928789, guid: 471f367e14f9cfb4fb2c40d799d4c292, type: 3} - - {fileID: 1408173265900928789, guid: 5f1734c5705cdfd49ae3180d678d28b3, type: 3} - - {fileID: 1408173265900928789, guid: 6bc84c3ea9854b54f85a8fb69c769790, type: 3} - - {fileID: 1408173265900928789, guid: 166c7e1bfcc4c854fab0af51cdfff746, type: 3} - - {fileID: 1408173265900928789, guid: 65810bfd58ebbaf4482527452258ae50, type: 3} - - {fileID: 1408173265900928789, guid: ae3986a7db087c845b618a9c897705ec, type: 3} - minSpawnInterval: 2 - maxSpawnInterval: 8 - difficultyRampDuration: 360 - difficultyCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 2 - outSlope: 2 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - intervalJitter: 0.3 - recentDecayDuration: 60 - minRecentWeight: 0.05 --- !u!1 &941621855 GameObject: m_ObjectHideFlags: 0 @@ -1484,6 +1456,81 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1481863787 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1481863788} + - component: {fileID: 1481863790} + - component: {fileID: 1481863789} + m_Layer: 5 + m_Name: Image + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1481863788 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1481863787} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.18, y: 1.18, z: 1.18} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 2064126205} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 316, y: 246} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1481863789 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1481863787} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: -7164639588303836088, guid: 6fee60c82a4f504419e535456268a19e, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1481863790 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1481863787} + m_CullTransparentMesh: 1 --- !u!1 &1498486830 GameObject: m_ObjectHideFlags: 0 @@ -1615,6 +1662,7 @@ RectTransform: m_Children: - {fileID: 1088771378} - {fileID: 81231372} + - {fileID: 2064126205} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -1844,6 +1892,42 @@ MonoBehaviour: - {fileID: 8373178063207716143, guid: 020f7494c613b06479ccad2c4cedde0f, type: 3} minTargetSpawnInterval: 9 maxTargetSpawnInterval: 15 +--- !u!1 &2064126204 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2064126205} + m_Layer: 5 + m_Name: StartGamePrompt + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2064126205 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2064126204} + 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: + - {fileID: 1481863788} + m_Father: {fileID: 1536057440} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} --- !u!1 &2103114174 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity b/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity index 87445600..3fa7828d 100644 --- a/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity +++ b/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity @@ -788,8 +788,11 @@ MonoBehaviour: camera: {fileID: 761345710} - state: 3 camera: {fileID: 263322553} + - state: 4 + camera: {fileID: 1842736650} inactivePriority: 10 activePriority: 20 + cinemachineBrain: {fileID: 1810521058} showDebugLogs: 0 --- !u!4 &597044887 Transform: @@ -3948,7 +3951,7 @@ Transform: m_GameObject: {fileID: 1810521056} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 12.800001, z: -10} + m_LocalPosition: {x: -18.9, y: 9.100001, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -4136,6 +4139,101 @@ SpriteRenderer: m_SpriteTileMode: 0 m_WasSpriteAssigned: 1 m_SpriteSortPoint: 0 +--- !u!1 &1842736649 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1842736651} + - component: {fileID: 1842736650} + - component: {fileID: 1842736652} + m_Layer: 0 + m_Name: TargetCamera + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1842736650 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1842736649} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f9dfa5b682dcd46bda6128250e975f58, type: 3} + m_Name: + m_EditorClassIdentifier: + Priority: + Enabled: 0 + m_Value: 0 + OutputChannel: 1 + StandbyUpdate: 2 + m_StreamingVersion: 20241001 + m_LegacyPriority: 0 + Target: + TrackingTarget: {fileID: 0} + LookAtTarget: {fileID: 0} + CustomLookAtTarget: 0 + Lens: + FieldOfView: 60 + OrthographicSize: 20 + NearClipPlane: 0.3 + FarClipPlane: 1000 + Dutch: 0 + ModeOverride: 0 + PhysicalProperties: + GateFit: 2 + SensorSize: {x: 21.946, y: 16.002} + LensShift: {x: 0, y: 0} + FocusDistance: 10 + Iso: 200 + ShutterSpeed: 0.005 + Aperture: 16 + BladeCount: 5 + Curvature: {x: 2, y: 11} + BarrelClipping: 0.25 + Anamorphism: 0 + BlendHint: 0 +--- !u!4 &1842736651 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1842736649} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: -9.6, y: 12.900002, z: -10} + 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!114 &1842736652 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1842736649} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b617507da6d07e749b7efdb34e1173e1, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Cinemachine::Unity.Cinemachine.CinemachineFollow + TrackerSettings: + BindingMode: 4 + PositionDamping: {x: 1, y: 1, z: 1} + AngularDampingMode: 0 + RotationDamping: {x: 1, y: 1, z: 1} + QuaternionDamping: 1 + FollowOffset: {x: 0, y: 0, z: -10} --- !u!1 &1897459173 GameObject: m_ObjectHideFlags: 0 @@ -4895,6 +4993,37 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2041920295 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2041920296} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2041920296 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2041920295} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 16.8, y: 12.6, 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 &2049960991 GameObject: m_ObjectHideFlags: 0 @@ -5024,81 +5153,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2090212067} m_CullTransparentMesh: 1 ---- !u!1 &2103114174 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 2103114178} - - component: {fileID: 2103114177} - m_Layer: 0 - m_Name: CinemachineCamera - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &2103114177 -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: f9dfa5b682dcd46bda6128250e975f58, type: 3} - m_Name: - m_EditorClassIdentifier: - Priority: - Enabled: 0 - m_Value: 0 - OutputChannel: 1 - StandbyUpdate: 2 - m_StreamingVersion: 20241001 - m_LegacyPriority: 0 - Target: - TrackingTarget: {fileID: 0} - LookAtTarget: {fileID: 0} - CustomLookAtTarget: 0 - Lens: - FieldOfView: 60 - OrthographicSize: 15 - NearClipPlane: 0.3 - FarClipPlane: 1000 - Dutch: 0 - ModeOverride: 0 - PhysicalProperties: - GateFit: 2 - SensorSize: {x: 21.946, y: 16.002} - LensShift: {x: 0, y: 0} - FocusDistance: 10 - Iso: 200 - ShutterSpeed: 0.005 - Aperture: 16 - BladeCount: 5 - Curvature: {x: 2, y: 11} - BarrelClipping: 0.25 - Anamorphism: 0 - BlendHint: 0 ---- !u!4 &2103114178 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2103114174} - serializedVersion: 2 - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 12.800001, z: -10} - 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 &2111947703 GameObject: m_ObjectHideFlags: 0 @@ -5578,7 +5632,6 @@ SceneRoots: m_ObjectHideFlags: 0 m_Roots: - {fileID: 1810521061} - - {fileID: 2103114178} - {fileID: 742382011} - {fileID: 2125208580} - {fileID: 1239857187} @@ -5596,8 +5649,10 @@ SceneRoots: - {fileID: 595230075} - {fileID: 2041347574} - {fileID: 761345711} + - {fileID: 1842736651} - {fileID: 263322554} - {fileID: 1017291799} - {fileID: 1064542409} - {fileID: 1701327424} + - {fileID: 2041920296} - {fileID: 1710395163} diff --git a/Assets/Scripts/Cinematics/LevelIntroDirector.cs b/Assets/Scripts/Cinematics/LevelIntroDirector.cs index 7a9a07f6..1224ddbe 100644 --- a/Assets/Scripts/Cinematics/LevelIntroDirector.cs +++ b/Assets/Scripts/Cinematics/LevelIntroDirector.cs @@ -1,41 +1,42 @@ -using UnityEngine; -using Core; using Core.Lifecycle; -using UnityEngine.Playables; using Input; -using Unity.Cinemachine; +using UnityEngine; +using UnityEngine.Playables; -public class LevelIntroDirector : ManagedBehaviour +namespace Cinematics { - public bool playOnSceneReady; - - [HideInInspector] - public PlayableDirector introPlayableDirector; - - internal override void OnSceneReady() + public class LevelIntroDirector : ManagedBehaviour { - base.OnSceneReady(); - if (playOnSceneReady) + public bool playOnSceneReady; + + [HideInInspector] + public PlayableDirector introPlayableDirector; + + internal override void OnSceneReady() { - introPlayableDirector = GetComponent(); - introPlayableDirector.stopped += IntroTimelineStopped; - PlayIntroTimeline(); + base.OnSceneReady(); + if (playOnSceneReady) + { + introPlayableDirector = GetComponent(); + introPlayableDirector.stopped += IntroTimelineStopped; + PlayIntroTimeline(); + } + else { gameObject.SetActive(false); } } - else { gameObject.SetActive(false); } - } - private void IntroTimelineStopped(PlayableDirector director) - { - InputManager.Instance.SetInputMode(InputMode.Game); - introPlayableDirector.stopped -= IntroTimelineStopped; - gameObject.SetActive(false); + private void IntroTimelineStopped(PlayableDirector director) + { + InputManager.Instance.SetInputMode(InputMode.Game); + introPlayableDirector.stopped -= IntroTimelineStopped; + gameObject.SetActive(false); - } + } - public void PlayIntroTimeline() - { - introPlayableDirector.Play(); - InputManager.Instance.SetInputMode(InputMode.InputDisabled); - } + public void PlayIntroTimeline() + { + introPlayableDirector.Play(); + InputManager.Instance.SetInputMode(InputMode.InputDisabled); + } + } } diff --git a/Assets/Scripts/Common/Camera/CameraStateManager.cs b/Assets/Scripts/Common/Camera/CameraStateManager.cs index 4a437a71..32a9bed8 100644 --- a/Assets/Scripts/Common/Camera/CameraStateManager.cs +++ b/Assets/Scripts/Common/Camera/CameraStateManager.cs @@ -49,6 +49,10 @@ namespace Common.Camera [Tooltip("Priority for the active camera")] [SerializeField] protected int activePriority = 20; + [Header("Cinemachine Brain")] + [Tooltip("CinemachineBrain for blend detection (auto-finds if null)")] + [SerializeField] protected CinemachineBrain cinemachineBrain; + [Header("Debug")] [SerializeField] protected bool showDebugLogs = false; @@ -60,6 +64,11 @@ namespace Common.Camera private TState _currentState; private bool _isInitialized = false; + // Event-driven blend tracking + private CinemachineCamera _pendingBlendTarget; + private bool _isBlendComplete; + private Action _pendingBlendCallback; + public TState CurrentState => _currentState; #endregion @@ -71,6 +80,11 @@ namespace Common.Camera /// public event Action OnStateChanged; + /// + /// Fired when camera blend completes after state switch + /// + public event Action OnBlendComplete; + #endregion #region Lifecycle @@ -84,6 +98,17 @@ namespace Common.Camera { base.OnManagedAwake(); + // Auto-find CinemachineBrain if not assigned + if (cinemachineBrain == null) + { + cinemachineBrain = UnityEngine.Camera.main?.GetComponent(); + + if (cinemachineBrain == null && showDebugLogs) + { + Logging.Warning($"[{GetType().Name}] CinemachineBrain not found. Blend tracking will be unavailable."); + } + } + // Initialize cameras from Inspector mappings InitializeCameraMap(); @@ -91,6 +116,35 @@ namespace Common.Camera ValidateCameras(); } + /// + /// Subscribe to Cinemachine events on enable + /// + private void OnEnable() + { + // Subscribe to Cinemachine global events + CinemachineCore.BlendFinishedEvent.AddListener(OnBlendFinished); + CinemachineCore.CameraActivatedEvent.AddListener(OnCameraActivated); + + if (showDebugLogs) + Logging.Debug($"[{GetType().Name}] Subscribed to Cinemachine events"); + } + + /// + /// Unsubscribe from Cinemachine events on disable + /// + private void OnDisable() + { + // Unsubscribe from Cinemachine events to prevent memory leaks + CinemachineCore.BlendFinishedEvent.RemoveListener(OnBlendFinished); + CinemachineCore.CameraActivatedEvent.RemoveListener(OnCameraActivated); + + // Clear any pending callbacks + _pendingBlendCallback = null; + + if (showDebugLogs) + Logging.Debug($"[{GetType().Name}] Unsubscribed from Cinemachine events"); + } + #endregion #region Initialization @@ -229,6 +283,115 @@ namespace Common.Camera return _cameraMap.ContainsKey(state); } + /// + /// Blend to a state and wait asynchronously (coroutine). + /// Yields until Cinemachine blend event fires. Event-driven, no polling. + /// Use this when you need to wait for the blend to complete before continuing. + /// + public System.Collections.IEnumerator BlendToStateAsync(TState newState) + { + // Reset completion flag + _isBlendComplete = false; + + // Set pending target camera + if (!_cameraMap.TryGetValue(newState, out _pendingBlendTarget)) + { + Logging.Error($"[{GetType().Name}] No camera for state {newState}"); + yield break; + } + + // Switch camera state (triggers blend) + SwitchToState(newState); + + // Fallback: if no brain, complete immediately + if (cinemachineBrain == null) + { + if (showDebugLogs) + Logging.Warning($"[{GetType().Name}] No brain, completing blend immediately"); + yield break; + } + + // Wait for event to fire (event handlers set _isBlendComplete = true) + yield return new WaitUntil(() => _isBlendComplete); + + if (showDebugLogs) + Logging.Debug($"[{GetType().Name}] Blend to {newState} completed (event-driven)"); + } + + /// + /// Blend to a state with callback invoked on completion. + /// Callback fires when Cinemachine blend event fires. Event-driven, no polling. + /// Use this when you want to perform an action after the blend completes. + /// + public void BlendToState(TState newState, Action onComplete) + { + // Set pending target camera + if (!_cameraMap.TryGetValue(newState, out _pendingBlendTarget)) + { + Logging.Error($"[{GetType().Name}] No camera for state {newState}"); + onComplete?.Invoke(); // Still invoke to prevent hanging + return; + } + + // Store callback + _pendingBlendCallback = onComplete; + + // Switch camera state (triggers blend) + SwitchToState(newState); + + // Fallback: if no brain, invoke callback immediately + if (cinemachineBrain == null) + { + if (showDebugLogs) + Logging.Warning($"[{GetType().Name}] No brain, invoking callback immediately"); + CompleteBlend(); + } + + // Event handlers will invoke callback when blend finishes + } + + #endregion + + #region Event Handlers + + /// + /// Called when Cinemachine finishes a blend (non-zero length blends) + /// + private void OnBlendFinished(ICinemachineMixer mixer, ICinemachineCamera cam) + { + // Filter: only respond to blends from our brain to our expected camera + if (mixer == cinemachineBrain && cam == _pendingBlendTarget) + { + CompleteBlend(); + } + } + + /// + /// Called when Cinemachine activates a camera (handles instant cuts) + /// + private void OnCameraActivated(ICinemachineCamera.ActivationEventParams evt) + { + // Filter: only respond to cuts from our brain to our expected camera + if (evt.Origin == cinemachineBrain && evt.IncomingCamera == _pendingBlendTarget && evt.IsCut) + { + CompleteBlend(); + } + } + + /// + /// Mark blend as complete and fire callbacks + /// + private void CompleteBlend() + { + _isBlendComplete = true; + _pendingBlendCallback?.Invoke(); + _pendingBlendCallback = null; + OnBlendComplete?.Invoke(); + + if (showDebugLogs) + Logging.Debug($"[{GetType().Name}] Blend completed, callbacks invoked"); + } + #endregion #region Validation diff --git a/Assets/Scripts/Core/Settings/BirdPooperSettings.cs b/Assets/Scripts/Core/Settings/BirdPooperSettings.cs index e0f6af68..837865f8 100644 --- a/Assets/Scripts/Core/Settings/BirdPooperSettings.cs +++ b/Assets/Scripts/Core/Settings/BirdPooperSettings.cs @@ -1,4 +1,5 @@ using AppleHills.Core.Settings; +using Minigames.BirdPooper; using UnityEngine; namespace Core.Settings @@ -33,8 +34,8 @@ namespace Core.Settings [Tooltip("Obstacle scroll speed in units/s")] [SerializeField] private float obstacleMoveSpeed = 5f; - [Tooltip("Time between obstacle spawns in seconds")] - [SerializeField] private float obstacleSpawnInterval = 2f; + [Tooltip("Obstacle spawning configuration (pools, timing, difficulty)")] + [SerializeField] private ObstacleSpawnConfig obstacleSpawnConfiguration; [Tooltip("X position where obstacles spawn (off-screen right)")] [SerializeField] private float obstacleSpawnXPosition = 12f; @@ -71,7 +72,7 @@ namespace Core.Settings public float MaxRotationAngle => maxRotationAngle; public float RotationSpeed => rotationSpeed; public float ObstacleMoveSpeed => obstacleMoveSpeed; - public float ObstacleSpawnInterval => obstacleSpawnInterval; + public ObstacleSpawnConfig ObstacleSpawnConfiguration => obstacleSpawnConfiguration; public float ObstacleSpawnXPosition => obstacleSpawnXPosition; public float ObstacleDestroyXPosition => obstacleDestroyXPosition; public float ObstacleMinSpawnY => obstacleMinSpawnY; @@ -91,9 +92,14 @@ namespace Core.Settings maxFallSpeed = Mathf.Max(0f, maxFallSpeed); maxRotationAngle = Mathf.Clamp(maxRotationAngle, 0f, 90f); rotationSpeed = Mathf.Max(0.1f, rotationSpeed); - obstacleSpawnInterval = Mathf.Max(0.1f, obstacleSpawnInterval); targetMoveSpeed = Mathf.Max(0.1f, targetMoveSpeed); targetSpawnInterval = Mathf.Max(0.1f, targetSpawnInterval); + + // Validate obstacle spawn configuration + if (obstacleSpawnConfiguration != null) + { + obstacleSpawnConfiguration.Validate(); + } } } } diff --git a/Assets/Scripts/Core/Settings/IBirdPooperSettings.cs b/Assets/Scripts/Core/Settings/IBirdPooperSettings.cs index 43e3b388..a67f8fc8 100644 --- a/Assets/Scripts/Core/Settings/IBirdPooperSettings.cs +++ b/Assets/Scripts/Core/Settings/IBirdPooperSettings.cs @@ -1,8 +1,10 @@ -namespace Core.Settings +using Minigames.BirdPooper; + +namespace Core.Settings { /// /// Settings interface for Bird Pooper minigame. - /// Accessed via GameManager.GetSettingsObject() + /// Accessed via GameManager.GetSettingsObject of IBirdPooperSettings /// public interface IBirdPooperSettings { @@ -19,12 +21,14 @@ // Obstacles float ObstacleMoveSpeed { get; } - float ObstacleSpawnInterval { get; } float ObstacleSpawnXPosition { get; } float ObstacleDestroyXPosition { get; } float ObstacleMinSpawnY { get; } float ObstacleMaxSpawnY { get; } + // Obstacle Spawning Configuration + ObstacleSpawnConfig ObstacleSpawnConfiguration { get; } + // Poop Projectile float PoopFallSpeed { get; } float PoopDestroyYPosition { get; } diff --git a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs index 41361788..a4f90f61 100644 --- a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs +++ b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs @@ -332,8 +332,11 @@ namespace Core.Settings float IntroDuration { get; } float PersonIntroDuration { get; } float EvaluationDuration { get; } + float TargetFlybyLingerDuration { get; } + float TargetFlybyCameraBlendTime { get; } // Spawn System + float PreSpawnBeyondTargetDistance { get; } float TargetMinDistance { get; } float TargetMaxDistance { get; } float ObjectSpawnMinDistance { get; } // Min distance between spawned objects diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs index cda8eff0..97138e3d 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs @@ -114,6 +114,30 @@ namespace Minigames.Airplane.Core if (showDebugLogs) Logging.Debug("[AirplaneCameraManager] Stopped following airplane"); } + /// + /// Set the target flyby camera to track the target's position + /// + public void SetTargetFlybyTracking(Transform targetTransform) + { + var flybyCamera = GetCamera(AirplaneCameraState.TargetFlyby); + if (flybyCamera == null) + { + Logging.Warning("[AirplaneCameraManager] Cannot set flyby tracking - TargetFlyby camera not assigned!"); + return; + } + + if (targetTransform == null) + { + Logging.Warning("[AirplaneCameraManager] Cannot set flyby tracking - target transform is null!"); + return; + } + + // Set the tracking target on the flyby camera + flybyCamera.Target.TrackingTarget = targetTransform; + + if (showDebugLogs) Logging.Debug($"[AirplaneCameraManager] TargetFlyby camera now tracking: {targetTransform.gameObject.name}"); + } + #endregion } } diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs index a1d12df3..c05c04ca 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs @@ -66,7 +66,7 @@ namespace Minigames.Airplane.Core #region State - private AirplaneGameState _currentState = AirplaneGameState.AirplaneSelection; + private AirplaneGameState _currentState = AirplaneGameState.Intro; private Person _currentPerson; private Person _previousPerson; private AirplaneController _currentAirplane; @@ -74,7 +74,7 @@ namespace Minigames.Airplane.Core private int _successCount; private int _failCount; private int _totalTurns; - private AirplaneAbilityType _selectedAirplaneType; + private IAirplaneSettings _cachedSettings; public AirplaneGameState CurrentState => _currentState; public Person CurrentPerson => _currentPerson; @@ -98,6 +98,13 @@ namespace Minigames.Airplane.Core } _instance = this; + // Cache settings for performance + _cachedSettings = GameManager.GetSettingsObject(); + if (_cachedSettings == null) + { + Logging.Error("[AirplaneGameManager] Failed to load IAirplaneSettings!"); + } + // Validate references ValidateReferences(); } @@ -216,74 +223,43 @@ namespace Minigames.Airplane.Core { if (showDebugLogs) Logging.Debug("[AirplaneGameManager] ===== GAME STARTING ====="); - // Start with intro camera blend, THEN show selection UI StartCoroutine(IntroSequence()); } + #endregion + + #region Turn Type Helpers + /// - /// Airplane selection sequence: show selection UI, wait for player choice - /// Called AFTER intro camera blend + /// Check if this is the first turn of the game /// - private IEnumerator AirplaneSelectionSequence() + private bool IsFirstTurn() { - ChangeState(AirplaneGameState.AirplaneSelection); - - if (showDebugLogs) Logging.Debug("[AirplaneGameManager] === AIRPLANE SELECTION STARTING ==="); - - // Show selection UI - if (selectionUI != null) - { - if (showDebugLogs) - { - Logging.Debug($"[AirplaneGameManager] SelectionUI found! GameObject: {selectionUI.gameObject.name}, Active: {selectionUI.gameObject.activeSelf}"); - } - - selectionUI.Show(); - - if (showDebugLogs) - { - Logging.Debug($"[AirplaneGameManager] Called selectionUI.Show(). GameObject now active: {selectionUI.gameObject.activeSelf}"); - } - - // Wait for player to select and confirm - yield return new WaitUntil(() => selectionUI.HasSelectedType); - - _selectedAirplaneType = selectionUI.GetSelectedType(); - selectionUI.Hide(); - - if (showDebugLogs) - { - Logging.Debug($"[AirplaneGameManager] Selected airplane: {_selectedAirplaneType}"); - } - } - else - { - Logging.Warning("[AirplaneGameManager] ⚠️ selectionUI is NULL! Cannot show selection UI. Check Inspector."); - Logging.Warning("[AirplaneGameManager] Using default airplane type from settings as fallback."); - - // Fallback: use default type from settings - var settings = GameManager.GetSettingsObject(); - if (settings != null) - { - _selectedAirplaneType = settings.DefaultAirplaneType; - - if (showDebugLogs) - { - Logging.Debug($"[AirplaneGameManager] No selection UI, using default: {_selectedAirplaneType}"); - } - } - else - { - _selectedAirplaneType = AirplaneAbilityType.Jet; // Ultimate fallback - } - } - - // Continue with hellos after selection - yield return StartCoroutine(IntroHellosSequence()); + return _totalTurns == 0; } /// - /// Intro sequence: blend to intro camera, THEN show airplane selection + /// Check if this is a new person (different from previous) + /// + private bool IsNewPerson() + { + return _previousPerson != _currentPerson; + } + + /// + /// Check if this is a retry turn (same person as previous) + /// + private bool IsRetryTurn() + { + return !IsNewPerson() && _previousPerson != null; + } + + #endregion + + #region Flow: Intro Sequence + + /// + /// Initial intro sequence: blend to intro camera, introduce all people (stub), then start new person flow /// private IEnumerator IntroSequence() { @@ -291,116 +267,50 @@ namespace Minigames.Airplane.Core if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Playing intro sequence..."); - // 1. Blend to intro camera + // 1. Blend to intro camera and wait for blend to complete if (cameraManager != null) { - cameraManager.SwitchToState(AirplaneCameraState.Intro); - yield return new WaitForSeconds(0.5f); // Camera blend time + yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.Intro)); } - if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Intro camera ready. Now showing airplane selection..."); + // 2. Introduce all people (stub for now) + yield return StartCoroutine(IntroduceAllPeople()); - // 2. Show airplane selection UI and wait for player choice - yield return StartCoroutine(AirplaneSelectionSequence()); + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Intro complete, moving to first person..."); + + // 3. Start first person flow + yield return StartCoroutine(ExecuteNewPersonFlow()); } /// - /// Hellos sequence: all people greet, then blend to aiming camera - /// Called AFTER airplane selection is complete + /// Stub: All people say hello animation. Empty for now. /// - private IEnumerator IntroHellosSequence() + private IEnumerator IntroduceAllPeople() { - if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Starting hellos sequence..."); - - // 1. Iterate over each person and allow them to say their hellos - if (personQueue != null && personQueue.HasMorePeople()) - { - if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Introducing all people..."); - - // Get all people from queue without removing them - var allPeople = personQueue.GetAllPeople(); - foreach (var person in allPeople) - { - if (person != null) - { - // Wait for each person's greeting to complete - yield return StartCoroutine(person.OnHello()); - } - } - - if (showDebugLogs) Logging.Debug("[AirplaneGameManager] All introductions complete"); - } - - // 2. Blend to aiming camera (first person's turn will start) - if (cameraManager != null) - { - cameraManager.SwitchToState(AirplaneCameraState.Aiming); - yield return new WaitForSeconds(0.5f); // Camera blend time - } - - if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Intro complete"); - - // Move to first person's turn - StartCoroutine(SetupNextPerson()); + // TODO: All people say hello animation + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] (Stub) All people introduction"); + yield return null; } + #endregion + + #region Flow: New Person Flow + /// - /// Setup the next person's turn + /// New Person Flow: Executed for first person or after successful hit. + /// Steps: Pop person → Introduce → Prepare level → Flyby → Select airplane → Aim → Shoot /// - private IEnumerator SetupNextPerson() + private IEnumerator ExecuteNewPersonFlow() { // Check if there are more people if (personQueue == null || !personQueue.HasMorePeople()) { if (showDebugLogs) Logging.Debug("[AirplaneGameManager] No more people, ending game"); - StartCoroutine(GameOver()); + yield return StartCoroutine(GameOver()); yield break; } - ChangeState(AirplaneGameState.NextPerson); - - // If this is NOT the first turn, handle post-shot reaction - if (_currentPerson != null) - { - // Switch to next person camera for reaction/transition - if (cameraManager != null) - { - cameraManager.SwitchToState(AirplaneCameraState.NextPerson); - - // Wait for camera blend to complete before cleanup and reaction - yield return new WaitForSeconds(0.5f); // Camera blend time - } - - // NOW cleanup spawned objects after camera has blended (camera shows scene before cleanup) - if (spawnManager != null) - { - if (_lastShotHit) - { - // Success: Full cleanup - destroy all spawned objects and target - spawnManager.CleanupSpawnedObjects(); - - if (showDebugLogs) - { - Logging.Debug("[AirplaneGameManager] Cleaned up spawned objects after successful shot"); - } - } - else - { - // Failure: Keep spawned objects for retry, just reset tracking state - spawnManager.ResetForRetry(); - - if (showDebugLogs) - { - Logging.Debug("[AirplaneGameManager] Kept spawned objects for retry after miss"); - } - } - } - - // Handle the previous person's reaction (celebrate/disappointment), removal (if hit), and shuffle - yield return StartCoroutine(personQueue.HandlePostShotReaction(_lastShotHit)); - } - - // Get the next person (now at front of queue after potential removal) + // Pop next person from queue _previousPerson = _currentPerson; _currentPerson = personQueue.PopNextPerson(); _totalTurns++; @@ -411,76 +321,308 @@ namespace Minigames.Airplane.Core yield break; } - // Check if this is a NEW person (different from previous) or a retry (same person) - bool isNewPerson = _previousPerson != _currentPerson; - - if (showDebugLogs) + if (showDebugLogs) { - string turnType = isNewPerson ? "NEW PERSON" : "RETRY"; - Logging.Debug($"[AirplaneGameManager] === Turn {_totalTurns}: {_currentPerson.PersonName} ({turnType}) ===" + + Logging.Debug($"[AirplaneGameManager] === NEW PERSON FLOW: Turn {_totalTurns}: {_currentPerson.PersonName} ===" + $"\n Target: {_currentPerson.TargetName}"); } OnPersonStartTurn?.Invoke(_currentPerson); - // Only introduce if this is a NEW person - if (isNewPerson && _previousPerson != null) - { - // Switching to a new person (after success) - they say hello - yield return StartCoroutine(personQueue.IntroduceNextPerson()); - } - else if (_previousPerson == null) - { - // First turn - they already said hello during intro, just brief camera pause - if (cameraManager != null) - { - cameraManager.SwitchToState(AirplaneCameraState.NextPerson); - yield return new WaitForSeconds(0.5f); - } - } - // else: Same person retry (after failure) - skip introduction, go straight to aiming + // 1. Introduce this person + yield return StartCoroutine(IntroducePerson()); - // Initialize spawn manager for this person's target + // 2. Prepare level (spawn objects, target) + yield return StartCoroutine(PrepareLevel()); + + // 3. Execute target flyby + yield return StartCoroutine(ExecuteTargetFlyby()); + + // 4. Select airplane + AirplaneAbilityType selectedType = AirplaneAbilityType.None; + yield return StartCoroutine(SelectAirplane((type) => selectedType = type)); + + // 5. Enter aiming state with selected airplane + EnterAimingState(selectedType); + + // Flow continues to shot → evaluation → routing + } + + #endregion + + #region Flow: Repeat Shot Flow + + /// + /// Repeat Shot Flow: Executed when same person retries after missing. + /// Steps: Brief camera → React (disappointed) → Select airplane → Aim → Shoot + /// + private IEnumerator ExecuteRepeatShotFlow() + { + if (showDebugLogs) + { + Logging.Debug($"[AirplaneGameManager] === REPEAT SHOT FLOW: Turn {_totalTurns}: {_currentPerson.PersonName} (RETRY) ===" + + $"\n Target: {_currentPerson.TargetName}"); + } + + _totalTurns++; + OnPersonStartTurn?.Invoke(_currentPerson); + + // 1. Switch to next person camera briefly and wait for blend + if (cameraManager != null) + { + yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.NextPerson)); + } + + // 2. Play person reaction (disappointment) + yield return StartCoroutine(PlayPersonReaction(false)); + + // 3. Reset spawn manager for retry (keeps spawned objects) if (spawnManager != null) { - // Pass retry flag: true if same person, false if new person - bool isRetry = !isNewPerson; - spawnManager.InitializeForGame(_currentPerson.TargetName, isRetry); + spawnManager.ResetForRetry(); + + if (showDebugLogs) + { + Logging.Debug("[AirplaneGameManager] Kept spawned objects for retry"); + } } - // Queue done - continue game flow + // 4. Select airplane (new choice for this shot) + AirplaneAbilityType selectedType = AirplaneAbilityType.None; + yield return StartCoroutine(SelectAirplane((type) => selectedType = type)); + + // 5. Enter aiming state with selected airplane + EnterAimingState(selectedType); + + // Flow continues to shot → evaluation → routing + } + + #endregion + + #region Flow: Person Shuffle Flow + + /// + /// Person Shuffle Flow: Executed after successful hit. + /// Steps: Camera → React (celebrate) → Advance queue → Cleanup → Next person or game over + /// + private IEnumerator ExecutePersonShuffleFlow() + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] === PERSON SHUFFLE FLOW ==="); + + // 1. Switch to next person camera and wait for blend + if (cameraManager != null) + { + yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.NextPerson)); + } + + // 2. Play person reaction (celebration) + yield return StartCoroutine(PlayPersonReaction(true)); + + // 3. Advance queue (remove person, shuffle) + if (personQueue != null) + { + yield return StartCoroutine(personQueue.AdvanceQueue(true)); + } + + // 4. Cleanup level (destroy all spawned objects) + if (spawnManager != null) + { + spawnManager.CleanupLevel(true); + + if (showDebugLogs) + { + Logging.Debug("[AirplaneGameManager] Cleaned up spawned objects after success"); + } + } + + // 5. Check if more people, then continue to new person flow or game over + if (personQueue != null && personQueue.HasMorePeople()) + { + yield return StartCoroutine(ExecuteNewPersonFlow()); + } + else + { + yield return StartCoroutine(GameOver()); + } + } + + #endregion + + #region Phase Methods + + /// + /// Phase: Introduce current person + /// + private IEnumerator IntroducePerson() + { + ChangeState(AirplaneGameState.NextPerson); + + if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Introducing {_currentPerson.PersonName}..."); + + // Blend to next person camera if not already there + // (Person shuffle flow already blends to NextPerson, so might already be there) + if (cameraManager != null && cameraManager.CurrentState != AirplaneCameraState.NextPerson) + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Blending to NextPerson camera..."); + yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.NextPerson)); + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Blend to NextPerson complete"); + } + else if (showDebugLogs) + { + Logging.Debug("[AirplaneGameManager] Already on NextPerson camera, skipping blend"); + } + + // Person says hello + if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Calling OnHello for {_currentPerson.PersonName}..."); + yield return StartCoroutine(_currentPerson.OnHello()); + if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] OnHello complete for {_currentPerson.PersonName}"); + + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Introduction complete"); + } + + /// + /// Phase: Prepare level (spawn ground, objects, target) + /// + private IEnumerator PrepareLevel() + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Preparing level..."); + + if (spawnManager == null) + { + Logging.Error("[AirplaneGameManager] Cannot prepare level - spawn manager not assigned!"); + yield break; + } + + // Initialize spawn manager with new target + spawnManager.InitializeForGame(_currentPerson.TargetName, isRetry: false); + + // Pre-spawn entire level + spawnManager.PreSpawnLevelToTarget(); + + // Set flyby camera tracking on spawned target + if (cameraManager != null) + { + Transform targetTransform = spawnManager.GetSpawnedTargetTransform(); + if (targetTransform != null) + { + cameraManager.SetTargetFlybyTracking(targetTransform); + + if (showDebugLogs) + { + Logging.Debug("[AirplaneGameManager] Target camera now tracking spawned target"); + } + } + } + + // Set expected target for validator if (targetValidator != null) { targetValidator.SetExpectedTarget(_currentPerson.TargetName); } - // Enter aiming state - EnterAimingState(); + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Level preparation complete"); + + yield return null; } /// - /// Enter aiming state - player can aim and launch + /// Phase: Select airplane type (shows UI, waits for player choice) + /// Blends to Aiming camera if not already there (needed for repeat shot flow) /// - private void EnterAimingState() + private IEnumerator SelectAirplane(Action onSelected) + { + ChangeState(AirplaneGameState.AirplaneSelection); + + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] === AIRPLANE SELECTION ==="); + + // Blend to aiming camera if not already there + // (New person flow: already on Aiming from flyby; Repeat shot flow: on NextPerson, needs blend) + if (cameraManager != null && cameraManager.CurrentState != AirplaneCameraState.Aiming) + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Blending to Aiming camera..."); + yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.Aiming)); + } + else if (showDebugLogs) + { + Logging.Debug("[AirplaneGameManager] Already on Aiming camera, skipping blend"); + } + + AirplaneAbilityType selectedType = AirplaneAbilityType.None; + + // Show selection UI + if (selectionUI != null) + { + selectionUI.Show(); + + // Wait for player to select and confirm + yield return new WaitUntil(() => selectionUI.HasSelectedType); + + selectedType = selectionUI.GetSelectedType(); + selectionUI.Hide(); + + if (showDebugLogs) + { + Logging.Debug($"[AirplaneGameManager] Selected airplane: {selectedType}"); + } + } + else + { + Logging.Warning("[AirplaneGameManager] SelectionUI not assigned! Using default airplane type."); + + // Fallback: use default type from settings + if (_cachedSettings != null) + { + selectedType = _cachedSettings.DefaultAirplaneType; + } + else + { + selectedType = AirplaneAbilityType.Jet; // Ultimate fallback + } + + if (showDebugLogs) + { + Logging.Debug($"[AirplaneGameManager] No selection UI, using default: {selectedType}"); + } + } + + onSelected?.Invoke(selectedType); + } + + /// + /// Phase: Play person reaction animation + /// + private IEnumerator PlayPersonReaction(bool success) + { + if (personQueue == null || _currentPerson == null) + { + yield break; + } + + if (showDebugLogs) + { + Logging.Debug($"[AirplaneGameManager] Playing {_currentPerson.PersonName} reaction (success={success})"); + } + + yield return StartCoroutine(personQueue.PlayPersonReaction(success)); + } + /// + /// Enter aiming state - player can aim and launch with selected airplane type + /// Camera should already be on Aiming state from SelectAirplane phase + /// + private void EnterAimingState(AirplaneAbilityType selectedType) { ChangeState(AirplaneGameState.Aiming); if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Ready to aim and launch!"); - // Switch to aiming camera - if (cameraManager != null) - { - cameraManager.SwitchToState(AirplaneCameraState.Aiming); - } - // Spawn airplane at slingshot with selected type - if (launchController != null && _selectedAirplaneType != AirplaneAbilityType.None) + if (launchController != null && selectedType != AirplaneAbilityType.None) { - launchController.SetAirplaneType(_selectedAirplaneType); + launchController.SetAirplaneType(selectedType); if (showDebugLogs) { - Logging.Debug($"[AirplaneGameManager] Spawned airplane at slingshot: {_selectedAirplaneType}"); + Logging.Debug($"[AirplaneGameManager] Spawned airplane at slingshot: {selectedType}"); } } @@ -497,6 +639,45 @@ namespace Minigames.Airplane.Core } } + #endregion + + #region Flyby Sequence + /// + /// Execute cinematic flyby of the target location. + /// Switches to TargetFlyby camera, waits for blend, lingers, then returns to Aiming camera. + /// + private IEnumerator ExecuteTargetFlyby() + { + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Starting target flyby sequence..."); + + if (cameraManager == null) + { + Logging.Warning("[AirplaneGameManager] Cannot execute flyby - camera manager not assigned!"); + yield break; + } + + if (_cachedSettings == null) + { + Logging.Warning("[AirplaneGameManager] Cannot execute flyby - settings not available!"); + yield break; + } + + // 1. Blend to TargetFlyby camera and wait for blend to complete + yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.TargetFlyby)); + + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Blend to target camera complete, lingering..."); + + // 2. Linger on target + yield return new WaitForSeconds(_cachedSettings.TargetFlybyLingerDuration); + + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Linger complete, blending back to aiming..."); + + // 3. Blend back to Aiming camera and wait for blend to complete + yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.Aiming)); + + if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Target flyby sequence complete!"); + } + #endregion #region Event Handlers @@ -700,11 +881,17 @@ namespace Minigames.Airplane.Core launchController.ClearActiveAirplane(); } - // NOTE: Spawned objects cleanup moved to SetupNextPerson() to happen AFTER camera blend - // This ensures camera shows the scene before cleanup and person reaction - - // Move to next person - StartCoroutine(SetupNextPerson()); + // Route to appropriate flow based on result + if (success) + { + // Success: Go to person shuffle flow + StartCoroutine(ExecutePersonShuffleFlow()); + } + else + { + // Failure: Go to repeat shot flow + StartCoroutine(ExecuteRepeatShotFlow()); + } } /// diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs index c23c09fc..31ad5395 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs @@ -69,7 +69,7 @@ namespace Minigames.Airplane.Core [Header("Spawn Threshold")] [Tooltip("Transform marker in scene where dynamic spawning begins (uses X position). If null, uses fallback from settings.")] [SerializeField] private Transform dynamicSpawnThresholdMarker; - + [Header("Spawn Parents")] [Tooltip("Parent transform for spawned objects (optional, for organization)")] [SerializeField] private Transform spawnedObjectsParent; @@ -101,9 +101,9 @@ namespace Minigames.Airplane.Core // Plane tracking private Transform _planeTransform; private bool _isSpawningActive; - private bool _hasPassedThreshold; // Spawning positions (distance-based) + private float _lastSpawnedX; // Tracks the furthest forward X position that has been pre-spawned private float _nextObjectSpawnX; private float _nextGroundSpawnX; @@ -139,7 +139,6 @@ namespace Minigames.Airplane.Core private void Update() { - if (!_isSpawningActive || _planeTransform == null) return; float planeX = _planeTransform.position.x; @@ -150,58 +149,26 @@ namespace Minigames.Airplane.Core _furthestReachedX = planeX; } - // Check if target should be spawned (when plane gets within spawn distance) - if (!_hasSpawnedTarget && _targetPrefabToSpawn != null) + // Check if plane has reached the point where we need to continue spawning beyond pre-spawned content + // Only spawn new content if plane is beyond previous furthest point (for retries) + bool shouldSpawnNewContent = !_isRetryAttempt || planeX > (_furthestReachedX - _settings.SpawnDistanceAhead); + + if (shouldSpawnNewContent) { - float distanceToTarget = _targetSpawnPosition.x - planeX; - if (distanceToTarget <= _settings.SpawnDistanceAhead) + // Continue spawning objects when plane approaches the last spawned position + float spawnTriggerX = _lastSpawnedX + _settings.SpawnDistanceAhead; + if (planeX >= spawnTriggerX && planeX >= _nextObjectSpawnX) { - SpawnTarget(); - _hasSpawnedTarget = true; - - if (showDebugLogs) - { - Logging.Debug($"[SpawnManager] Target spawned at distance {distanceToTarget:F2} from plane"); - } + SpawnRandomObject(); + ScheduleNextObjectSpawn(planeX); } - } - - // Check if plane has crossed threshold - float threshold = dynamicSpawnThresholdMarker.position.x; - - if (!_hasPassedThreshold && planeX >= threshold) - { - _hasPassedThreshold = true; - InitializeDynamicSpawning(); - if (showDebugLogs) + // Continue spawning ground tiles ahead of plane + float groundSpawnTargetX = planeX + GetGroundSpawnAheadDistance(); + while (_nextGroundSpawnX < groundSpawnTargetX) { - Logging.Debug($"[SpawnManager] Plane crossed threshold at X={planeX:F2}"); - } - } - - // If past threshold, handle spawning (only if we're going further than before) - if (_hasPassedThreshold) - { - // Only spawn new content if plane is beyond previous furthest point (for retries) - bool shouldSpawnNewContent = !_isRetryAttempt || planeX > (_furthestReachedX - _settings.SpawnDistanceAhead); - - if (shouldSpawnNewContent) - { - // Spawn objects when plane reaches spawn position - if (planeX >= _nextObjectSpawnX) - { - SpawnRandomObject(); - ScheduleNextObjectSpawn(planeX); - } - - // Spawn ground tiles ahead of plane - float groundSpawnTargetX = planeX + GetGroundSpawnAheadDistance(); - while (_nextGroundSpawnX < groundSpawnTargetX) - { - SpawnGroundTile(); - _nextGroundSpawnX += _settings.GroundSpawnInterval; - } + SpawnGroundTile(); + _nextGroundSpawnX += _settings.GroundSpawnInterval; } } } @@ -221,7 +188,6 @@ namespace Minigames.Airplane.Core { _currentTargetKey = targetKey; _isSpawningActive = false; - _hasPassedThreshold = false; _isRetryAttempt = isRetry; // Only reset target and spawn state if NOT a retry @@ -266,6 +232,94 @@ namespace Minigames.Airplane.Core SetupTargetUI(); } + /// + /// Pre-spawn the entire level from threshold marker to target + buffer. + /// Spawns range: (threshold_x, target_x + preSpawnBeyondTargetDistance) + /// Call this after InitializeForGame() for new turns (not retries). + /// + public void PreSpawnLevelToTarget() + { + if (_targetPrefabToSpawn == null) + { + Logging.Error("[SpawnManager] Cannot pre-spawn - target prefab not initialized! Call InitializeForGame first."); + return; + } + + if (_hasSpawnedTarget) + { + if (showDebugLogs) + { + Logging.Debug("[SpawnManager] Target already spawned, skipping pre-spawn (retry scenario)"); + } + return; + } + + if (dynamicSpawnThresholdMarker == null) + { + Logging.Error("[SpawnManager] Cannot pre-spawn - dynamicSpawnThresholdMarker not assigned!"); + return; + } + + // Get pre-spawn range: from threshold to target + buffer + float preSpawnStartX = dynamicSpawnThresholdMarker.position.x; + float preSpawnEndX = _targetDistance + _settings.PreSpawnBeyondTargetDistance; + + if (showDebugLogs) + { + Logging.Debug($"[SpawnManager] Pre-spawning level from X={preSpawnStartX:F2} (threshold) to X={preSpawnEndX:F2} (target={_targetDistance:F2} + buffer={_settings.PreSpawnBeyondTargetDistance:F2})"); + } + + // 1. Spawn ground tiles FIRST across entire range (so target can raycast) + float currentGroundX = preSpawnStartX; + while (currentGroundX <= preSpawnEndX) + { + SpawnGroundTileAt(currentGroundX); + currentGroundX += _settings.GroundSpawnInterval; + } + + // Set next ground spawn position beyond pre-spawn range + _nextGroundSpawnX = preSpawnEndX + _settings.GroundSpawnInterval; + + if (showDebugLogs) + { + Logging.Debug($"[SpawnManager] Ground tiles spawned, now spawning objects"); + } + + // 2. Spawn objects across entire range (skipping near target) + float currentObjectX = preSpawnStartX + Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance); + + while (currentObjectX <= preSpawnEndX) + { + // Spawn object at this position + SpawnRandomObjectAt(currentObjectX); + + // Move to next spawn position (forward) + float spawnDistance = Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance); + currentObjectX += spawnDistance; + } + + if (showDebugLogs) + { + Logging.Debug($"[SpawnManager] Objects spawned, now spawning target with raycast"); + } + + // 3. Spawn the target LAST (after ground exists for proper raycasting) + SpawnTarget(); + _hasSpawnedTarget = true; + + // 4. Store the furthest forward pre-spawn position as _lastSpawnedX + _lastSpawnedX = preSpawnEndX; + + // 5. Schedule next object spawn beyond the pre-spawn range + _nextObjectSpawnX = preSpawnEndX + Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance); + + if (showDebugLogs) + { + Logging.Debug($"[SpawnManager] Pre-spawn complete! Last spawned X={_lastSpawnedX:F2}, next object at X={_nextObjectSpawnX:F2}"); + Logging.Debug($"[SpawnManager] Spawned {_positiveSpawnCount} positive and {_negativeSpawnCount} negative objects"); + } + } + /// /// Start tracking the airplane and enable spawning. /// @@ -380,7 +434,6 @@ namespace Minigames.Airplane.Core // Reset all spawn state _hasSpawnedTarget = false; - _hasPassedThreshold = false; _furthestReachedX = 0f; _positiveSpawnCount = 0; _negativeSpawnCount = 0; @@ -391,6 +444,33 @@ namespace Minigames.Airplane.Core } } + /// + /// Clean up level based on shot result. + /// Success: Full cleanup (destroys all spawned objects). + /// Failure: Reset for retry (keeps spawned objects, resets tracking). + /// + public void CleanupLevel(bool success) + { + if (success) + { + CleanupSpawnedObjects(); + + if (showDebugLogs) + { + Logging.Debug("[SpawnManager] Level cleanup: SUCCESS - destroyed all objects"); + } + } + else + { + ResetForRetry(); + + if (showDebugLogs) + { + Logging.Debug("[SpawnManager] Level cleanup: FAILURE - kept objects for retry"); + } + } + } + /// /// Reset tracking state for retry attempt (keeps spawned objects). /// Call this when player fails and will retry the same shot. @@ -414,6 +494,15 @@ namespace Minigames.Airplane.Core return (_targetSpawnPosition, _targetDistance, _targetIconSprite); } + /// + /// Get the spawned target's transform for camera tracking. + /// Returns null if target hasn't been spawned yet. + /// + public Transform GetSpawnedTargetTransform() + { + return _spawnedTarget != null ? _spawnedTarget.transform : null; + } + #endregion #region Initialization @@ -494,6 +583,11 @@ namespace Minigames.Airplane.Core { Logging.Warning("[SpawnManager] Launch controller not assigned! Distance calculation will use world origin."); } + + if (dynamicSpawnThresholdMarker == null) + { + Logging.Warning("[SpawnManager] Dynamic spawn threshold marker not assigned! Pre-spawn will fail."); + } } #endregion @@ -501,7 +595,8 @@ namespace Minigames.Airplane.Core #region Target Spawning /// - /// Spawn the target at the predetermined position. + /// Spawn the target at the predetermined X position. + /// Y coordinate is determined via raycast from high position to find ground. /// private void SpawnTarget() { @@ -511,7 +606,13 @@ namespace Minigames.Airplane.Core return; } - // Spawn target at initial position + // Determine Y position via raycast from high position (Y=20) + float targetY = DetermineTargetYPosition(_targetDistance); + + // Update target spawn position with correct Y + _targetSpawnPosition = new Vector3(_targetDistance, targetY, 0f); + + // Spawn target at determined position _spawnedTarget = Instantiate(_currentTargetEntry.prefab, _targetSpawnPosition, Quaternion.identity); if (spawnedObjectsParent != null) @@ -519,14 +620,14 @@ namespace Minigames.Airplane.Core _spawnedTarget.transform.SetParent(spawnedObjectsParent); } - // Position target using configured spawn mode - PositionObject(_spawnedTarget, _targetSpawnPosition.x, + // Position target using configured spawn mode (will refine Y if needed) + PositionObject(_spawnedTarget, _targetDistance, _currentTargetEntry.spawnPositionMode, _currentTargetEntry.specifiedY, _currentTargetEntry.randomYMin, _currentTargetEntry.randomYMax); - // Update target spawn position to actual positioned location + // Update target spawn position to actual final positioned location _targetSpawnPosition = _spawnedTarget.transform.position; // Extract sprite for UI icon @@ -538,6 +639,41 @@ namespace Minigames.Airplane.Core } } + /// + /// Determine target Y position by raycasting downward from high position (Y=20). + /// Ensures target spawns touching the ground surface. + /// + private float DetermineTargetYPosition(float xPosition) + { + // Start raycast from high Y position (20 units up) + Vector2 rayOrigin = new Vector2(xPosition, 20f); + + // Raycast downward to find ground + int layerMask = 1 << _settings.GroundLayer; + RaycastHit2D hit = Physics2D.Raycast( + rayOrigin, + Vector2.down, + _settings.MaxGroundRaycastDistance, + layerMask + ); + + if (hit.collider != null) + { + // Found ground - return ground Y position + if (showDebugLogs) + { + Logging.Debug($"[SpawnManager] Target Y determined via raycast: ground at {hit.point.y:F2}"); + } + return hit.point.y; + } + else + { + // No ground found - use default + Logging.Warning($"[SpawnManager] No ground found for target at X={xPosition:F2} (raycast from Y=20 for {_settings.MaxGroundRaycastDistance} units), using default Y={_settings.DefaultObjectYOffset}"); + return _settings.DefaultObjectYOffset; + } + } + /// /// Extract sprite from target prefab for UI display (without instantiation). /// Finds first SpriteRenderer in prefab or children. @@ -613,24 +749,6 @@ namespace Minigames.Airplane.Core #region Dynamic Spawning - /// - /// Initialize dynamic spawning when threshold is crossed. - /// - private void InitializeDynamicSpawning() - { - // Schedule first spawn trigger from current plane position - // Actual spawning will happen at look-ahead distance - if (_planeTransform != null) - { - ScheduleNextObjectSpawn(_planeTransform.position.x); - - if (showDebugLogs) - { - Logging.Debug($"[SpawnManager] Dynamic spawning initialized, first spawn trigger at planeX={_nextObjectSpawnX:F2}"); - } - } - } - /// /// Get the distance ahead to spawn ground (2x object spawn distance). /// @@ -800,6 +918,105 @@ namespace Minigames.Airplane.Core } } + /// + /// Spawn a ground tile at a specific X position (used for pre-spawn). + /// + private void SpawnGroundTileAt(float xPosition) + { + if (groundTilePrefabs == null || groundTilePrefabs.Length == 0) return; + + // Pick random ground tile + GameObject tilePrefab = groundTilePrefabs[Random.Range(0, groundTilePrefabs.Length)]; + + // Calculate spawn position using configured Y + Vector3 spawnPosition = new Vector3(xPosition, groundSpawnY, 0f); + + // Spawn tile + GameObject spawnedTile = Instantiate(tilePrefab, spawnPosition, Quaternion.identity); + + if (groundTilesParent != null) + { + spawnedTile.transform.SetParent(groundTilesParent); + } + + if (showDebugLogs) + { + Logging.Debug($"[SpawnManager] Pre-spawned ground tile at ({xPosition:F2}, {groundSpawnY:F2})"); + } + } + + /// + /// Spawn a random positive or negative object at a specific X position (used for pre-spawn). + /// + private void SpawnRandomObjectAt(float xPosition) + { + // Check if spawn position is too close to target (avoid obscuring it) + float distanceToTarget = Mathf.Abs(xPosition - _targetSpawnPosition.x); + float targetClearanceZone = 10f; // Don't spawn within 10 units of target + + if (distanceToTarget < targetClearanceZone) + { + // Too close to target, skip this spawn + if (showDebugLogs) + { + Logging.Debug($"[SpawnManager] Skipped pre-spawn at X={xPosition:F2} (too close to target at X={_targetSpawnPosition.x:F2})"); + } + return; + } + + // Determine if spawning positive or negative based on weighted ratio + bool spawnPositive = ShouldSpawnPositive(); + + PrefabSpawnEntry entryToSpawn = null; + + if (spawnPositive) + { + if (positiveObjectPrefabs != null && positiveObjectPrefabs.Length > 0) + { + entryToSpawn = positiveObjectPrefabs[Random.Range(0, positiveObjectPrefabs.Length)]; + _positiveSpawnCount++; + } + } + else + { + if (negativeObjectPrefabs != null && negativeObjectPrefabs.Length > 0) + { + entryToSpawn = negativeObjectPrefabs[Random.Range(0, negativeObjectPrefabs.Length)]; + _negativeSpawnCount++; + } + } + + if (entryToSpawn == null || entryToSpawn.prefab == null) return; + + // Spawn object at temporary position + Vector3 tempPosition = new Vector3(xPosition, 0f, 0f); + GameObject spawnedObject = Instantiate(entryToSpawn.prefab, tempPosition, Quaternion.identity); + + if (spawnedObjectsParent != null) + { + spawnedObject.transform.SetParent(spawnedObjectsParent); + } + + // Position object using entry's spawn configuration + PositionObject(spawnedObject, xPosition, + entryToSpawn.spawnPositionMode, + entryToSpawn.specifiedY, + entryToSpawn.randomYMin, + entryToSpawn.randomYMax); + + // Initialize components that need post-spawn setup + var initializable = spawnedObject.GetComponent(); + if (initializable != null) + { + initializable.Initialize(); + } + + if (showDebugLogs) + { + Logging.Debug($"[SpawnManager] Pre-spawned {(spawnPositive ? "positive" : "negative")} object at {spawnedObject.transform.position}"); + } + } + #endregion #region Object Positioning diff --git a/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs b/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs index c135cf8e..39a4846d 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/PersonQueue.cs @@ -212,16 +212,15 @@ namespace Minigames.Airplane.Core } /// - /// Handle post-shot reaction for current person (who just shot). - /// If successful: celebrate, remove from queue, shuffle remaining people. - /// If failed: show disappointment, stay in queue. - /// Awaitable - game flow waits for reactions and animations to complete. + /// Play person reaction animation only (celebration or disappointment). + /// Does not modify queue - use AdvanceQueue() separately for queue management. + /// Awaitable - game flow waits for animation to complete. /// - public IEnumerator HandlePostShotReaction(bool targetHit) + public IEnumerator PlayPersonReaction(bool success) { if (peopleInQueue.Count == 0) { - Logging.Warning("[PersonQueue] HandlePostShotReaction called but queue is empty!"); + Logging.Warning("[PersonQueue] PlayPersonReaction called but queue is empty!"); yield break; } @@ -229,57 +228,59 @@ namespace Minigames.Airplane.Core Person currentPerson = peopleInQueue[0]; if (showDebugLogs) - Logging.Debug($"[PersonQueue] Post-shot reaction for {currentPerson.PersonName} (Hit: {targetHit})"); + Logging.Debug($"[PersonQueue] Playing reaction for {currentPerson.PersonName} (success={success})"); // Call person's reaction based on result - if (targetHit) + if (success) { // Success reaction yield return StartCoroutine(currentPerson.OnTargetHit()); - - if (showDebugLogs) Logging.Debug("[PersonQueue] Success! Removing person and shuffling queue..."); - - // Remember the first person's position BEFORE removing them - Vector3 firstPersonPosition = currentPerson.PersonTransform.position; - - // Remove successful person from queue (they're no longer in peopleInQueue) - RemoveCurrentPerson(); - - // Shuffle remaining people forward to fill the first person's spot - yield return StartCoroutine(ShuffleToPosition(firstPersonPosition)); } else { // Failure reaction yield return StartCoroutine(currentPerson.OnTargetMissed()); - - if (showDebugLogs) Logging.Debug("[PersonQueue] Failed - person stays in queue"); - // On failure, don't remove or shuffle, person gets another turn } - if (showDebugLogs) Logging.Debug("[PersonQueue] Post-shot reaction complete"); + if (showDebugLogs) Logging.Debug("[PersonQueue] Reaction complete"); } /// - /// Introduce the next person (at front of queue) for their turn. - /// Awaitable - game flow waits for introduction to complete. + /// Advance the queue after a shot result. + /// If success: Remove current person and shuffle remaining people forward. + /// If failure: Do nothing (person stays in queue for retry). + /// Awaitable - game flow waits for removal and shuffle animations to complete. /// - public IEnumerator IntroduceNextPerson() + public IEnumerator AdvanceQueue(bool success) { if (peopleInQueue.Count == 0) { - Logging.Warning("[PersonQueue] IntroduceNextPerson called but queue is empty!"); + Logging.Warning("[PersonQueue] AdvanceQueue called but queue is empty!"); yield break; } - Person nextPerson = peopleInQueue[0]; + Person currentPerson = peopleInQueue[0]; - if (showDebugLogs) Logging.Debug($"[PersonQueue] Introducing next person: {nextPerson.PersonName}"); - - // Call person's hello sequence - yield return StartCoroutine(nextPerson.OnHello()); - - if (showDebugLogs) Logging.Debug("[PersonQueue] Introduction complete"); + if (success) + { + if (showDebugLogs) Logging.Debug($"[PersonQueue] Advancing queue - removing {currentPerson.PersonName} and shuffling"); + + // Remember the first person's position BEFORE removing them + Vector3 firstPersonPosition = currentPerson.PersonTransform.position; + + // Remove successful person from queue + RemoveCurrentPerson(); + + // Shuffle remaining people forward to fill the first person's spot + yield return StartCoroutine(ShuffleToPosition(firstPersonPosition)); + + if (showDebugLogs) Logging.Debug("[PersonQueue] Queue advance complete"); + } + else + { + if (showDebugLogs) Logging.Debug($"[PersonQueue] Failed - {currentPerson.PersonName} stays in queue for retry"); + // On failure, don't remove or shuffle, person gets another turn + } } /// diff --git a/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs b/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs index 3068533f..042583e1 100644 --- a/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs +++ b/Assets/Scripts/Minigames/Airplane/Data/AirplaneCameraState.cs @@ -8,7 +8,8 @@ namespace Minigames.Airplane.Data Intro, // Intro sequence camera NextPerson, // Camera focusing on the next person Aiming, // Camera for aiming the airplane - Flight // Camera following the airplane in flight + Flight, // Camera following the airplane in flight + TargetFlyby // Camera showing the target location (cinematic flyby) } } diff --git a/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs b/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs index 5fe8b4f3..aa57e44d 100644 --- a/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs +++ b/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs @@ -75,9 +75,15 @@ namespace Minigames.Airplane.Settings [Tooltip("Duration of result evaluation (seconds)")] [SerializeField] private float evaluationDuration = 1f; + [Tooltip("Duration to linger on target during flyby (seconds)")] + [SerializeField] private float targetFlybyLingerDuration = 1.5f; + + [Tooltip("Fallback blend time if CinemachineBrain blend detection unavailable (seconds)")] + [SerializeField] private float targetFlybyCameraBlendTime = 1f; + [Header("Spawn System")] - [Tooltip("Transform marker in scene where dynamic spawning begins (uses X position). If null, uses fallback distance.")] - [SerializeField] private Transform dynamicSpawnThresholdMarker; + [Tooltip("Distance beyond target to pre-spawn (extends the pre-spawn range)")] + [SerializeField] private float preSpawnBeyondTargetDistance = 30f; [Tooltip("Minimum random distance for target spawn")] [SerializeField] private float targetMinDistance = 30f; @@ -139,6 +145,9 @@ namespace Minigames.Airplane.Settings public float IntroDuration => introDuration; public float PersonIntroDuration => personIntroDuration; public float EvaluationDuration => evaluationDuration; + public float TargetFlybyLingerDuration => targetFlybyLingerDuration; + public float TargetFlybyCameraBlendTime => targetFlybyCameraBlendTime; + public float PreSpawnBeyondTargetDistance => preSpawnBeyondTargetDistance; public float TargetMinDistance => targetMinDistance; public float TargetMaxDistance => targetMaxDistance; public float ObjectSpawnMinDistance => objectSpawnMinDistance; diff --git a/Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs b/Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs index 401d31a0..1b36be50 100644 --- a/Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs +++ b/Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs @@ -15,11 +15,12 @@ namespace Minigames.BirdPooper public UnityEngine.Events.UnityEvent OnFlap; public UnityEngine.Events.UnityEvent OnPlayerDamaged; - private Rigidbody2D rb; - private IBirdPooperSettings settings; - private float verticalVelocity = 0f; - private bool isDead = false; - private float fixedXPosition; // Store the initial X position from the scene + private Rigidbody2D _rb; + private IBirdPooperSettings _settings; + private float _verticalVelocity; + private bool _isDead; + private float _fixedXPosition; // Store the initial X position from the scene + private bool _isInitialized; // Flag to control when physics/input are active internal override void OnManagedAwake() { @@ -31,33 +32,49 @@ namespace Minigames.BirdPooper if (OnPlayerDamaged == null) OnPlayerDamaged = new UnityEngine.Events.UnityEvent(); - // Load settings - settings = GameManager.GetSettingsObject(); - if (settings == null) - { - Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!"); - return; - } - - // Get Rigidbody2D component (Dynamic with gravityScale = 0) - rb = GetComponent(); - if (rb != null) - { - rb.gravityScale = 0f; // Disable Unity physics gravity - rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces - - // Store the initial X position from the scene - fixedXPosition = rb.position.x; - Debug.Log($"[BirdPlayerController] Fixed X position set to: {fixedXPosition}"); - } - else + // Only cache component references - NO setup yet + _rb = GetComponent(); + if (_rb == null) { Debug.LogError("[BirdPlayerController] Rigidbody2D component not found!"); - return; } - // Register as default consumer (gets input if nothing else consumes it) - // This allows UI buttons to work while still flapping when tapping empty space + // Load settings + _settings = GameManager.GetSettingsObject(); + if (_settings == null) + { + Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!"); + } + + Debug.Log("[BirdPlayerController] References cached, waiting for initialization..."); + } + + /// + /// Initializes the player controller - enables physics and input. + /// Should be called by BirdPooperGameManager when ready to start the game. + /// + public void Initialize() + { + if (_isInitialized) + { + Debug.LogWarning("[BirdPlayerController] Already initialized!"); + return; + } + + if (_rb == null || _settings == null) + { + Debug.LogError("[BirdPlayerController] Cannot initialize - missing references!"); + return; + } + + // Setup physics + _rb.gravityScale = 0f; // Disable Unity physics gravity + _rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces + + // Store the initial X position from the scene + _fixedXPosition = _rb.position.x; + + // Register as default input consumer if (Input.InputManager.Instance != null) { Input.InputManager.Instance.SetDefaultConsumer(this); @@ -67,52 +84,74 @@ namespace Minigames.BirdPooper { Debug.LogError("[BirdPlayerController] InputManager instance not found!"); } + + _isInitialized = true; + Debug.Log($"[BirdPlayerController] Initialized! Fixed X position: {_fixedXPosition}"); } private void Update() { - if (!isDead && settings != null && rb != null) - { - // Apply manual gravity - verticalVelocity -= settings.Gravity * Time.deltaTime; - - // Cap fall speed (terminal velocity) - if (verticalVelocity < -settings.MaxFallSpeed) - verticalVelocity = -settings.MaxFallSpeed; - - // Update position manually - Vector2 newPosition = rb.position; - newPosition.y += verticalVelocity * Time.deltaTime; - newPosition.x = fixedXPosition; // Keep X fixed at scene-configured position - - // Clamp Y position to bounds - newPosition.y = Mathf.Clamp(newPosition.y, settings.MinY, settings.MaxY); - - rb.MovePosition(newPosition); - - // Update rotation based on velocity - UpdateRotation(); - } + // Only run physics/movement if initialized + if (!_isInitialized || _isDead || _settings == null || _rb == null) + return; + + // Apply manual gravity + _verticalVelocity -= _settings.Gravity * Time.deltaTime; + + // Cap fall speed (terminal velocity) + if (_verticalVelocity < -_settings.MaxFallSpeed) + _verticalVelocity = -_settings.MaxFallSpeed; + + // Update position manually + Vector2 newPosition = _rb.position; + newPosition.y += _verticalVelocity * Time.deltaTime; + newPosition.x = _fixedXPosition; // Keep X fixed at scene-configured position + + // Clamp Y position to bounds + newPosition.y = Mathf.Clamp(newPosition.y, _settings.MinY, _settings.MaxY); + + _rb.MovePosition(newPosition); + + // Update rotation based on velocity + UpdateRotation(); } #region ITouchInputConsumer Implementation public void OnTap(Vector2 tapPosition) { - if (!isDead && settings != null) - { - verticalVelocity = settings.FlapForce; - Debug.Log($"[BirdPlayerController] Flap! velocity = {verticalVelocity}"); - - // Emit flap event - OnFlap?.Invoke(); - } + // Only respond to input if initialized and alive + if (!_isInitialized || _isDead || _settings == null) + return; + + Flap(); } public void OnHoldStart(Vector2 position) { } public void OnHoldMove(Vector2 position) { } public void OnHoldEnd(Vector2 position) { } + #endregion + + #region Player Actions + + /// + /// Makes the bird flap, applying upward velocity. + /// Can be called by input system or externally (e.g., for first tap). + /// + public void Flap() + { + if (!_isInitialized || _isDead || _settings == null) + return; + + _verticalVelocity = _settings.FlapForce; + Debug.Log($"[BirdPlayerController] Flap! velocity = {_verticalVelocity}"); + + // Emit flap event + OnFlap?.Invoke(); + } + + #endregion #region Rotation @@ -123,19 +162,19 @@ namespace Minigames.BirdPooper /// private void UpdateRotation() { - if (settings == null) return; + if (_settings == null) return; // Map velocity to rotation angle // When falling at max speed (-MaxFallSpeed): -MaxRotationAngle (down) // When at flap velocity (+FlapForce): +MaxRotationAngle (up) float velocityPercent = Mathf.InverseLerp( - -settings.MaxFallSpeed, - settings.FlapForce, - verticalVelocity + -_settings.MaxFallSpeed, + _settings.FlapForce, + _verticalVelocity ); float targetAngle = Mathf.Lerp( - -settings.MaxRotationAngle, - settings.MaxRotationAngle, + -_settings.MaxRotationAngle, + _settings.MaxRotationAngle, velocityPercent ); @@ -148,7 +187,7 @@ namespace Minigames.BirdPooper float smoothedAngle = Mathf.Lerp( currentAngle, targetAngle, - settings.RotationSpeed * Time.deltaTime + _settings.RotationSpeed * Time.deltaTime ); // Apply rotation to Z axis only (2D rotation) @@ -175,10 +214,10 @@ namespace Minigames.BirdPooper private void HandleDeath() { // Only process death once - if (isDead) return; + if (_isDead) return; - isDead = true; - verticalVelocity = 0f; + _isDead = true; + _verticalVelocity = 0f; Debug.Log("[BirdPlayerController] Bird died!"); // Emit damage event - let the game manager handle UI @@ -187,9 +226,9 @@ namespace Minigames.BirdPooper #endregion - #region Public Properties + #region Public Accessors - public bool IsDead => isDead; + public bool IsDead => _isDead; #endregion } diff --git a/Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs b/Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs index 476dfa46..c3037092 100644 --- a/Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs +++ b/Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs @@ -17,12 +17,13 @@ namespace Minigames.BirdPooper [SerializeField] private BirdPlayerController player; [SerializeField] private ObstacleSpawner obstacleSpawner; [SerializeField] private TargetSpawner targetSpawner; + [SerializeField] private TapToStartController tapToStartController; [SerializeField] private GameOverScreen gameOverScreen; [SerializeField] private GameObject poopPrefab; [Header("Game State")] - private int targetsHit; - private bool isGameOver; + private int _targetsHit; + private bool _isGameOver; [Header("Input")] [Tooltip("Minimum seconds between consecutive poop spawns")] @@ -60,6 +61,11 @@ namespace Minigames.BirdPooper Debug.LogWarning("[BirdPooperGameManager] TargetSpawner reference not assigned! Targets will not spawn."); } + if (tapToStartController == null) + { + Debug.LogError("[BirdPooperGameManager] TapToStartController reference not assigned!"); + } + if (gameOverScreen == null) { Debug.LogError("[BirdPooperGameManager] GameOverScreen reference not assigned!"); @@ -76,34 +82,76 @@ namespace Minigames.BirdPooper } } - internal override void OnManagedStart() + /// + /// Called after scene is fully loaded and any save data is restored. + /// Activates tap-to-start UI instead of starting immediately. + /// + internal override void OnSceneRestoreCompleted() { - base.OnManagedStart(); + base.OnSceneRestoreCompleted(); + Debug.Log("[BirdPooperGameManager] Scene fully loaded, activating tap-to-start..."); + + if (tapToStartController != null) + { + tapToStartController.Activate(); + } + else + { + Debug.LogError("[BirdPooperGameManager] TapToStartController missing! Starting game immediately as fallback."); + BeginMinigame(); + } + } + + /// + /// Central method to begin the minigame. + /// Initializes player, starts spawners, and sets up game state. + /// + public void BeginMinigame() + { // Initialize game state - isGameOver = false; - targetsHit = 0; + _isGameOver = false; + _targetsHit = 0; - // Subscribe to player events + // Initialize and enable player if (player != null) { + player.Initialize(); player.OnPlayerDamaged.AddListener(HandlePlayerDamaged); - Debug.Log("[BirdPooperGameManager] Subscribed to player damaged event"); + + // Make bird do initial flap so first tap feels responsive + player.Flap(); + + Debug.Log("[BirdPooperGameManager] Player initialized and event subscribed"); + } + else + { + Debug.LogError("[BirdPooperGameManager] Cannot begin minigame - player reference missing!"); } // Start obstacle spawning if (obstacleSpawner != null) { obstacleSpawner.StartSpawning(); - Debug.Log("[BirdPooperGameManager] Started obstacle spawning"); + Debug.Log("[BirdPooperGameManager] Obstacle spawner started"); + } + else + { + Debug.LogError("[BirdPooperGameManager] Cannot begin minigame - obstacle spawner reference missing!"); } // Start target spawning if (targetSpawner != null) { targetSpawner.StartSpawning(); - Debug.Log("[BirdPooperGameManager] Started target spawning"); + Debug.Log("[BirdPooperGameManager] Target spawner started"); } + else + { + Debug.LogWarning("[BirdPooperGameManager] Target spawner reference missing - targets will not spawn"); + } + + Debug.Log("[BirdPooperGameManager] ✅ Minigame started successfully!"); } internal override void OnManagedDestroy() @@ -129,10 +177,10 @@ namespace Minigames.BirdPooper /// private void HandlePlayerDamaged() { - if (isGameOver) return; + if (_isGameOver) return; - isGameOver = true; - Debug.Log($"[BirdPooperGameManager] Player damaged - Game Over! Targets Hit: {targetsHit}"); + _isGameOver = true; + Debug.Log($"[BirdPooperGameManager] Player damaged - Game Over! Targets Hit: {_targetsHit}"); // Stop spawning obstacles if (obstacleSpawner != null) @@ -167,7 +215,7 @@ namespace Minigames.BirdPooper if (Time.time < _lastPoopTime + poopCooldown) return; - if (isGameOver || player == null || poopPrefab == null) + if (_isGameOver || player == null || poopPrefab == null) return; Vector3 spawnPosition = player.transform.position; @@ -183,16 +231,17 @@ namespace Minigames.BirdPooper /// public void OnTargetHit() { - if (isGameOver) return; + if (_isGameOver) return; - targetsHit++; - Debug.Log($"[BirdPooperGameManager] Target Hit! Total: {targetsHit}"); + _targetsHit++; + Debug.Log($"[BirdPooperGameManager] Target Hit! Total: {_targetsHit}"); } #region Public Accessors - public bool IsGameOver => isGameOver; - public int TargetsHit => targetsHit; + public bool IsGameOver => _isGameOver; + public int TargetsHit => _targetsHit; + public BirdPlayerController Player => player; #endregion } diff --git a/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs new file mode 100644 index 00000000..516b60ec --- /dev/null +++ b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs @@ -0,0 +1,111 @@ +using System; +using UnityEngine; + +namespace Minigames.BirdPooper +{ + /// + /// Container for a pool of obstacle prefabs at a specific difficulty tier. + /// Pools are ordered by difficulty, with pool[0] being the easiest. + /// + [Serializable] + public class ObstaclePool + { + [Tooltip("Obstacles in this difficulty tier")] + public GameObject[] obstacles; + } + + /// + /// Configuration for obstacle spawning in Bird Pooper minigame. + /// Includes difficulty pools, spawn timing, and diversity settings. + /// + [Serializable] + public class ObstacleSpawnConfig + { + [Header("Difficulty Pools")] + [Tooltip("Obstacle pools ordered by difficulty (pool[0] = easiest, always active)")] + public ObstaclePool[] obstaclePools; + + [Tooltip("Times (in seconds) when each additional pool unlocks. Length should be obstaclePools.Length - 1. At poolUnlockTimes[i], pool[i+1] becomes available.")] + public float[] poolUnlockTimes; + + [Header("Spawn Timing")] + [Tooltip("Minimum interval between spawns (seconds) - represents high difficulty")] + public float minSpawnInterval = 1f; + + [Tooltip("Maximum interval between spawns (seconds) - represents low difficulty")] + public float maxSpawnInterval = 2f; + + [Header("Difficulty Scaling")] + [Tooltip("Time in seconds for difficulty to ramp from 0 to 1")] + public float difficultyRampDuration = 60f; + + [Tooltip("Curve mapping normalized time (0..1) to difficulty (0..1). Default is linear.")] + public AnimationCurve difficultyCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f); + + [Tooltip("Maximum jitter applied to computed interval (fractional). 0.1 = +/-10% jitter")] + public float intervalJitter = 0.05f; + + [Header("Recency / Diversity")] + [Tooltip("Time in seconds it takes for a recently-used prefab to recover back to full weight")] + public float recentDecayDuration = 10f; + + [Tooltip("Minimum weight (0..1) applied to a just-used prefab so it can still appear occasionally")] + [Range(0f, 1f)] + public float minRecentWeight = 0.05f; + + /// + /// Validates the configuration and logs warnings for invalid settings. + /// + public void Validate() + { + // Validate pools + if (obstaclePools == null || obstaclePools.Length == 0) + { + Debug.LogError("[ObstacleSpawnConfig] No obstacle pools defined!"); + return; + } + + // Validate pool unlock times + int expectedUnlockTimes = obstaclePools.Length - 1; + if (poolUnlockTimes == null) + { + Debug.LogWarning($"[ObstacleSpawnConfig] poolUnlockTimes is null. Expected {expectedUnlockTimes} entries. Only pool[0] will be available."); + } + else if (poolUnlockTimes.Length != expectedUnlockTimes) + { + Debug.LogWarning($"[ObstacleSpawnConfig] poolUnlockTimes.Length ({poolUnlockTimes.Length}) does not match expected value ({expectedUnlockTimes}). Should be obstaclePools.Length - 1."); + } + + // Validate spawn intervals + if (minSpawnInterval < 0f) + { + Debug.LogWarning("[ObstacleSpawnConfig] minSpawnInterval is negative. Clamping to 0."); + } + if (maxSpawnInterval < 0f) + { + Debug.LogWarning("[ObstacleSpawnConfig] maxSpawnInterval is negative. Clamping to 0."); + } + if (minSpawnInterval > maxSpawnInterval) + { + Debug.LogWarning("[ObstacleSpawnConfig] minSpawnInterval is greater than maxSpawnInterval. Values should be swapped."); + } + + // Validate difficulty ramp + if (difficultyRampDuration < 0.01f) + { + Debug.LogWarning("[ObstacleSpawnConfig] difficultyRampDuration is too small. Should be at least 0.01."); + } + + // Validate recency settings + if (recentDecayDuration < 0.01f) + { + Debug.LogWarning("[ObstacleSpawnConfig] recentDecayDuration is too small. Should be at least 0.01."); + } + if (minRecentWeight < 0f || minRecentWeight > 1f) + { + Debug.LogWarning("[ObstacleSpawnConfig] minRecentWeight should be between 0 and 1."); + } + } + } +} + diff --git a/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs.meta b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs.meta new file mode 100644 index 00000000..0a45fee2 --- /dev/null +++ b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 74f4387c76774225afa2d02d590d5ad4 +timeCreated: 1765918010 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs index efffb54b..cfa2f0db 100644 --- a/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs +++ b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs @@ -4,6 +4,7 @@ using Core.Settings; using Core.Lifecycle; using AppleHillsCamera; using System.Text; +using System.Collections.Generic; namespace Minigames.BirdPooper { @@ -11,6 +12,7 @@ namespace Minigames.BirdPooper /// Spawns obstacles at regular intervals for Bird Pooper minigame. /// Uses Transform references for spawn and despawn positions instead of hardcoded values. /// All obstacles are spawned at Y = 0 (prefabs should be authored accordingly). + /// Supports dynamic difficulty pools that unlock over time. /// public class ObstacleSpawner : ManagedBehaviour { @@ -20,6 +22,9 @@ namespace Minigames.BirdPooper [Tooltip("Transform marking where obstacles despawn (off-screen left)")] [SerializeField] private Transform despawnPoint; + + [Tooltip("Optional parent transform for spawned obstacles (for scene organization)")] + [SerializeField] private Transform obstacleContainer; [Header("EdgeAnchor References")] [Tooltip("ScreenReferenceMarker to pass to spawned obstacles")] @@ -28,55 +33,45 @@ namespace Minigames.BirdPooper [Tooltip("CameraScreenAdapter to pass to spawned obstacles")] [SerializeField] private CameraScreenAdapter cameraAdapter; - [Header("Obstacle Prefabs")] - [Tooltip("Array of obstacle prefabs to spawn randomly")] - [SerializeField] private GameObject[] obstaclePrefabs; - - [Header("Spawn Timing")] - [Tooltip("Minimum interval between spawns (seconds)")] - [SerializeField] private float minSpawnInterval = 1f; - [Tooltip("Maximum interval between spawns (seconds)")] - [SerializeField] private float maxSpawnInterval = 2f; - - [Header("Difficulty")] - [Tooltip("Time in seconds for difficulty to ramp from 0 to 1")] - [SerializeField] private float difficultyRampDuration = 60f; - [Tooltip("Curve mapping normalized time (0..1) to difficulty (0..1). Default is linear.")] - [SerializeField] private AnimationCurve difficultyCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f); - [Tooltip("Maximum jitter applied to computed interval (fractional). 0.1 = +/-10% jitter")] - [SerializeField] private float intervalJitter = 0.05f; - - [Header("Recency / Diversity")] - [Tooltip("Time in seconds it takes for a recently-used prefab to recover back to full weight")] - [SerializeField] private float recentDecayDuration = 10f; - [Tooltip("Minimum weight (0..1) applied to a just-used prefab so it can still appear occasionally")] - [Range(0f, 1f)] - [SerializeField] private float minRecentWeight = 0.05f; - - private IBirdPooperSettings settings; - private float spawnTimer; - private bool isSpawning; + private IBirdPooperSettings _settings; + private ObstacleSpawnConfig _spawnConfig; + private float _spawnTimer; + private bool _isSpawning; private float _currentSpawnInterval = 1f; - // difficulty tracking - private float _elapsedTime = 0f; + // Difficulty tracking + private float _elapsedTime; - // recency tracking + // Master obstacle list for recency tracking + private List _allObstacles; + private Dictionary _obstacleToGlobalIndex; private float[] _lastUsedTimes; - internal override void OnManagedAwake() + /// + /// Initializes the obstacle spawner by loading settings, validating references, and building obstacle pools. + /// Should be called once before spawning begins. + /// + private void Initialize() { - base.OnManagedAwake(); - // Load settings - settings = GameManager.GetSettingsObject(); - if (settings == null) + _settings = GameManager.GetSettingsObject(); + if (_settings == null) { - Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found!"); - // continue — we now use min/max interval fields instead of relying on settings + Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found! Cannot initialize."); + return; } - // Validate references + _spawnConfig = _settings.ObstacleSpawnConfiguration; + if (_spawnConfig == null) + { + Debug.LogError("[ObstacleSpawner] ObstacleSpawnConfiguration not found in settings! Cannot initialize."); + return; + } + + // Validate spawn configuration + _spawnConfig.Validate(); + + // Validate scene references if (spawnPoint == null) { Debug.LogError("[ObstacleSpawner] Spawn Point not assigned! Please assign a Transform in the Inspector."); @@ -87,11 +82,6 @@ namespace Minigames.BirdPooper Debug.LogError("[ObstacleSpawner] Despawn Point not assigned! Please assign a Transform in the Inspector."); } - if (obstaclePrefabs == null || obstaclePrefabs.Length == 0) - { - Debug.LogError("[ObstacleSpawner] No obstacle prefabs assigned! Please assign at least one prefab in the Inspector."); - } - if (referenceMarker == null) { Debug.LogError("[ObstacleSpawner] ScreenReferenceMarker not assigned! Obstacles need this for EdgeAnchor positioning."); @@ -102,58 +92,100 @@ namespace Minigames.BirdPooper Debug.LogWarning("[ObstacleSpawner] CameraScreenAdapter not assigned. EdgeAnchor will attempt to auto-find camera."); } - // Validate interval range - if (minSpawnInterval < 0f) minSpawnInterval = 0f; - if (maxSpawnInterval < 0f) maxSpawnInterval = 0f; - if (minSpawnInterval > maxSpawnInterval) + // Build master obstacle list from all pools + BuildMasterObstacleList(); + + Debug.Log("[ObstacleSpawner] Initialized successfully with pool-based difficulty scaling"); + } + + /// + /// Builds a master list of all obstacles across all pools and creates index mappings for recency tracking. + /// + private void BuildMasterObstacleList() + { + _allObstacles = new List(); + _obstacleToGlobalIndex = new Dictionary(); + + if (_spawnConfig.obstaclePools == null || _spawnConfig.obstaclePools.Length == 0) { - float tmp = minSpawnInterval; - minSpawnInterval = maxSpawnInterval; - maxSpawnInterval = tmp; - Debug.LogWarning("[ObstacleSpawner] minSpawnInterval was greater than maxSpawnInterval. Values were swapped."); + Debug.LogError("[ObstacleSpawner] No obstacle pools defined in configuration!"); + return; } - // Clamp ramp duration - if (difficultyRampDuration < 0.01f) difficultyRampDuration = 0.01f; + int globalIndex = 0; + for (int poolIdx = 0; poolIdx < _spawnConfig.obstaclePools.Length; poolIdx++) + { + ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx]; + if (pool == null || pool.obstacles == null || pool.obstacles.Length == 0) + { + Debug.LogWarning($"[ObstacleSpawner] Pool[{poolIdx}] is empty or null!"); + continue; + } - // Clamp recency - if (recentDecayDuration < 0.01f) recentDecayDuration = 0.01f; - if (minRecentWeight < 0f) minRecentWeight = 0f; - if (minRecentWeight > 1f) minRecentWeight = 1f; + foreach (GameObject prefab in pool.obstacles) + { + if (prefab == null) + { + Debug.LogWarning($"[ObstacleSpawner] Null prefab found in pool[{poolIdx}]"); + continue; + } - // Initialize last-used timestamps so prefabs start available (set to sufficiently negative so they appear with full weight) - int n = obstaclePrefabs != null ? obstaclePrefabs.Length : 0; - _lastUsedTimes = new float[n]; - float initTime = -recentDecayDuration - 1f; - for (int i = 0; i < n; i++) _lastUsedTimes[i] = initTime; + // Allow duplicates - same prefab can appear in multiple pools + if (!_obstacleToGlobalIndex.ContainsKey(prefab)) + { + _obstacleToGlobalIndex[prefab] = globalIndex; + _allObstacles.Add(prefab); + globalIndex++; + } + } + } - Debug.Log("[ObstacleSpawner] Initialized successfully"); + // Initialize recency tracking + int totalObstacles = _allObstacles.Count; + _lastUsedTimes = new float[totalObstacles]; + float initTime = Time.time - _spawnConfig.recentDecayDuration - 1f; + for (int i = 0; i < totalObstacles; i++) + { + _lastUsedTimes[i] = initTime; + } + + // Log pool configuration + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"[ObstacleSpawner] Loaded {_spawnConfig.obstaclePools.Length} obstacle pools with {totalObstacles} unique obstacles:"); + for (int i = 0; i < _spawnConfig.obstaclePools.Length; i++) + { + ObstaclePool pool = _spawnConfig.obstaclePools[i]; + int obstacleCount = pool != null && pool.obstacles != null ? pool.obstacles.Length : 0; + float unlockTime = (i == 0) ? 0f : (_spawnConfig.poolUnlockTimes != null && i - 1 < _spawnConfig.poolUnlockTimes.Length ? _spawnConfig.poolUnlockTimes[i - 1] : -1f); + sb.AppendLine($" Pool[{i}]: {obstacleCount} obstacles, unlocks at {unlockTime}s"); + } + Debug.Log(sb.ToString()); } private void Update() { - if (!isSpawning || spawnPoint == null) return; + if (!_isSpawning || spawnPoint == null || _spawnConfig == null) return; - spawnTimer += Time.deltaTime; + _spawnTimer += Time.deltaTime; _elapsedTime += Time.deltaTime; - if (spawnTimer >= _currentSpawnInterval) + if (_spawnTimer >= _currentSpawnInterval) { SpawnObstacle(); - spawnTimer = 0f; + _spawnTimer = 0f; - // pick next interval based on difficulty ramp - float t = Mathf.Clamp01(_elapsedTime / difficultyRampDuration); - float difficulty = difficultyCurve.Evaluate(t); // 0..1 + // Pick next interval based on difficulty ramp + float t = Mathf.Clamp01(_elapsedTime / _spawnConfig.difficultyRampDuration); + float difficulty = _spawnConfig.difficultyCurve.Evaluate(t); // 0..1 - // map difficulty to interval: difficulty 0 -> max interval (easy), 1 -> min interval (hard) - float baseInterval = Mathf.Lerp(maxSpawnInterval, minSpawnInterval, difficulty); + // Map difficulty to interval: difficulty 0 -> max interval (easy), 1 -> min interval (hard) + float baseInterval = Mathf.Lerp(_spawnConfig.maxSpawnInterval, _spawnConfig.minSpawnInterval, difficulty); - // apply small jitter - if (intervalJitter > 0f) + // Apply small jitter + if (_spawnConfig.intervalJitter > 0f) { - float jitter = Random.Range(-intervalJitter, intervalJitter); + float jitter = Random.Range(-_spawnConfig.intervalJitter, _spawnConfig.intervalJitter); _currentSpawnInterval = Mathf.Max(0f, baseInterval * (1f + jitter)); } else @@ -167,14 +199,14 @@ namespace Minigames.BirdPooper } /// - /// Spawn a random obstacle at the spawn point position (Y = 0). + /// Spawn a random obstacle from currently unlocked pools at the spawn point position (Y = 0). /// Uses timestamp/decay weighting so prefabs used recently are less likely. /// private void SpawnObstacle() { - if (obstaclePrefabs == null || obstaclePrefabs.Length == 0) + if (_spawnConfig == null || _spawnConfig.obstaclePools == null || _spawnConfig.obstaclePools.Length == 0) { - Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs to spawn!"); + Debug.LogWarning("[ObstacleSpawner] No obstacle pools configured!"); return; } @@ -184,53 +216,133 @@ namespace Minigames.BirdPooper return; } - int count = obstaclePrefabs.Length; - - // Defensive: ensure _lastUsedTimes is initialized and matches prefab count - if (_lastUsedTimes == null || _lastUsedTimes.Length != count) + // Determine which pools are currently unlocked based on elapsed time + int unlockedPoolCount = 1; // Pool[0] is always unlocked + if (_spawnConfig.poolUnlockTimes != null) { - _lastUsedTimes = new float[count]; - float initTime = Time.time - recentDecayDuration - 1f; - for (int i = 0; i < count; i++) _lastUsedTimes[i] = initTime; + for (int i = 0; i < _spawnConfig.poolUnlockTimes.Length; i++) + { + if (_elapsedTime >= _spawnConfig.poolUnlockTimes[i]) + { + unlockedPoolCount = i + 2; // +2 because we're unlocking pool[i+1] + } + else + { + break; // Times should be in order, so stop when we hit a future unlock + } + } } - // compute weights based on recency (newer = lower weight) - float[] weights = new float[count]; + // Clamp to available pools + unlockedPoolCount = Mathf.Min(unlockedPoolCount, _spawnConfig.obstaclePools.Length); + + // Build list of available obstacles from unlocked pools + List availableObstacles = new List(); + List availableGlobalIndices = new List(); + + for (int poolIdx = 0; poolIdx < unlockedPoolCount; poolIdx++) + { + ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx]; + if (pool == null || pool.obstacles == null) continue; + + foreach (GameObject prefab in pool.obstacles) + { + if (prefab == null) continue; + + // Add to available list (duplicates allowed if same prefab is in multiple pools) + availableObstacles.Add(prefab); + + // Look up global index for recency tracking + if (_obstacleToGlobalIndex.TryGetValue(prefab, out int globalIdx)) + { + availableGlobalIndices.Add(globalIdx); + } + else + { + Debug.LogWarning($"[ObstacleSpawner] Prefab '{prefab.name}' not found in global index!"); + availableGlobalIndices.Add(-1); // Invalid index + } + } + } + + if (availableObstacles.Count == 0) + { + Debug.LogWarning($"[ObstacleSpawner] No obstacles available in unlocked pools (0..{unlockedPoolCount-1})"); + return; + } + + // Compute weights based on recency + float[] weights = new float[availableObstacles.Count]; float now = Time.time; - for (int i = 0; i < count; i++) + + for (int i = 0; i < availableObstacles.Count; i++) { - float age = now - _lastUsedTimes[i]; - float normalized = Mathf.Clamp01(age / recentDecayDuration); // 0 = just used, 1 = fully recovered - float weight = Mathf.Max(minRecentWeight, normalized); // ensure minimum probability - weights[i] = weight; // base weight = 1.0, could be extended to per-prefab weights + int globalIdx = availableGlobalIndices[i]; + if (globalIdx < 0 || globalIdx >= _lastUsedTimes.Length) + { + weights[i] = 1f; // Default weight for invalid indices + continue; + } + + float age = now - _lastUsedTimes[globalIdx]; + float normalized = Mathf.Clamp01(age / _spawnConfig.recentDecayDuration); // 0 = just used, 1 = fully recovered + float weight = Mathf.Max(_spawnConfig.minRecentWeight, normalized); + weights[i] = weight; } - // compute probabilities for logging + // Compute and log probabilities for debugging float totalW = 0f; - for (int i = 0; i < count; i++) totalW += Mathf.Max(0f, weights[i]); + for (int i = 0; i < availableObstacles.Count; i++) + { + totalW += Mathf.Max(0f, weights[i]); + } + if (totalW > 0f) { - var sb = new StringBuilder(); - sb.Append("[ObstacleSpawner] Prefab pick probabilities: "); - for (int i = 0; i < count; i++) + StringBuilder sb = new StringBuilder(); + sb.Append($"[ObstacleSpawner] Spawning from pools 0-{unlockedPoolCount-1}. Probabilities: "); + for (int i = 0; i < availableObstacles.Count; i++) { float p = weights[i] / totalW; - string name = obstaclePrefabs[i] != null ? obstaclePrefabs[i].name : i.ToString(); - sb.AppendFormat("{0}:{1:P1}", name, p); - if (i < count - 1) sb.Append(", "); + string prefabName = availableObstacles[i] != null ? availableObstacles[i].name : i.ToString(); + sb.AppendFormat("{0}:{1:P1}", prefabName, p); + if (i < availableObstacles.Count - 1) sb.Append(", "); } Debug.Log(sb.ToString()); } + // Select obstacle using weighted random int chosenIndex = WeightedPickIndex(weights); - GameObject selectedPrefab = obstaclePrefabs[chosenIndex]; + GameObject selectedPrefab = availableObstacles[chosenIndex]; + int selectedGlobalIndex = availableGlobalIndices[chosenIndex]; - // record usage timestamp - _lastUsedTimes[chosenIndex] = Time.time; + // Record usage timestamp for recency tracking + if (selectedGlobalIndex >= 0 && selectedGlobalIndex < _lastUsedTimes.Length) + { + _lastUsedTimes[selectedGlobalIndex] = Time.time; + } + + // Determine which pool this obstacle came from (for logging) + int sourcePool = -1; + for (int poolIdx = 0; poolIdx < unlockedPoolCount; poolIdx++) + { + ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx]; + if (pool != null && pool.obstacles != null && System.Array.IndexOf(pool.obstacles, selectedPrefab) >= 0) + { + sourcePool = poolIdx; + break; + } + } // Spawn at spawn point position with Y = 0 Vector3 spawnPosition = new Vector3(spawnPoint.position.x, 0f, 0f); GameObject obstacleObj = Instantiate(selectedPrefab, spawnPosition, Quaternion.identity); + + // Parent to container if provided + if (obstacleContainer != null) + { + obstacleObj.transform.SetParent(obstacleContainer, true); + } // Initialize obstacle with despawn X position and EdgeAnchor references Obstacle obstacle = obstacleObj.GetComponent(); @@ -244,7 +356,7 @@ namespace Minigames.BirdPooper Destroy(obstacleObj); } - Debug.Log($"[ObstacleSpawner] Spawned obstacle '{selectedPrefab.name}' at position {spawnPosition}"); + Debug.Log($"[ObstacleSpawner] Spawned '{selectedPrefab.name}' from pool[{sourcePool}] at {spawnPosition}"); } private int WeightedPickIndex(float[] weights) @@ -275,20 +387,45 @@ namespace Minigames.BirdPooper /// /// Start spawning obstacles. + /// Initializes the spawner if not already initialized, then begins spawning logic. /// Spawns the first obstacle immediately, then continues with interval-based spawning. /// public void StartSpawning() { - isSpawning = true; - spawnTimer = 0f; + // Initialize if not already done + if (_spawnConfig == null) + { + Initialize(); + } + + // Ensure initialization was successful + if (_spawnConfig == null) + { + Debug.LogError("[ObstacleSpawner] Cannot start spawning - initialization failed!"); + return; + } + + // Begin the spawning process + BeginSpawningObstacles(); + } + + /// + /// Internal method that handles the actual spawning startup logic. + /// Sets initial state, computes first interval, and spawns the first obstacle. + /// + private void BeginSpawningObstacles() + { + _isSpawning = true; + _spawnTimer = 0f; _elapsedTime = 0f; - // choose initial interval based on difficulty (at time 0) - float initialDifficulty = difficultyCurve.Evaluate(0f); - float initialBase = Mathf.Lerp(maxSpawnInterval, minSpawnInterval, initialDifficulty); - if (intervalJitter > 0f) + // Choose initial interval based on difficulty (at time 0) + float initialDifficulty = _spawnConfig.difficultyCurve.Evaluate(0f); + float initialBase = Mathf.Lerp(_spawnConfig.maxSpawnInterval, _spawnConfig.minSpawnInterval, initialDifficulty); + + if (_spawnConfig.intervalJitter > 0f) { - float jitter = Random.Range(-intervalJitter, intervalJitter); + float jitter = Random.Range(-_spawnConfig.intervalJitter, _spawnConfig.intervalJitter); _currentSpawnInterval = Mathf.Max(0f, initialBase * (1f + jitter)); } else @@ -310,14 +447,14 @@ namespace Minigames.BirdPooper /// public void StopSpawning() { - isSpawning = false; + _isSpawning = false; Debug.Log("[ObstacleSpawner] Stopped spawning"); } /// /// Check if spawner is currently active. /// - public bool IsSpawning => isSpawning; + public bool IsSpawning => _isSpawning; #if UNITY_EDITOR /// diff --git a/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs b/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs new file mode 100644 index 00000000..1e8eeabf --- /dev/null +++ b/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs @@ -0,0 +1,153 @@ +using UnityEngine; +using UnityEngine.UI; +using Core.Lifecycle; +using Pixelplacement.TweenSystem; +using Utils; + +namespace Minigames.BirdPooper +{ + /// + /// Manages "tap to start" flow for Bird Pooper minigame. + /// Shows blinking finger UI, waits for first tap, then starts the game. + /// + public class TapToStartController : ManagedBehaviour, ITouchInputConsumer + { + [Header("UI References")] + [SerializeField] private GameObject fingerContainer; + [SerializeField] private Image fingerImage; + + [Header("Animation Settings")] + [Tooltip("Duration for one complete fade in/out cycle")] + [SerializeField] private float blinkDuration = 1.5f; + [Tooltip("Minimum alpha value during blink")] + [SerializeField] private float minAlpha = 0.3f; + [Tooltip("Maximum alpha value during blink")] + [SerializeField] private float maxAlpha = 1f; + + private bool _isWaitingForTap; + private TweenBase _blinkTween; + + internal override void OnManagedAwake() + { + base.OnManagedAwake(); + + // Validate references + if (fingerContainer == null) + { + Debug.LogError("[TapToStartController] Finger container not assigned!"); + } + + if (fingerImage == null) + { + Debug.LogError("[TapToStartController] Finger image not assigned!"); + } + + // Start hidden + if (fingerContainer != null) + { + fingerContainer.SetActive(false); + } + } + + /// + /// Activates the tap-to-start UI and begins waiting for player input. + /// Called by BirdPooperGameManager when scene is ready. + /// + public void Activate() + { + if (_isWaitingForTap) + { + Debug.LogWarning("[TapToStartController] Already waiting for tap!"); + return; + } + + Debug.Log("[TapToStartController] Activating tap-to-start..."); + + _isWaitingForTap = true; + + // Show finger UI + if (fingerContainer != null) + { + fingerContainer.SetActive(true); + } + + // Start blinking animation using tween utility + if (fingerImage != null) + { + _blinkTween = TweenAnimationUtility.StartBlinkImage(fingerImage, minAlpha, maxAlpha, blinkDuration); + } + + // Register as high-priority input consumer to catch first tap + if (Input.InputManager.Instance != null) + { + Input.InputManager.Instance.SetDefaultConsumer(this); + Debug.Log("[TapToStartController] Registered as input consumer"); + } + else + { + Debug.LogError("[TapToStartController] InputManager instance not found!"); + } + } + + #region ITouchInputConsumer Implementation + + public void OnTap(Vector2 tapPosition) + { + if (!_isWaitingForTap) return; + + Debug.Log("[TapToStartController] First tap received! Starting game..."); + + // Stop waiting for tap + _isWaitingForTap = false; + + // Stop blinking animation + if (_blinkTween != null) + { + _blinkTween.Stop(); + _blinkTween = null; + } + + // Hide finger UI + if (fingerContainer != null) + { + fingerContainer.SetActive(false); + } + + // Unregister from input system + if (Input.InputManager.Instance != null) + { + Input.InputManager.Instance.SetDefaultConsumer(null); + Debug.Log("[TapToStartController] Unregistered from input"); + } + + // Tell game manager to start the game (it will handle the initial flap) + BirdPooperGameManager.Instance.BeginMinigame(); + + } + + public void OnHoldStart(Vector2 position) { } + public void OnHoldMove(Vector2 position) { } + public void OnHoldEnd(Vector2 position) { } + + #endregion + + internal override void OnManagedDestroy() + { + // Stop blinking animation if active + if (_blinkTween != null) + { + _blinkTween.Stop(); + _blinkTween = null; + } + + // Unregister from input if still registered + if (_isWaitingForTap && Input.InputManager.Instance != null) + { + Input.InputManager.Instance.SetDefaultConsumer(null); + } + + base.OnManagedDestroy(); + } + } +} + diff --git a/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs.meta b/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs.meta new file mode 100644 index 00000000..b75a6420 --- /dev/null +++ b/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2a6ee5aca3ca423c82b57e16c0b2cca3 +timeCreated: 1765922092 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/BirdPooper/TargetSpawner.cs b/Assets/Scripts/Minigames/BirdPooper/TargetSpawner.cs index 226a1aa1..e3a372d4 100644 --- a/Assets/Scripts/Minigames/BirdPooper/TargetSpawner.cs +++ b/Assets/Scripts/Minigames/BirdPooper/TargetSpawner.cs @@ -31,9 +31,9 @@ namespace Minigames.BirdPooper [Tooltip("Array of target prefabs to spawn randomly")] [SerializeField] private GameObject[] targetPrefabs; - private IBirdPooperSettings settings; - private float spawnTimer; - private bool isSpawning; + private IBirdPooperSettings _settings; + private float _spawnTimer; + private bool _isSpawning; private float _currentTargetInterval = 1f; [Header("Spawn Timing")] @@ -42,16 +42,17 @@ namespace Minigames.BirdPooper [Tooltip("Maximum interval between target spawns (seconds)")] [SerializeField] private float maxTargetSpawnInterval = 2f; - internal override void OnManagedAwake() + /// + /// Initializes the target spawner by loading settings and validating references. + /// Should be called once before spawning begins. + /// + private void Initialize() { - base.OnManagedAwake(); - // Load settings - settings = GameManager.GetSettingsObject(); - if (settings == null) + _settings = GameManager.GetSettingsObject(); + if (_settings == null) { - Debug.LogError("[TargetSpawner] BirdPooperSettings not found!"); - // continue – we'll use inspector intervals + Debug.LogWarning("[TargetSpawner] BirdPooperSettings not found! Using inspector intervals."); } // Validate interval range @@ -96,15 +97,15 @@ namespace Minigames.BirdPooper private void Update() { - if (!isSpawning) + if (!_isSpawning) return; - spawnTimer += Time.deltaTime; + _spawnTimer += Time.deltaTime; - if (spawnTimer >= _currentTargetInterval) + if (_spawnTimer >= _currentTargetInterval) { SpawnTarget(); - spawnTimer = 0f; + _spawnTimer = 0f; // pick next random interval _currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval); } @@ -167,15 +168,34 @@ namespace Minigames.BirdPooper } /// - /// Start spawning targets at regular intervals. + /// Start spawning targets. + /// Initializes the spawner if not already initialized, then begins spawning logic. /// public void StartSpawning() { - isSpawning = true; - spawnTimer = 0f; - // choose initial interval + // Initialize if not already done + if (_settings == null) + { + Initialize(); + } + + // Begin the spawning process + BeginSpawningTargets(); + } + + /// + /// Internal method that handles the actual spawning startup logic. + /// Sets initial state and computes first interval. + /// + private void BeginSpawningTargets() + { + _isSpawning = true; + _spawnTimer = 0f; + + // Choose initial interval _currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval); - Debug.Log("[TargetSpawner] Started spawning targets"); + + Debug.Log($"[TargetSpawner] Started spawning targets with interval {_currentTargetInterval:F2}s"); } /// @@ -183,14 +203,14 @@ namespace Minigames.BirdPooper /// public void StopSpawning() { - isSpawning = false; + _isSpawning = false; Debug.Log("[TargetSpawner] Stopped spawning targets"); } /// /// Check if spawner is currently spawning. /// - public bool IsSpawning => isSpawning; + public bool IsSpawning => _isSpawning; /// /// Draw gizmos to visualize spawn and despawn points in the editor. diff --git a/Assets/Scripts/Utils/TweenAnimationUtility.cs b/Assets/Scripts/Utils/TweenAnimationUtility.cs index 5859af82..93330480 100644 --- a/Assets/Scripts/Utils/TweenAnimationUtility.cs +++ b/Assets/Scripts/Utils/TweenAnimationUtility.cs @@ -1,4 +1,4 @@ -using System; +using System; using Pixelplacement; using Pixelplacement.TweenSystem; using UnityEngine; @@ -145,6 +145,52 @@ namespace Utils return Tween.CanvasGroupAlpha(canvasGroup, targetAlpha, duration, 0f, Tween.EaseInOut, completeCallback: onComplete); } + /// + /// Fade Image alpha + /// + public static TweenBase FadeImageAlpha(UnityEngine.UI.Image image, float targetAlpha, float duration, Action onComplete = null) + { + return Tween.Value(image.color.a, targetAlpha, (alpha) => + { + if (image != null) + { + Color color = image.color; + color.a = alpha; + image.color = color; + } + }, duration, 0f, Tween.EaseInOut, completeCallback: onComplete); + } + + /// + /// Start blinking animation on Image (ping-pong alpha fade) + /// + /// Image to blink + /// Minimum alpha value + /// Maximum alpha value + /// Duration for one complete cycle (in and out) + /// TweenBase that can be cancelled + public static TweenBase StartBlinkImage(UnityEngine.UI.Image image, float minAlpha = 0.3f, float maxAlpha = 1f, float duration = 1.5f) + { + // Set initial alpha to max + if (image != null) + { + Color color = image.color; + color.a = maxAlpha; + image.color = color; + } + + // Create ping-pong tween (half duration for each direction) + return Tween.Value(maxAlpha, minAlpha, (alpha) => + { + if (image != null) + { + Color color = image.color; + color.a = alpha; + image.color = color; + } + }, duration / 2f, 0f, Tween.EaseInOut, Tween.LoopType.PingPong); + } + /// /// Pop-out with fade - scale to 0 and fade out simultaneously /// diff --git a/Assets/Settings/BirdPooperSettings.asset b/Assets/Settings/BirdPooperSettings.asset index e84cae05..1bc0e422 100644 --- a/Assets/Settings/BirdPooperSettings.asset +++ b/Assets/Settings/BirdPooperSettings.asset @@ -20,7 +20,60 @@ MonoBehaviour: maxRotationAngle: 40 rotationSpeed: 18 obstacleMoveSpeed: 7 - obstacleSpawnInterval: 0.1 + obstacleSpawnConfiguration: + obstaclePools: + - obstacles: + - {fileID: 8855270423038321603, guid: 20ae02a8f50484045aaf3dcee33fb9a2, type: 3} + - {fileID: 2514399078413048981, guid: ee834e7efcf7d8749881f71f8b0da99c, type: 3} + - {fileID: 842802843766402460, guid: cdc806fd167bba3488797031a28657fa, type: 3} + - {fileID: 4239333156730914246, guid: 332d8cce2ed99054c83ecf84fbfa14c8, type: 3} + - {fileID: 6660502783540694524, guid: 5d42fc70e5838544ab654e30aa4b0c48, type: 3} + - {fileID: 2421410811796775077, guid: 371a09b68a5c0654bac9ba58ad3bcbe5, type: 3} + - obstacles: + - {fileID: 1408173265900928789, guid: 871373a85e5da0e4cafdf0e47496e105, type: 3} + - {fileID: 1408173265900928789, guid: d2998934362713545a040d7017a1bd36, type: 3} + - {fileID: 1408173265900928789, guid: 146d99398c0e7964dbed504e256adab7, type: 3} + - {fileID: 1408173265900928789, guid: dc8a19e9a4d30b44596237d915b3b73f, type: 3} + - {fileID: 1408173265900928789, guid: 471f367e14f9cfb4fb2c40d799d4c292, type: 3} + - {fileID: 1408173265900928789, guid: 5f1734c5705cdfd49ae3180d678d28b3, type: 3} + - obstacles: + - {fileID: 1408173265900928789, guid: 6bc84c3ea9854b54f85a8fb69c769790, type: 3} + - {fileID: 1408173265900928789, guid: 166c7e1bfcc4c854fab0af51cdfff746, type: 3} + - {fileID: 1408173265900928789, guid: 65810bfd58ebbaf4482527452258ae50, type: 3} + - {fileID: 1408173265900928789, guid: ae3986a7db087c845b618a9c897705ec, type: 3} + poolUnlockTimes: + - 20 + - 40 + minSpawnInterval: 1 + maxSpawnInterval: 2 + difficultyRampDuration: 60 + difficultyCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 1 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1 + value: 1 + inSlope: 1 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + intervalJitter: 0.05 + recentDecayDuration: 10 + minRecentWeight: 0.1 obstacleSpawnXPosition: 12 obstacleDestroyXPosition: -12 obstacleMinSpawnY: -3 diff --git a/Assets/Settings/FortFightSettings.asset b/Assets/Settings/FortFightSettings.asset index 0b4402c7..0e6686ed 100644 --- a/Assets/Settings/FortFightSettings.asset +++ b/Assets/Settings/FortFightSettings.asset @@ -28,18 +28,24 @@ MonoBehaviour: forceDeviation: 0.3 thinkTimeMin: 0.5 thinkTimeMax: 1 + trashBagDetonationDistanceMin: 0.3 + trashBagDetonationDistanceMax: 0.5 - difficulty: 1 data: angleDeviation: 30 forceDeviation: 0.2 thinkTimeMin: 0.2 thinkTimeMax: 0.8 + trashBagDetonationDistanceMin: 0.2 + trashBagDetonationDistanceMax: 0.4 - difficulty: 2 data: angleDeviation: 10 forceDeviation: 0.1 thinkTimeMin: 0.2 thinkTimeMax: 0.8 + trashBagDetonationDistanceMin: 0.1 + trashBagDetonationDistanceMax: 0.5 defaultAIDifficulty: 1 aiAllowedProjectiles: 000000000100000003000000 weakPointExplosionRadius: 6