Summoner
This commit is contained in:
8
Assets/AI/Common.meta
Normal file
8
Assets/AI/Common.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c2cfd6f241d01aa4db9370f43cc0950e
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
30
Assets/AI/Common/DelayedInvoker.cs
Normal file
30
Assets/AI/Common/DelayedInvoker.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DemonBoss.AI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tiny helper MonoBehaviour to delay a callback without coroutines.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DelayedInvoker : MonoBehaviour
|
||||||
|
{
|
||||||
|
private float _timeLeft;
|
||||||
|
private Action _callback;
|
||||||
|
|
||||||
|
public void Init(float delay, Action callback)
|
||||||
|
{
|
||||||
|
_timeLeft = delay;
|
||||||
|
_callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
_timeLeft -= Time.deltaTime;
|
||||||
|
if (_timeLeft <= 0f)
|
||||||
|
{
|
||||||
|
try { _callback?.Invoke(); }
|
||||||
|
finally { Destroy(this); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/AI/Common/DelayedInvoker.cs.meta
Normal file
2
Assets/AI/Common/DelayedInvoker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6c81673d9e75da44fb3d5c5f6911c775
|
||||||
2133
Assets/AI/FSM/FSM_Summoner.asset
Normal file
2133
Assets/AI/FSM/FSM_Summoner.asset
Normal file
File diff suppressed because it is too large
Load Diff
8
Assets/AI/FSM/FSM_Summoner.asset.meta
Normal file
8
Assets/AI/FSM/FSM_Summoner.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c82aa8a4a75da9b49b73d6b4d5d86158
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using DemonBoss.AI;
|
||||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||||
using Lean.Pool;
|
using Lean.Pool;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -331,29 +332,5 @@ namespace DemonBoss.Magic
|
|||||||
|
|
||||||
public float GetSequenceProgress() => meteorCount > 0 ? (float)_meteorsSpawned / meteorCount : 1f;
|
public float GetSequenceProgress() => meteorCount > 0 ? (float)_meteorsSpawned / meteorCount : 1f;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tiny helper MonoBehaviour to delay a callback without coroutines here.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class DelayedInvoker : MonoBehaviour
|
|
||||||
{
|
|
||||||
private float _timeLeft;
|
|
||||||
private System.Action _callback;
|
|
||||||
|
|
||||||
public void Init(float delay, System.Action callback)
|
|
||||||
{
|
|
||||||
_timeLeft = delay;
|
|
||||||
_callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
|
||||||
{
|
|
||||||
_timeLeft -= Time.deltaTime;
|
|
||||||
if (_timeLeft <= 0f)
|
|
||||||
{
|
|
||||||
try { _callback?.Invoke(); }
|
|
||||||
finally { Destroy(this); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ namespace DemonBoss.Summoner
|
|||||||
[Tooltip("Only spawn if player is within this distance (0 = disabled)")]
|
[Tooltip("Only spawn if player is within this distance (0 = disabled)")]
|
||||||
public float maxDistanceToPlayer = 0f;
|
public float maxDistanceToPlayer = 0f;
|
||||||
|
|
||||||
|
[Tooltip("Require summoner to be at or beyond safe distance before summoning")]
|
||||||
|
public bool requireSafeDistance = false;
|
||||||
|
|
||||||
[Header("Debug")]
|
[Header("Debug")]
|
||||||
[Tooltip("Enable debug logging")]
|
[Tooltip("Enable debug logging")]
|
||||||
public bool enableDebug = false;
|
public bool enableDebug = false;
|
||||||
@@ -77,6 +80,12 @@ namespace DemonBoss.Summoner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requireSafeDistance && !summoner.IsAtSafeDistance())
|
||||||
|
{
|
||||||
|
if (enableDebug) Debug.Log("[DEC_CanSpawnMinions] Not at safe distance - FALSE");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Check cooldown
|
// Check cooldown
|
||||||
if (checkCooldown)
|
if (checkCooldown)
|
||||||
{
|
{
|
||||||
@@ -102,4 +111,4 @@ namespace DemonBoss.Summoner
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
Assets/AI/_Summoner/DEC_IsSpawningComplete.cs
Normal file
32
Assets/AI/_Summoner/DEC_IsSpawningComplete.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DemonBoss.Summoner
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FSM Decision: true when summoner is NOT spawning (spawn sequence finished)
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/Summoner/Is Spawning Complete")]
|
||||||
|
public class DEC_IsSpawningComplete : vStateDecision
|
||||||
|
{
|
||||||
|
public override string categoryName => "Summoner";
|
||||||
|
public override string defaultName => "Is Spawning Complete";
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
public bool enableDebug = false;
|
||||||
|
|
||||||
|
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||||
|
{
|
||||||
|
var summoner = fsmBehaviour.gameObject.GetComponent<SummonerAI>();
|
||||||
|
if (summoner == null)
|
||||||
|
{
|
||||||
|
if (enableDebug) Debug.LogWarning("[DEC_IsSpawningComplete] No SummonerAI component found!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool done = !summoner.IsSpawning;
|
||||||
|
if (enableDebug) Debug.Log($"[DEC_IsSpawningComplete] done={done}");
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/AI/_Summoner/DEC_IsSpawningComplete.cs.meta
Normal file
2
Assets/AI/_Summoner/DEC_IsSpawningComplete.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 57576c59e67c93f47bf31ee6a7cb00a6
|
||||||
33
Assets/AI/_Summoner/DEC_ShouldCastSpell.cs
Normal file
33
Assets/AI/_Summoner/DEC_ShouldCastSpell.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DemonBoss.Summoner
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FSM Decision: should Summoner cast a ranged spell
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/Summoner/Should Cast Spell")]
|
||||||
|
public class DEC_ShouldCastSpell : vStateDecision
|
||||||
|
{
|
||||||
|
public override string categoryName => "Summoner";
|
||||||
|
public override string defaultName => "Should Cast Spell";
|
||||||
|
|
||||||
|
[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_ShouldCastSpell] No SummonerAI component found!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canCast = summoner.CanCastSpell();
|
||||||
|
if (enableDebug) Debug.Log($"[DEC_ShouldCastSpell] canCast={canCast}");
|
||||||
|
return canCast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/AI/_Summoner/DEC_ShouldCastSpell.cs.meta
Normal file
2
Assets/AI/_Summoner/DEC_ShouldCastSpell.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8d84d40bbfa01d24a8b3d0e9ac5ab2c3
|
||||||
33
Assets/AI/_Summoner/DEC_ShouldFleeToSafeDistance.cs
Normal file
33
Assets/AI/_Summoner/DEC_ShouldFleeToSafeDistance.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DemonBoss.Summoner
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FSM Decision: should Summoner flee to reach safe distance
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/Summoner/Should Flee To Safe Distance")]
|
||||||
|
public class DEC_ShouldFleeToSafeDistance : vStateDecision
|
||||||
|
{
|
||||||
|
public override string categoryName => "Summoner";
|
||||||
|
public override string defaultName => "Should Flee To Safe Distance";
|
||||||
|
|
||||||
|
[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_ShouldFleeToSafeDistance] No SummonerAI component found!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldFlee = summoner.ShouldFleeToSafeDistance();
|
||||||
|
if (enableDebug) Debug.Log($"[DEC_ShouldFleeToSafeDistance] shouldFlee={shouldFlee}");
|
||||||
|
return shouldFlee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/AI/_Summoner/DEC_ShouldFleeToSafeDistance.cs.meta
Normal file
2
Assets/AI/_Summoner/DEC_ShouldFleeToSafeDistance.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9f2c33cfc53333944b5c60a3ff6a3de2
|
||||||
@@ -24,6 +24,9 @@ namespace DemonBoss.Summoner
|
|||||||
[Tooltip("Attack even when minions are alive")]
|
[Tooltip("Attack even when minions are alive")]
|
||||||
public bool attackWithMinions = false;
|
public bool attackWithMinions = false;
|
||||||
|
|
||||||
|
[Tooltip("Force melee when player is too close, even if minions are alive")]
|
||||||
|
public bool forceMeleeWhenTooClose = true;
|
||||||
|
|
||||||
[Header("Debug")]
|
[Header("Debug")]
|
||||||
[Tooltip("Enable debug logging")]
|
[Tooltip("Enable debug logging")]
|
||||||
public bool enableDebug = false;
|
public bool enableDebug = false;
|
||||||
@@ -44,6 +47,25 @@ namespace DemonBoss.Summoner
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check distance to player
|
||||||
|
float distance = summoner.GetDistanceToPlayer();
|
||||||
|
|
||||||
|
bool inRange = distance >= minMeleeDistance && distance <= maxMeleeDistance;
|
||||||
|
|
||||||
|
// Last resort: only melee when in range AND cannot spawn or cast
|
||||||
|
bool canSpawn = summoner.CanSpawnMinions;
|
||||||
|
bool canCast = summoner.CanCastSpell();
|
||||||
|
|
||||||
|
if (forceMeleeWhenTooClose && inRange)
|
||||||
|
{
|
||||||
|
bool lastResort = !canSpawn && !canCast;
|
||||||
|
if (enableDebug)
|
||||||
|
{
|
||||||
|
Debug.Log($"[DEC_ShouldMeleeAttack] Last resort check (distance {distance:F1}m, canSpawn={canSpawn}, canCast={canCast}) - {(lastResort ? "TRUE" : "FALSE")}");
|
||||||
|
}
|
||||||
|
return lastResort;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if has minions and shouldn't attack with them
|
// Check if has minions and shouldn't attack with them
|
||||||
if (!attackWithMinions && summoner.HasActiveMinions)
|
if (!attackWithMinions && summoner.HasActiveMinions)
|
||||||
{
|
{
|
||||||
@@ -51,11 +73,6 @@ namespace DemonBoss.Summoner
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check distance to player
|
|
||||||
float distance = summoner.GetDistanceToPlayer();
|
|
||||||
|
|
||||||
bool inRange = distance >= minMeleeDistance && distance <= maxMeleeDistance;
|
|
||||||
|
|
||||||
if (enableDebug)
|
if (enableDebug)
|
||||||
{
|
{
|
||||||
if (inRange)
|
if (inRange)
|
||||||
@@ -68,7 +85,7 @@ namespace DemonBoss.Summoner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return inRange;
|
return inRange && !canSpawn && !canCast;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
145
Assets/AI/_Summoner/SA_CastFireball.cs
Normal file
145
Assets/AI/_Summoner/SA_CastFireball.cs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
using DemonBoss.AI;
|
||||||
|
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Animations;
|
||||||
|
using UnityEngine.Playables;
|
||||||
|
|
||||||
|
namespace DemonBoss.Summoner
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FSM Action: cast a single fireball with optional overlay clip
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(menuName = "Invector/FSM/Actions/Summoner/Cast Fireball")]
|
||||||
|
public class SA_CastFireball : vStateAction
|
||||||
|
{
|
||||||
|
public override string categoryName => "Summoner";
|
||||||
|
public override string defaultName => "Cast Fireball";
|
||||||
|
|
||||||
|
[Header("Timing")]
|
||||||
|
[Tooltip("Optional delay before the fireball is spawned")]
|
||||||
|
public float castDelay = 0f;
|
||||||
|
|
||||||
|
[Header("One-off Overlay Clip (No Animator Params)")]
|
||||||
|
public AnimationClip overlayClip;
|
||||||
|
|
||||||
|
[Tooltip("Playback speed (1 = normal)")]
|
||||||
|
public float overlaySpeed = 1f;
|
||||||
|
|
||||||
|
[Tooltip("Blend-in seconds (instant in this minimal impl)")]
|
||||||
|
public float overlayFadeIn = 0.10f;
|
||||||
|
|
||||||
|
[Tooltip("Blend-out seconds (instant in this minimal impl)")]
|
||||||
|
public float overlayFadeOut = 0.10f;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
public bool enableDebug = false;
|
||||||
|
|
||||||
|
private SummonerAI _summoner;
|
||||||
|
private bool _castScheduled;
|
||||||
|
|
||||||
|
// --- Playables runtime ---
|
||||||
|
private PlayableGraph _overlayGraph;
|
||||||
|
private AnimationPlayableOutput _overlayOutput;
|
||||||
|
private AnimationClipPlayable _overlayPlayable;
|
||||||
|
private bool _overlayPlaying;
|
||||||
|
private float _overlayStopAtTime;
|
||||||
|
|
||||||
|
public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType execType = vFSMComponentExecutionType.OnStateUpdate)
|
||||||
|
{
|
||||||
|
if (execType == vFSMComponentExecutionType.OnStateEnter)
|
||||||
|
{
|
||||||
|
OnEnter(fsm);
|
||||||
|
}
|
||||||
|
else if (execType == vFSMComponentExecutionType.OnStateUpdate)
|
||||||
|
{
|
||||||
|
if (_overlayPlaying && Time.time >= _overlayStopAtTime)
|
||||||
|
{
|
||||||
|
StopOverlayWithFade();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (execType == vFSMComponentExecutionType.OnStateExit)
|
||||||
|
{
|
||||||
|
OnExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnter(vIFSMBehaviourController fsm)
|
||||||
|
{
|
||||||
|
_summoner = fsm.gameObject.GetComponent<SummonerAI>();
|
||||||
|
if (_summoner == null)
|
||||||
|
{
|
||||||
|
if (enableDebug) Debug.LogWarning("[SA_CastFireball] No SummonerAI component found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_castScheduled = false;
|
||||||
|
|
||||||
|
// Play overlay clip (no Animator params)
|
||||||
|
PlayOverlayOnce(fsm.transform);
|
||||||
|
|
||||||
|
if (castDelay > 0f)
|
||||||
|
{
|
||||||
|
fsm.gameObject.AddComponent<DelayedInvoker>().Init(castDelay, SpawnFireballNow);
|
||||||
|
_castScheduled = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SpawnFireballNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExit()
|
||||||
|
{
|
||||||
|
StopOverlayImmediate();
|
||||||
|
_castScheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnFireballNow()
|
||||||
|
{
|
||||||
|
if (_summoner == null) return;
|
||||||
|
_summoner.CastFireball();
|
||||||
|
if (enableDebug) Debug.Log("[SA_CastFireball] Fireball cast");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------ Overlay helpers ------------------
|
||||||
|
|
||||||
|
private void PlayOverlayOnce(Transform owner)
|
||||||
|
{
|
||||||
|
if (overlayClip == null || owner == null) return;
|
||||||
|
|
||||||
|
Animator anim = owner.GetComponent<Animator>();
|
||||||
|
if (anim == null) return;
|
||||||
|
|
||||||
|
_overlayGraph = PlayableGraph.Create("ActionOverlay(CastFireball)");
|
||||||
|
_overlayGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
|
||||||
|
|
||||||
|
_overlayPlayable = AnimationClipPlayable.Create(_overlayGraph, overlayClip);
|
||||||
|
_overlayPlayable.SetSpeed(Mathf.Max(0.0001f, overlaySpeed));
|
||||||
|
_overlayPlayable.SetApplyFootIK(false);
|
||||||
|
_overlayPlayable.SetApplyPlayableIK(false);
|
||||||
|
|
||||||
|
_overlayOutput = AnimationPlayableOutput.Create(_overlayGraph, "AnimOut", anim);
|
||||||
|
_overlayOutput.SetSourcePlayable(_overlayPlayable);
|
||||||
|
_overlayOutput.SetWeight(1f);
|
||||||
|
|
||||||
|
_overlayGraph.Play();
|
||||||
|
|
||||||
|
float duration = (float)overlayClip.length / Mathf.Max(0.0001f, overlaySpeed);
|
||||||
|
_overlayStopAtTime = Time.time + duration;
|
||||||
|
_overlayPlaying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopOverlayWithFade()
|
||||||
|
{
|
||||||
|
StopOverlayImmediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopOverlayImmediate()
|
||||||
|
{
|
||||||
|
if (!_overlayGraph.IsValid()) return;
|
||||||
|
_overlayGraph.Stop();
|
||||||
|
_overlayGraph.Destroy();
|
||||||
|
_overlayPlaying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/AI/_Summoner/SA_CastFireball.cs.meta
Normal file
2
Assets/AI/_Summoner/SA_CastFireball.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0ed945c0d54773940ab5191abeac4cd0
|
||||||
@@ -23,6 +23,12 @@ namespace DemonBoss.Summoner
|
|||||||
[Header("Behavior")]
|
[Header("Behavior")]
|
||||||
[Tooltip("Wait for spawning to complete before allowing state exit")]
|
[Tooltip("Wait for spawning to complete before allowing state exit")]
|
||||||
public bool waitForCompletion = true;
|
public bool waitForCompletion = true;
|
||||||
|
|
||||||
|
[Tooltip("If true, spawn starts only after an animation event calls SummonerAI.OnSummonAnimationEvent")]
|
||||||
|
public bool spawnOnAnimationEvent = true;
|
||||||
|
|
||||||
|
[Tooltip("Fallback: start spawn if animation event doesn't fire within this time (seconds). 0 = no fallback.")]
|
||||||
|
public float animationEventTimeout = 1.2f;
|
||||||
|
|
||||||
[Header("Debug")]
|
[Header("Debug")]
|
||||||
[Tooltip("Enable debug logging")]
|
[Tooltip("Enable debug logging")]
|
||||||
@@ -31,6 +37,8 @@ namespace DemonBoss.Summoner
|
|||||||
private SummonerAI summoner;
|
private SummonerAI summoner;
|
||||||
private Animator animator;
|
private Animator animator;
|
||||||
private bool hasStartedSpawning = false;
|
private bool hasStartedSpawning = false;
|
||||||
|
private float spawnRequestTime = -1f;
|
||||||
|
private bool spawnTriggered = false;
|
||||||
|
|
||||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||||
{
|
{
|
||||||
@@ -73,15 +81,40 @@ namespace DemonBoss.Summoner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start spawning minions
|
if (spawnOnAnimationEvent)
|
||||||
summoner.StartSpawning();
|
{
|
||||||
hasStartedSpawning = true;
|
summoner.RequestSpawn();
|
||||||
|
hasStartedSpawning = true;
|
||||||
if (enableDebug) Debug.Log("[SA_SpawnMinions] Started spawning minions");
|
spawnTriggered = false;
|
||||||
|
spawnRequestTime = Time.time;
|
||||||
|
if (enableDebug) Debug.Log("[SA_SpawnMinions] Waiting for animation event to spawn");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Start spawning minions immediately
|
||||||
|
summoner.StartSpawning();
|
||||||
|
hasStartedSpawning = true;
|
||||||
|
spawnTriggered = true;
|
||||||
|
if (enableDebug) Debug.Log("[SA_SpawnMinions] Started spawning minions");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUpdate(vIFSMBehaviourController fsmBehaviour)
|
private void OnUpdate(vIFSMBehaviourController fsmBehaviour)
|
||||||
{
|
{
|
||||||
|
if (summoner != null && spawnOnAnimationEvent && !spawnTriggered)
|
||||||
|
{
|
||||||
|
if (summoner.IsSpawning)
|
||||||
|
{
|
||||||
|
spawnTriggered = true;
|
||||||
|
}
|
||||||
|
else if (animationEventTimeout > 0f && spawnRequestTime > 0f && (Time.time - spawnRequestTime) >= animationEventTimeout)
|
||||||
|
{
|
||||||
|
summoner.StartSpawning();
|
||||||
|
spawnTriggered = true;
|
||||||
|
if (enableDebug) Debug.Log("[SA_SpawnMinions] Animation event timeout - spawning fallback");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If waiting for completion, keep state active until spawning is done
|
// If waiting for completion, keep state active until spawning is done
|
||||||
if (waitForCompletion && summoner != null && summoner.IsSpawning)
|
if (waitForCompletion && summoner != null && summoner.IsSpawning)
|
||||||
{
|
{
|
||||||
@@ -107,8 +140,14 @@ namespace DemonBoss.Summoner
|
|||||||
summoner.StopSpawning();
|
summoner.StopSpawning();
|
||||||
if (enableDebug) Debug.Log("[SA_SpawnMinions] Spawning interrupted on state exit");
|
if (enableDebug) Debug.Log("[SA_SpawnMinions] Spawning interrupted on state exit");
|
||||||
}
|
}
|
||||||
|
else if (summoner != null)
|
||||||
|
{
|
||||||
|
summoner.CancelSpawnRequest();
|
||||||
|
}
|
||||||
|
|
||||||
hasStartedSpawning = false;
|
hasStartedSpawning = false;
|
||||||
|
spawnTriggered = false;
|
||||||
|
spawnRequestTime = -1f;
|
||||||
|
|
||||||
if (enableDebug) Debug.Log("[SA_SpawnMinions] State exited");
|
if (enableDebug) Debug.Log("[SA_SpawnMinions] State exited");
|
||||||
}
|
}
|
||||||
@@ -122,4 +161,4 @@ namespace DemonBoss.Summoner
|
|||||||
return hasStartedSpawning && !summoner.IsSpawning;
|
return hasStartedSpawning && !summoner.IsSpawning;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4266
Assets/AI/_Summoner/Summoner.prefab
Normal file
4266
Assets/AI/_Summoner/Summoner.prefab
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 8faaed5f0f8beac4193201e791fa0406
|
guid: 1ff92c6825e7585419de5a79794bc3f2
|
||||||
PrefabImporter:
|
PrefabImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
@@ -2,6 +2,7 @@ using Invector;
|
|||||||
using Invector.vCharacterController.AI;
|
using Invector.vCharacterController.AI;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Lean.Pool;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace DemonBoss.Summoner
|
namespace DemonBoss.Summoner
|
||||||
@@ -43,6 +44,16 @@ namespace DemonBoss.Summoner
|
|||||||
[Tooltip("Minimum distance to player before engaging in melee")]
|
[Tooltip("Minimum distance to player before engaging in melee")]
|
||||||
public float meleeEngageDistance = 3f;
|
public float meleeEngageDistance = 3f;
|
||||||
|
|
||||||
|
[Header("Melee Cooldown")]
|
||||||
|
[Tooltip("Minimum seconds between melee attacks")]
|
||||||
|
public float meleeCooldownMin = 5f;
|
||||||
|
|
||||||
|
[Tooltip("Maximum seconds between melee attacks")]
|
||||||
|
public float meleeCooldownMax = 8f;
|
||||||
|
|
||||||
|
[Tooltip("Preferred safe distance from player (used for flee/spacing)")]
|
||||||
|
public float safeDistance = 8f;
|
||||||
|
|
||||||
[Tooltip("Should summoner fight when minions are alive?")]
|
[Tooltip("Should summoner fight when minions are alive?")]
|
||||||
public bool fightWithMinions = false;
|
public bool fightWithMinions = false;
|
||||||
|
|
||||||
@@ -61,6 +72,29 @@ namespace DemonBoss.Summoner
|
|||||||
[Tooltip("Sound played when spawning minions")]
|
[Tooltip("Sound played when spawning minions")]
|
||||||
public AudioClip summonSound;
|
public AudioClip summonSound;
|
||||||
|
|
||||||
|
[Header("Summon Restrictions")]
|
||||||
|
[Tooltip("If true, summoner will only summon when no minions are alive")]
|
||||||
|
public bool requireZeroMinionsToSummon = true;
|
||||||
|
|
||||||
|
[Header("Spell Casting")]
|
||||||
|
[Tooltip("Fireball prefab (projectile handles its own targeting)")]
|
||||||
|
public GameObject fireballPrefab;
|
||||||
|
|
||||||
|
[Tooltip("Optional spawn pivot for spells (e.g. staff tip). If null, uses summoner transform.")]
|
||||||
|
public Transform spellSpawnPivot;
|
||||||
|
|
||||||
|
[Tooltip("Cooldown between spell casts (seconds)")]
|
||||||
|
public float spellCooldown = 6f;
|
||||||
|
|
||||||
|
[Tooltip("Minimum distance required to cast spell")]
|
||||||
|
public float spellMinDistance = 5f;
|
||||||
|
|
||||||
|
[Tooltip("Maximum distance allowed to cast spell (0 = no limit)")]
|
||||||
|
public float spellMaxDistance = 20f;
|
||||||
|
|
||||||
|
[Tooltip("Only cast spell when minions are alive")]
|
||||||
|
public bool spellRequiresMinionsAlive = true;
|
||||||
|
|
||||||
[Header("Debug")]
|
[Header("Debug")]
|
||||||
[Tooltip("Enable debug logging")]
|
[Tooltip("Enable debug logging")]
|
||||||
public bool enableDebug = false;
|
public bool enableDebug = false;
|
||||||
@@ -76,12 +110,23 @@ namespace DemonBoss.Summoner
|
|||||||
private vHealthController healthController;
|
private vHealthController healthController;
|
||||||
private bool isSpawning = false;
|
private bool isSpawning = false;
|
||||||
private Coroutine spawnCoroutine;
|
private Coroutine spawnCoroutine;
|
||||||
|
private float lastSpellTime = -999f;
|
||||||
|
private bool spawnRequested = false;
|
||||||
|
private float nextMeleeTime = 0f;
|
||||||
|
|
||||||
// Public properties for FSM decisions
|
// Public properties for FSM decisions
|
||||||
public bool IsSpawning => isSpawning;
|
public bool IsSpawning => isSpawning;
|
||||||
|
|
||||||
public int ActiveMinionCount => activeMinions.Count;
|
public int ActiveMinionCount => activeMinions.Count;
|
||||||
public bool CanSpawnMinions => activeMinions.Count < maxActiveMinions && !isSpawning;
|
public bool CanSpawnMinions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (isSpawning) return false;
|
||||||
|
if (requireZeroMinionsToSummon && activeMinions.Count > 0) return false;
|
||||||
|
return activeMinions.Count < maxActiveMinions;
|
||||||
|
}
|
||||||
|
}
|
||||||
public bool HasActiveMinions => activeMinions.Count > 0;
|
public bool HasActiveMinions => activeMinions.Count > 0;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
@@ -125,6 +170,7 @@ namespace DemonBoss.Summoner
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void StartSpawning()
|
public void StartSpawning()
|
||||||
{
|
{
|
||||||
|
spawnRequested = false;
|
||||||
if (isSpawning)
|
if (isSpawning)
|
||||||
{
|
{
|
||||||
if (enableDebug) Debug.Log("[SummonerAI] Already spawning minions");
|
if (enableDebug) Debug.Log("[SummonerAI] Already spawning minions");
|
||||||
@@ -137,9 +183,44 @@ namespace DemonBoss.Summoner
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requireZeroMinionsToSummon && HasActiveMinions)
|
||||||
|
{
|
||||||
|
if (enableDebug) Debug.Log("[SummonerAI] Minions alive - summon blocked");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
spawnCoroutine = StartCoroutine(SpawnMinionsCoroutine());
|
spawnCoroutine = StartCoroutine(SpawnMinionsCoroutine());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request spawning to begin on an animation event.
|
||||||
|
/// </summary>
|
||||||
|
public void RequestSpawn()
|
||||||
|
{
|
||||||
|
spawnRequested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancel a pending spawn request (e.g., on state exit).
|
||||||
|
/// </summary>
|
||||||
|
public void CancelSpawnRequest()
|
||||||
|
{
|
||||||
|
spawnRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Animation event hook. Call from the summon animation.
|
||||||
|
/// </summary>
|
||||||
|
public void OnSummonAnimationEvent()
|
||||||
|
{
|
||||||
|
if (!spawnRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartSpawning();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stop spawning minions immediately
|
/// Stop spawning minions immediately
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -282,14 +363,102 @@ namespace DemonBoss.Summoner
|
|||||||
return GetDistanceToPlayer() <= meleeEngageDistance;
|
return GetDistanceToPlayer() <= meleeEngageDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if player is within the desired safe distance
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPlayerTooCloseForSafeDistance()
|
||||||
|
{
|
||||||
|
return GetDistanceToPlayer() < safeDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if summoner has reached/maintains safe distance
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAtSafeDistance()
|
||||||
|
{
|
||||||
|
return GetDistanceToPlayer() >= safeDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should summoner try to flee to reach safe distance
|
||||||
|
/// </summary>
|
||||||
|
public bool ShouldFleeToSafeDistance()
|
||||||
|
{
|
||||||
|
float distance = GetDistanceToPlayer();
|
||||||
|
if (distance <= meleeEngageDistance) return false;
|
||||||
|
if (CanSpawnMinions || CanCastSpell()) return false;
|
||||||
|
return distance < safeDistance;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if summoner should engage in melee combat
|
/// Check if summoner should engage in melee combat
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ShouldEngageMelee()
|
public bool ShouldEngageMelee()
|
||||||
{
|
{
|
||||||
if (!IsPlayerInMeleeRange()) return false;
|
if (!IsPlayerInMeleeRange()) return false;
|
||||||
if (fightWithMinions) return true;
|
if (!CanMeleeNow()) return false;
|
||||||
return !HasActiveMinions;
|
return !CanSpawnMinions && !CanCastSpell();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanMeleeNow()
|
||||||
|
{
|
||||||
|
return Time.time >= nextMeleeTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyMeleeAttack()
|
||||||
|
{
|
||||||
|
float min = Mathf.Min(meleeCooldownMin, meleeCooldownMax);
|
||||||
|
float max = Mathf.Max(meleeCooldownMin, meleeCooldownMax);
|
||||||
|
if (max <= 0f)
|
||||||
|
{
|
||||||
|
nextMeleeTime = 0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextMeleeTime = Time.time + Random.Range(min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if summoner can cast a ranged spell now
|
||||||
|
/// </summary>
|
||||||
|
public bool CanCastSpell()
|
||||||
|
{
|
||||||
|
if (isSpawning) return false;
|
||||||
|
if (fireballPrefab == null) return false;
|
||||||
|
if (spellRequiresMinionsAlive && !HasActiveMinions) return false;
|
||||||
|
|
||||||
|
float distance = GetDistanceToPlayer();
|
||||||
|
if (distance < spellMinDistance) return false;
|
||||||
|
if (spellMaxDistance > 0f && distance > spellMaxDistance) return false;
|
||||||
|
|
||||||
|
return (Time.time - lastSpellTime) >= spellCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Notify that a spell was cast (updates cooldown timer)
|
||||||
|
/// </summary>
|
||||||
|
public void NotifySpellCast()
|
||||||
|
{
|
||||||
|
lastSpellTime = Time.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawn a fireball from pivot towards player
|
||||||
|
/// </summary>
|
||||||
|
public void CastFireball()
|
||||||
|
{
|
||||||
|
if (!CanCastSpell()) return;
|
||||||
|
|
||||||
|
Transform pivot = spellSpawnPivot != null ? spellSpawnPivot : transform;
|
||||||
|
Vector3 targetPos = playerTransform != null ? playerTransform.position + Vector3.up * 1f : (pivot.position + pivot.forward);
|
||||||
|
Vector3 dir = (targetPos - pivot.position).normalized;
|
||||||
|
if (dir == Vector3.zero) dir = pivot.forward;
|
||||||
|
|
||||||
|
Quaternion rot = Quaternion.LookRotation(dir);
|
||||||
|
LeanPool.Spawn(fireballPrefab, pivot.position, rot);
|
||||||
|
|
||||||
|
NotifySpellCast();
|
||||||
|
|
||||||
|
if (enableDebug) Debug.Log("[SummonerAI] Cast fireball");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -327,6 +496,10 @@ namespace DemonBoss.Summoner
|
|||||||
Gizmos.color = Color.red;
|
Gizmos.color = Color.red;
|
||||||
DrawCircle(transform.position, meleeEngageDistance, 16);
|
DrawCircle(transform.position, meleeEngageDistance, 16);
|
||||||
|
|
||||||
|
// Draw safe distance
|
||||||
|
Gizmos.color = Color.yellow;
|
||||||
|
DrawCircle(transform.position, safeDistance, 24);
|
||||||
|
|
||||||
// Draw lines to active minions
|
// Draw lines to active minions
|
||||||
Gizmos.color = Color.green;
|
Gizmos.color = Color.green;
|
||||||
foreach (GameObject minion in activeMinions)
|
foreach (GameObject minion in activeMinions)
|
||||||
@@ -356,4 +529,4 @@ namespace DemonBoss.Summoner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ ModelImporter:
|
|||||||
animationDoRetargetingWarnings: 0
|
animationDoRetargetingWarnings: 0
|
||||||
importAnimatedCustomProperties: 0
|
importAnimatedCustomProperties: 0
|
||||||
importConstraints: 0
|
importConstraints: 0
|
||||||
animationCompression: 1
|
animationCompression: 3
|
||||||
animationRotationError: 0.5
|
animationRotationError: 0.5
|
||||||
animationPositionError: 0.5
|
animationPositionError: 0.5
|
||||||
animationScaleError: 0.5
|
animationScaleError: 0.5
|
||||||
@@ -33,7 +33,7 @@ ModelImporter:
|
|||||||
isReadable: 0
|
isReadable: 0
|
||||||
meshes:
|
meshes:
|
||||||
lODScreenPercentages: []
|
lODScreenPercentages: []
|
||||||
globalScale: 1
|
globalScale: 3
|
||||||
meshCompression: 0
|
meshCompression: 0
|
||||||
addColliders: 0
|
addColliders: 0
|
||||||
useSRGBMaterialColor: 1
|
useSRGBMaterialColor: 1
|
||||||
@@ -88,16 +88,16 @@ ModelImporter:
|
|||||||
armStretch: 0.05
|
armStretch: 0.05
|
||||||
legStretch: 0.05
|
legStretch: 0.05
|
||||||
feetSpacing: 0
|
feetSpacing: 0
|
||||||
globalScale: 1
|
globalScale: 3
|
||||||
rootMotionBoneName:
|
rootMotionBoneName:
|
||||||
hasTranslationDoF: 0
|
hasTranslationDoF: 0
|
||||||
hasExtraRoot: 0
|
hasExtraRoot: 1
|
||||||
skeletonHasParents: 1
|
skeletonHasParents: 1
|
||||||
lastHumanDescriptionAvatarSource: {instanceID: 0}
|
lastHumanDescriptionAvatarSource: {instanceID: 0}
|
||||||
autoGenerateAvatarMappingIfUnspecified: 1
|
autoGenerateAvatarMappingIfUnspecified: 1
|
||||||
animationType: 2
|
animationType: 3
|
||||||
humanoidOversampling: 1
|
humanoidOversampling: 1
|
||||||
avatarSetup: 0
|
avatarSetup: 1
|
||||||
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
|
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
|
||||||
importBlendShapeDeformPercent: 1
|
importBlendShapeDeformPercent: 1
|
||||||
remapMaterialsIfMaterialImportModeIsNone: 0
|
remapMaterialsIfMaterialImportModeIsNone: 0
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
37167
Assets/ThirdParty/Invector-3rdPersonController/Melee Combat/Animator/Invector@Summoner.controller
vendored
Normal file
37167
Assets/ThirdParty/Invector-3rdPersonController/Melee Combat/Animator/Invector@Summoner.controller
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 49dfe01444f3aeb409a3e3ecc971322c
|
guid: 729b0e7341e674748acfbe93bbe04fec
|
||||||
timeCreated: 1511197476
|
|
||||||
licenseType: Store
|
|
||||||
NativeFormatImporter:
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 9100000
|
||||||
userData:
|
userData:
|
||||||
assetBundleName:
|
assetBundleName:
|
||||||
assetBundleVariant:
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user