Merge branch 'NewStory' of http://185.56.209.148/beyond/beyond into NewStory

This commit is contained in:
2025-11-20 20:52:04 +01:00
104 changed files with 3175 additions and 1049 deletions

View File

@@ -0,0 +1,327 @@
using Invector;
using Invector.vCharacterController;
using Lean.Pool;
using UnityEngine;
namespace ArcherEnemy
{
/// <summary>
/// Arrow projectile shot by archer enemies
/// Flies in straight line with gravity and deals damage on hit
/// </summary>
public class ArcherProjectile : MonoBehaviour
{
#region Configuration
[Header("Movement")]
[Tooltip("Initial velocity of the arrow (m/s)")]
public float initialSpeed = 30f;
[Tooltip("Gravity multiplier (higher = more arc)")]
public float gravityMultiplier = 1f;
[Tooltip("Max lifetime before auto-despawn (seconds)")]
public float maxLifetime = 10f;
[Header("Damage")]
[Tooltip("Damage dealt on hit")]
public int damage = 15;
[Tooltip("Knockback force")]
public float knockbackForce = 5f;
[Tooltip("Layers that can be hit")]
public LayerMask hitLayers = -1;
[Header("Effects")]
[Tooltip("Impact VFX prefab")]
public GameObject impactVFXPrefab;
[Tooltip("Trail renderer (optional)")]
public TrailRenderer trail;
[Header("Audio")]
[Tooltip("Impact sound")]
public AudioClip impactSound;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
[Tooltip("Show trajectory gizmos")]
public bool showGizmos = true;
#endregion Configuration
#region Runtime State
private Vector3 velocity;
private float lifetime = 0f;
private bool hasHit = false;
private AudioSource audioSource;
#endregion Runtime State
#region Unity Lifecycle
private void Awake()
{
if (impactSound != null)
{
audioSource = GetComponent<AudioSource>();
if (audioSource == null)
{
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialBlend = 1f; // 3D sound
}
}
}
private void OnEnable()
{
ResetState();
// Set initial velocity in forward direction
velocity = transform.forward * initialSpeed;
if (enableDebug)
Debug.Log($"[ArcherProjectile] Spawned at {transform.position}, velocity: {velocity}");
}
private void Update()
{
if (hasHit) return;
lifetime += Time.deltaTime;
// Check lifetime
if (lifetime >= maxLifetime)
{
if (enableDebug) Debug.Log("[ArcherProjectile] Lifetime expired");
Despawn();
return;
}
// Apply gravity
velocity += Physics.gravity * gravityMultiplier * Time.deltaTime;
// Calculate movement step
Vector3 moveStep = velocity * Time.deltaTime;
Vector3 newPosition = transform.position + moveStep;
// Raycast for collision detection
if (Physics.Raycast(transform.position, moveStep.normalized, out RaycastHit hit,
moveStep.magnitude, hitLayers, QueryTriggerInteraction.Ignore))
{
OnHit(hit);
return;
}
// Move arrow
transform.position = newPosition;
// Rotate arrow to face movement direction
if (velocity.sqrMagnitude > 0.001f)
{
transform.rotation = Quaternion.LookRotation(velocity.normalized);
}
}
#endregion Unity Lifecycle
#region Collision & Damage
private void OnHit(RaycastHit hit)
{
if (hasHit) return;
hasHit = true;
if (enableDebug)
Debug.Log($"[ArcherProjectile] Hit: {hit.collider.name} at {hit.point}");
// Position arrow at impact point
transform.position = hit.point;
transform.rotation = Quaternion.LookRotation(hit.normal);
// Try to deal damage
DealDamage(hit.collider, hit.point, hit.normal);
// Spawn impact VFX
SpawnImpactVFX(hit.point, hit.normal);
// Play impact sound
PlayImpactSound();
// Stick arrow to surface or despawn
StickToSurface(hit);
}
private void DealDamage(Collider targetCollider, Vector3 hitPoint, Vector3 hitNormal)
{
// Calculate hit direction (opposite of normal for knockback)
Vector3 hitDirection = -hitNormal;
if (velocity.sqrMagnitude > 0.001f)
{
hitDirection = velocity.normalized;
}
// Create damage info
vDamage damageInfo = new vDamage(damage)
{
sender = transform,
hitPosition = hitPoint
};
if (knockbackForce > 0f)
{
damageInfo.force = hitDirection * knockbackForce;
}
bool damageDealt = false;
// Try vIDamageReceiver
var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>() ??
targetCollider.GetComponentInParent<vIDamageReceiver>();
if (damageReceiver != null)
{
damageReceiver.TakeDamage(damageInfo);
damageDealt = true;
if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via vIDamageReceiver");
}
// Fallback to vHealthController
if (!damageDealt)
{
var healthController = targetCollider.GetComponent<vHealthController>() ??
targetCollider.GetComponentInParent<vHealthController>();
if (healthController != null)
{
healthController.TakeDamage(damageInfo);
damageDealt = true;
if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via vHealthController");
}
}
// Fallback to vThirdPersonController
if (!damageDealt)
{
var tpc = targetCollider.GetComponent<vThirdPersonController>() ??
targetCollider.GetComponentInParent<vThirdPersonController>();
if (tpc != null)
{
// Handle Beyond variant
if (tpc is Beyond.bThirdPersonController beyond)
{
if (!beyond.GodMode && !beyond.isImmortal)
{
tpc.TakeDamage(damageInfo);
damageDealt = true;
if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via bThirdPersonController");
}
else
{
if (enableDebug) Debug.Log("[ArcherProjectile] Target is immortal - no damage");
}
}
else
{
tpc.TakeDamage(damageInfo);
damageDealt = true;
if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via vThirdPersonController");
}
}
}
if (!damageDealt && enableDebug)
{
Debug.Log("[ArcherProjectile] No damage dealt - no valid receiver found");
}
}
#endregion Collision & Damage
#region Effects
private void SpawnImpactVFX(Vector3 position, Vector3 normal)
{
if (impactVFXPrefab == null) return;
Quaternion rotation = Quaternion.LookRotation(normal);
GameObject vfx = LeanPool.Spawn(impactVFXPrefab, position, rotation);
LeanPool.Despawn(vfx, 3f);
if (enableDebug) Debug.Log("[ArcherProjectile] Impact VFX spawned");
}
private void PlayImpactSound()
{
if (audioSource != null && impactSound != null)
{
audioSource.PlayOneShot(impactSound);
}
}
#endregion Effects
#region Arrow Sticking
private void StickToSurface(RaycastHit hit)
{
// Option 1: Parent arrow to hit object (if it has rigidbody, it will move with it)
// Option 2: Just despawn after short delay
// For now, despawn after brief delay to show impact
LeanPool.Despawn(gameObject, 0.1f);
}
#endregion Arrow Sticking
#region Pooling
private void ResetState()
{
hasHit = false;
lifetime = 0f;
velocity = Vector3.zero;
// Reset trail if present
if (trail != null)
{
trail.Clear();
}
}
private void Despawn()
{
if (enableDebug) Debug.Log("[ArcherProjectile] Despawning");
LeanPool.Despawn(gameObject);
}
#endregion Pooling
#region Gizmos
#if UNITY_EDITOR
private void OnDrawGizmos()
{
if (!showGizmos || !Application.isPlaying) return;
// Draw velocity vector
Gizmos.color = Color.yellow;
Gizmos.DrawRay(transform.position, velocity.normalized * 2f);
// Draw forward direction
Gizmos.color = Color.blue;
Gizmos.DrawRay(transform.position, transform.forward * 1f);
}
#endif
#endregion Gizmos
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9c5e129708014c64981ec3d5665de1b4

View File

@@ -0,0 +1,430 @@
using Lean.Pool;
using UnityEngine;
namespace ArcherEnemy
{
/// <summary>
/// AI component for archer enemy that shoots arrows at target
/// Should be attached to archer enemy prefab
/// </summary>
public class ArcherShootingAI : MonoBehaviour
{
[Header("References")]
[Tooltip("Transform point from which arrows are shot (usually hand bone or weapon tip)")]
public Transform shootPoint;
[Tooltip("Arrow prefab with ArcherProjectile component")]
public GameObject arrowPrefab;
[Tooltip("Animator for triggering shoot animation")]
public Animator animator;
[Header("Targeting")]
[Tooltip("Auto-find player on start")]
public bool autoFindPlayer = true;
[Tooltip("Player tag to search for")]
public string playerTag = "Player";
[Tooltip("Height offset for aiming (aim at chest/head)")]
public float targetHeightOffset = 1.2f;
[Header("Shooting Parameters")]
[Tooltip("Minimum distance to shoot from")]
public float minShootDistance = 8f;
[Tooltip("Maximum distance to shoot from")]
public float maxShootDistance = 25f;
[Tooltip("Time between shots (seconds)")]
public float shootCooldown = 2f;
[Tooltip("Arrow launch speed (m/s)")]
public float arrowSpeed = 30f;
[Tooltip("How much to lead the target (predict movement)")]
[Range(0f, 1f)]
public float leadTargetAmount = 0.5f;
[Header("Animation")]
[Tooltip("Animator trigger parameter for shooting")]
public string shootTriggerName = "Shoot";
[Tooltip("Delay after animation starts before spawning arrow")]
public float shootAnimationDelay = 0.3f;
[Header("Aiming")]
[Tooltip("How quickly archer rotates to face target (degrees/sec)")]
public float turnSpeed = 180f;
[Tooltip("Angle tolerance for shooting (degrees)")]
public float aimTolerance = 15f;
[Header("Effects")]
[Tooltip("Muzzle flash effect at shoot point")]
public GameObject muzzleFlashPrefab;
[Tooltip("Shoot sound")]
public AudioClip shootSound;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
[Tooltip("Show aiming gizmos")]
public bool showGizmos = true;
#region Private Fields
private Transform target;
private float lastShootTime = -999f;
private bool isAiming = false;
private AudioSource audioSource;
private Vector3 lastKnownTargetVelocity;
private Vector3 lastTargetPosition;
#endregion Private Fields
#region Unity Lifecycle
private void Awake()
{
// Setup audio source
if (shootSound != null)
{
audioSource = GetComponent<AudioSource>();
if (audioSource == null)
{
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialBlend = 1f;
}
}
// Find animator if not assigned
if (animator == null)
{
animator = GetComponent<Animator>();
}
// Find shoot point if not assigned
if (shootPoint == null)
{
// Try to find "ShootPoint" child transform
Transform shootPointChild = transform.Find("ShootPoint");
shootPoint = shootPointChild != null ? shootPointChild : transform;
}
}
private void Start()
{
if (autoFindPlayer)
{
FindPlayer();
}
}
private void Update()
{
if (target != null && isAiming)
{
RotateTowardsTarget();
UpdateTargetVelocity();
}
}
#endregion Unity Lifecycle
#region Target Management
/// <summary>
/// Finds player by tag
/// </summary>
private void FindPlayer()
{
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
if (player != null)
{
SetTarget(player.transform);
}
}
/// <summary>
/// Sets the target to shoot at
/// </summary>
public void SetTarget(Transform newTarget)
{
target = newTarget;
if (target != null)
{
lastTargetPosition = target.position;
}
}
/// <summary>
/// Updates target velocity for prediction
/// </summary>
private void UpdateTargetVelocity()
{
if (target == null) return;
Vector3 currentPosition = target.position;
lastKnownTargetVelocity = (currentPosition - lastTargetPosition) / Time.deltaTime;
lastTargetPosition = currentPosition;
}
#endregion Target Management
#region Shooting Logic
/// <summary>
/// Checks if archer can shoot at target
/// </summary>
public bool CanShoot()
{
if (target == null) return false;
float distance = Vector3.Distance(transform.position, target.position);
// Check distance range
if (distance < minShootDistance || distance > maxShootDistance)
{
return false;
}
// Check cooldown
if (Time.time < lastShootTime + shootCooldown)
{
return false;
}
// Check if facing target (within tolerance)
Vector3 directionToTarget = (target.position - transform.position).normalized;
float angleToTarget = Vector3.Angle(transform.forward, directionToTarget);
if (angleToTarget > aimTolerance)
{
return false;
}
return true;
}
/// <summary>
/// Initiates shooting sequence
/// </summary>
public void StartShooting()
{
if (!CanShoot())
{
if (enableDebug) Debug.Log("[ArcherShootingAI] Cannot shoot - conditions not met");
return;
}
isAiming = true;
// Trigger animation
if (animator != null && !string.IsNullOrEmpty(shootTriggerName))
{
animator.SetTrigger(shootTriggerName);
}
// Schedule arrow spawn after animation delay
Invoke(nameof(SpawnArrow), shootAnimationDelay);
lastShootTime = Time.time;
if (enableDebug) Debug.Log($"[ArcherShootingAI] Started shooting at {target.name}");
}
/// <summary>
/// Spawns arrow projectile
/// </summary>
private void SpawnArrow()
{
if (arrowPrefab == null)
{
Debug.LogError("[ArcherShootingAI] Arrow prefab not assigned!");
return;
}
if (shootPoint == null)
{
Debug.LogError("[ArcherShootingAI] Shoot point not assigned!");
return;
}
// Calculate aim point with prediction
Vector3 aimPoint = CalculateAimPoint();
// Calculate shoot direction
Vector3 shootDirection = (aimPoint - shootPoint.position).normalized;
// Calculate rotation for arrow
Quaternion arrowRotation = Quaternion.LookRotation(shootDirection);
// Spawn arrow
GameObject arrow = LeanPool.Spawn(arrowPrefab, shootPoint.position, arrowRotation);
// Configure arrow projectile
var projectile = arrow.GetComponent<ArcherProjectile>();
if (projectile != null)
{
projectile.initialSpeed = arrowSpeed;
}
// Play effects
PlayShootEffects();
if (enableDebug)
Debug.Log($"[ArcherShootingAI] Arrow spawned, direction: {shootDirection}");
}
/// <summary>
/// Calculates aim point with target prediction
/// </summary>
private Vector3 CalculateAimPoint()
{
if (target == null) return shootPoint.position + transform.forward * 10f;
// Base aim point
Vector3 targetPosition = target.position + Vector3.up * targetHeightOffset;
// Add prediction based on target velocity
if (leadTargetAmount > 0f && lastKnownTargetVelocity.sqrMagnitude > 0.1f)
{
// Estimate time to target
float distance = Vector3.Distance(shootPoint.position, targetPosition);
float timeToTarget = distance / arrowSpeed;
// Predict future position
Vector3 predictedOffset = lastKnownTargetVelocity * timeToTarget * leadTargetAmount;
targetPosition += predictedOffset;
}
return targetPosition;
}
/// <summary>
/// Stops shooting sequence
/// </summary>
public void StopShooting()
{
isAiming = false;
CancelInvoke(nameof(SpawnArrow));
}
#endregion Shooting Logic
#region Effects
private void PlayShootEffects()
{
// Spawn muzzle flash
if (muzzleFlashPrefab != null && shootPoint != null)
{
GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, shootPoint.position, shootPoint.rotation);
LeanPool.Despawn(flash, 2f);
}
// Play shoot sound
if (audioSource != null && shootSound != null)
{
audioSource.PlayOneShot(shootSound);
}
}
#endregion Effects
#region Rotation
/// <summary>
/// Smoothly rotates archer to face target
/// </summary>
private void RotateTowardsTarget()
{
if (target == null) return;
Vector3 directionToTarget = (target.position - transform.position);
directionToTarget.y = 0f; // Keep rotation on Y axis only
if (directionToTarget.sqrMagnitude > 0.001f)
{
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
transform.rotation = Quaternion.RotateTowards(
transform.rotation,
targetRotation,
turnSpeed * Time.deltaTime
);
}
}
/// <summary>
/// Checks if archer is facing target within tolerance
/// </summary>
public bool IsFacingTarget()
{
if (target == null) return false;
Vector3 directionToTarget = (target.position - transform.position).normalized;
float angle = Vector3.Angle(transform.forward, directionToTarget);
return angle <= aimTolerance;
}
#endregion Rotation
#region Public Query Methods
public Transform GetTarget() => target;
public bool IsAiming() => isAiming;
public float GetTimeSinceLastShot() => Time.time - lastShootTime;
public float GetDistanceToTarget() => target != null ? Vector3.Distance(transform.position, target.position) : float.MaxValue;
public bool IsInShootingRange() => GetDistanceToTarget() >= minShootDistance && GetDistanceToTarget() <= maxShootDistance;
#endregion Public Query Methods
#region Gizmos
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (!showGizmos) return;
// Draw shooting range
Gizmos.color = Color.yellow;
UnityEditor.Handles.color = new Color(1f, 1f, 0f, 0.1f);
UnityEditor.Handles.DrawSolidDisc(transform.position, Vector3.up, minShootDistance);
Gizmos.color = Color.green;
UnityEditor.Handles.color = new Color(0f, 1f, 0f, 0.1f);
UnityEditor.Handles.DrawSolidDisc(transform.position, Vector3.up, maxShootDistance);
// Draw shoot point
if (shootPoint != null)
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(shootPoint.position, 0.1f);
Gizmos.DrawLine(shootPoint.position, shootPoint.position + shootPoint.forward * 2f);
}
// Draw aim line to target
if (target != null && Application.isPlaying)
{
Vector3 aimPoint = CalculateAimPoint();
Gizmos.color = Color.cyan;
Gizmos.DrawLine(shootPoint != null ? shootPoint.position : transform.position, aimPoint);
Gizmos.DrawWireSphere(aimPoint, 0.2f);
}
}
#endif
#endregion Gizmos
}
}

