First MVP of sorting minigame #60

Merged
tschesky merged 2 commits from sort_minigame into main 2025-11-19 13:56:10 +00:00
43 changed files with 2739 additions and 1 deletions
Showing only changes of commit 359e0e35bd - Show all commits

View File

@@ -3,6 +3,7 @@ using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Core.Settings;
namespace AppleHills.Core.Settings.Editor
{
@@ -10,7 +11,7 @@ namespace AppleHills.Core.Settings.Editor
{
private Vector2 scrollPosition;
private List<BaseSettings> allSettings = new List<BaseSettings>();
private string[] tabNames = new string[] { "Player & Follower", "Interaction & Items", "Diving Minigame", "Card System" };
private string[] tabNames = new string[] { "Player & Follower", "Interaction & Items", "Diving Minigame", "Card System", "Card Sorting" };
private int selectedTab = 0;
private Dictionary<string, SerializedObject> serializedSettingsObjects = new Dictionary<string, SerializedObject>();
private GUIStyle headerStyle;
@@ -49,6 +50,7 @@ namespace AppleHills.Core.Settings.Editor
CreateSettingsIfMissing<InteractionSettings>("InteractionSettings");
CreateSettingsIfMissing<DivingMinigameSettings>("DivingMinigameSettings");
CreateSettingsIfMissing<CardSystemSettings>("CardSystemSettings");
CreateSettingsIfMissing<CardSortingSettings>("CardSortingSettings");
}
private void CreateSettingsIfMissing<T>(string fileName) where T : BaseSettings
@@ -118,6 +120,9 @@ namespace AppleHills.Core.Settings.Editor
case 3: // Card System
DrawSettingsEditor<CardSystemSettings>();
break;
case 4: // Card Sorting
DrawSettingsEditor<CardSortingSettings>();
break;
}
EditorGUILayout.EndScrollView();

View File

