diff --git a/Assets/Scenes/MiniGames/BirdPoop.unity b/Assets/Scenes/MiniGames/BirdPoop.unity
index 9d5faabc..4ca4923d 100644
--- a/Assets/Scenes/MiniGames/BirdPoop.unity
+++ b/Assets/Scenes/MiniGames/BirdPoop.unity
@@ -285,6 +285,7 @@ GameObject:
m_Component:
- component: {fileID: 128829408}
- component: {fileID: 128829407}
+ - component: {fileID: 128829409}
m_Layer: 0
m_Name: MinigameManager
m_TagString: Untagged
@@ -307,6 +308,7 @@ MonoBehaviour:
player: {fileID: 941621859}
obstacleSpawner: {fileID: 938885957}
targetSpawner: {fileID: 1838778561}
+ tapToStartController: {fileID: 128829409}
gameOverScreen: {fileID: 81231374}
poopPrefab: {fileID: 5552423787977869117, guid: 066f9990a9b1f5547b387633d5d204c0, type: 3}
poopCooldown: 0.5
@@ -325,6 +327,23 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &128829409
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 128829406}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 2a6ee5aca3ca423c82b57e16c0b2cca3, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: AppleHillsScripts::Minigames.BirdPooper.TapToStartController
+ fingerContainer: {fileID: 2064126204}
+ fingerImage: {fileID: 1481863789}
+ blinkDuration: 1.5
+ minAlpha: 0.3
+ maxAlpha: 1
--- !u!1 &402349268
GameObject:
m_ObjectHideFlags: 0
@@ -771,53 +790,6 @@ MonoBehaviour:
despawnPoint: {fileID: 938473626}
referenceMarker: {fileID: 1143700529}
cameraAdapter: {fileID: 2103114179}
- obstaclePrefabs:
- - {fileID: 8855270423038321603, guid: 20ae02a8f50484045aaf3dcee33fb9a2, type: 3}
- - {fileID: 2514399078413048981, guid: ee834e7efcf7d8749881f71f8b0da99c, type: 3}
- - {fileID: 842802843766402460, guid: cdc806fd167bba3488797031a28657fa, type: 3}
- - {fileID: 4239333156730914246, guid: 332d8cce2ed99054c83ecf84fbfa14c8, type: 3}
- - {fileID: 6660502783540694524, guid: 5d42fc70e5838544ab654e30aa4b0c48, type: 3}
- - {fileID: 2421410811796775077, guid: 371a09b68a5c0654bac9ba58ad3bcbe5, type: 3}
- - {fileID: 1408173265900928789, guid: 871373a85e5da0e4cafdf0e47496e105, type: 3}
- - {fileID: 1408173265900928789, guid: d2998934362713545a040d7017a1bd36, type: 3}
- - {fileID: 1408173265900928789, guid: 146d99398c0e7964dbed504e256adab7, type: 3}
- - {fileID: 1408173265900928789, guid: dc8a19e9a4d30b44596237d915b3b73f, type: 3}
- - {fileID: 1408173265900928789, guid: 471f367e14f9cfb4fb2c40d799d4c292, type: 3}
- - {fileID: 1408173265900928789, guid: 5f1734c5705cdfd49ae3180d678d28b3, type: 3}
- - {fileID: 1408173265900928789, guid: 6bc84c3ea9854b54f85a8fb69c769790, type: 3}
- - {fileID: 1408173265900928789, guid: 166c7e1bfcc4c854fab0af51cdfff746, type: 3}
- - {fileID: 1408173265900928789, guid: 65810bfd58ebbaf4482527452258ae50, type: 3}
- - {fileID: 1408173265900928789, guid: ae3986a7db087c845b618a9c897705ec, type: 3}
- minSpawnInterval: 2
- maxSpawnInterval: 8
- difficultyRampDuration: 360
- difficultyCurve:
- serializedVersion: 2
- m_Curve:
- - serializedVersion: 3
- time: 0
- value: 0
- inSlope: 0
- outSlope: 0
- tangentMode: 0
- weightedMode: 0
- inWeight: 0
- outWeight: 0
- - serializedVersion: 3
- time: 1
- value: 1
- inSlope: 2
- outSlope: 2
- tangentMode: 0
- weightedMode: 0
- inWeight: 0
- outWeight: 0
- m_PreInfinity: 2
- m_PostInfinity: 2
- m_RotationOrder: 4
- intervalJitter: 0.3
- recentDecayDuration: 60
- minRecentWeight: 0.05
--- !u!1 &941621855
GameObject:
m_ObjectHideFlags: 0
@@ -1484,6 +1456,81 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1481863787
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1481863788}
+ - component: {fileID: 1481863790}
+ - component: {fileID: 1481863789}
+ m_Layer: 5
+ m_Name: Image
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1481863788
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1481863787}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1.18, y: 1.18, z: 1.18}
+ m_ConstrainProportionsScale: 1
+ m_Children: []
+ m_Father: {fileID: 2064126205}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 316, y: 246}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &1481863789
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1481863787}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: -7164639588303836088, guid: 6fee60c82a4f504419e535456268a19e, type: 3}
+ m_Type: 0
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &1481863790
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1481863787}
+ m_CullTransparentMesh: 1
--- !u!1 &1498486830
GameObject:
m_ObjectHideFlags: 0
@@ -1615,6 +1662,7 @@ RectTransform:
m_Children:
- {fileID: 1088771378}
- {fileID: 81231372}
+ - {fileID: 2064126205}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@@ -1844,6 +1892,42 @@ MonoBehaviour:
- {fileID: 8373178063207716143, guid: 020f7494c613b06479ccad2c4cedde0f, type: 3}
minTargetSpawnInterval: 9
maxTargetSpawnInterval: 15
+--- !u!1 &2064126204
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2064126205}
+ m_Layer: 5
+ m_Name: StartGamePrompt
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &2064126205
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2064126204}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 1481863788}
+ m_Father: {fileID: 1536057440}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 0, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &2103114174
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/Scripts/Core/Settings/BirdPooperSettings.cs b/Assets/Scripts/Core/Settings/BirdPooperSettings.cs
index e0f6af68..837865f8 100644
--- a/Assets/Scripts/Core/Settings/BirdPooperSettings.cs
+++ b/Assets/Scripts/Core/Settings/BirdPooperSettings.cs
@@ -1,4 +1,5 @@
using AppleHills.Core.Settings;
+using Minigames.BirdPooper;
using UnityEngine;
namespace Core.Settings
@@ -33,8 +34,8 @@ namespace Core.Settings
[Tooltip("Obstacle scroll speed in units/s")]
[SerializeField] private float obstacleMoveSpeed = 5f;
- [Tooltip("Time between obstacle spawns in seconds")]
- [SerializeField] private float obstacleSpawnInterval = 2f;
+ [Tooltip("Obstacle spawning configuration (pools, timing, difficulty)")]
+ [SerializeField] private ObstacleSpawnConfig obstacleSpawnConfiguration;
[Tooltip("X position where obstacles spawn (off-screen right)")]
[SerializeField] private float obstacleSpawnXPosition = 12f;
@@ -71,7 +72,7 @@ namespace Core.Settings
public float MaxRotationAngle => maxRotationAngle;
public float RotationSpeed => rotationSpeed;
public float ObstacleMoveSpeed => obstacleMoveSpeed;
- public float ObstacleSpawnInterval => obstacleSpawnInterval;
+ public ObstacleSpawnConfig ObstacleSpawnConfiguration => obstacleSpawnConfiguration;
public float ObstacleSpawnXPosition => obstacleSpawnXPosition;
public float ObstacleDestroyXPosition => obstacleDestroyXPosition;
public float ObstacleMinSpawnY => obstacleMinSpawnY;
@@ -91,9 +92,14 @@ namespace Core.Settings
maxFallSpeed = Mathf.Max(0f, maxFallSpeed);
maxRotationAngle = Mathf.Clamp(maxRotationAngle, 0f, 90f);
rotationSpeed = Mathf.Max(0.1f, rotationSpeed);
- obstacleSpawnInterval = Mathf.Max(0.1f, obstacleSpawnInterval);
targetMoveSpeed = Mathf.Max(0.1f, targetMoveSpeed);
targetSpawnInterval = Mathf.Max(0.1f, targetSpawnInterval);
+
+ // Validate obstacle spawn configuration
+ if (obstacleSpawnConfiguration != null)
+ {
+ obstacleSpawnConfiguration.Validate();
+ }
}
}
}
diff --git a/Assets/Scripts/Core/Settings/IBirdPooperSettings.cs b/Assets/Scripts/Core/Settings/IBirdPooperSettings.cs
index 43e3b388..a67f8fc8 100644
--- a/Assets/Scripts/Core/Settings/IBirdPooperSettings.cs
+++ b/Assets/Scripts/Core/Settings/IBirdPooperSettings.cs
@@ -1,8 +1,10 @@
-namespace Core.Settings
+using Minigames.BirdPooper;
+
+namespace Core.Settings
{
///
/// Settings interface for Bird Pooper minigame.
- /// Accessed via GameManager.GetSettingsObject()
+ /// Accessed via GameManager.GetSettingsObject of IBirdPooperSettings
///
public interface IBirdPooperSettings
{
@@ -19,12 +21,14 @@
// Obstacles
float ObstacleMoveSpeed { get; }
- float ObstacleSpawnInterval { get; }
float ObstacleSpawnXPosition { get; }
float ObstacleDestroyXPosition { get; }
float ObstacleMinSpawnY { get; }
float ObstacleMaxSpawnY { get; }
+ // Obstacle Spawning Configuration
+ ObstacleSpawnConfig ObstacleSpawnConfiguration { get; }
+
// Poop Projectile
float PoopFallSpeed { get; }
float PoopDestroyYPosition { get; }
diff --git a/Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs b/Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs
index 401d31a0..1b36be50 100644
--- a/Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs
+++ b/Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs
@@ -15,11 +15,12 @@ namespace Minigames.BirdPooper
public UnityEngine.Events.UnityEvent OnFlap;
public UnityEngine.Events.UnityEvent OnPlayerDamaged;
- private Rigidbody2D rb;
- private IBirdPooperSettings settings;
- private float verticalVelocity = 0f;
- private bool isDead = false;
- private float fixedXPosition; // Store the initial X position from the scene
+ private Rigidbody2D _rb;
+ private IBirdPooperSettings _settings;
+ private float _verticalVelocity;
+ private bool _isDead;
+ private float _fixedXPosition; // Store the initial X position from the scene
+ private bool _isInitialized; // Flag to control when physics/input are active
internal override void OnManagedAwake()
{
@@ -31,33 +32,49 @@ namespace Minigames.BirdPooper
if (OnPlayerDamaged == null)
OnPlayerDamaged = new UnityEngine.Events.UnityEvent();
- // Load settings
- settings = GameManager.GetSettingsObject();
- if (settings == null)
- {
- Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!");
- return;
- }
-
- // Get Rigidbody2D component (Dynamic with gravityScale = 0)
- rb = GetComponent();
- if (rb != null)
- {
- rb.gravityScale = 0f; // Disable Unity physics gravity
- rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces
-
- // Store the initial X position from the scene
- fixedXPosition = rb.position.x;
- Debug.Log($"[BirdPlayerController] Fixed X position set to: {fixedXPosition}");
- }
- else
+ // Only cache component references - NO setup yet
+ _rb = GetComponent();
+ if (_rb == null)
{
Debug.LogError("[BirdPlayerController] Rigidbody2D component not found!");
- return;
}
- // Register as default consumer (gets input if nothing else consumes it)
- // This allows UI buttons to work while still flapping when tapping empty space
+ // Load settings
+ _settings = GameManager.GetSettingsObject();
+ if (_settings == null)
+ {
+ Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!");
+ }
+
+ Debug.Log("[BirdPlayerController] References cached, waiting for initialization...");
+ }
+
+ ///
+ /// Initializes the player controller - enables physics and input.
+ /// Should be called by BirdPooperGameManager when ready to start the game.
+ ///
+ public void Initialize()
+ {
+ if (_isInitialized)
+ {
+ Debug.LogWarning("[BirdPlayerController] Already initialized!");
+ return;
+ }
+
+ if (_rb == null || _settings == null)
+ {
+ Debug.LogError("[BirdPlayerController] Cannot initialize - missing references!");
+ return;
+ }
+
+ // Setup physics
+ _rb.gravityScale = 0f; // Disable Unity physics gravity
+ _rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces
+
+ // Store the initial X position from the scene
+ _fixedXPosition = _rb.position.x;
+
+ // Register as default input consumer
if (Input.InputManager.Instance != null)
{
Input.InputManager.Instance.SetDefaultConsumer(this);
@@ -67,52 +84,74 @@ namespace Minigames.BirdPooper
{
Debug.LogError("[BirdPlayerController] InputManager instance not found!");
}
+
+ _isInitialized = true;
+ Debug.Log($"[BirdPlayerController] Initialized! Fixed X position: {_fixedXPosition}");
}
private void Update()
{
- if (!isDead && settings != null && rb != null)
- {
- // Apply manual gravity
- verticalVelocity -= settings.Gravity * Time.deltaTime;
-
- // Cap fall speed (terminal velocity)
- if (verticalVelocity < -settings.MaxFallSpeed)
- verticalVelocity = -settings.MaxFallSpeed;
-
- // Update position manually
- Vector2 newPosition = rb.position;
- newPosition.y += verticalVelocity * Time.deltaTime;
- newPosition.x = fixedXPosition; // Keep X fixed at scene-configured position
-
- // Clamp Y position to bounds
- newPosition.y = Mathf.Clamp(newPosition.y, settings.MinY, settings.MaxY);
-
- rb.MovePosition(newPosition);
-
- // Update rotation based on velocity
- UpdateRotation();
- }
+ // Only run physics/movement if initialized
+ if (!_isInitialized || _isDead || _settings == null || _rb == null)
+ return;
+
+ // Apply manual gravity
+ _verticalVelocity -= _settings.Gravity * Time.deltaTime;
+
+ // Cap fall speed (terminal velocity)
+ if (_verticalVelocity < -_settings.MaxFallSpeed)
+ _verticalVelocity = -_settings.MaxFallSpeed;
+
+ // Update position manually
+ Vector2 newPosition = _rb.position;
+ newPosition.y += _verticalVelocity * Time.deltaTime;
+ newPosition.x = _fixedXPosition; // Keep X fixed at scene-configured position
+
+ // Clamp Y position to bounds
+ newPosition.y = Mathf.Clamp(newPosition.y, _settings.MinY, _settings.MaxY);
+
+ _rb.MovePosition(newPosition);
+
+ // Update rotation based on velocity
+ UpdateRotation();
}
#region ITouchInputConsumer Implementation
public void OnTap(Vector2 tapPosition)
{
- if (!isDead && settings != null)
- {
- verticalVelocity = settings.FlapForce;
- Debug.Log($"[BirdPlayerController] Flap! velocity = {verticalVelocity}");
-
- // Emit flap event
- OnFlap?.Invoke();
- }
+ // Only respond to input if initialized and alive
+ if (!_isInitialized || _isDead || _settings == null)
+ return;
+
+ Flap();
}
public void OnHoldStart(Vector2 position) { }
public void OnHoldMove(Vector2 position) { }
public void OnHoldEnd(Vector2 position) { }
+ #endregion
+
+ #region Player Actions
+
+ ///
+ /// Makes the bird flap, applying upward velocity.
+ /// Can be called by input system or externally (e.g., for first tap).
+ ///
+ public void Flap()
+ {
+ if (!_isInitialized || _isDead || _settings == null)
+ return;
+
+ _verticalVelocity = _settings.FlapForce;
+ Debug.Log($"[BirdPlayerController] Flap! velocity = {_verticalVelocity}");
+
+ // Emit flap event
+ OnFlap?.Invoke();
+ }
+
+
#endregion
#region Rotation
@@ -123,19 +162,19 @@ namespace Minigames.BirdPooper
///
private void UpdateRotation()
{
- if (settings == null) return;
+ if (_settings == null) return;
// Map velocity to rotation angle
// When falling at max speed (-MaxFallSpeed): -MaxRotationAngle (down)
// When at flap velocity (+FlapForce): +MaxRotationAngle (up)
float velocityPercent = Mathf.InverseLerp(
- -settings.MaxFallSpeed,
- settings.FlapForce,
- verticalVelocity
+ -_settings.MaxFallSpeed,
+ _settings.FlapForce,
+ _verticalVelocity
);
float targetAngle = Mathf.Lerp(
- -settings.MaxRotationAngle,
- settings.MaxRotationAngle,
+ -_settings.MaxRotationAngle,
+ _settings.MaxRotationAngle,
velocityPercent
);
@@ -148,7 +187,7 @@ namespace Minigames.BirdPooper
float smoothedAngle = Mathf.Lerp(
currentAngle,
targetAngle,
- settings.RotationSpeed * Time.deltaTime
+ _settings.RotationSpeed * Time.deltaTime
);
// Apply rotation to Z axis only (2D rotation)
@@ -175,10 +214,10 @@ namespace Minigames.BirdPooper
private void HandleDeath()
{
// Only process death once
- if (isDead) return;
+ if (_isDead) return;
- isDead = true;
- verticalVelocity = 0f;
+ _isDead = true;
+ _verticalVelocity = 0f;
Debug.Log("[BirdPlayerController] Bird died!");
// Emit damage event - let the game manager handle UI
@@ -187,9 +226,9 @@ namespace Minigames.BirdPooper
#endregion
- #region Public Properties
+ #region Public Accessors
- public bool IsDead => isDead;
+ public bool IsDead => _isDead;
#endregion
}
diff --git a/Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs b/Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs
index 476dfa46..c3037092 100644
--- a/Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs
+++ b/Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs
@@ -17,12 +17,13 @@ namespace Minigames.BirdPooper
[SerializeField] private BirdPlayerController player;
[SerializeField] private ObstacleSpawner obstacleSpawner;
[SerializeField] private TargetSpawner targetSpawner;
+ [SerializeField] private TapToStartController tapToStartController;
[SerializeField] private GameOverScreen gameOverScreen;
[SerializeField] private GameObject poopPrefab;
[Header("Game State")]
- private int targetsHit;
- private bool isGameOver;
+ private int _targetsHit;
+ private bool _isGameOver;
[Header("Input")]
[Tooltip("Minimum seconds between consecutive poop spawns")]
@@ -60,6 +61,11 @@ namespace Minigames.BirdPooper
Debug.LogWarning("[BirdPooperGameManager] TargetSpawner reference not assigned! Targets will not spawn.");
}
+ if (tapToStartController == null)
+ {
+ Debug.LogError("[BirdPooperGameManager] TapToStartController reference not assigned!");
+ }
+
if (gameOverScreen == null)
{
Debug.LogError("[BirdPooperGameManager] GameOverScreen reference not assigned!");
@@ -76,34 +82,76 @@ namespace Minigames.BirdPooper
}
}
- internal override void OnManagedStart()
+ ///
+ /// Called after scene is fully loaded and any save data is restored.
+ /// Activates tap-to-start UI instead of starting immediately.
+ ///
+ internal override void OnSceneRestoreCompleted()
{
- base.OnManagedStart();
+ base.OnSceneRestoreCompleted();
+ Debug.Log("[BirdPooperGameManager] Scene fully loaded, activating tap-to-start...");
+
+ if (tapToStartController != null)
+ {
+ tapToStartController.Activate();
+ }
+ else
+ {
+ Debug.LogError("[BirdPooperGameManager] TapToStartController missing! Starting game immediately as fallback.");
+ BeginMinigame();
+ }
+ }
+
+ ///
+ /// Central method to begin the minigame.
+ /// Initializes player, starts spawners, and sets up game state.
+ ///
+ public void BeginMinigame()
+ {
// Initialize game state
- isGameOver = false;
- targetsHit = 0;
+ _isGameOver = false;
+ _targetsHit = 0;
- // Subscribe to player events
+ // Initialize and enable player
if (player != null)
{
+ player.Initialize();
player.OnPlayerDamaged.AddListener(HandlePlayerDamaged);
- Debug.Log("[BirdPooperGameManager] Subscribed to player damaged event");
+
+ // Make bird do initial flap so first tap feels responsive
+ player.Flap();
+
+ Debug.Log("[BirdPooperGameManager] Player initialized and event subscribed");
+ }
+ else
+ {
+ Debug.LogError("[BirdPooperGameManager] Cannot begin minigame - player reference missing!");
}
// Start obstacle spawning
if (obstacleSpawner != null)
{
obstacleSpawner.StartSpawning();
- Debug.Log("[BirdPooperGameManager] Started obstacle spawning");
+ Debug.Log("[BirdPooperGameManager] Obstacle spawner started");
+ }
+ else
+ {
+ Debug.LogError("[BirdPooperGameManager] Cannot begin minigame - obstacle spawner reference missing!");
}
// Start target spawning
if (targetSpawner != null)
{
targetSpawner.StartSpawning();
- Debug.Log("[BirdPooperGameManager] Started target spawning");
+ Debug.Log("[BirdPooperGameManager] Target spawner started");
}
+ else
+ {
+ Debug.LogWarning("[BirdPooperGameManager] Target spawner reference missing - targets will not spawn");
+ }
+
+ Debug.Log("[BirdPooperGameManager] ✅ Minigame started successfully!");
}
internal override void OnManagedDestroy()
@@ -129,10 +177,10 @@ namespace Minigames.BirdPooper
///
private void HandlePlayerDamaged()
{
- if (isGameOver) return;
+ if (_isGameOver) return;
- isGameOver = true;
- Debug.Log($"[BirdPooperGameManager] Player damaged - Game Over! Targets Hit: {targetsHit}");
+ _isGameOver = true;
+ Debug.Log($"[BirdPooperGameManager] Player damaged - Game Over! Targets Hit: {_targetsHit}");
// Stop spawning obstacles
if (obstacleSpawner != null)
@@ -167,7 +215,7 @@ namespace Minigames.BirdPooper
if (Time.time < _lastPoopTime + poopCooldown)
return;
- if (isGameOver || player == null || poopPrefab == null)
+ if (_isGameOver || player == null || poopPrefab == null)
return;
Vector3 spawnPosition = player.transform.position;
@@ -183,16 +231,17 @@ namespace Minigames.BirdPooper
///
public void OnTargetHit()
{
- if (isGameOver) return;
+ if (_isGameOver) return;
- targetsHit++;
- Debug.Log($"[BirdPooperGameManager] Target Hit! Total: {targetsHit}");
+ _targetsHit++;
+ Debug.Log($"[BirdPooperGameManager] Target Hit! Total: {_targetsHit}");
}
#region Public Accessors
- public bool IsGameOver => isGameOver;
- public int TargetsHit => targetsHit;
+ public bool IsGameOver => _isGameOver;
+ public int TargetsHit => _targetsHit;
+ public BirdPlayerController Player => player;
#endregion
}
diff --git a/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs
new file mode 100644
index 00000000..516b60ec
--- /dev/null
+++ b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs
@@ -0,0 +1,111 @@
+using System;
+using UnityEngine;
+
+namespace Minigames.BirdPooper
+{
+ ///
+ /// Container for a pool of obstacle prefabs at a specific difficulty tier.
+ /// Pools are ordered by difficulty, with pool[0] being the easiest.
+ ///
+ [Serializable]
+ public class ObstaclePool
+ {
+ [Tooltip("Obstacles in this difficulty tier")]
+ public GameObject[] obstacles;
+ }
+
+ ///
+ /// Configuration for obstacle spawning in Bird Pooper minigame.
+ /// Includes difficulty pools, spawn timing, and diversity settings.
+ ///
+ [Serializable]
+ public class ObstacleSpawnConfig
+ {
+ [Header("Difficulty Pools")]
+ [Tooltip("Obstacle pools ordered by difficulty (pool[0] = easiest, always active)")]
+ public ObstaclePool[] obstaclePools;
+
+ [Tooltip("Times (in seconds) when each additional pool unlocks. Length should be obstaclePools.Length - 1. At poolUnlockTimes[i], pool[i+1] becomes available.")]
+ public float[] poolUnlockTimes;
+
+ [Header("Spawn Timing")]
+ [Tooltip("Minimum interval between spawns (seconds) - represents high difficulty")]
+ public float minSpawnInterval = 1f;
+
+ [Tooltip("Maximum interval between spawns (seconds) - represents low difficulty")]
+ public float maxSpawnInterval = 2f;
+
+ [Header("Difficulty Scaling")]
+ [Tooltip("Time in seconds for difficulty to ramp from 0 to 1")]
+ public float difficultyRampDuration = 60f;
+
+ [Tooltip("Curve mapping normalized time (0..1) to difficulty (0..1). Default is linear.")]
+ public AnimationCurve difficultyCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
+
+ [Tooltip("Maximum jitter applied to computed interval (fractional). 0.1 = +/-10% jitter")]
+ public float intervalJitter = 0.05f;
+
+ [Header("Recency / Diversity")]
+ [Tooltip("Time in seconds it takes for a recently-used prefab to recover back to full weight")]
+ public float recentDecayDuration = 10f;
+
+ [Tooltip("Minimum weight (0..1) applied to a just-used prefab so it can still appear occasionally")]
+ [Range(0f, 1f)]
+ public float minRecentWeight = 0.05f;
+
+ ///
+ /// Validates the configuration and logs warnings for invalid settings.
+ ///
+ public void Validate()
+ {
+ // Validate pools
+ if (obstaclePools == null || obstaclePools.Length == 0)
+ {
+ Debug.LogError("[ObstacleSpawnConfig] No obstacle pools defined!");
+ return;
+ }
+
+ // Validate pool unlock times
+ int expectedUnlockTimes = obstaclePools.Length - 1;
+ if (poolUnlockTimes == null)
+ {
+ Debug.LogWarning($"[ObstacleSpawnConfig] poolUnlockTimes is null. Expected {expectedUnlockTimes} entries. Only pool[0] will be available.");
+ }
+ else if (poolUnlockTimes.Length != expectedUnlockTimes)
+ {
+ Debug.LogWarning($"[ObstacleSpawnConfig] poolUnlockTimes.Length ({poolUnlockTimes.Length}) does not match expected value ({expectedUnlockTimes}). Should be obstaclePools.Length - 1.");
+ }
+
+ // Validate spawn intervals
+ if (minSpawnInterval < 0f)
+ {
+ Debug.LogWarning("[ObstacleSpawnConfig] minSpawnInterval is negative. Clamping to 0.");
+ }
+ if (maxSpawnInterval < 0f)
+ {
+ Debug.LogWarning("[ObstacleSpawnConfig] maxSpawnInterval is negative. Clamping to 0.");
+ }
+ if (minSpawnInterval > maxSpawnInterval)
+ {
+ Debug.LogWarning("[ObstacleSpawnConfig] minSpawnInterval is greater than maxSpawnInterval. Values should be swapped.");
+ }
+
+ // Validate difficulty ramp
+ if (difficultyRampDuration < 0.01f)
+ {
+ Debug.LogWarning("[ObstacleSpawnConfig] difficultyRampDuration is too small. Should be at least 0.01.");
+ }
+
+ // Validate recency settings
+ if (recentDecayDuration < 0.01f)
+ {
+ Debug.LogWarning("[ObstacleSpawnConfig] recentDecayDuration is too small. Should be at least 0.01.");
+ }
+ if (minRecentWeight < 0f || minRecentWeight > 1f)
+ {
+ Debug.LogWarning("[ObstacleSpawnConfig] minRecentWeight should be between 0 and 1.");
+ }
+ }
+ }
+}
+
diff --git a/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs.meta b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs.meta
new file mode 100644
index 00000000..0a45fee2
--- /dev/null
+++ b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 74f4387c76774225afa2d02d590d5ad4
+timeCreated: 1765918010
\ No newline at end of file
diff --git a/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs
index efffb54b..cfa2f0db 100644
--- a/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs
+++ b/Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs
@@ -4,6 +4,7 @@ using Core.Settings;
using Core.Lifecycle;
using AppleHillsCamera;
using System.Text;
+using System.Collections.Generic;
namespace Minigames.BirdPooper
{
@@ -11,6 +12,7 @@ namespace Minigames.BirdPooper
/// Spawns obstacles at regular intervals for Bird Pooper minigame.
/// Uses Transform references for spawn and despawn positions instead of hardcoded values.
/// All obstacles are spawned at Y = 0 (prefabs should be authored accordingly).
+ /// Supports dynamic difficulty pools that unlock over time.
///
public class ObstacleSpawner : ManagedBehaviour
{
@@ -20,6 +22,9 @@ namespace Minigames.BirdPooper
[Tooltip("Transform marking where obstacles despawn (off-screen left)")]
[SerializeField] private Transform despawnPoint;
+
+ [Tooltip("Optional parent transform for spawned obstacles (for scene organization)")]
+ [SerializeField] private Transform obstacleContainer;
[Header("EdgeAnchor References")]
[Tooltip("ScreenReferenceMarker to pass to spawned obstacles")]
@@ -28,55 +33,45 @@ namespace Minigames.BirdPooper
[Tooltip("CameraScreenAdapter to pass to spawned obstacles")]
[SerializeField] private CameraScreenAdapter cameraAdapter;
- [Header("Obstacle Prefabs")]
- [Tooltip("Array of obstacle prefabs to spawn randomly")]
- [SerializeField] private GameObject[] obstaclePrefabs;
-
- [Header("Spawn Timing")]
- [Tooltip("Minimum interval between spawns (seconds)")]
- [SerializeField] private float minSpawnInterval = 1f;
- [Tooltip("Maximum interval between spawns (seconds)")]
- [SerializeField] private float maxSpawnInterval = 2f;
-
- [Header("Difficulty")]
- [Tooltip("Time in seconds for difficulty to ramp from 0 to 1")]
- [SerializeField] private float difficultyRampDuration = 60f;
- [Tooltip("Curve mapping normalized time (0..1) to difficulty (0..1). Default is linear.")]
- [SerializeField] private AnimationCurve difficultyCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
- [Tooltip("Maximum jitter applied to computed interval (fractional). 0.1 = +/-10% jitter")]
- [SerializeField] private float intervalJitter = 0.05f;
-
- [Header("Recency / Diversity")]
- [Tooltip("Time in seconds it takes for a recently-used prefab to recover back to full weight")]
- [SerializeField] private float recentDecayDuration = 10f;
- [Tooltip("Minimum weight (0..1) applied to a just-used prefab so it can still appear occasionally")]
- [Range(0f, 1f)]
- [SerializeField] private float minRecentWeight = 0.05f;
-
- private IBirdPooperSettings settings;
- private float spawnTimer;
- private bool isSpawning;
+ private IBirdPooperSettings _settings;
+ private ObstacleSpawnConfig _spawnConfig;
+ private float _spawnTimer;
+ private bool _isSpawning;
private float _currentSpawnInterval = 1f;
- // difficulty tracking
- private float _elapsedTime = 0f;
+ // Difficulty tracking
+ private float _elapsedTime;
- // recency tracking
+ // Master obstacle list for recency tracking
+ private List _allObstacles;
+ private Dictionary _obstacleToGlobalIndex;
private float[] _lastUsedTimes;
- internal override void OnManagedAwake()
+ ///
+ /// Initializes the obstacle spawner by loading settings, validating references, and building obstacle pools.
+ /// Should be called once before spawning begins.
+ ///
+ private void Initialize()
{
- base.OnManagedAwake();
-
// Load settings
- settings = GameManager.GetSettingsObject();
- if (settings == null)
+ _settings = GameManager.GetSettingsObject();
+ if (_settings == null)
{
- Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found!");
- // continue — we now use min/max interval fields instead of relying on settings
+ Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found! Cannot initialize.");
+ return;
}
- // Validate references
+ _spawnConfig = _settings.ObstacleSpawnConfiguration;
+ if (_spawnConfig == null)
+ {
+ Debug.LogError("[ObstacleSpawner] ObstacleSpawnConfiguration not found in settings! Cannot initialize.");
+ return;
+ }
+
+ // Validate spawn configuration
+ _spawnConfig.Validate();
+
+ // Validate scene references
if (spawnPoint == null)
{
Debug.LogError("[ObstacleSpawner] Spawn Point not assigned! Please assign a Transform in the Inspector.");
@@ -87,11 +82,6 @@ namespace Minigames.BirdPooper
Debug.LogError("[ObstacleSpawner] Despawn Point not assigned! Please assign a Transform in the Inspector.");
}
- if (obstaclePrefabs == null || obstaclePrefabs.Length == 0)
- {
- Debug.LogError("[ObstacleSpawner] No obstacle prefabs assigned! Please assign at least one prefab in the Inspector.");
- }
-
if (referenceMarker == null)
{
Debug.LogError("[ObstacleSpawner] ScreenReferenceMarker not assigned! Obstacles need this for EdgeAnchor positioning.");
@@ -102,58 +92,100 @@ namespace Minigames.BirdPooper
Debug.LogWarning("[ObstacleSpawner] CameraScreenAdapter not assigned. EdgeAnchor will attempt to auto-find camera.");
}
- // Validate interval range
- if (minSpawnInterval < 0f) minSpawnInterval = 0f;
- if (maxSpawnInterval < 0f) maxSpawnInterval = 0f;
- if (minSpawnInterval > maxSpawnInterval)
+ // Build master obstacle list from all pools
+ BuildMasterObstacleList();
+
+ Debug.Log("[ObstacleSpawner] Initialized successfully with pool-based difficulty scaling");
+ }
+
+ ///
+ /// Builds a master list of all obstacles across all pools and creates index mappings for recency tracking.
+ ///
+ private void BuildMasterObstacleList()
+ {
+ _allObstacles = new List();
+ _obstacleToGlobalIndex = new Dictionary();
+
+ if (_spawnConfig.obstaclePools == null || _spawnConfig.obstaclePools.Length == 0)
{
- float tmp = minSpawnInterval;
- minSpawnInterval = maxSpawnInterval;
- maxSpawnInterval = tmp;
- Debug.LogWarning("[ObstacleSpawner] minSpawnInterval was greater than maxSpawnInterval. Values were swapped.");
+ Debug.LogError("[ObstacleSpawner] No obstacle pools defined in configuration!");
+ return;
}
- // Clamp ramp duration
- if (difficultyRampDuration < 0.01f) difficultyRampDuration = 0.01f;
+ int globalIndex = 0;
+ for (int poolIdx = 0; poolIdx < _spawnConfig.obstaclePools.Length; poolIdx++)
+ {
+ ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx];
+ if (pool == null || pool.obstacles == null || pool.obstacles.Length == 0)
+ {
+ Debug.LogWarning($"[ObstacleSpawner] Pool[{poolIdx}] is empty or null!");
+ continue;
+ }
- // Clamp recency
- if (recentDecayDuration < 0.01f) recentDecayDuration = 0.01f;
- if (minRecentWeight < 0f) minRecentWeight = 0f;
- if (minRecentWeight > 1f) minRecentWeight = 1f;
+ foreach (GameObject prefab in pool.obstacles)
+ {
+ if (prefab == null)
+ {
+ Debug.LogWarning($"[ObstacleSpawner] Null prefab found in pool[{poolIdx}]");
+ continue;
+ }
- // Initialize last-used timestamps so prefabs start available (set to sufficiently negative so they appear with full weight)
- int n = obstaclePrefabs != null ? obstaclePrefabs.Length : 0;
- _lastUsedTimes = new float[n];
- float initTime = -recentDecayDuration - 1f;
- for (int i = 0; i < n; i++) _lastUsedTimes[i] = initTime;
+ // Allow duplicates - same prefab can appear in multiple pools
+ if (!_obstacleToGlobalIndex.ContainsKey(prefab))
+ {
+ _obstacleToGlobalIndex[prefab] = globalIndex;
+ _allObstacles.Add(prefab);
+ globalIndex++;
+ }
+ }
+ }
- Debug.Log("[ObstacleSpawner] Initialized successfully");
+ // Initialize recency tracking
+ int totalObstacles = _allObstacles.Count;
+ _lastUsedTimes = new float[totalObstacles];
+ float initTime = Time.time - _spawnConfig.recentDecayDuration - 1f;
+ for (int i = 0; i < totalObstacles; i++)
+ {
+ _lastUsedTimes[i] = initTime;
+ }
+
+ // Log pool configuration
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine($"[ObstacleSpawner] Loaded {_spawnConfig.obstaclePools.Length} obstacle pools with {totalObstacles} unique obstacles:");
+ for (int i = 0; i < _spawnConfig.obstaclePools.Length; i++)
+ {
+ ObstaclePool pool = _spawnConfig.obstaclePools[i];
+ int obstacleCount = pool != null && pool.obstacles != null ? pool.obstacles.Length : 0;
+ float unlockTime = (i == 0) ? 0f : (_spawnConfig.poolUnlockTimes != null && i - 1 < _spawnConfig.poolUnlockTimes.Length ? _spawnConfig.poolUnlockTimes[i - 1] : -1f);
+ sb.AppendLine($" Pool[{i}]: {obstacleCount} obstacles, unlocks at {unlockTime}s");
+ }
+ Debug.Log(sb.ToString());
}
private void Update()
{
- if (!isSpawning || spawnPoint == null) return;
+ if (!_isSpawning || spawnPoint == null || _spawnConfig == null) return;
- spawnTimer += Time.deltaTime;
+ _spawnTimer += Time.deltaTime;
_elapsedTime += Time.deltaTime;
- if (spawnTimer >= _currentSpawnInterval)
+ if (_spawnTimer >= _currentSpawnInterval)
{
SpawnObstacle();
- spawnTimer = 0f;
+ _spawnTimer = 0f;
- // pick next interval based on difficulty ramp
- float t = Mathf.Clamp01(_elapsedTime / difficultyRampDuration);
- float difficulty = difficultyCurve.Evaluate(t); // 0..1
+ // Pick next interval based on difficulty ramp
+ float t = Mathf.Clamp01(_elapsedTime / _spawnConfig.difficultyRampDuration);
+ float difficulty = _spawnConfig.difficultyCurve.Evaluate(t); // 0..1
- // map difficulty to interval: difficulty 0 -> max interval (easy), 1 -> min interval (hard)
- float baseInterval = Mathf.Lerp(maxSpawnInterval, minSpawnInterval, difficulty);
+ // Map difficulty to interval: difficulty 0 -> max interval (easy), 1 -> min interval (hard)
+ float baseInterval = Mathf.Lerp(_spawnConfig.maxSpawnInterval, _spawnConfig.minSpawnInterval, difficulty);
- // apply small jitter
- if (intervalJitter > 0f)
+ // Apply small jitter
+ if (_spawnConfig.intervalJitter > 0f)
{
- float jitter = Random.Range(-intervalJitter, intervalJitter);
+ float jitter = Random.Range(-_spawnConfig.intervalJitter, _spawnConfig.intervalJitter);
_currentSpawnInterval = Mathf.Max(0f, baseInterval * (1f + jitter));
}
else
@@ -167,14 +199,14 @@ namespace Minigames.BirdPooper
}
///
- /// Spawn a random obstacle at the spawn point position (Y = 0).
+ /// Spawn a random obstacle from currently unlocked pools at the spawn point position (Y = 0).
/// Uses timestamp/decay weighting so prefabs used recently are less likely.
///
private void SpawnObstacle()
{
- if (obstaclePrefabs == null || obstaclePrefabs.Length == 0)
+ if (_spawnConfig == null || _spawnConfig.obstaclePools == null || _spawnConfig.obstaclePools.Length == 0)
{
- Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs to spawn!");
+ Debug.LogWarning("[ObstacleSpawner] No obstacle pools configured!");
return;
}
@@ -184,53 +216,133 @@ namespace Minigames.BirdPooper
return;
}
- int count = obstaclePrefabs.Length;
-
- // Defensive: ensure _lastUsedTimes is initialized and matches prefab count
- if (_lastUsedTimes == null || _lastUsedTimes.Length != count)
+ // Determine which pools are currently unlocked based on elapsed time
+ int unlockedPoolCount = 1; // Pool[0] is always unlocked
+ if (_spawnConfig.poolUnlockTimes != null)
{
- _lastUsedTimes = new float[count];
- float initTime = Time.time - recentDecayDuration - 1f;
- for (int i = 0; i < count; i++) _lastUsedTimes[i] = initTime;
+ for (int i = 0; i < _spawnConfig.poolUnlockTimes.Length; i++)
+ {
+ if (_elapsedTime >= _spawnConfig.poolUnlockTimes[i])
+ {
+ unlockedPoolCount = i + 2; // +2 because we're unlocking pool[i+1]
+ }
+ else
+ {
+ break; // Times should be in order, so stop when we hit a future unlock
+ }
+ }
}
- // compute weights based on recency (newer = lower weight)
- float[] weights = new float[count];
+ // Clamp to available pools
+ unlockedPoolCount = Mathf.Min(unlockedPoolCount, _spawnConfig.obstaclePools.Length);
+
+ // Build list of available obstacles from unlocked pools
+ List availableObstacles = new List();
+ List availableGlobalIndices = new List();
+
+ for (int poolIdx = 0; poolIdx < unlockedPoolCount; poolIdx++)
+ {
+ ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx];
+ if (pool == null || pool.obstacles == null) continue;
+
+ foreach (GameObject prefab in pool.obstacles)
+ {
+ if (prefab == null) continue;
+
+ // Add to available list (duplicates allowed if same prefab is in multiple pools)
+ availableObstacles.Add(prefab);
+
+ // Look up global index for recency tracking
+ if (_obstacleToGlobalIndex.TryGetValue(prefab, out int globalIdx))
+ {
+ availableGlobalIndices.Add(globalIdx);
+ }
+ else
+ {
+ Debug.LogWarning($"[ObstacleSpawner] Prefab '{prefab.name}' not found in global index!");
+ availableGlobalIndices.Add(-1); // Invalid index
+ }
+ }
+ }
+
+ if (availableObstacles.Count == 0)
+ {
+ Debug.LogWarning($"[ObstacleSpawner] No obstacles available in unlocked pools (0..{unlockedPoolCount-1})");
+ return;
+ }
+
+ // Compute weights based on recency
+ float[] weights = new float[availableObstacles.Count];
float now = Time.time;
- for (int i = 0; i < count; i++)
+
+ for (int i = 0; i < availableObstacles.Count; i++)
{
- float age = now - _lastUsedTimes[i];
- float normalized = Mathf.Clamp01(age / recentDecayDuration); // 0 = just used, 1 = fully recovered
- float weight = Mathf.Max(minRecentWeight, normalized); // ensure minimum probability
- weights[i] = weight; // base weight = 1.0, could be extended to per-prefab weights
+ int globalIdx = availableGlobalIndices[i];
+ if (globalIdx < 0 || globalIdx >= _lastUsedTimes.Length)
+ {
+ weights[i] = 1f; // Default weight for invalid indices
+ continue;
+ }
+
+ float age = now - _lastUsedTimes[globalIdx];
+ float normalized = Mathf.Clamp01(age / _spawnConfig.recentDecayDuration); // 0 = just used, 1 = fully recovered
+ float weight = Mathf.Max(_spawnConfig.minRecentWeight, normalized);
+ weights[i] = weight;
}
- // compute probabilities for logging
+ // Compute and log probabilities for debugging
float totalW = 0f;
- for (int i = 0; i < count; i++) totalW += Mathf.Max(0f, weights[i]);
+ for (int i = 0; i < availableObstacles.Count; i++)
+ {
+ totalW += Mathf.Max(0f, weights[i]);
+ }
+
if (totalW > 0f)
{
- var sb = new StringBuilder();
- sb.Append("[ObstacleSpawner] Prefab pick probabilities: ");
- for (int i = 0; i < count; i++)
+ StringBuilder sb = new StringBuilder();
+ sb.Append($"[ObstacleSpawner] Spawning from pools 0-{unlockedPoolCount-1}. Probabilities: ");
+ for (int i = 0; i < availableObstacles.Count; i++)
{
float p = weights[i] / totalW;
- string name = obstaclePrefabs[i] != null ? obstaclePrefabs[i].name : i.ToString();
- sb.AppendFormat("{0}:{1:P1}", name, p);
- if (i < count - 1) sb.Append(", ");
+ string prefabName = availableObstacles[i] != null ? availableObstacles[i].name : i.ToString();
+ sb.AppendFormat("{0}:{1:P1}", prefabName, p);
+ if (i < availableObstacles.Count - 1) sb.Append(", ");
}
Debug.Log(sb.ToString());
}
+ // Select obstacle using weighted random
int chosenIndex = WeightedPickIndex(weights);
- GameObject selectedPrefab = obstaclePrefabs[chosenIndex];
+ GameObject selectedPrefab = availableObstacles[chosenIndex];
+ int selectedGlobalIndex = availableGlobalIndices[chosenIndex];
- // record usage timestamp
- _lastUsedTimes[chosenIndex] = Time.time;
+ // Record usage timestamp for recency tracking
+ if (selectedGlobalIndex >= 0 && selectedGlobalIndex < _lastUsedTimes.Length)
+ {
+ _lastUsedTimes[selectedGlobalIndex] = Time.time;
+ }
+
+ // Determine which pool this obstacle came from (for logging)
+ int sourcePool = -1;
+ for (int poolIdx = 0; poolIdx < unlockedPoolCount; poolIdx++)
+ {
+ ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx];
+ if (pool != null && pool.obstacles != null && System.Array.IndexOf(pool.obstacles, selectedPrefab) >= 0)
+ {
+ sourcePool = poolIdx;
+ break;
+ }
+ }
// Spawn at spawn point position with Y = 0
Vector3 spawnPosition = new Vector3(spawnPoint.position.x, 0f, 0f);
GameObject obstacleObj = Instantiate(selectedPrefab, spawnPosition, Quaternion.identity);
+
+ // Parent to container if provided
+ if (obstacleContainer != null)
+ {
+ obstacleObj.transform.SetParent(obstacleContainer, true);
+ }
// Initialize obstacle with despawn X position and EdgeAnchor references
Obstacle obstacle = obstacleObj.GetComponent();
@@ -244,7 +356,7 @@ namespace Minigames.BirdPooper
Destroy(obstacleObj);
}
- Debug.Log($"[ObstacleSpawner] Spawned obstacle '{selectedPrefab.name}' at position {spawnPosition}");
+ Debug.Log($"[ObstacleSpawner] Spawned '{selectedPrefab.name}' from pool[{sourcePool}] at {spawnPosition}");
}
private int WeightedPickIndex(float[] weights)
@@ -275,20 +387,45 @@ namespace Minigames.BirdPooper
///
/// Start spawning obstacles.
+ /// Initializes the spawner if not already initialized, then begins spawning logic.
/// Spawns the first obstacle immediately, then continues with interval-based spawning.
///
public void StartSpawning()
{
- isSpawning = true;
- spawnTimer = 0f;
+ // Initialize if not already done
+ if (_spawnConfig == null)
+ {
+ Initialize();
+ }
+
+ // Ensure initialization was successful
+ if (_spawnConfig == null)
+ {
+ Debug.LogError("[ObstacleSpawner] Cannot start spawning - initialization failed!");
+ return;
+ }
+
+ // Begin the spawning process
+ BeginSpawningObstacles();
+ }
+
+ ///
+ /// Internal method that handles the actual spawning startup logic.
+ /// Sets initial state, computes first interval, and spawns the first obstacle.
+ ///
+ private void BeginSpawningObstacles()
+ {
+ _isSpawning = true;
+ _spawnTimer = 0f;
_elapsedTime = 0f;
- // choose initial interval based on difficulty (at time 0)
- float initialDifficulty = difficultyCurve.Evaluate(0f);
- float initialBase = Mathf.Lerp(maxSpawnInterval, minSpawnInterval, initialDifficulty);
- if (intervalJitter > 0f)
+ // Choose initial interval based on difficulty (at time 0)
+ float initialDifficulty = _spawnConfig.difficultyCurve.Evaluate(0f);
+ float initialBase = Mathf.Lerp(_spawnConfig.maxSpawnInterval, _spawnConfig.minSpawnInterval, initialDifficulty);
+
+ if (_spawnConfig.intervalJitter > 0f)
{
- float jitter = Random.Range(-intervalJitter, intervalJitter);
+ float jitter = Random.Range(-_spawnConfig.intervalJitter, _spawnConfig.intervalJitter);
_currentSpawnInterval = Mathf.Max(0f, initialBase * (1f + jitter));
}
else
@@ -310,14 +447,14 @@ namespace Minigames.BirdPooper
///
public void StopSpawning()
{
- isSpawning = false;
+ _isSpawning = false;
Debug.Log("[ObstacleSpawner] Stopped spawning");
}
///
/// Check if spawner is currently active.
///
- public bool IsSpawning => isSpawning;
+ public bool IsSpawning => _isSpawning;
#if UNITY_EDITOR
///
diff --git a/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs b/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs
new file mode 100644
index 00000000..1e8eeabf
--- /dev/null
+++ b/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs
@@ -0,0 +1,153 @@
+using UnityEngine;
+using UnityEngine.UI;
+using Core.Lifecycle;
+using Pixelplacement.TweenSystem;
+using Utils;
+
+namespace Minigames.BirdPooper
+{
+ ///
+ /// Manages "tap to start" flow for Bird Pooper minigame.
+ /// Shows blinking finger UI, waits for first tap, then starts the game.
+ ///
+ public class TapToStartController : ManagedBehaviour, ITouchInputConsumer
+ {
+ [Header("UI References")]
+ [SerializeField] private GameObject fingerContainer;
+ [SerializeField] private Image fingerImage;
+
+ [Header("Animation Settings")]
+ [Tooltip("Duration for one complete fade in/out cycle")]
+ [SerializeField] private float blinkDuration = 1.5f;
+ [Tooltip("Minimum alpha value during blink")]
+ [SerializeField] private float minAlpha = 0.3f;
+ [Tooltip("Maximum alpha value during blink")]
+ [SerializeField] private float maxAlpha = 1f;
+
+ private bool _isWaitingForTap;
+ private TweenBase _blinkTween;
+
+ internal override void OnManagedAwake()
+ {
+ base.OnManagedAwake();
+
+ // Validate references
+ if (fingerContainer == null)
+ {
+ Debug.LogError("[TapToStartController] Finger container not assigned!");
+ }
+
+ if (fingerImage == null)
+ {
+ Debug.LogError("[TapToStartController] Finger image not assigned!");
+ }
+
+ // Start hidden
+ if (fingerContainer != null)
+ {
+ fingerContainer.SetActive(false);
+ }
+ }
+
+ ///
+ /// Activates the tap-to-start UI and begins waiting for player input.
+ /// Called by BirdPooperGameManager when scene is ready.
+ ///
+ public void Activate()
+ {
+ if (_isWaitingForTap)
+ {
+ Debug.LogWarning("[TapToStartController] Already waiting for tap!");
+ return;
+ }
+
+ Debug.Log("[TapToStartController] Activating tap-to-start...");
+
+ _isWaitingForTap = true;
+
+ // Show finger UI
+ if (fingerContainer != null)
+ {
+ fingerContainer.SetActive(true);
+ }
+
+ // Start blinking animation using tween utility
+ if (fingerImage != null)
+ {
+ _blinkTween = TweenAnimationUtility.StartBlinkImage(fingerImage, minAlpha, maxAlpha, blinkDuration);
+ }
+
+ // Register as high-priority input consumer to catch first tap
+ if (Input.InputManager.Instance != null)
+ {
+ Input.InputManager.Instance.SetDefaultConsumer(this);
+ Debug.Log("[TapToStartController] Registered as input consumer");
+ }
+ else
+ {
+ Debug.LogError("[TapToStartController] InputManager instance not found!");
+ }
+ }
+
+ #region ITouchInputConsumer Implementation
+
+ public void OnTap(Vector2 tapPosition)
+ {
+ if (!_isWaitingForTap) return;
+
+ Debug.Log("[TapToStartController] First tap received! Starting game...");
+
+ // Stop waiting for tap
+ _isWaitingForTap = false;
+
+ // Stop blinking animation
+ if (_blinkTween != null)
+ {
+ _blinkTween.Stop();
+ _blinkTween = null;
+ }
+
+ // Hide finger UI
+ if (fingerContainer != null)
+ {
+ fingerContainer.SetActive(false);
+ }
+
+ // Unregister from input system
+ if (Input.InputManager.Instance != null)
+ {
+ Input.InputManager.Instance.SetDefaultConsumer(null);
+ Debug.Log("[TapToStartController] Unregistered from input");
+ }
+
+ // Tell game manager to start the game (it will handle the initial flap)
+ BirdPooperGameManager.Instance.BeginMinigame();
+
+ }
+
+ public void OnHoldStart(Vector2 position) { }
+ public void OnHoldMove(Vector2 position) { }
+ public void OnHoldEnd(Vector2 position) { }
+
+ #endregion
+
+ internal override void OnManagedDestroy()
+ {
+ // Stop blinking animation if active
+ if (_blinkTween != null)
+ {
+ _blinkTween.Stop();
+ _blinkTween = null;
+ }
+
+ // Unregister from input if still registered
+ if (_isWaitingForTap && Input.InputManager.Instance != null)
+ {
+ Input.InputManager.Instance.SetDefaultConsumer(null);
+ }
+
+ base.OnManagedDestroy();
+ }
+ }
+}
+
diff --git a/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs.meta b/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs.meta
new file mode 100644
index 00000000..b75a6420
--- /dev/null
+++ b/Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2a6ee5aca3ca423c82b57e16c0b2cca3
+timeCreated: 1765922092
\ No newline at end of file
diff --git a/Assets/Scripts/Minigames/BirdPooper/TargetSpawner.cs b/Assets/Scripts/Minigames/BirdPooper/TargetSpawner.cs
index 226a1aa1..e3a372d4 100644
--- a/Assets/Scripts/Minigames/BirdPooper/TargetSpawner.cs
+++ b/Assets/Scripts/Minigames/BirdPooper/TargetSpawner.cs
@@ -31,9 +31,9 @@ namespace Minigames.BirdPooper
[Tooltip("Array of target prefabs to spawn randomly")]
[SerializeField] private GameObject[] targetPrefabs;
- private IBirdPooperSettings settings;
- private float spawnTimer;
- private bool isSpawning;
+ private IBirdPooperSettings _settings;
+ private float _spawnTimer;
+ private bool _isSpawning;
private float _currentTargetInterval = 1f;
[Header("Spawn Timing")]
@@ -42,16 +42,17 @@ namespace Minigames.BirdPooper
[Tooltip("Maximum interval between target spawns (seconds)")]
[SerializeField] private float maxTargetSpawnInterval = 2f;
- internal override void OnManagedAwake()
+ ///
+ /// Initializes the target spawner by loading settings and validating references.
+ /// Should be called once before spawning begins.
+ ///
+ private void Initialize()
{
- base.OnManagedAwake();
-
// Load settings
- settings = GameManager.GetSettingsObject();
- if (settings == null)
+ _settings = GameManager.GetSettingsObject();
+ if (_settings == null)
{
- Debug.LogError("[TargetSpawner] BirdPooperSettings not found!");
- // continue – we'll use inspector intervals
+ Debug.LogWarning("[TargetSpawner] BirdPooperSettings not found! Using inspector intervals.");
}
// Validate interval range
@@ -96,15 +97,15 @@ namespace Minigames.BirdPooper
private void Update()
{
- if (!isSpawning)
+ if (!_isSpawning)
return;
- spawnTimer += Time.deltaTime;
+ _spawnTimer += Time.deltaTime;
- if (spawnTimer >= _currentTargetInterval)
+ if (_spawnTimer >= _currentTargetInterval)
{
SpawnTarget();
- spawnTimer = 0f;
+ _spawnTimer = 0f;
// pick next random interval
_currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval);
}
@@ -167,15 +168,34 @@ namespace Minigames.BirdPooper
}
///
- /// Start spawning targets at regular intervals.
+ /// Start spawning targets.
+ /// Initializes the spawner if not already initialized, then begins spawning logic.
///
public void StartSpawning()
{
- isSpawning = true;
- spawnTimer = 0f;
- // choose initial interval
+ // Initialize if not already done
+ if (_settings == null)
+ {
+ Initialize();
+ }
+
+ // Begin the spawning process
+ BeginSpawningTargets();
+ }
+
+ ///
+ /// Internal method that handles the actual spawning startup logic.
+ /// Sets initial state and computes first interval.
+ ///
+ private void BeginSpawningTargets()
+ {
+ _isSpawning = true;
+ _spawnTimer = 0f;
+
+ // Choose initial interval
_currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval);
- Debug.Log("[TargetSpawner] Started spawning targets");
+
+ Debug.Log($"[TargetSpawner] Started spawning targets with interval {_currentTargetInterval:F2}s");
}
///
@@ -183,14 +203,14 @@ namespace Minigames.BirdPooper
///
public void StopSpawning()
{
- isSpawning = false;
+ _isSpawning = false;
Debug.Log("[TargetSpawner] Stopped spawning targets");
}
///
/// Check if spawner is currently spawning.
///
- public bool IsSpawning => isSpawning;
+ public bool IsSpawning => _isSpawning;
///
/// Draw gizmos to visualize spawn and despawn points in the editor.
diff --git a/Assets/Scripts/Utils/TweenAnimationUtility.cs b/Assets/Scripts/Utils/TweenAnimationUtility.cs
index 5859af82..93330480 100644
--- a/Assets/Scripts/Utils/TweenAnimationUtility.cs
+++ b/Assets/Scripts/Utils/TweenAnimationUtility.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Pixelplacement;
using Pixelplacement.TweenSystem;
using UnityEngine;
@@ -145,6 +145,52 @@ namespace Utils
return Tween.CanvasGroupAlpha(canvasGroup, targetAlpha, duration, 0f, Tween.EaseInOut, completeCallback: onComplete);
}
+ ///
+ /// Fade Image alpha
+ ///
+ public static TweenBase FadeImageAlpha(UnityEngine.UI.Image image, float targetAlpha, float duration, Action onComplete = null)
+ {
+ return Tween.Value(image.color.a, targetAlpha, (alpha) =>
+ {
+ if (image != null)
+ {
+ Color color = image.color;
+ color.a = alpha;
+ image.color = color;
+ }
+ }, duration, 0f, Tween.EaseInOut, completeCallback: onComplete);
+ }
+
+ ///
+ /// Start blinking animation on Image (ping-pong alpha fade)
+ ///
+ /// Image to blink
+ /// Minimum alpha value
+ /// Maximum alpha value
+ /// Duration for one complete cycle (in and out)
+ /// TweenBase that can be cancelled
+ public static TweenBase StartBlinkImage(UnityEngine.UI.Image image, float minAlpha = 0.3f, float maxAlpha = 1f, float duration = 1.5f)
+ {
+ // Set initial alpha to max
+ if (image != null)
+ {
+ Color color = image.color;
+ color.a = maxAlpha;
+ image.color = color;
+ }
+
+ // Create ping-pong tween (half duration for each direction)
+ return Tween.Value(maxAlpha, minAlpha, (alpha) =>
+ {
+ if (image != null)
+ {
+ Color color = image.color;
+ color.a = alpha;
+ image.color = color;
+ }
+ }, duration / 2f, 0f, Tween.EaseInOut, Tween.LoopType.PingPong);
+ }
+
///
/// Pop-out with fade - scale to 0 and fade out simultaneously
///
diff --git a/Assets/Settings/BirdPooperSettings.asset b/Assets/Settings/BirdPooperSettings.asset
index e84cae05..1bc0e422 100644
--- a/Assets/Settings/BirdPooperSettings.asset
+++ b/Assets/Settings/BirdPooperSettings.asset
@@ -20,7 +20,60 @@ MonoBehaviour:
maxRotationAngle: 40
rotationSpeed: 18
obstacleMoveSpeed: 7
- obstacleSpawnInterval: 0.1
+ obstacleSpawnConfiguration:
+ obstaclePools:
+ - obstacles:
+ - {fileID: 8855270423038321603, guid: 20ae02a8f50484045aaf3dcee33fb9a2, type: 3}
+ - {fileID: 2514399078413048981, guid: ee834e7efcf7d8749881f71f8b0da99c, type: 3}
+ - {fileID: 842802843766402460, guid: cdc806fd167bba3488797031a28657fa, type: 3}
+ - {fileID: 4239333156730914246, guid: 332d8cce2ed99054c83ecf84fbfa14c8, type: 3}
+ - {fileID: 6660502783540694524, guid: 5d42fc70e5838544ab654e30aa4b0c48, type: 3}
+ - {fileID: 2421410811796775077, guid: 371a09b68a5c0654bac9ba58ad3bcbe5, type: 3}
+ - obstacles:
+ - {fileID: 1408173265900928789, guid: 871373a85e5da0e4cafdf0e47496e105, type: 3}
+ - {fileID: 1408173265900928789, guid: d2998934362713545a040d7017a1bd36, type: 3}
+ - {fileID: 1408173265900928789, guid: 146d99398c0e7964dbed504e256adab7, type: 3}
+ - {fileID: 1408173265900928789, guid: dc8a19e9a4d30b44596237d915b3b73f, type: 3}
+ - {fileID: 1408173265900928789, guid: 471f367e14f9cfb4fb2c40d799d4c292, type: 3}
+ - {fileID: 1408173265900928789, guid: 5f1734c5705cdfd49ae3180d678d28b3, type: 3}
+ - obstacles:
+ - {fileID: 1408173265900928789, guid: 6bc84c3ea9854b54f85a8fb69c769790, type: 3}
+ - {fileID: 1408173265900928789, guid: 166c7e1bfcc4c854fab0af51cdfff746, type: 3}
+ - {fileID: 1408173265900928789, guid: 65810bfd58ebbaf4482527452258ae50, type: 3}
+ - {fileID: 1408173265900928789, guid: ae3986a7db087c845b618a9c897705ec, type: 3}
+ poolUnlockTimes:
+ - 20
+ - 40
+ minSpawnInterval: 1
+ maxSpawnInterval: 2
+ difficultyRampDuration: 60
+ difficultyCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 0
+ inSlope: 0
+ outSlope: 1
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0
+ outWeight: 0
+ - serializedVersion: 3
+ time: 1
+ value: 1
+ inSlope: 1
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0
+ outWeight: 0
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+ intervalJitter: 0.05
+ recentDecayDuration: 10
+ minRecentWeight: 0.1
obstacleSpawnXPosition: 12
obstacleDestroyXPosition: -12
obstacleMinSpawnY: -3
diff --git a/Assets/Settings/FortFightSettings.asset b/Assets/Settings/FortFightSettings.asset
index 0b4402c7..0e6686ed 100644
--- a/Assets/Settings/FortFightSettings.asset
+++ b/Assets/Settings/FortFightSettings.asset
@@ -28,18 +28,24 @@ MonoBehaviour:
forceDeviation: 0.3
thinkTimeMin: 0.5
thinkTimeMax: 1
+ trashBagDetonationDistanceMin: 0.3
+ trashBagDetonationDistanceMax: 0.5
- difficulty: 1
data:
angleDeviation: 30
forceDeviation: 0.2
thinkTimeMin: 0.2
thinkTimeMax: 0.8
+ trashBagDetonationDistanceMin: 0.2
+ trashBagDetonationDistanceMax: 0.4
- difficulty: 2
data:
angleDeviation: 10
forceDeviation: 0.1
thinkTimeMin: 0.2
thinkTimeMax: 0.8
+ trashBagDetonationDistanceMin: 0.1
+ trashBagDetonationDistanceMax: 0.5
defaultAIDifficulty: 1
aiAllowedProjectiles: 000000000100000003000000
weakPointExplosionRadius: 6