using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
namespace DemonBoss.Magic
{
///
/// Spawns exactly 3 crystal turrets around the opponent.
/// Now with per-spawn randomness and position validation.
///
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn 3 Turrets Radial")]
public class SA_SpawnTurretSmart : vStateAction
{
public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Spawn 3 Turrets Radial";
[Header("Turret Configuration")]
public GameObject crystalPrefab;
public float minSpawnDistance = 2f;
public float maxSpawnDistance = 6f;
public float obstacleCheckRadius = 1f;
public float groundCheckHeight = 2f;
public LayerMask obstacleLayerMask = -1;
public LayerMask groundLayerMask = -1;
public string animatorBlockingBool = "IsBlocking";
[Header("Placement Search (Validation)")]
public int perTurretAdjustmentTries = 10;
public float maxAngleAdjust = 25f;
public float maxRadiusAdjust = 1.0f;
public float minSeparationBetweenTurrets = 1.5f;
[Header("Randomization (Formation)")]
[Tooltip("Random global rotation offset (degrees) applied to the 120° spokes.")]
public bool globalStartAngleRandom = true;
[Tooltip("Per-turret angle jitter around the spoke (degrees). 0 = disabled.")]
public float perTurretAngleJitter = 10f;
[Tooltip("Per-turret radius jitter (meters). 0 = disabled.")]
public float perTurretRadiusJitter = 0.75f;
[Header("One-off Overlay Clip (No Animator Params)")]
public AnimationClip overlayClip;
[Tooltip("Playback speed (1 = normal)")] public float overlaySpeed = 1f;
[Tooltip("Blend-in seconds (instant in this minimal impl)")] public float overlayFadeIn = 0.10f;
[Tooltip("Blend-out seconds (instant in this minimal impl)")] public float overlayFadeOut = 0.10f;
[Header("Debug")]
public bool enableDebug = false;
public bool showGizmos = true;
private Animator npcAnimator;
private Transform npcTransform;
private Transform playerTransform;
private readonly System.Collections.Generic.List _lastPlanned =
new System.Collections.Generic.List(3);
// --- Playables runtime ---
private PlayableGraph _overlayGraph;
private AnimationPlayableOutput _overlayOutput;
private AnimationClipPlayable _overlayPlayable;
private bool _overlayPlaying;
private float _overlayStopAtTime;
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{
if (executionType == vFSMComponentExecutionType.OnStateEnter) OnStateEnter(fsmBehaviour);
else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
{
// Auto-stop overlay when finished
if (_overlayPlaying && Time.time >= _overlayStopAtTime) StopOverlayWithFade();
}
else if (executionType == vFSMComponentExecutionType.OnStateExit) OnStateExit(fsmBehaviour);
}
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
{
npcTransform = fsmBehaviour.transform;
npcAnimator = npcTransform.GetComponent();
FindPlayer(fsmBehaviour);
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
npcAnimator.SetBool(animatorBlockingBool, true);
PlayOverlayOnce(npcTransform);
SpawnThreeTurretsRadial(fsmBehaviour);
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
}
private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
{
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
npcAnimator.SetBool(animatorBlockingBool, false);
StopOverlayWithFade();
}
private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerTransform = player.transform;
return;
}
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null && aiController.currentTarget != null)
playerTransform = aiController.currentTarget.transform;
}
private void SpawnThreeTurretsRadial(vIFSMBehaviourController fsmBehaviour)
{
if (crystalPrefab == null)
{
Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!");
return;
}
_lastPlanned.Clear();
Vector3 center = playerTransform != null ? playerTransform.position : npcTransform.position;
float baseRadius = Mathf.Clamp((minSpawnDistance + maxSpawnDistance) * 0.5f, minSpawnDistance, maxSpawnDistance);
Vector3 refDir = Vector3.forward;
if (playerTransform != null && npcTransform != null)
{
Vector3 d = (npcTransform.position - center); d.y = 0f;
if (d.sqrMagnitude > 0.0001f) refDir = d.normalized;
}
else if (npcTransform != null)
{
refDir = npcTransform.forward;
}
float globalOffset = globalStartAngleRandom ? Random.Range(0f, 360f) : 0f;
const int turretCount = 3;
for (int i = 0; i < turretCount; i++)
{
float baseAngle = globalOffset + i * 120f;
float angle = baseAngle + (perTurretAngleJitter > 0f ? Random.Range(-perTurretAngleJitter, perTurretAngleJitter) : 0f);
float radius = baseRadius + (perTurretRadiusJitter > 0f ? Random.Range(-perTurretRadiusJitter, perTurretRadiusJitter) : 0f);
radius = Mathf.Clamp(radius, minSpawnDistance, maxSpawnDistance);
Vector3 ideal = center + Quaternion.Euler(0f, angle, 0f) * refDir * radius;
Vector3? chosen = IsPositionValidAndSeparated(ideal)
? (Vector3?)ideal
: FindValidPositionAroundSpoke(center, refDir, angle, radius);
if (!chosen.HasValue)
{
Vector3 rough = center + Quaternion.Euler(0f, angle, 0f) * refDir * radius;
chosen = EnforceSeparationFallback(rough, center);
}
Vector3 spawnPos = chosen.Value;
_lastPlanned.Add(spawnPos);
SpawnCrystal(spawnPos, fsmBehaviour);
}
}
private Vector3? FindValidPositionAroundSpoke(Vector3 center, Vector3 refDir, float baseAngleDeg, float desiredRadius)
{
Vector3 ideal = center + Quaternion.Euler(0f, baseAngleDeg, 0f) * refDir * desiredRadius;
if (IsPositionValidAndSeparated(ideal)) return ideal;
for (int t = 0; t < perTurretAdjustmentTries; t++)
{
float ang = baseAngleDeg + Random.Range(-maxAngleAdjust, maxAngleAdjust);
float rad = Mathf.Clamp(desiredRadius + Random.Range(-maxRadiusAdjust, maxRadiusAdjust), minSpawnDistance, maxSpawnDistance);
Vector3 cand = center + Quaternion.Euler(0f, ang, 0f) * refDir * rad;
if (IsPositionValidAndSeparated(cand)) return cand;
}
return null;
}
private bool IsPositionValidAndSeparated(Vector3 position)
{
if (!IsPositionValid(position, out Vector3 grounded)) return false;
for (int i = 0; i < _lastPlanned.Count; i++)
{
if (Vector3.Distance(_lastPlanned[i], grounded) < Mathf.Max(minSeparationBetweenTurrets, obstacleCheckRadius * 2f))
return false;
}
return true;
}
private bool IsPositionValid(Vector3 position, out Vector3 groundPosition)
{
groundPosition = position;
if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
return false;
Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down);
if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask))
{
groundPosition = hit.point;
if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
return false;
return true;
}
return false;
}
private Vector3 EnforceSeparationFallback(Vector3 desired, Vector3 center)
{
Vector3 candidate = desired;
float step = 0.5f;
for (int i = 0; i < 10; i++)
{
bool ok = true;
for (int k = 0; k < _lastPlanned.Count; k++)
{
if (Vector3.Distance(_lastPlanned[k], candidate) < Mathf.Max(minSeparationBetweenTurrets, obstacleCheckRadius * 2f))
{ ok = false; break; }
}
if (ok) return candidate;
Vector3 dir = (candidate - center); dir.y = 0f;
if (dir.sqrMagnitude < 0.0001f) dir = Vector3.right;
candidate = center + dir.normalized * (dir.magnitude + step);
}
return candidate;
}
private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour)
{
Quaternion rotation = Quaternion.identity;
Transform lookAt = playerTransform != null ? playerTransform : npcTransform;
if (lookAt != null)
{
Vector3 lookDirection = (lookAt.position - position).normalized;
lookDirection.y = 0;
if (lookDirection != Vector3.zero)
rotation = Quaternion.LookRotation(lookDirection);
}
var spawned = LeanPool.Spawn(crystalPrefab, position, rotation);
var shooterAI = spawned.GetComponent();
if (shooterAI == null)
{
Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
}
else if (playerTransform != null)
{
shooterAI.SetTarget(playerTransform);
}
}
private void OnDrawGizmosSelected()
{
if (!showGizmos || npcTransform == null) return;
Vector3 c = playerTransform ? playerTransform.position : npcTransform.position;
Gizmos.color = Color.green;
DrawWireCircle(c, minSpawnDistance);
Gizmos.color = Color.red;
DrawWireCircle(c, maxSpawnDistance);
Gizmos.color = Color.cyan;
foreach (var p in _lastPlanned) Gizmos.DrawWireSphere(p + Vector3.up * 0.1f, 0.2f);
}
private void DrawWireCircle(Vector3 center, float radius)
{
int segments = 32;
Vector3 prevPoint = center + new Vector3(radius, 0, 0);
for (int i = 1; i <= segments; i++)
{
float angle = (float)i / segments * 360f * Mathf.Deg2Rad;
Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
Gizmos.DrawLine(prevPoint, newPoint);
prevPoint = newPoint;
}
}
private void PlayOverlayOnce(Transform owner)
{
if (overlayClip == null) return;
if (npcAnimator == null)
npcAnimator = owner.GetComponent();
if (npcAnimator == null) return;
StopOverlayImmediate(); // safety
_overlayGraph = PlayableGraph.Create("ActionOverlay(SpawnTurret)");
_overlayGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
_overlayPlayable = AnimationClipPlayable.Create(_overlayGraph, overlayClip);
_overlayPlayable.SetApplyFootIK(false);
_overlayPlayable.SetApplyPlayableIK(false);
_overlayPlayable.SetSpeed(Mathf.Max(0.0001f, overlaySpeed));
_overlayOutput = AnimationPlayableOutput.Create(_overlayGraph, "AnimOut", npcAnimator);
_overlayOutput.SetSourcePlayable(_overlayPlayable);
_overlayOutput.SetWeight(1f);
_overlayGraph.Play();
_overlayPlaying = true;
float len = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed);
_overlayStopAtTime = Time.time + len;
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Overlay clip started via Playables");
}
private void StopOverlayImmediate()
{
if (_overlayGraph.IsValid())
{
_overlayGraph.Stop();
_overlayGraph.Destroy();
}
_overlayPlaying = false;
}
private void StopOverlayWithFade()
{
if (!_overlayPlaying) { StopOverlayImmediate(); return; }
if (_overlayOutput.IsOutputNull() == false) _overlayOutput.SetWeight(0f);
StopOverlayImmediate();
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Overlay clip stopped");
}
}
}