Demon Fixes
This commit is contained in:
@@ -5,140 +5,97 @@ 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
|
||||
/// Spawns exactly 3 crystal turrets around the opponent.
|
||||
/// Now with per-spawn randomness: global rotation offset and per-turret angle/radius jitter.
|
||||
/// Validates positions (ground/obstacles) and enforces minimum separation.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")]
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn 3 Turrets Radial")]
|
||||
public class SA_SpawnTurretSmart : vStateAction
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Spawn Turret Smart";
|
||||
public override string defaultName => "Spawn 3 Turrets Radial";
|
||||
|
||||
[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 bool parameter name for blocking state")]
|
||||
public string animatorBlockingBool = "IsBlocking";
|
||||
|
||||
[Header("Smart Positioning")]
|
||||
[Tooltip("Preference multiplier for positions behind boss (relative to player)")]
|
||||
public float backPreferenceMultiplier = 2f;
|
||||
[Header("Placement Search (Validation)")]
|
||||
public int perTurretAdjustmentTries = 10;
|
||||
|
||||
[Tooltip("Number of attempts to find valid position")]
|
||||
public int maxSpawnAttempts = 12;
|
||||
public float maxAngleAdjust = 25f;
|
||||
public float maxRadiusAdjust = 1.0f;
|
||||
public float minSeparationBetweenTurrets = 1.5f;
|
||||
|
||||
[Header("Randomization (Formation)")]
|
||||
[Tooltip("Random global rotation offset (degrees) applied to the 120° spokes.")]
|
||||
public bool globalStartAngleRandom = true;
|
||||
|
||||
[Tooltip("Per-turret angle jitter around the spoke (degrees). 0 = disabled.")]
|
||||
public float perTurretAngleJitter = 10f;
|
||||
|
||||
[Tooltip("Per-turret radius jitter (meters). 0 = disabled.")]
|
||||
public float perTurretRadiusJitter = 0.75f;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
private GameObject spawnedCrystal;
|
||||
private Animator npcAnimator;
|
||||
private Transform npcTransform;
|
||||
private Transform playerTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Main action execution method called by FSM
|
||||
/// </summary>
|
||||
private readonly System.Collections.Generic.List<Vector3> _lastPlanned = new System.Collections.Generic.List<Vector3>(3);
|
||||
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter)
|
||||
{
|
||||
OnStateEnter(fsmBehaviour);
|
||||
}
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateExit)
|
||||
{
|
||||
OnStateExit(fsmBehaviour);
|
||||
}
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter) OnStateEnter(fsmBehaviour);
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateExit) OnStateExit(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");
|
||||
|
||||
// Store NPC references
|
||||
npcTransform = fsmBehaviour.transform;
|
||||
npcAnimator = npcTransform.GetComponent<Animator>();
|
||||
|
||||
FindPlayer(fsmBehaviour);
|
||||
|
||||
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
|
||||
{
|
||||
npcAnimator.SetBool(animatorBlockingBool, true);
|
||||
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set bool: {animatorBlockingBool} = true");
|
||||
}
|
||||
|
||||
SpawnCrystalSmart(fsmBehaviour);
|
||||
SpawnThreeTurretsRadial(fsmBehaviour);
|
||||
|
||||
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when exiting state - cleanup
|
||||
/// </summary>
|
||||
private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Exiting turret spawn state");
|
||||
|
||||
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
|
||||
{
|
||||
npcAnimator.SetBool(animatorBlockingBool, false);
|
||||
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set bool: {animatorBlockingBool} = 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_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)
|
||||
private void SpawnThreeTurretsRadial(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (crystalPrefab == null)
|
||||
{
|
||||
@@ -146,63 +103,85 @@ namespace DemonBoss.Magic
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 bestPosition = Vector3.zero;
|
||||
bool foundValidPosition = false;
|
||||
float bestScore = float.MinValue;
|
||||
_lastPlanned.Clear();
|
||||
|
||||
Vector3 bossPos = npcTransform.position;
|
||||
Vector3 playerDirection = Vector3.zero;
|
||||
Vector3 center = playerTransform != null ? playerTransform.position : npcTransform.position;
|
||||
float baseRadius = Mathf.Clamp((minSpawnDistance + maxSpawnDistance) * 0.5f, minSpawnDistance, maxSpawnDistance);
|
||||
|
||||
if (playerTransform != null)
|
||||
Vector3 refDir = Vector3.forward;
|
||||
if (playerTransform != null && npcTransform != null)
|
||||
{
|
||||
playerDirection = (playerTransform.position - bossPos).normalized;
|
||||
Vector3 d = (npcTransform.position - center); d.y = 0f;
|
||||
if (d.sqrMagnitude > 0.0001f) refDir = d.normalized;
|
||||
}
|
||||
else if (npcTransform != null)
|
||||
{
|
||||
refDir = npcTransform.forward;
|
||||
}
|
||||
|
||||
for (int i = 0; i < maxSpawnAttempts; i++)
|
||||
float globalOffset = globalStartAngleRandom ? Random.Range(0f, 360f) : 0f;
|
||||
|
||||
const int turretCount = 3;
|
||||
for (int i = 0; i < turretCount; 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 baseAngle = globalOffset + i * 120f;
|
||||
float angle = baseAngle + (perTurretAngleJitter > 0f ? Random.Range(-perTurretAngleJitter, perTurretAngleJitter) : 0f);
|
||||
|
||||
float distance = Random.Range(minSpawnDistance, maxSpawnDistance);
|
||||
Vector3 testPosition = bossPos + direction * distance;
|
||||
float radius = baseRadius + (perTurretRadiusJitter > 0f ? Random.Range(-perTurretRadiusJitter, perTurretRadiusJitter) : 0f);
|
||||
radius = Mathf.Clamp(radius, minSpawnDistance, maxSpawnDistance);
|
||||
|
||||
if (IsPositionValid(testPosition, out Vector3 groundPosition))
|
||||
Vector3 ideal = center + Quaternion.Euler(0f, angle, 0f) * refDir * radius;
|
||||
|
||||
Vector3? chosen = IsPositionValidAndSeparated(ideal)
|
||||
? (Vector3?)ideal
|
||||
: FindValidPositionAroundSpoke(center, refDir, angle, radius);
|
||||
|
||||
if (!chosen.HasValue)
|
||||
{
|
||||
float score = EvaluatePosition(groundPosition, playerDirection, direction, bossPos);
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestPosition = groundPosition;
|
||||
foundValidPosition = true;
|
||||
}
|
||||
Vector3 rough = center + Quaternion.Euler(0f, angle, 0f) * refDir * radius;
|
||||
chosen = EnforceSeparationFallback(rough, center);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundValidPosition)
|
||||
{
|
||||
SpawnCrystal(bestPosition, fsmBehaviour);
|
||||
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 fallbackPos = bossPos + npcTransform.forward * minSpawnDistance;
|
||||
SpawnCrystal(fallbackPos, fsmBehaviour);
|
||||
if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position");
|
||||
Vector3 spawnPos = chosen.Value;
|
||||
_lastPlanned.Add(spawnPos);
|
||||
SpawnCrystal(spawnPos, fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if position is valid (no obstacles, has ground)
|
||||
/// </summary>
|
||||
private Vector3? FindValidPositionAroundSpoke(Vector3 center, Vector3 refDir, float baseAngleDeg, float desiredRadius)
|
||||
{
|
||||
Vector3 ideal = center + Quaternion.Euler(0f, baseAngleDeg, 0f) * refDir * desiredRadius;
|
||||
if (IsPositionValidAndSeparated(ideal)) return ideal;
|
||||
|
||||
for (int t = 0; t < perTurretAdjustmentTries; t++)
|
||||
{
|
||||
float ang = baseAngleDeg + Random.Range(-maxAngleAdjust, maxAngleAdjust);
|
||||
float rad = Mathf.Clamp(desiredRadius + Random.Range(-maxRadiusAdjust, maxRadiusAdjust), minSpawnDistance, maxSpawnDistance);
|
||||
Vector3 cand = center + Quaternion.Euler(0f, ang, 0f) * refDir * rad;
|
||||
|
||||
if (IsPositionValidAndSeparated(cand)) return cand;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsPositionValidAndSeparated(Vector3 position)
|
||||
{
|
||||
if (!IsPositionValid(position, out Vector3 grounded)) return false;
|
||||
|
||||
for (int i = 0; i < _lastPlanned.Count; i++)
|
||||
{
|
||||
if (Vector3.Distance(_lastPlanned[i], grounded) < Mathf.Max(minSeparationBetweenTurrets, obstacleCheckRadius * 2f))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsPositionValid(Vector3 position, out Vector3 groundPosition)
|
||||
{
|
||||
groundPosition = position;
|
||||
|
||||
if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down);
|
||||
if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask))
|
||||
@@ -210,109 +189,82 @@ namespace DemonBoss.Magic
|
||||
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, Vector3 bossPos)
|
||||
private Vector3 EnforceSeparationFallback(Vector3 desired, Vector3 center)
|
||||
{
|
||||
float score = 0f;
|
||||
|
||||
if (playerTransform != null && playerDirection != Vector3.zero)
|
||||
Vector3 candidate = desired;
|
||||
float step = 0.5f;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection);
|
||||
bool ok = true;
|
||||
for (int k = 0; k < _lastPlanned.Count; k++)
|
||||
{
|
||||
if (Vector3.Distance(_lastPlanned[k], candidate) < Mathf.Max(minSeparationBetweenTurrets, obstacleCheckRadius * 2f))
|
||||
{ ok = false; break; }
|
||||
}
|
||||
if (ok) return candidate;
|
||||
|
||||
// The smaller the angle (closer to "behind"), the better the score
|
||||
float backScore = (180f - angleToPlayer) / 180f;
|
||||
score += backScore * backPreferenceMultiplier;
|
||||
Vector3 dir = (candidate - center); dir.y = 0f;
|
||||
if (dir.sqrMagnitude < 0.0001f) dir = Vector3.right;
|
||||
candidate = center + dir.normalized * (dir.magnitude + step);
|
||||
}
|
||||
|
||||
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;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns crystal at given position
|
||||
/// </summary>
|
||||
private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Quaternion rotation = Quaternion.identity;
|
||||
if (playerTransform != null)
|
||||
Transform lookAt = playerTransform != null ? playerTransform : npcTransform;
|
||||
|
||||
if (lookAt != null)
|
||||
{
|
||||
Vector3 lookDirection = (playerTransform.position - position).normalized;
|
||||
Vector3 lookDirection = (lookAt.position - position).normalized;
|
||||
lookDirection.y = 0;
|
||||
if (lookDirection != Vector3.zero)
|
||||
{
|
||||
rotation = Quaternion.LookRotation(lookDirection);
|
||||
}
|
||||
}
|
||||
|
||||
spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation);
|
||||
var spawned = LeanPool.Spawn(crystalPrefab, position, rotation);
|
||||
|
||||
var shooterAI = spawnedCrystal.GetComponent<CrystalShooterAI>();
|
||||
var shooterAI = spawned.GetComponent<CrystalShooterAI>();
|
||||
if (shooterAI == null)
|
||||
{
|
||||
Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
|
||||
}
|
||||
else
|
||||
else if (playerTransform != null)
|
||||
{
|
||||
if (playerTransform != null)
|
||||
{
|
||||
shooterAI.SetTarget(playerTransform);
|
||||
}
|
||||
shooterAI.SetTarget(playerTransform);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws gizmos in Scene View for debugging
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
if (!showGizmos || npcTransform == null) return;
|
||||
|
||||
if (npcTransform != null)
|
||||
{
|
||||
Vector3 pos = npcTransform.position;
|
||||
Vector3 c = playerTransform ? playerTransform.position : npcTransform.position;
|
||||
|
||||
// Spawn ring
|
||||
Gizmos.color = Color.green;
|
||||
DrawWireCircle(pos, minSpawnDistance);
|
||||
Gizmos.color = Color.red;
|
||||
DrawWireCircle(pos, maxSpawnDistance);
|
||||
Gizmos.color = Color.green;
|
||||
DrawWireCircle(c, minSpawnDistance);
|
||||
Gizmos.color = Color.red;
|
||||
DrawWireCircle(c, maxSpawnDistance);
|
||||
|
||||
// Obstacle check radius
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius);
|
||||
}
|
||||
Gizmos.color = Color.cyan;
|
||||
foreach (var p in _lastPlanned) Gizmos.DrawWireSphere(p + Vector3.up * 0.1f, 0.2f);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
float angle = (float)i / segments * 360f * Mathf.Deg2Rad;
|
||||
Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
|
||||
Gizmos.DrawLine(prevPoint, newPoint);
|
||||
prevPoint = newPoint;
|
||||
|
||||
Reference in New Issue
Block a user