From 7a598c302c5d65cb1fb4a3bcc02708dcba85f8b3 Mon Sep 17 00:00:00 2001 From: Michal Pikulski Date: Wed, 17 Dec 2025 12:39:44 +0100 Subject: [PATCH] Stash paralax work --- .../MiniGames/ValentineNoteDelivery.unity | 95 ++- .../Airplane/Core/AirplaneCameraManager.cs | 29 + .../Airplane/Core/AirplaneGameManager.cs | 2 + .../Airplane/Core/AirplaneSpawnManager.cs | 702 ++++++------------ .../Minigames/Airplane/Core/Spawning.meta | 3 + .../Core/Spawning/BaseDistanceSpawner.cs | 449 +++++++++++ .../Core/Spawning/BaseDistanceSpawner.cs.meta | 3 + .../Core/Spawning/GroundDistanceSpawner.cs | 38 + .../Spawning/GroundDistanceSpawner.cs.meta | 3 + .../Core/Spawning/ObstacleDistanceSpawner.cs | 199 +++++ .../Spawning/ObstacleDistanceSpawner.cs.meta | 3 + .../Spawning/ParallaxBackgroundSpawner.cs | 146 ++++ .../ParallaxBackgroundSpawner.cs.meta | 3 + .../Minigames/Airplane/Data/ParallaxLayer.cs | 24 + .../Airplane/Data/ParallaxLayer.cs.meta | 3 + .../Airplane/Data/SpawnPoolConfig.cs | 45 ++ .../Airplane/Data/SpawnPoolConfig.cs.meta | 3 + .../Minigames/Airplane/Data/SpawnPoolMode.cs | 21 + .../Airplane/Data/SpawnPoolMode.cs.meta | 3 + .../Airplane/Interactive/ParallaxElement.cs | 150 ++++ .../Interactive/ParallaxElement.cs.meta | 3 + 21 files changed, 1404 insertions(+), 523 deletions(-) create mode 100644 Assets/Scripts/Minigames/Airplane/Core/Spawning.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs.meta create mode 100644 Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs create mode 100644 Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs.meta diff --git a/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity b/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity index 3fa7828d..e20e43f2 100644 --- a/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity +++ b/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity @@ -2457,6 +2457,37 @@ MonoBehaviour: trajectoryPreview: {fileID: 1309397783} showDebugLogs: 0 airplanePrefab: {fileID: 2043346932243838886, guid: 582ed0c37f4ec6c4e930ddabea174eca, type: 3} +--- !u!1 &1405572708 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1405572709} + m_Layer: 0 + m_Name: SpawnContainer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1405572709 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1405572708} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1784207402} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1001 &1414189130 PrefabInstance: m_ObjectHideFlags: 0 @@ -3702,6 +3733,7 @@ GameObject: m_Component: - component: {fileID: 1784207402} - component: {fileID: 1784207403} + - component: {fileID: 1784207404} m_Layer: 0 m_Name: SpawnManager m_TagString: Untagged @@ -3724,6 +3756,7 @@ Transform: m_Children: - {fileID: 1219431443} - {fileID: 1287102785} + - {fileID: 1405572709} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1784207403 @@ -3738,6 +3771,9 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 70f14ee4b04b46b793ec2652fd2ca7b9, type: 3} m_Name: m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.AirplaneSpawnManager + obstacleSpawner: {fileID: 0} + groundSpawner: {fileID: 0} + parallaxSpawner: {fileID: 0} targetPrefabs: - targetKey: bob_target prefab: {fileID: 3207629437433571205, guid: 9f4bb48933059e543b60ac782d2140d8, type: 3} @@ -3751,42 +3787,37 @@ MonoBehaviour: specifiedY: 0 randomYMin: -5 randomYMax: 5 - positiveObjectPrefabs: - - prefab: {fileID: 497267990420767357, guid: dc32284941c693c4f9e422f4197ca61e, type: 3} - spawnPositionMode: 2 - specifiedY: 0 - randomYMin: 3 - randomYMax: 40 - - prefab: {fileID: 1917678391913987792, guid: 989121c9099e41e469824ddeaf0e34a5, type: 3} - spawnPositionMode: 0 - specifiedY: 0 - randomYMin: 5 - randomYMax: 20 - - prefab: {fileID: 7032677151789119314, guid: 7dc33e43acead834ba6a231b67cfd2d9, type: 3} - spawnPositionMode: 2 - specifiedY: 0 - randomYMin: 3 - randomYMax: 40 - negativeObjectPrefabs: - - prefab: {fileID: 1186710456879913970, guid: 006b956651124704dbae5bd4faab3152, type: 3} - spawnPositionMode: 2 - specifiedY: 0 - randomYMin: 3 - randomYMax: 40 - - prefab: {fileID: 2434350760695575337, guid: f3188909ff4e845499a5cbfd0ae93101, type: 3} - spawnPositionMode: 2 - specifiedY: 0 - randomYMin: 3 - randomYMax: 40 - groundTilePrefabs: - - {fileID: 5175967588203935335, guid: a9b4569fcc08080479d99b9c3bcee089, type: 3} targetDisplayUI: {fileID: 1520040329} launchController: {fileID: 1309397785} dynamicSpawnThresholdMarker: {fileID: 1653173574} - spawnedObjectsParent: {fileID: 1219431443} - groundTilesParent: {fileID: 1287102785} - groundSpawnY: -5 + targetParent: {fileID: 1405572709} showDebugLogs: 0 +--- !u!114 &1784207404 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1784207401} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0a550c256a3d4d91a17adf6d0338f022, type: 3} + m_Name: + m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.Spawning.ObstacleDistanceSpawner + poolMode: 0 + pools: [] + globalMinDistance: 5 + globalMaxDistance: 15 + spawnPoint: {fileID: 0} + spawnContainer: {fileID: 0} + recencyPenaltyDuration: 10 + showDebugLogs: 0 + positiveNegativeRatio: 0.5 + positivePoolIndices: + negativePoolIndices: + groundLayer: 0 + maxGroundRaycastDistance: 50 + defaultObjectYOffset: 0 --- !u!1 &1810521056 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs index 97138e3d..791b2efb 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneCameraManager.cs @@ -71,6 +71,35 @@ namespace Minigames.Airplane.Core #endregion + #region Camera Tracking API + + /// + /// Get the transform of the currently active camera. + /// Used by parallax elements to track camera movement. + /// + public Transform GetActiveCameraTransform() + { + var activeCamera = GetCamera(CurrentState); + return activeCamera != null ? activeCamera.transform : null; + } + + /// + /// Override to fire additional state changed events. + /// + public override void SwitchToState(AirplaneCameraState newState) + { + AirplaneCameraState oldState = CurrentState; + base.SwitchToState(newState); + + // Base class already fires OnStateChanged event, we just add logging + if (showDebugLogs) + { + Logging.Debug($"[AirplaneCameraManager] Camera state changed: {oldState} -> {newState}"); + } + } + + #endregion + #region Flight Camera Follow /// diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs index c05c04ca..e1a4f12a 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneGameManager.cs @@ -459,6 +459,8 @@ namespace Minigames.Airplane.Core if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Introducing {_currentPerson.PersonName}..."); + if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Checking camera state... Current: {(cameraManager != null ? cameraManager.CurrentState.ToString() : "NULL")}"); + // 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) diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs index 31ad5395..11208938 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using AppleHills.Core.Settings; using Core; using Core.Lifecycle; using Core.Settings; +using Minigames.Airplane.Core.Spawning; using Minigames.Airplane.Data; using Minigames.Airplane.UI; using UnityEngine; @@ -12,8 +12,9 @@ using Random = UnityEngine.Random; namespace Minigames.Airplane.Core { /// - /// Manages dynamic spawning of targets, positive/negative objects, and ground tiles - /// as the airplane moves through the level. + /// Orchestrates spawning for the airplane minigame. + /// Manages target spawning and coordinates specialized spawners (obstacles, ground, parallax). + /// Tracks game time and propagates to spawners for pool unlocking. /// public class AirplaneSpawnManager : ManagedBehaviour { @@ -32,7 +33,7 @@ namespace Minigames.Airplane.Core public SpawnPositionMode spawnPositionMode = SpawnPositionMode.SnapToGround; [Tooltip("Y position to use (SpecifiedY mode)")] - public float specifiedY = 0f; + public float specifiedY; [Tooltip("Min Y for random range (RandomRange mode)")] public float randomYMin = -5f; @@ -45,19 +46,20 @@ namespace Minigames.Airplane.Core #region Inspector References - [Header("Prefab References")] + [Header("Spawner References")] + [Tooltip("Obstacle spawner (positive/negative objects)")] + [SerializeField] private ObstacleDistanceSpawner obstacleSpawner; + + [Tooltip("Ground tile spawner")] + [SerializeField] private GroundDistanceSpawner groundSpawner; + + [Tooltip("Parallax background spawner (optional)")] + [SerializeField] private ParallaxBackgroundSpawner parallaxSpawner; + + [Header("Target Configuration")] [Tooltip("Dictionary of target prefabs (key = target name)")] [SerializeField] private TargetPrefabEntry[] targetPrefabs; - [Tooltip("Array of positive object prefabs with spawn configuration")] - [SerializeField] private PrefabSpawnEntry[] positiveObjectPrefabs; - - [Tooltip("Array of negative object prefabs with spawn configuration")] - [SerializeField] private PrefabSpawnEntry[] negativeObjectPrefabs; - - [Tooltip("Array of ground tile prefabs")] - [SerializeField] private GameObject[] groundTilePrefabs; - [Header("UI")] [Tooltip("Target display UI component")] [SerializeField] private TargetDisplayUI targetDisplayUI; @@ -67,19 +69,12 @@ namespace Minigames.Airplane.Core [SerializeField] private AirplaneLaunchController launchController; [Header("Spawn Threshold")] - [Tooltip("Transform marker in scene where dynamic spawning begins (uses X position). If null, uses fallback from settings.")] + [Tooltip("Transform marker in scene where dynamic spawning begins (uses X position)")] [SerializeField] private Transform dynamicSpawnThresholdMarker; [Header("Spawn Parents")] - [Tooltip("Parent transform for spawned objects (optional, for organization)")] - [SerializeField] private Transform spawnedObjectsParent; - - [Tooltip("Parent transform for ground tiles (optional)")] - [SerializeField] private Transform groundTilesParent; - - [Header("Ground Settings")] - [Tooltip("Y position at which to spawn ground tiles")] - [SerializeField] private float groundSpawnY = -18f; + [Tooltip("Parent transform for spawned target (optional)")] + [SerializeField] private Transform targetParent; [Header("Debug")] [SerializeField] private bool showDebugLogs; @@ -101,15 +96,10 @@ namespace Minigames.Airplane.Core // Plane tracking private Transform _planeTransform; private bool _isSpawningActive; + private float _gameTime; // Spawning positions (distance-based) - private float _lastSpawnedX; // Tracks the furthest forward X position that has been pre-spawned - private float _nextObjectSpawnX; - private float _nextGroundSpawnX; - - // Spawn statistics (for weighted ratio adjustment) - private int _positiveSpawnCount; - private int _negativeSpawnCount; + private float _lastSpawnedX; // Adaptive spawn distance (persistent across retries) private float _furthestReachedX; @@ -135,42 +125,23 @@ namespace Minigames.Airplane.Core // Validate references ValidateReferences(); + + // Initialize all spawners + InitializeSpawners(); } private void Update() { if (!_isSpawningActive || _planeTransform == null) return; - float planeX = _planeTransform.position.x; + float deltaTime = Time.deltaTime; + _gameTime += deltaTime; - // Track furthest X position reached - if (planeX > _furthestReachedX) - { - _furthestReachedX = planeX; - } + // Update spawner game times for pool unlocking + UpdateSpawnerTimes(deltaTime); - // 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) - { - // Continue spawning objects when plane approaches the last spawned position - float spawnTriggerX = _lastSpawnedX + _settings.SpawnDistanceAhead; - if (planeX >= spawnTriggerX && planeX >= _nextObjectSpawnX) - { - SpawnRandomObject(); - ScheduleNextObjectSpawn(planeX); - } - - // Continue spawning ground tiles ahead of plane - float groundSpawnTargetX = planeX + GetGroundSpawnAheadDistance(); - while (_nextGroundSpawnX < groundSpawnTargetX) - { - SpawnGroundTile(); - _nextGroundSpawnX += _settings.GroundSpawnInterval; - } - } + // Update dynamic spawning + UpdateDynamicSpawning(); } #endregion @@ -180,7 +151,6 @@ namespace Minigames.Airplane.Core /// /// Initialize the spawn system for a new game. /// Determines target spawn position and sets up UI, but doesn't spawn target yet. - /// Target will spawn when plane gets within spawn distance. /// /// Key of the target to spawn /// True if this is a retry attempt (keeps existing spawned objects and target position) @@ -189,13 +159,12 @@ namespace Minigames.Airplane.Core _currentTargetKey = targetKey; _isSpawningActive = false; _isRetryAttempt = isRetry; + _gameTime = 0f; // Only reset target and spawn state if NOT a retry if (!isRetry) { _hasSpawnedTarget = false; - _positiveSpawnCount = 0; - _negativeSpawnCount = 0; _furthestReachedX = 0f; // Determine NEW target spawn distance @@ -204,7 +173,7 @@ namespace Minigames.Airplane.Core if (showDebugLogs) { - Logging.Debug($"[SpawnManager] Initialized NEW turn for target '{targetKey}' at distance {_targetDistance:F2}"); + Logging.Debug($"[AirplaneSpawnManager] Initialized NEW turn for target '{targetKey}' at distance {_targetDistance:F2}"); } } else @@ -212,14 +181,14 @@ namespace Minigames.Airplane.Core // Retry: Keep existing target position and spawned objects if (showDebugLogs) { - Logging.Debug($"[SpawnManager] Initialized RETRY for target '{targetKey}' at distance {_targetDistance:F2}, furthest reached: {_furthestReachedX:F2}"); + Logging.Debug($"[AirplaneSpawnManager] Initialized RETRY for target '{targetKey}' at distance {_targetDistance:F2}, furthest reached: {_furthestReachedX:F2}"); } } // Find target entry and extract icon WITHOUT spawning if (!_targetPrefabDict.TryGetValue(_currentTargetKey, out _currentTargetEntry)) { - Logging.Error($"[SpawnManager] Target prefab not found for key '{_currentTargetKey}'!"); + Logging.Error($"[AirplaneSpawnManager] Target prefab not found for key '{_currentTargetKey}'!"); return; } @@ -241,7 +210,7 @@ namespace Minigames.Airplane.Core { if (_targetPrefabToSpawn == null) { - Logging.Error("[SpawnManager] Cannot pre-spawn - target prefab not initialized! Call InitializeForGame first."); + Logging.Error("[AirplaneSpawnManager] Cannot pre-spawn - target prefab not initialized! Call InitializeForGame first."); return; } @@ -249,14 +218,14 @@ namespace Minigames.Airplane.Core { if (showDebugLogs) { - Logging.Debug("[SpawnManager] Target already spawned, skipping pre-spawn (retry scenario)"); + Logging.Debug("[AirplaneSpawnManager] Target already spawned, skipping pre-spawn (retry scenario)"); } return; } if (dynamicSpawnThresholdMarker == null) { - Logging.Error("[SpawnManager] Cannot pre-spawn - dynamicSpawnThresholdMarker not assigned!"); + Logging.Error("[AirplaneSpawnManager] Cannot pre-spawn - dynamicSpawnThresholdMarker not assigned!"); return; } @@ -266,57 +235,37 @@ namespace Minigames.Airplane.Core if (showDebugLogs) { - Logging.Debug($"[SpawnManager] Pre-spawning level from X={preSpawnStartX:F2} (threshold) to X={preSpawnEndX:F2} (target={_targetDistance:F2} + buffer={_settings.PreSpawnBeyondTargetDistance:F2})"); + Logging.Debug($"[AirplaneSpawnManager] Pre-spawning level from X={preSpawnStartX:F2} to X={preSpawnEndX:F2}"); } - // 1. Spawn ground tiles FIRST across entire range (so target can raycast) - float currentGroundX = preSpawnStartX; - while (currentGroundX <= preSpawnEndX) + // 1. Spawn ground tiles FIRST (so objects can raycast to them) + if (groundSpawner != null) { - SpawnGroundTileAt(currentGroundX); - currentGroundX += _settings.GroundSpawnInterval; + groundSpawner.PreSpawn(preSpawnStartX, preSpawnEndX); } - // Set next ground spawn position beyond pre-spawn range - _nextGroundSpawnX = preSpawnEndX + _settings.GroundSpawnInterval; - - if (showDebugLogs) + // 2. Spawn parallax background (if assigned) + if (parallaxSpawner != null) { - Logging.Debug($"[SpawnManager] Ground tiles spawned, now spawning objects"); + parallaxSpawner.PreSpawn(preSpawnStartX, preSpawnEndX); } - // 2. Spawn objects across entire range (skipping near target) - float currentObjectX = preSpawnStartX + Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance); - - while (currentObjectX <= preSpawnEndX) + // 3. Spawn obstacles (after ground exists) + if (obstacleSpawner != null) { - // Spawn object at this position - SpawnRandomObjectAt(currentObjectX); - - // Move to next spawn position (forward) - float spawnDistance = Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance); - currentObjectX += spawnDistance; + obstacleSpawner.PreSpawn(preSpawnStartX, preSpawnEndX); } - if (showDebugLogs) - { - Logging.Debug($"[SpawnManager] Objects spawned, now spawning target with raycast"); - } - - // 3. Spawn the target LAST (after ground exists for proper raycasting) + // 4. Spawn the target LAST SpawnTarget(); _hasSpawnedTarget = true; - // 4. Store the furthest forward pre-spawn position as _lastSpawnedX + // 5. Store the furthest forward pre-spawn position _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"); + Logging.Debug($"[AirplaneSpawnManager] Pre-spawn complete! Last spawned X={_lastSpawnedX:F2}"); } } @@ -328,9 +277,25 @@ namespace Minigames.Airplane.Core { _planeTransform = planeTransform; _isSpawningActive = true; + _gameTime = 0f; - // Initialize ground spawning position ahead of plane - _nextGroundSpawnX = _planeTransform.position.x + GetGroundSpawnAheadDistance(); + // Start tracking in spawners + float startX = _lastSpawnedX; + + if (obstacleSpawner != null) + { + obstacleSpawner.StartTracking(planeTransform, startX); + } + + if (groundSpawner != null) + { + groundSpawner.StartTracking(planeTransform, startX); + } + + if (parallaxSpawner != null) + { + parallaxSpawner.StartTracking(planeTransform, startX); + } // Start UI tracking with calculated target position if (targetDisplayUI != null) @@ -339,13 +304,13 @@ namespace Minigames.Airplane.Core if (showDebugLogs) { - Logging.Debug("[SpawnManager] UI tracking started, distance updates will show"); + Logging.Debug("[AirplaneSpawnManager] UI tracking started"); } } if (showDebugLogs) { - Logging.Debug("[SpawnManager] Started tracking airplane"); + Logging.Debug("[AirplaneSpawnManager] Started tracking airplane"); } } @@ -357,6 +322,22 @@ namespace Minigames.Airplane.Core _isSpawningActive = false; _planeTransform = null; + // Stop spawners + if (obstacleSpawner != null) + { + obstacleSpawner.StopTracking(); + } + + if (groundSpawner != null) + { + groundSpawner.StopTracking(); + } + + if (parallaxSpawner != null) + { + parallaxSpawner.StopTracking(); + } + // Stop UI tracking if (targetDisplayUI != null) { @@ -365,31 +346,28 @@ namespace Minigames.Airplane.Core if (showDebugLogs) { - Logging.Debug("[SpawnManager] Stopped tracking"); + Logging.Debug("[AirplaneSpawnManager] Stopped tracking"); } } /// /// Show the target display UI. - /// Call when entering aiming state. /// public void ShowTargetUI() { if (targetDisplayUI != null) { - // Update distance and show UI (refreshes distance from launch point if plane not launched yet) targetDisplayUI.UpdateAndShow(); if (showDebugLogs) { - Logging.Debug("[SpawnManager] Target UI shown with updated distance"); + Logging.Debug("[AirplaneSpawnManager] Target UI shown"); } } } /// /// Hide the target display UI. - /// Call when target is successfully hit. /// public void HideTargetUI() { @@ -399,55 +377,52 @@ namespace Minigames.Airplane.Core if (showDebugLogs) { - Logging.Debug("[SpawnManager] Target UI hidden"); + Logging.Debug("[AirplaneSpawnManager] Target UI hidden"); } } } /// - /// Clean up all spawned objects (call on successful shot or game restart). - /// Destroys all spawned content including target, objects, and ground tiles. + /// Clean up all spawned objects. /// public void CleanupSpawnedObjects() { - if (spawnedObjectsParent != null) + // Cleanup spawners + if (obstacleSpawner != null) { - foreach (Transform child in spawnedObjectsParent) - { - Destroy(child.gameObject); - } + obstacleSpawner.Cleanup(); } - if (groundTilesParent != null) + if (groundSpawner != null) { - foreach (Transform child in groundTilesParent) - { - Destroy(child.gameObject); - } + groundSpawner.Cleanup(); } + if (parallaxSpawner != null) + { + parallaxSpawner.Cleanup(); + } + + // Cleanup target if (_spawnedTarget != null) { Destroy(_spawnedTarget); _spawnedTarget = null; } - // Reset all spawn state + // Reset state _hasSpawnedTarget = false; _furthestReachedX = 0f; - _positiveSpawnCount = 0; - _negativeSpawnCount = 0; + _gameTime = 0f; if (showDebugLogs) { - Logging.Debug("[SpawnManager] Full cleanup completed (success or game restart)"); + Logging.Debug("[AirplaneSpawnManager] Full cleanup completed"); } } /// /// 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) { @@ -457,7 +432,7 @@ namespace Minigames.Airplane.Core if (showDebugLogs) { - Logging.Debug("[SpawnManager] Level cleanup: SUCCESS - destroyed all objects"); + Logging.Debug("[AirplaneSpawnManager] Level cleanup: SUCCESS"); } } else @@ -466,23 +441,19 @@ namespace Minigames.Airplane.Core if (showDebugLogs) { - Logging.Debug("[SpawnManager] Level cleanup: FAILURE - kept objects for retry"); + Logging.Debug("[AirplaneSpawnManager] 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. + /// Reset tracking state for retry attempt. /// public void ResetForRetry() { - // Don't destroy anything - keep all spawned objects and target - // Just reset the tracking state so spawning can continue if plane goes further - if (showDebugLogs) { - Logging.Debug($"[SpawnManager] Reset for retry (keeping spawned objects, furthest reached: {_furthestReachedX:F2})"); + Logging.Debug($"[AirplaneSpawnManager] Reset for retry (furthest reached: {_furthestReachedX:F2})"); } } @@ -496,7 +467,6 @@ namespace Minigames.Airplane.Core /// /// Get the spawned target's transform for camera tracking. - /// Returns null if target hasn't been spawned yet. /// public Transform GetSpawnedTargetTransform() { @@ -505,6 +475,87 @@ namespace Minigames.Airplane.Core #endregion + #region Spawner Management + + private void InitializeSpawners() + { + if (obstacleSpawner != null) + { + obstacleSpawner.Initialize(); + } + + if (groundSpawner != null) + { + groundSpawner.Initialize(); + } + + if (parallaxSpawner != null) + { + parallaxSpawner.Initialize(); + } + + if (showDebugLogs) + { + Logging.Debug("[AirplaneSpawnManager] All spawners initialized"); + } + } + + private void UpdateSpawnerTimes(float deltaTime) + { + if (obstacleSpawner != null) + { + obstacleSpawner.UpdateGameTime(deltaTime); + } + + if (groundSpawner != null) + { + groundSpawner.UpdateGameTime(deltaTime); + } + + if (parallaxSpawner != null) + { + parallaxSpawner.UpdateGameTime(deltaTime); + } + } + + private void UpdateDynamicSpawning() + { + if (_planeTransform == null) return; + + float planeX = _planeTransform.position.x; + + // Track furthest X position reached + if (planeX > _furthestReachedX) + { + _furthestReachedX = planeX; + } + + // Check if plane should trigger new spawning + bool shouldSpawnNewContent = !_isRetryAttempt || planeX > (_furthestReachedX - _settings.SpawnDistanceAhead); + + if (shouldSpawnNewContent) + { + float spawnAheadDistance = _settings.SpawnDistanceAhead; + + if (obstacleSpawner != null) + { + obstacleSpawner.UpdateSpawning(spawnAheadDistance); + } + + if (groundSpawner != null) + { + groundSpawner.UpdateSpawning(spawnAheadDistance * 2f); // Ground spawns further ahead + } + + if (parallaxSpawner != null) + { + parallaxSpawner.UpdateSpawning(spawnAheadDistance * 1.5f); + } + } + } + + #endregion + #region Initialization /// @@ -556,37 +607,37 @@ namespace Minigames.Airplane.Core { if (_settings == null) { - Logging.Error("[SpawnManager] Could not load IAirplaneSettings!"); + Logging.Error("[AirplaneSpawnManager] Could not load IAirplaneSettings!"); } - if (positiveObjectPrefabs == null || positiveObjectPrefabs.Length == 0) + if (obstacleSpawner == null) { - Logging.Warning("[SpawnManager] No positive object prefabs assigned!"); + Logging.Warning("[AirplaneSpawnManager] Obstacle spawner not assigned!"); } - if (negativeObjectPrefabs == null || negativeObjectPrefabs.Length == 0) + if (groundSpawner == null) { - Logging.Warning("[SpawnManager] No negative object prefabs assigned!"); + Logging.Warning("[AirplaneSpawnManager] Ground spawner not assigned!"); } - if (groundTilePrefabs == null || groundTilePrefabs.Length == 0) + if (parallaxSpawner == null) { - Logging.Warning("[SpawnManager] No ground tile prefabs assigned!"); + Logging.Warning("[AirplaneSpawnManager] Parallax spawner not assigned (optional)"); } if (targetDisplayUI == null) { - Logging.Warning("[SpawnManager] Target display UI not assigned!"); + Logging.Warning("[AirplaneSpawnManager] Target display UI not assigned!"); } if (launchController == null) { - Logging.Warning("[SpawnManager] Launch controller not assigned! Distance calculation will use world origin."); + Logging.Warning("[AirplaneSpawnManager] Launch controller not assigned!"); } if (dynamicSpawnThresholdMarker == null) { - Logging.Warning("[SpawnManager] Dynamic spawn threshold marker not assigned! Pre-spawn will fail."); + Logging.Warning("[AirplaneSpawnManager] Dynamic spawn threshold marker not assigned!"); } } @@ -615,9 +666,9 @@ namespace Minigames.Airplane.Core // Spawn target at determined position _spawnedTarget = Instantiate(_currentTargetEntry.prefab, _targetSpawnPosition, Quaternion.identity); - if (spawnedObjectsParent != null) + if (targetParent != null) { - _spawnedTarget.transform.SetParent(spawnedObjectsParent); + _spawnedTarget.transform.SetParent(targetParent); } // Position target using configured spawn mode (will refine Y if needed) @@ -747,289 +798,12 @@ namespace Minigames.Airplane.Core #endregion - #region Dynamic Spawning - - /// - /// Get the distance ahead to spawn ground (2x object spawn distance). - /// - private float GetGroundSpawnAheadDistance() - { - return _settings.SpawnDistanceAhead * 2f; - } - - /// - /// Schedule the next object spawn based on random distance from current position. - /// - /// Current X position (usually plane's X or last spawn X) - private void ScheduleNextObjectSpawn(float currentX) - { - float spawnDistance = Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance); - _nextObjectSpawnX = currentX + spawnDistance; - - if (showDebugLogs) - { - Logging.Debug($"[SpawnManager] Next object scheduled at X={_nextObjectSpawnX:F2} (distance: {spawnDistance:F2} from {currentX:F2})"); - } - } - - /// - /// Spawn a random positive or negative object. - /// Uses weighted randomness to maintain target ratio. - /// Avoids spawning near target position to prevent obscuring it. - /// Objects spawn at look-ahead distance to ensure they're off-screen. - /// - private void SpawnRandomObject() - { - if (_planeTransform == null) return; - - // Spawn at look-ahead distance from plane's current position - // This ensures objects always spawn off-screen - float spawnX = _planeTransform.position.x + _settings.SpawnDistanceAhead; - - // Check if spawn position is too close to target (avoid obscuring it) - float distanceToTarget = Mathf.Abs(spawnX - _targetSpawnPosition.x); - float targetClearanceZone = 10f; // Don't spawn within 10 units of target - - if (distanceToTarget < targetClearanceZone) - { - // Too close to target, skip this spawn and schedule the next one - if (showDebugLogs) - { - Logging.Debug($"[SpawnManager] Skipped object spawn at X={spawnX:F2} (too close to target at X={_targetSpawnPosition.x:F2})"); - } - // Schedule next spawn further ahead - ScheduleNextObjectSpawn(spawnX); - 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[UnityEngine.Random.Range(0, positiveObjectPrefabs.Length)]; - _positiveSpawnCount++; - } - } - else - { - if (negativeObjectPrefabs != null && negativeObjectPrefabs.Length > 0) - { - entryToSpawn = negativeObjectPrefabs[UnityEngine.Random.Range(0, negativeObjectPrefabs.Length)]; - _negativeSpawnCount++; - } - } - - if (entryToSpawn == null || entryToSpawn.prefab == null) return; - - - // Spawn object at temporary position - Vector3 tempPosition = new Vector3(spawnX, 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, spawnX, - 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] Spawned {(spawnPositive ? "positive" : "negative")} object at {spawnedObject.transform.position}"); - } - } - - /// - /// Determine if next spawn should be positive based on weighted ratio. - /// Adjusts to maintain target positive/negative ratio. - /// - private bool ShouldSpawnPositive() - { - int totalSpawned = _positiveSpawnCount + _negativeSpawnCount; - - // First few spawns - use pure random based on ratio - if (totalSpawned < 5) - { - return UnityEngine.Random.value <= _settings.PositiveNegativeRatio; - } - - // Calculate current ratio - float currentRatio = totalSpawned > 0 ? (float)_positiveSpawnCount / totalSpawned : 0.5f; - float targetRatio = _settings.PositiveNegativeRatio; - - // If we're below target ratio, heavily favor positive - // If we're above target ratio, heavily favor negative - float adjustedProbability; - if (currentRatio < targetRatio) - { - // Need more positive - increase probability - adjustedProbability = Mathf.Lerp(targetRatio, 1f, (targetRatio - currentRatio) * 2f); - } - else - { - // Need more negative - decrease probability - adjustedProbability = Mathf.Lerp(0f, targetRatio, 1f - (currentRatio - targetRatio) * 2f); - } - - return UnityEngine.Random.value <= adjustedProbability; - } - - /// - /// Spawn a ground tile at the next ground spawn position. - /// - private void SpawnGroundTile() - { - 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(_nextGroundSpawnX, groundSpawnY, 0f); - - // Spawn tile - GameObject spawnedTile = Instantiate(tilePrefab, spawnPosition, Quaternion.identity); - - if (groundTilesParent != null) - { - spawnedTile.transform.SetParent(groundTilesParent); - } - - if (showDebugLogs) - { - Logging.Debug($"[SpawnManager] Spawned ground tile at ({_nextGroundSpawnX:F2}, {groundSpawnY:F2})"); - } - } - - /// - /// 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 + #region Object Positioning Helpers /// /// Position an object based on the specified spawn mode. + /// Used for target positioning. /// - /// Object to position - /// X position for the object - /// Spawn position mode to use - /// Y value for SpecifiedY mode - /// Min Y for RandomRange mode - /// Max Y for RandomRange mode private void PositionObject(GameObject obj, float xPosition, SpawnPositionMode mode, float specifiedY, float randomYMin, float randomYMax) { @@ -1045,22 +819,14 @@ namespace Minigames.Airplane.Core case SpawnPositionMode.SpecifiedY: targetY = specifiedY; - if (showDebugLogs) - { - Logging.Debug($"[SpawnManager] Positioned object at specified Y={targetY:F2}"); - } break; case SpawnPositionMode.RandomRange: targetY = Random.Range(randomYMin, randomYMax); - if (showDebugLogs) - { - Logging.Debug($"[SpawnManager] Positioned object at random Y={targetY:F2} (range: {randomYMin:F2} to {randomYMax:F2})"); - } break; default: - Logging.Error($"[SpawnManager] Unknown spawn position mode: {mode}"); + Logging.Error($"[AirplaneSpawnManager] Unknown spawn position mode: {mode}"); targetY = 0f; break; } @@ -1073,83 +839,37 @@ namespace Minigames.Airplane.Core /// /// Snap an object to the ground using raycast. - /// Positions object so its bottom bounds touch the ground. /// - /// Object to snap to ground - /// X position to raycast from - /// The Y position where object was snapped private float SnapToGround(GameObject obj, float xPosition) { - // Start raycast from high Y position Vector2 rayOrigin = new Vector2(xPosition, 0.0f); - - // Raycast downward to find ground (convert layer to layer mask) int layerMask = 1 << _settings.GroundLayer; - RaycastHit2D hit = Physics2D.Raycast( - rayOrigin, - Vector2.down, - _settings.MaxGroundRaycastDistance, - layerMask - ); - - float targetY; + RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, _settings.MaxGroundRaycastDistance, layerMask); if (hit.collider != null) { - // Found ground - calculate Y position float groundY = hit.point.y; - - // Get object bounds Bounds bounds = GetObjectBounds(obj); - float boundsBottomOffset = bounds.extents.y; // Half height - - // Position object so bottom touches ground - targetY = groundY + boundsBottomOffset; - - if (showDebugLogs) - { - Logging.Debug($"[SpawnManager] Snapped object to ground at Y={targetY:F2} (ground at {groundY:F2})"); - } - } - else - { - // No ground found - use default offset - targetY = _settings.DefaultObjectYOffset; - - Logging.Warning($"[SpawnManager] No ground found at X={xPosition}, using default Y={targetY}"); + return groundY + bounds.extents.y; } - return targetY; + return _settings.DefaultObjectYOffset; } /// - /// Get the bounds of an object from its Renderer or Collider. + /// Get the bounds of an object. /// private Bounds GetObjectBounds(GameObject obj) { - // Try Renderer first Renderer objRenderer = obj.GetComponentInChildren(); - if (objRenderer != null) - { - return objRenderer.bounds; - } + if (objRenderer != null) return objRenderer.bounds; - // Try Collider2D Collider2D objCollider2D = obj.GetComponentInChildren(); - if (objCollider2D != null) - { - return objCollider2D.bounds; - } + if (objCollider2D != null) return objCollider2D.bounds; - // Try Collider3D Collider objCollider3D = obj.GetComponentInChildren(); - if (objCollider3D != null) - { - return objCollider3D.bounds; - } + if (objCollider3D != null) return objCollider3D.bounds; - // Fallback - create minimal bounds - Logging.Warning($"[SpawnManager] No Renderer or Collider found on {obj.name}, using default bounds"); return new Bounds(obj.transform.position, Vector3.one); } diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning.meta b/Assets/Scripts/Minigames/Airplane/Core/Spawning.meta new file mode 100644 index 00000000..fdfcac7a --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5dc5cd57fce342659690bcf1c3411048 +timeCreated: 1765965907 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs b/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs new file mode 100644 index 00000000..6329e71d --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs @@ -0,0 +1,449 @@ +using System.Collections.Generic; +using Core; +using Core.Lifecycle; +using Minigames.Airplane.Data; +using UnityEngine; + +namespace Minigames.Airplane.Core.Spawning +{ + /// + /// Base class for distance-based spawners in the airplane minigame. + /// Handles pool management, time-based unlocking, distance tracking, and recency diversity. + /// Derived classes implement specific spawn logic via SpawnFromPool. + /// + public abstract class BaseDistanceSpawner : ManagedBehaviour + { + #region Inspector Fields + + [Header("Pool Configuration")] + [Tooltip("How pools are combined: Together = shared spawn stream, Exclusive = independent spawning")] + [SerializeField] protected SpawnPoolMode poolMode = SpawnPoolMode.Together; + + [Tooltip("Spawn pools ordered by difficulty/progression")] + [SerializeField] protected SpawnPoolConfig[] pools; + + [Header("Global Spawn Parameters")] + [Tooltip("Minimum distance between spawns (used when poolMode = Together or as fallback)")] + [SerializeField] protected float globalMinDistance = 5f; + + [Tooltip("Maximum distance between spawns (used when poolMode = Together or as fallback)")] + [SerializeField] protected float globalMaxDistance = 15f; + + [Header("Spawn References")] + [Tooltip("Transform marking spawn position (typically off-screen right)")] + [SerializeField] protected Transform spawnPoint; + + [Tooltip("Optional parent for spawned objects (organization)")] + [SerializeField] protected Transform spawnContainer; + + [Header("Recency Tracking")] + [Tooltip("Time penalty (seconds) applied to recently-used prefabs for diversity")] + [SerializeField] protected float recencyPenaltyDuration = 10f; + + [Header("Debug")] + [SerializeField] protected bool showDebugLogs; + + #endregion + + #region State + + // Tracking + protected Transform PlaneTransform; + protected float GameTime; + protected bool IsSpawning; + protected float LastSpawnedX; + + // Pool state + protected List UnlockedPoolIndices = new List(); + protected Dictionary LastUsedTimes = new Dictionary(); + + // Together mode state + protected float NextSpawnX; + + // Exclusive mode state + protected Dictionary PoolNextSpawnX = new Dictionary(); + + #endregion + + #region Initialization + + /// + /// Initialize the spawner. Call this before starting spawning. + /// + public virtual void Initialize() + { + ValidateConfiguration(); + + // Unlock pool 0 immediately + if (pools != null && pools.Length > 0) + { + UnlockedPoolIndices.Add(0); + + if (showDebugLogs) + { + Logging.Debug($"[{GetType().Name}] Initialized with pool 0 unlocked"); + } + } + + // Initialize exclusive mode spawn positions if needed + if (poolMode == SpawnPoolMode.Exclusive) + { + for (int i = 0; i < pools.Length; i++) + { + PoolNextSpawnX[i] = 0f; + } + } + } + + protected virtual void ValidateConfiguration() + { + if (pools == null || pools.Length == 0) + { + Logging.Error($"[{GetType().Name}] No pools configured!"); + return; + } + + if (spawnPoint == null) + { + Logging.Warning($"[{GetType().Name}] Spawn point not assigned!"); + } + + // Validate pool unlock times are monotonically increasing + for (int i = 1; i < pools.Length; i++) + { + if (pools[i].unlockTime < pools[i - 1].unlockTime) + { + Logging.Warning($"[{GetType().Name}] Pool {i} unlock time ({pools[i].unlockTime}s) is earlier than pool {i-1} ({pools[i-1].unlockTime}s)!"); + } + } + } + + #endregion + + #region Public API + + /// + /// Start tracking the plane and enable spawning. + /// + public void StartTracking(Transform planeTransform, float startX) + { + PlaneTransform = planeTransform; + IsSpawning = true; + GameTime = 0f; + LastSpawnedX = startX; + + // Initialize next spawn position + if (poolMode == SpawnPoolMode.Together) + { + NextSpawnX = startX + Random.Range(globalMinDistance, globalMaxDistance); + } + else // Exclusive + { + foreach (int poolIndex in UnlockedPoolIndices) + { + float minDist = pools[poolIndex].GetMinDistance(globalMinDistance); + float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance); + PoolNextSpawnX[poolIndex] = startX + Random.Range(minDist, maxDist); + } + } + + if (showDebugLogs) + { + Logging.Debug($"[{GetType().Name}] Started tracking from X={startX}"); + } + } + + /// + /// Stop spawning and tracking. + /// + public void StopTracking() + { + IsSpawning = false; + PlaneTransform = null; + + if (showDebugLogs) + { + Logging.Debug($"[{GetType().Name}] Stopped tracking"); + } + } + + /// + /// Update game time and check for pool unlocks. + /// Call this every frame from the orchestrator. + /// + public void UpdateGameTime(float deltaTime) + { + GameTime += deltaTime; + CheckPoolUnlocks(); + } + + /// + /// Pre-spawn objects from start to end X position. + /// + public virtual void PreSpawn(float startX, float endX) + { + if (poolMode == SpawnPoolMode.Together) + { + PreSpawnTogether(startX, endX); + } + else + { + PreSpawnExclusive(startX, endX); + } + + LastSpawnedX = endX; + } + + /// + /// Update spawning based on plane position. + /// Call this every frame from the orchestrator. + /// + public virtual void UpdateSpawning(float spawnAheadDistance) + { + if (!IsSpawning || PlaneTransform == null) return; + + float planeX = PlaneTransform.position.x; + + if (poolMode == SpawnPoolMode.Together) + { + UpdateSpawningTogether(planeX, spawnAheadDistance); + } + else + { + UpdateSpawningExclusive(planeX, spawnAheadDistance); + } + } + + /// + /// Clean up all spawned objects. + /// + public virtual void Cleanup() + { + if (spawnContainer != null) + { + foreach (Transform child in spawnContainer) + { + Destroy(child.gameObject); + } + } + + LastUsedTimes.Clear(); + UnlockedPoolIndices.Clear(); + UnlockedPoolIndices.Add(0); // Re-add pool 0 + + if (showDebugLogs) + { + Logging.Debug($"[{GetType().Name}] Cleanup complete"); + } + } + + #endregion + + #region Pool Management + + protected void CheckPoolUnlocks() + { + for (int i = 1; i < pools.Length; i++) + { + if (UnlockedPoolIndices.Contains(i)) continue; + + if (GameTime >= pools[i].unlockTime) + { + UnlockedPoolIndices.Add(i); + + // If exclusive mode, initialize spawn position for this pool + if (poolMode == SpawnPoolMode.Exclusive && PlaneTransform != null) + { + float minDist = pools[i].GetMinDistance(globalMinDistance); + float maxDist = pools[i].GetMaxDistance(globalMaxDistance); + PoolNextSpawnX[i] = PlaneTransform.position.x + Random.Range(minDist, maxDist); + } + + if (showDebugLogs) + { + Logging.Debug($"[{GetType().Name}] Unlocked pool {i} '{pools[i].description}' at time {GameTime:F2}s"); + } + } + } + } + + protected GameObject SelectPrefabFromPools(out int selectedPoolIndex) + { + selectedPoolIndex = -1; + + if (UnlockedPoolIndices.Count == 0) return null; + + // Build weighted list of all available prefabs + List availablePrefabs = new List(); + List prefabPoolIndices = new List(); + List prefabWeights = new List(); + + foreach (int poolIndex in UnlockedPoolIndices) + { + if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue; + + foreach (GameObject prefab in pools[poolIndex].prefabs) + { + if (prefab == null) continue; + + availablePrefabs.Add(prefab); + prefabPoolIndices.Add(poolIndex); + + // Calculate weight based on recency + float weight = 1f; + if (LastUsedTimes.TryGetValue(prefab, out float lastUsedTime)) + { + float timeSinceUse = GameTime - lastUsedTime; + if (timeSinceUse < recencyPenaltyDuration) + { + weight = timeSinceUse / recencyPenaltyDuration; // 0 to 1 linear recovery + } + } + + prefabWeights.Add(weight); + } + } + + if (availablePrefabs.Count == 0) return null; + + // Select using weighted random + int selectedIndex = WeightedRandom(prefabWeights); + GameObject selectedPrefab = availablePrefabs[selectedIndex]; + selectedPoolIndex = prefabPoolIndices[selectedIndex]; + + // Update recency + LastUsedTimes[selectedPrefab] = GameTime; + + return selectedPrefab; + } + + protected int WeightedRandom(List weights) + { + float totalWeight = 0f; + foreach (float w in weights) totalWeight += w; + + float randomValue = Random.Range(0f, totalWeight); + float cumulative = 0f; + + for (int i = 0; i < weights.Count; i++) + { + cumulative += weights[i]; + if (randomValue <= cumulative) return i; + } + + return weights.Count - 1; + } + + #endregion + + #region Together Mode Spawning + + protected virtual void PreSpawnTogether(float startX, float endX) + { + float currentX = startX + Random.Range(globalMinDistance, globalMaxDistance); + + while (currentX <= endX) + { + SpawnAtPosition(currentX); + currentX += Random.Range(globalMinDistance, globalMaxDistance); + } + + NextSpawnX = currentX; + } + + protected virtual void UpdateSpawningTogether(float planeX, float spawnAheadDistance) + { + float spawnTriggerX = LastSpawnedX + spawnAheadDistance; + + if (planeX >= spawnTriggerX && planeX >= NextSpawnX) + { + SpawnAtPosition(NextSpawnX); + LastSpawnedX = NextSpawnX; + NextSpawnX += Random.Range(globalMinDistance, globalMaxDistance); + } + } + + #endregion + + #region Exclusive Mode Spawning + + protected virtual void PreSpawnExclusive(float startX, float endX) + { + foreach (int poolIndex in UnlockedPoolIndices) + { + if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue; + + float minDist = pools[poolIndex].GetMinDistance(globalMinDistance); + float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance); + float currentX = startX + Random.Range(minDist, maxDist); + + while (currentX <= endX) + { + SpawnFromPool(poolIndex, currentX); + currentX += Random.Range(minDist, maxDist); + } + + PoolNextSpawnX[poolIndex] = currentX; + } + } + + protected virtual void UpdateSpawningExclusive(float planeX, float spawnAheadDistance) + { + foreach (int poolIndex in UnlockedPoolIndices) + { + if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue; + if (!PoolNextSpawnX.ContainsKey(poolIndex)) continue; + + float nextX = PoolNextSpawnX[poolIndex]; + float spawnTargetX = planeX + spawnAheadDistance; + + if (nextX <= spawnTargetX) + { + SpawnFromPool(poolIndex, nextX); + + float minDist = pools[poolIndex].GetMinDistance(globalMinDistance); + float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance); + PoolNextSpawnX[poolIndex] = nextX + Random.Range(minDist, maxDist); + LastSpawnedX = Mathf.Max(LastSpawnedX, nextX); + } + } + } + + #endregion + + #region Spawning + + protected void SpawnAtPosition(float xPosition) + { + int selectedPoolIndex; + GameObject prefab = SelectPrefabFromPools(out selectedPoolIndex); + if (prefab == null) return; + + SpawnFromPool(selectedPoolIndex, xPosition); + } + + /// + /// Spawn a specific prefab from a pool at the given X position. + /// Override this in derived classes to implement specific spawn logic. + /// + protected virtual void SpawnFromPool(int poolIndex, float xPosition) + { + // Derived classes implement specific spawn logic + } + + protected GameObject InstantiatePrefab(GameObject prefab, Vector3 position) + { + GameObject instance = Instantiate(prefab, position, Quaternion.identity); + + if (spawnContainer != null) + { + instance.transform.SetParent(spawnContainer); + } + + return instance; + } + + #endregion + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs.meta new file mode 100644 index 00000000..66436a89 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 387858903c2c44e0ab007cb2ac886343 +timeCreated: 1765965907 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs b/Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs new file mode 100644 index 00000000..e257bf83 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs @@ -0,0 +1,38 @@ +using Core; +using UnityEngine; + +namespace Minigames.Airplane.Core.Spawning +{ + /// + /// Spawns ground tiles at fixed intervals. + /// Inherits distance-based spawning from BaseDistanceSpawner. + /// + public class GroundDistanceSpawner : BaseDistanceSpawner + { + [Header("Ground-Specific")] + [Tooltip("Y position at which to spawn ground tiles")] + [SerializeField] private float groundSpawnY = -18f; + + protected override void SpawnFromPool(int poolIndex, float xPosition) + { + if (poolIndex < 0 || poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) + return; + + // Select random tile from pool + GameObject tilePrefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)]; + if (tilePrefab == null) return; + + // Calculate spawn position + Vector3 spawnPosition = new Vector3(xPosition, groundSpawnY, 0f); + + // Instantiate + GameObject instance = InstantiatePrefab(tilePrefab, spawnPosition); + + if (showDebugLogs) + { + Logging.Debug($"[GroundDistanceSpawner] Spawned ground tile at X={xPosition:F2}, Y={groundSpawnY:F2}"); + } + } + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs.meta new file mode 100644 index 00000000..7329e382 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 76db01f1871c4b9ea66fb981b01b3ee1 +timeCreated: 1765965959 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs new file mode 100644 index 00000000..0e3dfa3e --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs @@ -0,0 +1,199 @@ +using Core; +using Minigames.Airplane.Data; +using Minigames.Airplane.Interactive; +using UnityEngine; + +namespace Minigames.Airplane.Core.Spawning +{ + /// + /// Spawns obstacle objects (positive and negative) with weighted ratio management. + /// Inherits distance-based spawning from BaseDistanceSpawner. + /// + public class ObstacleDistanceSpawner : BaseDistanceSpawner + { + [Header("Obstacle-Specific")] + [Tooltip("Ratio of positive to negative objects (0 = all negative, 1 = all positive)")] + [Range(0f, 1f)] + [SerializeField] private float positiveNegativeRatio = 0.5f; + + [Tooltip("Array indices that should be treated as positive objects")] + [SerializeField] private int[] positivePoolIndices; + + [Tooltip("Array indices that should be treated as negative objects")] + [SerializeField] private int[] negativePoolIndices; + + [Header("Positioning")] + [SerializeField] private int groundLayer; + [SerializeField] private float maxGroundRaycastDistance = 50f; + [SerializeField] private float defaultObjectYOffset; + + private int _positiveSpawnCount; + private int _negativeSpawnCount; + + public override void Initialize() + { + base.Initialize(); + _positiveSpawnCount = 0; + _negativeSpawnCount = 0; + } + + protected override void SpawnFromPool(int poolIndex, float xPosition) + { + if (poolIndex < 0 || poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) + return; + + // Determine if this should spawn positive or negative + bool isPositivePool = System.Array.IndexOf(positivePoolIndices, poolIndex) >= 0; + bool isNegativePool = System.Array.IndexOf(negativePoolIndices, poolIndex) >= 0; + + // If pool is not specifically marked, use weighted random + bool spawnPositive = isPositivePool || (!isNegativePool && ShouldSpawnPositive()); + + // Select random prefab from pool + GameObject prefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)]; + if (prefab == null) return; + + // Get spawn position from spawnPoint or use default + float spawnY = spawnPoint != null ? spawnPoint.position.y : 0f; + Vector3 tempPosition = new Vector3(xPosition, spawnY, 0f); + + // Instantiate + GameObject instance = InstantiatePrefab(prefab, tempPosition); + + // Try to get spawn entry for positioning + var spawnEntry = prefab.GetComponent(); + if (spawnEntry != null) + { + PositionObject(instance, xPosition, spawnEntry.spawnPositionMode, + spawnEntry.specifiedY, spawnEntry.randomYMin, spawnEntry.randomYMax); + } + + // Initialize if implements interface + var initializable = instance.GetComponent(); + if (initializable != null) + { + initializable.Initialize(); + } + + // Track counts + if (spawnPositive) + _positiveSpawnCount++; + else + _negativeSpawnCount++; + + if (showDebugLogs) + { + Logging.Debug($"[ObstacleDistanceSpawner] Spawned {(spawnPositive ? "positive" : "negative")} at X={xPosition:F2} from pool {poolIndex}"); + } + } + + private bool ShouldSpawnPositive() + { + int totalSpawned = _positiveSpawnCount + _negativeSpawnCount; + + // First few spawns - use pure random + if (totalSpawned < 5) + { + return Random.value <= positiveNegativeRatio; + } + + // Calculate current ratio and adjust + float currentRatio = totalSpawned > 0 ? (float)_positiveSpawnCount / totalSpawned : 0.5f; + float targetRatio = positiveNegativeRatio; + + float adjustedProbability; + if (currentRatio < targetRatio) + { + adjustedProbability = Mathf.Lerp(targetRatio, 1f, (targetRatio - currentRatio) * 2f); + } + else + { + adjustedProbability = Mathf.Lerp(0f, targetRatio, 1f - (currentRatio - targetRatio) * 2f); + } + + return Random.value <= adjustedProbability; + } + + private void PositionObject(GameObject obj, float xPosition, SpawnPositionMode mode, + float specifiedY, float randomYMin, float randomYMax) + { + if (obj == null) return; + + float targetY; + + switch (mode) + { + case SpawnPositionMode.SnapToGround: + targetY = SnapToGround(obj, xPosition); + break; + + case SpawnPositionMode.SpecifiedY: + targetY = specifiedY; + break; + + case SpawnPositionMode.RandomRange: + targetY = Random.Range(randomYMin, randomYMax); + break; + + default: + targetY = 0f; + break; + } + + Vector3 newPosition = obj.transform.position; + newPosition.y = targetY; + obj.transform.position = newPosition; + } + + private float SnapToGround(GameObject obj, float xPosition) + { + Vector2 rayOrigin = new Vector2(xPosition, 0f); + int layerMask = 1 << groundLayer; + RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, maxGroundRaycastDistance, layerMask); + + if (hit.collider != null) + { + float groundY = hit.point.y; + Bounds bounds = GetObjectBounds(obj); + return groundY + bounds.extents.y; + } + + return defaultObjectYOffset; + } + + private Bounds GetObjectBounds(GameObject obj) + { + Renderer objRenderer = obj.GetComponentInChildren(); + if (objRenderer != null) return objRenderer.bounds; + + Collider2D objCollider2D = obj.GetComponentInChildren(); + if (objCollider2D != null) return objCollider2D.bounds; + + Collider objCollider3D = obj.GetComponentInChildren(); + if (objCollider3D != null) return objCollider3D.bounds; + + return new Bounds(obj.transform.position, Vector3.one); + } + + public override void Cleanup() + { + base.Cleanup(); + _positiveSpawnCount = 0; + _negativeSpawnCount = 0; + } + } + + /// + /// Helper component to store spawn entry data on prefabs. + /// Attach this to prefabs that need specific positioning. + /// + public class PrefabSpawnEntryComponent : MonoBehaviour + { + public SpawnPositionMode spawnPositionMode = SpawnPositionMode.SnapToGround; + public float specifiedY; + public float randomYMin = -5f; + public float randomYMax = 5f; + } +} + + diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs.meta new file mode 100644 index 00000000..93854f40 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0a550c256a3d4d91a17adf6d0338f022 +timeCreated: 1765965959 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs new file mode 100644 index 00000000..dca88d21 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs @@ -0,0 +1,146 @@ +using Core; +using Minigames.Airplane.Data; +using Minigames.Airplane.Interactive; +using UnityEngine; + +namespace Minigames.Airplane.Core.Spawning +{ + /// + /// Spawns parallax background elements across 3 fixed layers (Background/Middle/Foreground). + /// Always operates in Exclusive pool mode. + /// Integrates with AirplaneCameraManager to provide camera reference to parallax elements. + /// + public class ParallaxBackgroundSpawner : BaseDistanceSpawner + { + [Header("Parallax-Specific")] + [Tooltip("Sort layer names for each parallax layer")] + [SerializeField] private string backgroundSortLayer = "Background"; + [SerializeField] private string middleSortLayer = "Midground"; + [SerializeField] private string foregroundSortLayer = "Foreground"; + + [Header("Camera Integration")] + [Tooltip("Camera manager for tracking active camera")] + [SerializeField] private AirplaneCameraManager cameraManager; + + private Transform _currentCameraTransform; + + public override void Initialize() + { + // Force exclusive mode + poolMode = SpawnPoolMode.Exclusive; + + // Validate exactly 3 pools + if (pools == null || pools.Length != 3) + { + Logging.Error("[ParallaxBackgroundSpawner] Must have exactly 3 pools (Background, Middle, Foreground)!"); + } + + base.Initialize(); + + // Subscribe to camera changes + if (cameraManager != null) + { + cameraManager.OnStateChanged += HandleCameraChanged; + _currentCameraTransform = cameraManager.GetActiveCameraTransform(); + } + else + { + Logging.Warning("[ParallaxBackgroundSpawner] Camera manager not assigned! Parallax elements won't track camera."); + } + } + + private void OnDestroy() + { + if (cameraManager != null) + { + cameraManager.OnStateChanged -= HandleCameraChanged; + } + } + + private void HandleCameraChanged(AirplaneCameraState oldState, AirplaneCameraState newState) + { + if (cameraManager == null) return; + + _currentCameraTransform = cameraManager.GetActiveCameraTransform(); + + // Update all existing parallax elements with new camera + if (spawnContainer != null) + { + foreach (Transform child in spawnContainer) + { + var parallaxElement = child.GetComponent(); + if (parallaxElement != null) + { + parallaxElement.SetCameraTransform(_currentCameraTransform); + } + } + } + + if (showDebugLogs) + { + Logging.Debug($"[ParallaxBackgroundSpawner] Camera changed to {newState}, updated parallax elements"); + } + } + + protected override void SpawnFromPool(int poolIndex, float xPosition) + { + if (poolIndex < 0 || poolIndex >= 3 || !pools[poolIndex].HasPrefabs) + return; + + // Select random prefab from pool + GameObject prefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)]; + if (prefab == null) return; + + // Get spawn Y from spawnPoint or default + float spawnY = spawnPoint != null ? spawnPoint.position.y : 0f; + Vector3 spawnPosition = new Vector3(xPosition, spawnY, 0f); + + // Instantiate + GameObject instance = InstantiatePrefab(prefab, spawnPosition); + + // Determine parallax layer based on pool index + ParallaxLayer layer = (ParallaxLayer)poolIndex; + + // Get or add ParallaxElement component + ParallaxElement parallaxElement = instance.GetComponent(); + if (parallaxElement == null) + { + parallaxElement = instance.AddComponent(); + } + + // Configure parallax element + parallaxElement.SetLayer(layer); + parallaxElement.SetCameraTransform(_currentCameraTransform); + + // Set sort layer on sprite renderer + SetSortLayer(instance, layer); + + if (showDebugLogs) + { + Logging.Debug($"[ParallaxBackgroundSpawner] Spawned {layer} element at X={xPosition:F2}"); + } + } + + private void SetSortLayer(GameObject obj, ParallaxLayer layer) + { + SpriteRenderer spriteRenderer = obj.GetComponentInChildren(); + if (spriteRenderer == null) return; + + string sortLayerName = layer switch + { + ParallaxLayer.Background => backgroundSortLayer, + ParallaxLayer.Middle => middleSortLayer, + ParallaxLayer.Foreground => foregroundSortLayer, + _ => "Default" + }; + + spriteRenderer.sortingLayerName = sortLayerName; + + if (showDebugLogs) + { + Logging.Debug($"[ParallaxBackgroundSpawner] Set sort layer '{sortLayerName}' for {layer}"); + } + } + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs.meta new file mode 100644 index 00000000..b5b1bbdd --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b126f2d189024140ac0bc6fb14155c7f +timeCreated: 1765965959 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs b/Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs new file mode 100644 index 00000000..7f44515e --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs @@ -0,0 +1,24 @@ +namespace Minigames.Airplane.Data +{ + /// + /// Defines parallax layer depth for background elements. + /// + public enum ParallaxLayer + { + /// + /// Furthest back layer, moves slowest (lowest parallax factor). + /// + Background, + + /// + /// Middle layer, moves at medium speed. + /// + Middle, + + /// + /// Closest layer, moves fastest (highest parallax factor). + /// + Foreground + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs.meta b/Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs.meta new file mode 100644 index 00000000..28f744ec --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 30cd4bc1743c44089146678153542aae +timeCreated: 1765965854 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs b/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs new file mode 100644 index 00000000..2995b0a1 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs @@ -0,0 +1,45 @@ +using System; +using UnityEngine; + +namespace Minigames.Airplane.Data +{ + /// + /// Configuration for a single spawn pool. + /// Contains prefabs, unlock timing, and optional spawn parameter overrides. + /// + [Serializable] + public class SpawnPoolConfig + { + [Tooltip("Prefabs that can spawn from this pool")] + public GameObject[] prefabs; + + [Tooltip("Time (seconds from game start) when this pool becomes available. 0 = available immediately.")] + public float unlockTime = 0f; + + [Tooltip("Description for this pool (editor reference only)")] + public string description = "Pool"; + + [Header("Spawn Parameter Overrides (0 = use global)")] + [Tooltip("Override minimum spawn distance for this pool (0 = use global)")] + public float overrideMinDistance = 0f; + + [Tooltip("Override maximum spawn distance for this pool (0 = use global)")] + public float overrideMaxDistance = 0f; + + /// + /// Check if this pool has valid prefabs assigned. + /// + public bool HasPrefabs => prefabs != null && prefabs.Length > 0; + + /// + /// Get effective minimum distance (uses override if non-zero, otherwise uses global). + /// + public float GetMinDistance(float globalMin) => overrideMinDistance > 0f ? overrideMinDistance : globalMin; + + /// + /// Get effective maximum distance (uses override if non-zero, otherwise uses global). + /// + public float GetMaxDistance(float globalMax) => overrideMaxDistance > 0f ? overrideMaxDistance : globalMax; + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs.meta b/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs.meta new file mode 100644 index 00000000..db59ad4c --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 51d29acb7cdd48819588acd1401456a9 +timeCreated: 1765965854 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs b/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs new file mode 100644 index 00000000..26bb273f --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs @@ -0,0 +1,21 @@ +namespace Minigames.Airplane.Data +{ + /// + /// Defines how multiple spawn pools are combined. + /// + public enum SpawnPoolMode + { + /// + /// All unlocked pools contribute to a single shared spawn stream. + /// Objects spawn at regular intervals considering all available pools. + /// + Together, + + /// + /// Each pool spawns independently with its own timing. + /// Multiple objects can spawn simultaneously from different pools. + /// + Exclusive + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs.meta b/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs.meta new file mode 100644 index 00000000..fd060b97 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5206332ad0e54ed4b20df47663202967 +timeCreated: 1765965854 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs b/Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs new file mode 100644 index 00000000..959638f6 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs @@ -0,0 +1,150 @@ +using Core; +using Minigames.Airplane.Data; +using UnityEngine; + +namespace Minigames.Airplane.Interactive +{ + /// + /// Parallax element that adjusts position based on camera movement. + /// Creates depth illusion by moving at different speeds for different layers. + /// Continuously tracks active camera for seamless transitions. + /// + public class ParallaxElement : MonoBehaviour + { + [Header("Layer Configuration")] + [Tooltip("Which parallax layer this element belongs to")] + [SerializeField] private ParallaxLayer layer = ParallaxLayer.Background; + + [Header("Parallax Settings")] + [Tooltip("Global parallax strength multiplier (0 = no parallax, 1 = full)")] + [SerializeField] private float globalStrength = 1f; + + [Tooltip("Per-layer parallax factors (Background/Middle/Foreground)")] + [SerializeField] private float backgroundFactor = 0.3f; + [SerializeField] private float middleFactor = 0.6f; + [SerializeField] private float foregroundFactor = 0.9f; + + [Header("Debug")] + [SerializeField] private bool showDebugLogs; + + private Transform _cameraTransform; + private Vector3 _startPosition; + private float _startCameraX; + private bool _isInitialized; + + private void Awake() + { + // Ensure correct sort layer + EnsureSortLayer(); + } + + private void Start() + { + _startPosition = transform.position; + + if (_cameraTransform != null) + { + _startCameraX = _cameraTransform.position.x; + _isInitialized = true; + } + } + + private void Update() + { + if (!_isInitialized || _cameraTransform == null) return; + + ApplyParallax(); + } + + /// + /// Set the parallax layer for this element. + /// + public void SetLayer(ParallaxLayer newLayer) + { + layer = newLayer; + EnsureSortLayer(); + + if (showDebugLogs) + { + Logging.Debug($"[ParallaxElement] Layer set to {layer}"); + } + } + + /// + /// Set the camera transform to track. + /// Call this when camera changes. + /// + public void SetCameraTransform(Transform cameraTransform) + { + if (cameraTransform == null) return; + + // If camera changed, recalculate base position + if (_cameraTransform != null && _cameraTransform != cameraTransform) + { + // Smooth transition: current world position becomes new start position + _startPosition = transform.position; + } + + _cameraTransform = cameraTransform; + _startCameraX = _cameraTransform.position.x; + _isInitialized = true; + + if (showDebugLogs) + { + Logging.Debug($"[ParallaxElement] Camera set, starting camera X={_startCameraX:F2}"); + } + } + + private void ApplyParallax() + { + // Calculate camera displacement from start + float cameraDisplacement = _cameraTransform.position.x - _startCameraX; + + // Get layer-specific parallax factor + float layerFactor = GetLayerFactor(); + + // Calculate parallax offset (reduced displacement based on layer depth) + float parallaxOffset = cameraDisplacement * layerFactor * globalStrength; + + // Apply offset to start position + Vector3 newPosition = _startPosition; + newPosition.x += parallaxOffset; + transform.position = newPosition; + } + + private float GetLayerFactor() + { + return layer switch + { + ParallaxLayer.Background => backgroundFactor, + ParallaxLayer.Middle => middleFactor, + ParallaxLayer.Foreground => foregroundFactor, + _ => 1f + }; + } + + private void EnsureSortLayer() + { + SpriteRenderer spriteRenderer = GetComponentInChildren(); + if (spriteRenderer == null) return; + + // Sort layer is set by spawner, this is just a validation/fallback + string expectedLayer = layer switch + { + ParallaxLayer.Background => "Background", + ParallaxLayer.Middle => "Midground", + ParallaxLayer.Foreground => "Foreground", + _ => "Default" + }; + + if (spriteRenderer.sortingLayerName != expectedLayer) + { + if (showDebugLogs) + { + Logging.Debug($"[ParallaxElement] Adjusting sort layer to '{expectedLayer}' for {layer}"); + } + } + } + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs.meta b/Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs.meta new file mode 100644 index 00000000..0eff2f41 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b32c96c5365743d3a1a5c849ba340b6e +timeCreated: 1765965978 \ No newline at end of file