using UnityEngine; using System.Collections.Generic; using System.Collections; namespace SpecialMoveCards { /// /// Spawns card pickups similar to common pickup systems: /// - Maintains a target count of active cards (default 2) /// - Spawns at random spawn points or within an area /// - When a card is picked up (destroyed), schedules a delayed respawn to keep the target count /// public class CardSpawner : MonoBehaviour { // ========== MASTER DISABLE FLAG ========== // Set to TRUE to completely disable the card/special move system // This is a quick toggle - you can implement a proper system later public static bool CARD_SYSTEM_DISABLED = true; // ========================================== private static readonly List s_instances = new List(); [Header("Card Prefab & Data")] [Tooltip("Generic Card prefab that contains CardBehaviour (used if 'Card Prefabs' list is empty).")] public GameObject cardPrefab; [Tooltip("If provided, a random prefab from this list will be spawned instead of the single prefab above. Each prefab can carry its own CardData and visuals (e.g., per-color prefabs).")] public List cardPrefabs = new List(); [Tooltip("Pool of possible CardData to spawn from.")] public List cardPool = new List(); [Header("Spawning")] public List spawnPoints = new List(); [Tooltip("If no spawn points, use a box area in local space.")] public Vector3 areaSize = new Vector3(10, 0, 10); [Tooltip("Number of cards to keep alive concurrently. Typical big-game behavior uses small numbers like 1-2.")] public int targetCardsInScene = 2; [Tooltip("Random delay before a replacement card is spawned after pickup (x..y seconds).")] public Vector2 respawnDelay = new Vector2(2f, 5f); [Tooltip("Ensure only one active card per spawn point at a time.")] public bool useUniqueSpawnPoints = true; private readonly HashSet alive = new HashSet(); private readonly Dictionary occupiedPoints = new Dictionary(); private void OnEnable() { // Skip if card system is disabled if (CARD_SYSTEM_DISABLED) { Debug.Log("[CardSpawner] Card System is DISABLED - skipping initialization"); return; } if (!s_instances.Contains(this)) s_instances.Add(this); GameEvents.CardPicked += OnAnyCardPicked; MaintainTargetCount(); } private void OnDisable() { GameEvents.CardPicked -= OnAnyCardPicked; alive.RemoveWhere(go => go == null); occupiedPoints.Clear(); s_instances.Remove(this); } private void TrySpawnOnce() { CleanupDead(); if (alive.Count >= targetCardsInScene) return; // Build disallowed type set to keep two cards different HashSet disallowed = BuildDisallowedTypes(); Vector3 pos; Quaternion rot = Quaternion.identity; int usedIndex = -1; if (spawnPoints != null && spawnPoints.Count > 0) { // Choose a free spawn point if required int safety = 25; int idx = -1; while (safety-- > 0) { int r = Random.Range(0, spawnPoints.Count); if (!useUniqueSpawnPoints || !occupiedPoints.ContainsKey(r)) { idx = r; break; } } if (idx < 0) idx = Random.Range(0, spawnPoints.Count); // fallback var t = spawnPoints[idx]; pos = t.position; rot = t.rotation; usedIndex = idx; } else { Vector3 half = areaSize * 0.5f; Vector3 local = new Vector3( Random.Range(-half.x, half.x), Random.Range(-half.y, half.y), Random.Range(-half.z, half.z)); pos = transform.TransformPoint(local); } var go = SpawnCard(pos, rot, disallowed); if (go == null) return; // nothing feasible to spawn alive.Add(go); if (useUniqueSpawnPoints && usedIndex >= 0) occupiedPoints[usedIndex] = go; // Track destruction to free spawn point var token = go.AddComponent(); token.Init(this, usedIndex); } private void OnDrawGizmosSelected() { if (spawnPoints == null || spawnPoints.Count == 0) { Gizmos.color = Color.yellow; Gizmos.matrix = transform.localToWorldMatrix; Gizmos.DrawWireCube(Vector3.zero, areaSize); } } // --- Enemy-death drops API --- /// /// Called by external systems to attempt a drop at a world position with a given probability. /// Respects each spawner's targetCardsInScene limit. /// public static void TrySpawnOnEnemyDeath(Vector3 position, float chance = 0.25f) { if (s_instances.Count == 0) return; if (Random.value > Mathf.Clamp01(chance)) return; // Pick the first spawner that has capacity for (int i = 0; i < s_instances.Count; i++) { var sp = s_instances[i]; if (sp == null || !sp.isActiveAndEnabled) continue; sp.CleanupDead(); if (sp.alive.Count < sp.targetCardsInScene) { sp.SpawnAtPosition(position + Vector3.up * 0.25f, Quaternion.identity); return; } } } private void SpawnAtPosition(Vector3 pos, Quaternion rot) { // For death-drops, also try to keep existing type different if only one is alive var disallowed = BuildDisallowedTypes(); var go = SpawnCard(pos, rot, disallowed); if (go == null) return; alive.Add(go); var token = go.AddComponent(); token.Init(this, -1); // no spawn point occupancy } private void MaintainTargetCount() { CleanupDead(); while (alive.Count < targetCardsInScene) TrySpawnOnce(); } private void CleanupDead() { // Remove destroyed refs and free occupied points if (alive.Count == 0) return; var toRemove = new List(); foreach (var go in alive) { if (go == null) toRemove.Add(go); } for (int i = 0; i < toRemove.Count; i++) alive.Remove(toRemove[i]); // occupiedPoints are freed in NotifyCardDestroyed; this is extra safety if something slipped through var free = new List(); foreach (var kvp in occupiedPoints) { if (kvp.Value == null) free.Add(kvp.Key); } for (int i = 0; i < free.Count; i++) occupiedPoints.Remove(free[i]); } private void OnAnyCardPicked(CardData _) { // Schedule a replacement after a small random delay like big games do float delay = Random.Range(respawnDelay.x, respawnDelay.y); StartCoroutine(RespawnDelayed(delay)); } private IEnumerator RespawnDelayed(float delay) { yield return new WaitForSeconds(delay); MaintainTargetCount(); } internal void NotifyCardDestroyed(SpawnedCardToken token) { if (token == null) return; // Free occupancy & alive set; spawning replacement happens via CardPicked event or periodic Maintain if (token.spawnPointIndex >= 0 && occupiedPoints.ContainsKey(token.spawnPointIndex)) occupiedPoints.Remove(token.spawnPointIndex); alive.Remove(token.gameObject); } public class SpawnedCardToken : MonoBehaviour { private CardSpawner owner; internal int spawnPointIndex = -1; public void Init(CardSpawner o, int index) { owner = o; spawnPointIndex = index; } private void OnDestroy() { if (owner != null) owner.NotifyCardDestroyed(this); } } // --- Helper logic to enforce different types when two are present --- private HashSet BuildDisallowedTypes() { var disallowed = new HashSet(); // Only enforce strict difference when target is 2 and we currently have exactly 1 alive card if (targetCardsInScene == 2 && alive.Count == 1) { foreach (var go in alive) { if (go == null) continue; var cb = go.GetComponent(); if (cb != null && cb.Data != null) { disallowed.Add(cb.Data.type); break; } } } return disallowed; } private GameObject SpawnCard(Vector3 pos, Quaternion rot, HashSet disallowed) { bool multiPrefabs = cardPrefabs != null && cardPrefabs.Count > 0; if (!multiPrefabs && cardPrefab == null) return null; // Collect prefab capabilities var fixedPrefabs = new List<(GameObject prefab, CardType type)>(); var genericPrefabs = new List(); if (multiPrefabs) { for (int i = 0; i < cardPrefabs.Count; i++) { var pf = cardPrefabs[i]; if (pf == null) continue; var cb = pf.GetComponent(); if (cb != null && cb.Data != null) fixedPrefabs.Add((pf, cb.Data.type)); else genericPrefabs.Add(pf); } } else { var pf = cardPrefab; var cb = pf != null ? pf.GetComponent() : null; if (cb != null && cb.Data != null) fixedPrefabs.Add((pf, cb.Data.type)); else if (pf != null) genericPrefabs.Add(pf); } // Determine feasible types var feasibleTypes = new List(); var fixedTypes = new HashSet(); for (int i = 0; i < fixedPrefabs.Count; i++) fixedTypes.Add(fixedPrefabs[i].type); foreach (var t in fixedTypes) if (!disallowed.Contains(t)) feasibleTypes.Add(t); bool hasGeneric = genericPrefabs.Count > 0; if (hasGeneric && cardPool != null && cardPool.Count > 0) { var poolTypesSet = new HashSet(); for (int i = 0; i < cardPool.Count; i++) poolTypesSet.Add(cardPool[i].type); foreach (var t in poolTypesSet) { if (!disallowed.Contains(t) && !feasibleTypes.Contains(t)) feasibleTypes.Add(t); } } // If nothing feasible with disallowed constraint, relax constraint if (feasibleTypes.Count == 0) { foreach (var t in fixedTypes) if (!feasibleTypes.Contains(t)) feasibleTypes.Add(t); if (hasGeneric && cardPool != null && cardPool.Count > 0) { var poolTypesSet = new HashSet(); for (int i = 0; i < cardPool.Count; i++) poolTypesSet.Add(cardPool[i].type); foreach (var t in poolTypesSet) if (!feasibleTypes.Contains(t)) feasibleTypes.Add(t); } } // Fallback: if still empty (e.g., no pool and only generic prefabs), just spawn any prefab without enforcing type if (feasibleTypes.Count == 0) { GameObject anyPrefab = multiPrefabs ? (cardPrefabs.Count > 0 ? cardPrefabs[Random.Range(0, cardPrefabs.Count)] : null) : cardPrefab; if (anyPrefab == null) return null; return Instantiate(anyPrefab, pos, rot); } // Choose a target type randomly var targetType = feasibleTypes[Random.Range(0, feasibleTypes.Count)]; // Choose a matching prefab if possible; else a generic prefab GameObject chosenPrefab = null; var matches = new List(); for (int i = 0; i < fixedPrefabs.Count; i++) if (fixedPrefabs[i].type.Equals(targetType)) matches.Add(fixedPrefabs[i].prefab); if (matches.Count > 0) chosenPrefab = matches[Random.Range(0, matches.Count)]; else if (hasGeneric) chosenPrefab = genericPrefabs[Random.Range(0, genericPrefabs.Count)]; else chosenPrefab = multiPrefabs ? cardPrefabs[Random.Range(0, cardPrefabs.Count)] : cardPrefab; // last resort var go = Instantiate(chosenPrefab, pos, rot); var behaviour = go.GetComponent(); if (behaviour != null && behaviour.Data == null) { // Assign data matching chosen type CardData assign = null; if (cardPool != null && cardPool.Count > 0) { // collect indices with targetType var idxs = new List(); for (int i = 0; i < cardPool.Count; i++) if (cardPool[i].type.Equals(targetType)) idxs.Add(i); if (idxs.Count > 0) assign = cardPool[idxs[Random.Range(0, idxs.Count)]]; else assign = cardPool[Random.Range(0, cardPool.Count)]; } if (assign != null) behaviour.SetData(assign); } return go; } } }