View File

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

View File

@@ -0,0 +1,66 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace ArcherEnemy
{
/// <summary>
/// Decision checking if archer can shoot
/// Checks cooldown, aiming, and射ing AI component readiness
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/Archer/Can Shoot")]
public class DEC_CanShoot : vStateDecision
{
public override string categoryName => "Archer/Combat";
public override string defaultName => "Can Shoot";
[Header("Configuration")]
[Tooltip("Check if archer is facing target within tolerance")]
public bool checkFacingTarget = true;
[Tooltip("Angle tolerance for shooting (degrees)")]
public float aimTolerance = 20f;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
{
// Get ArcherShootingAI component
var shootingAI = fsmBehaviour.gameObject.GetComponent<ArcherShootingAI>();
if (shootingAI == null)
{
if (enableDebug) Debug.LogWarning("[DEC_CanShoot] No ArcherShootingAI component found!");
return false;
}
// Use the shooting AI's CanShoot method
bool canShoot = shootingAI.CanShoot();
// Optional: additional facing check
if (canShoot && checkFacingTarget)
{
Transform target = shootingAI.GetTarget();
if (target != null)
{
Vector3 directionToTarget = (target.position - fsmBehaviour.transform.position).normalized;
float angle = Vector3.Angle(fsmBehaviour.transform.forward, directionToTarget);
if (angle > aimTolerance)
{
canShoot = false;
if (enableDebug) Debug.Log($"[DEC_CanShoot] Not facing target: {angle:F1}° (tolerance: {aimTolerance}°)");
}
}
}
if (enableDebug)
{
Debug.Log($"[DEC_CanShoot] {(canShoot ? "CAN SHOOT" : "CANNOT SHOOT")}");
}
return canShoot;
}
}
}

