Merge branch 'main' of https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using AppleHills.Core.Settings;
|
||||
using Minigames.CardSorting.Data;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Core.Settings
|
||||
@@ -29,21 +28,21 @@ namespace Core.Settings
|
||||
[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];
|
||||
[Tooltip("Normal rarity card prefabs that can spawn")]
|
||||
[SerializeField] private GameObject[] normalCardPrefabs = new GameObject[0];
|
||||
|
||||
[Tooltip("Rare rarity card prefabs that can spawn")]
|
||||
[SerializeField] private GameObject[] rareCardPrefabs = new GameObject[0];
|
||||
|
||||
[Tooltip("Legendary rarity card prefabs that can spawn")]
|
||||
[SerializeField] private GameObject[] legendaryCardPrefabs = new GameObject[0];
|
||||
|
||||
[Tooltip("Garbage prefabs that can spawn")]
|
||||
[SerializeField] private GameObject[] garbagePrefabs = new GameObject[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;
|
||||
[Tooltip("Ratio of cards to garbage (0 = all garbage, 0.5 = 50/50 split, 1 = all cards)")]
|
||||
[Range(0, 1)] [SerializeField] private float cardToGarbageRatio = 0.5f;
|
||||
|
||||
[Header("Scoring")]
|
||||
[Tooltip("Points awarded for correct sort")]
|
||||
@@ -81,11 +80,11 @@ namespace Core.Settings
|
||||
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 GameObject[] NormalCardPrefabs => normalCardPrefabs;
|
||||
public GameObject[] RareCardPrefabs => rareCardPrefabs;
|
||||
public GameObject[] LegendaryCardPrefabs => legendaryCardPrefabs;
|
||||
public GameObject[] GarbagePrefabs => garbagePrefabs;
|
||||
public float CardToGarbageRatio => cardToGarbageRatio;
|
||||
public int CorrectSortPoints => correctSortPoints;
|
||||
public int IncorrectSortPenalty => incorrectSortPenalty;
|
||||
public int MissedItemPenalty => missedItemPenalty;
|
||||
|
||||
@@ -18,14 +18,14 @@ namespace Core.Settings
|
||||
float MaxBeltSpeed { get; }
|
||||
AnimationCurve SpeedCurve { get; }
|
||||
|
||||
// Item Pools
|
||||
GarbageItemDefinition[] GarbageItems { get; }
|
||||
// Item Pools - Arrays of prefabs
|
||||
GameObject[] NormalCardPrefabs { get; }
|
||||
GameObject[] RareCardPrefabs { get; }
|
||||
GameObject[] LegendaryCardPrefabs { get; }
|
||||
GameObject[] GarbagePrefabs { get; }
|
||||
|
||||
// Spawn Weights
|
||||
float NormalCardWeight { get; }
|
||||
float RareCardWeight { get; }
|
||||
float LegendCardWeight { get; }
|
||||
float GarbageWeight { get; }
|
||||
// Spawn Ratio (0 = all garbage, 1 = all cards)
|
||||
float CardToGarbageRatio { get; }
|
||||
|
||||
// Scoring
|
||||
int CorrectSortPoints { get; }
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5112d15beb144088e8b8752879deda3
|
||||
timeCreated: 1760105142
|
||||
@@ -1,86 +0,0 @@
|
||||
using UnityEngine;
|
||||
using AppleHills.Core;
|
||||
using Core;
|
||||
|
||||
namespace AppleHills.Examples
|
||||
{
|
||||
/// <summary>
|
||||
/// Example script demonstrating how to use QuickAccess to quickly retrieve and use game objects
|
||||
/// </summary>
|
||||
public class QuickAccessExample : MonoBehaviour
|
||||
{
|
||||
void Start()
|
||||
{
|
||||
// Retrieve player and follower objects using QuickAccess
|
||||
GameObject player = QuickAccess.Instance.PlayerGameObject;
|
||||
GameObject follower = QuickAccess.Instance.FollowerGameObject;
|
||||
|
||||
// Print info about the player
|
||||
if (player != null)
|
||||
{
|
||||
Logging.Debug($"[QuickAccessExample] Player found: {player.name}");
|
||||
Logging.Debug($"[QuickAccessExample] Player position: {player.transform.position}");
|
||||
|
||||
// Access player controller
|
||||
var playerController = QuickAccess.Instance.PlayerController;
|
||||
if (playerController != null)
|
||||
{
|
||||
Logging.Debug($"[QuickAccessExample] Player controller found on object");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning($"[QuickAccessExample] Player controller not found");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning($"[QuickAccessExample] Player not found in scene");
|
||||
}
|
||||
|
||||
// Print info about the follower (Pulver)
|
||||
if (follower != null)
|
||||
{
|
||||
Logging.Debug($"[QuickAccessExample] Follower found: {follower.name}");
|
||||
Logging.Debug($"[QuickAccessExample] Follower position: {follower.transform.position}");
|
||||
|
||||
// Access follower controller
|
||||
var followerController = QuickAccess.Instance.FollowerController;
|
||||
if (followerController != null)
|
||||
{
|
||||
Logging.Debug($"[QuickAccessExample] Follower controller found on object");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning($"[QuickAccessExample] Follower controller not found");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning($"[QuickAccessExample] Follower not found in scene");
|
||||
}
|
||||
|
||||
// Access camera
|
||||
var camera = QuickAccess.Instance.MainCamera;
|
||||
if (camera != null)
|
||||
{
|
||||
Logging.Debug($"[QuickAccessExample] Main camera found: {camera.name}");
|
||||
Logging.Debug($"[QuickAccessExample] Camera position: {camera.transform.position}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning($"[QuickAccessExample] Main camera not found");
|
||||
}
|
||||
|
||||
// Access managers
|
||||
try
|
||||
{
|
||||
Logging.Debug($"[QuickAccessExample] Game Manager instance accessed: {QuickAccess.Instance.GameManager != null}");
|
||||
Logging.Debug($"[QuickAccessExample] Input Manager instance accessed: {QuickAccess.Instance.InputManager != null}");
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"[QuickAccessExample] Error accessing managers: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6086c2645c14cad92be0a9c7c191fdc
|
||||
timeCreated: 1760105142
|
||||
@@ -1,8 +1,6 @@
|
||||
using AppleHills.Data.CardSystem;
|
||||
using Core.Settings;
|
||||
using Data.CardSystem;
|
||||
using Minigames.CardSorting.Core;
|
||||
using Minigames.CardSorting.Data;
|
||||
using System.Collections.Generic;
|
||||
using UI.DragAndDrop.Core;
|
||||
using UnityEngine;
|
||||
@@ -20,8 +18,6 @@ namespace Minigames.CardSorting.Controllers
|
||||
private readonly Transform spawnPoint;
|
||||
private readonly Transform endPoint; // Visual end - scoring happens here
|
||||
private readonly Transform despawnPoint; // Off-screen - destruction happens here
|
||||
private readonly GameObject cardPrefab;
|
||||
private readonly GameObject garbagePrefab;
|
||||
private readonly ICardSortingSettings settings;
|
||||
private readonly Transform spawnContainer; // Container for all spawned items
|
||||
|
||||
@@ -30,7 +26,7 @@ namespace Minigames.CardSorting.Controllers
|
||||
private float currentSpeed;
|
||||
private SortableItem lastSpawnedItem; // Track last spawned item for distance-based spawning
|
||||
private float cachedSpawnOffsetX; // Cached random offset for next spawn
|
||||
private bool isGameOver = false; // Flag to stop conveyor when game ends
|
||||
private bool isGameOver; // Flag to stop conveyor when game ends
|
||||
|
||||
// Events - conveyor owns item lifecycle
|
||||
public event System.Action<SortableItem> OnItemSpawned; // Fired when new item spawns
|
||||
@@ -47,16 +43,12 @@ namespace Minigames.CardSorting.Controllers
|
||||
Transform spawnPoint,
|
||||
Transform endPoint,
|
||||
Transform despawnPoint,
|
||||
GameObject cardPrefab,
|
||||
GameObject garbagePrefab,
|
||||
ICardSortingSettings settings,
|
||||
Transform spawnContainer)
|
||||
{
|
||||
this.spawnPoint = spawnPoint;
|
||||
this.endPoint = endPoint;
|
||||
this.despawnPoint = despawnPoint;
|
||||
this.cardPrefab = cardPrefab;
|
||||
this.garbagePrefab = garbagePrefab;
|
||||
this.settings = settings;
|
||||
this.spawnContainer = spawnContainer;
|
||||
|
||||
@@ -114,33 +106,60 @@ namespace Minigames.CardSorting.Controllers
|
||||
|
||||
/// <summary>
|
||||
/// Spawn a new item at the spawn point.
|
||||
/// Uses cardToGarbageRatio (0-1) to determine card vs garbage spawn chance.
|
||||
/// If cards spawn, rarity is determined by array lengths.
|
||||
/// </summary>
|
||||
private SortableItem SpawnNewItem(float gameProgress)
|
||||
private void SpawnNewItem(float gameProgress)
|
||||
{
|
||||
// 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);
|
||||
// Use ratio to decide: card or garbage?
|
||||
float cardChance = settings.CardToGarbageRatio; // 0 = all garbage, 1 = all cards
|
||||
float roll = Random.Range(0f, 1f);
|
||||
|
||||
SortableItem item;
|
||||
|
||||
if (roll < settings.GarbageWeight)
|
||||
if (roll < cardChance)
|
||||
{
|
||||
// Spawn garbage
|
||||
item = SpawnGarbageItem();
|
||||
// Spawn card - determine rarity based on array lengths
|
||||
int normalWeight = settings.NormalCardPrefabs?.Length ?? 0;
|
||||
int rareWeight = settings.RareCardPrefabs?.Length ?? 0;
|
||||
int legendWeight = settings.LegendaryCardPrefabs?.Length ?? 0;
|
||||
|
||||
int totalCardWeight = normalWeight + rareWeight + legendWeight;
|
||||
|
||||
if (totalCardWeight <= 0)
|
||||
{
|
||||
Debug.LogWarning("[ConveyorBeltController] No card prefabs configured, spawning garbage instead");
|
||||
item = SpawnRandomPrefabFromArray(settings.GarbagePrefabs, true, CardRarity.Normal);
|
||||
}
|
||||
else
|
||||
{
|
||||
float rarityRoll = Random.Range(0f, totalCardWeight);
|
||||
CardRarity rarity;
|
||||
GameObject[] targetArray;
|
||||
|
||||
if (rarityRoll < normalWeight)
|
||||
{
|
||||
rarity = CardRarity.Normal;
|
||||
targetArray = settings.NormalCardPrefabs;
|
||||
}
|
||||
else if (rarityRoll < normalWeight + rareWeight)
|
||||
{
|
||||
rarity = CardRarity.Rare;
|
||||
targetArray = settings.RareCardPrefabs;
|
||||
}
|
||||
else
|
||||
{
|
||||
rarity = CardRarity.Legendary;
|
||||
targetArray = settings.LegendaryCardPrefabs;
|
||||
}
|
||||
|
||||
item = SpawnRandomPrefabFromArray(targetArray, false, rarity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Spawn card - determine rarity, get random card from CardSystemManager
|
||||
CardRarity rarity = DetermineRarity(roll);
|
||||
item = SpawnCardItem(rarity);
|
||||
// Spawn garbage
|
||||
item = SpawnRandomPrefabFromArray(settings.GarbagePrefabs, true, CardRarity.Normal);
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
@@ -154,114 +173,61 @@ namespace Minigames.CardSorting.Controllers
|
||||
// Emit spawn event
|
||||
OnItemSpawned?.Invoke(item);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private SortableItem SpawnGarbageItem()
|
||||
{
|
||||
if (settings.GarbageItems == null || settings.GarbageItems.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[ConveyorBeltController] No garbage items configured!");
|
||||
return null;
|
||||
}
|
||||
|
||||
GarbageItemDefinition garbage = SelectRandomGarbage();
|
||||
|
||||
// Apply random Y offset to spawn position
|
||||
float randomOffsetY = Random.Range(settings.SpawnOffsetY.x, settings.SpawnOffsetY.y);
|
||||
Vector3 spawnPos = spawnPoint.position + new Vector3(0f, randomOffsetY, 0f);
|
||||
|
||||
GameObject obj = Object.Instantiate(garbagePrefab, spawnPos, Quaternion.identity, spawnContainer);
|
||||
SortableItem item = obj.GetComponent<SortableItem>();
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
item.SetupAsGarbage(garbage);
|
||||
|
||||
// Apply card size (garbage items use same size as cards)
|
||||
ApplyCardSize(item);
|
||||
|
||||
// Subscribe to item events
|
||||
item.OnItemDroppedInBox += HandleItemDroppedInBox;
|
||||
item.OnItemDroppedOnFloor += HandleItemDroppedOnFloor;
|
||||
item.OnItemReturnedToConveyor += HandleItemReturnedToConveyor;
|
||||
|
||||
// Subscribe to drag events to remove from tracking
|
||||
item.OnDragStarted += HandleItemDragStarted;
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Apply random Y offset to spawn position
|
||||
float randomOffsetY = Random.Range(settings.SpawnOffsetY.x, settings.SpawnOffsetY.y);
|
||||
Vector3 spawnPos = spawnPoint.position + new Vector3(0f, randomOffsetY, 0f);
|
||||
|
||||
GameObject obj = Object.Instantiate(cardPrefab, spawnPos, Quaternion.identity, spawnContainer);
|
||||
SortableItem item = obj.GetComponent<SortableItem>();
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
item.SetupAsCard(cardData);
|
||||
|
||||
// Apply card size
|
||||
ApplyCardSize(item);
|
||||
|
||||
// Subscribe to item events
|
||||
item.OnItemDroppedInBox += HandleItemDroppedInBox;
|
||||
item.OnItemDroppedOnFloor += HandleItemDroppedOnFloor;
|
||||
item.OnItemReturnedToConveyor += HandleItemReturnedToConveyor;
|
||||
|
||||
// Subscribe to drag events to remove from tracking
|
||||
item.OnDragStarted += HandleItemDragStarted;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[ConveyorBeltController] Card prefab missing SortableItem component!");
|
||||
Object.Destroy(obj);
|
||||
return null;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get a random card of a specific rarity.
|
||||
/// Gets a CardDefinition from CardSystemManager and converts to CardData.
|
||||
/// Does NOT affect player's collection or open boosters.
|
||||
/// Spawn a random prefab from an array.
|
||||
/// Prefab is assumed to have SortableItem component and be visually pre-configured.
|
||||
/// </summary>
|
||||
private CardData GetRandomCardDataByRarity(CardRarity targetRarity)
|
||||
private SortableItem SpawnRandomPrefabFromArray(GameObject[] prefabArray, bool isGarbage, CardRarity rarity)
|
||||
{
|
||||
// Get random card definition from manager
|
||||
var definition = CardSystemManager.Instance.GetRandomCardDefinitionByRarity(targetRarity);
|
||||
|
||||
if (definition == null)
|
||||
if (prefabArray == null || prefabArray.Length == 0)
|
||||
{
|
||||
Debug.LogWarning($"[ConveyorBeltController] No card definition found for rarity {targetRarity}");
|
||||
Debug.LogWarning($"[ConveyorBeltController] No prefabs configured for {(isGarbage ? "garbage" : rarity.ToString())}!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create CardData from definition using constructor
|
||||
// This properly links the definition and sets all properties
|
||||
return new CardData(definition);
|
||||
// Pick random prefab
|
||||
GameObject prefab = prefabArray[Random.Range(0, prefabArray.Length)];
|
||||
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning($"[ConveyorBeltController] Null prefab in array for {(isGarbage ? "garbage" : rarity.ToString())}!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply random Y offset to spawn position
|
||||
float randomOffsetY = Random.Range(settings.SpawnOffsetY.x, settings.SpawnOffsetY.y);
|
||||
Vector3 spawnPos = spawnPoint.position + new Vector3(0f, randomOffsetY, 0f);
|
||||
|
||||
// Instantiate prefab
|
||||
GameObject obj = Object.Instantiate(prefab, spawnPos, Quaternion.identity, spawnContainer);
|
||||
SortableItem item = obj.GetComponent<SortableItem>();
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
Debug.LogError($"[ConveyorBeltController] Prefab missing SortableItem component: {prefab.name}");
|
||||
Object.Destroy(obj);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Initialize item based on type (just sets flags and state machine)
|
||||
if (isGarbage)
|
||||
{
|
||||
item.SetupAsGarbage();
|
||||
}
|
||||
else
|
||||
{
|
||||
item.SetupAsCard(rarity);
|
||||
}
|
||||
|
||||
// Subscribe to item events
|
||||
item.OnItemDroppedInBox += HandleItemDroppedInBox;
|
||||
item.OnItemDroppedOnFloor += HandleItemDroppedOnFloor;
|
||||
item.OnItemReturnedToConveyor += HandleItemReturnedToConveyor;
|
||||
item.OnDragStarted += HandleItemDragStarted;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private void UpdateBeltSpeed(float gameProgress)
|
||||
@@ -391,7 +357,7 @@ namespace Minigames.CardSorting.Controllers
|
||||
if (!activeItems.Contains(item))
|
||||
{
|
||||
activeItems.Add(item);
|
||||
Debug.Log($"[ConveyorBeltController] Item returned to conveyor: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
Debug.Log($"[ConveyorBeltController] Item returned to conveyor: {(item.IsGarbage ? "Garbage" : $"{item.Rarity} Card")}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,42 +386,7 @@ namespace Minigames.CardSorting.Controllers
|
||||
// Emit event for scoring
|
||||
OnItemDroppedOnFloor?.Invoke(item);
|
||||
|
||||
Debug.Log($"[ConveyorBeltController] Item dropped on floor: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
}
|
||||
|
||||
private CardRarity DetermineRarity(float roll)
|
||||
{
|
||||
// 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)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply configured card size to spawned item.
|
||||
/// </summary>
|
||||
private void ApplyCardSize(SortableItem item)
|
||||
{
|
||||
if (item == null || item.Context == null || item.Context.RootTransform == null)
|
||||
return;
|
||||
|
||||
// Get the RectTransform to resize (root object)
|
||||
var rectTransform = item.Context.RootTransform.GetComponent<RectTransform>();
|
||||
if (rectTransform != null)
|
||||
{
|
||||
rectTransform.sizeDelta = settings.CardSize;
|
||||
}
|
||||
Debug.Log($"[ConveyorBeltController] Item dropped on floor: {(item.IsGarbage ? "Garbage" : $"{item.Rarity} Card")}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -477,7 +408,7 @@ namespace Minigames.CardSorting.Controllers
|
||||
|
||||
if (wasTracked)
|
||||
{
|
||||
Debug.Log($"[ConveyorBeltController] Item removed from tracking (picked up): {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
Debug.Log($"[ConveyorBeltController] Item removed from tracking (picked up): {(item.IsGarbage ? "Garbage" : $"{item.Rarity} Card")}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Minigames.CardSorting.Core
|
||||
{
|
||||
if (item == null) return;
|
||||
|
||||
Debug.Log($"[ConveyorBeltSlot] Item dropped back on conveyor: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
Debug.Log($"[ConveyorBeltSlot] Item dropped back on conveyor: {(item.IsGarbage ? "Garbage" : $"{item.Rarity} Card")}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minigames.CardSorting.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple sprite renderer for garbage items.
|
||||
/// Parallel to CardDisplay for cards.
|
||||
/// </summary>
|
||||
public class GarbageVisual : MonoBehaviour
|
||||
{
|
||||
[Header("Visual Components")]
|
||||
[SerializeField] private Image spriteRenderer;
|
||||
|
||||
/// <summary>
|
||||
/// Update the displayed sprite.
|
||||
/// </summary>
|
||||
public void UpdateDisplay(Sprite sprite)
|
||||
{
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
spriteRenderer.sprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Auto-find Image component if not assigned
|
||||
if (spriteRenderer == null)
|
||||
{
|
||||
spriteRenderer = GetComponent<Image>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b707770fc3a6448ea0dcd1b2fbf41e00
|
||||
timeCreated: 1763461824
|
||||
@@ -1,4 +1,4 @@
|
||||
using AppleHills.Data.CardSystem;
|
||||
using AppleHills.Data.CardSystem;
|
||||
using Core;
|
||||
using Core.SaveLoad;
|
||||
using Minigames.CardSorting.Data;
|
||||
@@ -21,10 +21,12 @@ namespace Minigames.CardSorting.Core
|
||||
[Header("Configuration")]
|
||||
[SerializeField] private string initialState = "OnConveyorState";
|
||||
|
||||
// Data tracking
|
||||
private bool isGarbage;
|
||||
private CardData cardData;
|
||||
private GarbageItemDefinition garbageItem;
|
||||
// Data tracking - set during spawn, no complex data initialization
|
||||
private bool _isGarbage;
|
||||
private CardRarity _rarity; // Only relevant for cards
|
||||
|
||||
// Track last hovered box for hover indicator feedback
|
||||
private SortingBox _lastHoveredBox;
|
||||
|
||||
// Events - item emits notifications, conveyor subscribes
|
||||
public event System.Action<SortableItem, SortingBox, bool> OnItemDroppedInBox;
|
||||
@@ -34,9 +36,8 @@ namespace Minigames.CardSorting.Core
|
||||
// Public accessors
|
||||
public SortableItemContext Context => context;
|
||||
public AppleMachine StateMachine => stateMachine;
|
||||
public bool IsGarbage => isGarbage;
|
||||
public CardData CardData => cardData;
|
||||
public GarbageItemDefinition GarbageItem => garbageItem;
|
||||
public bool IsGarbage => _isGarbage;
|
||||
public CardRarity Rarity => _rarity;
|
||||
|
||||
/// <summary>
|
||||
/// Get the correct box type for this item.
|
||||
@@ -45,10 +46,10 @@ namespace Minigames.CardSorting.Core
|
||||
{
|
||||
get
|
||||
{
|
||||
if (isGarbage)
|
||||
if (_isGarbage)
|
||||
return BoxType.Trash;
|
||||
|
||||
return cardData.Rarity switch
|
||||
return _rarity switch
|
||||
{
|
||||
CardRarity.Normal => BoxType.Normal,
|
||||
CardRarity.Rare => BoxType.Rare,
|
||||
@@ -71,43 +72,23 @@ namespace Minigames.CardSorting.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup item as a card.
|
||||
/// Setup item as a card. Prefab is already visually configured.
|
||||
/// State machine auto-starts via Initialization component (calls StartMachine in Start).
|
||||
/// </summary>
|
||||
public void SetupAsCard(CardData data)
|
||||
public void SetupAsCard(CardRarity rarity)
|
||||
{
|
||||
isGarbage = false;
|
||||
cardData = data;
|
||||
garbageItem = null;
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
context.SetupAsCard(data);
|
||||
}
|
||||
|
||||
if (stateMachine != null && !string.IsNullOrEmpty(initialState))
|
||||
{
|
||||
stateMachine.ChangeState(initialState);
|
||||
}
|
||||
_isGarbage = false;
|
||||
_rarity = rarity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup item as garbage.
|
||||
/// Setup item as garbage. Prefab is already visually configured.
|
||||
/// State machine auto-starts via Initialization component (calls StartMachine in Start).
|
||||
/// </summary>
|
||||
public void SetupAsGarbage(GarbageItemDefinition garbage)
|
||||
public void SetupAsGarbage()
|
||||
{
|
||||
isGarbage = true;
|
||||
cardData = default;
|
||||
garbageItem = garbage;
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
context.SetupAsGarbage(garbage.Sprite);
|
||||
}
|
||||
|
||||
if (stateMachine != null && !string.IsNullOrEmpty(initialState))
|
||||
{
|
||||
stateMachine.ChangeState(initialState);
|
||||
}
|
||||
_isGarbage = true;
|
||||
_rarity = CardRarity.Normal; // Default, not used for garbage
|
||||
}
|
||||
|
||||
protected override void OnDragStartedHook()
|
||||
@@ -125,7 +106,7 @@ namespace Minigames.CardSorting.Core
|
||||
}
|
||||
|
||||
// Default behavior if state doesn't handle
|
||||
Logging.Debug($"[SortableItem] Drag started on {(isGarbage ? garbageItem.DisplayName : cardData.Name)}");
|
||||
Logging.Debug($"[SortableItem] Drag started on {(_isGarbage ? "Garbage" : $"{_rarity} Card")}");
|
||||
}
|
||||
|
||||
// TODO: Fixed when base slot/draggable reworked
|
||||
@@ -143,6 +124,13 @@ namespace Minigames.CardSorting.Core
|
||||
{
|
||||
base.OnDragEndedHook();
|
||||
|
||||
// Hide hover indicator on any previously hovered box
|
||||
if (_lastHoveredBox != null)
|
||||
{
|
||||
_lastHoveredBox.HideHoverIndicator();
|
||||
_lastHoveredBox = null;
|
||||
}
|
||||
|
||||
// Check what type of slot we're over
|
||||
if (CurrentSlot is SortingBox box)
|
||||
{
|
||||
@@ -203,6 +191,7 @@ namespace Minigames.CardSorting.Core
|
||||
UnityEngine.EventSystems.EventSystem.current.RaycastAll(eventData, raycastResults);
|
||||
|
||||
DraggableSlot hoveredSlot = null;
|
||||
SortingBox hoveredBox = null;
|
||||
|
||||
// Find first slot (SortingBox or ConveyorBeltSlot) in raycast results
|
||||
foreach (var result in raycastResults)
|
||||
@@ -212,6 +201,7 @@ namespace Minigames.CardSorting.Core
|
||||
if (box != null)
|
||||
{
|
||||
hoveredSlot = box;
|
||||
hoveredBox = box;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -224,6 +214,26 @@ namespace Minigames.CardSorting.Core
|
||||
}
|
||||
}
|
||||
|
||||
// Update hover indicator on boxes
|
||||
if (hoveredBox != _lastHoveredBox)
|
||||
{
|
||||
// Hide indicator on previously hovered box
|
||||
if (_lastHoveredBox != null)
|
||||
{
|
||||
_lastHoveredBox.HideHoverIndicator();
|
||||
}
|
||||
|
||||
// Show indicator on newly hovered box
|
||||
if (hoveredBox != null)
|
||||
{
|
||||
// Check if this is the correct box for visual feedback
|
||||
bool isCorrectBox = hoveredBox.ValidateItem(this);
|
||||
hoveredBox.ShowHoverIndicator(isCorrectBox);
|
||||
}
|
||||
|
||||
_lastHoveredBox = hoveredBox;
|
||||
}
|
||||
|
||||
// Update current slot (used in OnDragEndedHook)
|
||||
if (hoveredSlot != null && hoveredSlot != CurrentSlot)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using AppleHills.Data.CardSystem;
|
||||
using Core.SaveLoad;
|
||||
using UI.CardSystem;
|
||||
using Core.SaveLoad;
|
||||
using Minigames.CardSorting.Data;
|
||||
using UI.CardSystem.StateMachine;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -9,13 +8,13 @@ namespace Minigames.CardSorting.Core
|
||||
/// <summary>
|
||||
/// Shared context for sortable item states.
|
||||
/// Provides access to common components and data that states need.
|
||||
/// Routes data to appropriate visual component (CardDisplay for cards, GarbageVisual for garbage).
|
||||
/// Prefabs handle their own visual setup - no runtime initialization needed.
|
||||
/// </summary>
|
||||
public class SortableItemContext : MonoBehaviour
|
||||
{
|
||||
[Header("Visual Components (one or the other)")]
|
||||
[SerializeField] private CardDisplay cardDisplay; // For cards
|
||||
[SerializeField] private GarbageVisual garbageVisual; // For garbage
|
||||
[Header("Effect Components")]
|
||||
[Tooltip("Top-level overlay image for visual effects (blink red, etc). Should be hidden by default.")]
|
||||
[SerializeField] private UnityEngine.UI.Image blinkOverlayImage;
|
||||
|
||||
[Header("Shared Components")]
|
||||
[SerializeField] private CardAnimator animator;
|
||||
@@ -24,8 +23,7 @@ namespace Minigames.CardSorting.Core
|
||||
private AppleMachine stateMachine;
|
||||
|
||||
// Public accessors
|
||||
public CardDisplay CardDisplay => cardDisplay;
|
||||
public GarbageVisual GarbageVisual => garbageVisual;
|
||||
public UnityEngine.UI.Image BlinkOverlayImage => blinkOverlayImage;
|
||||
public CardAnimator Animator => animator;
|
||||
public Transform VisualTransform => visualTransform;
|
||||
public AppleMachine StateMachine => stateMachine;
|
||||
@@ -42,6 +40,20 @@ namespace Minigames.CardSorting.Core
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Capture original transform for drag animations
|
||||
// This preserves the prefab's configured scale (e.g., 0.05 for world-space Canvas)
|
||||
var currentScale = transform.localScale;
|
||||
if (currentScale.x < 0.01f && currentScale.y < 0.01f && currentScale.z < 0.01f)
|
||||
{
|
||||
OriginalScale = Vector3.one; // Fallback if scale is ~0
|
||||
}
|
||||
else
|
||||
{
|
||||
OriginalScale = currentScale;
|
||||
}
|
||||
OriginalPosition = transform.localPosition;
|
||||
OriginalRotation = transform.localRotation;
|
||||
|
||||
// Auto-find components if not assigned
|
||||
if (visualTransform == null)
|
||||
{
|
||||
@@ -64,76 +76,16 @@ namespace Minigames.CardSorting.Core
|
||||
}
|
||||
}
|
||||
|
||||
if (cardDisplay == null && visualTransform != null)
|
||||
{
|
||||
cardDisplay = visualTransform.GetComponentInChildren<CardDisplay>();
|
||||
}
|
||||
|
||||
if (garbageVisual == null && visualTransform != null)
|
||||
{
|
||||
garbageVisual = visualTransform.GetComponentInChildren<GarbageVisual>();
|
||||
}
|
||||
|
||||
stateMachine = GetComponentInChildren<AppleMachine>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup as card item - CardDisplay handles all rendering.
|
||||
/// </summary>
|
||||
public void SetupAsCard(CardData cardData)
|
||||
{
|
||||
// Capture original root transform for drag animations
|
||||
// This preserves the tiny world-space Canvas scale (e.g., 0.05)
|
||||
var currentScale = transform.localScale;
|
||||
if (currentScale.x < 0.01f && currentScale.y < 0.01f && currentScale.z < 0.01f)
|
||||
{
|
||||
OriginalScale = Vector3.one; // Fallback if scale is ~0
|
||||
}
|
||||
else
|
||||
{
|
||||
OriginalScale = currentScale;
|
||||
}
|
||||
OriginalPosition = transform.localPosition;
|
||||
OriginalRotation = transform.localRotation;
|
||||
|
||||
if (cardDisplay != null)
|
||||
// Hide blink overlay initially
|
||||
if (blinkOverlayImage != null)
|
||||
{
|
||||
cardDisplay.SetupCard(cardData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[SortableItemContext] CardDisplay not found on {name}");
|
||||
blinkOverlayImage.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup as garbage item - simple sprite display.
|
||||
/// </summary>
|
||||
public void SetupAsGarbage(Sprite sprite)
|
||||
{
|
||||
// Capture original root transform for drag animations
|
||||
// This preserves the tiny world-space Canvas scale (e.g., 0.05)
|
||||
var currentScale = transform.localScale;
|
||||
if (currentScale.x < 0.01f && currentScale.y < 0.01f && currentScale.z < 0.01f)
|
||||
{
|
||||
OriginalScale = Vector3.one; // Fallback if scale is ~0
|
||||
}
|
||||
else
|
||||
{
|
||||
OriginalScale = currentScale;
|
||||
}
|
||||
OriginalPosition = transform.localPosition;
|
||||
OriginalRotation = transform.localRotation;
|
||||
|
||||
if (garbageVisual != null)
|
||||
{
|
||||
garbageVisual.UpdateDisplay(sprite);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[SortableItemContext] GarbageVisual not found on {name}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ebfb8d9e40f4532b3a3919ced988330
|
||||
timeCreated: 1766069349
|
||||
@@ -13,8 +13,47 @@ namespace Minigames.CardSorting.Core
|
||||
[Header("Box Configuration")]
|
||||
[SerializeField] private BoxType boxType;
|
||||
|
||||
[Header("Visual Feedback")]
|
||||
[Tooltip("Sprite renderer to show when an item is hovering over this box")]
|
||||
[SerializeField] private SpriteRenderer hoverIndicator;
|
||||
|
||||
public BoxType BoxType => boxType;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Hide hover indicator on start
|
||||
if (hoverIndicator != null)
|
||||
{
|
||||
hoverIndicator.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show the hover indicator when an item is hovering over this box.
|
||||
/// </summary>
|
||||
/// <param name="isCorrect">If true, tints the indicator green. Otherwise uses default color.</param>
|
||||
public void ShowHoverIndicator(bool isCorrect = false)
|
||||
{
|
||||
if (hoverIndicator != null)
|
||||
{
|
||||
hoverIndicator.enabled = true;
|
||||
|
||||
// Tint green if correct box, white otherwise
|
||||
hoverIndicator.color = isCorrect ? Color.green : Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide the hover indicator when an item stops hovering.
|
||||
/// </summary>
|
||||
public void HideHoverIndicator()
|
||||
{
|
||||
if (hoverIndicator != null)
|
||||
{
|
||||
hoverIndicator.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if item belongs in this box.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,8 +21,6 @@ namespace Minigames.CardSorting.Core
|
||||
[SerializeField] private Transform conveyorSpawnPoint;
|
||||
[SerializeField] private Transform conveyorEndPoint; // Visual end - items scored as missed here
|
||||
[SerializeField] private Transform conveyorDespawnPoint; // Off-screen - items destroyed here
|
||||
[SerializeField] private GameObject sortableCardPrefab;
|
||||
[SerializeField] private GameObject sortableGarbagePrefab;
|
||||
[SerializeField] private SortingBox[] sortingBoxes;
|
||||
[SerializeField] private Transform spawnedItemsContainer; // Container for all spawned items (optional, will auto-create if null)
|
||||
|
||||
@@ -44,8 +42,6 @@ namespace Minigames.CardSorting.Core
|
||||
conveyorSpawnPoint,
|
||||
conveyorEndPoint,
|
||||
conveyorDespawnPoint,
|
||||
sortableCardPrefab,
|
||||
sortableGarbagePrefab,
|
||||
_settings,
|
||||
GetOrCreateSpawnContainer()
|
||||
);
|
||||
@@ -213,23 +209,24 @@ namespace Minigames.CardSorting.Core
|
||||
}
|
||||
|
||||
// Blink the item red (if it still exists)
|
||||
if (item != null && item.Context != null && item.Context.Animator != null)
|
||||
if (item != null && item.Context != null && item.Context.Animator != null && item.Context.BlinkOverlayImage != null)
|
||||
{
|
||||
UnityEngine.UI.Image imageToBlink = null;
|
||||
// Show overlay before blinking
|
||||
item.Context.BlinkOverlayImage.enabled = true;
|
||||
|
||||
if (item.Context.CardDisplay != null)
|
||||
{
|
||||
imageToBlink = item.Context.CardDisplay.GetComponent<UnityEngine.UI.Image>();
|
||||
}
|
||||
else if (item.Context.GarbageVisual != null)
|
||||
{
|
||||
imageToBlink = item.Context.GarbageVisual.GetComponent<UnityEngine.UI.Image>();
|
||||
}
|
||||
item.Context.Animator.BlinkRed(item.Context.BlinkOverlayImage, 0.15f);
|
||||
|
||||
if (imageToBlink != null)
|
||||
{
|
||||
item.Context.Animator.BlinkRed(imageToBlink, 0.15f);
|
||||
}
|
||||
// Hide overlay after blink duration (assuming blink duration + buffer)
|
||||
StartCoroutine(HideOverlayAfterDelay(item.Context.BlinkOverlayImage, 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator HideOverlayAfterDelay(UnityEngine.UI.Image overlay, float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
if (overlay != null)
|
||||
{
|
||||
overlay.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +307,7 @@ namespace Minigames.CardSorting.Core
|
||||
{
|
||||
if (item != null && item.gameObject != null)
|
||||
{
|
||||
Logging.Debug($"[SortingGameManager] Destroying orphaned item: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
Logging.Debug($"[SortingGameManager] Destroying orphaned item: {(item.IsGarbage ? "Garbage" : $"{item.Rarity} Card")}");
|
||||
Destroy(item.gameObject);
|
||||
}
|
||||
}
|
||||
@@ -364,7 +361,7 @@ namespace Minigames.CardSorting.Core
|
||||
// Forward to public event for UI/other systems
|
||||
OnItemSpawned?.Invoke(item);
|
||||
|
||||
Logging.Debug($"[SortingGameManager] Item spawned: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
Logging.Debug($"[SortingGameManager] Item spawned: {(item.IsGarbage ? "Garbage" : $"{item.Rarity} Card")}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -383,11 +380,11 @@ namespace Minigames.CardSorting.Core
|
||||
Score.RecordMissedItem();
|
||||
PlayWrongStateFeedback(item);
|
||||
LoseLife();
|
||||
Logging.Debug($"[SortingGameManager] Trash fell off belt! {item.GarbageItem?.DisplayName} - PENALTY");
|
||||
Logging.Debug($"[SortingGameManager] Trash fell off belt! Garbage - PENALTY");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Debug($"[SortingGameManager] Card fell off belt: {item.CardData?.Name} - no penalty");
|
||||
Logging.Debug($"[SortingGameManager] Card fell off belt: {item.Rarity} Card - no penalty");
|
||||
}
|
||||
|
||||
// Fire global fell off belt event for effects
|
||||
@@ -412,11 +409,11 @@ namespace Minigames.CardSorting.Core
|
||||
Score.RecordIncorrectSort();
|
||||
PlayWrongStateFeedback(item);
|
||||
LoseLife();
|
||||
Logging.Debug($"[SortingGameManager] Trash dropped on floor! {item.GarbageItem?.DisplayName} - PENALTY");
|
||||
Logging.Debug($"[SortingGameManager] Trash dropped on floor! Garbage - PENALTY");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Debug($"[SortingGameManager] Card dropped on floor: {item.CardData?.Name} - no penalty");
|
||||
Logging.Debug($"[SortingGameManager] Card dropped on floor: {item.Rarity} Card - no penalty");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +423,8 @@ namespace Minigames.CardSorting.Core
|
||||
/// </summary>
|
||||
private void OnConveyorItemDespawned(SortableItem item)
|
||||
{
|
||||
Logging.Debug($"[SortingGameManager] Item despawned: {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
Logging.Debug($"[SortingGameManager] Item despawned: {(item.IsGarbage ? "Garbage" : $"{item.Rarity} Card")}");
|
||||
|
||||
|
||||
// Destroy the item
|
||||
if (item != null)
|
||||
@@ -446,7 +444,7 @@ namespace Minigames.CardSorting.Core
|
||||
if (correct)
|
||||
{
|
||||
Score.RecordCorrectSort();
|
||||
Logging.Debug($"[SortingGameManager] Correct sort! {item.CardData?.Name ?? item.GarbageItem?.DisplayName}");
|
||||
Logging.Debug($"[SortingGameManager] Correct sort! {(item.IsGarbage ? "Garbage" : $"{item.Rarity} Card")}");
|
||||
|
||||
// Fire global correct sort event for effects
|
||||
OnItemSortedCorrectly?.Invoke(item);
|
||||
@@ -460,14 +458,14 @@ namespace Minigames.CardSorting.Core
|
||||
Score.RecordIncorrectSort();
|
||||
PlayWrongStateFeedback(item);
|
||||
LoseLife();
|
||||
Logging.Debug($"[SortingGameManager] Incorrect trash sort! {item.GarbageItem?.DisplayName} - PENALTY");
|
||||
Logging.Debug($"[SortingGameManager] Incorrect trash sort! Garbage - PENALTY");
|
||||
|
||||
// Fire global incorrect sort event for effects
|
||||
OnItemSortedIncorrectly?.Invoke(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Debug($"[SortingGameManager] Card sorted incorrectly: {item.CardData?.Name} - no penalty");
|
||||
Logging.Debug($"[SortingGameManager] Card sorted incorrectly: {item.Rarity} Card - no penalty");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d88deb3df9e54bdb83b9a7ed1c7e3e27
|
||||
timeCreated: 1766069228
|
||||
@@ -1,38 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.CardSorting.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Definition for garbage items (banana peels, cans, receipts, etc.).
|
||||
/// Cards use existing CardDefinition from CardSystemManager.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "GarbageItem", menuName = "Minigames/CardSorting/GarbageItem", order = 0)]
|
||||
public class GarbageItemDefinition : ScriptableObject
|
||||
{
|
||||
[Tooltip("Unique identifier for this garbage item")]
|
||||
[SerializeField] private string itemId;
|
||||
|
||||
[Tooltip("Display name for debugging")]
|
||||
[SerializeField] private string displayName;
|
||||
|
||||
[Tooltip("Sprite to display for this garbage item")]
|
||||
[SerializeField] private Sprite sprite;
|
||||
|
||||
// Public accessors
|
||||
public string ItemId => itemId;
|
||||
public string DisplayName => displayName;
|
||||
public Sprite Sprite => sprite;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
// Auto-generate itemId from asset name if empty
|
||||
if (string.IsNullOrEmpty(itemId))
|
||||
{
|
||||
itemId = name;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e69a2167710437798b1980126d5a4f6
|
||||
timeCreated: 1763461765
|
||||
@@ -57,28 +57,12 @@ namespace Minigames.CardSorting.StateMachine.States
|
||||
|
||||
private void StartBlinkingRed()
|
||||
{
|
||||
if (_context.Animator == null) return;
|
||||
if (_context.Animator == null || _context.BlinkOverlayImage == null) return;
|
||||
|
||||
// Get the image to tint (CardDisplay or GarbageVisual)
|
||||
UnityEngine.UI.Image imageToBlink = null;
|
||||
// Show overlay before blinking
|
||||
_context.BlinkOverlayImage.enabled = true;
|
||||
|
||||
if (_context.CardDisplay != null)
|
||||
{
|
||||
imageToBlink =
|
||||
_context.CardDisplay.GetComponent<UnityEngine.UI.Image>()
|
||||
?? _context.CardDisplay.GetComponentInChildren<UnityEngine.UI.Image>();
|
||||
}
|
||||
else if (_context.GarbageVisual != null)
|
||||
{
|
||||
imageToBlink =
|
||||
_context.GarbageVisual.GetComponent<UnityEngine.UI.Image>()
|
||||
?? _context.GarbageVisual.GetComponentInChildren<UnityEngine.UI.Image>();
|
||||
}
|
||||
|
||||
if (imageToBlink != null)
|
||||
{
|
||||
_context.Animator.BlinkRed(imageToBlink);
|
||||
}
|
||||
_context.Animator.BlinkRed(_context.BlinkOverlayImage);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
|
||||
8
Assets/Scripts/UI/Tracking.meta
Normal file
8
Assets/Scripts/UI/Tracking.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ea2c8cb26e7d7a44b21c052322ab6ba
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
237
Assets/Scripts/UI/Tracking/OffScreenPin.cs
Normal file
237
Assets/Scripts/UI/Tracking/OffScreenPin.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using AppleHills.Core;
|
||||
|
||||
namespace UI.Tracking
|
||||
{
|
||||
/// <summary>
|
||||
/// UI pin that displays on screen edges pointing to off-screen targets.
|
||||
/// Consists of a static icon in the center and a rotatable frame that points toward the target.
|
||||
/// Optionally displays distance to target if enabled.
|
||||
/// Updates every frame for smooth tracking.
|
||||
/// </summary>
|
||||
public class OffScreenPin : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[Tooltip("The image component that displays the target's icon (static, in center)")]
|
||||
[SerializeField] private Image iconImage;
|
||||
|
||||
[Tooltip("The image component that rotates to point toward the target (default: pointing downward)")]
|
||||
[SerializeField] private Image frameImage;
|
||||
|
||||
[Tooltip("Optional: Text component to display distance to target")]
|
||||
[SerializeField] private TMPro.TextMeshProUGUI distanceText;
|
||||
|
||||
private RectTransform _rectTransform;
|
||||
private TrackableTarget _target;
|
||||
private bool _isInitialized;
|
||||
private float _screenPadding;
|
||||
private bool _trackDistance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Update position and rotation every frame for smooth tracking
|
||||
if (_isInitialized && _target != null)
|
||||
{
|
||||
Camera mainCamera = QuickAccess.Instance?.MainCamera;
|
||||
if (mainCamera != null)
|
||||
{
|
||||
UpdatePositionAndRotation(mainCamera, _screenPadding);
|
||||
}
|
||||
|
||||
// Update distance display if enabled
|
||||
if (_trackDistance && distanceText != null)
|
||||
{
|
||||
UpdateDistanceDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the pin with a target reference and set the icon
|
||||
/// </summary>
|
||||
public void Initialize(TrackableTarget target, float screenPadding)
|
||||
{
|
||||
_target = target;
|
||||
_isInitialized = true;
|
||||
_screenPadding = screenPadding;
|
||||
_trackDistance = target.TrackDistance;
|
||||
|
||||
// Set the icon sprite if available
|
||||
if (iconImage != null && target.Icon != null)
|
||||
{
|
||||
iconImage.sprite = target.Icon;
|
||||
iconImage.enabled = true;
|
||||
}
|
||||
else if (iconImage != null)
|
||||
{
|
||||
iconImage.enabled = false;
|
||||
}
|
||||
|
||||
// Configure distance text
|
||||
if (distanceText != null)
|
||||
{
|
||||
distanceText.gameObject.SetActive(_trackDistance);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the pin's position and rotation based on the target's world position.
|
||||
/// Called by the manager each update cycle.
|
||||
/// </summary>
|
||||
public void UpdatePositionAndRotation(Camera cam, float screenPadding)
|
||||
{
|
||||
if (!_isInitialized || _target == null)
|
||||
return;
|
||||
|
||||
// Get target position in screen space
|
||||
Vector3 targetScreenPos = cam.WorldToScreenPoint(_target.WorldPosition);
|
||||
|
||||
// Calculate direction from screen center to target
|
||||
Vector2 screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f);
|
||||
Vector2 targetScreenPos2D = new Vector2(targetScreenPos.x, targetScreenPos.y);
|
||||
Vector2 directionToTarget = (targetScreenPos2D - screenCenter).normalized;
|
||||
|
||||
// Calculate screen bounds with padding (inset from edges)
|
||||
float minX = screenPadding;
|
||||
float maxX = Screen.width - screenPadding;
|
||||
float minY = screenPadding;
|
||||
float maxY = Screen.height - screenPadding;
|
||||
|
||||
// Find intersection point with screen bounds
|
||||
Vector2 intersectionPoint = CalculateScreenEdgeIntersection(
|
||||
screenCenter,
|
||||
directionToTarget,
|
||||
minX, maxX, minY, maxY
|
||||
);
|
||||
|
||||
// Offset the intersection point slightly toward the center to ensure pin is fully visible
|
||||
Vector2 offsetTowardCenter = -directionToTarget * screenPadding * 0.5f;
|
||||
Vector2 finalPosition = intersectionPoint + offsetTowardCenter;
|
||||
|
||||
// Update pin position
|
||||
_rectTransform.position = finalPosition;
|
||||
|
||||
// Update frame rotation to point toward target
|
||||
// Frame's default orientation points downward (0 degrees in UI space)
|
||||
// In UI space: 0° = down, 90° = right, 180° = up, 270° = left
|
||||
// Atan2(y, x) gives angle from right (+X axis), so we need to adjust
|
||||
float angle = Mathf.Atan2(directionToTarget.y, directionToTarget.x) * Mathf.Rad2Deg;
|
||||
// Add 90 to convert from "right is 0°" to align with down-pointing sprite
|
||||
angle = angle + 90f;
|
||||
|
||||
if (frameImage != null)
|
||||
{
|
||||
frameImage.rectTransform.localRotation = Quaternion.Euler(0, 0, angle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the distance display text
|
||||
/// </summary>
|
||||
private void UpdateDistanceDisplay()
|
||||
{
|
||||
// Get distance source (typically the player)
|
||||
if (TrackingDistanceSource.Instance == null)
|
||||
{
|
||||
distanceText.text = "???";
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate distance between source and target
|
||||
float distance = Vector3.Distance(TrackingDistanceSource.Instance.WorldPosition, _target.WorldPosition);
|
||||
|
||||
// Format distance nicely (meters with 1 decimal place)
|
||||
distanceText.text = $"{distance:F1}m";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the intersection point of a ray from center in given direction with screen bounds
|
||||
/// </summary>
|
||||
private Vector2 CalculateScreenEdgeIntersection(
|
||||
Vector2 center,
|
||||
Vector2 direction,
|
||||
float minX, float maxX, float minY, float maxY)
|
||||
{
|
||||
// Calculate intersection with each edge and find the closest one
|
||||
float tMin = float.MaxValue;
|
||||
Vector2 intersection = center;
|
||||
|
||||
// Check intersection with right edge (x = maxX)
|
||||
if (direction.x > 0.001f)
|
||||
{
|
||||
float t = (maxX - center.x) / direction.x;
|
||||
float y = center.y + t * direction.y;
|
||||
if (y >= minY && y <= maxY && t < tMin)
|
||||
{
|
||||
tMin = t;
|
||||
intersection = new Vector2(maxX, y);
|
||||
}
|
||||
}
|
||||
|
||||
// Check intersection with left edge (x = minX)
|
||||
if (direction.x < -0.001f)
|
||||
{
|
||||
float t = (minX - center.x) / direction.x;
|
||||
float y = center.y + t * direction.y;
|
||||
if (y >= minY && y <= maxY && t < tMin)
|
||||
{
|
||||
tMin = t;
|
||||
intersection = new Vector2(minX, y);
|
||||
}
|
||||
}
|
||||
|
||||
// Check intersection with top edge (y = maxY)
|
||||
if (direction.y > 0.001f)
|
||||
{
|
||||
float t = (maxY - center.y) / direction.y;
|
||||
float x = center.x + t * direction.x;
|
||||
if (x >= minX && x <= maxX && t < tMin)
|
||||
{
|
||||
tMin = t;
|
||||
intersection = new Vector2(x, maxY);
|
||||
}
|
||||
}
|
||||
|
||||
// Check intersection with bottom edge (y = minY)
|
||||
if (direction.y < -0.001f)
|
||||
{
|
||||
float t = (minY - center.y) / direction.y;
|
||||
float x = center.x + t * direction.x;
|
||||
if (x >= minX && x <= maxX && t < tMin)
|
||||
{
|
||||
tMin = t;
|
||||
intersection = new Vector2(x, minY);
|
||||
}
|
||||
}
|
||||
|
||||
return intersection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the pin for pooling reuse
|
||||
/// </summary>
|
||||
public void ResetPin()
|
||||
{
|
||||
_target = null;
|
||||
_isInitialized = false;
|
||||
|
||||
if (iconImage != null)
|
||||
{
|
||||
iconImage.sprite = null;
|
||||
iconImage.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current target (for null checking)
|
||||
/// </summary>
|
||||
public TrackableTarget Target => _target;
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/UI/Tracking/OffScreenPin.cs.meta
Normal file
3
Assets/Scripts/UI/Tracking/OffScreenPin.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 548d13ffdad349b6939e2b873a39b54e
|
||||
timeCreated: 1766074498
|
||||
494
Assets/Scripts/UI/Tracking/OffScreenTrackerManager.cs
Normal file
494
Assets/Scripts/UI/Tracking/OffScreenTrackerManager.cs
Normal file
@@ -0,0 +1,494 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using AppleHills.Core;
|
||||
|
||||
namespace UI.Tracking
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton manager that tracks off-screen targets and displays directional pins.
|
||||
/// Uses ManagedBehaviour pattern and lazily accesses camera via QuickAccess (like AudioManager).
|
||||
/// </summary>
|
||||
public class OffScreenTrackerManager : ManagedBehaviour
|
||||
{
|
||||
[Header("Configuration")]
|
||||
[Tooltip("Prefab for the off-screen tracking pin")]
|
||||
[SerializeField] private OffScreenPin pinPrefab;
|
||||
|
||||
[Tooltip("Pixel padding from screen edges (pins appear this many pixels from edge)")]
|
||||
[SerializeField] private float screenPadding = 50f;
|
||||
|
||||
[Tooltip("Buffer zone in pixels to prevent spawn/despawn flickering (spawn is stricter than despawn)")]
|
||||
[SerializeField] private float bufferZone = 100f;
|
||||
|
||||
[Tooltip("Time in seconds a target must be off-screen before pin appears")]
|
||||
[SerializeField] private float spawnDebounceDelay = 0.3f;
|
||||
|
||||
[Tooltip("Time in seconds a target must be on-screen before pin disappears")]
|
||||
[SerializeField] private float despawnDebounceDelay = 0.2f;
|
||||
|
||||
[Tooltip("Update interval in seconds for checking target visibility")]
|
||||
[SerializeField] private float updateInterval = 0.1f;
|
||||
|
||||
// Singleton instance
|
||||
private static OffScreenTrackerManager _instance;
|
||||
public static OffScreenTrackerManager Instance => _instance;
|
||||
|
||||
// Tracking data
|
||||
private Dictionary<TrackableTarget, TargetTrackingData> _trackedTargets = new Dictionary<TrackableTarget, TargetTrackingData>();
|
||||
|
||||
// Pin pooling
|
||||
private List<OffScreenPin> _inactivePins = new List<OffScreenPin>();
|
||||
|
||||
// Coroutine tracking
|
||||
private Coroutine _updateCoroutine;
|
||||
|
||||
// Auto-created canvas for pins
|
||||
private Canvas _pinCanvas;
|
||||
private RectTransform _pinContainer;
|
||||
|
||||
/// <summary>
|
||||
/// Nested class to track per-target state and timers
|
||||
/// </summary>
|
||||
private class TargetTrackingData
|
||||
{
|
||||
public TrackableTarget Target;
|
||||
public OffScreenPin ActivePin;
|
||||
public float OffScreenTimer;
|
||||
public float OnScreenTimer;
|
||||
public bool IsCurrentlyOffScreen;
|
||||
|
||||
public TargetTrackingData(TrackableTarget target)
|
||||
{
|
||||
Target = target;
|
||||
ActivePin = null;
|
||||
OffScreenTimer = 0f;
|
||||
OnScreenTimer = 0f;
|
||||
IsCurrentlyOffScreen = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
// Set singleton instance
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
// Validate configuration
|
||||
if (pinPrefab == null)
|
||||
{
|
||||
Logging.Error("[OffScreenTrackerManager] Pin prefab not assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create dedicated canvas for pins
|
||||
CreatePinCanvas();
|
||||
|
||||
// Subscribe to scene load events from SceneManagerService (like InputManager does)
|
||||
// This must happen in ManagedStart because SceneManagerService instance needs to be set first
|
||||
if (SceneManagerService.Instance != null)
|
||||
{
|
||||
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
|
||||
Logging.Debug("[OffScreenTrackerManager] Subscribed to SceneLoadCompleted events");
|
||||
}
|
||||
|
||||
// Initialize for current scene and start coroutine
|
||||
InitializeForCurrentScene();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when any scene finishes loading. Refreshes camera and restarts coroutine.
|
||||
/// </summary>
|
||||
private void OnSceneLoadCompleted(string sceneName)
|
||||
{
|
||||
Logging.Debug($"[OffScreenTrackerManager] Scene loaded: {sceneName}, reinitializing camera and coroutine");
|
||||
InitializeForCurrentScene();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize camera reference and start/restart tracking coroutine for current scene
|
||||
/// </summary>
|
||||
private void InitializeForCurrentScene()
|
||||
{
|
||||
// Stop existing coroutine if running
|
||||
if (_updateCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_updateCoroutine);
|
||||
_updateCoroutine = null;
|
||||
Logging.Debug("[OffScreenTrackerManager] Stopped previous coroutine");
|
||||
}
|
||||
|
||||
// Start the tracking coroutine (camera accessed lazily via QuickAccess)
|
||||
_updateCoroutine = StartCoroutine(UpdateTrackingCoroutine());
|
||||
Logging.Debug("[OffScreenTrackerManager] Started tracking coroutine");
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
// Unsubscribe from SceneManagerService events (like InputManager does)
|
||||
if (SceneManagerService.Instance != null)
|
||||
{
|
||||
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Stop coroutine
|
||||
if (_updateCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_updateCoroutine);
|
||||
}
|
||||
|
||||
// Clean up pooled pins
|
||||
foreach (var pin in _inactivePins)
|
||||
{
|
||||
if (pin != null)
|
||||
{
|
||||
Destroy(pin.gameObject);
|
||||
}
|
||||
}
|
||||
_inactivePins.Clear();
|
||||
|
||||
// Clean up active pins
|
||||
foreach (var data in _trackedTargets.Values)
|
||||
{
|
||||
if (data.ActivePin != null)
|
||||
{
|
||||
Destroy(data.ActivePin.gameObject);
|
||||
}
|
||||
}
|
||||
_trackedTargets.Clear();
|
||||
|
||||
// Clean up canvas
|
||||
if (_pinCanvas != null)
|
||||
{
|
||||
Destroy(_pinCanvas.gameObject);
|
||||
}
|
||||
|
||||
// Clear singleton
|
||||
if (_instance == this)
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a dedicated canvas for pins with sort order 50
|
||||
/// </summary>
|
||||
private void CreatePinCanvas()
|
||||
{
|
||||
// Create a new GameObject for the canvas
|
||||
GameObject canvasObj = new GameObject("OffScreenPinCanvas");
|
||||
canvasObj.transform.SetParent(transform, false);
|
||||
|
||||
// Add and configure Canvas
|
||||
_pinCanvas = canvasObj.AddComponent<Canvas>();
|
||||
_pinCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
_pinCanvas.sortingOrder = 50;
|
||||
|
||||
// Add CanvasScaler for consistent sizing
|
||||
var scaler = canvasObj.AddComponent<UnityEngine.UI.CanvasScaler>();
|
||||
scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.screenMatchMode = UnityEngine.UI.CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
// Add GraphicRaycaster (required for UI)
|
||||
canvasObj.AddComponent<UnityEngine.UI.GraphicRaycaster>();
|
||||
|
||||
// Get RectTransform for pin container
|
||||
_pinContainer = canvasObj.GetComponent<RectTransform>();
|
||||
|
||||
Logging.Debug("[OffScreenTrackerManager] Created dedicated pin canvas with sort order 50");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a target for tracking
|
||||
/// </summary>
|
||||
public void RegisterTarget(TrackableTarget target)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
if (_trackedTargets.ContainsKey(target))
|
||||
{
|
||||
Logging.Warning($"[OffScreenTrackerManager] Target {target.name} is already registered");
|
||||
return;
|
||||
}
|
||||
|
||||
_trackedTargets.Add(target, new TargetTrackingData(target));
|
||||
Logging.Debug($"[OffScreenTrackerManager] Registered target: {target.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister a target from tracking
|
||||
/// </summary>
|
||||
public void UnregisterTarget(TrackableTarget target)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
if (_trackedTargets.TryGetValue(target, out TargetTrackingData data))
|
||||
{
|
||||
// Despawn pin if active
|
||||
if (data.ActivePin != null)
|
||||
{
|
||||
DespawnPin(data);
|
||||
}
|
||||
|
||||
_trackedTargets.Remove(target);
|
||||
Logging.Debug($"[OffScreenTrackerManager] Unregistered target: {target.name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main update coroutine that runs every updateInterval seconds
|
||||
/// </summary>
|
||||
private IEnumerator UpdateTrackingCoroutine()
|
||||
{
|
||||
Logging.Debug("[OffScreenTrackerManager] Tracking coroutine started");
|
||||
WaitForSeconds wait = new WaitForSeconds(updateInterval);
|
||||
|
||||
while (true)
|
||||
{
|
||||
yield return wait;
|
||||
|
||||
// Get camera lazily via QuickAccess (like AudioManager does)
|
||||
Camera mainCamera = QuickAccess.Instance?.MainCamera;
|
||||
|
||||
if (mainCamera == null)
|
||||
{
|
||||
// Camera not available yet (early in boot or scene transition)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create list of targets to remove (for null cleanup)
|
||||
List<TrackableTarget> targetsToRemove = new List<TrackableTarget>();
|
||||
|
||||
foreach (var kvp in _trackedTargets)
|
||||
{
|
||||
TrackableTarget target = kvp.Key;
|
||||
TargetTrackingData data = kvp.Value;
|
||||
|
||||
// Check if target was destroyed
|
||||
if (target == null)
|
||||
{
|
||||
targetsToRemove.Add(target);
|
||||
if (data.ActivePin != null)
|
||||
{
|
||||
DespawnPin(data);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if target is off-screen
|
||||
bool isOffScreen = IsTargetOffScreen(target, mainCamera);
|
||||
|
||||
// Update timers and state
|
||||
if (isOffScreen)
|
||||
{
|
||||
// Target is off-screen
|
||||
data.OffScreenTimer += updateInterval;
|
||||
data.OnScreenTimer = 0f;
|
||||
|
||||
// Check if we should spawn a pin
|
||||
if (!data.IsCurrentlyOffScreen && data.OffScreenTimer >= spawnDebounceDelay)
|
||||
{
|
||||
data.IsCurrentlyOffScreen = true;
|
||||
SpawnPin(data);
|
||||
}
|
||||
// Pin updates itself every frame, no need to call UpdatePositionAndRotation here
|
||||
}
|
||||
else
|
||||
{
|
||||
// Target is on-screen
|
||||
data.OnScreenTimer += updateInterval;
|
||||
data.OffScreenTimer = 0f;
|
||||
|
||||
// Check if we should despawn the pin
|
||||
if (data.IsCurrentlyOffScreen && data.OnScreenTimer >= despawnDebounceDelay)
|
||||
{
|
||||
data.IsCurrentlyOffScreen = false;
|
||||
if (data.ActivePin != null)
|
||||
{
|
||||
DespawnPin(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up null targets
|
||||
foreach (var target in targetsToRemove)
|
||||
{
|
||||
_trackedTargets.Remove(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a target is off-screen by checking its actual bounds.
|
||||
/// - For SPAWNING: Entire object must be off-screen (all corners outside viewport)
|
||||
/// - For DESPAWNING: Any part of object on-screen triggers despawn (any corner inside viewport)
|
||||
/// Uses bufferZone to prevent flickering at boundaries.
|
||||
/// </summary>
|
||||
private bool IsTargetOffScreen(TrackableTarget target, Camera cam)
|
||||
{
|
||||
// Get the world bounds of the target
|
||||
Bounds worldBounds = target.GetWorldBounds();
|
||||
|
||||
// Get the 8 corners of the bounds (we only need the min/max points in 2D)
|
||||
Vector3 min = worldBounds.min;
|
||||
Vector3 max = worldBounds.max;
|
||||
|
||||
// Convert corners to screen space
|
||||
Vector3 minScreen = cam.WorldToScreenPoint(new Vector3(min.x, min.y, worldBounds.center.z));
|
||||
Vector3 maxScreen = cam.WorldToScreenPoint(new Vector3(max.x, max.y, worldBounds.center.z));
|
||||
|
||||
// Check if behind camera
|
||||
if (minScreen.z < 0 && maxScreen.z < 0)
|
||||
return true;
|
||||
|
||||
// Shrink detection zones to 80% of screen (10% inset on each side)
|
||||
// This makes spawn/despawn more conservative
|
||||
float insetPercent = 0.1f; // 10% on each side = 80% total
|
||||
float horizontalInset = Screen.width * insetPercent;
|
||||
float verticalInset = Screen.height * insetPercent;
|
||||
|
||||
float screenLeft = horizontalInset;
|
||||
float screenRight = Screen.width - horizontalInset;
|
||||
float screenBottom = verticalInset;
|
||||
float screenTop = Screen.height - verticalInset;
|
||||
|
||||
// Check if ENTIRELY off-screen (all corners outside viewport)
|
||||
// This is when we should spawn the pin
|
||||
bool entirelyOffScreenLeft = maxScreen.x < screenLeft;
|
||||
bool entirelyOffScreenRight = minScreen.x > screenRight;
|
||||
bool entirelyOffScreenBottom = maxScreen.y < screenBottom;
|
||||
bool entirelyOffScreenTop = minScreen.y > screenTop;
|
||||
|
||||
bool entirelyOffScreen = entirelyOffScreenLeft || entirelyOffScreenRight ||
|
||||
entirelyOffScreenBottom || entirelyOffScreenTop;
|
||||
|
||||
// Apply buffer zone to prevent flickering
|
||||
// If already off-screen, require target to move bufferZone pixels on-screen before considering it "on-screen"
|
||||
// This creates hysteresis to prevent rapid spawn/despawn cycles
|
||||
if (entirelyOffScreen)
|
||||
{
|
||||
return true; // Definitely off-screen
|
||||
}
|
||||
|
||||
// Check if ANY part is on-screen (for despawn logic)
|
||||
// We add bufferZone to make despawn slightly more eager
|
||||
bool anyPartOnScreen = !(minScreen.x > screenRight - bufferZone ||
|
||||
maxScreen.x < screenLeft + bufferZone ||
|
||||
minScreen.y > screenTop - bufferZone ||
|
||||
maxScreen.y < screenBottom + bufferZone);
|
||||
|
||||
// If any part is on-screen (with buffer), consider it "on-screen" (pin should despawn)
|
||||
return !anyPartOnScreen;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn a pin for the given target data
|
||||
/// </summary>
|
||||
private void SpawnPin(TargetTrackingData data)
|
||||
{
|
||||
// Try to get pin from pool
|
||||
OffScreenPin pin = GetPinFromPool();
|
||||
|
||||
// Initialize the pin (this also caches the target and settings)
|
||||
pin.Initialize(data.Target, screenPadding);
|
||||
|
||||
// CRITICAL: Update position BEFORE activating to prevent flicker
|
||||
// Get camera for immediate position update
|
||||
Camera mainCamera = QuickAccess.Instance?.MainCamera;
|
||||
if (mainCamera != null)
|
||||
{
|
||||
pin.UpdatePositionAndRotation(mainCamera, screenPadding);
|
||||
}
|
||||
|
||||
// Now activate the pin at the correct position
|
||||
pin.gameObject.SetActive(true);
|
||||
|
||||
|
||||
data.ActivePin = pin;
|
||||
|
||||
Logging.Debug($"[OffScreenTrackerManager] Spawned pin for target: {data.Target.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Despawn a pin and return it to the pool
|
||||
/// </summary>
|
||||
private void DespawnPin(TargetTrackingData data)
|
||||
{
|
||||
if (data.ActivePin == null)
|
||||
return;
|
||||
|
||||
OffScreenPin pin = data.ActivePin;
|
||||
data.ActivePin = null;
|
||||
|
||||
// Reset and return to pool
|
||||
pin.ResetPin();
|
||||
pin.gameObject.SetActive(false);
|
||||
_inactivePins.Add(pin);
|
||||
|
||||
Logging.Debug($"[OffScreenTrackerManager] Despawned pin for target: {data.Target?.name ?? "null"}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a pin from the pool or instantiate a new one
|
||||
/// </summary>
|
||||
private OffScreenPin GetPinFromPool()
|
||||
{
|
||||
// Try to reuse an inactive pin
|
||||
if (_inactivePins.Count > 0)
|
||||
{
|
||||
OffScreenPin pin = _inactivePins[_inactivePins.Count - 1];
|
||||
_inactivePins.RemoveAt(_inactivePins.Count - 1);
|
||||
return pin;
|
||||
}
|
||||
|
||||
// Create a new pin
|
||||
OffScreenPin newPin = Instantiate(pinPrefab, _pinContainer);
|
||||
newPin.gameObject.SetActive(false);
|
||||
return newPin;
|
||||
}
|
||||
|
||||
#region Public Configuration Accessors
|
||||
|
||||
/// <summary>
|
||||
/// Get or set the screen padding in pixels
|
||||
/// </summary>
|
||||
public float ScreenPadding
|
||||
{
|
||||
get => screenPadding;
|
||||
set => screenPadding = Mathf.Max(0f, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or set the spawn debounce delay
|
||||
/// </summary>
|
||||
public float SpawnDebounceDelay
|
||||
{
|
||||
get => spawnDebounceDelay;
|
||||
set => spawnDebounceDelay = Mathf.Max(0f, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or set the despawn debounce delay
|
||||
/// </summary>
|
||||
public float DespawnDebounceDelay
|
||||
{
|
||||
get => despawnDebounceDelay;
|
||||
set => despawnDebounceDelay = Mathf.Max(0f, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8599140fd23e492fa7f14cb7633209fe
|
||||
timeCreated: 1766074538
|
||||
84
Assets/Scripts/UI/Tracking/TrackableTarget.cs
Normal file
84
Assets/Scripts/UI/Tracking/TrackableTarget.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI.Tracking
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that marks a GameObject as trackable by the OffScreenTrackerManager.
|
||||
/// Automatically registers/unregisters with the manager when enabled/disabled.
|
||||
/// </summary>
|
||||
public class TrackableTarget : MonoBehaviour
|
||||
{
|
||||
[Header("Configuration")]
|
||||
[Tooltip("Icon to display in the off-screen tracking pin")]
|
||||
[SerializeField] private Sprite icon;
|
||||
|
||||
[Tooltip("Should this target display distance to the TrackingDistanceSource (e.g., player)?")]
|
||||
[SerializeField] private bool trackDistance = false;
|
||||
|
||||
/// <summary>
|
||||
/// The icon to display in the tracking pin
|
||||
/// </summary>
|
||||
public Sprite Icon => icon;
|
||||
|
||||
/// <summary>
|
||||
/// Should this target track and display distance?
|
||||
/// </summary>
|
||||
public bool TrackDistance => trackDistance;
|
||||
|
||||
/// <summary>
|
||||
/// The world position of this target
|
||||
/// </summary>
|
||||
public Vector3 WorldPosition => transform.position;
|
||||
|
||||
/// <summary>
|
||||
/// Get the screen-space bounds of this target's visual representation.
|
||||
/// Checks for Renderer or Collider2D to determine size.
|
||||
/// </summary>
|
||||
public Bounds GetWorldBounds()
|
||||
{
|
||||
// Try to get bounds from Renderer first (most accurate for visuals)
|
||||
Renderer renderer = GetComponentInChildren<Renderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
return renderer.bounds;
|
||||
}
|
||||
|
||||
// Fallback to Collider2D
|
||||
Collider2D collider = GetComponent<Collider2D>();
|
||||
if (collider != null)
|
||||
{
|
||||
return collider.bounds;
|
||||
}
|
||||
|
||||
// Last resort: just return position with minimal bounds
|
||||
return new Bounds(transform.position, Vector3.one * 0.1f);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Register with the manager when enabled
|
||||
if (OffScreenTrackerManager.Instance != null)
|
||||
{
|
||||
OffScreenTrackerManager.Instance.RegisterTarget(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// Unregister from the manager when disabled
|
||||
if (OffScreenTrackerManager.Instance != null)
|
||||
{
|
||||
OffScreenTrackerManager.Instance.UnregisterTarget(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allow runtime icon changes
|
||||
/// </summary>
|
||||
public void SetIcon(Sprite newIcon)
|
||||
{
|
||||
icon = newIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/UI/Tracking/TrackableTarget.cs.meta
Normal file
2
Assets/Scripts/UI/Tracking/TrackableTarget.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e41f200c954677b4b8bde8cafa01d5f1
|
||||
43
Assets/Scripts/UI/Tracking/TrackingDistanceSource.cs
Normal file
43
Assets/Scripts/UI/Tracking/TrackingDistanceSource.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI.Tracking
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks a GameObject as the source point for distance calculations in the tracking system.
|
||||
/// Typically attached to the player or camera. Only one should be active at a time.
|
||||
/// </summary>
|
||||
public class TrackingDistanceSource : MonoBehaviour
|
||||
{
|
||||
private static TrackingDistanceSource _instance;
|
||||
|
||||
/// <summary>
|
||||
/// The currently active distance source (typically the player)
|
||||
/// </summary>
|
||||
public static TrackingDistanceSource Instance => _instance;
|
||||
|
||||
/// <summary>
|
||||
/// The world position of this distance source
|
||||
/// </summary>
|
||||
public Vector3 WorldPosition => transform.position;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Set as the active instance
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Debug.LogWarning($"[TrackingDistanceSource] Multiple distance sources detected. Overwriting previous instance ({_instance.name}) with {name}");
|
||||
}
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// Clear instance if this was the active one
|
||||
if (_instance == this)
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1788298f42bd40f6b077ca3719861752
|
||||
timeCreated: 1766077591
|
||||
Reference in New Issue
Block a user