Demon boss part (1/3) Invector FSM nodes

This commit is contained in:
Szymon Miś
2025-08-11 13:34:13 +02:00
parent 46b6c5dd9a
commit eaa10c4c11
13 changed files with 1223 additions and 0 deletions

8
Assets/AI/Demon.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 67c253388a8e1594783ff96204c292f8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,205 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace DemonBoss.Magic
{
/// <summary>
/// Decision node checking cooldown for different boss abilities
/// Stores Time.time in FSM timers and checks if required cooldown time has passed
/// </summary>
[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;
/// <summary>
/// Main method checking if ability is available
/// </summary>
/// <returns>True if cooldown has passed and ability can be used</returns>
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;
}
/// <summary>
/// Sets cooldown for ability - call this after using ability
/// </summary>
public void SetCooldown(vIFSMBehaviourController fsmBehaviour)
{
SetCooldown(fsmBehaviour, cooldownTime);
}
/// <summary>
/// Sets cooldown with custom time
/// </summary>
/// <param name="fsmBehaviour">FSM behaviour reference</param>
/// <param name="customCooldownTime">Custom cooldown time</param>
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");
}
}
/// <summary>
/// Resets cooldown - ability becomes immediately available
/// </summary>
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}");
}
/// <summary>
/// Returns remaining cooldown time in seconds
/// </summary>
/// <returns>Remaining cooldown time (0 if available)</returns>
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);
}
/// <summary>
/// Checks if ability is available without running main Decision logic
/// </summary>
/// <returns>True if ability is available</returns>
public bool IsAvailable(vIFSMBehaviourController fsmBehaviour)
{
return GetRemainingCooldown(fsmBehaviour) <= 0f;
}
/// <summary>
/// Returns cooldown progress percentage (0-1)
/// </summary>
/// <returns>Progress percentage: 0 = just used, 1 = fully recharged</returns>
public float GetCooldownProgress(vIFSMBehaviourController fsmBehaviour)
{
if (cooldownTime <= 0f) return 1f;
float remainingTime = GetRemainingCooldown(fsmBehaviour);
return 1f - (remainingTime / cooldownTime);
}
/// <summary>
/// Helper method for setting cooldown from external code (e.g. from StateAction)
/// </summary>
/// <param name="fsmBehaviour">FSM reference</param>
/// <param name="key">Ability key</param>
/// <param name="cooldown">Cooldown time</param>
public static void SetCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
{
if (fsmBehaviour == null) return;
string timerKey = "cooldown_" + key;
fsmBehaviour.SetTimer(timerKey, Time.time);
}
/// <summary>
/// Helper method for checking cooldown from external code
/// </summary>
/// <param name="fsmBehaviour">FSM reference</param>
/// <param name="key">Ability key</param>
/// <param name="cooldown">Cooldown time</param>
/// <returns>True if available</returns>
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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: af82d1d082ce4b9478d420f8ca1e72c2

View File

@@ -0,0 +1,105 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace DemonBoss.Magic
{
/// <summary>
/// Decision checking if target has clear sky above (for meteor)
/// </summary>
[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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a60b21a6b7b97264b856431f5eb253b1

View File

@@ -0,0 +1,58 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace DemonBoss.Magic
{
/// <summary>
/// Decision checking if target is far away (for Turret ability)
/// </summary>
[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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6df4a5087a0930d479908e8416bc8a2a

View File

@@ -0,0 +1,352 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool;
using System.Collections;
using UnityEngine;
namespace DemonBoss.Magic
{
/// <summary>
/// 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
/// </summary>
[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;
/// <summary>
/// Main action execution method called by FSM
/// </summary>
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{
if (executionType == vFSMComponentExecutionType.OnStateEnter)
{
OnStateEnter(fsmBehaviour);
}
else if (executionType == vFSMComponentExecutionType.OnStateExit)
{
OnStateExit(fsmBehaviour);
}
}
/// <summary>
/// Called when entering state - starts meteor casting
/// </summary>
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
{
if (enableDebug) Debug.Log("[SA_CallMeteor] Starting meteor casting");
FindPlayer(fsmBehaviour);
var animator = fsmBehaviour.transform.GetComponent<Animator>();
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
{
animator.SetTrigger(animatorTrigger);
if (enableDebug) Debug.Log($"[SA_CallMeteor] Set trigger: {animatorTrigger}");
}
StartMeteorCast(fsmBehaviour);
}
/// <summary>
/// Called when exiting state - cleanup
/// </summary>
private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
{
if (enableDebug) Debug.Log("[SA_CallMeteor] Exiting meteor state");
meteorCasting = false;
}
/// <summary>
/// Finds player transform
/// </summary>
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!");
}
/// <summary>
/// Starts meteor casting process
/// </summary>
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<MonoBehaviour>();
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");
}
}
/// <summary>
/// Finds ground under player using raycast
/// </summary>
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;
}
/// <summary>
/// Spawns decal showing meteor impact location
/// </summary>
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}");
}
/// <summary>
/// Coroutine handling meteor casting process with delay
/// </summary>
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;
}
/// <summary>
/// Immediate meteor cast without coroutine (fallback)
/// </summary>
private void CastMeteorImmediate()
{
if (IsPathClearForMeteor(targetPosition))
{
SpawnMeteor(targetPosition);
}
CleanupDecal();
}
/// <summary>
/// Checks if path above target is clear for meteor
/// </summary>
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;
}
/// <summary>
/// Spawns meteor in air above target
/// </summary>
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<Rigidbody>();
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");
}
/// <summary>
/// Removes decal from map
/// </summary>
private void CleanupDecal()
{
if (spawnedDecal != null)
{
LeanPool.Despawn(spawnedDecal);
spawnedDecal = null;
if (enableDebug) Debug.Log("[SA_CallMeteor] Decal removed");
}
}
/// <summary>
/// Checks if meteor is currently being cast
/// </summary>
public bool IsCasting()
{
return meteorCasting;
}
/// <summary>
/// Returns meteor target position
/// </summary>
public Vector3 GetTargetPosition()
{
return targetPosition;
}
/// <summary>
/// Draws gizmos in Scene View for debugging
/// </summary>
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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6704e78bcf7b30a43a2d06d91e87d694

