From 359e0e35bd433c318d7366b8e15d99f6ce76260f Mon Sep 17 00:00:00 2001 From: Michal Pikulski Date: Wed, 19 Nov 2025 08:30:47 +0100 Subject: [PATCH] Stash work on mingame --- .../Editor/Settings/SettingsEditorWindow.cs | 7 +- .../Core/Settings/CardSortingSettings.cs | 97 ++ .../Core/Settings/CardSortingSettings.cs.meta | 3 + .../Core/Settings/ICardSortingSettings.cs | 40 + .../Settings/ICardSortingSettings.cs.meta | 3 + Assets/Scripts/Minigames/CardSorting.meta | 3 + .../Minigames/CardSorting/Controllers.meta | 3 + .../Controllers/ConveyorBeltController.cs | 266 ++++ .../ConveyorBeltController.cs.meta | 3 + .../Controllers/SortingScoreController.cs | 88 ++ .../SortingScoreController.cs.meta | 3 + .../Scripts/Minigames/CardSorting/Core.meta | 3 + .../CardSorting/Core/GarbageVisual.cs | 36 + .../CardSorting/Core/GarbageVisual.cs.meta | 3 + .../CardSorting/Core/SortableItem.cs | 179 +++ .../CardSorting/Core/SortableItem.cs.meta | 3 + .../CardSorting/Core/SortableItemContext.cs | 99 ++ .../Core/SortableItemContext.cs.meta | 3 + .../Minigames/CardSorting/Core/SortingBox.cs | 41 + .../CardSorting/Core/SortingBox.cs.meta | 3 + .../CardSorting/Core/SortingGameManager.cs | 239 ++++ .../Core/SortingGameManager.cs.meta | 3 + .../Scripts/Minigames/CardSorting/Data.meta | 3 + .../Minigames/CardSorting/Data/Enums.cs | 14 + .../Minigames/CardSorting/Data/Enums.cs.meta | 3 + .../CardSorting/Data/GarbageItemDefinition.cs | 38 + .../Data/GarbageItemDefinition.cs.meta | 3 + .../Minigames/CardSorting/StateMachine.meta | 3 + .../StateMachine/SortingStateMachine.cs | 21 + .../StateMachine/SortingStateMachine.cs.meta | 3 + .../CardSorting/StateMachine/States.meta | 3 + .../StateMachine/States/BeingDraggedState.cs | 51 + .../States/BeingDraggedState.cs.meta | 3 + .../StateMachine/States/OnConveyorState.cs | 62 + .../States/OnConveyorState.cs.meta | 3 + .../StateMachine/States/SortedState.cs | 34 + .../StateMachine/States/SortedState.cs.meta | 3 + Assets/Scripts/Minigames/CardSorting/UI.meta | 3 + .../CardSorting/UI/SortingGameHUD.cs | 116 ++ .../CardSorting/UI/SortingGameHUD.cs.meta | 3 + .../CardSorting/UI/SortingResultsScreen.cs | 126 ++ .../UI/SortingResultsScreen.cs.meta | 3 + docs/card_sorting_minigame_plan.md | 1114 +++++++++++++++++ 43 files changed, 2739 insertions(+), 1 deletion(-) create mode 100644 Assets/Scripts/Core/Settings/CardSortingSettings.cs create mode 100644 Assets/Scripts/Core/Settings/CardSortingSettings.cs.meta create mode 100644 Assets/Scripts/Core/Settings/ICardSortingSettings.cs create mode 100644 Assets/Scripts/Core/Settings/ICardSortingSettings.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Controllers.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Controllers/ConveyorBeltController.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/Controllers/ConveyorBeltController.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Controllers/SortingScoreController.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/Controllers/SortingScoreController.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Core.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/GarbageVisual.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/GarbageVisual.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/SortableItem.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/SortableItem.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/SortableItemContext.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/SortableItemContext.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/SortingBox.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/SortingBox.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/SortingGameManager.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/Core/SortingGameManager.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Data.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Data/Enums.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/Data/Enums.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/Data/GarbageItemDefinition.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/Data/GarbageItemDefinition.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine/SortingStateMachine.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine/SortingStateMachine.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine/States.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine/States/BeingDraggedState.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine/States/BeingDraggedState.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine/States/OnConveyorState.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine/States/OnConveyorState.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine/States/SortedState.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/StateMachine/States/SortedState.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/UI.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/UI/SortingGameHUD.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/UI/SortingGameHUD.cs.meta create mode 100644 Assets/Scripts/Minigames/CardSorting/UI/SortingResultsScreen.cs create mode 100644 Assets/Scripts/Minigames/CardSorting/UI/SortingResultsScreen.cs.meta create mode 100644 docs/card_sorting_minigame_plan.md diff --git a/Assets/Editor/Settings/SettingsEditorWindow.cs b/Assets/Editor/Settings/SettingsEditorWindow.cs index 039e5963..53eb23ec 100644 --- a/Assets/Editor/Settings/SettingsEditorWindow.cs +++ b/Assets/Editor/Settings/SettingsEditorWindow.cs @@ -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 allSettings = new List(); - 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 serializedSettingsObjects = new Dictionary(); private GUIStyle headerStyle; @@ -49,6 +50,7 @@ namespace AppleHills.Core.Settings.Editor CreateSettingsIfMissing("InteractionSettings"); CreateSettingsIfMissing("DivingMinigameSettings"); CreateSettingsIfMissing("CardSystemSettings"); + CreateSettingsIfMissing("CardSortingSettings"); } private void CreateSettingsIfMissing(string fileName) where T : BaseSettings @@ -118,6 +120,9 @@ namespace AppleHills.Core.Settings.Editor case 3: // Card System DrawSettingsEditor(); break; + case 4: // Card Sorting + DrawSettingsEditor(); + break; } EditorGUILayout.EndScrollView(); diff --git a/Assets/Scripts/Core/Settings/CardSortingSettings.cs b/Assets/Scripts/Core/Settings/CardSortingSettings.cs new file mode 100644 index 00000000..5ecc8a5e --- /dev/null +++ b/Assets/Scripts/Core/Settings/CardSortingSettings.cs @@ -0,0 +1,97 @@ +using AppleHills.Core.Settings; +using Minigames.CardSorting.Data; +using UnityEngine; + +namespace Core.Settings +{ + /// + /// Settings for Card Sorting minigame. + /// Follows DivingMinigameSettings pattern. + /// + [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); + } + } +} + diff --git a/Assets/Scripts/Core/Settings/CardSortingSettings.cs.meta b/Assets/Scripts/Core/Settings/CardSortingSettings.cs.meta new file mode 100644 index 00000000..c9c99aa5 --- /dev/null +++ b/Assets/Scripts/Core/Settings/CardSortingSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a0fd5f8ab5d74b12968501dd4e3cc416 +timeCreated: 1763461789 \ No newline at end of file diff --git a/Assets/Scripts/Core/Settings/ICardSortingSettings.cs b/Assets/Scripts/Core/Settings/ICardSortingSettings.cs new file mode 100644 index 00000000..89ab9d05 --- /dev/null +++ b/Assets/Scripts/Core/Settings/ICardSortingSettings.cs @@ -0,0 +1,40 @@ +using Minigames.CardSorting.Data; +using UnityEngine; + +namespace Core.Settings +{ + /// + /// Settings interface for Card Sorting minigame. + /// Accessed via GameManager.GetSettingsObject<ICardSortingSettings>() + /// + 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; } + } +} + diff --git a/Assets/Scripts/Core/Settings/ICardSortingSettings.cs.meta b/Assets/Scripts/Core/Settings/ICardSortingSettings.cs.meta new file mode 100644 index 00000000..fa644e0e --- /dev/null +++ b/Assets/Scripts/Core/Settings/ICardSortingSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 646b90dfa92640e59430f871037affea +timeCreated: 1763461774 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting.meta b/Assets/Scripts/Minigames/CardSorting.meta new file mode 100644 index 00000000..3285e18d --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1f5c5963e2ff4b33a086f5b97e648914 +timeCreated: 1763461758 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Controllers.meta b/Assets/Scripts/Minigames/CardSorting/Controllers.meta new file mode 100644 index 00000000..dbf2ca7d --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Controllers.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d35b6d1850b94234bbb54e77b202861b +timeCreated: 1763469999 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Controllers/ConveyorBeltController.cs b/Assets/Scripts/Minigames/CardSorting/Controllers/ConveyorBeltController.cs new file mode 100644 index 00000000..587a3f34 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Controllers/ConveyorBeltController.cs @@ -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 +{ + /// + /// Non-MonoBehaviour controller for conveyor belt logic. + /// Handles spawning, speed, item lifecycle. + /// Parallel to CornerCardManager in card system. + /// + 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 activeItems = new List(); + 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; + } + + /// + /// Update belt speed and check for items falling off. + /// + public void Update(float deltaTime, float gameProgress) + { + UpdateBeltSpeed(gameProgress); + CheckItemsOffBelt(); + } + + /// + /// Try to spawn an item if enough time has passed. + /// + 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; + } + + /// + /// Remove item from tracking (when sorted or missed). + /// + 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(); + + 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(); + + if (item != null) + { + item.SetupAsCard(cardData); + } + else + { + Debug.LogError("[ConveyorBeltController] Card prefab missing SortableItem component!"); + Object.Destroy(obj); + return null; + } + + return item; + } + + /// + /// Helper method to get a random card of a specific rarity. + /// Uses CardSystemManager's internal DrawRandomCards logic. + /// + 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)]; + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/Controllers/ConveyorBeltController.cs.meta b/Assets/Scripts/Minigames/CardSorting/Controllers/ConveyorBeltController.cs.meta new file mode 100644 index 00000000..9d440ea8 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Controllers/ConveyorBeltController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 681596c1ece04da18b2c3394441ebd4b +timeCreated: 1763469999 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Controllers/SortingScoreController.cs b/Assets/Scripts/Minigames/CardSorting/Controllers/SortingScoreController.cs new file mode 100644 index 00000000..493c9da0 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Controllers/SortingScoreController.cs @@ -0,0 +1,88 @@ +using Core.Settings; +using System; + +namespace Minigames.CardSorting.Controllers +{ + /// + /// Non-MonoBehaviour controller for score tracking. + /// Handles scoring, accuracy calculation, and reward calculation. + /// + 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 OnScoreChanged; + public event Action OnCorrectSort; + public event Action OnIncorrectSort; + + public SortingScoreController(ICardSortingSettings settings) + { + this.settings = settings; + } + + /// + /// Record a correct sort. + /// + public void RecordCorrectSort() + { + correctSorts++; + totalScore += settings.CorrectSortPoints; + OnScoreChanged?.Invoke(totalScore); + OnCorrectSort?.Invoke(correctSorts); + } + + /// + /// Record an incorrect sort. + /// + public void RecordIncorrectSort() + { + incorrectSorts++; + totalScore += settings.IncorrectSortPenalty; // This is a negative value + OnScoreChanged?.Invoke(totalScore); + OnIncorrectSort?.Invoke(incorrectSorts); + } + + /// + /// Record a missed item (fell off belt). + /// + public void RecordMissedItem() + { + missedItems++; + totalScore += settings.MissedItemPenalty; // This is a negative value + OnScoreChanged?.Invoke(totalScore); + } + + /// + /// Calculate booster pack reward based on performance. + /// + public int CalculateBoosterReward() + { + // Simple: 1 booster per correct sort (or use settings multiplier) + return correctSorts * settings.BoosterPacksPerCorrectItem; + } + + /// + /// Reset all scores (for restarting game). + /// + public void Reset() + { + totalScore = 0; + correctSorts = 0; + incorrectSorts = 0; + missedItems = 0; + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/Controllers/SortingScoreController.cs.meta b/Assets/Scripts/Minigames/CardSorting/Controllers/SortingScoreController.cs.meta new file mode 100644 index 00000000..b66e1154 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Controllers/SortingScoreController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 280957c6e5b24e91b9fdc49ec685ed2f +timeCreated: 1763470011 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Core.meta b/Assets/Scripts/Minigames/CardSorting/Core.meta new file mode 100644 index 00000000..e332cfa6 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fa35cf17256e403ab8bed2555352eaf5 +timeCreated: 1763461824 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Core/GarbageVisual.cs b/Assets/Scripts/Minigames/CardSorting/Core/GarbageVisual.cs new file mode 100644 index 00000000..99de3579 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/GarbageVisual.cs @@ -0,0 +1,36 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace Minigames.CardSorting.Core +{ + /// + /// Simple sprite renderer for garbage items. + /// Parallel to CardDisplay for cards. + /// + public class GarbageVisual : MonoBehaviour + { + [Header("Visual Components")] + [SerializeField] private Image spriteRenderer; + + /// + /// Update the displayed sprite. + /// + 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(); + } + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/Core/GarbageVisual.cs.meta b/Assets/Scripts/Minigames/CardSorting/Core/GarbageVisual.cs.meta new file mode 100644 index 00000000..96c07f2a --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/GarbageVisual.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b707770fc3a6448ea0dcd1b2fbf41e00 +timeCreated: 1763461824 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Core/SortableItem.cs b/Assets/Scripts/Minigames/CardSorting/Core/SortableItem.cs new file mode 100644 index 00000000..ddb1340e --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/SortableItem.cs @@ -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 +{ + /// + /// Draggable sortable item on conveyor belt. + /// Uses state machine for behavior (OnConveyor → BeingDragged → Sorted). + /// Inherits from DraggableObject to reuse drag/drop system. + /// + 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; + + /// + /// Get the correct box type for this item. + /// + 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(); + + if (stateMachine == null) + stateMachine = GetComponentInChildren(); + } + + /// + /// Setup item as a card. + /// + 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); + } + } + + /// + /// Setup item as garbage. + /// + 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(); + 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"); + } + } + + /// + /// Change to a specific state. + /// + public void ChangeState(string stateName) + { + if (stateMachine != null) + { + stateMachine.ChangeState(stateName); + } + } + + /// + /// Called when item falls off conveyor belt. + /// + public void OnFellOffBelt() + { + // Notify game manager + SortingGameManager.Instance?.OnItemMissed(this); + Destroy(gameObject); + } + } + + /// + /// Interface for states that handle drag behavior. + /// + public interface ISortableItemDragHandler + { + bool OnDragStarted(SortableItemContext context); + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/Core/SortableItem.cs.meta b/Assets/Scripts/Minigames/CardSorting/Core/SortableItem.cs.meta new file mode 100644 index 00000000..3ca3844d --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/SortableItem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9b3db9ce867c4df884411fb3da8fd80a +timeCreated: 1763461867 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Core/SortableItemContext.cs b/Assets/Scripts/Minigames/CardSorting/Core/SortableItemContext.cs new file mode 100644 index 00000000..87d560d1 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/SortableItemContext.cs @@ -0,0 +1,99 @@ +using AppleHills.Data.CardSystem; +using Core.SaveLoad; +using UI.CardSystem; +using UI.CardSystem.StateMachine; +using UnityEngine; + +namespace Minigames.CardSorting.Core +{ + /// + /// 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). + /// + 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(); + } + + if (cardDisplay == null && visualTransform != null) + { + cardDisplay = visualTransform.GetComponentInChildren(); + } + + if (garbageVisual == null && visualTransform != null) + { + garbageVisual = visualTransform.GetComponentInChildren(); + } + + stateMachine = GetComponentInChildren(); + } + + /// + /// Setup as card item - CardDisplay handles all rendering. + /// + public void SetupAsCard(CardData cardData) + { + if (cardDisplay != null) + { + cardDisplay.SetupCard(cardData); + } + else + { + Debug.LogError($"[SortableItemContext] CardDisplay not found on {name}"); + } + } + + /// + /// Setup as garbage item - simple sprite display. + /// + public void SetupAsGarbage(Sprite sprite) + { + if (garbageVisual != null) + { + garbageVisual.UpdateDisplay(sprite); + } + else + { + Debug.LogError($"[SortableItemContext] GarbageVisual not found on {name}"); + } + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/Core/SortableItemContext.cs.meta b/Assets/Scripts/Minigames/CardSorting/Core/SortableItemContext.cs.meta new file mode 100644 index 00000000..d8eb5d2d --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/SortableItemContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9a9c60767eef4a3090d8bf70ee87340f +timeCreated: 1763461839 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Core/SortingBox.cs b/Assets/Scripts/Minigames/CardSorting/Core/SortingBox.cs new file mode 100644 index 00000000..2bd04fcd --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/SortingBox.cs @@ -0,0 +1,41 @@ +using Minigames.CardSorting.Data; +using UI.DragAndDrop.Core; +using UnityEngine; + +namespace Minigames.CardSorting.Core +{ + /// + /// Drop target for sortable items. + /// Validates if item belongs in this box. + /// + public class SortingBox : DraggableSlot + { + [Header("Box Configuration")] + [SerializeField] private BoxType boxType; + [SerializeField] private Sprite boxSprite; + + public BoxType BoxType => boxType; + + /// + /// Check if item belongs in this box. + /// + public bool ValidateItem(SortableItem item) + { + if (item == null) return false; + + BoxType correctBox = item.CorrectBox; + return correctBox == boxType; + } + + /// + /// Check if this slot can accept a specific draggable type. + /// SortingBox accepts all SortableItems (validation happens on drop). + /// + public new bool CanAccept(DraggableObject draggable) + { + // Accept all sortable items (validation happens on drop) + return draggable is SortableItem; + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/Core/SortingBox.cs.meta b/Assets/Scripts/Minigames/CardSorting/Core/SortingBox.cs.meta new file mode 100644 index 00000000..03014ee0 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/SortingBox.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a25b1c9a82b540c8ac0d6c016849f561 +timeCreated: 1763461875 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Core/SortingGameManager.cs b/Assets/Scripts/Minigames/CardSorting/Core/SortingGameManager.cs new file mode 100644 index 00000000..3cfa8a07 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/SortingGameManager.cs @@ -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 +{ + /// + /// Main manager for card sorting minigame. + /// Orchestrates game loop, timer, controllers, and integration with CardSystemManager. + /// Parallel to DivingGameManager. + /// + 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 OnItemSpawned; + public event Action OnItemSortedEvent; + public event Action OnTimerUpdated; // Remaining time + + internal override void OnManagedAwake() + { + _instance = this; + + // Load settings + _settings = GameManager.GetSettingsObject(); + + 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) + } + + /// + /// Called by SortableItem when placed in box. + /// + 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); + } + } + + /// + /// Called when item falls off belt. + /// + 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; + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/Core/SortingGameManager.cs.meta b/Assets/Scripts/Minigames/CardSorting/Core/SortingGameManager.cs.meta new file mode 100644 index 00000000..8f91f86d --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Core/SortingGameManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 20acac8b97ca4d6397612679b3bbde50 +timeCreated: 1763470372 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Data.meta b/Assets/Scripts/Minigames/CardSorting/Data.meta new file mode 100644 index 00000000..54a6aece --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Data.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a450e2687ce14a66b1495e1f2db7d403 +timeCreated: 1763461758 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Data/Enums.cs b/Assets/Scripts/Minigames/CardSorting/Data/Enums.cs new file mode 100644 index 00000000..35a9f244 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Data/Enums.cs @@ -0,0 +1,14 @@ +namespace Minigames.CardSorting.Data +{ + /// + /// Types of sorting boxes in the minigame. + /// + 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 + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/Data/Enums.cs.meta b/Assets/Scripts/Minigames/CardSorting/Data/Enums.cs.meta new file mode 100644 index 00000000..41a16e53 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Data/Enums.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7e238b51f7164867b519bf139fe22c01 +timeCreated: 1763461758 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/Data/GarbageItemDefinition.cs b/Assets/Scripts/Minigames/CardSorting/Data/GarbageItemDefinition.cs new file mode 100644 index 00000000..2faf3c2b --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Data/GarbageItemDefinition.cs @@ -0,0 +1,38 @@ +using UnityEngine; + +namespace Minigames.CardSorting.Data +{ + /// + /// Definition for garbage items (banana peels, cans, receipts, etc.). + /// Cards use existing CardDefinition from CardSystemManager. + /// + [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 + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/Data/GarbageItemDefinition.cs.meta b/Assets/Scripts/Minigames/CardSorting/Data/GarbageItemDefinition.cs.meta new file mode 100644 index 00000000..5896cd19 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/Data/GarbageItemDefinition.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2e69a2167710437798b1980126d5a4f6 +timeCreated: 1763461765 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine.meta b/Assets/Scripts/Minigames/CardSorting/StateMachine.meta new file mode 100644 index 00000000..86845772 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0197a7c7c3174a5fbf1ddd5b1445f24c +timeCreated: 1763461845 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine/SortingStateMachine.cs b/Assets/Scripts/Minigames/CardSorting/StateMachine/SortingStateMachine.cs new file mode 100644 index 00000000..98be0052 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine/SortingStateMachine.cs @@ -0,0 +1,21 @@ +using Core.SaveLoad; + +namespace Minigames.CardSorting.StateMachine +{ + /// + /// 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. + /// + public class SortingStateMachine : AppleMachine + { + /// + /// Opt out of save/load system - sortable items are transient minigame objects. + /// + public override bool ShouldParticipateInSave() + { + return false; + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine/SortingStateMachine.cs.meta b/Assets/Scripts/Minigames/CardSorting/StateMachine/SortingStateMachine.cs.meta new file mode 100644 index 00000000..58e28f91 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine/SortingStateMachine.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1b459f40574b45839aa32d5730627ca6 +timeCreated: 1763461845 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine/States.meta b/Assets/Scripts/Minigames/CardSorting/StateMachine/States.meta new file mode 100644 index 00000000..cfb0f4c8 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine/States.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 24973c8bb25d493885224ac6f099492d +timeCreated: 1763469762 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine/States/BeingDraggedState.cs b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/BeingDraggedState.cs new file mode 100644 index 00000000..75a11290 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/BeingDraggedState.cs @@ -0,0 +1,51 @@ +using Core; +using Core.SaveLoad; +using Minigames.CardSorting.Core; +using UnityEngine; + +namespace Minigames.CardSorting.StateMachine.States +{ + /// + /// 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. + /// + public class BeingDraggedState : AppleState + { + private SortableItemContext _context; + private Vector3 _originalScale; + + private void Awake() + { + _context = GetComponentInParent(); + } + + 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); + } + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine/States/BeingDraggedState.cs.meta b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/BeingDraggedState.cs.meta new file mode 100644 index 00000000..08340806 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/BeingDraggedState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 015c0740240748c8901c9304490cb80d +timeCreated: 1763469770 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine/States/OnConveyorState.cs b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/OnConveyorState.cs new file mode 100644 index 00000000..da42ed7f --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/OnConveyorState.cs @@ -0,0 +1,62 @@ +using Core; +using Core.SaveLoad; +using Minigames.CardSorting.Core; +using UnityEngine; + +namespace Minigames.CardSorting.StateMachine.States +{ + /// + /// Item is moving along the conveyor belt. + /// Transitions to BeingDraggedState when player drags the item. + /// + public class OnConveyorState : AppleState, ISortableItemDragHandler + { + private SortableItemContext _context; + private SortableItem _item; + + private void Awake() + { + _context = GetComponentInParent(); + _item = GetComponentInParent(); + } + + 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; + } + + /// + /// Handle drag start - transition to BeingDraggedState. + /// + 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 + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine/States/OnConveyorState.cs.meta b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/OnConveyorState.cs.meta new file mode 100644 index 00000000..2adba518 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/OnConveyorState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 17d2ba6f5aec4b698247b082734cad8f +timeCreated: 1763469762 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine/States/SortedState.cs b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/SortedState.cs new file mode 100644 index 00000000..c68ec309 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/SortedState.cs @@ -0,0 +1,34 @@ +using Core; +using Core.SaveLoad; +using Minigames.CardSorting.Core; + +namespace Minigames.CardSorting.StateMachine.States +{ + /// + /// Item has been successfully sorted into a box. + /// Plays animation then marks for destruction. + /// Manager handles the actual PopOut animation and destruction. + /// + public class SortedState : AppleState + { + private SortableItemContext _context; + + private void Awake() + { + _context = GetComponentInParent(); + } + + 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 + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/StateMachine/States/SortedState.cs.meta b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/SortedState.cs.meta new file mode 100644 index 00000000..8d2edcd9 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/StateMachine/States/SortedState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0717f922952c4f228930ef0a5f6617b0 +timeCreated: 1763469776 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/UI.meta b/Assets/Scripts/Minigames/CardSorting/UI.meta new file mode 100644 index 00000000..5e555d04 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/UI.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2194dbe8c2c4479f89f7307ce56cac5d +timeCreated: 1763470403 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/UI/SortingGameHUD.cs b/Assets/Scripts/Minigames/CardSorting/UI/SortingGameHUD.cs new file mode 100644 index 00000000..56bfbdd0 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/UI/SortingGameHUD.cs @@ -0,0 +1,116 @@ +using Core.Settings; +using Minigames.CardSorting.Core; +using TMPro; +using UnityEngine; + +namespace Minigames.CardSorting.UI +{ + /// + /// HUD display for card sorting minigame. + /// Shows timer and score during gameplay. + /// + 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(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 + } + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/UI/SortingGameHUD.cs.meta b/Assets/Scripts/Minigames/CardSorting/UI/SortingGameHUD.cs.meta new file mode 100644 index 00000000..dd8fb785 --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/UI/SortingGameHUD.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aa656e03d5384a9eae31fab73b6fe5e2 +timeCreated: 1763470403 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/CardSorting/UI/SortingResultsScreen.cs b/Assets/Scripts/Minigames/CardSorting/UI/SortingResultsScreen.cs new file mode 100644 index 00000000..507ffebb --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/UI/SortingResultsScreen.cs @@ -0,0 +1,126 @@ +using Minigames.CardSorting.Core; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Minigames.CardSorting.UI +{ + /// + /// Results screen shown at end of card sorting minigame. + /// Displays final score, accuracy, and boosters earned. + /// + 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"); + } + } +} + diff --git a/Assets/Scripts/Minigames/CardSorting/UI/SortingResultsScreen.cs.meta b/Assets/Scripts/Minigames/CardSorting/UI/SortingResultsScreen.cs.meta new file mode 100644 index 00000000..0b7a826a --- /dev/null +++ b/Assets/Scripts/Minigames/CardSorting/UI/SortingResultsScreen.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 03823b5ad80b482086569050fbb8bb40 +timeCreated: 1763470418 \ No newline at end of file diff --git a/docs/card_sorting_minigame_plan.md b/docs/card_sorting_minigame_plan.md new file mode 100644 index 00000000..604e3882 --- /dev/null +++ b/docs/card_sorting_minigame_plan.md @@ -0,0 +1,1114 @@ +# Card Sorting Minigame - Implementation Plan + +**Game Type:** Quality Control Sorting Game +**Duration:** 2 minutes +**Objective:** Sort cards and garbage into correct boxes before timer expires +**Rewards:** Booster packs based on performance + +--- + +## Game Design Summary + +### Narrative +Mr. Cement needs help sorting cards from the printing machine. Some cards are defective, and good cards must be sorted by rarity (Normal/Rare/Legend). Defective items go to trash. + +### Gameplay +- **Conveyor Belt**: Items spawn and move right at increasing speed +- **Player Action**: Drag items to correct sorting boxes +- **Difficulty Curve**: Spawn rate and belt speed increase over time +- **Scoring**: Points for correct sorts, penalties for mistakes/misses +- **Game Over**: Timer expires, machine explodes, rewards granted + +### Item Types +1. **Normal Cards** → Copper-rimmed box +2. **Rare Cards** → Silver-rimmed box +3. **Legendary Cards** → Gold-rimmed box +4. **Defective Items** → Trash can (includes: deformed borders, embarrassing images, garbage like banana peels/cans/receipts) + +--- + +## Technical Architecture + +### Code Reuse Strategy + +**From Card System:** +- ✅ `CardDisplay` (nested prefab) - Renders cards with rarity colors, borders, frames +- ✅ `CardAnimator` (component) - All animations (PopIn, PopOut, Scale, Shake, Bounce) +- ✅ `DraggableObject` (base class) - Drag/drop logic, slot detection, pointer events +- ✅ `CardData` (struct) - Visual rendering format for cards + +**From Drag System:** +- ✅ `DraggableSlot` (base class) - Drop target validation + +**New for Minigame:** +- 🆕 `SortingStateMachine` - Opts out of save system (5 lines, copy CardStateMachine pattern) +- 🆕 `SortableItem` - Drag orchestration (inherits DraggableObject) +- 🆕 `SortableItemContext` - Shared state for minigame items +- 🆕 3 States - OnConveyor, BeingDragged, Sorted +- 🆕 `GarbageVisual` - Simple sprite renderer for non-card items (~20 lines) +- 🆕 Data layer - GarbageItemDefinition, SortingGameConfig, Enums (BoxType only) +- 🆕 Controllers - ConveyorBeltController, SortingScoreController +- 🆕 `SortingGameManager` - Main game loop +- 🆕 `SortingBox` - Drop targets with validation + +### Prefab Architecture + +#### Two Prefab Types + +**SortableCardPrefab** (for actual cards): +``` +Root +├── SortableItem.cs +├── SortableItemContext.cs +├── SortingStateMachine +│ ├── OnConveyorState +│ ├── BeingDraggedState +│ └── SortedState +└── Visual (GameObject) ← CardAnimator targets this + └── CardDisplay (NESTED PREFAB) ← Drag from Prefabs/ +``` + +**SortableGarbagePrefab** (for trash items): +``` +Root +├── SortableItem.cs (SAME script) +├── SortableItemContext.cs (SAME script) +├── SortingStateMachine (SAME states) +└── Visual (GameObject) ← CardAnimator targets this + └── GarbageVisual.cs + Image component +``` + +**Key Points:** +- Both prefabs share ALL logic scripts +- Visual differences isolated to "Visual" child GameObject +- CardAnimator animates "Visual" GameObject (works for both) +- SortableItemContext detects which visual component is assigned (cardDisplay vs garbageVisual) + +--- + +## Data Layer + +### Enums + +**BoxType:** +- `Normal` - Copper-rimmed box (Normal rarity cards) +- `Rare` - Silver-rimmed box (Rare rarity cards) +- `Legend` - Gold-rimmed box (Legendary rarity cards) +- `Trash` - Garbage items + +### GarbageItemDefinition (ScriptableObject) + +**Purpose:** Define garbage items only (banana peels, cans, receipts, etc.) + +```csharp +[CreateAssetMenu("Minigames/CardSorting/GarbageItem")] +class GarbageItemDefinition : ScriptableObject +{ + string itemId; + string displayName; + Sprite sprite; +} +``` + +**Cards use existing CardDefinition from CardSystemManager!** +- No need to duplicate card data +- CardSystemManager already has all cards organized by zone/rarity +- Just query CardSystemManager for random card from collection + +### CardSortingSettings (BaseSettings) + +**Follows existing settings architecture pattern (see DivingMinigameSettings)** +- Inherits from `BaseSettings` +- Implements `ICardSortingSettings` interface +- Accessed via `GameManager.GetSettingsObject()` +- Registered in SettingsEditorWindow with "Card Sorting" tab +- Contains all game configuration (timing, speed, weights, scoring, rewards) + +### CardSortingSettings (BaseSettings + Interface) + +**Follows DivingMinigameSettings pattern:** +- Inherits from `BaseSettings` +- Implements `ICardSortingSettings` interface +- Registered in SettingsEditorWindow with own tab +- Accessed via `GameManager.GetSettingsObject()` + +```csharp +[CreateAssetMenu(fileName = "CardSortingSettings", menuName = "AppleHills/Settings/CardSorting", order = 4)] +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; + + [Header("Item Pools")] + [Tooltip("Garbage items that can spawn (banana peels, cans, receipts, etc.)")] + [SerializeField] private GarbageItemDefinition[] garbageItems; + + [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); + initialBeltSpeed = Mathf.Max(0.1f, initialBeltSpeed); + maxBeltSpeed = Mathf.Max(initialBeltSpeed, maxBeltSpeed); + correctSortPoints = Mathf.Max(0, correctSortPoints); + boosterPacksPerCorrectItem = Mathf.Max(0, boosterPacksPerCorrectItem); + } +} + +// Interface +interface ICardSortingSettings +{ + float GameDuration { get; } + float InitialSpawnInterval { get; } + float MinimumSpawnInterval { get; } + float InitialBeltSpeed { get; } + float MaxBeltSpeed { get; } + AnimationCurve SpeedCurve { get; } + GarbageItemDefinition[] GarbageItems { get; } + float NormalCardWeight { get; } + float RareCardWeight { get; } + float LegendCardWeight { get; } + float GarbageWeight { get; } + int CorrectSortPoints { get; } + int IncorrectSortPenalty { get; } + int MissedItemPenalty { get; } + int BoosterPacksPerCorrectItem { get; } +} +``` + +--- + +## Core Components + +### SortableItem (MonoBehaviour) + +**Responsibilities:** +- Inherits from `DraggableObject` +- Orchestrates drag/drop behavior +- Routes events to state machine +- Handles drop validation +- Tracks whether this is a card or garbage item + +**Key Methods:** +```csharp +void SetupAsCard(CardData cardData) // Setup with card from CardSystemManager +void SetupAsGarbage(GarbageItemDefinition) // Setup with garbage item +void OnFellOffBelt() // Called when reaches end of conveyor +protected override void OnDragStartedHook() // Check state handler +protected override void OnDragEndedHook() // Validate drop, notify manager +``` + +**Properties:** +```csharp +bool IsGarbage { get; } // True if garbage, false if card +CardData CardData { get; } // Null if garbage +GarbageItemDefinition GarbageItem { get; } // Null if card +BoxType CorrectBox { get; } // Calculated from rarity or garbage flag +``` + +**Integration Points:** +- Queries state for `ISortableItemDragHandler` +- Validates drop on `SortingBox` +- Notifies `SortingGameManager` on sort/miss + +### SortableItemContext (MonoBehaviour) + +**Responsibilities:** +- Shared state container for states +- Routes data to appropriate visual component (card vs garbage) +- Provides component references + +**Key Properties:** +```csharp +CardDisplay cardDisplay; // Assigned for cards +GarbageVisual garbageVisual; // Assigned for garbage +CardAnimator animator; // Always assigned +Transform visualTransform; // "Visual" GameObject +bool IsOnConveyor; +float ConveyorSpeed; +``` + +**Setup Logic:** +```csharp +void SetupAsCard(CardData cardData) +{ + // CardDisplay handles EVERYTHING + cardDisplay?.UpdateDisplay(cardData); +} + +void SetupAsGarbage(Sprite sprite) +{ + // Simple sprite assignment + garbageVisual?.UpdateDisplay(sprite); +} +``` + +**That's it! No conversion, no mapping, no adapters.** + +### GarbageVisual (MonoBehaviour) + +**Responsibilities:** +- Simple sprite display for non-card items +- Minimal component (~20 lines) + +**Implementation:** +```csharp +class GarbageVisual : MonoBehaviour +{ + [SerializeField] private Image spriteRenderer; + + public void UpdateDisplay(Sprite sprite) + { + if (spriteRenderer != null) + spriteRenderer.sprite = sprite; + } +} +``` + +### SortingBox (MonoBehaviour) + +**Responsibilities:** +- Drop target for sortable items +- Validates correct placement +- Visual feedback (optional) + +**Key Methods:** +```csharp +bool ValidateItem(SortableItem item) +{ + // Trash box accepts garbage + if (boxType == BoxType.Trash) + return item.IsGarbage; + + // Other boxes reject garbage, check card rarity + if (item.IsGarbage) return false; + + return item.CardData.Rarity switch { + CardRarity.Normal => boxType == BoxType.Normal, + CardRarity.Rare => boxType == BoxType.Rare, + CardRarity.Legendary => boxType == BoxType.Legend, + _ => false + }; +} + +override bool CanAccept(DraggableObject draggable) +{ + return draggable is SortableItem; +} +``` + +--- + +## State Machine + +### SortingStateMachine (Opts Out of Save System) + +```csharp +class SortingStateMachine : AppleMachine +{ + public override bool ShouldParticipateInSave() => false; +} +``` + +### Three States + +#### 1. OnConveyorState + +**Purpose:** Item moves along belt + +**Behavior:** +```csharp +void Update() +{ + Vector3 movement = Vector3.right * context.ConveyorSpeed * Time.deltaTime; + context.visualTransform.position += movement; +} + +bool OnDragStarted(SortableItemContext context) +{ + item.ChangeState("BeingDraggedState"); + return true; // Handled +} +``` + +**Entry:** Item spawned on belt +**Exit:** Player drags item OR item falls off end + +#### 2. BeingDraggedState + +**Purpose:** Player is dragging item + +**Behavior:** +```csharp +void OnEnterState() +{ + context.IsOnConveyor = false; + context.Animator.AnimateScale(Vector3.one * 1.1f, 0.2f); // Visual feedback +} + +void OnExitState() +{ + context.Animator.AnimateScale(Vector3.one, 0.2f); // Restore +} +``` + +**Entry:** Drag started from OnConveyorState +**Exit:** Drop on box (→ SortedState) OR drop elsewhere (→ OnConveyorState) + +#### 3. SortedState + +**Purpose:** Item placed in box, ready for destruction + +**Behavior:** +```csharp +void OnEnterState() +{ + context.IsOnConveyor = false; + // Animation handled by manager +} +``` + +**Entry:** Successfully dropped on box +**Exit:** Destroyed after animation + +--- + +## Controllers (Non-Component) + +### ConveyorBeltController + +**Responsibilities:** +- Spawn items at intervals +- Track active items +- Update belt speed +- Detect items falling off end + +**Constructor:** +```csharp +ConveyorBeltController( + Transform spawnPoint, + Transform endPoint, + GameObject cardPrefab, + GameObject garbagePrefab, + ICardSortingSettings settings // Uses settings interface +) +``` + +**Key Methods:** +```csharp +void Update(float deltaTime, float gameProgress) +{ + UpdateBeltSpeed(gameProgress); // Adjust speed via config curve + UpdateItemPositions(deltaTime); // Movement handled by states + CheckItemsOffBelt(); // Detect falls, notify manager +} + +SortableItem TrySpawnItem(float currentTime) +{ + if (currentTime < nextSpawnTime) return null; + + // Weighted random: card or garbage? + float totalWeight = config.normalCardWeight + config.rareCardWeight + + config.legendCardWeight + config.garbageWeight; + float roll = Random.Range(0f, totalWeight); + + SortableItem item; + + if (roll < config.garbageWeight) + { + // Spawn garbage + GarbageItemDefinition garbage = SelectRandomGarbage(); + GameObject obj = Instantiate(garbagePrefab, spawnPoint.position, Quaternion.identity); + item = obj.GetComponent(); + item.SetupAsGarbage(garbage); + } + else + { + // Spawn card - determine rarity, get random card from CardSystemManager + CardRarity rarity = DetermineRarity(roll); + CardData cardData = CardSystemManager.Instance.GetRandomCardOfRarity(rarity); + + GameObject obj = Instantiate(cardPrefab, spawnPoint.position, Quaternion.identity); + item = obj.GetComponent(); + item.SetupAsCard(cardData); + } + + activeItems.Add(item); + ScheduleNextSpawn(gameProgress); + return item; +} + +CardRarity DetermineRarity(float roll) +{ + // Adjust roll to be relative to card weights only + float adjusted = roll - config.garbageWeight; + + if (adjusted < config.normalCardWeight) return CardRarity.Normal; + if (adjusted < config.normalCardWeight + config.rareCardWeight) return CardRarity.Rare; + return CardRarity.Legendary; +} + +GarbageItemDefinition SelectRandomGarbage() +{ + return config.garbageItems[Random.Range(0, config.garbageItems.Length)]; +} + +void RemoveItem(SortableItem item) // Manual removal +``` + +**Properties:** +```csharp +float CurrentSpeed { get; } +int ActiveItemCount { get; } +``` + +### SortingScoreController + +**Responsibilities:** +- Track correct/incorrect/missed counts +- Calculate total score +- Calculate accuracy +- Calculate booster rewards + +**Constructor:** +```csharp +SortingScoreController(ICardSortingSettings settings) +``` + +**Properties:** +```csharp +int TotalScore { get; } +int CorrectSorts { get; } +int IncorrectSorts { get; } +int MissedItems { get; } +float Accuracy { get; } // correctSorts / totalAttempts +``` + +**Events:** +```csharp +event Action OnScoreChanged; +event Action OnCorrectSort; +event Action OnIncorrectSort; +``` + +--- + +## Main Manager + +### SortingGameManager (ManagedBehaviour) + +**Responsibilities:** +- Game loop orchestration +- Timer management +- Controller lifecycle +- Event coordination +- Integration with CardSystemManager + +**Singleton:** +```csharp +static SortingGameManager Instance { get; } +``` + +**Settings Access:** +```csharp +private ICardSortingSettings _settings; + +internal override void OnManagedAwake() +{ + _instance = this; + _settings = GameManager.GetSettingsObject(); +} +``` + +**Lazy-Init Controllers:** +```csharp +private ConveyorBeltController _conveyorController; +private ConveyorBeltController Conveyor => _conveyorController ??= new ConveyorBeltController( + conveyorSpawnPoint, + conveyorEndPoint, + cardPrefab, + garbagePrefab, + _settings +); + +private SortingScoreController _scoreController; +private SortingScoreController Score => _scoreController ??= new SortingScoreController(_settings); +``` + +**Game Loop:** +```csharp +void Update() +{ + if (!isGameActive || isGameOver) return; + + gameTimer += Time.deltaTime; + float remainingTime = config.gameDuration - gameTimer; + float gameProgress = gameTimer / config.gameDuration; + + OnTimerUpdated?.Invoke(remainingTime); + + if (remainingTime <= 0f) { EndGame(); return; } + + Conveyor.Update(Time.deltaTime, gameProgress); + + var item = Conveyor.TrySpawnItem(gameTimer); + if (item != null) OnItemSpawned?.Invoke(item); +} +``` + +**Public API (Called by SortableItem):** +```csharp +void OnItemSorted(SortableItem item, SortingBox box, bool correct) +{ + if (correct) Score.RecordCorrectSort(); + else Score.RecordIncorrectSort(); + + Conveyor.RemoveItem(item); + OnItemSortedEvent?.Invoke(item, box, correct); + + // Play animation then destroy + item.Context.Animator.PopOut(0.4f, () => Destroy(item.gameObject)); +} + +void OnItemMissed(SortableItem item) +{ + Score.RecordMissedItem(); + Conveyor.RemoveItem(item); +} +``` + +**End Game:** +```csharp +void EndGame() +{ + isGameOver = true; + int boosters = Score.CalculateBoosterReward(); + CardSystemManager.Instance.AddBoosterPack(boosters); + OnGameEnded?.Invoke(); +} +``` + +**Events:** +```csharp +event Action OnGameStarted; +event Action OnGameEnded; +event Action OnItemSpawned; +event Action OnItemSortedEvent; +event Action OnTimerUpdated; // Remaining time +``` + +--- + +## Animation Strategy + +### All Animations via CardAnimator + +**Target:** "Visual" GameObject (parent of CardDisplay/GarbageVisual) + +**Common Animations:** +```csharp +// Spawn on conveyor +context.Animator.PopIn(0.3f); + +// Pick up (BeingDraggedState enter) +context.Animator.AnimateScale(Vector3.one * 1.1f, 0.2f); +context.Animator.PlayBounce(0.1f); + +// Drop back to belt (BeingDraggedState exit) +context.Animator.AnimateScale(Vector3.one, 0.2f); + +// Successful sort (SortedState) +context.Animator.PopOut(0.4f, () => Destroy(gameObject)); + +// Failed sort feedback +context.Animator.PlayShake(0.3f); + +// Conveyor wobble (continuous in OnConveyorState) +context.Animator.PlayBounce(0.05f); + +// Hover over correct box +context.Animator.AnimateScale(Vector3.one * 1.1f, 0.15f); + +// Hover exit +context.Animator.AnimateScale(Vector3.one, 0.15f); +``` + +**Visual GameObject Setup:** +- CardAnimator component attached to "Visual" GameObject +- SortableItemContext.visualTransform references "Visual" +- SortableItemContext.animator references CardAnimator +- States call `context.Animator.MethodName()` + +--- + +## Implementation Phases + +### Phase 1: Data Foundation +**Goal:** Define all data structures and configurations + +**Tasks:** +1. Create `Data/Enums.cs` - BoxType only +2. Create `Data/GarbageItemDefinition.cs` - ScriptableObject for garbage items +3. Create `Core/Settings/ICardSortingSettings.cs` - Interface (follow IDivingMinigameSettings pattern) +4. Create `Core/Settings/CardSortingSettings.cs` - Settings class (follow DivingMinigameSettings pattern) +5. Register CardSortingSettings in SettingsEditorWindow: + - Add to tab names array: "Card Sorting" + - Add case to switch statement + - Add to CreateSettingsIfMissing calls +6. Create 3-5 GarbageItemDefinitions in editor (banana peel, can, receipt, etc.) +7. Create CardSortingSettings asset via menu (will auto-create on first editor window open) + +**Validation:** +- Can create GarbageItemDefinition assets +- CardSortingSettings appears in Settings Editor window +- Can modify settings values in editor +- Settings accessible via `GameManager.GetSettingsObject()` + +--- + +### Phase 2: Core Components +**Goal:** Build sortable item and visual system + +**Tasks:** +1. Create `Core/GarbageVisual.cs` - Simple sprite renderer +2. Create `Core/SortableItemContext.cs` - Shared state, visual routing +3. Create `StateMachine/SortingStateMachine.cs` - Save opt-out (copy CardStateMachine) +4. Create `Core/SortableItem.cs` - Drag orchestration +5. Create `Core/SortingBox.cs` - Drop target validation + +**Validation:** +- GarbageVisual displays sprite correctly +- SortableItemContext routes data to cardDisplay/garbageVisual +- SortingStateMachine doesn't register with SaveLoadManager +- SortableItem inherits DraggableObject correctly +- SortingBox validates card rarity or garbage flag correctly + +--- + +### Phase 3: State Machine +**Goal:** Implement 3 states for item behavior + +**Tasks:** +1. Create `StateMachine/States/OnConveyorState.cs` - Movement logic +2. Create `StateMachine/States/BeingDraggedState.cs` - Visual feedback +3. Create `StateMachine/States/SortedState.cs` - Destruction ready +4. Add `ISortableItemDragHandler` interface to OnConveyorState + +**Validation:** +- OnConveyorState moves item right at ConveyorSpeed +- Drag starts → transitions to BeingDraggedState +- BeingDraggedState scales up visuals +- Drop on box → transitions to SortedState +- States use OnDisable() for cleanup (not OnExitState - AppleState doesn't have it) + +--- + +### Phase 4: Prefab Creation +**Goal:** Build two prefab variants with shared logic + +**Tasks:** +1. Create `SortableCardPrefab`: + - Add SortableItem, SortableItemContext components + - Add CardAnimator component + - Create "Visual" child GameObject + - Nest CardDisplay prefab under "Visual" + - Add SortingStateMachine with 3 state children + - Wire all references +2. Create `SortableGarbagePrefab`: + - Duplicate SortableCardPrefab + - Remove CardDisplay nested prefab + - Add GarbageVisual component to "Visual" GameObject + - Add Image component to "Visual" + - Wire garbageVisual reference +3. Create 4 `SortingBox` prefabs (Normal/Rare/Legend/Trash) + +**Validation:** +- Both prefabs use SAME scripts +- CardAnimator targets "Visual" GameObject on both +- SortableCardPrefab has cardDisplay assigned, garbageVisual null +- SortableGarbagePrefab has garbageVisual assigned, cardDisplay null +- Dragging works on both prefabs +- Visual differences isolated to "Visual" child + +--- + +### Phase 5: Controllers ✅ COMPLETE +**Goal:** Implement non-component logic controllers + +**Tasks:** +1. ✅ Create `Controllers/ConveyorBeltController.cs`: + - Spawning with weighted random selection + - Speed curve evaluation + - Active item tracking + - Fall-off detection +2. ✅ Create `Controllers/SortingScoreController.cs`: + - Score tracking + - Accuracy calculation + - Booster reward calculation + - Event firing + +**Validation:** +- ✅ ConveyorBeltController spawns correct prefab (card vs garbage) +- ✅ Spawn rate increases over time (lerp based on game progress) +- ✅ Belt speed increases per config curve +- ✅ Items detected when falling off end (x position > endPoint.x) +- ✅ Score updates correctly on correct/incorrect/missed +- ✅ Accuracy percentage calculated correctly (correctSorts / totalAttempts) +- ✅ No compilation errors + +--- + +### Phase 6: Game Manager ✅ COMPLETE +**Goal:** Orchestrate game loop and integration + +**Tasks:** +1. ✅ Create `Core/SortingGameManager.cs`: + - Singleton setup + - Lazy-init controllers + - Game loop (Update) + - Timer management + - Event handling + - Integration with CardSystemManager +2. ✅ Add inspector references (spawnPoint, endPoint, prefabs, boxes, config) +3. ✅ Uncomment SortingGameManager references in SortableItem + +**Validation:** +- ✅ Timer counts down correctly +- ✅ Items spawn at increasing rate +- ✅ Belt speed increases +- ✅ OnItemSorted called when dropped on box +- ✅ OnItemMissed called when falls off belt +- ✅ Boosters granted at end via CardSystemManager +- ✅ Events fire correctly +- ✅ No compilation errors + +--- + +### Phase 7: Scene Setup +**Goal:** Build playable minigame scene + +**Tasks:** +1. Create new scene `CardSorting.unity` +2. Add conveyor belt visual (sprite/sprite renderer) +3. Position 4 SortingBox instances: + - Normal (bottom-left) + - Rare (bottom-center-left) + - Legend (bottom-center-right) + - Trash (bottom-right) +4. Add spawn point marker (empty GameObject at left edge) +5. Add end point marker (empty GameObject at right edge) +6. Add SortingGameManager to scene +7. Wire all references in inspector (spawn points, prefabs, boxes) + - Note: Settings accessed via GameManager, no inspector reference needed +8. Add Canvas with UI layout (timer, score - can be placeholder) + +**Validation:** +- Play scene, items spawn and move +- Drag item to box, validates correctly +- Correct sort → score increases +- Incorrect sort → penalty applied +- Items fall off end → miss penalty +- Timer expires → game ends, boosters granted +- Settings changes in editor reflect in game + +--- + +### Phase 8: UI (Minimal MVP) ✅ COMPLETE +**Goal:** Display essential game info + +**Tasks:** +1. ✅ Create `UI/SortingGameHUD.cs`: + - Timer display (TextMeshProUGUI) + - Score display (TextMeshProUGUI) + - Accuracy display (TextMeshProUGUI) + - Subscribe to SortingGameManager events +2. ✅ Create `UI/SortingResultsScreen.cs`: + - Final score + - Accuracy percentage + - Boosters earned + - Close button + +**Validation:** +- ✅ Timer updates every frame +- ✅ Score updates on events +- ✅ Accuracy displays correctly +- ✅ Results screen shows correct data +- ✅ Can close and return to game +- ✅ No compilation errors + +--- + +### Phase 9: Polish (Post-MVP) +**Optional enhancements after core functionality proven** + +**Tasks:** +1. Particle effects on correct sort +2. Sound effects (pickup, drop, correct, wrong) +3. Visual feedback when hovering over correct/wrong box +4. Combo system (consecutive correct sorts) +5. More defect types with visual indicators +6. Tutorial sequence with Mr. Cement dialogue +7. Background music +8. Conveyor belt animation (scrolling texture) + +--- + +## File Structure + +``` +Assets/Scripts/Minigames/CardSorting/ +├── Core/ +│ ├── SortingGameManager.cs +│ ├── SortableItem.cs +│ ├── SortableItemContext.cs +│ ├── GarbageVisual.cs +│ └── SortingBox.cs +├── StateMachine/ +│ ├── SortingStateMachine.cs +│ └── States/ +│ ├── OnConveyorState.cs +│ ├── BeingDraggedState.cs +│ └── SortedState.cs +├── Controllers/ +│ ├── ConveyorBeltController.cs +│ └── SortingScoreController.cs +├── Data/ +│ ├── Enums.cs +│ └── GarbageItemDefinition.cs +└── UI/ + ├── SortingGameHUD.cs + └── SortingResultsScreen.cs + +Assets/Scripts/Core/Settings/ +├── ICardSortingSettings.cs (interface) +└── CardSortingSettings.cs (BaseSettings implementation) + +Assets/Editor/Settings/ +└── SettingsEditorWindow.cs (add CardSortingSettings tab) +``` + +--- + +## Integration Points + +### GameManager (Settings) +```csharp +// Access settings (available after GameManager initialization) +ICardSortingSettings settings = GameManager.GetSettingsObject(); +``` + +### CardSystemManager +```csharp +// Get random card by rarity +CardData cardData = CardSystemManager.Instance.GetRandomCardOfRarity(CardRarity.Rare); + +// Grant boosters at game end +CardSystemManager.Instance.AddBoosterPack(boosterCount); +``` + +### InputManager +```csharp +// Set input mode on game start +InputManager.Instance.SetInputMode(InputMode.GameAndUI); +``` + +### UIPageController (Optional) +```csharp +// Show results screen +UIPageController.Instance.PushPage(sortingResultsScreen); +``` + +### SettingsEditorWindow +```csharp +// Add CardSortingSettings tab (Phase 1) +// Follows pattern of DivingMinigameSettings registration +``` + +--- + +## Key Design Decisions + +### 1. Two Prefabs, Shared Logic +- Cards and garbage use SAME scripts (SortableItem, SortableItemContext, states) +- Visual differences isolated to "Visual" child GameObject +- CardAnimator animates "Visual" on both prefabs +- No code duplication + +### 2. Cards Use Existing CardDefinitions +- **No data duplication!** Cards spawn from CardSystemManager's existing CardDefinition collection +- CardDisplay receives CardData directly, handles all rendering +- Garbage items have simple GarbageItemDefinition (id, name, sprite) +- Zero conversion/mapping layer needed + +### 3. Settings Architecture Pattern +- **Follows DivingMinigameSettings pattern exactly** +- CardSortingSettings inherits BaseSettings, implements ICardSortingSettings +- Registered in SettingsEditorWindow with own tab +- Accessed via `GameManager.GetSettingsObject()` +- Designer-friendly: all configuration in Unity editor +- No ScriptableObject references in scene (accessed globally via GameManager) + +### 4. Visual GameObject Strategy +- "Visual" GameObject is parent of CardDisplay/GarbageVisual +- CardAnimator targets "Visual" +- Animations work identically for both item types +- Clean separation of logic (root) vs presentation (visual child) + +### 5. State Machine Simplicity +- Only 3 states vs 10 in card system +- Linear flow: OnConveyor → BeingDragged → Sorted +- No complex branching or edge cases +- Opts out of save system (runtime-only) + +### 6. Non-Component Controllers +- ConveyorBeltController, SortingScoreController are plain C# classes +- Lazy-initialized in SortingGameManager +- No Unity lifecycle overhead +- Easy to test and maintain + +### 7. Event-Driven Architecture +- Controllers fire events, UI subscribes +- Decoupled components +- Easy to extend with new features + +--- + +## Testing Checklist + +### Core Functionality +- [ ] Items spawn on conveyor at correct rate +- [ ] Items move right at increasing speed +- [ ] Can drag items from conveyor +- [ ] Cards display with correct rarity colors/borders +- [ ] Garbage displays with sprite only +- [ ] Drop on correct box → score increases +- [ ] Drop on wrong box → penalty applied +- [ ] Item falls off belt → miss penalty +- [ ] Timer counts down correctly +- [ ] Game ends at 0:00 + +### Animations +- [ ] PopIn on spawn +- [ ] Scale up when dragged +- [ ] Scale down when released +- [ ] PopOut on successful sort +- [ ] Shake on failed sort +- [ ] Conveyor wobble (continuous) + +### Scoring +- [ ] Correct sort grants points +- [ ] Incorrect sort applies penalty +- [ ] Missed item applies penalty +- [ ] Accuracy calculated correctly +- [ ] Booster reward calculated correctly + +### Integration +- [ ] Boosters granted to CardSystemManager +- [ ] Input mode set correctly +- [ ] No save system participation + +### Edge Cases +- [ ] Drag item off screen → returns to belt or destroys +- [ ] Multiple items on belt simultaneously +- [ ] Rapid dragging doesn't break spawning +- [ ] Game end mid-drag handles gracefully +- [ ] Empty item pools handled gracefully + +--- + +## Code Metrics Estimate + +**Total New Code:** ~800 lines +- SortingGameManager: ~150 lines +- ConveyorBeltController: ~160 lines (simpler spawning) +- SortingScoreController: ~80 lines +- SortableItem + SortableItemContext: ~120 lines (no conversion logic) +- 3 States: ~120 lines +- Data types: ~40 lines (Enums, GarbageItemDefinition only) +- Settings: ~100 lines (ICardSortingSettings interface + CardSortingSettings class) +- SettingsEditorWindow changes: ~10 lines (registration) +- GarbageVisual: ~20 lines +- SortingBox: ~40 lines +- UI scripts: ~70 lines + +**Reused Code:** ~2000+ lines +- CardDisplay (complete visual system) +- CardAnimator (all animations) +- DraggableObject (drag/drop framework) +- AppleMachine (state machine base) +- **CardData (used directly, no conversion!)** +- **CardDefinition (from CardSystemManager!)** +- **Settings architecture (BaseSettings, GameManager integration)** + +**Code Duplication:** 0 lines +**Data Duplication:** 0 (cards reuse existing definitions!) + +--- + +**Last Updated:** November 18, 2025 +**Status:** Planning Phase +**Next Step:** Phase 1 - Data Foundation +