chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
352
Assets/Scripts/SpecialMoveCardSystem/CardSpawner.cs
Normal file
352
Assets/Scripts/SpecialMoveCardSystem/CardSpawner.cs
Normal file
@@ -0,0 +1,352 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
|
||||
namespace SpecialMoveCards
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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<CardSpawner> s_instances = new List<CardSpawner>();
|
||||
[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<GameObject> cardPrefabs = new List<GameObject>();
|
||||
[Tooltip("Pool of possible CardData to spawn from.")]
|
||||
public List<CardData> cardPool = new List<CardData>();
|
||||
|
||||
[Header("Spawning")]
|
||||
public List<Transform> spawnPoints = new List<Transform>();
|
||||
[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<GameObject> alive = new HashSet<GameObject>();
|
||||
private readonly Dictionary<int, GameObject> occupiedPoints = new Dictionary<int, GameObject>();
|
||||
|
||||
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<CardType> 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<SpawnedCardToken>();
|
||||
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 ---
|
||||
/// <summary>
|
||||
/// Called by external systems to attempt a drop at a world position with a given probability.
|
||||
/// Respects each spawner's targetCardsInScene limit.
|
||||
/// </summary>
|
||||
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<SpawnedCardToken>();
|
||||
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<GameObject>();
|
||||
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<int>();
|
||||
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<CardType> BuildDisallowedTypes()
|
||||
{
|
||||
var disallowed = new HashSet<CardType>();
|
||||
// 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<CardBehaviour>();
|
||||
if (cb != null && cb.Data != null)
|
||||
{
|
||||
disallowed.Add(cb.Data.type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return disallowed;
|
||||
}
|
||||
|
||||
private GameObject SpawnCard(Vector3 pos, Quaternion rot, HashSet<CardType> 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<GameObject>();
|
||||
|
||||
if (multiPrefabs)
|
||||
{
|
||||
for (int i = 0; i < cardPrefabs.Count; i++)
|
||||
{
|
||||
var pf = cardPrefabs[i];
|
||||
if (pf == null) continue;
|
||||
var cb = pf.GetComponent<CardBehaviour>();
|
||||
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<CardBehaviour>() : 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<CardType>();
|
||||
var fixedTypes = new HashSet<CardType>();
|
||||
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<CardType>();
|
||||
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<CardType>();
|
||||
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<GameObject>();
|
||||
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<CardBehaviour>();
|
||||
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<int>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user