View File

@@ -0,0 +1,185 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool;
using UnityEngine;
namespace DemonBoss.Magic
{
/// <summary>
/// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds
/// During casting boss stands still, after completion returns to Combat
/// </summary>
[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;
/// <summary>
/// Main action execution method called by FSM
/// </summary>
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);
}
}
/// <summary>
/// Called when entering state - starts shield casting
/// </summary>
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<Animator>();
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;
}
/// <summary>
/// Called every frame during state duration
/// </summary>
private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour)
{
if (shieldActive && Time.time - shieldStartTime >= shieldDuration)
{
if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state");
FinishShield(fsmBehaviour);
}
}
/// <summary>
/// Called when exiting state - cleanup
/// </summary>
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");
}
}
/// <summary>
/// Spawns magical shield particle effect
/// </summary>
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);
}
}
/// <summary>
/// Finishes shield operation and transitions to next state
/// </summary>
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
}
/// <summary>
/// Cleans up spawned shield
/// </summary>
private void CleanupShield()
{
if (spawnedShield != null)
{
LeanPool.Despawn(spawnedShield);
spawnedShield = null;
if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned");
}
}
/// <summary>
/// Checks if shield is currently active
/// </summary>
public bool IsShieldActive()
{
return shieldActive;
}
/// <summary>
/// Returns remaining shield time
/// </summary>
public float GetRemainingShieldTime()
{
if (!shieldActive) return 0f;
return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c53af76a8a097a244a3002b8aa7b4ceb

View File

@@ -0,0 +1,298 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool;
using UnityEngine;
namespace DemonBoss.Magic
{
/// <summary>
/// StateAction for intelligent crystal turret spawning
/// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player
/// </summary>
[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;
/// <summary>
/// Main action execution method called by FSM
/// </summary>
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{
if (executionType == vFSMComponentExecutionType.OnStateEnter)
{
OnStateEnter(fsmBehaviour);
}
}
/// <summary>
/// Called when entering state - intelligently spawns crystal
/// </summary>
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
{
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn");
FindPlayer(fsmBehaviour);
var animator = fsmBehaviour.transform.GetComponent<Animator>();
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);
}
/// <summary>
/// Finds player transform
/// </summary>
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!");
}
/// <summary>
/// Intelligently spawns crystal in optimal position
/// </summary>
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");
}
}
/// <summary>
/// Checks if position is valid (no obstacles, has ground)
/// </summary>
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;
}
/// <summary>
/// Evaluates position quality (higher score = better position)
/// </summary>
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;
}
/// <summary>
/// Spawns crystal at given position
/// </summary>
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<CrystalShooterAI>();
if (shooterAI == null)
{
Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
}
else
{
if (playerTransform != null)
{
shooterAI.SetTarget(playerTransform);
}
}
}
/// <summary>
/// Draws gizmos in Scene View for debugging
/// </summary>
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);
}
/// <summary>
/// Helper method for drawing circles
/// </summary>
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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5fcb700ea476bed44becf27143de31bd