View File

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

View File

@@ -0,0 +1,112 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace ArcherEnemy
{
/// <summary>
/// Decision checking if player is in optimal shooting range
/// Returns true when player is far enough but not too far
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/Archer/Player In Shoot Range")]
public class DEC_PlayerInShootRange : vStateDecision
{
public override string categoryName => "Archer/Combat";
public override string defaultName => "Player In Shoot Range";
[Header("Range Configuration")]
[Tooltip("Minimum safe distance to start shooting")]
public float minShootDistance = 8f;
[Tooltip("Maximum effective shooting distance")]
public float maxShootDistance = 25f;
[Tooltip("Also check if we have clear line of sight")]
public bool checkLineOfSight = true;
[Tooltip("Layers that block line of sight")]
public LayerMask obstacleMask = -1;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
[Tooltip("Show range gizmos")]
public bool showGizmos = true;
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
{
Transform target = GetTarget(fsmBehaviour);
if (target == null)
{
if (enableDebug) Debug.Log("[DEC_PlayerInShootRange] No target found");
return false;
}
Vector3 archerPos = fsmBehaviour.transform.position;
Vector3 targetPos = target.position;
float distance = Vector3.Distance(archerPos, targetPos);
// Check distance range
bool inRange = distance >= minShootDistance && distance <= maxShootDistance;
if (!inRange)
{
if (enableDebug)
{
if (distance < minShootDistance)
Debug.Log($"[DEC_PlayerInShootRange] Player too close: {distance:F1}m (min: {minShootDistance})");
else
Debug.Log($"[DEC_PlayerInShootRange] Player too far: {distance:F1}m (max: {maxShootDistance})");
}
return false;
}
// Check line of sight if enabled
if (checkLineOfSight)
{
Vector3 shootPoint = archerPos + Vector3.up * 1.5f; // Approximate chest height
Vector3 targetPoint = targetPos + Vector3.up * 1f;
Vector3 direction = targetPoint - shootPoint;
if (Physics.Raycast(shootPoint, direction.normalized, distance, obstacleMask, QueryTriggerInteraction.Ignore))
{
if (enableDebug) Debug.Log("[DEC_PlayerInShootRange] Line of sight blocked");
return false;
}
}
if (enableDebug)
{
Debug.Log($"[DEC_PlayerInShootRange] IN RANGE: {distance:F1}m");
}
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;
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (!showGizmos) return;
// This would need to be drawn from the archer's position in-game
// For now, just a visual reference
}
#endif
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6577855f02c3b2649b6a787e88baea8b

View File

@@ -0,0 +1,80 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace ArcherEnemy
{
/// <summary>
/// Decision checking if player is too close to the archer
/// Used to trigger retreat/flee behavior
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/Archer/Player Too Close")]
public class DEC_PlayerTooClose : vStateDecision
{
public override string categoryName => "Archer/Combat";
public override string defaultName => "Player Too Close";
[Header("Distance Configuration")]
[Tooltip("Distance below which player is considered too close")]
public float dangerDistance = 6f;
[Tooltip("Optional: check only if player is approaching (not retreating)")]
public bool checkIfApproaching = false;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
private Vector3 lastPlayerPosition;
private bool hasLastPosition = false;
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
{
Transform target = GetTarget(fsmBehaviour);
if (target == null)
{
if (enableDebug) Debug.Log("[DEC_PlayerTooClose] No target found");
hasLastPosition = false;
return false;
}
float distance = Vector3.Distance(fsmBehaviour.transform.position, target.position);
bool tooClose = distance < dangerDistance;
// Optional: check if player is approaching
if (checkIfApproaching && hasLastPosition)
{
float previousDistance = Vector3.Distance(fsmBehaviour.transform.position, lastPlayerPosition);
bool isApproaching = distance < previousDistance;
if (!isApproaching)
{
tooClose = false; // Player is moving away, not a threat
}
}
// Store current position for next frame
lastPlayerPosition = target.position;
hasLastPosition = true;
if (enableDebug)
{
Debug.Log($"[DEC_PlayerTooClose] Distance: {distance:F1}m - {(tooClose ? "TOO CLOSE" : "SAFE")}");
}
return tooClose;
}
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: 3508c9b7b0af0d540a18d406403cff4d

View File

@@ -0,0 +1,269 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace ArcherEnemy
{
/// <summary>
/// State action that makes archer flee/retreat from player
/// Moves archer away from player while trying to maintain shooting distance
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Actions/Archer/Flee From Player")]
public class SA_FleeFromPlayer : vStateAction
{
public override string categoryName => "Archer/Combat";
public override string defaultName => "Flee From Player";
[Header("Flee Configuration")]
[Tooltip("Desired safe distance from player")]
public float safeDistance = 12f;
[Tooltip("How far to look ahead when fleeing")]
public float fleeDistance = 5f;
[Tooltip("Check for obstacles when fleeing")]
public bool avoidObstacles = true;
[Tooltip("Layers considered as obstacles")]
public LayerMask obstacleMask = -1;
[Tooltip("Number of directions to try when finding flee path")]
public int directionSamples = 8;
[Header("Movement")]
[Tooltip("Movement speed multiplier (uses AI's speed)")]
[Range(0.5f, 2f)]
public float speedMultiplier = 1.2f;
[Tooltip("Make archer sprint while fleeing")]
public bool useSprint = true;
[Header("Rotation")]
[Tooltip("Keep facing player while backing away")]
public bool facePlayer = true;
[Tooltip("Rotation speed when facing player (degrees/sec)")]
public float turnSpeed = 180f;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
[Tooltip("Show flee direction gizmos")]
public bool showGizmos = true;
private Vector3 currentFleeDirection;
private Transform currentTarget;
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{
if (executionType == vFSMComponentExecutionType.OnStateEnter)
{
OnEnter(fsmBehaviour);
}
else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
{
OnUpdate(fsmBehaviour);
}
else if (executionType == vFSMComponentExecutionType.OnStateExit)
{
OnExit(fsmBehaviour);
}
}
private void OnEnter(vIFSMBehaviourController fsmBehaviour)
{
currentTarget = GetTarget(fsmBehaviour);
if (currentTarget == null)
{
if (enableDebug) Debug.LogWarning("[SA_FleeFromPlayer] No target found!");
return;
}
// Calculate initial flee direction
currentFleeDirection = CalculateFleeDirection(fsmBehaviour);
// Set AI to sprint if enabled
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null && useSprint)
{
}
if (enableDebug) Debug.Log("[SA_FleeFromPlayer] Started fleeing from player");
}
private void OnUpdate(vIFSMBehaviourController fsmBehaviour)
{
if (currentTarget == null)
{
currentTarget = GetTarget(fsmBehaviour);
if (currentTarget == null) return;
}
// Recalculate flee direction periodically
currentFleeDirection = CalculateFleeDirection(fsmBehaviour);
// Move in flee direction
MoveInDirection(fsmBehaviour, currentFleeDirection);
// Face player while fleeing if enabled
if (facePlayer)
{
RotateTowardsPlayer(fsmBehaviour);
}
}
private void OnExit(vIFSMBehaviourController fsmBehaviour)
{
// Reset AI speed
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null)
{
}
if (enableDebug) Debug.Log("[SA_FleeFromPlayer] Stopped fleeing");
}
/// <summary>
/// Calculates the best direction to flee
/// </summary>
private Vector3 CalculateFleeDirection(vIFSMBehaviourController fsmBehaviour)
{
Vector3 archerPos = fsmBehaviour.transform.position;
Vector3 playerPos = currentTarget.position;
// Basic flee direction: away from player
Vector3 awayFromPlayer = (archerPos - playerPos).normalized;
// If not avoiding obstacles, return simple direction
if (!avoidObstacles)
{
return awayFromPlayer;
}
// Try to find clear path
Vector3 bestDirection = awayFromPlayer;
float bestScore = EvaluateDirection(archerPos, awayFromPlayer, playerPos);
// Sample multiple directions around the flee vector
for (int i = 0; i < directionSamples; i++)
{
float angle = (360f / directionSamples) * i;
Vector3 testDirection = Quaternion.Euler(0f, angle, 0f) * awayFromPlayer;
float score = EvaluateDirection(archerPos, testDirection, playerPos);
if (score > bestScore)
{
bestScore = score;
bestDirection = testDirection;
}
}
return bestDirection;
}
/// <summary>
/// Evaluates how good a flee direction is (higher = better)
/// </summary>
private float EvaluateDirection(Vector3 from, Vector3 direction, Vector3 playerPos)
{
float score = 0f;
// Check if path is clear
Ray ray = new Ray(from + Vector3.up * 0.5f, direction);
bool isBlocked = Physics.Raycast(ray, fleeDistance, obstacleMask, QueryTriggerInteraction.Ignore);
if (!isBlocked)
{
score += 10f; // Big bonus for clear path
}
// Prefer directions that move away from player
Vector3 awayFromPlayer = (from - playerPos).normalized;
float alignment = Vector3.Dot(direction, awayFromPlayer);
score += alignment * 5f;
// Check destination height (avoid running off cliffs)
Vector3 destination = from + direction * fleeDistance;
if (Physics.Raycast(destination + Vector3.up * 2f, Vector3.down, 5f, obstacleMask))
{
score += 3f; // Bonus for having ground
}
return score;
}
/// <summary>
/// Moves archer in specified direction
/// </summary>
private void MoveInDirection(vIFSMBehaviourController fsmBehaviour, Vector3 direction)
{
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController == null) return;
// Calculate destination point
Vector3 destination = fsmBehaviour.transform.position + direction * fleeDistance;
// Use Invector's AI movement
aiController.MoveTo(destination);
// Apply speed multiplier
var motor = aiController as Invector.vCharacterController.AI.vSimpleMeleeAI_Controller;
if (motor != null)
{
// This would need to be adapted to your specific Invector version
// The goal is to make the archer move faster while fleeing
}
}
/// <summary>
/// Rotates archer to face player while backing away
/// </summary>
private void RotateTowardsPlayer(vIFSMBehaviourController fsmBehaviour)
{
if (currentTarget == null) return;
Vector3 directionToPlayer = (currentTarget.position - fsmBehaviour.transform.position);
directionToPlayer.y = 0f;
if (directionToPlayer.sqrMagnitude > 0.001f)
{
Quaternion targetRotation = Quaternion.LookRotation(directionToPlayer);
fsmBehaviour.transform.rotation = Quaternion.RotateTowards(
fsmBehaviour.transform.rotation,
targetRotation,
turnSpeed * Time.deltaTime
);
}
}
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;
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (!showGizmos || !Application.isPlaying) return;
// Draw current flee direction
if (currentFleeDirection != Vector3.zero)
{
Gizmos.color = Color.red;
// This would need the archer's position to draw properly
}
}
#endif
}
}

