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"); } } }