diff --git a/Assets/Editor/CustomEditorsAndDrawers/EdgeAnchorEditor.cs b/Assets/Editor/CustomEditorsAndDrawers/EdgeAnchorEditor.cs index dd11aba6..64e7a629 100644 --- a/Assets/Editor/CustomEditorsAndDrawers/EdgeAnchorEditor.cs +++ b/Assets/Editor/CustomEditorsAndDrawers/EdgeAnchorEditor.cs @@ -1,6 +1,7 @@ using UnityEngine; using UnityEditor; using AppleHillsCamera; +using Minigames.BirdPooper; namespace Editor { @@ -16,7 +17,7 @@ namespace Editor EdgeAnchor edgeAnchor = (EdgeAnchor)target; // Check if there's an Obstacle component on the same GameObject - var obstacle = edgeAnchor.GetComponent(); + var obstacle = edgeAnchor.GetComponent(); if (obstacle != null) { diff --git a/Assets/Editor/Minigames.meta b/Assets/Editor/Minigames.meta new file mode 100644 index 00000000..77121b17 --- /dev/null +++ b/Assets/Editor/Minigames.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5d8af6b4c7a999a4aa2180ec5422baf3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Minigames/Airplane.meta b/Assets/Editor/Minigames/Airplane.meta new file mode 100644 index 00000000..338f36c9 --- /dev/null +++ b/Assets/Editor/Minigames/Airplane.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 259f822b327e0d740ae0542eb24e1d0a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Minigames/Airplane/AirplaneSettingsEditor.cs b/Assets/Editor/Minigames/Airplane/AirplaneSettingsEditor.cs new file mode 100644 index 00000000..0ca88d31 --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/AirplaneSettingsEditor.cs @@ -0,0 +1,96 @@ +using Minigames.Airplane.Data; +using Minigames.Airplane.Settings; +using UnityEditor; +using UnityEngine; + +namespace Editor.Minigames.Airplane +{ + /// + /// Custom editor for AirplaneSettings that conditionally shows default obstacle positioning fields. + /// Integrates with SettingsEditorWindow. + /// + [CustomEditor(typeof(AirplaneSettings))] + public class AirplaneSettingsEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // Draw all properties except the default obstacle positioning section + SerializedProperty iterator = serializedObject.GetIterator(); + bool enterChildren = true; + + while (iterator.NextVisible(enterChildren)) + { + enterChildren = false; + + // Skip script field + if (iterator.propertyPath == "m_Script") + continue; + + // Handle Default Obstacle Positioning section specially + if (iterator.propertyPath == "defaultObstaclePositionMode") + { + DrawDefaultObstaclePositioningSection(); + + // Skip the related fields as we'll draw them conditionally + iterator.NextVisible(false); // Skip defaultObstacleRandomYMin + iterator.NextVisible(false); // Skip defaultObstacleRandomYMax + continue; + } + + EditorGUILayout.PropertyField(iterator, true); + } + + // Apply changes and mark dirty for SettingsEditorWindow integration + if (serializedObject.ApplyModifiedProperties()) + { + EditorUtility.SetDirty(target); + } + } + + private void DrawDefaultObstaclePositioningSection() + { + var modeProperty = serializedObject.FindProperty("defaultObstaclePositionMode"); + var randomYMinProperty = serializedObject.FindProperty("defaultObstacleRandomYMin"); + var randomYMaxProperty = serializedObject.FindProperty("defaultObstacleRandomYMax"); + + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(modeProperty, new GUIContent("Default Position Mode")); + + SpawnPositionMode currentMode = (SpawnPositionMode)modeProperty.enumValueIndex; + + EditorGUILayout.Space(5); + + switch (currentMode) + { + case SpawnPositionMode.SnapToGround: + EditorGUILayout.HelpBox("Obstacles will raycast to find ground and snap to it. If raycast fails, Fallback Y Position (configured in Ground Snapping section above) will be used.", MessageType.Info); + break; + + case SpawnPositionMode.SpecifiedY: + EditorGUILayout.HelpBox("Obstacles will spawn at Fallback Y Position (configured in Ground Snapping section above).", MessageType.Info); + break; + + case SpawnPositionMode.RandomRange: + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Random Y Range", EditorStyles.miniBoldLabel); + EditorGUILayout.PropertyField(randomYMinProperty, new GUIContent("Min Y")); + EditorGUILayout.PropertyField(randomYMaxProperty, new GUIContent("Max Y")); + + // Validation + if (randomYMinProperty.floatValue > randomYMaxProperty.floatValue) + { + EditorGUILayout.HelpBox("Min Y should be less than Max Y!", MessageType.Warning); + } + else + { + EditorGUILayout.HelpBox($"Obstacles will spawn at random Y between {randomYMinProperty.floatValue:F2} and {randomYMaxProperty.floatValue:F2}.", MessageType.Info); + } + EditorGUILayout.EndVertical(); + break; + } + } + } +} + diff --git a/Assets/Editor/Minigames/Airplane/AirplaneSettingsEditor.cs.meta b/Assets/Editor/Minigames/Airplane/AirplaneSettingsEditor.cs.meta new file mode 100644 index 00000000..af4b085b --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/AirplaneSettingsEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aa9b104969324b8584672126a4277d37 +timeCreated: 1765990746 \ No newline at end of file diff --git a/Assets/Editor/Minigames/Airplane/BaseDistanceSpawnerEditor.cs b/Assets/Editor/Minigames/Airplane/BaseDistanceSpawnerEditor.cs new file mode 100644 index 00000000..80ba3a4c --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/BaseDistanceSpawnerEditor.cs @@ -0,0 +1,128 @@ +using Minigames.Airplane.Core.Spawning; +using Minigames.Airplane.Data; +using UnityEditor; +using UnityEngine; + +namespace Editor.Minigames.Airplane +{ + /// + /// Custom editor for BaseDistanceSpawner that conditionally shows/hides spawn parameters + /// based on the selected SpawnPoolMode. + /// + [CustomEditor(typeof(BaseDistanceSpawner), true)] + public class BaseDistanceSpawnerEditor : UnityEditor.Editor + { + private SerializedProperty _poolModeProperty; + private SerializedProperty _poolsProperty; + private SerializedProperty _globalMinDistanceProperty; + private SerializedProperty _globalMaxDistanceProperty; + private SerializedProperty _recencyPenaltyProperty; + private SerializedProperty _groundLayerProperty; + private SerializedProperty _maxGroundRaycastProperty; + private SerializedProperty _defaultObjectYOffsetProperty; + private SerializedProperty _showDebugLogsProperty; + + protected virtual void OnEnable() + { + _poolModeProperty = serializedObject.FindProperty("poolMode"); + _poolsProperty = serializedObject.FindProperty("pools"); + _globalMinDistanceProperty = serializedObject.FindProperty("globalMinDistance"); + _globalMaxDistanceProperty = serializedObject.FindProperty("globalMaxDistance"); + _recencyPenaltyProperty = serializedObject.FindProperty("recencyPenaltyDuration"); + _groundLayerProperty = serializedObject.FindProperty("groundLayer"); + _maxGroundRaycastProperty = serializedObject.FindProperty("maxGroundRaycastDistance"); + _defaultObjectYOffsetProperty = serializedObject.FindProperty("defaultObjectYOffset"); + _showDebugLogsProperty = serializedObject.FindProperty("showDebugLogs"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.LabelField("Distance-Based Spawner", EditorStyles.boldLabel); + EditorGUILayout.HelpBox("Spawns objects at calculated X positions based on distance from plane. Containers are configured in AirplaneSpawnManager.", MessageType.Info); + EditorGUILayout.Space(); + + // Pool Configuration + EditorGUILayout.LabelField("Pool Configuration", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(_poolModeProperty); + + SpawnPoolMode currentMode = (SpawnPoolMode)_poolModeProperty.enumValueIndex; + + // Show global parameters only in Together mode + if (currentMode == SpawnPoolMode.Together) + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Global Spawn Parameters", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(_globalMinDistanceProperty); + EditorGUILayout.PropertyField(_globalMaxDistanceProperty); + + EditorGUILayout.HelpBox("Together Mode: All pools use global spawn distances. Per-pool overrides are ignored.", MessageType.Info); + } + else + { + EditorGUILayout.HelpBox("Exclusive Mode: Each pool spawns independently. Configure per-pool spawn distances below.", MessageType.Info); + } + + EditorGUILayout.Space(); + + // Pools array with custom display + EditorGUILayout.PropertyField(_poolsProperty, true); + + // Show per-pool parameter hints + if (_poolsProperty.isExpanded && _poolsProperty.arraySize > 0) + { + EditorGUI.indentLevel++; + for (int i = 0; i < _poolsProperty.arraySize; i++) + { + var poolProperty = _poolsProperty.GetArrayElementAtIndex(i); + var overrideMinProperty = poolProperty.FindPropertyRelative("overrideMinDistance"); + var overrideMaxProperty = poolProperty.FindPropertyRelative("overrideMaxDistance"); + + if (currentMode == SpawnPoolMode.Together) + { + // Grey out per-pool overrides in Together mode + if (overrideMinProperty.floatValue > 0 || overrideMaxProperty.floatValue > 0) + { + EditorGUILayout.HelpBox($"Pool {i}: Per-pool overrides ignored in Together mode", MessageType.Warning); + } + } + else + { + // Show active overrides in Exclusive mode + if (overrideMinProperty.floatValue <= 0 && overrideMaxProperty.floatValue <= 0) + { + EditorGUILayout.HelpBox($"Pool {i}: Using global distances (set overrides > 0 to customize)", MessageType.Info); + } + } + } + EditorGUI.indentLevel--; + } + + EditorGUILayout.Space(); + + // Object Positioning + EditorGUILayout.LabelField("Object Positioning", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(_groundLayerProperty); + EditorGUILayout.PropertyField(_maxGroundRaycastProperty); + EditorGUILayout.PropertyField(_defaultObjectYOffsetProperty); + EditorGUILayout.HelpBox("Prefabs can use PrefabSpawnEntryComponent to specify Y positioning: SnapToGround, SpecifiedY, or RandomRange", MessageType.Info); + + EditorGUILayout.Space(); + + // Recency Tracking + EditorGUILayout.LabelField("Recency Tracking", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(_recencyPenaltyProperty); + EditorGUILayout.HelpBox("Recently spawned prefabs receive lower weight for diversity", MessageType.Info); + + EditorGUILayout.Space(); + + // Debug + EditorGUILayout.LabelField("Debug", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(_showDebugLogsProperty); + + serializedObject.ApplyModifiedProperties(); + } + } +} + diff --git a/Assets/Editor/Minigames/Airplane/BaseDistanceSpawnerEditor.cs.meta b/Assets/Editor/Minigames/Airplane/BaseDistanceSpawnerEditor.cs.meta new file mode 100644 index 00000000..af48b92a --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/BaseDistanceSpawnerEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 32aded175b00b8a4bae4a95861db8123 \ No newline at end of file diff --git a/Assets/Editor/Minigames/Airplane/GroundDistanceSpawnerEditor.cs b/Assets/Editor/Minigames/Airplane/GroundDistanceSpawnerEditor.cs new file mode 100644 index 00000000..07fa86ae --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/GroundDistanceSpawnerEditor.cs @@ -0,0 +1,63 @@ +using Minigames.Airplane.Core.Spawning; +using UnityEditor; +using UnityEngine; + +namespace Editor.Minigames.Airplane +{ + /// + /// Simplified custom editor for GroundDistanceSpawner. + /// Shows only ground-relevant fields. Ground Y and interval are in AirplaneSettings. + /// + [CustomEditor(typeof(GroundDistanceSpawner))] + public class GroundDistanceSpawnerEditor : UnityEditor.Editor + { + private SerializedProperty _poolsProperty; + + private void OnEnable() + { + _poolsProperty = serializedObject.FindProperty("pools"); + + // Ensure exactly 1 pool exists + if (_poolsProperty.arraySize != 1) + { + _poolsProperty.arraySize = 1; + serializedObject.ApplyModifiedProperties(); + } + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // Ensure exactly 1 pool + if (_poolsProperty.arraySize != 1) + { + _poolsProperty.arraySize = 1; + } + + // Draw single pool + EditorGUILayout.LabelField("Ground Tiles (Fixed: 1 pool)", EditorStyles.boldLabel); + + var poolElement = _poolsProperty.GetArrayElementAtIndex(0); + var prefabsProperty = poolElement.FindPropertyRelative("prefabs"); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + EditorGUILayout.PropertyField(prefabsProperty, new GUIContent("Prefabs"), true); + + if (prefabsProperty.arraySize == 0) + { + EditorGUILayout.HelpBox("Add at least one ground tile prefab", MessageType.Warning); + } + else + { + EditorGUILayout.LabelField($"Prefabs: {prefabsProperty.arraySize}", EditorStyles.miniLabel); + } + + EditorGUILayout.EndVertical(); + + serializedObject.ApplyModifiedProperties(); + } + } +} + diff --git a/Assets/Editor/Minigames/Airplane/GroundDistanceSpawnerEditor.cs.meta b/Assets/Editor/Minigames/Airplane/GroundDistanceSpawnerEditor.cs.meta new file mode 100644 index 00000000..d71a66f4 --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/GroundDistanceSpawnerEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 31225636ea0743fdbfe13fbb0c5f46af +timeCreated: 1765987013 \ No newline at end of file diff --git a/Assets/Editor/Minigames/Airplane/ObstacleDistanceSpawnerEditor.cs b/Assets/Editor/Minigames/Airplane/ObstacleDistanceSpawnerEditor.cs new file mode 100644 index 00000000..117f1375 --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/ObstacleDistanceSpawnerEditor.cs @@ -0,0 +1,145 @@ +using Minigames.Airplane.Core.Spawning; +using Minigames.Airplane.Data; +using UnityEditor; +using UnityEngine; + +namespace Editor.Minigames.Airplane +{ + /// + /// Custom editor for ObstacleDistanceSpawner. + /// Enforces exactly 2 pools (Positive/Negative) with custom labels. + /// All spawn parameters configured in AirplaneSettings. + /// + [CustomEditor(typeof(ObstacleDistanceSpawner))] + public class ObstacleDistanceSpawnerEditor : UnityEditor.Editor + { + private SerializedProperty _poolModeProperty; + private SerializedProperty _poolsProperty; + + private readonly string[] _poolNames = { "Positive Obstacles", "Negative Obstacles" }; + private readonly string[] _poolDescriptions = + { + "Obstacles that benefit the player", + "Obstacles that hinder the player" + }; + + private bool[] _poolFoldouts = new bool[2]; + + private void OnEnable() + { + _poolModeProperty = serializedObject.FindProperty("poolMode"); + _poolsProperty = serializedObject.FindProperty("pools"); + + // Ensure exactly 2 pools exist + EnsureTwoPools(); + } + + private void EnsureTwoPools() + { + if (_poolsProperty != null && _poolsProperty.arraySize != 2) + { + _poolsProperty.arraySize = 2; + + // Initialize pool descriptions + for (int i = 0; i < 2; i++) + { + var poolElement = _poolsProperty.GetArrayElementAtIndex(i); + var descProperty = poolElement.FindPropertyRelative("description"); + if (descProperty != null && string.IsNullOrEmpty(descProperty.stringValue)) + { + descProperty.stringValue = _poolNames[i]; + } + } + + serializedObject.ApplyModifiedProperties(); + } + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.HelpBox("Note: Positive/Negative ratio, unlock times, spawn distances, recency tracking, positioning, and debug settings are configured globally in AirplaneSettings. Containers are configured in AirplaneSpawnManager.", MessageType.Info); + EditorGUILayout.Space(); + + if (_poolModeProperty != null) + { + EditorGUILayout.PropertyField(_poolModeProperty); + + SpawnPoolMode currentMode = (SpawnPoolMode)_poolModeProperty.enumValueIndex; + + if (currentMode == SpawnPoolMode.Together) + { + EditorGUILayout.HelpBox("Together Mode: Both pools share a single spawn stream using global distances from settings.", MessageType.Info); + } + else + { + EditorGUILayout.HelpBox("Exclusive Mode: Each pool spawns independently. Use per-pool overrides or global distances from settings.", MessageType.Info); + } + } + + EditorGUILayout.Space(); + + // Obstacle Pools (exactly 2, custom display) + EditorGUILayout.LabelField("Obstacle Pools (Fixed: 2 pools)", EditorStyles.boldLabel); + + EnsureTwoPools(); + + if (_poolsProperty != null && _poolsProperty.arraySize == 2) + { + for (int i = 0; i < 2; i++) + { + var poolElement = _poolsProperty.GetArrayElementAtIndex(i); + var prefabsProperty = poolElement.FindPropertyRelative("prefabs"); + var descriptionProperty = poolElement.FindPropertyRelative("description"); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Foldout with custom label + _poolFoldouts[i] = EditorGUILayout.Foldout(_poolFoldouts[i], $"{_poolNames[i]} (Pool {i})", true, EditorStyles.foldoutHeader); + + if (_poolFoldouts[i]) + { + EditorGUI.indentLevel++; + + EditorGUILayout.LabelField(_poolDescriptions[i], EditorStyles.miniLabel); + EditorGUILayout.Space(5); + + if (prefabsProperty != null) + { + EditorGUILayout.PropertyField(prefabsProperty, new GUIContent("Prefabs"), true); + } + + if (descriptionProperty != null) + { + EditorGUILayout.PropertyField(descriptionProperty, new GUIContent("Description")); + } + + // Show info about prefab count + if (prefabsProperty != null) + { + if (prefabsProperty.arraySize == 0) + { + EditorGUILayout.HelpBox("Add prefabs for this pool", MessageType.Warning); + } + else + { + EditorGUILayout.LabelField($"Prefabs: {prefabsProperty.arraySize}", EditorStyles.miniLabel); + } + } + + EditorGUI.indentLevel--; + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(5); + } + } + + EditorGUILayout.Space(); + EditorGUILayout.HelpBox("Note: Positive/Negative ratio, unlock times, spawn distances, recency tracking, positioning, and debug settings are configured globally in AirplaneSettings. Containers are configured in AirplaneSpawnManager.", MessageType.Info); + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Assets/Editor/Minigames/Airplane/ObstacleDistanceSpawnerEditor.cs.meta b/Assets/Editor/Minigames/Airplane/ObstacleDistanceSpawnerEditor.cs.meta new file mode 100644 index 00000000..db89de99 --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/ObstacleDistanceSpawnerEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5d7f009b2123488381edd87bfb3ab2e8 +timeCreated: 1765985361 \ No newline at end of file diff --git a/Assets/Editor/Minigames/Airplane/ParallaxBackgroundSpawnerEditor.cs b/Assets/Editor/Minigames/Airplane/ParallaxBackgroundSpawnerEditor.cs new file mode 100644 index 00000000..e9517d28 --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/ParallaxBackgroundSpawnerEditor.cs @@ -0,0 +1,142 @@ +using Minigames.Airplane.Core.Spawning; +using Minigames.Airplane.Data; +using UnityEditor; +using UnityEngine; + +namespace Editor.Minigames.Airplane +{ + /// + /// Custom editor for ParallaxBackgroundSpawner. + /// Enforces exactly 3 pools with custom labels (Background/Middle/Foreground). + /// Prevents array manipulation and provides clean, designer-friendly interface. + /// + [CustomEditor(typeof(ParallaxBackgroundSpawner))] + public class ParallaxBackgroundSpawnerEditor : UnityEditor.Editor + { + private SerializedProperty _poolsProperty; + private SerializedProperty _backgroundSortLayerProperty; + private SerializedProperty _middleSortLayerProperty; + private SerializedProperty _foregroundSortLayerProperty; + private SerializedProperty _cameraManagerProperty; + private SerializedProperty _showDebugLogsProperty; + + private readonly string[] _layerNames = { "Background Layer", "Middle Layer", "Foreground Layer" }; + private readonly string[] _layerDescriptions = + { + "Slowest parallax - furthest back", + "Medium parallax - middle depth", + "Fastest parallax - closest to camera" + }; + + private bool[] _poolFoldouts = new bool[3]; + + private void OnEnable() + { + _poolsProperty = serializedObject.FindProperty("pools"); + _backgroundSortLayerProperty = serializedObject.FindProperty("backgroundSortLayer"); + _middleSortLayerProperty = serializedObject.FindProperty("middleSortLayer"); + _foregroundSortLayerProperty = serializedObject.FindProperty("foregroundSortLayer"); + _cameraManagerProperty = serializedObject.FindProperty("cameraManager"); + _showDebugLogsProperty = serializedObject.FindProperty("showDebugLogs"); + + // Ensure exactly 3 pools exist + EnsureThreePools(); + } + + private void EnsureThreePools() + { + if (_poolsProperty.arraySize != 3) + { + _poolsProperty.arraySize = 3; + + // Initialize pool descriptions + for (int i = 0; i < 3; i++) + { + var poolElement = _poolsProperty.GetArrayElementAtIndex(i); + var descProperty = poolElement.FindPropertyRelative("description"); + if (string.IsNullOrEmpty(descProperty.stringValue)) + { + descProperty.stringValue = _layerNames[i]; + } + } + + serializedObject.ApplyModifiedProperties(); + } + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + EditorGUILayout.HelpBox("Note: Spawn distances, recency tracking, and debug settings are configured globally in AirplaneSettings. Containers are configured in AirplaneSpawnManager.", MessageType.Info); + EditorGUILayout.Space(); + + // Camera Integration + EditorGUILayout.PropertyField(_cameraManagerProperty); + EditorGUILayout.Space(); + + // Sort Layers + EditorGUILayout.LabelField("Sort Layer Configuration", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(_backgroundSortLayerProperty, new GUIContent("Background Sort Layer")); + EditorGUILayout.PropertyField(_middleSortLayerProperty, new GUIContent("Middle Sort Layer")); + EditorGUILayout.PropertyField(_foregroundSortLayerProperty, new GUIContent("Foreground Sort Layer")); + EditorGUILayout.Space(); + + // Pool Mode (locked to Exclusive) + EditorGUILayout.LabelField("Spawn Mode", EditorStyles.boldLabel); + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.TextField("Pool Mode", "Exclusive (Fixed)"); + EditorGUI.EndDisabledGroup(); + EditorGUILayout.HelpBox("Parallax spawner always uses Exclusive mode - each layer spawns independently", MessageType.Info); + EditorGUILayout.Space(); + + // Parallax Layers (exactly 3, custom display) + EditorGUILayout.LabelField("Parallax Layers (Fixed: 3 layers)", EditorStyles.boldLabel); + + EnsureThreePools(); // Safety check + + for (int i = 0; i < 3; i++) + { + var poolElement = _poolsProperty.GetArrayElementAtIndex(i); + var prefabsProperty = poolElement.FindPropertyRelative("prefabs"); + var unlockTimeProperty = poolElement.FindPropertyRelative("unlockTime"); + var descProperty = poolElement.FindPropertyRelative("description"); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Foldout with custom label + _poolFoldouts[i] = EditorGUILayout.Foldout(_poolFoldouts[i], $"{_layerNames[i]} (Pool {i})", true, EditorStyles.foldoutHeader); + + if (_poolFoldouts[i]) + { + EditorGUI.indentLevel++; + + EditorGUILayout.LabelField(_layerDescriptions[i], EditorStyles.miniLabel); + EditorGUILayout.Space(5); + + EditorGUILayout.PropertyField(prefabsProperty, new GUIContent("Prefabs"), true); + EditorGUILayout.PropertyField(unlockTimeProperty, new GUIContent("Unlock Time (seconds)")); + + // Show info about prefab count + if (prefabsProperty.arraySize == 0) + { + EditorGUILayout.HelpBox("Add prefabs for this layer", MessageType.Warning); + } + else + { + EditorGUILayout.LabelField($"Prefabs: {prefabsProperty.arraySize}", EditorStyles.miniLabel); + } + + EditorGUI.indentLevel--; + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(5); + } + + EditorGUILayout.Space(); + + serializedObject.ApplyModifiedProperties(); + } + } +} + diff --git a/Assets/Editor/Minigames/Airplane/ParallaxBackgroundSpawnerEditor.cs.meta b/Assets/Editor/Minigames/Airplane/ParallaxBackgroundSpawnerEditor.cs.meta new file mode 100644 index 00000000..5b5bb725 --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/ParallaxBackgroundSpawnerEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 41d4944f4ed9436f84384fb2361f6e0c +timeCreated: 1765972253 \ No newline at end of file diff --git a/Assets/Editor/Minigames/Airplane/PrefabSpawnEntryComponentEditor.cs b/Assets/Editor/Minigames/Airplane/PrefabSpawnEntryComponentEditor.cs new file mode 100644 index 00000000..9a6aa9a1 --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/PrefabSpawnEntryComponentEditor.cs @@ -0,0 +1,75 @@ +using Minigames.Airplane.Data; +using UnityEditor; +using UnityEngine; + +namespace Editor.Minigames.Airplane +{ + /// + /// Custom editor for PrefabSpawnEntryComponent that conditionally shows fields based on spawn mode. + /// + [CustomEditor(typeof(PrefabSpawnEntryComponent))] + public class PrefabSpawnEntryComponentEditor : UnityEditor.Editor + { + private SerializedProperty _spawnPositionModeProperty; + private SerializedProperty _specifiedYProperty; + private SerializedProperty _randomYMinProperty; + private SerializedProperty _randomYMaxProperty; + + private void OnEnable() + { + _spawnPositionModeProperty = serializedObject.FindProperty("spawnPositionMode"); + _specifiedYProperty = serializedObject.FindProperty("specifiedY"); + _randomYMinProperty = serializedObject.FindProperty("randomYMin"); + _randomYMaxProperty = serializedObject.FindProperty("randomYMax"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.LabelField("Prefab Spawn Positioning", EditorStyles.boldLabel); + EditorGUILayout.HelpBox("Controls how this prefab is positioned vertically when spawned. If this component is missing, spawner uses default settings from AirplaneSettings.", MessageType.Info); + EditorGUILayout.Space(); + + // Always show the mode selector + EditorGUILayout.PropertyField(_spawnPositionModeProperty, new GUIContent("Spawn Position Mode")); + + SpawnPositionMode currentMode = (SpawnPositionMode)_spawnPositionModeProperty.enumValueIndex; + + EditorGUILayout.Space(); + + // Show mode-specific fields + switch (currentMode) + { + case SpawnPositionMode.SnapToGround: + EditorGUILayout.HelpBox("Object will raycast down to find ground and snap its bottom to the surface. If no ground is found, fallback Y position from settings will be used.", MessageType.Info); + break; + + case SpawnPositionMode.SpecifiedY: + EditorGUILayout.LabelField("Fixed Y Position", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(_specifiedYProperty, new GUIContent("Y Position")); + EditorGUILayout.HelpBox("Object will spawn at this exact Y coordinate.", MessageType.Info); + break; + + case SpawnPositionMode.RandomRange: + EditorGUILayout.LabelField("Random Y Range", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(_randomYMinProperty, new GUIContent("Min Y")); + EditorGUILayout.PropertyField(_randomYMaxProperty, new GUIContent("Max Y")); + + // Validation + if (_randomYMinProperty.floatValue > _randomYMaxProperty.floatValue) + { + EditorGUILayout.HelpBox("Min Y should be less than Max Y!", MessageType.Warning); + } + else + { + EditorGUILayout.HelpBox($"Object will spawn at random Y between {_randomYMinProperty.floatValue:F2} and {_randomYMaxProperty.floatValue:F2}.", MessageType.Info); + } + break; + } + + serializedObject.ApplyModifiedProperties(); + } + } +} + diff --git a/Assets/Editor/Minigames/Airplane/PrefabSpawnEntryComponentEditor.cs.meta b/Assets/Editor/Minigames/Airplane/PrefabSpawnEntryComponentEditor.cs.meta new file mode 100644 index 00000000..7569e669 --- /dev/null +++ b/Assets/Editor/Minigames/Airplane/PrefabSpawnEntryComponentEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0eccbe64a21c4c07a09b2df66faf43b1 +timeCreated: 1765990535 \ No newline at end of file diff --git a/Assets/Editor/Settings/SettingsEditorWindow.cs b/Assets/Editor/Settings/SettingsEditorWindow.cs index c2848f50..f2c3e283 100644 --- a/Assets/Editor/Settings/SettingsEditorWindow.cs +++ b/Assets/Editor/Settings/SettingsEditorWindow.cs @@ -181,28 +181,38 @@ namespace AppleHills.Core.Settings.Editor return; } - SerializedObject serializedObj = serializedSettingsObjects[typeof(T).Name]; - serializedObj.Update(); - EditorGUILayout.Space(10); - // Draw all properties - SerializedProperty property = serializedObj.GetIterator(); - bool enterChildren = true; - while (property.NextVisible(enterChildren)) + // Use CreateEditor to respect custom editors (like AirplaneSettingsEditor) + UnityEditor.Editor editor = UnityEditor.Editor.CreateEditor(settings); + if (editor != null) { - enterChildren = false; - - // Skip the script field - if (property.name == "m_Script") continue; - - EditorGUILayout.PropertyField(property, true); + editor.OnInspectorGUI(); + DestroyImmediate(editor); } - - // Apply changes - if (serializedObj.ApplyModifiedProperties()) + else { - EditorUtility.SetDirty(settings); + // Fallback to default drawing if no custom editor exists + SerializedObject serializedObj = serializedSettingsObjects[typeof(T).Name]; + serializedObj.Update(); + + SerializedProperty property = serializedObj.GetIterator(); + bool enterChildren = true; + while (property.NextVisible(enterChildren)) + { + enterChildren = false; + + // Skip the script field + if (property.name == "m_Script") continue; + + EditorGUILayout.PropertyField(property, true); + } + + // Apply changes + if (serializedObj.ApplyModifiedProperties()) + { + EditorUtility.SetDirty(settings); + } } } diff --git a/Assets/Prefabs/Minigames/Airplane/PlaceholderSpawns/AirplaneWindZone.prefab b/Assets/Prefabs/Minigames/Airplane/PlaceholderSpawns/AirplaneWindZone.prefab index 4620cd8b..70d10cda 100644 --- a/Assets/Prefabs/Minigames/Airplane/PlaceholderSpawns/AirplaneWindZone.prefab +++ b/Assets/Prefabs/Minigames/Airplane/PlaceholderSpawns/AirplaneWindZone.prefab @@ -11,6 +11,7 @@ GameObject: - component: {fileID: 4572188480515029494} - component: {fileID: 6979039544463298865} - component: {fileID: 1989378388080844566} + - component: {fileID: 7567962733712432860} m_Layer: 0 m_Name: AirplaneWindZone m_TagString: Untagged @@ -96,6 +97,22 @@ MonoBehaviour: isWorldSpace: 1 windParticles: {fileID: 0} showDebugLogs: 0 +--- !u!114 &7567962733712432860 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 497267990420767357} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 90239fb003214b4087d0717f6f417161, type: 3} + m_Name: + m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Data.PrefabSpawnEntryComponent + spawnPositionMode: 0 + specifiedY: 0 + randomYMin: -5 + randomYMax: 5 --- !u!1 &2715744416533832886 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/Minigames/Airplane/PlaceholderSpawns/BouncySurface.prefab b/Assets/Prefabs/Minigames/Airplane/PlaceholderSpawns/BouncySurface.prefab index aa8485e7..7fc004ed 100644 --- a/Assets/Prefabs/Minigames/Airplane/PlaceholderSpawns/BouncySurface.prefab +++ b/Assets/Prefabs/Minigames/Airplane/PlaceholderSpawns/BouncySurface.prefab @@ -12,6 +12,7 @@ GameObject: - component: {fileID: 8126210844742366787} - component: {fileID: 7874709284498167353} - component: {fileID: 806255670199755721} + - component: {fileID: 6677313130545265604} m_Layer: 0 m_Name: BouncySurface m_TagString: Untagged @@ -160,3 +161,19 @@ SpriteRenderer: m_SpriteTileMode: 0 m_WasSpriteAssigned: 1 m_SpriteSortPoint: 0 +--- !u!114 &6677313130545265604 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1917678391913987792} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 90239fb003214b4087d0717f6f417161, type: 3} + m_Name: + m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Data.PrefabSpawnEntryComponent + spawnPositionMode: 0 + specifiedY: 0 + randomYMin: -5 + randomYMax: 5 diff --git a/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity b/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity index e20e43f2..4643e799 100644 --- a/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity +++ b/Assets/Scenes/MiniGames/ValentineNoteDelivery.unity @@ -173,6 +173,37 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button +--- !u!1 &39044400 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 39044401} + m_Layer: 0 + m_Name: GameObject (1) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &39044401 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 39044400} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 78.47819, y: 25.5, 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 &108199298 GameObject: m_ObjectHideFlags: 0 @@ -584,6 +615,37 @@ Transform: m_CorrespondingSourceObject: {fileID: 5380908876971534942, guid: a9b4569fcc08080479d99b9c3bcee089, type: 3} m_PrefabInstance: {fileID: 453576884181909409} m_PrefabAsset: {fileID: 0} +--- !u!1 &377530944 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 377530945} + m_Layer: 0 + m_Name: SpawnedParalax + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &377530945 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 377530944} + 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!1 &469774135 GameObject: m_ObjectHideFlags: 0 @@ -2128,7 +2190,7 @@ GameObject: m_Component: - component: {fileID: 1219431443} m_Layer: 0 - m_Name: SpawnedObjects + m_Name: SpawnedTargets m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -2467,7 +2529,7 @@ GameObject: m_Component: - component: {fileID: 1405572709} m_Layer: 0 - m_Name: SpawnContainer + m_Name: SpawnedObstacles m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -3734,6 +3796,8 @@ GameObject: - component: {fileID: 1784207402} - component: {fileID: 1784207403} - component: {fileID: 1784207404} + - component: {fileID: 1784207406} + - component: {fileID: 1784207405} m_Layer: 0 m_Name: SpawnManager m_TagString: Untagged @@ -3750,13 +3814,14 @@ Transform: m_GameObject: {fileID: 1784207401} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 17.2, y: -0.1, z: 0} + m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1219431443} - - {fileID: 1287102785} - {fileID: 1405572709} + - {fileID: 1287102785} + - {fileID: 377530945} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1784207403 @@ -3771,9 +3836,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} + obstacleSpawner: {fileID: 1784207404} + groundSpawner: {fileID: 1784207406} + parallaxSpawner: {fileID: 1784207405} targetPrefabs: - targetKey: bob_target prefab: {fileID: 3207629437433571205, guid: 9f4bb48933059e543b60ac782d2140d8, type: 3} @@ -3790,7 +3855,10 @@ MonoBehaviour: targetDisplayUI: {fileID: 1520040329} launchController: {fileID: 1309397785} dynamicSpawnThresholdMarker: {fileID: 1653173574} - targetParent: {fileID: 1405572709} + obstacleContainer: {fileID: 1405572709} + groundContainer: {fileID: 1287102785} + parallaxContainer: {fileID: 377530945} + targetParent: {fileID: 1219431443} showDebugLogs: 0 --- !u!114 &1784207404 MonoBehaviour: @@ -3805,19 +3873,76 @@ MonoBehaviour: 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 + pools: + - prefabs: + - {fileID: 497267990420767357, guid: dc32284941c693c4f9e422f4197ca61e, type: 3} + - {fileID: 1917678391913987792, guid: 989121c9099e41e469824ddeaf0e34a5, type: 3} + - {fileID: 7032677151789119314, guid: 7dc33e43acead834ba6a231b67cfd2d9, type: 3} + unlockTime: 0 + description: Base pool of positive obstacles + overrideMinDistance: 0 + overrideMaxDistance: 0 + - prefabs: + - {fileID: 1186710456879913970, guid: 006b956651124704dbae5bd4faab3152, type: 3} + - {fileID: 2434350760695575337, guid: f3188909ff4e845499a5cbfd0ae93101, type: 3} + unlockTime: 0 + description: Base pool of negative obstacles + overrideMinDistance: 0 + overrideMaxDistance: 0 +--- !u!114 &1784207405 +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: b126f2d189024140ac0bc6fb14155c7f, type: 3} + m_Name: + m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.Spawning.ParallaxBackgroundSpawner + poolMode: 0 + pools: + - prefabs: [] + unlockTime: 0 + description: Background Layer + overrideMinDistance: 0 + overrideMaxDistance: 0 + - prefabs: [] + unlockTime: 0 + description: Middle Layer + overrideMinDistance: 0 + overrideMaxDistance: 0 + - prefabs: [] + unlockTime: 0 + description: Foreground Layer + overrideMinDistance: 0 + overrideMaxDistance: 0 + backgroundSortLayer: Background + middleSortLayer: Midground + foregroundSortLayer: Foreground + cameraManager: {fileID: 0} +--- !u!114 &1784207406 +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: 76db01f1871c4b9ea66fb981b01b3ee1, type: 3} + m_Name: + m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.Spawning.GroundDistanceSpawner + poolMode: 0 + pools: + - prefabs: + - {fileID: 5175967588203935335, guid: a9b4569fcc08080479d99b9c3bcee089, type: 3} + unlockTime: 0 + description: + overrideMinDistance: 0 + overrideMaxDistance: 0 + groundSpawnY: -18 --- !u!1 &1810521056 GameObject: m_ObjectHideFlags: 0 @@ -5687,3 +5812,4 @@ SceneRoots: - {fileID: 1701327424} - {fileID: 2041920296} - {fileID: 1710395163} + - {fileID: 39044401} diff --git a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs index a4f90f61..d7b47922 100644 --- a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs +++ b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AppleHills.Core.Settings; +using Minigames.Airplane.Data; using Minigames.StatueDressup.Data; using UnityEngine; @@ -344,11 +345,18 @@ namespace Core.Settings float PositiveNegativeRatio { get; } // 0-1, where 1 = all positive, 0 = all negative float SpawnDistanceAhead { get; } float GroundSpawnInterval { get; } + float RecencyPenaltyDuration { get; } // Time penalty for recently-spawned prefabs // Ground Snapping int GroundLayer { get; } float MaxGroundRaycastDistance { get; } - float DefaultObjectYOffset { get; } + float FallbackYPosition { get; } // Y position when SnapToGround fails OR when using SpecifiedY mode (universal fallback) + float GroundSpawnY { get; } + + // Default Obstacle Positioning (used when prefab has no PrefabSpawnEntryComponent) + SpawnPositionMode DefaultObstaclePositionMode { get; } + float DefaultObstacleRandomYMin { get; } // Min Y when DefaultObstaclePositionMode = RandomRange + float DefaultObstacleRandomYMax { get; } // Max Y when DefaultObstaclePositionMode = RandomRange // Debug bool ShowDebugLogs { get; } diff --git a/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs b/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs index 11208938..0cd8a6c1 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/AirplaneSpawnManager.cs @@ -72,6 +72,16 @@ namespace Minigames.Airplane.Core [Tooltip("Transform marker in scene where dynamic spawning begins (uses X position)")] [SerializeField] private Transform dynamicSpawnThresholdMarker; + [Header("Spawn Organization")] + [Tooltip("Parent transform for spawned obstacles (organization)")] + [SerializeField] private Transform obstacleContainer; + + [Tooltip("Parent transform for spawned ground tiles (organization)")] + [SerializeField] private Transform groundContainer; + + [Tooltip("Parent transform for spawned parallax elements (organization)")] + [SerializeField] private Transform parallaxContainer; + [Header("Spawn Parents")] [Tooltip("Parent transform for spawned target (optional)")] [SerializeField] private Transform targetParent; @@ -242,6 +252,14 @@ namespace Minigames.Airplane.Core if (groundSpawner != null) { groundSpawner.PreSpawn(preSpawnStartX, preSpawnEndX); + + // Force physics system to update so ground colliders are available for raycasting + Physics2D.SyncTransforms(); + + if (showDebugLogs) + { + Logging.Debug("[AirplaneSpawnManager] Physics synced after ground spawn"); + } } // 2. Spawn parallax background (if assigned) @@ -250,7 +268,7 @@ namespace Minigames.Airplane.Core parallaxSpawner.PreSpawn(preSpawnStartX, preSpawnEndX); } - // 3. Spawn obstacles (after ground exists) + // 3. Spawn obstacles (after ground exists and physics updated) if (obstacleSpawner != null) { obstacleSpawner.PreSpawn(preSpawnStartX, preSpawnEndX); @@ -279,33 +297,10 @@ namespace Minigames.Airplane.Core _isSpawningActive = true; _gameTime = 0f; - // 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) { targetDisplayUI.StartTracking(planeTransform); - - if (showDebugLogs) - { - Logging.Debug("[AirplaneSpawnManager] UI tracking started"); - } } if (showDebugLogs) @@ -322,22 +317,6 @@ 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) { @@ -481,22 +460,25 @@ namespace Minigames.Airplane.Core { if (obstacleSpawner != null) { - obstacleSpawner.Initialize(); + var initParams = new SpawnInitParameters(obstacleContainer, _settings); + obstacleSpawner.Initialize(initParams); } if (groundSpawner != null) { - groundSpawner.Initialize(); + var initParams = new SpawnInitParameters(groundContainer, _settings); + groundSpawner.Initialize(initParams); } if (parallaxSpawner != null) { - parallaxSpawner.Initialize(); + var initParams = new SpawnInitParameters(parallaxContainer, _settings); + parallaxSpawner.Initialize(initParams); } if (showDebugLogs) { - Logging.Debug("[AirplaneSpawnManager] All spawners initialized"); + Logging.Debug("[AirplaneSpawnManager] All spawners initialized - distance-based spawning ready"); } } @@ -520,7 +502,7 @@ namespace Minigames.Airplane.Core private void UpdateDynamicSpawning() { - if (_planeTransform == null) return; + if (_planeTransform == null || !_isSpawningActive) return; float planeX = _planeTransform.position.x; @@ -535,21 +517,36 @@ namespace Minigames.Airplane.Core if (shouldSpawnNewContent) { - float spawnAheadDistance = _settings.SpawnDistanceAhead; + // Centralized distance checking - orchestrator decides when to spawn + // Obstacles - check if next spawn point is within ahead distance if (obstacleSpawner != null) { - obstacleSpawner.UpdateSpawning(spawnAheadDistance); + float nextObstacleX = obstacleSpawner.GetNextSpawnX(); + if (nextObstacleX <= planeX + _settings.SpawnDistanceAhead) + { + obstacleSpawner.SpawnNext(); + } } + // Ground - spawn further ahead (2x distance) if (groundSpawner != null) { - groundSpawner.UpdateSpawning(spawnAheadDistance * 2f); // Ground spawns further ahead + float nextGroundX = groundSpawner.GetNextSpawnX(); + if (nextGroundX <= planeX + (_settings.SpawnDistanceAhead * 2f)) + { + groundSpawner.SpawnNext(); + } } + // Parallax - spawn at 1.5x distance if (parallaxSpawner != null) { - parallaxSpawner.UpdateSpawning(spawnAheadDistance * 1.5f); + float nextParallaxX = parallaxSpawner.GetNextSpawnX(); + if (nextParallaxX <= planeX + (_settings.SpawnDistanceAhead * 1.5f)) + { + parallaxSpawner.SpawnNext(); + } } } } @@ -719,9 +716,9 @@ namespace Minigames.Airplane.Core } 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; + // No ground found - use fallback + Logging.Warning($"[SpawnManager] No ground found for target at X={xPosition:F2} (raycast from Y=20 for {_settings.MaxGroundRaycastDistance} units), using fallback Y={_settings.FallbackYPosition}"); + return _settings.FallbackYPosition; } } @@ -853,7 +850,7 @@ namespace Minigames.Airplane.Core return groundY + bounds.extents.y; } - return _settings.DefaultObjectYOffset; + return _settings.FallbackYPosition; } /// diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs b/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs index 6329e71d..443697d5 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Core; using Core.Lifecycle; +using Core.Settings; using Minigames.Airplane.Data; using UnityEngine; @@ -9,7 +10,9 @@ 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. + /// All spawn parameters (distances, recency, positioning, debug) are configured in AirplaneSettings. /// Derived classes implement specific spawn logic via SpawnFromPool. + /// Orchestrator (AirplaneSpawnManager) handles distance checking and calls SpawnNext when needed. /// public abstract class BaseDistanceSpawner : ManagedBehaviour { @@ -22,35 +25,16 @@ namespace Minigames.Airplane.Core.Spawning [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 + // Shared references (set by orchestrator via SpawnInitParameters) + protected Transform spawnContainer; + protected IAirplaneSettings settings; + // Tracking - protected Transform PlaneTransform; protected float GameTime; - protected bool IsSpawning; protected float LastSpawnedX; // Pool state @@ -68,10 +52,15 @@ namespace Minigames.Airplane.Core.Spawning #region Initialization /// - /// Initialize the spawner. Call this before starting spawning. + /// Initialize the spawner with shared configuration. Call this before starting spawning. /// - public virtual void Initialize() + /// Initialization parameters containing shared references + public virtual void Initialize(SpawnInitParameters initParams) { + // Store shared references + spawnContainer = initParams.SpawnContainer; + settings = initParams.Settings; + ValidateConfiguration(); // Unlock pool 0 immediately @@ -79,7 +68,7 @@ namespace Minigames.Airplane.Core.Spawning { UnlockedPoolIndices.Add(0); - if (showDebugLogs) + if (settings.ShowDebugLogs) { Logging.Debug($"[{GetType().Name}] Initialized with pool 0 unlocked"); } @@ -88,9 +77,12 @@ namespace Minigames.Airplane.Core.Spawning // Initialize exclusive mode spawn positions if needed if (poolMode == SpawnPoolMode.Exclusive) { - for (int i = 0; i < pools.Length; i++) + if (pools != null) { - PoolNextSpawnX[i] = 0f; + for (int i = 0; i < pools.Length; i++) + { + PoolNextSpawnX[i] = 0f; + } } } } @@ -103,11 +95,6 @@ namespace Minigames.Airplane.Core.Spawning 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++) { @@ -122,51 +109,6 @@ namespace Minigames.Airplane.Core.Spawning #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. @@ -177,6 +119,58 @@ namespace Minigames.Airplane.Core.Spawning CheckPoolUnlocks(); } + /// + /// Spawn the next object at the predetermined NextSpawnX position. + /// Updates LastSpawnedX and calculates new NextSpawnX. + /// Called by orchestrator when distance check determines spawning is needed. + /// + public virtual void SpawnNext() + { + if (poolMode == SpawnPoolMode.Together) + { + SpawnAtPosition(NextSpawnX); + LastSpawnedX = NextSpawnX; + NextSpawnX = LastSpawnedX + Random.Range(settings.ObjectSpawnMinDistance, settings.ObjectSpawnMaxDistance); + } + else // Exclusive mode - spawn from each pool independently + { + foreach (int poolIndex in UnlockedPoolIndices) + { + if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue; + if (!PoolNextSpawnX.ContainsKey(poolIndex)) continue; + + SpawnFromPool(poolIndex, PoolNextSpawnX[poolIndex]); + + float minDist = pools[poolIndex].GetMinDistance(settings.ObjectSpawnMinDistance); + float maxDist = pools[poolIndex].GetMaxDistance(settings.ObjectSpawnMaxDistance); + PoolNextSpawnX[poolIndex] += Random.Range(minDist, maxDist); + LastSpawnedX = Mathf.Max(LastSpawnedX, PoolNextSpawnX[poolIndex]); + } + } + } + + /// + /// Get the X position where the next spawn will occur. + /// Used by orchestrator for distance checking. + /// + public float GetNextSpawnX() + { + if (poolMode == SpawnPoolMode.Together) + { + return NextSpawnX; + } + else // Exclusive - return the closest spawn point across all pools + { + float closest = float.MaxValue; + foreach (var kvp in PoolNextSpawnX) + { + if (kvp.Value < closest) + closest = kvp.Value; + } + return closest; + } + } + /// /// Pre-spawn objects from start to end X position. /// @@ -194,26 +188,6 @@ namespace Minigames.Airplane.Core.Spawning 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. /// @@ -231,7 +205,7 @@ namespace Minigames.Airplane.Core.Spawning UnlockedPoolIndices.Clear(); UnlockedPoolIndices.Add(0); // Re-add pool 0 - if (showDebugLogs) + if (settings.ShowDebugLogs) { Logging.Debug($"[{GetType().Name}] Cleanup complete"); } @@ -252,14 +226,15 @@ namespace Minigames.Airplane.Core.Spawning UnlockedPoolIndices.Add(i); // If exclusive mode, initialize spawn position for this pool - if (poolMode == SpawnPoolMode.Exclusive && PlaneTransform != null) + if (poolMode == SpawnPoolMode.Exclusive) { - float minDist = pools[i].GetMinDistance(globalMinDistance); - float maxDist = pools[i].GetMaxDistance(globalMaxDistance); - PoolNextSpawnX[i] = PlaneTransform.position.x + Random.Range(minDist, maxDist); + float minDist = pools[i].GetMinDistance(settings.ObjectSpawnMinDistance); + float maxDist = pools[i].GetMaxDistance(settings.ObjectSpawnMaxDistance); + // Initialize at last spawned position plus random distance + PoolNextSpawnX[i] = LastSpawnedX + Random.Range(minDist, maxDist); } - if (showDebugLogs) + if (settings.ShowDebugLogs) { Logging.Debug($"[{GetType().Name}] Unlocked pool {i} '{pools[i].description}' at time {GameTime:F2}s"); } @@ -267,7 +242,7 @@ namespace Minigames.Airplane.Core.Spawning } } - protected GameObject SelectPrefabFromPools(out int selectedPoolIndex) + protected virtual GameObject SelectPrefabFromPools(out int selectedPoolIndex) { selectedPoolIndex = -1; @@ -294,9 +269,9 @@ namespace Minigames.Airplane.Core.Spawning if (LastUsedTimes.TryGetValue(prefab, out float lastUsedTime)) { float timeSinceUse = GameTime - lastUsedTime; - if (timeSinceUse < recencyPenaltyDuration) + if (timeSinceUse < settings.RecencyPenaltyDuration) { - weight = timeSinceUse / recencyPenaltyDuration; // 0 to 1 linear recovery + weight = timeSinceUse / settings.RecencyPenaltyDuration; // 0 to 1 linear recovery } } @@ -340,29 +315,17 @@ namespace Minigames.Airplane.Core.Spawning protected virtual void PreSpawnTogether(float startX, float endX) { - float currentX = startX + Random.Range(globalMinDistance, globalMaxDistance); + float currentX = startX + Random.Range(settings.ObjectSpawnMinDistance, settings.ObjectSpawnMaxDistance); while (currentX <= endX) { SpawnAtPosition(currentX); - currentX += Random.Range(globalMinDistance, globalMaxDistance); + currentX += Random.Range(settings.ObjectSpawnMinDistance, settings.ObjectSpawnMaxDistance); } 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 @@ -373,8 +336,8 @@ namespace Minigames.Airplane.Core.Spawning { if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue; - float minDist = pools[poolIndex].GetMinDistance(globalMinDistance); - float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance); + float minDist = pools[poolIndex].GetMinDistance(settings.ObjectSpawnMinDistance); + float maxDist = pools[poolIndex].GetMaxDistance(settings.ObjectSpawnMaxDistance); float currentX = startX + Random.Range(minDist, maxDist); while (currentX <= endX) @@ -387,49 +350,24 @@ namespace Minigames.Airplane.Core.Spawning } } - 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); + GameObject prefab = SelectPrefabFromPools(out int poolIndex); + if (prefab != null && poolIndex >= 0) + { + SpawnFromPool(poolIndex, xPosition); + } } /// /// Spawn a specific prefab from a pool at the given X position. - /// Override this in derived classes to implement specific spawn logic. + /// Derived classes must implement this to define spawn behavior. /// - protected virtual void SpawnFromPool(int poolIndex, float xPosition) - { - // Derived classes implement specific spawn logic - } + protected abstract void SpawnFromPool(int poolIndex, float xPosition); protected GameObject InstantiatePrefab(GameObject prefab, Vector3 position) { @@ -444,6 +382,95 @@ namespace Minigames.Airplane.Core.Spawning } #endregion + + #region Object Positioning + + /// + /// Position an object based on the specified spawn mode. + /// Used by spawners to apply Y positioning after instantiation. + /// + protected 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; + } + + /// + /// Snap an object to the ground using raycast. + /// Positions object so its bottom bounds touch the ground surface. + /// + protected float SnapToGround(GameObject obj, float xPosition) + { + // Raycast from high up to ensure we're above the ground + Vector2 rayOrigin = new Vector2(xPosition, settings.MaxGroundRaycastDistance); + int layerMask = 1 << settings.GroundLayer; + RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, settings.MaxGroundRaycastDistance + 100f, layerMask); + + if (hit.collider != null) + { + float groundY = hit.point.y; + Bounds bounds = GetObjectBounds(obj); + float finalY = groundY + bounds.extents.y; + + if (settings.ShowDebugLogs) + { + Logging.Debug($"[{GetType().Name}] SnapToGround: X={xPosition:F2}, Ground={groundY:F2}, Final={finalY:F2}"); + } + + return finalY; + } + + if (settings.ShowDebugLogs) + { + Logging.Warning($"[{GetType().Name}] SnapToGround FAILED at X={xPosition:F2} - no ground found! Using fallback Y={settings.FallbackYPosition:F2}"); + } + + return settings.FallbackYPosition; + } + + /// + /// Get the bounds of an object from its Renderer or Collider. + /// + protected 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); + } + + #endregion } } diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs.meta b/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs.meta index 66436a89..077fc476 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs.meta +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/BaseDistanceSpawner.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 387858903c2c44e0ab007cb2ac886343 -timeCreated: 1765965907 \ No newline at end of file +guid: f65e211e40b247ffb0ac920be5a9ce53 +timeCreated: 1765993195 \ 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 index e257bf83..f51be8ec 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/GroundDistanceSpawner.cs @@ -1,4 +1,6 @@ using Core; +using Core.Settings; +using Minigames.Airplane.Data; using UnityEngine; namespace Minigames.Airplane.Core.Spawning @@ -6,6 +8,7 @@ namespace Minigames.Airplane.Core.Spawning /// /// Spawns ground tiles at fixed intervals. /// Inherits distance-based spawning from BaseDistanceSpawner. + /// Uses IAirplaneSettings.GroundSpawnInterval for consistent tile spacing. /// public class GroundDistanceSpawner : BaseDistanceSpawner { @@ -13,6 +16,49 @@ namespace Minigames.Airplane.Core.Spawning [Tooltip("Y position at which to spawn ground tiles")] [SerializeField] private float groundSpawnY = -18f; + private float _groundSpawnInterval; + + public override void Initialize(SpawnInitParameters initParams) + { + base.Initialize(initParams); + + // Get fixed ground spawn interval from settings + _groundSpawnInterval = initParams.Settings.GroundSpawnInterval; + + // Force Together mode for ground spawning + poolMode = SpawnPoolMode.Together; + + if (settings.ShowDebugLogs) + { + Logging.Debug($"[GroundDistanceSpawner] Using fixed interval: {_groundSpawnInterval}f from settings"); + } + } + + // Override to use fixed intervals instead of random distances + protected override void PreSpawnTogether(float startX, float endX) + { + float currentX = startX; + + while (currentX <= endX) + { + SpawnAtPosition(currentX); + currentX += _groundSpawnInterval; // FIXED interval, not random + } + + NextSpawnX = currentX; + } + + /// + /// Override SpawnNext to use fixed ground intervals instead of random distances. + /// Called by orchestrator when distance check determines spawning is needed. + /// + public override void SpawnNext() + { + SpawnAtPosition(NextSpawnX); + LastSpawnedX = NextSpawnX; + NextSpawnX += _groundSpawnInterval; // FIXED interval, not random + } + protected override void SpawnFromPool(int poolIndex, float xPosition) { if (poolIndex < 0 || poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) @@ -22,15 +68,15 @@ namespace Minigames.Airplane.Core.Spawning 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); + // Calculate spawn position using settings + Vector3 spawnPosition = new Vector3(xPosition, settings.GroundSpawnY, 0f); // Instantiate GameObject instance = InstantiatePrefab(tilePrefab, spawnPosition); - if (showDebugLogs) + if (settings.ShowDebugLogs) { - Logging.Debug($"[GroundDistanceSpawner] Spawned ground tile at X={xPosition:F2}, Y={groundSpawnY:F2}"); + Logging.Debug($"[GroundDistanceSpawner] Spawned ground tile at X={xPosition:F2}, Y={settings.GroundSpawnY:F2}"); } } } diff --git a/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs index 0e3dfa3e..265bbca8 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ObstacleDistanceSpawner.cs @@ -1,71 +1,183 @@ using Core; using Minigames.Airplane.Data; using Minigames.Airplane.Interactive; +using System.Collections.Generic; using UnityEngine; namespace Minigames.Airplane.Core.Spawning { /// /// Spawns obstacle objects (positive and negative) with weighted ratio management. + /// Uses exactly 2 fixed pools: Pool 0 = Positive, Pool 1 = Negative. /// 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() + public override void Initialize(SpawnInitParameters initParams) { - base.Initialize(); + // TEMPORARY: Unconditional log to verify this is being called + Logging.Debug("[ObstacleDistanceSpawner] Initialize() called - spawner is active!"); + + // Validate exactly 2 pools + if (pools == null || pools.Length != 2) + { + Logging.Error("[ObstacleDistanceSpawner] Must have exactly 2 pools (Positive, Negative)!"); + } + + base.Initialize(initParams); _positiveSpawnCount = 0; _negativeSpawnCount = 0; } + /// + /// Override base prefab selection to implement positive/negative ratio management. + /// Uses actual spawn counts to maintain target ratio over time. + /// + protected override GameObject SelectPrefabFromPools(out int selectedPoolIndex) + { + selectedPoolIndex = -1; + + // Determine which pool (positive or negative) based on ratio + int targetPoolIndex = DeterminePoolIndexByRatio(); + + if (targetPoolIndex < 0 || targetPoolIndex >= pools.Length || !pools[targetPoolIndex].HasPrefabs) + { + if (settings.ShowDebugLogs) + { + Logging.Warning($"[ObstacleDistanceSpawner] Target pool {targetPoolIndex} has no prefabs!"); + } + return null; + } + + // Get all prefabs from the target pool + List availablePrefabs = new List(); + List prefabWeights = new List(); + + foreach (GameObject prefab in pools[targetPoolIndex].prefabs) + { + if (prefab == null) continue; + + availablePrefabs.Add(prefab); + + // Calculate weight based on recency tracking + float weight = 1f; + if (LastUsedTimes.TryGetValue(prefab, out float lastUsedTime)) + { + float timeSinceUse = GameTime - lastUsedTime; + if (timeSinceUse < settings.RecencyPenaltyDuration) + { + weight = timeSinceUse / settings.RecencyPenaltyDuration; // 0 to 1 linear recovery + } + } + + prefabWeights.Add(weight); + } + + if (availablePrefabs.Count == 0) return null; + + // Select using weighted random (respects recency) + int selectedIndex = WeightedRandom(prefabWeights); + GameObject selectedPrefab = availablePrefabs[selectedIndex]; + selectedPoolIndex = targetPoolIndex; + + // Update recency tracking + LastUsedTimes[selectedPrefab] = GameTime; + + if (settings.ShowDebugLogs) + { + Logging.Debug($"[ObstacleDistanceSpawner] Selected {(targetPoolIndex == 0 ? "positive" : "negative")} prefab from pool {targetPoolIndex}"); + } + + return selectedPrefab; + } + + /// + /// Determines which pool (positive or negative) to spawn from based on target ratio. + /// Uses actual spawn counts to push toward target ratio over time. + /// + private int DeterminePoolIndexByRatio() + { + int totalSpawned = _positiveSpawnCount + _negativeSpawnCount; + float targetPositiveRatio = settings.PositiveNegativeRatio; + + bool shouldSpawnPositive; + + // First spawn - use ratio as pure probability + if (totalSpawned == 0) + { + shouldSpawnPositive = Random.value < targetPositiveRatio; + if (settings.ShowDebugLogs) + { + Logging.Debug($"[ObstacleDistanceSpawner] First spawn - ratio {targetPositiveRatio:P0} → {(shouldSpawnPositive ? "positive" : "negative")}"); + } + } + else + { + // Calculate current ratio vs target + float currentPositiveRatio = (float)_positiveSpawnCount / totalSpawned; + float difference = targetPositiveRatio - currentPositiveRatio; + + // Adjust probability based on how far we are from target + // If difference > 0: we need more positives, increase positive chance + // If difference < 0: we need more negatives, decrease positive chance + float adjustedProbability = Mathf.Clamp01(targetPositiveRatio + difference); + + shouldSpawnPositive = Random.value < adjustedProbability; + + if (settings.ShowDebugLogs) + { + Logging.Debug($"[ObstacleDistanceSpawner] Ratio tracking: {_positiveSpawnCount}pos/{_negativeSpawnCount}neg ({currentPositiveRatio:P0} current vs {targetPositiveRatio:P0} target) → adjusted probability {adjustedProbability:P0} → {(shouldSpawnPositive ? "positive" : "negative")}"); + } + } + + return shouldSpawnPositive ? 0 : 1; + } + protected override void SpawnFromPool(int poolIndex, float xPosition) { - if (poolIndex < 0 || poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) + if (poolIndex < 0 || poolIndex >= 2 || !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()); + // Pool index directly determines positive/negative + // Pool 0 = Positive, Pool 1 = Negative + bool spawnPositive = (poolIndex == 0); // 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); + // Temporary spawn position (Y will be adjusted by PositionObject) + Vector3 tempPosition = new Vector3(xPosition, 0f, 0f); // Instantiate GameObject instance = InstantiatePrefab(prefab, tempPosition); - // Try to get spawn entry for positioning + // Try to get spawn entry for positioning (per-prefab override) var spawnEntry = prefab.GetComponent(); if (spawnEntry != null) { + // Use per-prefab configuration PositionObject(instance, xPosition, spawnEntry.spawnPositionMode, spawnEntry.specifiedY, spawnEntry.randomYMin, spawnEntry.randomYMax); + + if (settings.ShowDebugLogs) + { + Logging.Debug($"[ObstacleDistanceSpawner] Using per-prefab positioning: {spawnEntry.spawnPositionMode}"); + } + } + else + { + // Fall back to global default configuration from settings + PositionObject(instance, xPosition, settings.DefaultObstaclePositionMode, + settings.FallbackYPosition, settings.DefaultObstacleRandomYMin, settings.DefaultObstacleRandomYMax); + + if (settings.ShowDebugLogs) + { + Logging.Debug($"[ObstacleDistanceSpawner] Using default positioning from settings: {settings.DefaultObstaclePositionMode}"); + } } // Initialize if implements interface @@ -81,100 +193,12 @@ namespace Minigames.Airplane.Core.Spawning else _negativeSpawnCount++; - if (showDebugLogs) + if (settings.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(); @@ -182,18 +206,5 @@ namespace Minigames.Airplane.Core.Spawning _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/ParallaxBackgroundSpawner.cs b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs index dca88d21..40c1494e 100644 --- a/Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs +++ b/Assets/Scripts/Minigames/Airplane/Core/Spawning/ParallaxBackgroundSpawner.cs @@ -24,7 +24,7 @@ namespace Minigames.Airplane.Core.Spawning private Transform _currentCameraTransform; - public override void Initialize() + public override void Initialize(SpawnInitParameters initParams) { // Force exclusive mode poolMode = SpawnPoolMode.Exclusive; @@ -35,7 +35,7 @@ namespace Minigames.Airplane.Core.Spawning Logging.Error("[ParallaxBackgroundSpawner] Must have exactly 3 pools (Background, Middle, Foreground)!"); } - base.Initialize(); + base.Initialize(initParams); // Subscribe to camera changes if (cameraManager != null) @@ -76,7 +76,7 @@ namespace Minigames.Airplane.Core.Spawning } } - if (showDebugLogs) + if (settings.ShowDebugLogs) { Logging.Debug($"[ParallaxBackgroundSpawner] Camera changed to {newState}, updated parallax elements"); } @@ -91,9 +91,8 @@ namespace Minigames.Airplane.Core.Spawning 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); + // Spawn at temporary position (Y will be adjusted by PositionObject if component exists) + Vector3 spawnPosition = new Vector3(xPosition, 0f, 0f); // Instantiate GameObject instance = InstantiatePrefab(prefab, spawnPosition); @@ -115,7 +114,16 @@ namespace Minigames.Airplane.Core.Spawning // Set sort layer on sprite renderer SetSortLayer(instance, layer); - if (showDebugLogs) + // Try to get spawn entry for Y positioning (same logic as obstacles) + var spawnEntry = prefab.GetComponent(); + if (spawnEntry != null) + { + PositionObject(instance, xPosition, spawnEntry.spawnPositionMode, + spawnEntry.specifiedY, spawnEntry.randomYMin, spawnEntry.randomYMax); + } + // Otherwise stays at Y=0 + + if (settings.ShowDebugLogs) { Logging.Debug($"[ParallaxBackgroundSpawner] Spawned {layer} element at X={xPosition:F2}"); } @@ -136,7 +144,7 @@ namespace Minigames.Airplane.Core.Spawning spriteRenderer.sortingLayerName = sortLayerName; - if (showDebugLogs) + if (settings.ShowDebugLogs) { Logging.Debug($"[ParallaxBackgroundSpawner] Set sort layer '{sortLayerName}' for {layer}"); } diff --git a/Assets/Scripts/Minigames/Airplane/Data/PrefabSpawnEntryComponent.cs b/Assets/Scripts/Minigames/Airplane/Data/PrefabSpawnEntryComponent.cs new file mode 100644 index 00000000..f25d9d3c --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/PrefabSpawnEntryComponent.cs @@ -0,0 +1,27 @@ +using Minigames.Airplane.Data; +using UnityEngine; + +namespace Minigames.Airplane.Data +{ + /// + /// Component to store spawn positioning data on prefabs. + /// Attach this to obstacle prefabs that need specific positioning behavior. + /// If not present, spawner will use default positioning from AirplaneSettings. + /// + [AddComponentMenu("Airplane/Prefab Spawn Entry")] + public class PrefabSpawnEntryComponent : MonoBehaviour + { + [Tooltip("How to position this object vertically")] + public SpawnPositionMode spawnPositionMode = SpawnPositionMode.SnapToGround; + + [Tooltip("Y position to use (when mode is SpecifiedY)")] + public float specifiedY; + + [Tooltip("Min Y for random range (when mode is RandomRange)")] + public float randomYMin = -5f; + + [Tooltip("Max Y for random range (when mode is RandomRange)")] + public float randomYMax = 5f; + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Data/PrefabSpawnEntryComponent.cs.meta b/Assets/Scripts/Minigames/Airplane/Data/PrefabSpawnEntryComponent.cs.meta new file mode 100644 index 00000000..89df8434 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/PrefabSpawnEntryComponent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 90239fb003214b4087d0717f6f417161 +timeCreated: 1765990367 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Data/SpawnInitParameters.cs b/Assets/Scripts/Minigames/Airplane/Data/SpawnInitParameters.cs new file mode 100644 index 00000000..1170e188 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/SpawnInitParameters.cs @@ -0,0 +1,32 @@ +using Core.Settings; +using UnityEngine; + +namespace Minigames.Airplane.Data +{ + /// + /// Parameters passed from AirplaneSpawnManager to spawners during initialization. + /// Encapsulates shared references and configuration. + /// + public class SpawnInitParameters + { + /// + /// Parent transform for spawned objects (organization) + /// + public Transform SpawnContainer { get; set; } + + /// + /// Settings reference for spawn configuration + /// + public IAirplaneSettings Settings { get; set; } + + /// + /// Create spawn initialization parameters + /// + public SpawnInitParameters(Transform spawnContainer, IAirplaneSettings settings) + { + SpawnContainer = spawnContainer; + Settings = settings; + } + } +} + diff --git a/Assets/Scripts/Minigames/Airplane/Data/SpawnInitParameters.cs.meta b/Assets/Scripts/Minigames/Airplane/Data/SpawnInitParameters.cs.meta new file mode 100644 index 00000000..4a9dfe45 --- /dev/null +++ b/Assets/Scripts/Minigames/Airplane/Data/SpawnInitParameters.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: eb788e2f6d204a5fb1b2ae79ed38e7c2 +timeCreated: 1765972065 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs b/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs index aa57e44d..6798d655 100644 --- a/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs +++ b/Assets/Scripts/Minigames/Airplane/Settings/AirplaneSettings.cs @@ -107,6 +107,9 @@ namespace Minigames.Airplane.Settings [Tooltip("Distance interval for ground tile spawning")] [SerializeField] private float groundSpawnInterval = 5f; + [Tooltip("Time penalty (seconds) applied to recently-spawned prefabs for diversity")] + [SerializeField] private float recencyPenaltyDuration = 10f; + [Header("Ground Snapping")] [Tooltip("Layer for ground detection (objects will snap to this)")] [Layer] @@ -115,8 +118,24 @@ namespace Minigames.Airplane.Settings [Tooltip("Maximum distance to raycast for ground")] [SerializeField] private float maxGroundRaycastDistance = 50f; - [Tooltip("Default Y offset for objects if no ground found")] - [SerializeField] private float defaultObjectYOffset = 0f; + [Tooltip("Fallback Y position (used when SnapToGround fails OR when using SpecifiedY positioning mode)")] + [SerializeField] private float fallbackYPosition = 0f; + + [Tooltip("Y position at which to spawn ground tiles")] + [SerializeField] private float groundSpawnY = -18f; + + [Header("Default Obstacle Positioning")] + [Tooltip("Default mode for obstacle positioning")] + [SerializeField] private Data.SpawnPositionMode defaultObstaclePositionMode = Data.SpawnPositionMode.SnapToGround; + + [Tooltip("Default Y position for obstacles (if specified)")] + [SerializeField] private float defaultObstacleSpecifiedY = -10f; + + [Tooltip("Minimum random Y position for obstacles (if random range used)")] + [SerializeField] private float defaultObstacleRandomYMin = -5; + + [Tooltip("Maximum random Y position for obstacles (if random range used)")] + [SerializeField] private float defaultObstacleRandomYMax = 5; [Header("Debug")] [Tooltip("Show debug logs in console")] @@ -155,9 +174,14 @@ namespace Minigames.Airplane.Settings public float PositiveNegativeRatio => positiveNegativeRatio; public float SpawnDistanceAhead => spawnDistanceAhead; public float GroundSpawnInterval => groundSpawnInterval; + public float RecencyPenaltyDuration => recencyPenaltyDuration; public int GroundLayer => groundLayer; public float MaxGroundRaycastDistance => maxGroundRaycastDistance; - public float DefaultObjectYOffset => defaultObjectYOffset; + public float FallbackYPosition => fallbackYPosition; + public float GroundSpawnY => groundSpawnY; + public Data.SpawnPositionMode DefaultObstaclePositionMode => defaultObstaclePositionMode; + public float DefaultObstacleRandomYMin => defaultObstacleRandomYMin; + public float DefaultObstacleRandomYMax => defaultObstacleRandomYMax; public bool ShowDebugLogs => showDebugLogs; #endregion diff --git a/Assets/Settings/AirplaneSettings.asset b/Assets/Settings/AirplaneSettings.asset index c439a976..ebab573f 100644 --- a/Assets/Settings/AirplaneSettings.asset +++ b/Assets/Settings/AirplaneSettings.asset @@ -78,15 +78,23 @@ MonoBehaviour: introDuration: 2 personIntroDuration: 2 evaluationDuration: 2 - dynamicSpawnThresholdMarker: {fileID: 0} - targetMinDistance: 100 + targetFlybyLingerDuration: 1.5 + targetFlybyCameraBlendTime: 1 + preSpawnBeyondTargetDistance: 50 + targetMinDistance: 200 targetMaxDistance: 300 objectSpawnMinDistance: 5 objectSpawnMaxDistance: 30 - positiveNegativeRatio: 0.5 - spawnDistanceAhead: 50 + positiveNegativeRatio: 0.7 + spawnDistanceAhead: 75 groundSpawnInterval: 30 + recencyPenaltyDuration: 10 groundLayer: 14 maxGroundRaycastDistance: 50 - defaultObjectYOffset: -18 - showDebugLogs: 0 + fallbackYPosition: 0 + groundSpawnY: -5 + defaultObstaclePositionMode: 2 + defaultObstacleSpecifiedY: -10 + defaultObstacleRandomYMin: 5 + defaultObstacleRandomYMax: 25 + showDebugLogs: 1