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 Lean.Pool;
|
||||
using UnityEngine;
|
||||
@@ -331,29 +332,5 @@ namespace DemonBoss.Magic
|
||||
|
||||
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)")]
|
||||
public float maxDistanceToPlayer = 0f;
|
||||
|
||||
[Tooltip("Require summoner to be at or beyond safe distance before summoning")]
|
||||
public bool requireSafeDistance = false;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
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
|
||||
if (checkCooldown)
|
||||
{
|
||||
@@ -102,4 +111,4 @@ namespace DemonBoss.Summoner
|
||||
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")]
|
||||
public bool attackWithMinions = false;
|
||||
|
||||
[Tooltip("Force melee when player is too close, even if minions are alive")]
|
||||
public bool forceMeleeWhenTooClose = true;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
@@ -44,6 +47,25 @@ namespace DemonBoss.Summoner
|
||||
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
|
||||
if (!attackWithMinions && summoner.HasActiveMinions)
|
||||
{
|
||||
@@ -51,11 +73,6 @@ namespace DemonBoss.Summoner
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check distance to player
|
||||
float distance = summoner.GetDistanceToPlayer();
|
||||
|
||||
bool inRange = distance >= minMeleeDistance && distance <= maxMeleeDistance;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
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")]
|
||||
[Tooltip("Wait for spawning to complete before allowing state exit")]
|
||||
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")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
@@ -31,6 +37,8 @@ namespace DemonBoss.Summoner
|
||||
private SummonerAI summoner;
|
||||
private Animator animator;
|
||||
private bool hasStartedSpawning = false;
|
||||
private float spawnRequestTime = -1f;
|
||||
private bool spawnTriggered = false;
|
||||
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
@@ -73,15 +81,40 @@ namespace DemonBoss.Summoner
|
||||
}
|
||||
}
|
||||
|
||||
// Start spawning minions
|
||||
summoner.StartSpawning();
|
||||
hasStartedSpawning = true;
|
||||
|
||||
if (enableDebug) Debug.Log("[SA_SpawnMinions] Started spawning minions");
|
||||
if (spawnOnAnimationEvent)
|
||||
{
|
||||
summoner.RequestSpawn();
|
||||
hasStartedSpawning = true;
|
||||
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)
|
||||
{
|
||||
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 (waitForCompletion && summoner != null && summoner.IsSpawning)
|
||||
{
|
||||
@@ -107,8 +140,14 @@ namespace DemonBoss.Summoner
|
||||
summoner.StopSpawning();
|
||||
if (enableDebug) Debug.Log("[SA_SpawnMinions] Spawning interrupted on state exit");
|
||||
}
|
||||
else if (summoner != null)
|
||||
{
|
||||
summoner.CancelSpawnRequest();
|
||||
}
|
||||
|
||||
hasStartedSpawning = false;
|
||||
spawnTriggered = false;
|
||||
spawnRequestTime = -1f;
|
||||
|
||||
if (enableDebug) Debug.Log("[SA_SpawnMinions] State exited");
|
||||
}
|
||||
@@ -122,4 +161,4 @@ namespace DemonBoss.Summoner
|
||||
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
|
||||
guid: 8faaed5f0f8beac4193201e791fa0406
|
||||
guid: 1ff92c6825e7585419de5a79794bc3f2
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
@@ -2,6 +2,7 @@ using Invector;
|
||||
using Invector.vCharacterController.AI;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Summoner
|
||||
@@ -43,6 +44,16 @@ namespace DemonBoss.Summoner
|
||||
[Tooltip("Minimum distance to player before engaging in melee")]
|
||||
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?")]
|
||||
public bool fightWithMinions = false;
|
||||
|
||||
@@ -61,6 +72,29 @@ namespace DemonBoss.Summoner
|
||||
[Tooltip("Sound played when spawning minions")]
|
||||
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")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
@@ -76,12 +110,23 @@ namespace DemonBoss.Summoner
|
||||
private vHealthController healthController;
|
||||
private bool isSpawning = false;
|
||||
private Coroutine spawnCoroutine;
|
||||
private float lastSpellTime = -999f;
|
||||
private bool spawnRequested = false;
|
||||
private float nextMeleeTime = 0f;
|
||||
|
||||
// Public properties for FSM decisions
|
||||
public bool IsSpawning => isSpawning;
|
||||
|
||||
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;
|
||||
|
||||
private void Awake()
|
||||
@@ -125,6 +170,7 @@ namespace DemonBoss.Summoner
|
||||
/// </summary>
|
||||
public void StartSpawning()
|
||||
{
|
||||
spawnRequested = false;
|
||||
if (isSpawning)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SummonerAI] Already spawning minions");
|
||||
@@ -137,9 +183,44 @@ namespace DemonBoss.Summoner
|
||||
return;
|
||||
}
|
||||
|
||||
if (requireZeroMinionsToSummon && HasActiveMinions)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SummonerAI] Minions alive - summon blocked");
|
||||
return;
|
||||
}
|
||||
|
||||
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>
|
||||
/// Stop spawning minions immediately
|
||||
/// </summary>
|
||||
@@ -282,14 +363,102 @@ namespace DemonBoss.Summoner
|
||||
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>
|
||||
/// Check if summoner should engage in melee combat
|
||||
/// </summary>
|
||||
public bool ShouldEngageMelee()
|
||||
{
|
||||
if (!IsPlayerInMeleeRange()) return false;
|
||||
if (fightWithMinions) return true;
|
||||
return !HasActiveMinions;
|
||||
if (!CanMeleeNow()) return false;
|
||||
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>
|
||||
@@ -327,6 +496,10 @@ namespace DemonBoss.Summoner
|
||||
Gizmos.color = Color.red;
|
||||
DrawCircle(transform.position, meleeEngageDistance, 16);
|
||||
|
||||
// Draw safe distance
|
||||
Gizmos.color = Color.yellow;
|
||||
DrawCircle(transform.position, safeDistance, 24);
|
||||
|
||||
// Draw lines to active minions
|
||||
Gizmos.color = Color.green;
|
||||
foreach (GameObject minion in activeMinions)
|
||||
@@ -356,4 +529,4 @@ namespace DemonBoss.Summoner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ ModelImporter:
|
||||
animationDoRetargetingWarnings: 0
|
||||
importAnimatedCustomProperties: 0
|
||||
importConstraints: 0
|
||||
animationCompression: 1
|
||||
animationCompression: 3
|
||||
animationRotationError: 0.5
|
||||
animationPositionError: 0.5
|
||||
animationScaleError: 0.5
|
||||
@@ -33,7 +33,7 @@ ModelImporter:
|
||||
isReadable: 0
|
||||
meshes:
|
||||
lODScreenPercentages: []
|
||||
globalScale: 1
|
||||
globalScale: 3
|
||||
meshCompression: 0
|
||||
addColliders: 0
|
||||
useSRGBMaterialColor: 1
|
||||
@@ -88,16 +88,16 @@ ModelImporter:
|
||||
armStretch: 0.05
|
||||
legStretch: 0.05
|
||||
feetSpacing: 0
|
||||
globalScale: 1
|
||||
globalScale: 3
|
||||
rootMotionBoneName:
|
||||
hasTranslationDoF: 0
|
||||
hasExtraRoot: 0
|
||||
hasExtraRoot: 1
|
||||
skeletonHasParents: 1
|
||||
lastHumanDescriptionAvatarSource: {instanceID: 0}
|
||||
autoGenerateAvatarMappingIfUnspecified: 1
|
||||
animationType: 2
|
||||
animationType: 3
|
||||
humanoidOversampling: 1
|
||||
avatarSetup: 0
|
||||
avatarSetup: 1
|
||||
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
|
||||
importBlendShapeDeformPercent: 1
|
||||
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
|
||||
guid: 49dfe01444f3aeb409a3e3ecc971322c
|
||||
timeCreated: 1511197476
|
||||
licenseType: Store
|
||||
guid: 729b0e7341e674748acfbe93bbe04fec
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 9100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user