diff --git a/Assets/AI/Demon.meta b/Assets/AI/Demon.meta new file mode 100644 index 000000000..ff0c21357 --- /dev/null +++ b/Assets/AI/Demon.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 67c253388a8e1594783ff96204c292f8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AI/Demon/DEC_CheckCooldown.cs b/Assets/AI/Demon/DEC_CheckCooldown.cs new file mode 100644 index 000000000..bfa3ad2e3 --- /dev/null +++ b/Assets/AI/Demon/DEC_CheckCooldown.cs @@ -0,0 +1,205 @@ +using Invector.vCharacterController.AI.FSMBehaviour; +using UnityEngine; + +namespace DemonBoss.Magic +{ + /// + /// Decision node checking cooldown for different boss abilities + /// Stores Time.time in FSM timers and checks if required cooldown time has passed + /// + [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Check Cooldown")] + public class DEC_CheckCooldown : vStateDecision + { + public override string categoryName => "DemonBoss/Magic"; + public override string defaultName => "Check Cooldown"; + + [Header("Cooldown Configuration")] + [Tooltip("Unique key for this ability (e.g. 'Shield', 'Turret', 'Meteor')")] + public string cooldownKey = "Shield"; + + [Tooltip("Cooldown time in seconds")] + public float cooldownTime = 10f; + + [Tooltip("Whether ability should be available immediately at fight start")] + public bool availableAtStart = true; + + [Header("Debug")] + [Tooltip("Enable debug logging")] + public bool enableDebug = false; + + /// + /// Main method checking if ability is available + /// + /// True if cooldown has passed and ability can be used + public override bool Decide(vIFSMBehaviourController fsmBehaviour) + { + if (fsmBehaviour == null) + { + if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] No FSM Behaviour for key: {cooldownKey}"); + return false; + } + + string timerKey = "cooldown_" + cooldownKey; + + if (!fsmBehaviour.HasTimer(timerKey)) + { + if (availableAtStart) + { + if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - available"); + return true; + } + else + { + SetCooldown(fsmBehaviour, cooldownTime); + if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - setting cooldown"); + return false; + } + } + + float lastUsedTime = fsmBehaviour.GetTimer(timerKey); + float timeSinceLastUse = Time.time - lastUsedTime; + + bool isAvailable = timeSinceLastUse >= cooldownTime; + + if (enableDebug) + { + if (isAvailable) + { + Debug.Log($"[DEC_CheckCooldown] {cooldownKey} available - {timeSinceLastUse:F1}s passed of required {cooldownTime}s"); + } + else + { + float remainingTime = cooldownTime - timeSinceLastUse; + Debug.Log($"[DEC_CheckCooldown] {cooldownKey} on cooldown - {remainingTime:F1}s remaining"); + } + } + + return isAvailable; + } + + /// + /// Sets cooldown for ability - call this after using ability + /// + public void SetCooldown(vIFSMBehaviourController fsmBehaviour) + { + SetCooldown(fsmBehaviour, cooldownTime); + } + + /// + /// Sets cooldown with custom time + /// + /// FSM behaviour reference + /// Custom cooldown time + public void SetCooldown(vIFSMBehaviourController fsmBehaviour, float customCooldownTime) + { + if (fsmBehaviour == null) + { + if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot set cooldown - no FSM Behaviour"); + return; + } + + string timerKey = "cooldown_" + cooldownKey; + + fsmBehaviour.SetTimer(timerKey, Time.time); + + if (enableDebug) + { + Debug.Log($"[DEC_CheckCooldown] Set cooldown for {cooldownKey}: {customCooldownTime}s"); + } + } + + /// + /// Resets cooldown - ability becomes immediately available + /// + public void ResetCooldown(vIFSMBehaviourController fsmBehaviour) + { + if (fsmBehaviour == null) + { + if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot reset cooldown - no FSM Behaviour"); + return; + } + + string timerKey = "cooldown_" + cooldownKey; + + float pastTime = Time.time - cooldownTime - 1f; + fsmBehaviour.SetTimer(timerKey, pastTime); + + if (enableDebug) Debug.Log($"[DEC_CheckCooldown] Reset cooldown for {cooldownKey}"); + } + + /// + /// Returns remaining cooldown time in seconds + /// + /// Remaining cooldown time (0 if available) + public float GetRemainingCooldown(vIFSMBehaviourController fsmBehaviour) + { + if (fsmBehaviour == null) return 0f; + + string timerKey = "cooldown_" + cooldownKey; + + if (!fsmBehaviour.HasTimer(timerKey)) + { + return availableAtStart ? 0f : cooldownTime; + } + + float lastUsedTime = fsmBehaviour.GetTimer(timerKey); + float timeSinceLastUse = Time.time - lastUsedTime; + + return Mathf.Max(0f, cooldownTime - timeSinceLastUse); + } + + /// + /// Checks if ability is available without running main Decision logic + /// + /// True if ability is available + public bool IsAvailable(vIFSMBehaviourController fsmBehaviour) + { + return GetRemainingCooldown(fsmBehaviour) <= 0f; + } + + /// + /// Returns cooldown progress percentage (0-1) + /// + /// Progress percentage: 0 = just used, 1 = fully recharged + public float GetCooldownProgress(vIFSMBehaviourController fsmBehaviour) + { + if (cooldownTime <= 0f) return 1f; + + float remainingTime = GetRemainingCooldown(fsmBehaviour); + return 1f - (remainingTime / cooldownTime); + } + + /// + /// Helper method for setting cooldown from external code (e.g. from StateAction) + /// + /// FSM reference + /// Ability key + /// Cooldown time + public static void SetCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown) + { + if (fsmBehaviour == null) return; + + string timerKey = "cooldown_" + key; + fsmBehaviour.SetTimer(timerKey, Time.time); + } + + /// + /// Helper method for checking cooldown from external code + /// + /// FSM reference + /// Ability key + /// Cooldown time + /// True if available + public static bool CheckCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown) + { + if (fsmBehaviour == null) return false; + + string timerKey = "cooldown_" + key; + + if (!fsmBehaviour.HasTimer(timerKey)) return true; + + float lastUsedTime = fsmBehaviour.GetTimer(timerKey); + return (Time.time - lastUsedTime) >= cooldown; + } + } +} \ No newline at end of file diff --git a/Assets/AI/Demon/DEC_CheckCooldown.cs.meta b/Assets/AI/Demon/DEC_CheckCooldown.cs.meta new file mode 100644 index 000000000..191fafb3e --- /dev/null +++ b/Assets/AI/Demon/DEC_CheckCooldown.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: af82d1d082ce4b9478d420f8ca1e72c2 \ No newline at end of file diff --git a/Assets/AI/Demon/DEC_TargetClearSky.cs b/Assets/AI/Demon/DEC_TargetClearSky.cs new file mode 100644 index 000000000..95ab638f3 --- /dev/null +++ b/Assets/AI/Demon/DEC_TargetClearSky.cs @@ -0,0 +1,105 @@ +using Invector.vCharacterController.AI.FSMBehaviour; +using UnityEngine; + +namespace DemonBoss.Magic +{ + /// + /// Decision checking if target has clear sky above (for meteor) + /// + [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Clear Sky")] + public class DEC_TargetClearSky : vStateDecision + { + public override string categoryName => "DemonBoss/Magic"; + public override string defaultName => "Target Clear Sky"; + + [Header("Sky Check Configuration")] + [Tooltip("Check height above target")] + public float checkHeight = 25f; + + [Tooltip("Obstacle check radius")] + public float checkRadius = 2f; + + [Tooltip("Obstacle layer mask")] + public LayerMask obstacleLayerMask = -1; + + [Header("Debug")] + [Tooltip("Enable debug logging")] + public bool enableDebug = false; + + [Tooltip("Show gizmos in Scene View")] + public bool showGizmos = true; + + public override bool Decide(vIFSMBehaviourController fsmBehaviour) + { + Transform target = GetTarget(fsmBehaviour); + if (target == null) + { + if (enableDebug) Debug.Log("[DEC_TargetClearSky] No target found"); + return false; + } + + bool isClear = IsSkyClear(target.position); + + if (enableDebug) + { + Debug.Log($"[DEC_TargetClearSky] Sky above target: {(isClear ? "CLEAR" : "BLOCKED")}"); + } + + return isClear; + } + + private bool IsSkyClear(Vector3 targetPosition) + { + Vector3 skyCheckPoint = targetPosition + Vector3.up * checkHeight; + + if (Physics.CheckSphere(skyCheckPoint, checkRadius, obstacleLayerMask)) + { + return false; + } + + Ray skyRay = new Ray(skyCheckPoint, Vector3.down); + RaycastHit[] hits = Physics.RaycastAll(skyRay, checkHeight, obstacleLayerMask); + + foreach (var hit in hits) + { + if (hit.point.y <= targetPosition.y + 0.5f) continue; + + return false; + } + + return true; + } + + private Transform GetTarget(vIFSMBehaviourController fsmBehaviour) + { + // Try through AI controller + var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; + if (aiController != null && aiController.currentTarget != null) + return aiController.currentTarget.transform; + + // Fallback - find player + GameObject player = GameObject.FindGameObjectWithTag("Player"); + return player?.transform; + } + + private void OnDrawGizmosSelected() + { + if (!showGizmos) return; + + GameObject player = GameObject.FindGameObjectWithTag("Player"); + if (player == null) return; + + Vector3 targetPos = player.transform.position; + Vector3 skyCheckPoint = targetPos + Vector3.up * checkHeight; + + Gizmos.color = Color.cyan; + Gizmos.DrawWireSphere(skyCheckPoint, checkRadius); + + Gizmos.color = Color.yellow; + Gizmos.DrawLine(targetPos, skyCheckPoint); + + Gizmos.color = Color.red; + Gizmos.DrawWireSphere(targetPos, 0.5f); + } + } +} \ No newline at end of file diff --git a/Assets/AI/Demon/DEC_TargetClearSky.cs.meta b/Assets/AI/Demon/DEC_TargetClearSky.cs.meta new file mode 100644 index 000000000..9ca0aecfa --- /dev/null +++ b/Assets/AI/Demon/DEC_TargetClearSky.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a60b21a6b7b97264b856431f5eb253b1 \ No newline at end of file diff --git a/Assets/AI/Demon/DEC_TargetFar.cs b/Assets/AI/Demon/DEC_TargetFar.cs new file mode 100644 index 000000000..f73103635 --- /dev/null +++ b/Assets/AI/Demon/DEC_TargetFar.cs @@ -0,0 +1,58 @@ +using Invector.vCharacterController.AI.FSMBehaviour; +using UnityEngine; + +namespace DemonBoss.Magic +{ + /// + /// Decision checking if target is far away (for Turret ability) + /// + [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Far")] + public class DEC_TargetFar : vStateDecision + { + public override string categoryName => "DemonBoss/Magic"; + public override string defaultName => "Target Far"; + + [Header("Distance Configuration")] + [Tooltip("Minimum distance for target to be considered far")] + public float minDistance = 8f; + + [Tooltip("Maximum distance for checking")] + public float maxDistance = 30f; + + [Header("Debug")] + [Tooltip("Enable debug logging")] + public bool enableDebug = false; + + public override bool Decide(vIFSMBehaviourController fsmBehaviour) + { + Transform target = GetTarget(fsmBehaviour); + if (target == null) + { + if (enableDebug) Debug.Log("[DEC_TargetFar] No target found"); + return false; + } + + float distance = Vector3.Distance(fsmBehaviour.transform.position, target.position); + bool isFar = distance >= minDistance && distance <= maxDistance; + + if (enableDebug) + { + Debug.Log($"[DEC_TargetFar] Distance to target: {distance:F1}m - {(isFar ? "FAR" : "CLOSE")}"); + } + + return isFar; + } + + private Transform GetTarget(vIFSMBehaviourController fsmBehaviour) + { + // Try through AI controller + var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; + if (aiController != null && aiController.currentTarget != null) + return aiController.currentTarget.transform; + + // Fallback - find player + GameObject player = GameObject.FindGameObjectWithTag("Player"); + return player?.transform; + } + } +} \ No newline at end of file diff --git a/Assets/AI/Demon/DEC_TargetFar.cs.meta b/Assets/AI/Demon/DEC_TargetFar.cs.meta new file mode 100644 index 000000000..df2d95120 --- /dev/null +++ b/Assets/AI/Demon/DEC_TargetFar.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6df4a5087a0930d479908e8416bc8a2a \ No newline at end of file diff --git a/Assets/AI/Demon/SA_CallMeteor.cs b/Assets/AI/Demon/SA_CallMeteor.cs new file mode 100644 index 000000000..ff9987890 --- /dev/null +++ b/Assets/AI/Demon/SA_CallMeteor.cs @@ -0,0 +1,352 @@ +using Invector.vCharacterController.AI.FSMBehaviour; +using Lean.Pool; +using System.Collections; +using UnityEngine; + +namespace DemonBoss.Magic +{ + /// + /// StateAction for Meteor Strike spell - boss summons meteor falling on player + /// First places decal at player position, after 1.5s checks if path is clear and drops rock + /// + [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Call Meteor")] + public class SA_CallMeteor : vStateAction + { + public override string categoryName => "DemonBoss/Magic"; + public override string defaultName => "Call Meteor"; + + [Header("Meteor Configuration")] + [Tooltip("Meteor rock prefab")] + public GameObject rockPrefab; + + [Tooltip("Decal prefab showing impact location")] + public GameObject decalPrefab; + + [Tooltip("Height from which meteor falls")] + public float meteorHeight = 30f; + + [Tooltip("Delay between placing decal and spawning meteor")] + public float castDelay = 1.5f; + + [Tooltip("Obstacle check radius above target")] + public float obstacleCheckRadius = 2f; + + [Tooltip("Layer mask for obstacles blocking meteor")] + public LayerMask obstacleLayerMask = -1; + + [Tooltip("Layer mask for ground")] + public LayerMask groundLayerMask = -1; + + [Tooltip("Animator trigger name for meteor summoning animation")] + public string animatorTrigger = "CastMeteor"; + + [Header("Targeting")] + [Tooltip("Maximum raycast distance to find ground")] + public float maxGroundDistance = 100f; + + [Tooltip("Height above ground for obstacle checking")] + public float airCheckHeight = 5f; + + [Header("Debug")] + [Tooltip("Enable debug logging")] + public bool enableDebug = false; + + [Tooltip("Show gizmos in Scene View")] + public bool showGizmos = true; + + private GameObject spawnedDecal; + + private Transform playerTransform; + private Vector3 targetPosition; + private bool meteorCasting = false; + private MonoBehaviour coroutineRunner; + + /// + /// Main action execution method called by FSM + /// + public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate) + { + if (executionType == vFSMComponentExecutionType.OnStateEnter) + { + OnStateEnter(fsmBehaviour); + } + else if (executionType == vFSMComponentExecutionType.OnStateExit) + { + OnStateExit(fsmBehaviour); + } + } + + /// + /// Called when entering state - starts meteor casting + /// + private void OnStateEnter(vIFSMBehaviourController fsmBehaviour) + { + if (enableDebug) Debug.Log("[SA_CallMeteor] Starting meteor casting"); + + FindPlayer(fsmBehaviour); + + var animator = fsmBehaviour.transform.GetComponent(); + if (animator != null && !string.IsNullOrEmpty(animatorTrigger)) + { + animator.SetTrigger(animatorTrigger); + if (enableDebug) Debug.Log($"[SA_CallMeteor] Set trigger: {animatorTrigger}"); + } + + StartMeteorCast(fsmBehaviour); + } + + /// + /// Called when exiting state - cleanup + /// + private void OnStateExit(vIFSMBehaviourController fsmBehaviour) + { + if (enableDebug) Debug.Log("[SA_CallMeteor] Exiting meteor state"); + meteorCasting = false; + } + + /// + /// Finds player transform + /// + private void FindPlayer(vIFSMBehaviourController fsmBehaviour) + { + GameObject player = GameObject.FindGameObjectWithTag("Player"); + if (player != null) + { + playerTransform = player.transform; + if (enableDebug) Debug.Log("[SA_CallMeteor] Player found by tag"); + return; + } + + var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; + if (aiController != null && aiController.currentTarget != null) + { + playerTransform = aiController.currentTarget.transform; + if (enableDebug) Debug.Log("[SA_CallMeteor] Player found through AI target"); + return; + } + + if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Player not found!"); + } + + /// + /// Starts meteor casting process + /// + private void StartMeteorCast(vIFSMBehaviourController fsmBehaviour) + { + if (playerTransform == null) + { + if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target - finishing state"); + return; + } + + if (FindGroundUnderPlayer(out Vector3 groundPos)) + { + targetPosition = groundPos; + + SpawnDecal(targetPosition); + + coroutineRunner = fsmBehaviour.transform.GetComponent(); + if (coroutineRunner != null) + { + coroutineRunner.StartCoroutine(MeteorCastCoroutine(fsmBehaviour)); + } + else + { + CastMeteorImmediate(); + } + + meteorCasting = true; + + if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor target: {targetPosition}"); + } + else + { + if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Cannot find ground under player"); + } + } + + /// + /// Finds ground under player using raycast + /// + private bool FindGroundUnderPlayer(out Vector3 groundPosition) + { + groundPosition = Vector3.zero; + + Vector3 playerPos = playerTransform.position; + Vector3 rayStart = playerPos + Vector3.up * 5f; + + Ray groundRay = new Ray(rayStart, Vector3.down); + if (Physics.Raycast(groundRay, out RaycastHit hit, maxGroundDistance, groundLayerMask)) + { + groundPosition = hit.point; + return true; + } + + groundPosition = playerPos; + return true; + } + + /// + /// Spawns decal showing meteor impact location + /// + private void SpawnDecal(Vector3 position) + { + if (decalPrefab == null) + { + if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Missing decal prefab"); + return; + } + + Quaternion decalRotation = Quaternion.identity; + + Ray surfaceRay = new Ray(position + Vector3.up * 2f, Vector3.down); + if (Physics.Raycast(surfaceRay, out RaycastHit hit, 5f, groundLayerMask)) + { + decalRotation = Quaternion.FromToRotation(Vector3.up, hit.normal); + } + + spawnedDecal = LeanPool.Spawn(decalPrefab, position, decalRotation); + + if (enableDebug) Debug.Log($"[SA_CallMeteor] Decal spawned at: {position}"); + } + + /// + /// Coroutine handling meteor casting process with delay + /// + private IEnumerator MeteorCastCoroutine(vIFSMBehaviourController fsmBehaviour) + { + yield return new WaitForSeconds(castDelay); + + if (enableDebug) Debug.Log("[SA_CallMeteor] Checking if path is clear for meteor"); + + if (IsPathClearForMeteor(targetPosition)) + { + SpawnMeteor(targetPosition); + if (enableDebug) Debug.Log("[SA_CallMeteor] Meteor spawned"); + } + else + { + if (enableDebug) Debug.Log("[SA_CallMeteor] Path blocked - meteor not spawned"); + } + + CleanupDecal(); + + DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Meteor", 20f); + + meteorCasting = false; + } + + /// + /// Immediate meteor cast without coroutine (fallback) + /// + private void CastMeteorImmediate() + { + if (IsPathClearForMeteor(targetPosition)) + { + SpawnMeteor(targetPosition); + } + CleanupDecal(); + } + + /// + /// Checks if path above target is clear for meteor + /// + private bool IsPathClearForMeteor(Vector3 targetPos) + { + Vector3 checkStart = targetPos + Vector3.up * airCheckHeight; + Vector3 checkEnd = targetPos + Vector3.up * meteorHeight; + + if (Physics.CheckSphere(checkStart, obstacleCheckRadius, obstacleLayerMask)) + { + return false; + } + + Ray pathRay = new Ray(checkEnd, Vector3.down); + if (Physics.SphereCast(pathRay, obstacleCheckRadius, meteorHeight - airCheckHeight, obstacleLayerMask)) + { + return false; + } + + return true; + } + + /// + /// Spawns meteor in air above target + /// + private void SpawnMeteor(Vector3 targetPos) + { + if (rockPrefab == null) + { + Debug.LogError("[SA_CallMeteor] Missing meteor prefab!"); + return; + } + + Vector3 meteorSpawnPos = targetPos + Vector3.up * meteorHeight; + + GameObject meteor = LeanPool.Spawn(rockPrefab, meteorSpawnPos, Quaternion.identity); + + Rigidbody meteorRb = meteor.GetComponent(); + if (meteorRb != null) + { + meteorRb.linearVelocity = Vector3.down * 5f; + meteorRb.angularVelocity = Random.insideUnitSphere * 2f; + } + + if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor spawned at height: {meteorHeight}m"); + } + + /// + /// Removes decal from map + /// + private void CleanupDecal() + { + if (spawnedDecal != null) + { + LeanPool.Despawn(spawnedDecal); + spawnedDecal = null; + if (enableDebug) Debug.Log("[SA_CallMeteor] Decal removed"); + } + } + + /// + /// Checks if meteor is currently being cast + /// + public bool IsCasting() + { + return meteorCasting; + } + + /// + /// Returns meteor target position + /// + public Vector3 GetTargetPosition() + { + return targetPosition; + } + + /// + /// Draws gizmos in Scene View for debugging + /// + private void OnDrawGizmosSelected() + { + if (!showGizmos) return; + + if (meteorCasting && targetPosition != Vector3.zero) + { + Gizmos.color = Color.red; + Gizmos.DrawWireSphere(targetPosition, 1f); + + Vector3 spawnPos = targetPosition + Vector3.up * meteorHeight; + Gizmos.color = Color.yellow; + Gizmos.DrawWireSphere(spawnPos, 0.5f); + + Gizmos.color = Color.yellow; + Gizmos.DrawLine(spawnPos, targetPosition); + + Vector3 checkPos = targetPosition + Vector3.up * airCheckHeight; + Gizmos.color = Color.blue; + Gizmos.DrawWireSphere(checkPos, obstacleCheckRadius); + } + } + } +} \ No newline at end of file diff --git a/Assets/AI/Demon/SA_CallMeteor.cs.meta b/Assets/AI/Demon/SA_CallMeteor.cs.meta new file mode 100644 index 000000000..9ffe57f46 --- /dev/null +++ b/Assets/AI/Demon/SA_CallMeteor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6704e78bcf7b30a43a2d06d91e87d694 \ No newline at end of file diff --git a/Assets/AI/Demon/SA_CastShield.cs b/Assets/AI/Demon/SA_CastShield.cs new file mode 100644 index 000000000..7cdfb80be --- /dev/null +++ b/Assets/AI/Demon/SA_CastShield.cs @@ -0,0 +1,185 @@ +using Invector.vCharacterController.AI.FSMBehaviour; +using Lean.Pool; +using UnityEngine; + +namespace DemonBoss.Magic +{ + /// + /// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds + /// During casting boss stands still, after completion returns to Combat + /// + [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Cast Shield")] + public class SA_CastShield : vStateAction + { + public override string categoryName => "DemonBoss/Magic"; + public override string defaultName => "Cast Shield"; + + [Header("Shield Configuration")] + [Tooltip("Prefab with magical shield particle effect")] + public GameObject shieldFXPrefab; + + [Tooltip("Transform where shield should appear (usually boss center)")] + public Transform shieldSpawnPoint; + + [Tooltip("Shield duration in seconds")] + public float shieldDuration = 5f; + + [Tooltip("Animator trigger name for shield casting animation")] + public string animatorTrigger = "CastShield"; + + [Header("Debug")] + [Tooltip("Enable debug logging")] + public bool enableDebug = false; + + private GameObject spawnedShield; + + private float shieldStartTime; + private bool shieldActive = false; + + /// + /// Main action execution method called by FSM + /// + public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate) + { + if (executionType == vFSMComponentExecutionType.OnStateEnter) + { + OnStateEnter(fsmBehaviour); + } + else if (executionType == vFSMComponentExecutionType.OnStateUpdate) + { + OnStateUpdate(fsmBehaviour); + } + else if (executionType == vFSMComponentExecutionType.OnStateExit) + { + OnStateExit(fsmBehaviour); + } + } + + /// + /// Called when entering state - starts shield casting + /// + private void OnStateEnter(vIFSMBehaviourController fsmBehaviour) + { + if (enableDebug) Debug.Log("[SA_CastShield] Entering shield casting state"); + + var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; + if (aiController != null) + { + aiController.Stop(); + if (enableDebug) Debug.Log("[SA_CastShield] AI stopped"); + } + + var animator = fsmBehaviour.transform.GetComponent(); + if (animator != null && !string.IsNullOrEmpty(animatorTrigger)) + { + animator.SetTrigger(animatorTrigger); + if (enableDebug) Debug.Log($"[SA_CastShield] Set trigger: {animatorTrigger}"); + } + + SpawnShieldEffect(fsmBehaviour); + + shieldStartTime = Time.time; + shieldActive = true; + } + + /// + /// Called every frame during state duration + /// + private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour) + { + if (shieldActive && Time.time - shieldStartTime >= shieldDuration) + { + if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state"); + FinishShield(fsmBehaviour); + } + } + + /// + /// Called when exiting state - cleanup + /// + private void OnStateExit(vIFSMBehaviourController fsmBehaviour) + { + if (enableDebug) Debug.Log("[SA_CastShield] Exiting shield state"); + + if (shieldActive) + { + CleanupShield(); + } + + var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; + if (aiController != null) + { + if (enableDebug) Debug.Log("[SA_CastShield] AI resumed"); + } + } + + /// + /// Spawns magical shield particle effect + /// + private void SpawnShieldEffect(vIFSMBehaviourController fsmBehaviour) + { + if (shieldFXPrefab == null) + { + Debug.LogWarning("[SA_CastShield] Missing shieldFXPrefab!"); + return; + } + + Vector3 spawnPosition = shieldSpawnPoint != null ? + shieldSpawnPoint.position : fsmBehaviour.transform.position; + + spawnedShield = LeanPool.Spawn(shieldFXPrefab, spawnPosition, + shieldSpawnPoint != null ? shieldSpawnPoint.rotation : fsmBehaviour.transform.rotation); + + if (enableDebug) Debug.Log($"[SA_CastShield] Shield spawned at position: {spawnPosition}"); + + if (spawnedShield != null && shieldSpawnPoint != null) + { + spawnedShield.transform.SetParent(shieldSpawnPoint); + } + } + + /// + /// Finishes shield operation and transitions to next state + /// + private void FinishShield(vIFSMBehaviourController fsmBehaviour) + { + shieldActive = false; + CleanupShield(); + + DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Shield", 15f); + + // End state - FSM will transition to next state + // FYI: In Invector FSM, state completion is handled automatically + } + + /// + /// Cleans up spawned shield + /// + private void CleanupShield() + { + if (spawnedShield != null) + { + LeanPool.Despawn(spawnedShield); + spawnedShield = null; + if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned"); + } + } + + /// + /// Checks if shield is currently active + /// + public bool IsShieldActive() + { + return shieldActive; + } + + /// + /// Returns remaining shield time + /// + public float GetRemainingShieldTime() + { + if (!shieldActive) return 0f; + return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime)); + } + } +} \ No newline at end of file diff --git a/Assets/AI/Demon/SA_CastShield.cs.meta b/Assets/AI/Demon/SA_CastShield.cs.meta new file mode 100644 index 000000000..5897e16c6 --- /dev/null +++ b/Assets/AI/Demon/SA_CastShield.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c53af76a8a097a244a3002b8aa7b4ceb \ No newline at end of file diff --git a/Assets/AI/Demon/SA_SpawnTurretSmart.cs b/Assets/AI/Demon/SA_SpawnTurretSmart.cs new file mode 100644 index 000000000..e0b377cc2 --- /dev/null +++ b/Assets/AI/Demon/SA_SpawnTurretSmart.cs @@ -0,0 +1,298 @@ +using Invector.vCharacterController.AI.FSMBehaviour; +using Lean.Pool; +using UnityEngine; + +namespace DemonBoss.Magic +{ + /// + /// StateAction for intelligent crystal turret spawning + /// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player + /// + [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")] + public class SA_SpawnTurretSmart : vStateAction + { + public override string categoryName => "DemonBoss/Magic"; + public override string defaultName => "Spawn Turret Smart"; + + [Header("Turret Configuration")] + [Tooltip("Crystal prefab with CrystalShooterAI component")] + public GameObject crystalPrefab; + + [Tooltip("Minimum distance from boss for crystal spawn")] + public float minSpawnDistance = 2f; + + [Tooltip("Maximum distance from boss for crystal spawn")] + public float maxSpawnDistance = 6f; + + [Tooltip("Collision check radius when choosing position")] + public float obstacleCheckRadius = 1f; + + [Tooltip("Height above ground for raycast ground checking")] + public float groundCheckHeight = 2f; + + [Tooltip("Layer mask for obstacles")] + public LayerMask obstacleLayerMask = -1; + + [Tooltip("Layer mask for ground")] + public LayerMask groundLayerMask = -1; + + [Tooltip("Animator trigger name for crystal casting animation")] + public string animatorTrigger = "CastCrystal"; + + [Header("Smart Positioning")] + [Tooltip("Preference multiplier for positions behind boss (relative to player)")] + public float backPreferenceMultiplier = 2f; + + [Tooltip("Number of attempts to find valid position")] + public int maxSpawnAttempts = 12; + + [Header("Debug")] + [Tooltip("Enable debug logging")] + public bool enableDebug = false; + + [Tooltip("Show gizmos in Scene View")] + public bool showGizmos = true; + + private GameObject spawnedCrystal; + + private Transform playerTransform; + + /// + /// Main action execution method called by FSM + /// + public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate) + { + if (executionType == vFSMComponentExecutionType.OnStateEnter) + { + OnStateEnter(fsmBehaviour); + } + } + + /// + /// Called when entering state - intelligently spawns crystal + /// + private void OnStateEnter(vIFSMBehaviourController fsmBehaviour) + { + if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn"); + + FindPlayer(fsmBehaviour); + + var animator = fsmBehaviour.transform.GetComponent(); + if (animator != null && !string.IsNullOrEmpty(animatorTrigger)) + { + animator.SetTrigger(animatorTrigger); + if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set trigger: {animatorTrigger}"); + } + + SpawnCrystalSmart(fsmBehaviour); + + DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f); + } + + /// + /// Finds player transform + /// + private void FindPlayer(vIFSMBehaviourController fsmBehaviour) + { + GameObject player = GameObject.FindGameObjectWithTag("Player"); + if (player != null) + { + playerTransform = player.transform; + if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found by tag"); + return; + } + + var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; + if (aiController != null && aiController.currentTarget != null) + { + playerTransform = aiController.currentTarget.transform; + if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found through AI target"); + return; + } + + if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Player not found!"); + } + + /// + /// Intelligently spawns crystal in optimal position + /// + private void SpawnCrystalSmart(vIFSMBehaviourController fsmBehaviour) + { + if (crystalPrefab == null) + { + Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!"); + return; + } + + Vector3 bestPosition = Vector3.zero; + bool foundValidPosition = false; + float bestScore = float.MinValue; + + Vector3 bossPos = fsmBehaviour.transform.position; + Vector3 playerDirection = Vector3.zero; + + if (playerTransform != null) + { + playerDirection = (playerTransform.position - bossPos).normalized; + } + + for (int i = 0; i < maxSpawnAttempts; i++) + { + float angle = (360f / maxSpawnAttempts) * i + Random.Range(-15f, 15f); + Vector3 direction = new Vector3(Mathf.Cos(angle * Mathf.Deg2Rad), 0, Mathf.Sin(angle * Mathf.Deg2Rad)); + + float distance = Random.Range(minSpawnDistance, maxSpawnDistance); + Vector3 testPosition = bossPos + direction * distance; + + if (IsPositionValid(testPosition, out Vector3 groundPosition)) + { + float score = EvaluatePosition(groundPosition, playerDirection, direction); + + if (score > bestScore) + { + bestScore = score; + bestPosition = groundPosition; + foundValidPosition = true; + } + } + } + + if (foundValidPosition) + { + SpawnCrystal(bestPosition, fsmBehaviour); + if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})"); + } + else + { + Vector3 fallbackPos = bossPos + fsmBehaviour.transform.forward * minSpawnDistance; + SpawnCrystal(fallbackPos, fsmBehaviour); + if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position"); + } + } + + /// + /// Checks if position is valid (no obstacles, has ground) + /// + 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; + } + + /// + /// Evaluates position quality (higher score = better position) + /// + private float EvaluatePosition(Vector3 position, Vector3 playerDirection, Vector3 positionDirection) + { + float score = 0f; + + if (playerTransform != null && playerDirection != Vector3.zero) + { + float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection); + + // The smaller the angle (closer to "behind"), the better the score + float backScore = (180f - angleToPlayer) / 180f; + score += backScore * backPreferenceMultiplier; + } + + Vector3 bossPos = new Vector3(); + float distance = Vector3.Distance(position, bossPos); + float optimalDistance = (minSpawnDistance + maxSpawnDistance) * 0.5f; + float distanceScore = 1f - Mathf.Abs(distance - optimalDistance) / maxSpawnDistance; + score += distanceScore; + + score += Random.Range(-0.1f, 0.1f); + + return score; + } + + /// + /// Spawns crystal at given position + /// + private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour) + { + Quaternion rotation = Quaternion.identity; + if (playerTransform != null) + { + Vector3 lookDirection = (playerTransform.position - position).normalized; + lookDirection.y = 0; + if (lookDirection != Vector3.zero) + { + rotation = Quaternion.LookRotation(lookDirection); + } + } + + spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation); + + var shooterAI = spawnedCrystal.GetComponent(); + if (shooterAI == null) + { + Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!"); + } + else + { + if (playerTransform != null) + { + shooterAI.SetTarget(playerTransform); + } + } + } + + /// + /// Draws gizmos in Scene View for debugging + /// + private void OnDrawGizmosSelected() + { + if (!showGizmos) return; + + Vector3 pos = new Vector3(); + + // Spawn ring + Gizmos.color = Color.green; + DrawWireCircle(pos, minSpawnDistance); + Gizmos.color = Color.red; + DrawWireCircle(pos, maxSpawnDistance); + + // Obstacle check radius + Gizmos.color = Color.yellow; + Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius); + } + + /// + /// Helper method for drawing circles + /// + private void DrawWireCircle(Vector3 center, float radius) + { + int segments = 32; + float angle = 0f; + Vector3 prevPoint = center + new Vector3(radius, 0, 0); + + for (int i = 1; i <= segments; i++) + { + 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; + } + } + } +} \ No newline at end of file diff --git a/Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta b/Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta new file mode 100644 index 000000000..93a534a26 --- /dev/null +++ b/Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5fcb700ea476bed44becf27143de31bd \ No newline at end of file