View File

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

View File

@@ -0,0 +1,105 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace ArcherEnemy
{
/// <summary>
/// State action that makes archer shoot an arrow
/// Should be called in OnStateEnter or OnStateUpdate
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Actions/Archer/Shoot Arrow")]
public class SA_ShootArrow : vStateAction
{
public override string categoryName => "Archer/Combat";
public override string defaultName => "Shoot Arrow";
[Header("Configuration")]
[Tooltip("Shoot once per state enter, or continuously?")]
public bool shootOnce = true;
[Tooltip("Time between shots when shooting continuously")]
public float shootInterval = 2f;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
private float lastShootTime = -999f;
private bool hasShotThisState = false;
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{
if (executionType == vFSMComponentExecutionType.OnStateEnter)
{
OnEnter(fsmBehaviour);
}
else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
{
OnUpdate(fsmBehaviour);
}
else if (executionType == vFSMComponentExecutionType.OnStateExit)
{
OnExit(fsmBehaviour);
}
}
private void OnEnter(vIFSMBehaviourController fsmBehaviour)
{
hasShotThisState = false;
if (shootOnce)
{
TryShoot(fsmBehaviour);
}
}
private void OnUpdate(vIFSMBehaviourController fsmBehaviour)
{
if (shootOnce && hasShotThisState)
{
return; // Already shot once this state
}
// Check interval for continuous shooting
if (!shootOnce && Time.time >= lastShootTime + shootInterval)
{
TryShoot(fsmBehaviour);
}
}
private void OnExit(vIFSMBehaviourController fsmBehaviour)
{
// Stop any shooting sequence
var shootingAI = fsmBehaviour.gameObject.GetComponent<ArcherShootingAI>();
if (shootingAI != null)
{
shootingAI.StopShooting();
}
}
private void TryShoot(vIFSMBehaviourController fsmBehaviour)
{
var shootingAI = fsmBehaviour.gameObject.GetComponent<ArcherShootingAI>();
if (shootingAI == null)
{
if (enableDebug) Debug.LogError("[SA_ShootArrow] No ArcherShootingAI component found!");
return;
}
// Attempt to shoot
if (shootingAI.CanShoot())
{
shootingAI.StartShooting();
hasShotThisState = true;
lastShootTime = Time.time;
if (enableDebug) Debug.Log("[SA_ShootArrow] Shooting arrow");
}
else
{
if (enableDebug) Debug.Log("[SA_ShootArrow] Cannot shoot - conditions not met");
}
}
}
}

