pooper_minigame (#62)
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #62
This commit is contained in:
@@ -17,6 +17,7 @@ namespace AppleHillsCamera
|
||||
public enum AnchorEdge
|
||||
{
|
||||
Top,
|
||||
Middle,
|
||||
Bottom,
|
||||
Left,
|
||||
Right
|
||||
@@ -49,6 +50,10 @@ namespace AppleHillsCamera
|
||||
[Tooltip("Whether to account for this object's size in positioning")]
|
||||
public bool accountForObjectSize = true;
|
||||
|
||||
[Header("Custom Anchor Point")]
|
||||
[Tooltip("Optional: Use this child Transform's world position as the anchor point instead of calculated bounds")]
|
||||
public Transform customAnchorPoint;
|
||||
|
||||
[Header("Visualization")]
|
||||
[Tooltip("Whether to show the anchor visualization in the editor")]
|
||||
public bool showVisualization = true;
|
||||
@@ -302,10 +307,22 @@ namespace AppleHillsCamera
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the combined bounds of all renderers on this object and its children
|
||||
/// Get the combined bounds of all renderers on this object and its children.
|
||||
/// If customAnchorPoint is set, returns a zero-size bounds at the anchor point's world position.
|
||||
/// </summary>
|
||||
private Bounds GetObjectBounds()
|
||||
{
|
||||
// If custom anchor point is specified, use its world position as the bounds center
|
||||
if (customAnchorPoint != null)
|
||||
{
|
||||
// Return zero-size bounds centered at the custom anchor point
|
||||
// This makes the anchor point the exact position that will snap to the edge
|
||||
Bounds customBounds = new Bounds(customAnchorPoint.position, Vector3.zero);
|
||||
_objectBounds = customBounds;
|
||||
return customBounds;
|
||||
}
|
||||
|
||||
// Default behavior: calculate bounds from renderers
|
||||
Bounds bounds = new Bounds(transform.position, Vector3.zero);
|
||||
|
||||
// Get all renderers in this object and its children
|
||||
@@ -404,6 +421,8 @@ namespace AppleHillsCamera
|
||||
{
|
||||
case AnchorEdge.Top:
|
||||
return referenceMarker.topMargin;
|
||||
case AnchorEdge.Middle:
|
||||
return 0f; // Middle has no margin
|
||||
case AnchorEdge.Bottom:
|
||||
return referenceMarker.bottomMargin;
|
||||
case AnchorEdge.Left:
|
||||
@@ -445,6 +464,10 @@ namespace AppleHillsCamera
|
||||
// For top edge, offset is negative (moving down) by the top extent
|
||||
offsetY = -extents.y - centerOffset.y;
|
||||
break;
|
||||
case AnchorEdge.Middle:
|
||||
// For middle, no offset needed - object centers on middle
|
||||
offsetY = -centerOffset.y;
|
||||
break;
|
||||
case AnchorEdge.Bottom:
|
||||
// For bottom edge, offset is positive (moving up) by the bottom extent
|
||||
offsetY = extents.y - centerOffset.y;
|
||||
@@ -468,6 +491,11 @@ namespace AppleHillsCamera
|
||||
newPosition.y = cameraPosition.y + cameraOrthoSize - margin + offsetY;
|
||||
break;
|
||||
|
||||
case AnchorEdge.Middle:
|
||||
// Position at the vertical center of the screen
|
||||
newPosition.y = cameraPosition.y + offsetY;
|
||||
break;
|
||||
|
||||
case AnchorEdge.Bottom:
|
||||
// Position from the bottom of the screen
|
||||
// When margin is 0, object's bottom edge is exactly at the bottom screen edge
|
||||
@@ -518,6 +546,14 @@ namespace AppleHillsCamera
|
||||
objectPosition.z
|
||||
);
|
||||
|
||||
case AnchorEdge.Middle:
|
||||
// Point at vertical center with same X coordinate as the object
|
||||
return new Vector3(
|
||||
objectPosition.x,
|
||||
cameraPosition.y,
|
||||
objectPosition.z
|
||||
);
|
||||
|
||||
case AnchorEdge.Bottom:
|
||||
// Point on bottom edge with same X coordinate as the object
|
||||
return new Vector3(
|
||||
|
||||
@@ -171,6 +171,7 @@ namespace Core
|
||||
var minigameSettings = SettingsProvider.Instance.LoadSettingsSynchronous<DivingMinigameSettings>();
|
||||
var cardSystemSettings = SettingsProvider.Instance.LoadSettingsSynchronous<CardSystemSettings>();
|
||||
var sortingGameSettings = SettingsProvider.Instance.LoadSettingsSynchronous<CardSortingSettings>();
|
||||
var birdPooperSettings = SettingsProvider.Instance.LoadSettingsSynchronous<BirdPooperSettings>();
|
||||
|
||||
// Register settings with service locator
|
||||
if (playerSettings != null)
|
||||
@@ -222,9 +223,19 @@ namespace Core
|
||||
{
|
||||
Debug.LogError("Failed to load CardSystemSettings");
|
||||
}
|
||||
|
||||
if (birdPooperSettings != null)
|
||||
{
|
||||
ServiceLocator.Register<IBirdPooperSettings>(birdPooperSettings);
|
||||
Logging.Debug("BirdPooperSettings registered successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Failed to load BirdPooperSettings");
|
||||
}
|
||||
|
||||
// Log success
|
||||
_settingsLoaded = playerSettings != null && interactionSettings != null && minigameSettings != null && cardSystemSettings != null;
|
||||
_settingsLoaded = playerSettings != null && interactionSettings != null && minigameSettings != null && cardSystemSettings != null && birdPooperSettings != null;
|
||||
if (_settingsLoaded)
|
||||
{
|
||||
Logging.Debug("All settings loaded and registered with ServiceLocator");
|
||||
|
||||
89
Assets/Scripts/Core/Settings/BirdPooperSettings.cs
Normal file
89
Assets/Scripts/Core/Settings/BirdPooperSettings.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using AppleHills.Core.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Core.Settings
|
||||
{
|
||||
[CreateAssetMenu(fileName = "BirdPooperSettings", menuName = "AppleHills/Settings/BirdPooper", order = 5)]
|
||||
public class BirdPooperSettings : BaseSettings, IBirdPooperSettings
|
||||
{
|
||||
[Header("Player Controller")]
|
||||
[Tooltip("Gravity acceleration in units/s²")]
|
||||
[SerializeField] private float gravity = 20f;
|
||||
|
||||
[Tooltip("Upward velocity applied on flap in units/s")]
|
||||
[SerializeField] private float flapForce = 8f;
|
||||
|
||||
[Tooltip("Maximum fall speed (terminal velocity) in units/s")]
|
||||
[SerializeField] private float maxFallSpeed = 15f;
|
||||
|
||||
[Tooltip("Minimum Y boundary")]
|
||||
[SerializeField] private float minY = -5f;
|
||||
|
||||
[Tooltip("Maximum Y boundary")]
|
||||
[SerializeField] private float maxY = 5f;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Maximum rotation angle in degrees (positive = up, negative = down)")]
|
||||
[SerializeField] private float maxRotationAngle = 30f;
|
||||
|
||||
[Tooltip("Speed of rotation interpolation (higher = snappier)")]
|
||||
[SerializeField] private float rotationSpeed = 8f;
|
||||
|
||||
[Header("Obstacles")]
|
||||
[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("X position where obstacles spawn (off-screen right)")]
|
||||
[SerializeField] private float obstacleSpawnXPosition = 12f;
|
||||
|
||||
[Tooltip("X position where obstacles are destroyed (off-screen left)")]
|
||||
[SerializeField] private float obstacleDestroyXPosition = -12f;
|
||||
|
||||
[Tooltip("Minimum Y position for obstacle spawns")]
|
||||
[SerializeField] private float obstacleMinSpawnY = -3f;
|
||||
|
||||
[Tooltip("Maximum Y position for obstacle spawns")]
|
||||
[SerializeField] private float obstacleMaxSpawnY = 3f;
|
||||
|
||||
[Header("Poop Projectile")]
|
||||
[Tooltip("Poop fall speed in units/s")]
|
||||
[SerializeField] private float poopFallSpeed = 10f;
|
||||
|
||||
[Tooltip("Y position where poop is destroyed (off-screen bottom)")]
|
||||
[SerializeField] private float poopDestroyYPosition = -10f;
|
||||
|
||||
// Interface implementation
|
||||
public float Gravity => gravity;
|
||||
public float FlapForce => flapForce;
|
||||
public float MaxFallSpeed => maxFallSpeed;
|
||||
public float MinY => minY;
|
||||
public float MaxY => maxY;
|
||||
public float MaxRotationAngle => maxRotationAngle;
|
||||
public float RotationSpeed => rotationSpeed;
|
||||
public float ObstacleMoveSpeed => obstacleMoveSpeed;
|
||||
public float ObstacleSpawnInterval => obstacleSpawnInterval;
|
||||
public float ObstacleSpawnXPosition => obstacleSpawnXPosition;
|
||||
public float ObstacleDestroyXPosition => obstacleDestroyXPosition;
|
||||
public float ObstacleMinSpawnY => obstacleMinSpawnY;
|
||||
public float ObstacleMaxSpawnY => obstacleMaxSpawnY;
|
||||
public float PoopFallSpeed => poopFallSpeed;
|
||||
public float PoopDestroyYPosition => poopDestroyYPosition;
|
||||
|
||||
public override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// Validation logic
|
||||
gravity = Mathf.Max(0f, gravity);
|
||||
flapForce = Mathf.Max(0f, flapForce);
|
||||
maxFallSpeed = Mathf.Max(0f, maxFallSpeed);
|
||||
maxRotationAngle = Mathf.Clamp(maxRotationAngle, 0f, 90f);
|
||||
rotationSpeed = Mathf.Max(0.1f, rotationSpeed);
|
||||
obstacleSpawnInterval = Mathf.Max(0.1f, obstacleSpawnInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Core/Settings/BirdPooperSettings.cs.meta
Normal file
3
Assets/Scripts/Core/Settings/BirdPooperSettings.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7d3f5b948b3454681fa573071bee978
|
||||
timeCreated: 1763596494
|
||||
33
Assets/Scripts/Core/Settings/IBirdPooperSettings.cs
Normal file
33
Assets/Scripts/Core/Settings/IBirdPooperSettings.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace Core.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings interface for Bird Pooper minigame.
|
||||
/// Accessed via GameManager.GetSettingsObject<IBirdPooperSettings>()
|
||||
/// </summary>
|
||||
public interface IBirdPooperSettings
|
||||
{
|
||||
// Player Controller
|
||||
float Gravity { get; }
|
||||
float FlapForce { get; }
|
||||
float MaxFallSpeed { get; }
|
||||
float MinY { get; }
|
||||
float MaxY { get; }
|
||||
|
||||
// Rotation
|
||||
float MaxRotationAngle { get; }
|
||||
float RotationSpeed { get; }
|
||||
|
||||
// Obstacles
|
||||
float ObstacleMoveSpeed { get; }
|
||||
float ObstacleSpawnInterval { get; }
|
||||
float ObstacleSpawnXPosition { get; }
|
||||
float ObstacleDestroyXPosition { get; }
|
||||
float ObstacleMinSpawnY { get; }
|
||||
float ObstacleMaxSpawnY { get; }
|
||||
|
||||
// Poop Projectile
|
||||
float PoopFallSpeed { get; }
|
||||
float PoopDestroyYPosition { get; }
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Core/Settings/IBirdPooperSettings.cs.meta
Normal file
3
Assets/Scripts/Core/Settings/IBirdPooperSettings.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66509e37e0a549a79de6fe3fa710fd63
|
||||
timeCreated: 1763596482
|
||||
3
Assets/Scripts/Debug.meta
Normal file
3
Assets/Scripts/Debug.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b74b4580a37241779a6995f59d93e304
|
||||
timeCreated: 1763648961
|
||||
1
Assets/Scripts/Debug/BoosterPackPickupDebugger.cs
Normal file
1
Assets/Scripts/Debug/BoosterPackPickupDebugger.cs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
3
Assets/Scripts/Debug/BoosterPackPickupDebugger.cs.meta
Normal file
3
Assets/Scripts/Debug/BoosterPackPickupDebugger.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 504a790db6b1440aacd1b137f4850461
|
||||
timeCreated: 1763648961
|
||||
3
Assets/Scripts/Editor.meta
Normal file
3
Assets/Scripts/Editor.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12ea065dca5840acae696a40c0cd96dc
|
||||
timeCreated: 1763632462
|
||||
33
Assets/Scripts/Interactions/BoosterGlowEffect.cs
Normal file
33
Assets/Scripts/Interactions/BoosterGlowEffect.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple glow effect component for booster pack pickups.
|
||||
/// Attach to a GameObject with a SpriteRenderer for basic glow visual.
|
||||
/// The BoosterPackPickup will handle the scale animation.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(SpriteRenderer))]
|
||||
public class BoosterGlowEffect : MonoBehaviour
|
||||
{
|
||||
[Header("Glow Settings")]
|
||||
[SerializeField] private Color glowColor = Color.yellow;
|
||||
[SerializeField] private float baseAlpha = 0.5f;
|
||||
|
||||
private SpriteRenderer spriteRenderer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
|
||||
// Apply glow color with alpha
|
||||
Color color = glowColor;
|
||||
color.a = baseAlpha;
|
||||
spriteRenderer.color = color;
|
||||
|
||||
// Set sorting order to be behind the item
|
||||
spriteRenderer.sortingOrder = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Interactions/BoosterGlowEffect.cs.meta
Normal file
3
Assets/Scripts/Interactions/BoosterGlowEffect.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6488b58253b4e6789103e341090ca2f
|
||||
timeCreated: 1763639769
|
||||
299
Assets/Scripts/Interactions/BoosterPackPickup.cs
Normal file
299
Assets/Scripts/Interactions/BoosterPackPickup.cs
Normal file
@@ -0,0 +1,299 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using Unity.Cinemachine;
|
||||
using Data.CardSystem;
|
||||
using Pixelplacement;
|
||||
using Core;
|
||||
using UI;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Special pickup that plays a fancy sequence when collected:
|
||||
/// - Follower picks up normally
|
||||
/// - Pauses follower movement
|
||||
/// - Zooms camera in
|
||||
/// - Grows/pulses the booster pack
|
||||
/// - Animates to backpack icon
|
||||
/// - Gives player a booster pack
|
||||
/// - Restores camera and resumes movement
|
||||
/// </summary>
|
||||
public class BoosterPackPickup : Pickup
|
||||
{
|
||||
[Header("Booster Pack Sequence")]
|
||||
[SerializeField] private GameObject glowVisualPrefab;
|
||||
[SerializeField] private CinemachineCamera zoomCamera;
|
||||
[SerializeField] private int boosterPackCount = 1;
|
||||
|
||||
[Header("Sequence Timing")]
|
||||
[SerializeField] private float cameraBlendWait = 0.3f;
|
||||
[SerializeField] private float growDuration = 0.25f;
|
||||
[SerializeField] private float pulseDuration = 0.3f;
|
||||
[SerializeField] private int pulseCount = 3;
|
||||
[SerializeField] private float disappearDuration = 0.5f;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private float growScale = 1.5f;
|
||||
[SerializeField] private float pulseScale = 1.2f;
|
||||
|
||||
private GameObject glowInstance;
|
||||
private bool sequencePlaying;
|
||||
|
||||
protected override bool DoInteraction()
|
||||
{
|
||||
// Let the normal pickup flow happen first
|
||||
bool success = base.DoInteraction();
|
||||
|
||||
// If pickup was successful and we're not already playing the sequence, start it
|
||||
// IMPORTANT: We start the coroutine on the FollowerController, not on this object,
|
||||
// because this object gets disabled immediately after pickup
|
||||
if (success && !sequencePlaying)
|
||||
{
|
||||
var follower = FollowerController.FindInstance();
|
||||
if (follower != null)
|
||||
{
|
||||
follower.StartCoroutine(BoosterSequence(follower));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning("[BoosterPackPickup] No follower found, cannot start sequence");
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private IEnumerator BoosterSequence(FollowerController follower)
|
||||
{
|
||||
sequencePlaying = true;
|
||||
|
||||
// IMPORTANT: Disable all children on the original pickup GameObject
|
||||
// This prevents glow effects and other child objects from appearing in Pulver's hand
|
||||
foreach (Transform child in transform)
|
||||
{
|
||||
child.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// Wait one frame to ensure pickup is fully processed
|
||||
yield return null;
|
||||
|
||||
// Verify follower still exists (safety check)
|
||||
if (follower == null)
|
||||
{
|
||||
Logging.Warning("[BoosterPackPickup] Follower destroyed during sequence, aborting");
|
||||
sequencePlaying = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
Transform heldItemTransform = follower.GetHeldItemTransform();
|
||||
if (heldItemTransform == null)
|
||||
{
|
||||
Logging.Warning("[BoosterPackPickup] No held item transform found, skipping sequence");
|
||||
sequencePlaying = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 1. Pause follower movement
|
||||
follower.PauseMovement();
|
||||
|
||||
// 2. Activate zoom camera if assigned
|
||||
if (zoomCamera != null)
|
||||
{
|
||||
// Unparent the camera from the booster pack (if it's parented)
|
||||
zoomCamera.transform.SetParent(null);
|
||||
|
||||
// Position camera at follower's location
|
||||
zoomCamera.transform.position = new Vector3(
|
||||
follower.transform.position.x,
|
||||
follower.transform.position.y,
|
||||
zoomCamera.transform.position.z
|
||||
);
|
||||
|
||||
// Make the blend instant by setting a custom blend hint
|
||||
var brain = Camera.main?.GetComponent<CinemachineBrain>();
|
||||
CinemachineBlendDefinition originalBlend = default;
|
||||
if (brain != null)
|
||||
{
|
||||
originalBlend = brain.DefaultBlend;
|
||||
brain.DefaultBlend = new CinemachineBlendDefinition(CinemachineBlendDefinition.Styles.EaseInOut, 1);
|
||||
}
|
||||
|
||||
zoomCamera.Priority = 20;
|
||||
zoomCamera.gameObject.SetActive(true);
|
||||
|
||||
// Wait a frame for the cut to take effect
|
||||
yield return null;
|
||||
|
||||
// Restore original blend settings
|
||||
if (brain != null)
|
||||
{
|
||||
brain.DefaultBlend = originalBlend;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Grow animation (no glow during this)
|
||||
Vector3 originalScale = heldItemTransform.localScale;
|
||||
Tween.LocalScale(heldItemTransform, originalScale * growScale, growDuration, 0f, Tween.EaseOutBack);
|
||||
yield return new WaitForSeconds(growDuration);
|
||||
|
||||
// 4. Instantiate and pulse glow
|
||||
if (glowVisualPrefab != null)
|
||||
{
|
||||
glowInstance = Instantiate(glowVisualPrefab, heldItemTransform);
|
||||
glowInstance.transform.localPosition = Vector3.zero;
|
||||
glowInstance.transform.localScale = Vector3.one;
|
||||
|
||||
for (int i = 0; i < pulseCount; i++)
|
||||
{
|
||||
Tween.LocalScale(glowInstance.transform, Vector3.one * pulseScale, pulseDuration, 0f, Tween.EaseIn);
|
||||
yield return new WaitForSeconds(pulseDuration);
|
||||
Tween.LocalScale(glowInstance.transform, Vector3.one, pulseDuration, 0f, Tween.EaseInOut);
|
||||
yield return new WaitForSeconds(pulseDuration);
|
||||
}
|
||||
|
||||
// Delete glow before flying to button
|
||||
Destroy(glowInstance);
|
||||
glowInstance = null;
|
||||
}
|
||||
|
||||
// 5. Disappear to scrapbook button (backpack icon) - straight line movement
|
||||
GameObject scrabookButton = PlayerHudManager.Instance?.GetScrabookButton();
|
||||
if (scrabookButton != null)
|
||||
{
|
||||
// Get start position in world space
|
||||
Vector3 startPos = heldItemTransform.position;
|
||||
|
||||
// Convert UI button position to world space
|
||||
RectTransform buttonRect = scrabookButton.GetComponent<RectTransform>();
|
||||
Vector3 targetWorldPos;
|
||||
|
||||
if (buttonRect != null)
|
||||
{
|
||||
// UI element - get screen position correctly
|
||||
Vector3[] corners = new Vector3[4];
|
||||
buttonRect.GetWorldCorners(corners);
|
||||
|
||||
// Get center of the button in screen space
|
||||
Vector3 buttonCenter = (corners[0] + corners[2]) / 2f;
|
||||
|
||||
// For Screen Space Overlay, the corners are already in screen space
|
||||
Canvas canvas = buttonRect.GetComponentInParent<Canvas>();
|
||||
Vector3 buttonScreenPos;
|
||||
|
||||
if (canvas != null && canvas.renderMode == RenderMode.ScreenSpaceOverlay)
|
||||
{
|
||||
// Already in screen space
|
||||
buttonScreenPos = buttonCenter;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Convert from world to screen space
|
||||
buttonScreenPos = RectTransformUtility.WorldToScreenPoint(canvas?.worldCamera ?? Camera.main, buttonCenter);
|
||||
}
|
||||
|
||||
// Convert screen position to world space at the same Z as the booster pack
|
||||
targetWorldPos = Camera.main.ScreenToWorldPoint(new Vector3(buttonScreenPos.x, buttonScreenPos.y, Mathf.Abs(Camera.main.transform.position.z - startPos.z)));
|
||||
|
||||
Logging.Debug($"[BoosterPackPickup] Button screen pos: {buttonScreenPos}, converted world pos: {targetWorldPos}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a UI element, use direct world position
|
||||
targetWorldPos = scrabookButton.transform.position;
|
||||
}
|
||||
|
||||
Logging.Debug($"[BoosterPackPickup] Flying from {startPos} to {targetWorldPos} (button: {scrabookButton.name})");
|
||||
|
||||
// Use custom tween with linear interpolation for straight line
|
||||
float elapsed = 0f;
|
||||
Vector3 startScale = heldItemTransform.localScale;
|
||||
|
||||
while (elapsed < disappearDuration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / disappearDuration);
|
||||
|
||||
// Linear interpolation for straight line movement
|
||||
heldItemTransform.position = Vector3.Lerp(startPos, targetWorldPos, t);
|
||||
|
||||
// Ease in for scale (gets smaller as it approaches)
|
||||
float scaleT = t * t; // Simple ease-in curve
|
||||
heldItemTransform.localScale = Vector3.Lerp(startScale, Vector3.zero, scaleT);
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Ensure final position/scale
|
||||
heldItemTransform.position = targetWorldPos;
|
||||
heldItemTransform.localScale = Vector3.zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning("[BoosterPackPickup] No scrapbook button found from PlayerHudManager, skipping fly-to animation");
|
||||
}
|
||||
|
||||
// 7. Give booster pack to player
|
||||
if (CardSystemManager.Instance != null)
|
||||
{
|
||||
CardSystemManager.Instance.AddBoosterPack(boosterPackCount);
|
||||
Logging.Debug($"[BoosterPackPickup] Gave {boosterPackCount} booster pack(s) to player");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning("[BoosterPackPickup] CardSystemManager not found, cannot give booster pack");
|
||||
}
|
||||
|
||||
// 8. Cleanup glow
|
||||
if (glowInstance != null)
|
||||
{
|
||||
Destroy(glowInstance);
|
||||
}
|
||||
|
||||
// 9. Blend back to main camera and cleanup
|
||||
if (zoomCamera != null)
|
||||
{
|
||||
// Lower priority to blend back to main camera
|
||||
zoomCamera.Priority = 0;
|
||||
|
||||
// Wait for blend back to complete (using default blend settings)
|
||||
var brain = Camera.main?.GetComponent<CinemachineBrain>();
|
||||
if (brain != null && brain.IsBlending)
|
||||
{
|
||||
// Wait for the blend to finish
|
||||
while (brain.IsBlending)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If not blending or no brain, just wait a short moment
|
||||
yield return new WaitForSeconds(0.3f);
|
||||
}
|
||||
|
||||
// Destroy the zoom camera
|
||||
Destroy(zoomCamera.gameObject);
|
||||
}
|
||||
|
||||
// 10. Clear the follower's held item since we "consumed" it
|
||||
follower.ClearHeldItem();
|
||||
|
||||
// 11. Resume follower movement
|
||||
follower.ResumeMovement();
|
||||
|
||||
sequencePlaying = false;
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
base.OnManagedDestroy();
|
||||
|
||||
// Cleanup glow if still exists
|
||||
if (glowInstance != null)
|
||||
{
|
||||
Destroy(glowInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Interactions/BoosterPackPickup.cs.meta
Normal file
3
Assets/Scripts/Interactions/BoosterPackPickup.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11629418bd2d44fa9da229ad62c04332
|
||||
timeCreated: 1763639641
|
||||
3
Assets/Scripts/Minigames/BirdPooper.meta
Normal file
3
Assets/Scripts/Minigames/BirdPooper.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa5389b421944c6e8f43d7fad84ef470
|
||||
timeCreated: 1763596581
|
||||
69
Assets/Scripts/Minigames/BirdPooper/BirdFlapAnimator.cs
Normal file
69
Assets/Scripts/Minigames/BirdPooper/BirdFlapAnimator.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// Listens to BirdPlayerController flap events and triggers wing flap animation.
|
||||
/// Uses Animator with a trigger parameter to play flap animation from idle state.
|
||||
/// </summary>
|
||||
public class BirdFlapAnimator : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Animator animator;
|
||||
[Tooltip("Name of the trigger parameter in the Animator")]
|
||||
[SerializeField] private string flapTriggerName = "Flap";
|
||||
|
||||
private BirdPlayerController birdController;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// Auto-assign animator component if not set
|
||||
if (animator == null)
|
||||
{
|
||||
animator = GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
{
|
||||
Debug.LogError("[BirdFlapAnimator] No Animator component found! Please add an Animator component or assign one in the Inspector.");
|
||||
}
|
||||
}
|
||||
|
||||
// Find parent BirdPlayerController
|
||||
birdController = GetComponentInParent<BirdPlayerController>();
|
||||
if (birdController == null)
|
||||
{
|
||||
Debug.LogError("[BirdFlapAnimator] No BirdPlayerController found in parent! This component must be a child of the bird GameObject.");
|
||||
}
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
// Subscribe to flap event
|
||||
if (birdController != null && birdController.OnFlap != null)
|
||||
{
|
||||
birdController.OnFlap.AddListener(OnBirdFlapped);
|
||||
Debug.Log("[BirdFlapAnimator] Subscribed to OnFlap event");
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
// Unsubscribe from flap event
|
||||
if (birdController != null && birdController.OnFlap != null)
|
||||
{
|
||||
birdController.OnFlap.RemoveListener(OnBirdFlapped);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the bird flaps. Triggers the flap animation.
|
||||
/// </summary>
|
||||
private void OnBirdFlapped()
|
||||
{
|
||||
if (animator == null) return;
|
||||
|
||||
// Trigger the flap animation
|
||||
animator.SetTrigger(flapTriggerName);
|
||||
Debug.Log($"[BirdFlapAnimator] Triggered animation: {flapTriggerName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ba8e12a2b8b4d468ccfa995ed56980c
|
||||
timeCreated: 1763600531
|
||||
197
Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs
Normal file
197
Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using Core.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// Bird player controller with Flappy Bird-style flight mechanics.
|
||||
/// Responds to tap input to flap, with manual gravity simulation.
|
||||
/// </summary>
|
||||
public class BirdPlayerController : ManagedBehaviour, ITouchInputConsumer
|
||||
{
|
||||
[Header("Events")]
|
||||
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
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Initialize events
|
||||
if (OnFlap == null)
|
||||
OnFlap = new UnityEngine.Events.UnityEvent();
|
||||
if (OnPlayerDamaged == null)
|
||||
OnPlayerDamaged = new UnityEngine.Events.UnityEvent();
|
||||
|
||||
// Load settings
|
||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Rigidbody2D component (Dynamic with gravityScale = 0)
|
||||
rb = GetComponent<Rigidbody2D>();
|
||||
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
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] Rigidbody2D component not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Register as override consumer to capture ALL input (except UI button)
|
||||
// Register as override consumer to capture ALL input (except UI button)
|
||||
if (Input.InputManager.Instance != null)
|
||||
{
|
||||
Input.InputManager.Instance.RegisterOverrideConsumer(this);
|
||||
Debug.Log("[BirdPlayerController] Registered as override input consumer");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] InputManager instance not found!");
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
#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();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnHoldStart(Vector2 position) { }
|
||||
public void OnHoldMove(Vector2 position) { }
|
||||
public void OnHoldEnd(Vector2 position) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rotation
|
||||
|
||||
/// <summary>
|
||||
/// Updates the bird's rotation based on vertical velocity.
|
||||
/// Bird tilts up when flapping, down when falling.
|
||||
/// </summary>
|
||||
private void UpdateRotation()
|
||||
{
|
||||
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
|
||||
);
|
||||
float targetAngle = Mathf.Lerp(
|
||||
-settings.MaxRotationAngle,
|
||||
settings.MaxRotationAngle,
|
||||
velocityPercent
|
||||
);
|
||||
|
||||
// Get current angle (handle 0-360 wrapping to -180-180)
|
||||
float currentAngle = transform.rotation.eulerAngles.z;
|
||||
if (currentAngle > 180f)
|
||||
currentAngle -= 360f;
|
||||
|
||||
// Smooth interpolation to target
|
||||
float smoothedAngle = Mathf.Lerp(
|
||||
currentAngle,
|
||||
targetAngle,
|
||||
settings.RotationSpeed * Time.deltaTime
|
||||
);
|
||||
|
||||
// Apply rotation to Z axis only (2D rotation)
|
||||
transform.rotation = Quaternion.Euler(0, 0, smoothedAngle);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Trigger-Based Collision Detection
|
||||
|
||||
/// <summary>
|
||||
/// Called when a trigger collider enters this object's trigger.
|
||||
/// Used for detecting obstacles without physics interactions.
|
||||
/// </summary>
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
// Check if the colliding object is tagged as an obstacle
|
||||
if (other.CompareTag("Obstacle"))
|
||||
{
|
||||
HandleDeath();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDeath()
|
||||
{
|
||||
// Only process death once
|
||||
if (isDead) return;
|
||||
|
||||
isDead = true;
|
||||
verticalVelocity = 0f;
|
||||
Debug.Log("[BirdPlayerController] Bird died!");
|
||||
|
||||
// Emit damage event - let the game manager handle UI
|
||||
OnPlayerDamaged?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Properties
|
||||
|
||||
public bool IsDead => isDead;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d56e44aad9044f8808b892c7a5cfc50
|
||||
timeCreated: 1763596581
|
||||
118
Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs
Normal file
118
Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using UnityEngine;
|
||||
using Core.Lifecycle;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// Central game manager for Bird Pooper minigame.
|
||||
/// Manages game flow, UI, obstacle spawning, and reacts to player events.
|
||||
/// Singleton pattern for easy access.
|
||||
/// </summary>
|
||||
public class BirdPooperGameManager : ManagedBehaviour
|
||||
{
|
||||
private static BirdPooperGameManager _instance;
|
||||
public static BirdPooperGameManager Instance => _instance;
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private BirdPlayerController player;
|
||||
[SerializeField] private ObstacleSpawner obstacleSpawner;
|
||||
[SerializeField] private GameOverScreen gameOverScreen;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Set singleton instance
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Debug.LogWarning("[BirdPooperGameManager] Multiple instances detected! Destroying duplicate.");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
_instance = this;
|
||||
|
||||
// Validate references
|
||||
if (player == null)
|
||||
{
|
||||
Debug.LogError("[BirdPooperGameManager] Player reference not assigned!");
|
||||
}
|
||||
|
||||
if (obstacleSpawner == null)
|
||||
{
|
||||
Debug.LogError("[BirdPooperGameManager] ObstacleSpawner reference not assigned!");
|
||||
}
|
||||
|
||||
if (gameOverScreen == null)
|
||||
{
|
||||
Debug.LogError("[BirdPooperGameManager] GameOverScreen reference not assigned!");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hide game over screen on start
|
||||
gameOverScreen.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
// Subscribe to player events
|
||||
if (player != null)
|
||||
{
|
||||
player.OnPlayerDamaged.AddListener(HandlePlayerDamaged);
|
||||
Debug.Log("[BirdPooperGameManager] Subscribed to player damaged event");
|
||||
}
|
||||
|
||||
// Start obstacle spawning
|
||||
if (obstacleSpawner != null)
|
||||
{
|
||||
obstacleSpawner.StartSpawning();
|
||||
Debug.Log("[BirdPooperGameManager] Started obstacle spawning");
|
||||
}
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
// Unsubscribe from player events
|
||||
if (player != null)
|
||||
{
|
||||
player.OnPlayerDamaged.RemoveListener(HandlePlayerDamaged);
|
||||
}
|
||||
|
||||
// Clear singleton
|
||||
if (_instance == this)
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
base.OnManagedDestroy();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when player takes damage/dies.
|
||||
/// Shows game over screen.
|
||||
/// </summary>
|
||||
private void HandlePlayerDamaged()
|
||||
{
|
||||
Debug.Log("[BirdPooperGameManager] Player damaged - showing game over screen");
|
||||
|
||||
// Stop spawning obstacles
|
||||
if (obstacleSpawner != null)
|
||||
{
|
||||
obstacleSpawner.StopSpawning();
|
||||
}
|
||||
|
||||
// Show game over screen
|
||||
if (gameOverScreen != null)
|
||||
{
|
||||
gameOverScreen.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[BirdPooperGameManager] GameOverScreen reference missing!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8cb19f77b49b4e299fac404c56e0455a
|
||||
timeCreated: 1763636566
|
||||
101
Assets/Scripts/Minigames/BirdPooper/GameOverScreen.cs
Normal file
101
Assets/Scripts/Minigames/BirdPooper/GameOverScreen.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Core;
|
||||
using UI;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// Game over screen for Bird Pooper minigame.
|
||||
/// Displays when the player dies and allows restarting the level.
|
||||
/// Uses unscaled time for UI updates (works when Time.timeScale = 0).
|
||||
/// </summary>
|
||||
public class GameOverScreen : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
[SerializeField] private Button dismissButton;
|
||||
[SerializeField] private CanvasGroup canvasGroup;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Subscribe to button click
|
||||
if (dismissButton != null)
|
||||
{
|
||||
dismissButton.onClick.AddListener(OnDismissClicked);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[GameOverScreen] Dismiss button not assigned!");
|
||||
}
|
||||
|
||||
// Get or add CanvasGroup for fade effects
|
||||
if (canvasGroup == null)
|
||||
{
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null)
|
||||
{
|
||||
canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
}
|
||||
|
||||
// Hide by default
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Unsubscribe from button
|
||||
if (dismissButton != null)
|
||||
{
|
||||
dismissButton.onClick.RemoveListener(OnDismissClicked);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show the game over screen and pause the game.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
|
||||
// Set canvas group for interaction
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
canvasGroup.alpha = 1f;
|
||||
canvasGroup.interactable = true;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
}
|
||||
|
||||
// Pause the game (set timescale to 0)
|
||||
// PauseMenu uses unscaled time for tweens, so it will still work
|
||||
Time.timeScale = 0f;
|
||||
|
||||
Debug.Log("[GameOverScreen] Game Over - Time.timeScale set to 0");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when dismiss button is clicked. Reloads the level.
|
||||
/// </summary>
|
||||
private async void OnDismissClicked()
|
||||
{
|
||||
Debug.Log("[GameOverScreen] Dismiss button clicked - Reloading level");
|
||||
|
||||
// Hide this screen first
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
gameObject.SetActive(false);
|
||||
|
||||
// Reset time scale BEFORE reloading
|
||||
Time.timeScale = 1f;
|
||||
|
||||
// Now reload the current scene with fresh state - skipSave=true prevents re-saving cleared data
|
||||
var progress = new Progress<float>(p => Logging.Debug($"Loading progress: {p * 100:F0}%"));
|
||||
await SceneManagerService.Instance.ReloadCurrentScene(progress, autoHideLoadingScreen: true, skipSave: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5896ecc85e77484599b2f2c7ac240991
|
||||
timeCreated: 1763636057
|
||||
300
Assets/Scripts/Minigames/BirdPooper/Obstacle.cs
Normal file
300
Assets/Scripts/Minigames/BirdPooper/Obstacle.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
using UnityEngine;
|
||||
using Core;
|
||||
using Core.Settings;
|
||||
using AppleHillsCamera;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// Individual obstacle behavior for Bird Pooper minigame.
|
||||
/// Scrolls left at constant speed and self-destructs when reaching despawn position.
|
||||
/// Uses trigger colliders for collision detection (no Rigidbody2D needed).
|
||||
/// Uses EdgeAnchor for vertical positioning (Top/Middle/Bottom).
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
[RequireComponent(typeof(EdgeAnchor))]
|
||||
public class Obstacle : MonoBehaviour
|
||||
{
|
||||
[Header("Positioning")]
|
||||
[Tooltip("Which vertical edge to anchor to (Top/Middle/Bottom)")]
|
||||
[SerializeField] private EdgeAnchor.AnchorEdge verticalAnchor = EdgeAnchor.AnchorEdge.Middle;
|
||||
|
||||
private IBirdPooperSettings settings;
|
||||
private float despawnXPosition;
|
||||
private bool isInitialized;
|
||||
private EdgeAnchor edgeAnchor;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the obstacle with despawn position and EdgeAnchor references.
|
||||
/// Called by ObstacleSpawner immediately after instantiation.
|
||||
/// </summary>
|
||||
/// <param name="despawnX">X position where obstacle should be destroyed</param>
|
||||
/// <param name="referenceMarker">ScreenReferenceMarker for EdgeAnchor</param>
|
||||
/// <param name="cameraAdapter">CameraScreenAdapter for EdgeAnchor</param>
|
||||
public void Initialize(float despawnX, ScreenReferenceMarker referenceMarker, CameraScreenAdapter cameraAdapter)
|
||||
{
|
||||
despawnXPosition = despawnX;
|
||||
isInitialized = true;
|
||||
|
||||
// Load settings
|
||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogError("[Obstacle] BirdPooperSettings not found!");
|
||||
}
|
||||
|
||||
// Tag all child GameObjects with colliders as "Obstacle" for trigger detection
|
||||
TagChildCollidersRecursive(transform);
|
||||
|
||||
// Configure and update EdgeAnchor
|
||||
edgeAnchor = GetComponent<EdgeAnchor>();
|
||||
if (edgeAnchor != null)
|
||||
{
|
||||
// Assign references from spawner
|
||||
edgeAnchor.referenceMarker = referenceMarker;
|
||||
edgeAnchor.cameraAdapter = cameraAdapter;
|
||||
|
||||
// Only allow Top, Middle, or Bottom anchoring
|
||||
if (verticalAnchor == EdgeAnchor.AnchorEdge.Left || verticalAnchor == EdgeAnchor.AnchorEdge.Right)
|
||||
{
|
||||
Debug.LogWarning("[Obstacle] Invalid anchor edge (Left/Right not supported). Defaulting to Middle.");
|
||||
verticalAnchor = EdgeAnchor.AnchorEdge.Middle;
|
||||
}
|
||||
|
||||
edgeAnchor.anchorEdge = verticalAnchor;
|
||||
edgeAnchor.useReferenceMargin = false; // No custom offset
|
||||
edgeAnchor.customMargin = 0f;
|
||||
edgeAnchor.preserveOtherAxes = true; // Keep X position (for scrolling)
|
||||
edgeAnchor.accountForObjectSize = true;
|
||||
|
||||
// Trigger position update
|
||||
edgeAnchor.UpdatePosition();
|
||||
|
||||
Debug.Log($"[Obstacle] EdgeAnchor configured to {verticalAnchor} at position {transform.position}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[Obstacle] EdgeAnchor component not found! Make sure the prefab has an EdgeAnchor component.");
|
||||
}
|
||||
|
||||
Debug.Log($"[Obstacle] Initialized at position {transform.position} with despawn X: {despawnX}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively tag all GameObjects with Collider2D as "Obstacle" for player collision detection.
|
||||
/// </summary>
|
||||
private void TagChildCollidersRecursive(Transform current)
|
||||
{
|
||||
// Tag this GameObject if it has a collider
|
||||
Collider2D col = current.GetComponent<Collider2D>();
|
||||
if (col != null && !current.CompareTag("Obstacle"))
|
||||
{
|
||||
current.tag = "Obstacle";
|
||||
Debug.Log($"[Obstacle] Tagged '{current.name}' as Obstacle");
|
||||
}
|
||||
|
||||
// Recurse to children
|
||||
foreach (Transform child in current)
|
||||
{
|
||||
TagChildCollidersRecursive(child);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Called when values are changed in the Inspector (Editor only).
|
||||
/// Updates EdgeAnchor configuration to match Obstacle settings.
|
||||
/// Also finds and assigns ScreenReferenceMarker and CameraScreenAdapter for visual updates.
|
||||
/// </summary>
|
||||
private void OnValidate()
|
||||
{
|
||||
// Only run in editor, not during play mode
|
||||
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
return;
|
||||
|
||||
EdgeAnchor anchor = GetComponent<EdgeAnchor>();
|
||||
if (anchor != null)
|
||||
{
|
||||
// Auto-find and assign references if not set (for editor-time visual updates)
|
||||
if (anchor.referenceMarker == null)
|
||||
{
|
||||
anchor.referenceMarker = FindAnyObjectByType<ScreenReferenceMarker>();
|
||||
if (anchor.referenceMarker == null)
|
||||
{
|
||||
Debug.LogWarning("[Obstacle] No ScreenReferenceMarker found in scene. EdgeAnchor positioning won't work in editor.");
|
||||
}
|
||||
}
|
||||
|
||||
if (anchor.cameraAdapter == null)
|
||||
{
|
||||
anchor.cameraAdapter = FindAnyObjectByType<CameraScreenAdapter>();
|
||||
// CameraScreenAdapter is optional - EdgeAnchor can auto-find camera
|
||||
}
|
||||
|
||||
// Validate and set anchor edge
|
||||
if (verticalAnchor == EdgeAnchor.AnchorEdge.Left || verticalAnchor == EdgeAnchor.AnchorEdge.Right)
|
||||
{
|
||||
Debug.LogWarning("[Obstacle] Invalid anchor edge (Left/Right not supported). Defaulting to Middle.");
|
||||
verticalAnchor = EdgeAnchor.AnchorEdge.Middle;
|
||||
}
|
||||
|
||||
// Configure EdgeAnchor to match Obstacle settings
|
||||
anchor.anchorEdge = verticalAnchor;
|
||||
anchor.useReferenceMargin = false;
|
||||
anchor.customMargin = 0f;
|
||||
anchor.preserveOtherAxes = true;
|
||||
anchor.accountForObjectSize = true;
|
||||
|
||||
// Mark as dirty so Unity saves the changes
|
||||
UnityEditor.EditorUtility.SetDirty(anchor);
|
||||
}
|
||||
|
||||
// Tag all child GameObjects with colliders as "Obstacle" for collision detection
|
||||
TagChildCollidersRecursiveEditor(transform);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Editor version of recursive tagging for child colliders.
|
||||
/// </summary>
|
||||
private void TagChildCollidersRecursiveEditor(Transform current)
|
||||
{
|
||||
// Tag this GameObject if it has a collider
|
||||
Collider2D col = current.GetComponent<Collider2D>();
|
||||
if (col != null && !current.CompareTag("Obstacle"))
|
||||
{
|
||||
current.tag = "Obstacle";
|
||||
UnityEditor.EditorUtility.SetDirty(current.gameObject);
|
||||
}
|
||||
|
||||
// Recurse to children
|
||||
foreach (Transform child in current)
|
||||
{
|
||||
TagChildCollidersRecursiveEditor(child);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isInitialized || settings == null) return;
|
||||
|
||||
MoveLeft();
|
||||
CheckBounds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move obstacle left at constant speed (manual movement, no physics).
|
||||
/// </summary>
|
||||
private void MoveLeft()
|
||||
{
|
||||
transform.position += Vector3.left * (settings.ObstacleMoveSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if obstacle has passed despawn position and destroy if so.
|
||||
/// </summary>
|
||||
private void CheckBounds()
|
||||
{
|
||||
if (transform.position.x < despawnXPosition)
|
||||
{
|
||||
Debug.Log($"[Obstacle] Reached despawn position, destroying at X: {transform.position.x}");
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Draw debug visualization of the obstacle's anchor point.
|
||||
/// Red horizontal line through custom anchor point OR bounds edge (top/bottom).
|
||||
/// </summary>
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
EdgeAnchor anchor = GetComponent<EdgeAnchor>();
|
||||
if (anchor == null) return;
|
||||
|
||||
// Determine what Y position to visualize
|
||||
float visualY;
|
||||
|
||||
// If using custom anchor point, draw line through it
|
||||
if (anchor.customAnchorPoint != null)
|
||||
{
|
||||
visualY = anchor.customAnchorPoint.position.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get bounds and determine which edge to visualize
|
||||
Bounds bounds = GetVisualBounds();
|
||||
|
||||
// Check which vertical anchor is configured
|
||||
EdgeAnchor.AnchorEdge edge = anchor.anchorEdge;
|
||||
|
||||
if (edge == EdgeAnchor.AnchorEdge.Top)
|
||||
{
|
||||
// Show top edge of bounds
|
||||
visualY = bounds.max.y;
|
||||
}
|
||||
else if (edge == EdgeAnchor.AnchorEdge.Bottom)
|
||||
{
|
||||
// Show bottom edge of bounds
|
||||
visualY = bounds.min.y;
|
||||
}
|
||||
else // Middle
|
||||
{
|
||||
// Show center of bounds
|
||||
visualY = bounds.center.y;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw thick red horizontal line through the anchor point
|
||||
Color oldColor = Gizmos.color;
|
||||
Gizmos.color = Color.red;
|
||||
|
||||
// Draw multiple lines to make it thicker
|
||||
float lineLength = 2f; // Extend 2 units on each side
|
||||
Vector3 leftPoint = new Vector3(transform.position.x - lineLength, visualY, transform.position.z);
|
||||
Vector3 rightPoint = new Vector3(transform.position.x + lineLength, visualY, transform.position.z);
|
||||
|
||||
// Draw 5 lines stacked vertically to create thickness
|
||||
for (int i = -2; i <= 2; i++)
|
||||
{
|
||||
float offset = i * 0.02f; // Small vertical offset for thickness
|
||||
Vector3 offsetLeft = leftPoint + Vector3.up * offset;
|
||||
Vector3 offsetRight = rightPoint + Vector3.up * offset;
|
||||
Gizmos.DrawLine(offsetLeft, offsetRight);
|
||||
}
|
||||
|
||||
Gizmos.color = oldColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get bounds for visualization purposes (works in editor without initialized settings).
|
||||
/// </summary>
|
||||
private Bounds GetVisualBounds()
|
||||
{
|
||||
// Get all renderers in this object and its children
|
||||
Renderer[] renderers = GetComponentsInChildren<Renderer>();
|
||||
|
||||
if (renderers.Length > 0)
|
||||
{
|
||||
Bounds bounds = renderers[0].bounds;
|
||||
for (int i = 1; i < renderers.Length; i++)
|
||||
{
|
||||
bounds.Encapsulate(renderers[i].bounds);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
// Fallback to collider bounds
|
||||
Collider2D col = GetComponent<Collider2D>();
|
||||
if (col != null)
|
||||
{
|
||||
return col.bounds;
|
||||
}
|
||||
|
||||
// Default small bounds
|
||||
return new Bounds(transform.position, new Vector3(0.5f, 0.5f, 0.1f));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Minigames/BirdPooper/Obstacle.cs.meta
Normal file
3
Assets/Scripts/Minigames/BirdPooper/Obstacle.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 759c93e488a34f8d8f7abf6d8af5c73d
|
||||
timeCreated: 1763629702
|
||||
184
Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs
Normal file
184
Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using UnityEngine;
|
||||
using Core;
|
||||
using Core.Settings;
|
||||
using Core.Lifecycle;
|
||||
using AppleHillsCamera;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public class ObstacleSpawner : ManagedBehaviour
|
||||
{
|
||||
[Header("Spawn Configuration")]
|
||||
[Tooltip("Transform marking where obstacles spawn (off-screen right)")]
|
||||
[SerializeField] private Transform spawnPoint;
|
||||
|
||||
[Tooltip("Transform marking where obstacles despawn (off-screen left)")]
|
||||
[SerializeField] private Transform despawnPoint;
|
||||
|
||||
[Header("EdgeAnchor References")]
|
||||
[Tooltip("ScreenReferenceMarker to pass to spawned obstacles")]
|
||||
[SerializeField] private ScreenReferenceMarker referenceMarker;
|
||||
|
||||
[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;
|
||||
|
||||
private IBirdPooperSettings settings;
|
||||
private float spawnTimer;
|
||||
private bool isSpawning;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Load settings
|
||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate references
|
||||
if (spawnPoint == null)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawner] Spawn Point not assigned! Please assign a Transform in the Inspector.");
|
||||
}
|
||||
|
||||
if (despawnPoint == null)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
if (cameraAdapter == null)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawner] CameraScreenAdapter not assigned. EdgeAnchor will attempt to auto-find camera.");
|
||||
}
|
||||
|
||||
Debug.Log("[ObstacleSpawner] Initialized successfully");
|
||||
}
|
||||
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isSpawning || settings == null || spawnPoint == null) return;
|
||||
|
||||
spawnTimer += Time.deltaTime;
|
||||
|
||||
if (spawnTimer >= settings.ObstacleSpawnInterval)
|
||||
{
|
||||
SpawnObstacle();
|
||||
spawnTimer = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn a random obstacle at the spawn point position (Y = 0).
|
||||
/// </summary>
|
||||
private void SpawnObstacle()
|
||||
{
|
||||
if (obstaclePrefabs == null || obstaclePrefabs.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs to spawn!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (despawnPoint == null)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawner] Cannot spawn obstacle without despawn point reference!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Select random prefab
|
||||
GameObject selectedPrefab = obstaclePrefabs[Random.Range(0, obstaclePrefabs.Length)];
|
||||
|
||||
// Spawn at spawn point position with Y = 0
|
||||
Vector3 spawnPosition = new Vector3(spawnPoint.position.x, 0f, 0f);
|
||||
GameObject obstacleObj = Instantiate(selectedPrefab, spawnPosition, Quaternion.identity);
|
||||
|
||||
// Initialize obstacle with despawn X position and EdgeAnchor references
|
||||
Obstacle obstacle = obstacleObj.GetComponent<Obstacle>();
|
||||
if (obstacle != null)
|
||||
{
|
||||
obstacle.Initialize(despawnPoint.position.x, referenceMarker, cameraAdapter);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[ObstacleSpawner] Spawned prefab '{selectedPrefab.name}' does not have Obstacle component!");
|
||||
Destroy(obstacleObj);
|
||||
}
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] Spawned obstacle '{selectedPrefab.name}' at position {spawnPosition}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start spawning obstacles.
|
||||
/// Spawns the first obstacle immediately, then continues with interval-based spawning.
|
||||
/// </summary>
|
||||
public void StartSpawning()
|
||||
{
|
||||
isSpawning = true;
|
||||
spawnTimer = 0f;
|
||||
|
||||
// Spawn the first obstacle immediately
|
||||
SpawnObstacle();
|
||||
|
||||
Debug.Log("[ObstacleSpawner] Started spawning (first obstacle spawned immediately)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop spawning obstacles.
|
||||
/// </summary>
|
||||
public void StopSpawning()
|
||||
{
|
||||
isSpawning = false;
|
||||
Debug.Log("[ObstacleSpawner] Stopped spawning");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if spawner is currently active.
|
||||
/// </summary>
|
||||
public bool IsSpawning => isSpawning;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Draw gizmos in editor to visualize spawn/despawn points.
|
||||
/// </summary>
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (spawnPoint != null)
|
||||
{
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawLine(spawnPoint.position + Vector3.up * 10f, spawnPoint.position + Vector3.down * 10f);
|
||||
Gizmos.DrawWireSphere(spawnPoint.position, 0.5f);
|
||||
}
|
||||
|
||||
if (despawnPoint != null)
|
||||
{
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawLine(despawnPoint.position + Vector3.up * 10f, despawnPoint.position + Vector3.down * 10f);
|
||||
Gizmos.DrawWireSphere(despawnPoint.position, 0.5f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cfe555fe3074f75ab3c7b33bf3446e0
|
||||
timeCreated: 1763629720
|
||||
@@ -654,6 +654,15 @@ public class FollowerController : ManagedBehaviour
|
||||
{
|
||||
return _cachedPickupObject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transform of the held item's visual representation.
|
||||
/// Used for animating the held item sprite.
|
||||
/// </summary>
|
||||
public Transform GetHeldItemTransform()
|
||||
{
|
||||
return heldObjectRenderer?.transform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set held item from a GameObject. Extracts Pickup component and sets up visuals.
|
||||
|
||||
@@ -234,7 +234,7 @@ namespace UI
|
||||
case "Quarry":
|
||||
currentUIMode = UIMode.Puzzle;
|
||||
break;
|
||||
case "DivingForPictures" or "CardQualityControl":
|
||||
case "DivingForPictures" or "CardQualityControl" or "BirdPoop":
|
||||
currentUIMode = UIMode.Minigame;
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user