@@ -0,0 +1,97 @@
using AppleHills.Core.Settings;
using Minigames.CardSorting.Data;
using UnityEngine;
namespace Core.Settings
{
/// <summary>
/// Settings for Card Sorting minigame.
/// Follows DivingMinigameSettings pattern.
/// </summary>
[CreateAssetMenu(fileName = "CardSortingSettings", menuName = "AppleHills/Settings/CardSorting", order = 4)]
public class CardSortingSettings : BaseSettings, ICardSortingSettings
{
[Header("Timing")]
[Tooltip("Total game duration in seconds")]
[SerializeField] private float gameDuration = 120f;
[Tooltip("Initial time between item spawns (seconds)")]
[SerializeField] private float initialSpawnInterval = 2f;
[Tooltip("Minimum time between item spawns as difficulty increases (seconds)")]
[SerializeField] private float minimumSpawnInterval = 0.5f;
[Header("Conveyor Speed")]
[Tooltip("Initial belt movement speed")]
[SerializeField] private float initialBeltSpeed = 1f;
[Tooltip("Maximum belt movement speed")]
[SerializeField] private float maxBeltSpeed = 3f;
[Tooltip("Curve for difficulty progression (X=time%, Y=speed multiplier)")]
[SerializeField] private AnimationCurve speedCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
[Header("Item Pools")]
[Tooltip("Garbage items that can spawn (banana peels, cans, receipts, etc.)")]
[SerializeField] private GarbageItemDefinition[] garbageItems = new GarbageItemDefinition[0];
[Header("Spawn Weights")]
[Tooltip("Weight for spawning normal rarity cards")]
[Range(0, 100)] [SerializeField] private float normalCardWeight = 40f;
[Tooltip("Weight for spawning rare rarity cards")]
[Range(0, 100)] [SerializeField] private float rareCardWeight = 30f;
[Tooltip("Weight for spawning legendary rarity cards")]
[Range(0, 100)] [SerializeField] private float legendCardWeight = 20f;
[Tooltip("Weight for spawning garbage items")]
[Range(0, 100)] [SerializeField] private float garbageWeight = 10f;
[Header("Scoring")]
[Tooltip("Points awarded for correct sort")]
[SerializeField] private int correctSortPoints = 10;
[Tooltip("Points deducted for incorrect sort")]
[SerializeField] private int incorrectSortPenalty = -5;
[Tooltip("Points deducted when item falls off belt")]
[SerializeField] private int missedItemPenalty = -3;
[Header("Rewards")]
[Tooltip("Booster packs awarded per correct sort")]
[SerializeField] private int boosterPacksPerCorrectItem = 1;
// Interface implementation
public float GameDuration => gameDuration;
public float InitialSpawnInterval => initialSpawnInterval;
public float MinimumSpawnInterval => minimumSpawnInterval;
public float InitialBeltSpeed => initialBeltSpeed;
public float MaxBeltSpeed => maxBeltSpeed;
public AnimationCurve SpeedCurve => speedCurve;
public GarbageItemDefinition[] GarbageItems => garbageItems;
public float NormalCardWeight => normalCardWeight;
public float RareCardWeight => rareCardWeight;
public float LegendCardWeight => legendCardWeight;
public float GarbageWeight => garbageWeight;
public int CorrectSortPoints => correctSortPoints;
public int IncorrectSortPenalty => incorrectSortPenalty;
public int MissedItemPenalty => missedItemPenalty;
public int BoosterPacksPerCorrectItem => boosterPacksPerCorrectItem;
public override void OnValidate()
{
base.OnValidate();
gameDuration = Mathf.Max(1f, gameDuration);
initialSpawnInterval = Mathf.Max(0.1f, initialSpawnInterval);
minimumSpawnInterval = Mathf.Max(0.1f, minimumSpawnInterval);
minimumSpawnInterval = Mathf.Min(minimumSpawnInterval, initialSpawnInterval);
initialBeltSpeed = Mathf.Max(0.1f, initialBeltSpeed);
maxBeltSpeed = Mathf.Max(initialBeltSpeed, maxBeltSpeed);
correctSortPoints = Mathf.Max(0, correctSortPoints);
boosterPacksPerCorrectItem = Mathf.Max(0, boosterPacksPerCorrectItem);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a0fd5f8ab5d74b12968501dd4e3cc416
timeCreated: 1763461789

View File

@@ -0,0 +1,40 @@
using Minigames.CardSorting.Data;
using UnityEngine;
namespace Core.Settings
{
/// <summary>
/// Settings interface for Card Sorting minigame.
/// Accessed via GameManager.GetSettingsObject&lt;ICardSortingSettings&gt;()
/// </summary>
public interface ICardSortingSettings
{
// Timing
float GameDuration { get; }
float InitialSpawnInterval { get; }
float MinimumSpawnInterval { get; }
// Conveyor Speed
float InitialBeltSpeed { get; }
float MaxBeltSpeed { get; }
AnimationCurve SpeedCurve { get; }
// Item Pools
GarbageItemDefinition[] GarbageItems { get; }
// Spawn Weights
float NormalCardWeight { get; }
float RareCardWeight { get; }
float LegendCardWeight { get; }
float GarbageWeight { get; }
// Scoring
int CorrectSortPoints { get; }
int IncorrectSortPenalty { get; }
int MissedItemPenalty { get; }
// Rewards
int BoosterPacksPerCorrectItem { get; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 646b90dfa92640e59430f871037affea
timeCreated: 1763461774

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1f5c5963e2ff4b33a086f5b97e648914
timeCreated: 1763461758

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d35b6d1850b94234bbb54e77b202861b
timeCreated: 1763469999

View File

@@ -0,0 +1,266 @@
using AppleHills.Data.CardSystem;
using Core.Settings;
using Data.CardSystem;
using Minigames.CardSorting.Core;
using Minigames.CardSorting.Data;
using System.Collections.Generic;
using UnityEngine;
namespace Minigames.CardSorting.Controllers
{
/// <summary>
/// Non-MonoBehaviour controller for conveyor belt logic.
/// Handles spawning, speed, item lifecycle.
/// Parallel to CornerCardManager in card system.
/// </summary>
public class ConveyorBeltController
{
private readonly Transform spawnPoint;
private readonly Transform endPoint;
private readonly GameObject cardPrefab;
private readonly GameObject garbagePrefab;
private readonly ICardSortingSettings settings;
private List<SortableItem> activeItems = new List<SortableItem>();
private float currentSpeed;
private float nextSpawnTime;
public float CurrentSpeed => currentSpeed;
public int ActiveItemCount => activeItems.Count;
public ConveyorBeltController(
Transform spawnPoint,
Transform endPoint,
GameObject cardPrefab,
GameObject garbagePrefab,
ICardSortingSettings settings)
{
this.spawnPoint = spawnPoint;
this.endPoint = endPoint;
this.cardPrefab = cardPrefab;
this.garbagePrefab = garbagePrefab;
this.settings = settings;
this.currentSpeed = settings.InitialBeltSpeed;
this.nextSpawnTime = 0f;
}
/// <summary>
/// Update belt speed and check for items falling off.
/// </summary>
public void Update(float deltaTime, float gameProgress)
{
UpdateBeltSpeed(gameProgress);
CheckItemsOffBelt();
}
/// <summary>
/// Try to spawn an item if enough time has passed.
/// </summary>
public SortableItem TrySpawnItem(float currentTime, float gameProgress)
{
if (currentTime < nextSpawnTime) return null;
// Weighted random: card or garbage?
float totalWeight = settings.NormalCardWeight + settings.RareCardWeight +
settings.LegendCardWeight + settings.GarbageWeight;
if (totalWeight <= 0f)
{
Debug.LogWarning("[ConveyorBeltController] Total spawn weight is 0, cannot spawn items!");
return null;
}
float roll = Random.Range(0f, totalWeight);
SortableItem item;
if (roll < settings.GarbageWeight)
{
// Spawn garbage
item = SpawnGarbageItem();
}
else
{
// Spawn card - determine rarity, get random card from CardSystemManager
CardRarity rarity = DetermineRarity(roll);
item = SpawnCardItem(rarity);
}
if (item != null)
{
// Set conveyor speed on item
item.Context.ConveyorSpeed = currentSpeed;
activeItems.Add(item);
ScheduleNextSpawn(gameProgress);
}
return item;
}
/// <summary>
/// Remove item from tracking (when sorted or missed).
/// </summary>
public void RemoveItem(SortableItem item)
{
activeItems.Remove(item);
}
private SortableItem SpawnGarbageItem()
{
if (settings.GarbageItems == null || settings.GarbageItems.Length == 0)
{
Debug.LogWarning("[ConveyorBeltController] No garbage items configured!");
return null;
}
GarbageItemDefinition garbage = SelectRandomGarbage();
GameObject obj = Object.Instantiate(garbagePrefab, spawnPoint.position, Quaternion.identity);
SortableItem item = obj.GetComponent<SortableItem>();
if (item != null)
{
item.SetupAsGarbage(garbage);
}
else
{
Debug.LogError("[ConveyorBeltController] Garbage prefab missing SortableItem component!");
Object.Destroy(obj);
return null;
}
return item;
}
private SortableItem SpawnCardItem(CardRarity rarity)
{
// Get a random card of the specified rarity
CardData cardData = GetRandomCardDataByRarity(rarity);
if (cardData == null)
{
Debug.LogWarning($"[ConveyorBeltController] No card data found for rarity {rarity}");
return null;
}
GameObject obj = Object.Instantiate(cardPrefab, spawnPoint.position, Quaternion.identity);
SortableItem item = obj.GetComponent<SortableItem>();
if (item != null)
{
item.SetupAsCard(cardData);
}
else
{
Debug.LogError("[ConveyorBeltController] Card prefab missing SortableItem component!");
Object.Destroy(obj);
return null;
}
return item;
}
/// <summary>
/// Helper method to get a random card of a specific rarity.
/// Uses CardSystemManager's internal DrawRandomCards logic.
/// </summary>
private CardData GetRandomCardDataByRarity(CardRarity targetRarity)
{
// Use reflection or create cards manually
// For now, open a temporary booster and filter
// This is not ideal but works until we add a proper method to CardSystemManager
// Better approach: Draw cards until we get one of the right rarity
// Simulate drawing process
int maxAttempts = 20;
for (int i = 0; i < maxAttempts; i++)
{
var drawnCards = CardSystemManager.Instance.OpenBoosterPack();
CardSystemManager.Instance.AddBoosterPack(); // Restore the booster we used
var matchingCard = drawnCards.Find(c => c.Rarity == targetRarity);
if (matchingCard != null)
{
return matchingCard;
}
}
Debug.LogWarning($"[ConveyorBeltController] Failed to draw card of rarity {targetRarity} after {maxAttempts} attempts");
return null;
}
private void UpdateBeltSpeed(float gameProgress)
{
// Evaluate speed curve
float speedMultiplier = settings.SpeedCurve.Evaluate(gameProgress);
currentSpeed = Mathf.Lerp(
settings.InitialBeltSpeed,
settings.MaxBeltSpeed,
speedMultiplier
);
// Update all active items
foreach (var item in activeItems)
{
if (item != null && item.Context.IsOnConveyor)
{
item.Context.ConveyorSpeed = currentSpeed;
}
}
}
private void CheckItemsOffBelt()
{
for (int i = activeItems.Count - 1; i >= 0; i--)
{
var item = activeItems[i];
if (item == null)
{
activeItems.RemoveAt(i);
continue;
}
// Check if past end point
if (item.transform.position.x > endPoint.position.x)
{
item.OnFellOffBelt();
activeItems.RemoveAt(i);
}
}
}
private void ScheduleNextSpawn(float gameProgress)
{
// Calculate next spawn time based on difficulty progression
float interval = Mathf.Lerp(
settings.InitialSpawnInterval,
settings.MinimumSpawnInterval,
gameProgress
);
nextSpawnTime = Time.time + interval;
}
private CardRarity DetermineRarity(float roll)
{
// Adjust roll to be relative to card weights only (subtract garbage weight)
float adjusted = roll - settings.GarbageWeight;
if (adjusted < settings.NormalCardWeight)
return CardRarity.Normal;
if (adjusted < settings.NormalCardWeight + settings.RareCardWeight)
return CardRarity.Rare;
return CardRarity.Legendary;
}
private GarbageItemDefinition SelectRandomGarbage()
{
return settings.GarbageItems[Random.Range(0, settings.GarbageItems.Length)];
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 681596c1ece04da18b2c3394441ebd4b
timeCreated: 1763469999

View File

@@ -0,0 +1,88 @@
using Core.Settings;
using System;
namespace Minigames.CardSorting.Controllers
{
/// <summary>
/// Non-MonoBehaviour controller for score tracking.
/// Handles scoring, accuracy calculation, and reward calculation.
/// </summary>
public class SortingScoreController
{
private readonly ICardSortingSettings settings;
private int totalScore;
private int correctSorts;
private int incorrectSorts;
private int missedItems;
public int TotalScore => totalScore;
public int CorrectSorts => correctSorts;
public int IncorrectSorts => incorrectSorts;
public int MissedItems => missedItems;
public int TotalAttempts => correctSorts + incorrectSorts;
public float Accuracy => TotalAttempts > 0 ? (float)correctSorts / TotalAttempts : 0f;
public event Action<int> OnScoreChanged;
public event Action<int> OnCorrectSort;
public event Action<int> OnIncorrectSort;
public SortingScoreController(ICardSortingSettings settings)
{
this.settings = settings;
}
/// <summary>
/// Record a correct sort.
/// </summary>
public void RecordCorrectSort()
{
correctSorts++;
totalScore += settings.CorrectSortPoints;
OnScoreChanged?.Invoke(totalScore);
OnCorrectSort?.Invoke(correctSorts);
}
/// <summary>
/// Record an incorrect sort.
/// </summary>
public void RecordIncorrectSort()
{
incorrectSorts++;
totalScore += settings.IncorrectSortPenalty; // This is a negative value
OnScoreChanged?.Invoke(totalScore);
OnIncorrectSort?.Invoke(incorrectSorts);
}
/// <summary>
/// Record a missed item (fell off belt).
/// </summary>
public void RecordMissedItem()
{
missedItems++;
totalScore += settings.MissedItemPenalty; // This is a negative value
OnScoreChanged?.Invoke(totalScore);
}
/// <summary>
/// Calculate booster pack reward based on performance.
/// </summary>
public int CalculateBoosterReward()
{
// Simple: 1 booster per correct sort (or use settings multiplier)
return correctSorts * settings.BoosterPacksPerCorrectItem;
}
/// <summary>
/// Reset all scores (for restarting game).
/// </summary>
public void Reset()
{
totalScore = 0;
correctSorts = 0;
incorrectSorts = 0;
missedItems = 0;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 280957c6e5b24e91b9fdc49ec685ed2f
timeCreated: 1763470011

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa35cf17256e403ab8bed2555352eaf5
timeCreated: 1763461824

View File

@@ -0,0 +1,36 @@
using UnityEngine;
using UnityEngine.UI;
namespace Minigames.CardSorting.Core
{
/// <summary>
/// Simple sprite renderer for garbage items.
/// Parallel to CardDisplay for cards.
/// </summary>
public class GarbageVisual : MonoBehaviour
{
[Header("Visual Components")]
[SerializeField] private Image spriteRenderer;
/// <summary>
/// Update the displayed sprite.
/// </summary>
public void UpdateDisplay(Sprite sprite)
{
if (spriteRenderer != null)
{
spriteRenderer.sprite = sprite;
}
}
private void Awake()
{
// Auto-find Image component if not assigned
if (spriteRenderer == null)
{
spriteRenderer = GetComponent<Image>();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b707770fc3a6448ea0dcd1b2fbf41e00
timeCreated: 1763461824

View File

@@ -0,0 +1,179 @@
using AppleHills.Data.CardSystem;
using Core;
using Core.SaveLoad;
using Minigames.CardSorting.Data;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace Minigames.CardSorting.Core
{
/// <summary>
/// Draggable sortable item on conveyor belt.
/// Uses state machine for behavior (OnConveyor → BeingDragged → Sorted).
/// Inherits from DraggableObject to reuse drag/drop system.
/// </summary>
public class SortableItem : DraggableObject
{
[Header("Components")]
[SerializeField] private SortableItemContext context;
[SerializeField] private AppleMachine stateMachine;
[Header("Configuration")]
[SerializeField] private string initialState = "OnConveyorState";
// Data tracking
private bool isGarbage;
private CardData cardData;
private GarbageItemDefinition garbageItem;
// Public accessors
public SortableItemContext Context => context;
public AppleMachine StateMachine => stateMachine;
public bool IsGarbage => isGarbage;
public CardData CardData => cardData;
public GarbageItemDefinition GarbageItem => garbageItem;
/// <summary>
/// Get the correct box type for this item.
/// </summary>
public BoxType CorrectBox
{
get
{
if (isGarbage)
return BoxType.Trash;
return cardData.Rarity switch
{
CardRarity.Normal => BoxType.Normal,
CardRarity.Rare => BoxType.Rare,
CardRarity.Legendary => BoxType.Legend,
_ => BoxType.Trash
};
}
}
protected override void Initialize()
{
base.Initialize();
// Auto-find components if not assigned
if (context == null)
context = GetComponent<SortableItemContext>();
if (stateMachine == null)
stateMachine = GetComponentInChildren<AppleMachine>();
}
/// <summary>
/// Setup item as a card.
/// </summary>
public void SetupAsCard(CardData data)
{
isGarbage = false;
cardData = data;
garbageItem = null;
if (context != null)
{
context.SetupAsCard(data);
}
if (stateMachine != null && !string.IsNullOrEmpty(initialState))
{
stateMachine.ChangeState(initialState);
}
}
/// <summary>
/// Setup item as garbage.
/// </summary>
public void SetupAsGarbage(GarbageItemDefinition garbage)
{
isGarbage = true;
cardData = default;
garbageItem = garbage;
if (context != null)
{
context.SetupAsGarbage(garbage.Sprite);
}
if (stateMachine != null && !string.IsNullOrEmpty(initialState))
{
stateMachine.ChangeState(initialState);
}
}
protected override void OnDragStartedHook()
{
base.OnDragStartedHook();
// Check if current state wants to handle drag behavior
if (stateMachine?.currentState != null)
{
var dragHandler = stateMachine.currentState.GetComponent<ISortableItemDragHandler>();
if (dragHandler != null && dragHandler.OnDragStarted(context))
{
return; // State handled it
}
}
// Default behavior if state doesn't handle
Logging.Debug($"[SortableItem] Drag started on {(isGarbage ? garbageItem.DisplayName : cardData.Name)}");
}
protected override void OnDragEndedHook()
{
base.OnDragEndedHook();
// Validate drop on sorting box
if (CurrentSlot is SortingBox box)
{
bool correctSort = box.ValidateItem(this);
// Notify game manager
SortingGameManager.Instance?.OnItemSorted(this, box, correctSort);
// Transition to sorted state
ChangeState("SortedState");
}
else
{
// Dropped outside valid box - return to conveyor
Logging.Debug("[SortableItem] Dropped outside box, returning to conveyor");
ChangeState("OnConveyorState");
}
}
/// <summary>
/// Change to a specific state.
/// </summary>
public void ChangeState(string stateName)
{
if (stateMachine != null)
{
stateMachine.ChangeState(stateName);
}
}
/// <summary>
/// Called when item falls off conveyor belt.
/// </summary>
public void OnFellOffBelt()
{
// Notify game manager
SortingGameManager.Instance?.OnItemMissed(this);
Destroy(gameObject);
}
}
/// <summary>
/// Interface for states that handle drag behavior.
/// </summary>
public interface ISortableItemDragHandler
{
bool OnDragStarted(SortableItemContext context);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9b3db9ce867c4df884411fb3da8fd80a
timeCreated: 1763461867

View File

@@ -0,0 +1,99 @@
using AppleHills.Data.CardSystem;
using Core.SaveLoad;
using UI.CardSystem;
using UI.CardSystem.StateMachine;
using UnityEngine;
namespace Minigames.CardSorting.Core
{
/// <summary>
/// Shared context for sortable item states.
/// Provides access to common components and data that states need.
/// Routes data to appropriate visual component (CardDisplay for cards, GarbageVisual for garbage).
/// </summary>
public class SortableItemContext : MonoBehaviour
{
[Header("Visual Components (one or the other)")]
[SerializeField] private CardDisplay cardDisplay; // For cards
[SerializeField] private GarbageVisual garbageVisual; // For garbage
[Header("Shared Components")]
[SerializeField] private CardAnimator animator;
[SerializeField] private Transform visualTransform; // "Visual" GameObject
private AppleMachine stateMachine;
// Public accessors
public CardDisplay CardDisplay => cardDisplay;
public GarbageVisual GarbageVisual => garbageVisual;
public CardAnimator Animator => animator;
public Transform VisualTransform => visualTransform;
public AppleMachine StateMachine => stateMachine;
public Transform RootTransform => transform;
// Conveyor state
public bool IsOnConveyor { get; set; } = true;
public float ConveyorSpeed { get; set; } = 1f;
private void Awake()
{
// Auto-find components if not assigned
if (visualTransform == null)
{
visualTransform = transform.Find("Visual");
if (visualTransform == null)
{
Debug.LogWarning($"[SortableItemContext] 'Visual' child GameObject not found on {name}");
}
}
if (animator == null && visualTransform != null)
{
animator = visualTransform.GetComponent<CardAnimator>();
}
if (cardDisplay == null && visualTransform != null)
{
cardDisplay = visualTransform.GetComponentInChildren<CardDisplay>();
}
if (garbageVisual == null && visualTransform != null)
{
garbageVisual = visualTransform.GetComponentInChildren<GarbageVisual>();
}
stateMachine = GetComponentInChildren<AppleMachine>();
}
/// <summary>
/// Setup as card item - CardDisplay handles all rendering.
/// </summary>
public void SetupAsCard(CardData cardData)
{
if (cardDisplay != null)
{
cardDisplay.SetupCard(cardData);
}
else
{
Debug.LogError($"[SortableItemContext] CardDisplay not found on {name}");
}
}
/// <summary>
/// Setup as garbage item - simple sprite display.
/// </summary>
public void SetupAsGarbage(Sprite sprite)
{
if (garbageVisual != null)
{
garbageVisual.UpdateDisplay(sprite);
}
else
{
Debug.LogError($"[SortableItemContext] GarbageVisual not found on {name}");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9a9c60767eef4a3090d8bf70ee87340f
timeCreated: 1763461839

View File

@@ -0,0 +1,41 @@
using Minigames.CardSorting.Data;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace Minigames.CardSorting.Core
{
/// <summary>
/// Drop target for sortable items.
/// Validates if item belongs in this box.
/// </summary>
public class SortingBox : DraggableSlot
{
[Header("Box Configuration")]
[SerializeField] private BoxType boxType;
[SerializeField] private Sprite boxSprite;
public BoxType BoxType => boxType;
/// <summary>
/// Check if item belongs in this box.
/// </summary>
public bool ValidateItem(SortableItem item)
{
if (item == null) return false;
BoxType correctBox = item.CorrectBox;
return correctBox == boxType;
}
/// <summary>
/// Check if this slot can accept a specific draggable type.
/// SortingBox accepts all SortableItems (validation happens on drop).
/// </summary>
public new bool CanAccept(DraggableObject draggable)
{
// Accept all sortable items (validation happens on drop)
return draggable is SortableItem;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a25b1c9a82b540c8ac0d6c016849f561
timeCreated: 1763461875

View File

@@ -0,0 +1,239 @@
using System;
using Core;
using Core.Lifecycle;
using Core.Settings;
using Data.CardSystem;
using Input;
using Minigames.CardSorting.Controllers;
using Minigames.CardSorting.Core;
using UnityEngine;
namespace Minigames.CardSorting.Core
{
/// <summary>
/// Main manager for card sorting minigame.
/// Orchestrates game loop, timer, controllers, and integration with CardSystemManager.
/// Parallel to DivingGameManager.
/// </summary>
public class SortingGameManager : ManagedBehaviour
{
[Header("Scene References")]
[SerializeField] private Transform conveyorSpawnPoint;
[SerializeField] private Transform conveyorEndPoint;
[SerializeField] private GameObject sortableCardPrefab;
[SerializeField] private GameObject sortableGarbagePrefab;
[SerializeField] private SortingBox[] sortingBoxes;
// Settings
private ICardSortingSettings _settings;
// Controllers (lazy init)
private ConveyorBeltController _conveyorController;
private ConveyorBeltController Conveyor => _conveyorController ??= new ConveyorBeltController(
conveyorSpawnPoint,
conveyorEndPoint,
sortableCardPrefab,
sortableGarbagePrefab,
_settings
);
private SortingScoreController _scoreController;
private SortingScoreController Score => _scoreController ??= new SortingScoreController(_settings);
// Game state
private float gameTimer;
private bool isGameActive;
private bool isGameOver;
// Singleton
private static SortingGameManager _instance;
public static SortingGameManager Instance => _instance;
// Events
public event Action OnGameStarted;
public event Action OnGameEnded;
public event Action<SortableItem> OnItemSpawned;
public event Action<SortableItem, SortingBox, bool> OnItemSortedEvent;
public event Action<float> OnTimerUpdated; // Remaining time
internal override void OnManagedAwake()
{
_instance = this;
// Load settings
_settings = GameManager.GetSettingsObject<ICardSortingSettings>();
if (_settings == null)
{
Debug.LogError("[SortingGameManager] Failed to load CardSortingSettings!");
return;
}
Logging.Debug("[SortingGameManager] Initialized with settings");
}
internal override void OnManagedStart()
{
// Subscribe to score events
Score.OnScoreChanged += OnScoreChanged;
Score.OnCorrectSort += OnCorrectSort;
Score.OnIncorrectSort += OnIncorrectSort;
// Start game automatically or wait for trigger
// For now, auto-start
StartGame();
}
private void OnDestroy()
{
if (_scoreController != null)
{
Score.OnScoreChanged -= OnScoreChanged;
Score.OnCorrectSort -= OnCorrectSort;
Score.OnIncorrectSort -= OnIncorrectSort;
}
}
private void Update()
{
if (!isGameActive || isGameOver) return;
gameTimer += Time.deltaTime;
float remainingTime = _settings.GameDuration - gameTimer;
float gameProgress = gameTimer / _settings.GameDuration;
// Update timer
OnTimerUpdated?.Invoke(remainingTime);
// Check game over
if (remainingTime <= 0f)
{
EndGame();
return;
}
// Update conveyor
Conveyor.Update(Time.deltaTime, gameProgress);
// Try spawn item
var item = Conveyor.TrySpawnItem(Time.time, gameProgress);
if (item != null)
{
OnItemSpawned?.Invoke(item);
}
}
public void StartGame()
{
isGameActive = true;
isGameOver = false;
gameTimer = 0f;
// Reset score
Score.Reset();
OnGameStarted?.Invoke();
// Set input mode to game
if (InputManager.Instance != null)
{
InputManager.Instance.SetInputMode(InputMode.GameAndUI);
}
Logging.Debug("[SortingGameManager] Game started!");
}
public void EndGame()
{
if (isGameOver) return;
isGameOver = true;
isGameActive = false;
// Calculate rewards
int boosterReward = Score.CalculateBoosterReward();
Logging.Debug($"[SortingGameManager] Game ended! Score: {Score.TotalScore}, Boosters: {boosterReward}");
// Grant boosters
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.AddBoosterPack(boosterReward);
}
OnGameEnded?.Invoke();
// Show results screen (handled by UI controller)
}
/// <summary>
/// Called by SortableItem when placed in box.
/// </summary>
public void OnItemSorted(SortableItem item, SortingBox box, bool correct)
{
if (correct)
{
Score.RecordCorrectSort();
Logging.Debug($"[SortingGameManager] Correct sort! {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
}
else
{
Score.RecordIncorrectSort();
Logging.Debug($"[SortingGameManager] Incorrect sort! {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
}
Conveyor.RemoveItem(item);
OnItemSortedEvent?.Invoke(item, box, correct);
// Play animation then destroy
if (item.Context?.Animator != null)
{
item.Context.Animator.PopOut(0.4f, () => {
if (item != null)
Destroy(item.gameObject);
});
}
else
{
Destroy(item.gameObject, 0.5f);
}
}
/// <summary>
/// Called when item falls off belt.
/// </summary>
public void OnItemMissed(SortableItem item)
{
Score.RecordMissedItem();
Conveyor.RemoveItem(item);
Logging.Debug($"[SortingGameManager] Item missed! {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
}
private void OnScoreChanged(int newScore)
{
// UI will subscribe to this event
Logging.Debug($"[SortingGameManager] Score changed: {newScore}");
}
private void OnCorrectSort(int totalCorrect)
{
// Could play effects, update combo, etc.
}
private void OnIncorrectSort(int totalIncorrect)
{
// Could play error effects
}
// Public accessors for UI
public int CurrentScore => Score?.TotalScore ?? 0;
public int CorrectSorts => Score?.CorrectSorts ?? 0;
public int IncorrectSorts => Score?.IncorrectSorts ?? 0;
public int MissedItems => Score?.MissedItems ?? 0;
public float Accuracy => Score?.Accuracy ?? 0f;
public float RemainingTime => Mathf.Max(0f, _settings.GameDuration - gameTimer);
public bool IsGameActive => isGameActive && !isGameOver;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 20acac8b97ca4d6397612679b3bbde50
timeCreated: 1763470372

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a450e2687ce14a66b1495e1f2db7d403
timeCreated: 1763461758

View File

@@ -0,0 +1,14 @@
namespace Minigames.CardSorting.Data
{
/// <summary>
/// Types of sorting boxes in the minigame.
/// </summary>
public enum BoxType
{
Normal, // Copper-rimmed box for Normal rarity cards
Rare, // Silver-rimmed box for Rare rarity cards
Legend, // Gold-rimmed box for Legendary rarity cards
Trash // Trash can for garbage items
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7e238b51f7164867b519bf139fe22c01
timeCreated: 1763461758

View File

@@ -0,0 +1,38 @@
using UnityEngine;
namespace Minigames.CardSorting.Data
{
/// <summary>
/// Definition for garbage items (banana peels, cans, receipts, etc.).
/// Cards use existing CardDefinition from CardSystemManager.
/// </summary>
[CreateAssetMenu(fileName = "GarbageItem", menuName = "Minigames/CardSorting/GarbageItem", order = 0)]
public class GarbageItemDefinition : ScriptableObject
{
[Tooltip("Unique identifier for this garbage item")]
[SerializeField] private string itemId;
[Tooltip("Display name for debugging")]
[SerializeField] private string displayName;
[Tooltip("Sprite to display for this garbage item")]
[SerializeField] private Sprite sprite;
// Public accessors
public string ItemId => itemId;
public string DisplayName => displayName;
public Sprite Sprite => sprite;
#if UNITY_EDITOR
private void OnValidate()
{
// Auto-generate itemId from asset name if empty
if (string.IsNullOrEmpty(itemId))
{
itemId = name;
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2e69a2167710437798b1980126d5a4f6
timeCreated: 1763461765

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0197a7c7c3174a5fbf1ddd5b1445f24c
timeCreated: 1763461845

View File

@@ -0,0 +1,21 @@
using Core.SaveLoad;
namespace Minigames.CardSorting.StateMachine
{
/// <summary>
/// State machine for sortable items that opts out of save system.
/// Sorting minigame is session-only and doesn't persist between loads.
/// Follows CardStateMachine pattern.
/// </summary>
public class SortingStateMachine : AppleMachine
{
/// <summary>
/// Opt out of save/load system - sortable items are transient minigame objects.
/// </summary>
public override bool ShouldParticipateInSave()
{
return false;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1b459f40574b45839aa32d5730627ca6
timeCreated: 1763461845

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 24973c8bb25d493885224ac6f099492d
timeCreated: 1763469762

View File

@@ -0,0 +1,51 @@
using Core;
using Core.SaveLoad;
using Minigames.CardSorting.Core;
using UnityEngine;
namespace Minigames.CardSorting.StateMachine.States
{
/// <summary>
/// Item is being dragged by the player.
/// Provides visual feedback (scale up).
/// Transitions to SortedState when dropped in box, or back to OnConveyorState if dropped elsewhere.
/// </summary>
public class BeingDraggedState : AppleState
{
private SortableItemContext _context;
private Vector3 _originalScale;
private void Awake()
{
_context = GetComponentInParent<SortableItemContext>();
}
public override void OnEnterState()
{
if (_context == null) return;
_context.IsOnConveyor = false;
// Store original scale
if (_context.VisualTransform != null)
{
_originalScale = _context.VisualTransform.localScale;
// Visual feedback: scale up 10%
_context.Animator?.AnimateScale(_originalScale * 1.1f, 0.2f);
}
Logging.Debug("[BeingDraggedState] Item being dragged, scaled up for feedback");
}
private void OnDisable()
{
// Restore original scale
if (_context != null && _context.Animator != null)
{
_context.Animator.AnimateScale(_originalScale, 0.2f);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 015c0740240748c8901c9304490cb80d
timeCreated: 1763469770

View File

@@ -0,0 +1,62 @@
using Core;
using Core.SaveLoad;
using Minigames.CardSorting.Core;
using UnityEngine;
namespace Minigames.CardSorting.StateMachine.States
{
/// <summary>
/// Item is moving along the conveyor belt.
/// Transitions to BeingDraggedState when player drags the item.
/// </summary>
public class OnConveyorState : AppleState, ISortableItemDragHandler
{
private SortableItemContext _context;
private SortableItem _item;
private void Awake()
{
_context = GetComponentInParent<SortableItemContext>();
_item = GetComponentInParent<SortableItem>();
}
public override void OnEnterState()
{
if (_context == null) return;
_context.IsOnConveyor = true;
Logging.Debug($"[OnConveyorState] Item entered conveyor state");
}
private void OnDisable()
{
if (_context == null) return;
_context.IsOnConveyor = false;
}
private void Update()
{
if (_context == null || !_context.IsOnConveyor) return;
// Move item along conveyor (right direction)
Vector3 movement = Vector3.right * _context.ConveyorSpeed * Time.deltaTime;
_context.RootTransform.position += movement;
}
/// <summary>
/// Handle drag start - transition to BeingDraggedState.
/// </summary>
public bool OnDragStarted(SortableItemContext context)
{
Logging.Debug("[OnConveyorState] Drag started, transitioning to BeingDraggedState");
// Transition to dragging state
_item?.ChangeState("BeingDraggedState");
return true; // We handled it
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 17d2ba6f5aec4b698247b082734cad8f
timeCreated: 1763469762

View File

@@ -0,0 +1,34 @@
using Core;
using Core.SaveLoad;
using Minigames.CardSorting.Core;
namespace Minigames.CardSorting.StateMachine.States
{
/// <summary>
/// Item has been successfully sorted into a box.
/// Plays animation then marks for destruction.
/// Manager handles the actual PopOut animation and destruction.
/// </summary>
public class SortedState : AppleState
{
private SortableItemContext _context;
private void Awake()
{
_context = GetComponentInParent<SortableItemContext>();
}
public override void OnEnterState()
{
if (_context == null) return;
_context.IsOnConveyor = false;
Logging.Debug("[SortedState] Item sorted, ready for destruction animation");
// Manager will handle PopOut animation and destruction
// State just marks item as no longer on conveyor
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0717f922952c4f228930ef0a5f6617b0
timeCreated: 1763469776

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2194dbe8c2c4479f89f7307ce56cac5d
timeCreated: 1763470403

View File

@@ -0,0 +1,116 @@
using Core.Settings;
using Minigames.CardSorting.Core;
using TMPro;
using UnityEngine;
namespace Minigames.CardSorting.UI
{
/// <summary>
/// HUD display for card sorting minigame.
/// Shows timer and score during gameplay.
/// </summary>
public class SortingGameHUD : MonoBehaviour
{
[Header("UI Elements")]
[SerializeField] private TextMeshProUGUI timerText;
[SerializeField] private TextMeshProUGUI scoreText;
[SerializeField] private TextMeshProUGUI accuracyText;
private SortingGameManager gameManager;
private void Start()
{
gameManager = SortingGameManager.Instance;
if (gameManager == null)
{
Debug.LogError("[SortingGameHUD] SortingGameManager not found!");
return;
}
// Subscribe to events
gameManager.OnTimerUpdated += UpdateTimer;
if (gameManager != null)
{
var scoreController = typeof(SortingGameManager)
.GetProperty("Score", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?.GetValue(gameManager);
if (scoreController != null)
{
var scoreChangedEvent = scoreController.GetType().GetEvent("OnScoreChanged");
if (scoreChangedEvent != null)
{
scoreChangedEvent.AddEventHandler(scoreController,
new System.Action<int>(UpdateScore));
}
}
}
// Initial display
UpdateScore(0);
UpdateTimer(120f); // Default timer display
}
private void OnDestroy()
{
if (gameManager != null)
{
gameManager.OnTimerUpdated -= UpdateTimer;
}
}
private void Update()
{
// Update accuracy every frame (could optimize to only update on score change)
if (gameManager != null && accuracyText != null)
{
float accuracy = gameManager.Accuracy;
accuracyText.text = $"Accuracy: {accuracy:P0}";
}
}
public void UpdateTimer(float remainingTime)
{
if (timerText == null) return;
int minutes = Mathf.FloorToInt(remainingTime / 60f);
int seconds = Mathf.FloorToInt(remainingTime % 60f);
timerText.text = $"{minutes:00}:{seconds:00}";
// Change color if time running out
if (remainingTime <= 10f)
{
timerText.color = Color.red;
}
else if (remainingTime <= 30f)
{
timerText.color = Color.yellow;
}
else
{
timerText.color = Color.white;
}
}
public void UpdateScore(int newScore)
{
if (scoreText == null) return;
scoreText.text = $"Score: {newScore}";
// Color based on positive/negative
if (newScore >= 0)
{
scoreText.color = Color.white;
}
else
{
scoreText.color = new Color(1f, 0.5f, 0.5f); // Light red
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aa656e03d5384a9eae31fab73b6fe5e2
timeCreated: 1763470403

View File

@@ -0,0 +1,126 @@
using Minigames.CardSorting.Core;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Minigames.CardSorting.UI
{
/// <summary>
/// Results screen shown at end of card sorting minigame.
/// Displays final score, accuracy, and boosters earned.
/// </summary>
public class SortingResultsScreen : MonoBehaviour
{
[Header("UI Elements")]
[SerializeField] private TextMeshProUGUI finalScoreText;
[SerializeField] private TextMeshProUGUI correctSortsText;
[SerializeField] private TextMeshProUGUI incorrectSortsText;
[SerializeField] private TextMeshProUGUI missedItemsText;
[SerializeField] private TextMeshProUGUI accuracyText;
[SerializeField] private TextMeshProUGUI boostersEarnedText;
[SerializeField] private Button closeButton;
[Header("Screen")]
[SerializeField] private CanvasGroup canvasGroup;
private void Awake()
{
// Hide initially
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
else
{
gameObject.SetActive(false);
}
// Setup close button
if (closeButton != null)
{
closeButton.onClick.AddListener(OnCloseClicked);
}
}
private void Start()
{
var gameManager = SortingGameManager.Instance;
if (gameManager != null)
{
gameManager.OnGameEnded += ShowResults;
}
}
private void OnDestroy()
{
var gameManager = SortingGameManager.Instance;
if (gameManager != null)
{
gameManager.OnGameEnded -= ShowResults;
}
}
private void ShowResults()
{
var gameManager = SortingGameManager.Instance;
if (gameManager == null) return;
// Populate data
if (finalScoreText != null)
finalScoreText.text = $"Final Score: {gameManager.CurrentScore}";
if (correctSortsText != null)
correctSortsText.text = $"Correct: {gameManager.CorrectSorts}";
if (incorrectSortsText != null)
incorrectSortsText.text = $"Incorrect: {gameManager.IncorrectSorts}";
if (missedItemsText != null)
missedItemsText.text = $"Missed: {gameManager.MissedItems}";
if (accuracyText != null)
accuracyText.text = $"Accuracy: {gameManager.Accuracy:P0}";
// Calculate boosters (already granted by manager)
int boosters = gameManager.CorrectSorts; // Simple 1:1 ratio
if (boostersEarnedText != null)
boostersEarnedText.text = $"Boosters Earned: {boosters}";
// Show screen
if (canvasGroup != null)
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
else
{
gameObject.SetActive(true);
}
}
private void OnCloseClicked()
{
// Hide screen
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
else
{
gameObject.SetActive(false);
}
// Could also trigger scene transition, return to menu, etc.
Debug.Log("[SortingResultsScreen] Closed results screen");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 03823b5ad80b482086569050fbb8bb40
timeCreated: 1763470418

File diff suppressed because it is too large Load Diff