View File

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

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

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

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

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

View File

@@ -0,0 +1,105 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace DemonBoss.Summoner
{
/// <summary>
/// FSM Decision checking if summoner can spawn minions
/// Checks: not already spawning, minion count below max, cooldown passed
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/Summoner/Can Spawn Minions")]
public class DEC_CanSpawnMinions : vStateDecision
{
public override string categoryName => "Summoner";
public override string defaultName => "Can Spawn Minions";
[Header("Spawn Conditions")]
[Tooltip("Check if health is below threshold")]
public bool checkHealthThreshold = true;
[Tooltip("Check if minions are below max count")]
public bool checkMinionCount = true;
[Tooltip("Check if cooldown has passed")]
public bool checkCooldown = true;
[Tooltip("Cooldown between summon attempts (seconds)")]
public float cooldownTime = 15f;
[Header("Distance Check")]
[Tooltip("Only spawn if player is within this distance (0 = disabled)")]
public float maxDistanceToPlayer = 0f;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
private string cooldownKey = "SummonMinions";
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
{
var summoner = fsmBehaviour.gameObject.GetComponent<SummonerAI>();
if (summoner == null)
{
if (enableDebug) Debug.LogWarning("[DEC_CanSpawnMinions] No SummonerAI component found!");
return false;
}
// Check if already spawning
if (summoner.IsSpawning)
{
if (enableDebug) Debug.Log("[DEC_CanSpawnMinions] Already spawning - FALSE");
return false;
}
// Check minion count
if (checkMinionCount && !summoner.CanSpawnMinions)
{
if (enableDebug) Debug.Log($"[DEC_CanSpawnMinions] Max minions reached ({summoner.ActiveMinionCount}) - FALSE");
return false;
}
// Check health threshold
if (checkHealthThreshold && !summoner.ShouldSummonByHealth())
{
if (enableDebug) Debug.Log("[DEC_CanSpawnMinions] Health threshold not met - FALSE");
return false;
}
// Check distance to player
if (maxDistanceToPlayer > 0f)
{
float distance = summoner.GetDistanceToPlayer();
if (distance > maxDistanceToPlayer)
{
if (enableDebug) Debug.Log($"[DEC_CanSpawnMinions] Player too far ({distance:F1}m) - FALSE");
return false;
}
}
// Check cooldown
if (checkCooldown)
{
string timerKey = "cooldown_" + cooldownKey;
if (fsmBehaviour.HasTimer(timerKey))
{
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
float timeSinceLastUse = Time.time - lastUsedTime;
if (timeSinceLastUse < cooldownTime)
{
if (enableDebug) Debug.Log($"[DEC_CanSpawnMinions] On cooldown - {cooldownTime - timeSinceLastUse:F1}s remaining - FALSE");
return false;
}
}
// Set cooldown for next use
fsmBehaviour.SetTimer(timerKey, Time.time);
}
if (enableDebug) Debug.Log("[DEC_CanSpawnMinions] All conditions met - TRUE");
return true;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6932f86375a52954785f89df47a2d61e

View File

@@ -0,0 +1,16 @@
using UnityEngine;
public class DEC_CheckDistanceToPlayer : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 40326c8e0d34db64d9aec176f7d5bc97

View File

@@ -0,0 +1,16 @@
using UnityEngine;
public class DEC_HasActiveMinions : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3a537cd142b23fa4b8285f3d22cda409

View File

@@ -0,0 +1,74 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace DemonBoss.Summoner
{
/// <summary>
/// FSM Decision checking if summoner should engage in melee combat
/// Checks: player distance, minion status, combat settings
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/Summoner/Should Melee Attack")]
public class DEC_ShouldMeleeAttack : vStateDecision
{
public override string categoryName => "Summoner";
public override string defaultName => "Should Melee Attack";
[Header("Distance Configuration")]
[Tooltip("Minimum distance to engage melee")]
public float minMeleeDistance = 0f;
[Tooltip("Maximum distance to engage melee")]
public float maxMeleeDistance = 3f;
[Header("Behavior")]
[Tooltip("Attack even when minions are alive")]
public bool attackWithMinions = false;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
{
var summoner = fsmBehaviour.gameObject.GetComponent<SummonerAI>();
if (summoner == null)
{
if (enableDebug) Debug.LogWarning("[DEC_ShouldMeleeAttack] No SummonerAI component found!");
return false;
}
// Don't attack while spawning
if (summoner.IsSpawning)
{
if (enableDebug) Debug.Log("[DEC_ShouldMeleeAttack] Currently spawning - FALSE");
return false;
}
// Check if has minions and shouldn't attack with them
if (!attackWithMinions && summoner.HasActiveMinions)
{
if (enableDebug) Debug.Log($"[DEC_ShouldMeleeAttack] Has {summoner.ActiveMinionCount} minions and attackWithMinions=false - FALSE");
return false;
}
// Check distance to player
float distance = summoner.GetDistanceToPlayer();
bool inRange = distance >= minMeleeDistance && distance <= maxMeleeDistance;
if (enableDebug)
{
if (inRange)
{
Debug.Log($"[DEC_ShouldMeleeAttack] Player in melee range ({distance:F1}m) - TRUE");
}
else
{
Debug.Log($"[DEC_ShouldMeleeAttack] Player not in melee range ({distance:F1}m, need {minMeleeDistance:F1}-{maxMeleeDistance:F1}m) - FALSE");
}
}
return inRange;
}
}
}

View File

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

View File

@@ -0,0 +1,125 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine;
namespace DemonBoss.Summoner
{
/// <summary>
/// FSM State Action that triggers minion spawning
/// Calls SummonerAI.StartSpawning() on state enter
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Actions/Summoner/Spawn Minions")]
public class SA_SpawnMinions : vStateAction
{
public override string categoryName => "Summoner";
public override string defaultName => "Spawn Minions";
[Header("Animation")]
[Tooltip("Animator trigger parameter for summoning animation")]
public string summonTriggerName = "Summon";
[Tooltip("Animator bool for summoning state")]
public string summoningBoolName = "IsSummoning";
[Header("Behavior")]
[Tooltip("Wait for spawning to complete before allowing state exit")]
public bool waitForCompletion = true;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
private SummonerAI summoner;
private Animator animator;
private bool hasStartedSpawning = false;
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{
if (executionType == vFSMComponentExecutionType.OnStateEnter)
{
OnEnter(fsmBehaviour);
}
else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
{
OnUpdate(fsmBehaviour);
}
else if (executionType == vFSMComponentExecutionType.OnStateExit)
{
OnExit(fsmBehaviour);
}
}
private void OnEnter(vIFSMBehaviourController fsmBehaviour)
{
summoner = fsmBehaviour.gameObject.GetComponent<SummonerAI>();
animator = fsmBehaviour.gameObject.GetComponent<Animator>();
if (summoner == null)
{
Debug.LogError("[SA_SpawnMinions] No SummonerAI component found!");
return;
}
// Trigger animation
if (animator != null)
{
if (!string.IsNullOrEmpty(summonTriggerName))
{
animator.SetTrigger(summonTriggerName);
}
if (!string.IsNullOrEmpty(summoningBoolName))
{
animator.SetBool(summoningBoolName, true);
}
}
// Start spawning minions
summoner.StartSpawning();
hasStartedSpawning = true;
if (enableDebug) Debug.Log("[SA_SpawnMinions] Started spawning minions");
}
private void OnUpdate(vIFSMBehaviourController fsmBehaviour)
{
// If waiting for completion, keep state active until spawning is done
if (waitForCompletion && summoner != null && summoner.IsSpawning)
{
// State will continue until spawning is complete
if (enableDebug && Time.frameCount % 60 == 0) // Log once per second
{
Debug.Log("[SA_SpawnMinions] Waiting for spawning to complete...");
}
}
}
private void OnExit(vIFSMBehaviourController fsmBehaviour)
{
// Reset animation bool
if (animator != null && !string.IsNullOrEmpty(summoningBoolName))
{
animator.SetBool(summoningBoolName, false);
}
// If spawning was interrupted, stop it
if (summoner != null && summoner.IsSpawning)
{
summoner.StopSpawning();
if (enableDebug) Debug.Log("[SA_SpawnMinions] Spawning interrupted on state exit");
}
hasStartedSpawning = false;
if (enableDebug) Debug.Log("[SA_SpawnMinions] State exited");
}
/// <summary>
/// Check if spawning is complete (for FSM decision nodes)
/// </summary>
public bool IsSpawningComplete()
{
if (summoner == null) return true;
return hasStartedSpawning && !summoner.IsSpawning;
}
}
}

View File

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

View File

@@ -0,0 +1,359 @@
using Invector;
using Invector.vCharacterController.AI;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DemonBoss.Summoner
{
/// <summary>
/// Main AI controller for Summoner enemy
/// Manages spawned minions and provides combat state tracking
/// Attach to Summoner character along with vControlAI
/// </summary>
public class SummonerAI : MonoBehaviour
{
[Header("Minion Management")]
[Tooltip("Prefab of minion to spawn")]
public GameObject minionPrefab;
[Tooltip("Maximum number of minions alive at once")]
public int maxActiveMinions = 3;
[Tooltip("Distance from summoner to spawn minions")]
public float spawnRadius = 5f;
[Tooltip("Height offset for spawn position")]
public float spawnHeightOffset = 0f;
[Tooltip("Should minions look at summoner after spawn?")]
public bool minionsLookAtCenter = false;
[Header("Spawn Configuration")]
[Tooltip("Number of minions to spawn per summon action")]
public int minionsPerSummon = 3;
[Tooltip("Delay before first minion spawns")]
public float initialSpawnDelay = 0.5f;
[Tooltip("Time between spawning each minion")]
public float timeBetweenSpawns = 0.3f;
[Header("Combat Behavior")]
[Tooltip("Minimum distance to player before engaging in melee")]
public float meleeEngageDistance = 3f;
[Tooltip("Should summoner fight when minions are alive?")]
public bool fightWithMinions = false;
[Tooltip("Health percentage threshold to spawn minions (0-1)")]
[Range(0f, 1f)]
public float healthThresholdForSummon = 0.7f;
[Header("Targeting")]
[Tooltip("Tag to find player")]
public string playerTag = "Player";
[Header("Effects")]
[Tooltip("Particle effect at spawn location")]
public GameObject spawnEffectPrefab;
[Tooltip("Sound played when spawning minions")]
public AudioClip summonSound;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
[Tooltip("Show gizmos in Scene View")]
public bool showGizmos = true;
// Runtime state
private List<GameObject> activeMinions = new List<GameObject>();
private Transform playerTransform;
private AudioSource audioSource;
private vHealthController healthController;
private bool isSpawning = false;
private Coroutine spawnCoroutine;
// Public properties for FSM decisions
public bool IsSpawning => isSpawning;
public int ActiveMinionCount => activeMinions.Count;
public bool CanSpawnMinions => activeMinions.Count < maxActiveMinions && !isSpawning;
public bool HasActiveMinions => activeMinions.Count > 0;
private void Awake()
{
healthController = GetComponent<vHealthController>();
audioSource = GetComponent<AudioSource>();
if (audioSource == null && summonSound != null)
{
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialBlend = 1f;
}
}
private void Start()
{
FindPlayer();
}
private void Update()
{
CleanupDeadMinions();
}
/// <summary>
/// Find player by tag
/// </summary>
private void FindPlayer()
{
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
if (player != null)
{
playerTransform = player.transform;
if (enableDebug) Debug.Log("[SummonerAI] Player found: " + player.name);
}
}
/// <summary>
/// Start spawning minions
/// </summary>
public void StartSpawning()
{
if (isSpawning)
{
if (enableDebug) Debug.Log("[SummonerAI] Already spawning minions");
return;
}
if (minionPrefab == null)
{
Debug.LogError("[SummonerAI] No minion prefab assigned!");
return;
}
spawnCoroutine = StartCoroutine(SpawnMinionsCoroutine());
}
/// <summary>
/// Stop spawning minions immediately
/// </summary>
public void StopSpawning()
{
if (spawnCoroutine != null)
{
StopCoroutine(spawnCoroutine);
spawnCoroutine = null;
}
isSpawning = false;
}
/// <summary>
/// Coroutine that spawns minions with delays
/// </summary>
private IEnumerator SpawnMinionsCoroutine()
{
isSpawning = true;
if (enableDebug) Debug.Log($"[SummonerAI] Starting to spawn {minionsPerSummon} minions");
// Initial delay
yield return new WaitForSeconds(initialSpawnDelay);
// Play summon sound
if (audioSource != null && summonSound != null)
{
audioSource.PlayOneShot(summonSound);
}
// Spawn minions
int spawned = 0;
for (int i = 0; i < minionsPerSummon && activeMinions.Count < maxActiveMinions; i++)
{
SpawnSingleMinion();
spawned++;
// Wait between spawns (except after last one)
if (i < minionsPerSummon - 1)
{
yield return new WaitForSeconds(timeBetweenSpawns);
}
}
if (enableDebug) Debug.Log($"[SummonerAI] Finished spawning {spawned} minions. Total active: {activeMinions.Count}");
isSpawning = false;
}
/// <summary>
/// Spawn a single minion at random position around summoner
/// </summary>
private void SpawnSingleMinion()
{
// Calculate random spawn position
float angle = Random.Range(0f, 360f) * Mathf.Deg2Rad;
float x = transform.position.x + spawnRadius * Mathf.Cos(angle);
float z = transform.position.z + spawnRadius * Mathf.Sin(angle);
Vector3 spawnPosition = new Vector3(x, transform.position.y + spawnHeightOffset, z);
// Spawn minion
GameObject minion = Instantiate(minionPrefab, spawnPosition, Quaternion.identity);
// Set rotation
if (minionsLookAtCenter)
{
minion.transform.LookAt(transform.position);
}
else if (playerTransform != null)
{
// Make minion face player
Vector3 directionToPlayer = (playerTransform.position - minion.transform.position).normalized;
directionToPlayer.y = 0;
if (directionToPlayer != Vector3.zero)
{
minion.transform.rotation = Quaternion.LookRotation(directionToPlayer);
}
}
// Configure minion AI to target player
var minionAI = minion.GetComponent<vControlAI>();
if (minionAI != null && playerTransform != null)
{
// Set player as target through AI system
minionAI.SetCurrentTarget(playerTransform);
}
// Add to active minions list
activeMinions.Add(minion);
// Spawn visual effect
if (spawnEffectPrefab != null)
{
GameObject effect = Instantiate(spawnEffectPrefab, spawnPosition, Quaternion.identity);
Destroy(effect, 3f);
}
if (enableDebug) Debug.Log($"[SummonerAI] Spawned minion at {spawnPosition}");
}
/// <summary>
/// Remove destroyed/null minions from list
/// </summary>
private void CleanupDeadMinions()
{
activeMinions.RemoveAll(minion => minion == null);
}
/// <summary>
/// Check if summoner should spawn minions based on health
/// </summary>
public bool ShouldSummonByHealth()
{
if (healthController == null) return false;
float healthPercent = healthController.currentHealth / healthController.MaxHealth;
return healthPercent <= healthThresholdForSummon;
}
/// <summary>
/// Get distance to player
/// </summary>
public float GetDistanceToPlayer()
{
if (playerTransform == null)
{
FindPlayer();
if (playerTransform == null) return float.MaxValue;
}
return Vector3.Distance(transform.position, playerTransform.position);
}
/// <summary>
/// Check if player is in melee range
/// </summary>
public bool IsPlayerInMeleeRange()
{
return GetDistanceToPlayer() <= meleeEngageDistance;
}
/// <summary>
/// Check if summoner should engage in melee combat
/// </summary>
public bool ShouldEngageMelee()
{
if (!IsPlayerInMeleeRange()) return false;
if (fightWithMinions) return true;
return !HasActiveMinions;
}
/// <summary>
/// Destroy all active minions (e.g., when summoner dies)
/// </summary>
public void DestroyAllMinions()
{
foreach (GameObject minion in activeMinions)
{
if (minion != null)
{
Destroy(minion);
}
}
activeMinions.Clear();
if (enableDebug) Debug.Log("[SummonerAI] All minions destroyed");
}
private void OnDestroy()
{
// Cleanup minions when summoner dies
DestroyAllMinions();
}
private void OnDrawGizmosSelected()
{
if (!showGizmos) return;
// Draw spawn radius
Gizmos.color = Color.cyan;
DrawCircle(transform.position, spawnRadius, 32);
// Draw melee range
Gizmos.color = Color.red;
DrawCircle(transform.position, meleeEngageDistance, 16);
// Draw lines to active minions
Gizmos.color = Color.green;
foreach (GameObject minion in activeMinions)
{
if (minion != null)
{
Gizmos.DrawLine(transform.position + Vector3.up, minion.transform.position + Vector3.up);
}
}
}
private void DrawCircle(Vector3 center, float radius, int segments)
{
float angleStep = 360f / segments;
Vector3 previousPoint = center + new Vector3(radius, 0, 0);
for (int i = 1; i <= segments; i++)
{
float angle = i * angleStep * Mathf.Deg2Rad;
Vector3 newPoint = center + new Vector3(
Mathf.Cos(angle) * radius,
0,
Mathf.Sin(angle) * radius
);
Gizmos.DrawLine(previousPoint, newPoint);
previousPoint = newPoint;
}
}
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More