Update card sorting MVP
This commit is contained in:
@@ -4,6 +4,7 @@ using Data.CardSystem;
|
||||
using Minigames.CardSorting.Core;
|
||||
using Minigames.CardSorting.Data;
|
||||
using System.Collections.Generic;
|
||||
using UI.DragAndDrop.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.CardSorting.Controllers
|
||||
@@ -27,6 +28,8 @@ namespace Minigames.CardSorting.Controllers
|
||||
private HashSet<SortableItem> missedItems = new HashSet<SortableItem>(); // Items past visual end, moving to despawn
|
||||
private float currentSpeed;
|
||||
private SortableItem lastSpawnedItem; // Track last spawned item for distance-based spawning
|
||||
private float cachedSpawnOffsetX; // Cached random offset for next spawn
|
||||
private bool isGameOver = false; // Flag to stop conveyor when game ends
|
||||
|
||||
// Events - conveyor owns item lifecycle
|
||||
public event System.Action<SortableItem> OnItemSpawned; // Fired when new item spawns
|
||||
@@ -37,6 +40,7 @@ namespace Minigames.CardSorting.Controllers
|
||||
|
||||
public float CurrentSpeed => currentSpeed;
|
||||
public int ActiveItemCount => activeItems.Count;
|
||||
public bool IsGameOver => isGameOver;
|
||||
|
||||
public ConveyorBeltController(
|
||||
Transform spawnPoint,
|
||||
@@ -55,6 +59,9 @@ namespace Minigames.CardSorting.Controllers
|
||||
|
||||
this.currentSpeed = settings.InitialBeltSpeed;
|
||||
this.lastSpawnedItem = null; // No items spawned yet
|
||||
|
||||
// Initialize first cached offset
|
||||
this.cachedSpawnOffsetX = Random.Range(settings.SpawnOffsetX.x, settings.SpawnOffsetX.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,6 +69,9 @@ namespace Minigames.CardSorting.Controllers
|
||||
/// </summary>
|
||||
public void Update(float deltaTime, float gameProgress)
|
||||
{
|
||||
// Stop processing if game is over
|
||||
if (isGameOver) return;
|
||||
|
||||
UpdateBeltSpeed(gameProgress);
|
||||
CheckItemsOffBelt();
|
||||
CheckDistanceBasedSpawn(gameProgress);
|
||||
@@ -70,6 +80,7 @@ namespace Minigames.CardSorting.Controllers
|
||||
/// <summary>
|
||||
/// Check if we should spawn a new item based on distance from last spawn.
|
||||
/// Items spawn when last item has moved far enough from spawn point.
|
||||
/// Uses cached random X offset for spawn distance variation.
|
||||
/// </summary>
|
||||
private void CheckDistanceBasedSpawn(float gameProgress)
|
||||
{
|
||||
@@ -77,15 +88,24 @@ namespace Minigames.CardSorting.Controllers
|
||||
if (lastSpawnedItem == null)
|
||||
{
|
||||
SpawnNewItem(gameProgress);
|
||||
|
||||
// Generate new offset for next spawn
|
||||
cachedSpawnOffsetX = Random.Range(settings.SpawnOffsetX.x, settings.SpawnOffsetX.y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if last spawned item is far enough from spawn point
|
||||
float distanceFromSpawn = Mathf.Abs(lastSpawnedItem.transform.position.x - spawnPoint.position.x);
|
||||
|
||||
if (distanceFromSpawn >= settings.SpawnDistance) // Using InitialSpawnInterval as distance threshold
|
||||
// Use cached offset for required distance
|
||||
float requiredDistance = settings.SpawnDistance + cachedSpawnOffsetX;
|
||||
|
||||
if (distanceFromSpawn >= requiredDistance)
|
||||
{
|
||||
SpawnNewItem(gameProgress);
|
||||
|
||||
// Generate new offset for next spawn
|
||||
cachedSpawnOffsetX = Random.Range(settings.SpawnOffsetX.x, settings.SpawnOffsetX.y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,16 +165,27 @@ namespace Minigames.CardSorting.Controllers
|
||||
|
||||
GarbageItemDefinition garbage = SelectRandomGarbage();
|
||||
|
||||
GameObject obj = Object.Instantiate(garbagePrefab, spawnPoint.position, Quaternion.identity);
|
||||
// Apply random Y offset to spawn position
|
||||
float randomOffsetY = Random.Range(settings.SpawnOffsetY.x, settings.SpawnOffsetY.y);
|
||||
Vector3 spawnPos = spawnPoint.position + new Vector3(0f, randomOffsetY, 0f);
|
||||
|
||||
GameObject obj = Object.Instantiate(garbagePrefab, spawnPos, Quaternion.identity);
|
||||
SortableItem item = obj.GetComponent<SortableItem>();
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
item.SetupAsGarbage(garbage);
|
||||
|
||||
// Apply card size (garbage items use same size as cards)
|
||||
ApplyCardSize(item);
|
||||
|
||||
// Subscribe to item events
|
||||
item.OnItemDroppedInBox += HandleItemDroppedInBox;
|
||||
item.OnItemDroppedOnFloor += HandleItemDroppedOnFloor;
|
||||
item.OnItemReturnedToConveyor += HandleItemReturnedToConveyor;
|
||||
|
||||
// Subscribe to drag events to remove from tracking
|
||||
item.OnDragStarted += HandleItemDragStarted;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -177,16 +208,27 @@ namespace Minigames.CardSorting.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
GameObject obj = Object.Instantiate(cardPrefab, spawnPoint.position, Quaternion.identity);
|
||||
// Apply random Y offset to spawn position
|
||||
float randomOffsetY = Random.Range(settings.SpawnOffsetY.x, settings.SpawnOffsetY.y);
|
||||
Vector3 spawnPos = spawnPoint.position + new Vector3(0f, randomOffsetY, 0f);
|
||||
|
||||
GameObject obj = Object.Instantiate(cardPrefab, spawnPos, Quaternion.identity);
|
||||
SortableItem item = obj.GetComponent<SortableItem>();
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
item.SetupAsCard(cardData);
|
||||
|
||||
// Apply card size
|
||||
ApplyCardSize(item);
|
||||
|
||||
// Subscribe to item events
|
||||
item.OnItemDroppedInBox += HandleItemDroppedInBox;
|
||||
item.OnItemDroppedOnFloor += HandleItemDroppedOnFloor;
|
||||
item.OnItemReturnedToConveyor += HandleItemReturnedToConveyor;
|
||||
|
||||
// Subscribe to drag events to remove from tracking
|
||||
item.OnDragStarted += HandleItemDragStarted;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -296,55 +338,86 @@ namespace Minigames.CardSorting.Controllers
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handle when an item starts being dragged.
|
||||
/// Remove from tracking to prevent "fell off belt" detection while dragging.
|
||||
/// </summary>
|
||||
private void HandleItemDragStarted(DraggableObject draggableObj)
|
||||
{
|
||||
SortableItem item = draggableObj as SortableItem;
|
||||
if (item == null) return;
|
||||
|
||||
RemoveItemFromTracking(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle when an item is dropped in a box (correct or incorrect).
|
||||
/// Note: Item was already removed from activeItems when dragging started (HandleItemDragStarted).
|
||||
/// </summary>
|
||||
private void HandleItemDroppedInBox(SortableItem item, SortingBox box, bool correct)
|
||||
{
|
||||
// Remove from tracking and unsubscribe
|
||||
if (activeItems.Remove(item))
|
||||
// Clean up tracking (item already removed from activeItems when picked up)
|
||||
activeItems.Remove(item);
|
||||
missedItems.Remove(item);
|
||||
|
||||
// Clear lastSpawnedItem reference if this was it
|
||||
if (lastSpawnedItem == item)
|
||||
{
|
||||
// Also remove from missed items if it was there
|
||||
missedItems.Remove(item);
|
||||
|
||||
// Clear lastSpawnedItem reference if this was it
|
||||
if (lastSpawnedItem == item)
|
||||
{
|
||||
lastSpawnedItem = null;
|
||||
}
|
||||
|
||||
item.OnItemDroppedInBox -= HandleItemDroppedInBox;
|
||||
item.OnItemReturnedToConveyor -= HandleItemReturnedToConveyor;
|
||||
|
||||
// Emit event for game manager to handle scoring, passing box and correctness
|
||||
OnItemSorted?.Invoke(item, box, correct);
|
||||
lastSpawnedItem = null;
|
||||
}
|
||||
|
||||
// Unsubscribe from events
|
||||
item.OnItemDroppedInBox -= HandleItemDroppedInBox;
|
||||
item.OnItemDroppedOnFloor -= HandleItemDroppedOnFloor;
|
||||
item.OnItemReturnedToConveyor -= HandleItemReturnedToConveyor;
|
||||
item.OnDragStarted -= HandleItemDragStarted;
|
||||
|
||||
// Emit event for game manager to handle scoring, passing box and correctness
|
||||
OnItemSorted?.Invoke(item, box, correct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle when an item is returned to conveyor (dropped back on conveyor belt).
|
||||
/// Re-adds item to tracking so it continues moving along the belt.
|
||||
/// </summary>
|
||||
private void HandleItemReturnedToConveyor(SortableItem item)
|
||||
{
|
||||
if (item == null) return;
|
||||
|
||||
// Re-add to active tracking
|
||||
if (!activeItems.Contains(item))
|
||||
{
|
||||
activeItems.Add(item);
|
||||
Debug.Log($"[ConveyorBeltController] Item returned to conveyor: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle when an item is returned to conveyor (dropped outside box).
|
||||
/// Handle when an item is dropped on floor (dropped outside box).
|
||||
/// Item transitions to DroppedOnFloorState and gets destroyed.
|
||||
/// Note: Item was already removed from activeItems when dragging started (HandleItemDragStarted).
|
||||
/// </summary>
|
||||
private void HandleItemReturnedToConveyor(SortableItem item)
|
||||
private void HandleItemDroppedOnFloor(SortableItem item)
|
||||
{
|
||||
// Remove from tracking and unsubscribe (item will be destroyed)
|
||||
if (activeItems.Remove(item))
|
||||
// Clean up tracking (item already removed from activeItems when picked up)
|
||||
activeItems.Remove(item);
|
||||
missedItems.Remove(item);
|
||||
|
||||
if (lastSpawnedItem == item)
|
||||
{
|
||||
missedItems.Remove(item);
|
||||
|
||||
if (lastSpawnedItem == item)
|
||||
{
|
||||
lastSpawnedItem = null;
|
||||
}
|
||||
|
||||
item.OnItemDroppedInBox -= HandleItemDroppedInBox;
|
||||
item.OnItemReturnedToConveyor -= HandleItemReturnedToConveyor;
|
||||
|
||||
// Emit event for scoring
|
||||
OnItemDroppedOnFloor?.Invoke(item);
|
||||
|
||||
Debug.Log($"[ConveyorBeltController] Item dropped on floor: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
lastSpawnedItem = null;
|
||||
}
|
||||
|
||||
// Unsubscribe from events
|
||||
item.OnItemDroppedInBox -= HandleItemDroppedInBox;
|
||||
item.OnItemDroppedOnFloor -= HandleItemDroppedOnFloor;
|
||||
item.OnItemReturnedToConveyor -= HandleItemReturnedToConveyor;
|
||||
item.OnDragStarted -= HandleItemDragStarted;
|
||||
|
||||
// Emit event for scoring
|
||||
OnItemDroppedOnFloor?.Invoke(item);
|
||||
|
||||
Debug.Log($"[ConveyorBeltController] Item dropped on floor: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
}
|
||||
|
||||
private CardRarity DetermineRarity(float roll)
|
||||
@@ -365,6 +438,65 @@ namespace Minigames.CardSorting.Controllers
|
||||
{
|
||||
return settings.GarbageItems[Random.Range(0, settings.GarbageItems.Length)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply configured card size to spawned item.
|
||||
/// </summary>
|
||||
private void ApplyCardSize(SortableItem item)
|
||||
{
|
||||
if (item == null || item.Context == null || item.Context.RootTransform == null)
|
||||
return;
|
||||
|
||||
// Get the RectTransform to resize (root object)
|
||||
var rectTransform = item.Context.RootTransform.GetComponent<RectTransform>();
|
||||
if (rectTransform != null)
|
||||
{
|
||||
rectTransform.sizeDelta = settings.CardSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an item from tracking (called when picked up via event).
|
||||
/// Prevents item from being subject to belt end detection while being dragged.
|
||||
/// </summary>
|
||||
private void RemoveItemFromTracking(SortableItem item)
|
||||
{
|
||||
if (item == null) return;
|
||||
|
||||
bool wasTracked = activeItems.Remove(item);
|
||||
missedItems.Remove(item);
|
||||
|
||||
// Clear lastSpawnedItem reference if this was it
|
||||
if (lastSpawnedItem == item)
|
||||
{
|
||||
lastSpawnedItem = null;
|
||||
}
|
||||
|
||||
if (wasTracked)
|
||||
{
|
||||
Debug.Log($"[ConveyorBeltController] Item removed from tracking (picked up): {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop the conveyor belt when game ends.
|
||||
/// Disables dragging on all active items so they can't be interacted with.
|
||||
/// </summary>
|
||||
public void StopConveyor()
|
||||
{
|
||||
isGameOver = true;
|
||||
|
||||
// Disable dragging on all active items
|
||||
foreach (var item in activeItems)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
item.SetDraggingEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log("[ConveyorBeltController] Conveyor stopped - all items disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using UI.DragAndDrop.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.CardSorting.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Slot component for the conveyor belt.
|
||||
/// Allows detection when items are dropped back onto the conveyor.
|
||||
/// Place this on a UI element (Image/Panel) that covers the conveyor belt area.
|
||||
/// </summary>
|
||||
public class ConveyorBeltSlot : DraggableSlot
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if this slot can accept a specific draggable type.
|
||||
/// ConveyorBeltSlot accepts all SortableItems.
|
||||
/// </summary>
|
||||
public new bool CanAccept(DraggableObject draggable)
|
||||
{
|
||||
// Accept all sortable items
|
||||
return draggable is SortableItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is dropped on the conveyor.
|
||||
/// Items dropped here should return to their conveyor state.
|
||||
/// </summary>
|
||||
public void OnItemDroppedOnConveyor(SortableItem item)
|
||||
{
|
||||
if (item == null) return;
|
||||
|
||||
Debug.Log($"[ConveyorBeltSlot] Item dropped back on conveyor: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9304d17587314133a4d8d1e582cfbf81
|
||||
timeCreated: 1763590821
|
||||
@@ -28,7 +28,8 @@ namespace Minigames.CardSorting.Core
|
||||
|
||||
// Events - item emits notifications, conveyor subscribes
|
||||
public event System.Action<SortableItem, SortingBox, bool> OnItemDroppedInBox;
|
||||
public event System.Action<SortableItem> OnItemReturnedToConveyor;
|
||||
public event System.Action<SortableItem> OnItemDroppedOnFloor;
|
||||
public event System.Action<SortableItem> OnItemReturnedToConveyor; // Fired when dropped back on conveyor
|
||||
|
||||
// Public accessors
|
||||
public SortableItemContext Context => context;
|
||||
@@ -142,9 +143,10 @@ namespace Minigames.CardSorting.Core
|
||||
{
|
||||
base.OnDragEndedHook();
|
||||
|
||||
// Validate drop on sorting box
|
||||
// Check what type of slot we're over
|
||||
if (CurrentSlot is SortingBox box)
|
||||
{
|
||||
// Dropped in sorting box
|
||||
bool correctSort = box.ValidateItem(this);
|
||||
|
||||
// Fire event IMMEDIATELY when card is released over bin
|
||||
@@ -162,10 +164,28 @@ namespace Minigames.CardSorting.Core
|
||||
ChangeState("SortedIncorrectlyState");
|
||||
}
|
||||
}
|
||||
else if (CurrentSlot is ConveyorBeltSlot conveyorSlot)
|
||||
{
|
||||
// Dropped back on conveyor - return to conveyor state
|
||||
Logging.Debug("[SortableItem] Dropped back on conveyor, returning to OnConveyorState");
|
||||
|
||||
// Notify conveyor slot
|
||||
conveyorSlot.OnItemDroppedOnConveyor(this);
|
||||
|
||||
// Fire event for conveyor controller to re-add to tracking
|
||||
OnItemReturnedToConveyor?.Invoke(this);
|
||||
|
||||
// Return to conveyor state
|
||||
ChangeState("OnConveyorState");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dropped outside valid box - transition to dropped on floor state
|
||||
Logging.Debug("[SortableItem] Dropped outside box, transitioning to floor state");
|
||||
// Dropped outside valid box or conveyor - fire event then transition to dropped on floor state
|
||||
Logging.Debug("[SortableItem] Dropped outside box/conveyor, transitioning to floor state");
|
||||
|
||||
// Fire event for conveyor controller to handle scoring
|
||||
OnItemDroppedOnFloor?.Invoke(this);
|
||||
|
||||
ChangeState("DroppedOnFloorState");
|
||||
}
|
||||
}
|
||||
@@ -174,6 +194,7 @@ namespace Minigames.CardSorting.Core
|
||||
/// <summary>
|
||||
/// Detect which slot (if any) is under the pointer during drag.
|
||||
/// Updates CurrentSlot for drop detection.
|
||||
/// Scans for both SortingBox and ConveyorBeltSlot.
|
||||
/// </summary>
|
||||
private void DetectSlotUnderPointer(UnityEngine.EventSystems.PointerEventData eventData)
|
||||
{
|
||||
@@ -181,29 +202,46 @@ namespace Minigames.CardSorting.Core
|
||||
var raycastResults = new System.Collections.Generic.List<UnityEngine.EventSystems.RaycastResult>();
|
||||
UnityEngine.EventSystems.EventSystem.current.RaycastAll(eventData, raycastResults);
|
||||
|
||||
SortingBox hoveredBox = null;
|
||||
DraggableSlot hoveredSlot = null;
|
||||
|
||||
// Find first SortingBox in raycast results
|
||||
// Find first slot (SortingBox or ConveyorBeltSlot) in raycast results
|
||||
foreach (var result in raycastResults)
|
||||
{
|
||||
// Check for SortingBox first (higher priority)
|
||||
var box = result.gameObject.GetComponentInParent<SortingBox>();
|
||||
if (box != null)
|
||||
{
|
||||
hoveredBox = box;
|
||||
hoveredSlot = box;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for ConveyorBeltSlot if no box found
|
||||
var conveyorSlot = result.gameObject.GetComponentInParent<ConveyorBeltSlot>();
|
||||
if (conveyorSlot != null)
|
||||
{
|
||||
hoveredSlot = conveyorSlot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update current slot (used in OnDragEndedHook)
|
||||
if (hoveredBox != null && hoveredBox != CurrentSlot)
|
||||
if (hoveredSlot != null && hoveredSlot != CurrentSlot)
|
||||
{
|
||||
_currentSlot = hoveredBox;
|
||||
Logging.Debug($"[SortableItem] Now hovering over {hoveredBox.BoxType} box");
|
||||
_currentSlot = hoveredSlot;
|
||||
|
||||
if (hoveredSlot is SortingBox sortBox)
|
||||
{
|
||||
Logging.Debug($"[SortableItem] Now hovering over {sortBox.BoxType} box");
|
||||
}
|
||||
else if (hoveredSlot is ConveyorBeltSlot)
|
||||
{
|
||||
Logging.Debug("[SortableItem] Now hovering over conveyor belt");
|
||||
}
|
||||
}
|
||||
else if (hoveredBox == null && CurrentSlot != null)
|
||||
else if (hoveredSlot == null && CurrentSlot != null)
|
||||
{
|
||||
_currentSlot = null;
|
||||
Logging.Debug("[SortableItem] No longer over any box");
|
||||
Logging.Debug("[SortableItem] No longer over any slot");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,15 @@ namespace Minigames.CardSorting.Core
|
||||
[Header("Effects")]
|
||||
[SerializeField] private CinemachineImpulseSource impulseSource; // Screen shake on incorrect sort
|
||||
|
||||
[Header("UI References")]
|
||||
[SerializeField] private UI.LivesDisplay livesDisplay;
|
||||
|
||||
// Settings
|
||||
private ICardSortingSettings _settings;
|
||||
|
||||
// Lives tracking
|
||||
private int currentLives;
|
||||
|
||||
// Controllers (lazy init)
|
||||
private ConveyorBeltController _conveyorController;
|
||||
private ConveyorBeltController Conveyor => _conveyorController ??= new ConveyorBeltController(
|
||||
@@ -43,6 +49,9 @@ namespace Minigames.CardSorting.Core
|
||||
_settings
|
||||
);
|
||||
|
||||
// Public accessor for states to check game over status
|
||||
public ConveyorBeltController ConveyorController => Conveyor;
|
||||
|
||||
private SortingScoreController _scoreController;
|
||||
private SortingScoreController Score => _scoreController ??= new SortingScoreController(_settings);
|
||||
|
||||
@@ -152,6 +161,13 @@ namespace Minigames.CardSorting.Core
|
||||
// Reset score
|
||||
Score.Reset();
|
||||
|
||||
// Initialize lives
|
||||
currentLives = _settings.MaxLives;
|
||||
if (livesDisplay != null)
|
||||
{
|
||||
livesDisplay.Initialize(currentLives);
|
||||
}
|
||||
|
||||
OnGameStarted?.Invoke();
|
||||
|
||||
// Set input mode to game
|
||||
@@ -163,6 +179,61 @@ namespace Minigames.CardSorting.Core
|
||||
Logging.Debug("[SortingGameManager] Game started!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified feedback for wrong actions (trash penalties).
|
||||
/// Plays blink animation on item and triggers camera shake.
|
||||
/// </summary>
|
||||
public void PlayWrongStateFeedback(SortableItem item)
|
||||
{
|
||||
// Camera shake
|
||||
if (impulseSource != null)
|
||||
{
|
||||
impulseSource.GenerateImpulse();
|
||||
}
|
||||
|
||||
// Blink the item red (if it still exists)
|
||||
if (item != null && item.Context != null && item.Context.Animator != null)
|
||||
{
|
||||
UnityEngine.UI.Image imageToBlink = null;
|
||||
|
||||
if (item.Context.CardDisplay != null)
|
||||
{
|
||||
imageToBlink = item.Context.CardDisplay.GetComponent<UnityEngine.UI.Image>();
|
||||
}
|
||||
else if (item.Context.GarbageVisual != null)
|
||||
{
|
||||
imageToBlink = item.Context.GarbageVisual.GetComponent<UnityEngine.UI.Image>();
|
||||
}
|
||||
|
||||
if (imageToBlink != null)
|
||||
{
|
||||
item.Context.Animator.BlinkRed(imageToBlink, 0.15f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lose a life. If lives reach 0, end the game.
|
||||
/// </summary>
|
||||
private void LoseLife()
|
||||
{
|
||||
currentLives--;
|
||||
|
||||
if (livesDisplay != null)
|
||||
{
|
||||
livesDisplay.RemoveLife();
|
||||
}
|
||||
|
||||
Logging.Debug($"[SortingGameManager] Life lost! Remaining lives: {currentLives}");
|
||||
|
||||
// Check for game over
|
||||
if (currentLives <= 0)
|
||||
{
|
||||
Logging.Debug("[SortingGameManager] No lives remaining - Game Over!");
|
||||
EndGame();
|
||||
}
|
||||
}
|
||||
|
||||
public void EndGame()
|
||||
{
|
||||
if (isGameOver) return;
|
||||
@@ -170,6 +241,9 @@ namespace Minigames.CardSorting.Core
|
||||
isGameOver = true;
|
||||
isGameActive = false;
|
||||
|
||||
// Stop the conveyor and disable all items
|
||||
Conveyor.StopConveyor();
|
||||
|
||||
// Calculate rewards
|
||||
int boosterReward = Score.CalculateBoosterReward();
|
||||
|
||||
@@ -201,7 +275,7 @@ namespace Minigames.CardSorting.Core
|
||||
/// Called when item reaches visual end of belt (via conveyor event).
|
||||
/// Item continues moving off-screen until despawn point.
|
||||
/// Scoring rules:
|
||||
/// - Trash fell off: Negative score (penalty)
|
||||
/// - Trash fell off: Negative score (penalty) + lose life
|
||||
/// - Card fell off: Neutral (no score change)
|
||||
/// </summary>
|
||||
private void OnConveyorItemFellOff(SortableItem item)
|
||||
@@ -211,6 +285,8 @@ namespace Minigames.CardSorting.Core
|
||||
if (item.IsGarbage)
|
||||
{
|
||||
Score.RecordMissedItem();
|
||||
PlayWrongStateFeedback(item);
|
||||
LoseLife();
|
||||
Logging.Debug($"[SortingGameManager] Trash fell off belt! {item.GarbageItem?.DisplayName} - PENALTY");
|
||||
}
|
||||
else
|
||||
@@ -228,7 +304,7 @@ namespace Minigames.CardSorting.Core
|
||||
/// <summary>
|
||||
/// Called when item is dropped on floor (via conveyor event).
|
||||
/// Scoring rules:
|
||||
/// - Trash dropped on floor: Negative score (penalty)
|
||||
/// - Trash dropped on floor: Negative score (penalty) + lose life
|
||||
/// - Card dropped on floor: Neutral (no score change)
|
||||
/// </summary>
|
||||
private void OnConveyorItemDroppedOnFloor(SortableItem item)
|
||||
@@ -238,13 +314,9 @@ namespace Minigames.CardSorting.Core
|
||||
if (item.IsGarbage)
|
||||
{
|
||||
Score.RecordIncorrectSort();
|
||||
PlayWrongStateFeedback(item);
|
||||
LoseLife();
|
||||
Logging.Debug($"[SortingGameManager] Trash dropped on floor! {item.GarbageItem?.DisplayName} - PENALTY");
|
||||
|
||||
// Trigger screen shake for trash dropped on floor
|
||||
if (impulseSource != null)
|
||||
{
|
||||
impulseSource.GenerateImpulse();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -270,7 +342,7 @@ namespace Minigames.CardSorting.Core
|
||||
/// Handles scoring only - the state (SortedCorrectlyState/SortedIncorrectlyState) handles animation and destruction.
|
||||
/// Scoring rules:
|
||||
/// - Correct sort: Positive score (cards or trash in correct box)
|
||||
/// - Incorrect trash: Negative score (trash in wrong box)
|
||||
/// - Incorrect trash: Negative score (trash in wrong box) + lose life
|
||||
/// - Incorrect card: Neutral (no score change)
|
||||
/// </summary>
|
||||
private void OnConveyorItemSorted(SortableItem item, SortingBox box, bool correct)
|
||||
@@ -290,16 +362,12 @@ namespace Minigames.CardSorting.Core
|
||||
if (item.IsGarbage)
|
||||
{
|
||||
Score.RecordIncorrectSort();
|
||||
PlayWrongStateFeedback(item);
|
||||
LoseLife();
|
||||
Logging.Debug($"[SortingGameManager] Incorrect trash sort! {item.GarbageItem?.DisplayName} - PENALTY");
|
||||
|
||||
// Fire global incorrect sort event for effects
|
||||
OnItemSortedIncorrectly?.Invoke(item);
|
||||
|
||||
// Trigger screen shake
|
||||
if (impulseSource != null)
|
||||
{
|
||||
impulseSource.GenerateImpulse();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -13,10 +13,12 @@ namespace Minigames.CardSorting.StateMachine.States
|
||||
public class BeingDraggedState : AppleState
|
||||
{
|
||||
private SortableItemContext _context;
|
||||
private SortableItem _item;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_context = GetComponentInParent<SortableItemContext>();
|
||||
_item = GetComponentInParent<SortableItem>();
|
||||
}
|
||||
|
||||
public override void OnEnterState()
|
||||
@@ -24,9 +26,7 @@ namespace Minigames.CardSorting.StateMachine.States
|
||||
if (_context == null) return;
|
||||
|
||||
_context.IsOnConveyor = false;
|
||||
|
||||
// Visual feedback: scale up root transform by 10%
|
||||
// Use OriginalScale from context (captured at spawn, preserves world-space Canvas scale)
|
||||
|
||||
if (_context.RootTransform != null && _context.Animator != null)
|
||||
{
|
||||
Vector3 targetScale = _context.OriginalScale * 1.1f;
|
||||
|
||||
@@ -25,45 +25,11 @@ namespace Minigames.CardSorting.StateMachine.States
|
||||
|
||||
_context.IsOnConveyor = false;
|
||||
|
||||
Logging.Debug("[DroppedOnFloorState] Item dropped on floor, blinking red then disappearing");
|
||||
Logging.Debug("[DroppedOnFloorState] Item dropped on floor, disappearing");
|
||||
|
||||
// Blink red briefly, then play disappear animation
|
||||
StartBlinkThenDisappear();
|
||||
}
|
||||
|
||||
private void StartBlinkThenDisappear()
|
||||
{
|
||||
if (_context.Animator == null || _item == null) return;
|
||||
|
||||
// Get the image to blink
|
||||
UnityEngine.UI.Image imageToBlink = null;
|
||||
|
||||
if (_context.CardDisplay != null)
|
||||
{
|
||||
imageToBlink = _context.CardDisplay.GetComponent<UnityEngine.UI.Image>();
|
||||
}
|
||||
else if (_context.GarbageVisual != null)
|
||||
{
|
||||
imageToBlink = _context.GarbageVisual.GetComponent<UnityEngine.UI.Image>();
|
||||
}
|
||||
|
||||
if (imageToBlink != null)
|
||||
{
|
||||
// Blink red briefly (2-3 times), then stop and disappear
|
||||
_context.Animator.BlinkRed(imageToBlink, 0.15f); // Fast blink
|
||||
|
||||
// After brief delay, stop blinking and play disappear animation
|
||||
_context.Animator.AnimateScale(_context.RootTransform.localScale, 0.5f, () =>
|
||||
{
|
||||
_context.Animator.StopBlinking();
|
||||
PlayDisappearAnimation();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// No image found, just disappear directly
|
||||
PlayDisappearAnimation();
|
||||
}
|
||||
// Feedback (blink + shake) already triggered by game manager when scoring (for trash)
|
||||
// Just play disappear animation
|
||||
PlayDisappearAnimation();
|
||||
}
|
||||
|
||||
private void PlayDisappearAnimation()
|
||||
|
||||
@@ -44,6 +44,12 @@ namespace Minigames.CardSorting.StateMachine.States
|
||||
{
|
||||
if (_context == null || !_context.IsOnConveyor) return;
|
||||
|
||||
// Stop moving if game is over
|
||||
if (SortingGameManager.Instance.ConveyorController.IsGameOver)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue moving item toward despawn point (same logic as OnConveyorState)
|
||||
Vector3 movement = Vector3.right * _context.ConveyorSpeed * Time.deltaTime;
|
||||
_context.RootTransform.position += movement;
|
||||
|
||||
@@ -40,6 +40,12 @@ namespace Minigames.CardSorting.StateMachine.States
|
||||
{
|
||||
if (_context == null || !_context.IsOnConveyor) return;
|
||||
|
||||
// Stop moving if game is over
|
||||
if (SortingGameManager.Instance.ConveyorController.IsGameOver)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Move item along conveyor (right direction)
|
||||
Vector3 movement = Vector3.right * _context.ConveyorSpeed * Time.deltaTime;
|
||||
_context.RootTransform.position += movement;
|
||||
|
||||
@@ -26,45 +26,11 @@ namespace Minigames.CardSorting.StateMachine.States
|
||||
|
||||
_context.IsOnConveyor = false;
|
||||
|
||||
Logging.Debug("[SortedIncorrectlyState] Item incorrectly sorted, blinking red then tweening to box");
|
||||
Logging.Debug("[SortedIncorrectlyState] Item incorrectly sorted, tweening to box then falling in");
|
||||
|
||||
// Start blinking red briefly, then tween to box
|
||||
StartBlinkThenTween();
|
||||
}
|
||||
|
||||
private void StartBlinkThenTween()
|
||||
{
|
||||
if (_context.Animator == null || _item == null) return;
|
||||
|
||||
// Get the image to blink
|
||||
UnityEngine.UI.Image imageToBlink = null;
|
||||
|
||||
if (_context.CardDisplay != null)
|
||||
{
|
||||
imageToBlink = _context.CardDisplay.GetComponent<UnityEngine.UI.Image>();
|
||||
}
|
||||
else if (_context.GarbageVisual != null)
|
||||
{
|
||||
imageToBlink = _context.GarbageVisual.GetComponent<UnityEngine.UI.Image>();
|
||||
}
|
||||
|
||||
if (imageToBlink != null)
|
||||
{
|
||||
// Blink red briefly (2-3 times), then stop and continue with tween
|
||||
_context.Animator.BlinkRed(imageToBlink, 0.15f); // Fast blink
|
||||
|
||||
// After brief delay, stop blinking and tween to box
|
||||
_context.Animator.AnimateScale(_context.RootTransform.localScale, 0.5f, () =>
|
||||
{
|
||||
_context.Animator.StopBlinking();
|
||||
TweenToBoxThenFall();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// No image found, just tween directly
|
||||
TweenToBoxThenFall();
|
||||
}
|
||||
// Feedback (blink + shake) already triggered by game manager when scoring
|
||||
// Just proceed with animation
|
||||
TweenToBoxThenFall();
|
||||
}
|
||||
|
||||
private void TweenToBoxThenFall()
|
||||
|
||||
208
Assets/Scripts/Minigames/CardSorting/UI/LivesDisplay.cs
Normal file
208
Assets/Scripts/Minigames/CardSorting/UI/LivesDisplay.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minigames.CardSorting.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays and animates lives for the card sorting minigame.
|
||||
/// Manages a layout group of life icons with add/remove animations.
|
||||
/// </summary>
|
||||
public class LivesDisplay : MonoBehaviour
|
||||
{
|
||||
[Header("Configuration")]
|
||||
[SerializeField] private LayoutGroup layoutGroup;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private float animationDuration = 0.3f;
|
||||
[SerializeField] private float scaleMultiplier = 1.5f;
|
||||
|
||||
private GameObject lifeIconPrefab;
|
||||
private List<GameObject> lifeIcons = new List<GameObject>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (layoutGroup == null)
|
||||
{
|
||||
layoutGroup = GetComponent<LayoutGroup>();
|
||||
}
|
||||
|
||||
// Find the prefab (first child) if not assigned
|
||||
if (lifeIconPrefab == null && layoutGroup != null && layoutGroup.transform.childCount > 0)
|
||||
{
|
||||
lifeIconPrefab = layoutGroup.transform.GetChild(0).gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the display with the maximum number of lives.
|
||||
/// Clones the prefab element multiple times.
|
||||
/// </summary>
|
||||
public void Initialize(int maxLives)
|
||||
{
|
||||
// Clear existing lives
|
||||
ClearAllLives();
|
||||
|
||||
if (lifeIconPrefab == null)
|
||||
{
|
||||
Debug.LogError("[LivesDisplay] No life icon prefab assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (layoutGroup == null)
|
||||
{
|
||||
Debug.LogError("[LivesDisplay] No layout group assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure prefab is active to use as template
|
||||
bool wasActive = lifeIconPrefab.activeSelf;
|
||||
lifeIconPrefab.SetActive(true);
|
||||
lifeIcons.Add(lifeIconPrefab);
|
||||
|
||||
// Create life icons
|
||||
for (int i = 0; i < maxLives - 1; i++)
|
||||
{
|
||||
GameObject icon = Instantiate(lifeIconPrefab, layoutGroup.transform);
|
||||
icon.SetActive(true);
|
||||
lifeIcons.Add(icon);
|
||||
}
|
||||
|
||||
// Hide the original prefab
|
||||
lifeIconPrefab.SetActive(wasActive);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a life with animation.
|
||||
/// </summary>
|
||||
public void RemoveLife()
|
||||
{
|
||||
if (lifeIcons.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[LivesDisplay] No lives left to remove!");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject icon = lifeIcons[0];
|
||||
lifeIcons.RemoveAt(0);
|
||||
|
||||
// Play removal animation
|
||||
StartCoroutine(AnimateLifeRemoval(icon));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a life with animation.
|
||||
/// </summary>
|
||||
public void AddLife()
|
||||
{
|
||||
if (lifeIconPrefab == null || layoutGroup == null)
|
||||
{
|
||||
Debug.LogError("[LivesDisplay] Cannot add life - missing prefab or layout group!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new life icon
|
||||
GameObject icon = Instantiate(lifeIconPrefab, layoutGroup.transform);
|
||||
icon.SetActive(true);
|
||||
lifeIcons.Add(icon);
|
||||
|
||||
// Play addition animation
|
||||
StartCoroutine(AnimateLifeAddition(icon));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all lives (for reset).
|
||||
/// </summary>
|
||||
private void ClearAllLives()
|
||||
{
|
||||
foreach (var icon in lifeIcons)
|
||||
{
|
||||
if (icon != null)
|
||||
{
|
||||
Destroy(icon);
|
||||
}
|
||||
}
|
||||
lifeIcons.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animate life removal: scale up, then scale down and destroy.
|
||||
/// </summary>
|
||||
private IEnumerator AnimateLifeRemoval(GameObject icon)
|
||||
{
|
||||
if (icon == null) yield break;
|
||||
|
||||
Vector3 originalScale = icon.transform.localScale;
|
||||
Vector3 targetScale = originalScale * scaleMultiplier;
|
||||
|
||||
// Scale up
|
||||
yield return AnimateScale(icon.transform, originalScale, targetScale, animationDuration * 0.5f);
|
||||
|
||||
// Scale down to zero
|
||||
yield return AnimateScale(icon.transform, targetScale, Vector3.zero, animationDuration * 0.5f);
|
||||
|
||||
// Destroy
|
||||
if (icon != null)
|
||||
{
|
||||
Destroy(icon);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animate life addition: start at zero, scale up beyond target, then settle to normal.
|
||||
/// </summary>
|
||||
private IEnumerator AnimateLifeAddition(GameObject icon)
|
||||
{
|
||||
if (icon == null) yield break;
|
||||
|
||||
Vector3 originalScale = icon.transform.localScale;
|
||||
Vector3 overshootScale = originalScale * scaleMultiplier;
|
||||
|
||||
// Start at zero
|
||||
icon.transform.localScale = Vector3.zero;
|
||||
|
||||
// Scale up to overshoot
|
||||
yield return AnimateScale(icon.transform, Vector3.zero, overshootScale, animationDuration * 0.5f);
|
||||
|
||||
// Settle back to original scale
|
||||
yield return AnimateScale(icon.transform, overshootScale, originalScale, animationDuration * 0.5f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic scale animation coroutine.
|
||||
/// </summary>
|
||||
private IEnumerator AnimateScale(Transform target, Vector3 from, Vector3 to, float duration)
|
||||
{
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
|
||||
// Use ease-in-out curve for smooth animation
|
||||
t = t * t * (3f - 2f * t); // Smoothstep
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
target.localScale = Vector3.Lerp(from, to, t);
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Ensure final scale is set
|
||||
if (target != null)
|
||||
{
|
||||
target.localScale = to;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current number of lives displayed.
|
||||
/// </summary>
|
||||
public int CurrentLives => lifeIcons.Count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8bcb252540442e593cc6467c518ff75
|
||||
timeCreated: 1763586518
|
||||
@@ -99,7 +99,8 @@ namespace Minigames.CardSorting.UI
|
||||
{
|
||||
if (scoreText == null) return;
|
||||
|
||||
scoreText.text = $"Score: {newScore}";
|
||||
// Only display numerical value (label is separate UI element)
|
||||
scoreText.text = $"Score: {newScore.ToString()}";
|
||||
|
||||
// Color based on positive/negative
|
||||
if (newScore >= 0)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Minigames.CardSorting.Core;
|
||||
using System;
|
||||
using Core;
|
||||
using Minigames.CardSorting.Core;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -70,26 +72,26 @@ namespace Minigames.CardSorting.UI
|
||||
|
||||
if (gameManager == null) return;
|
||||
|
||||
// Populate data
|
||||
// Populate data - only numerical values (labels are separate UI elements)
|
||||
if (finalScoreText != null)
|
||||
finalScoreText.text = $"Final Score: {gameManager.CurrentScore}";
|
||||
finalScoreText.text = gameManager.CurrentScore.ToString();
|
||||
|
||||
if (correctSortsText != null)
|
||||
correctSortsText.text = $"Correct: {gameManager.CorrectSorts}";
|
||||
correctSortsText.text = gameManager.CorrectSorts.ToString();
|
||||
|
||||
if (incorrectSortsText != null)
|
||||
incorrectSortsText.text = $"Incorrect: {gameManager.IncorrectSorts}";
|
||||
incorrectSortsText.text = gameManager.IncorrectSorts.ToString();
|
||||
|
||||
if (missedItemsText != null)
|
||||
missedItemsText.text = $"Missed: {gameManager.MissedItems}";
|
||||
missedItemsText.text = gameManager.MissedItems.ToString();
|
||||
|
||||
if (accuracyText != null)
|
||||
accuracyText.text = $"Accuracy: {gameManager.Accuracy:P0}";
|
||||
accuracyText.text = gameManager.Accuracy.ToString("P0");
|
||||
|
||||
// Calculate boosters (already granted by manager)
|
||||
int boosters = gameManager.CorrectSorts; // Simple 1:1 ratio
|
||||
if (boostersEarnedText != null)
|
||||
boostersEarnedText.text = $"Boosters Earned: {boosters}";
|
||||
boostersEarnedText.text = boosters.ToString();
|
||||
|
||||
// Show screen
|
||||
if (canvasGroup != null)
|
||||
@@ -104,7 +106,7 @@ namespace Minigames.CardSorting.UI
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseClicked()
|
||||
private async void OnCloseClicked()
|
||||
{
|
||||
// Hide screen
|
||||
if (canvasGroup != null)
|
||||
@@ -118,8 +120,9 @@ namespace Minigames.CardSorting.UI
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// Could also trigger scene transition, return to menu, etc.
|
||||
Debug.Log("[SortingResultsScreen] Closed results screen");
|
||||
// TODO: Smarter replay?
|
||||
var progress = new Progress<float>(p => Logging.Debug($"Loading progress: {p * 100:F0}%"));
|
||||
await SceneManagerService.Instance.ReloadCurrentScene(progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user